mirror of
https://mirror.skon.top/github.com/ILoveBingLu/CipherTalk
synced 2026-04-30 13:51:50 +08:00
chore: improve release CI context and local secret env for release scripts
Made-with: Cursor
This commit is contained in:
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -30,6 +30,9 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -17,6 +17,30 @@ git push origin v2.2.14
|
||||
|
||||
只有推送 `v*` 标签时,GitHub Actions 才会自动构建和发布。
|
||||
|
||||
## 本地测试(不提交密钥)
|
||||
|
||||
发布相关脚本支持从本地私有环境文件读取密钥与模型配置,读取顺序为:
|
||||
|
||||
1. 进程环境变量(例如手动 `set` / CI 注入)
|
||||
2. 仓库根目录 `.release.local.env`
|
||||
3. 仓库根目录 `.env.local`
|
||||
|
||||
可用键(按需填写):
|
||||
|
||||
- `AI_API_KEY`
|
||||
- `AI_API_URL`
|
||||
- `AI_MODEL`
|
||||
- `GH_TOKEN`
|
||||
|
||||
示例(文件不会被提交):
|
||||
|
||||
```env
|
||||
AI_API_KEY=sk-xxxx
|
||||
AI_API_URL=https://api.openai.com/v1/chat/completions
|
||||
AI_MODEL=gpt-5.4
|
||||
GH_TOKEN=ghp_xxxx
|
||||
```
|
||||
|
||||
## GitHub Actions 会做什么
|
||||
|
||||
`.github/workflows/release.yml` 会在 `v*` 标签触发后执行:
|
||||
|
||||
@@ -5,9 +5,51 @@ const rootDir = path.resolve(__dirname, '..')
|
||||
const releaseDir = path.join(rootDir, 'release')
|
||||
const contextPath = path.join(releaseDir, 'release-context.json')
|
||||
const outputPath = path.join(releaseDir, 'release-body.md')
|
||||
const aiApiKey = process.env.AI_API_KEY || ''
|
||||
const aiApiUrl = process.env.AI_API_URL || 'https://api.openai.com/v1/chat/completions'
|
||||
const aiModel = process.env.AI_MODEL || 'gpt-5.4'
|
||||
|
||||
function parseEnvText(content) {
|
||||
const result = {}
|
||||
for (const line of String(content || '').split(/\r?\n/)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
const eqIndex = trimmed.indexOf('=')
|
||||
if (eqIndex <= 0) continue
|
||||
const key = trimmed.slice(0, eqIndex).trim()
|
||||
let value = trimmed.slice(eqIndex + 1).trim()
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function loadLocalSecretEnv() {
|
||||
const candidates = [
|
||||
path.join(rootDir, '.release.local.env'),
|
||||
path.join(rootDir, '.env.local')
|
||||
]
|
||||
|
||||
const merged = {}
|
||||
for (const filePath of candidates) {
|
||||
if (!fs.existsSync(filePath)) continue
|
||||
try {
|
||||
const parsed = parseEnvText(fs.readFileSync(filePath, 'utf8'))
|
||||
Object.assign(merged, parsed)
|
||||
console.log(`[ReleaseBody] Loaded local env file: ${path.basename(filePath)}`)
|
||||
} catch (e) {
|
||||
console.warn(`[ReleaseBody] Failed to read local env file: ${filePath}`, String(e))
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
const localSecrets = loadLocalSecretEnv()
|
||||
const aiApiKey = process.env.AI_API_KEY || localSecrets.AI_API_KEY || ''
|
||||
const aiApiUrl = process.env.AI_API_URL || localSecrets.AI_API_URL || 'https://api.openai.com/v1/chat/completions'
|
||||
const aiModel = process.env.AI_MODEL || localSecrets.AI_MODEL || 'gpt-5.4'
|
||||
|
||||
const PRIMARY_AUTHOR_LOGINS = new Set(['ILoveBingLu'])
|
||||
const PRIMARY_AUTHOR_NAMES = new Set(['ILoveBingLu', 'BingLu', 'ILoveBinglu'])
|
||||
@@ -128,8 +170,8 @@ async function generateAiBody(context) {
|
||||
const systemPrompt = [
|
||||
'你是一个发布说明撰写助手。',
|
||||
'只能基于输入中的 commits 和 pull requests 生成,不得编造任何功能或修复。',
|
||||
'输出必须是中文 Markdown。',
|
||||
'必须包含以下章节:',
|
||||
'输出必须是中文 Markdown,风格尽量自然,不要机械复读。',
|
||||
'为保证格式一致性:优先使用以下标题结构(即使某一类内容为空也要写出对应章节,并在该章节内标注“无/未检测到”):',
|
||||
'## CipherTalk vX.Y.Z',
|
||||
'### 概览',
|
||||
'### 新增',
|
||||
@@ -138,9 +180,12 @@ async function generateAiBody(context) {
|
||||
'### 感谢贡献者',
|
||||
'### 相关提交与 PR',
|
||||
'如果存在最低安全版本或封禁版本,增加 ### 升级提醒 章节。',
|
||||
'分类建议:可参考提交标题前缀 feat/fix 做粗分类到 新增/修复;其余放到 调整(如果标题无法判断,就放到 调整)。',
|
||||
'引用规则:',
|
||||
'有 PR 时优先引用 PR 标题;没有 PR 时才引用 commit 标题。',
|
||||
'感谢规则:只有非主作者的 PR/commit 才出现在感谢段。',
|
||||
'不要写模糊词,不要写猜测,不要写未在输入中出现的功能。'
|
||||
'列表尽量短:最多每类列出 5 条最关键的标题;其余可在概览里用一句话说明总量。',
|
||||
'感谢规则:只有非主作者的 PR/commit 才出现在感谢段;主作者按代码中的逻辑是 ILoveBingLu(及其大小写/拼写变体)相关。',
|
||||
'不要写猜测:如果输入里没有足够信息,就用“无/未检测到”或“仅维护性发布”描述。'
|
||||
].join('\n')
|
||||
|
||||
const userPrompt = `请根据以下发布上下文为 ${context.tag} 生成标准化发布说明:\n\n${JSON.stringify(context, null, 2)}`
|
||||
|
||||
@@ -7,9 +7,50 @@ const releaseDir = path.join(rootDir, 'release')
|
||||
const owner = process.env.GITHUB_REPOSITORY_OWNER || 'ILoveBingLu'
|
||||
const repo = (process.env.GITHUB_REPOSITORY || `${owner}/CipherTalk`).split('/')[1] || 'CipherTalk'
|
||||
const currentTag = process.env.RELEASE_TAG || process.env.GITHUB_REF_NAME || ''
|
||||
const ghToken = process.env.GH_TOKEN || ''
|
||||
const pkg = require(path.join(rootDir, 'package.json'))
|
||||
|
||||
function parseEnvText(content) {
|
||||
const result = {}
|
||||
for (const line of String(content || '').split(/\r?\n/)) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
const eqIndex = trimmed.indexOf('=')
|
||||
if (eqIndex <= 0) continue
|
||||
const key = trimmed.slice(0, eqIndex).trim()
|
||||
let value = trimmed.slice(eqIndex + 1).trim()
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function loadLocalSecretEnv() {
|
||||
const candidates = [
|
||||
path.join(rootDir, '.release.local.env'),
|
||||
path.join(rootDir, '.env.local')
|
||||
]
|
||||
const merged = {}
|
||||
for (const filePath of candidates) {
|
||||
if (!fs.existsSync(filePath)) continue
|
||||
try {
|
||||
const parsed = parseEnvText(fs.readFileSync(filePath, 'utf8'))
|
||||
Object.assign(merged, parsed)
|
||||
console.log(`[ReleaseContext] Loaded local env file: ${path.basename(filePath)}`)
|
||||
} catch (e) {
|
||||
console.warn(`[ReleaseContext] Failed to read local env file: ${filePath}`, String(e))
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
const localSecrets = loadLocalSecretEnv()
|
||||
const ghToken = process.env.GH_TOKEN || localSecrets.GH_TOKEN || ''
|
||||
|
||||
function runGit(command) {
|
||||
return execSync(command, {
|
||||
cwd: rootDir,
|
||||
@@ -49,7 +90,10 @@ function getPreviousTag() {
|
||||
|
||||
function getCommitRange(previousTag, tag) {
|
||||
if (!tag) return 'HEAD'
|
||||
if (!previousTag || previousTag === tag) return tag
|
||||
// 如果上一标签拿不到(例如 checkout 浅克隆/无 tags),则退化成取 tag 前最近 50 次提交,
|
||||
// 避免 release-context 里 commits/pullRequests 为空,导致后续 AI 发布说明内容很少。
|
||||
if (!previousTag) return `${tag}~50..${tag}`
|
||||
if (previousTag === tag) return tag
|
||||
return `${previousTag}..${tag}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user