From 060a850f7472f8863284e66a48eb844bcaee7b4d Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 22 Sep 2026 18:44:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AGC=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=91=98=E8=A6=81=E6=B3=84=E6=BC=8F=E6=8F=90=E4=BA=A4=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自动摘要改为读取提交消息首行,仅展示标题并忽略后续说明 更新摘要不再附带短 SHA 补充多行提交消息与摘要格式回归测试 同步客户端更新检查与开发运维文档 --- .../scripts/build-release.mjs | 70 ++++++++++++------- .../scripts/build-release.test.mjs | 30 ++++++-- ...方案】AGC客户端更新检查与下载-2026-08-31.md | 2 +- ...发运维】本地开发验证与生产运维-2026-05-15.md | 2 +- 4 files changed, 71 insertions(+), 33 deletions(-) diff --git a/apps/ai-game-creator-shell/scripts/build-release.mjs b/apps/ai-game-creator-shell/scripts/build-release.mjs index bd1d5907b..742e4f8d1 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.mjs @@ -648,6 +648,32 @@ function readHeadCommit() { } } +/** + * Git 的 %s 会把「标题后紧接说明行、没有空行」的整个首段拼成一行; + * 更新摘要只允许展示原始提交消息的第一行,避免把说明暴露给用户。 + */ +function commitMessageTitle(message) { + return String(message ?? '') + .split(/\r?\n/u, 1)[0] + .trim(); +} + +/** 解析 `git log -z --format=%h%x09%B`,只保留每个 commit 的消息首行。 */ +function parseReleaseCommitLog(output) { + return output + .split('\0') + .map((record) => record.trimEnd()) + .filter(Boolean) + .map((record) => { + const separator = record.indexOf('\t'); + if (separator < 0) return null; + const sha = record.slice(0, separator); + const subject = commitMessageTitle(record.slice(separator + 1)); + return sha && subject ? { sha, subject } : null; + }) + .filter(Boolean); +} + /** * 上一次发布到本次之间的客户端相关提交。 * @@ -675,8 +701,9 @@ export function collectReleaseCommits( 'git', [ 'log', + '-z', '--no-merges', - '--format=%h%x09%s', + '--format=%h%x09%B', `${previousCommit}..${headCommit}`, '--', ...paths, @@ -686,28 +713,22 @@ export function collectReleaseCommits( } catch { return null; } - return output - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const [sha = '', ...subject] = line.split('\t'); - return { sha, subject: subject.join('\t') }; - }); + return parseReleaseCommitLog(output); } -/** 自动更新摘要:逐条列客户端相关改动,超过上限时折叠并整体截断。 */ +/** 自动更新摘要:逐条列客户端相关改动标题,超过上限时折叠并整体截断。 */ export function formatReleaseNotes( commits, { limit = 12, subjectLength = 80, maxLength = 900 } = {}, ) { if (!commits || commits.length === 0) return ''; - const lines = commits.slice(0, limit).map(({ sha, subject }) => { + const lines = commits.slice(0, limit).map(({ subject }) => { + const title = commitMessageTitle(subject); const trimmed = - subject.length > subjectLength - ? `${subject.slice(0, subjectLength - 1)}…` - : subject; - return `- ${trimmed}(${sha})`; + title.length > subjectLength + ? `${title.slice(0, subjectLength - 1)}…` + : title; + return `- ${trimmed}`; }); if (commits.length > limit) { lines.push(`- 其余 ${commits.length - limit} 项客户端改动省略`); @@ -726,20 +747,21 @@ export function collectRecentReleaseCommits({ try { output = execFileSync( 'git', - ['log', '--no-merges', `-n${limit}`, '--format=%h%x09%s', '--', ...paths], + [ + 'log', + '-z', + '--no-merges', + `-n${limit}`, + '--format=%h%x09%B', + '--', + ...paths, + ], { cwd, encoding: 'utf8' }, ); } catch { return null; } - const commits = output - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - const [sha = '', ...subject] = line.split('\t'); - return { sha, subject: subject.join('\t') }; - }); + const commits = parseReleaseCommitLog(output); return commits.length > 0 ? commits : null; } diff --git a/apps/ai-game-creator-shell/scripts/build-release.test.mjs b/apps/ai-game-creator-shell/scripts/build-release.test.mjs index b159be8a2..427166456 100644 --- a/apps/ai-game-creator-shell/scripts/build-release.test.mjs +++ b/apps/ai-game-creator-shell/scripts/build-release.test.mjs @@ -1077,20 +1077,17 @@ test('release entry forwards the built artifacts and dry-run mode to the uploade assert.ok(source.includes('\n dryRun,\n')); }); -test('release notes list client commits with short sha and bound their size', () => { +test('release notes list client commit subjects only and bound their size', () => { const notes = formatReleaseNotes([ { sha: 'a5fd25f1', subject: '客户端更新切换到官方更新插件' }, { sha: '55af6014', subject: '修'.repeat(120) }, ]); const lines = notes.split('\n'); assert.equal(lines.length, 2); - assert.match(lines[0], /^- 客户端更新切换到官方更新插件(a5fd25f1)$/u); - const truncatedSubject = lines[1] - .replace(/^- /u, '') - .replace(/(55af6014)$/u, ''); + assert.equal(lines[0], '- 客户端更新切换到官方更新插件'); + const truncatedSubject = lines[1].replace(/^- /u, ''); assert.equal(truncatedSubject.length, 80, `主题应截断到 80 字:${lines[1]}`); assert.match(truncatedSubject, /…$/u); - assert.match(lines[1], /(55af6014)$/u); const many = formatReleaseNotes( Array.from({ length: 20 }, (_, index) => ({ @@ -1099,6 +1096,15 @@ test('release notes list client commits with short sha and bound their size', () })), ); assert.match(many, /- 其余 8 项客户端改动省略$/u); + assert.equal( + formatReleaseNotes([ + { + sha: 'ignored', + subject: '提交标题\n不应展示的说明一\n不应展示的说明二', + }, + ]), + '- 提交标题', + ); assert.equal(formatReleaseNotes([]), ''); assert.equal(formatReleaseNotes(null), ''); }); @@ -1122,8 +1128,17 @@ test('release commits cover only client paths and skip merge commits', () => { path.join(directory, 'apps/ai-game-creator-shell/main.rs'), 'fn main() {}\n', ); + const commitMessagePath = path.join( + directory, + '.git', + 'commit-message.txt', + ); + writeFileSync( + commitMessagePath, + '客户端:新增更新插件接入\n补充更新插件接入的详细说明\n', + ); git('add', '.'); - git('commit', '--quiet', '-m', '客户端:新增更新插件接入'); + git('commit', '--quiet', '-F', commitMessagePath); writeFileSync(path.join(directory, 'docs/readme.md'), '# 文档\n'); git('add', '.'); @@ -1149,6 +1164,7 @@ test('release commits cover only client paths and skip merge commits', () => { const commits = collectReleaseCommits(base, 'HEAD', { cwd: directory }); assert.ok(commits, '应能在临时仓库里收集提交'); const subjects = commits.map((entry) => entry.subject); + // 标题后没有空行时,git %s 会把说明行拼进标题;摘要必须取原始首行。 // 合并提交本身被 --no-merges 排除,但它带入的客户端改动仍然计入。 assert.deepEqual(subjects, [ '客户端:侧分支改动', diff --git a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md index d048ec805..7e5f202f8 100644 --- a/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md +++ b/docs/technical/【技术方案】AGC客户端更新检查与下载-2026-08-31.md @@ -138,7 +138,7 @@ - 渠道由 `AGC_UPDATE_CHANNEL` 显式指定,默认 dev;Windows 与 macOS 目标均支持 dev、release 和自定义渠道,目标校验独立进行。 - 渠道 `--config` 在 Tauri 构建前最后合并,同时注入 `productName`、`identifier` 与 updater 端点:安装身份与更新端点必须来自同一个渠道,不能各自回读默认值。macOS 发布入口构建 `*.app`、updater 归档与 DMG 前先按发布渠道解析产品名,产物名一律派生而不写死。 - 定时调度分别判断服务端与客户端 scope:dev 小时调度在提交含 AGC 相关路径时发布对应渠道,纯文档或流水线自身的提交仍只跑 Full Build;release 每日调度在服务端相关路径变化时发布正式 Full Build,在 AGC 相关路径变化时发布 release 客户端,并在同一调度内等待、汇总各 lane 结果,失败 lane 下一轮补发。判定失败或勾选强制触发时按"需要发布"处理。 -- 更新摘要自动生成:发布脚本用渠道清单里的 `commit` 字段(上一次发布的提交)到本次提交之间、且只覆盖客户端相关路径的提交列表生成 `notes`(每条 `- 提交标题(短 SHA)`,最多 12 条、主题 80 字、整体 900 字,超出折叠或截断),同时写入旧协议清单的 `releaseNotes` 和归档文件 `release-notes.txt`。`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准;无法判定起点(缺少上次 `commit` 或本地没有该提交)时不写摘要。清单缺少 `commit` 时回退用上一次成功构建的 `COMMIT_HASH`(CI 通过 `AGC_UPDATE_PREVIOUS_COMMIT` 传入)作为锚点,因此首次启用摘要或更换渠道后也能立即产出摘要。锚点仍不可得(清单读取失败或没有 CI 锚点)时降级为「最近客户端改动」列表并注明可能与上一版重复 —— 摘要属于附注,任何情况下都不允许因为它让发布失败。 +- 更新摘要自动生成:发布脚本用渠道清单里的 `commit` 字段(上一次发布的提交)到本次提交之间、且只覆盖客户端相关路径的提交列表生成 `notes`(每条 `- 提交标题`,标题只取 commit message 第一行并忽略后续说明行;最多 12 条、主题 80 字、整体 900 字,超出折叠或截断),同时写入旧协议清单的 `releaseNotes` 和归档文件 `release-notes.txt`。`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准;无法判定起点(缺少上次 `commit` 或本地没有该提交)时不写摘要。清单缺少 `commit` 时回退用上一次成功构建的 `COMMIT_HASH`(CI 通过 `AGC_UPDATE_PREVIOUS_COMMIT` 传入)作为锚点,因此首次启用摘要或更换渠道后也能立即产出摘要。锚点仍不可得(清单读取失败或没有 CI 锚点)时降级为「最近客户端改动」列表并注明可能与上一版重复 —— 摘要属于附注,任何情况下都不允许因为它让发布失败。 - 清单里的 `commit` 是非标准字段:更新插件忽略未知字段,发布脚本用它定位下一次摘要的起点。 - 上传:安装包与 `.sig` 上传到 `agc/-win|mac//`,清单以 `--force` 覆盖上传到对应分区的 `latest.json`,保证 latest 指针与清单内 URL 指向已存在的对象。 - 首装发布:发布脚本生成 `downloads`,Windows 复用已选 NSIS `.exe`,Mac 选择本次版本和目标架构匹配的非空 `.dmg`;缺失、歧义或版本/架构不匹配时失败,不发布带悬空地址的清单。上传顺序为更新包、签名及首装包全部成功后再更新渠道清单,Windows 相同对象只上传一次。`dry-run` 不写 OSS。各渠道独立写自己的清单,由 BFF 汇总,Windows 与 Mac 发布不会覆盖彼此的下载项;Mac 跨架构合并仍遵循现有单架构发布约束。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index e57caadba..3aa30b5be 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -143,7 +143,7 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去 `Genarrative-Scheduled-Revision-Trigger` 是 dev 渠道的小时调度入口,每小时检查一次(`H * * * *`,分钟由 Jenkins 按 Job 名散列,不等同于整点)。`Genarrative-Scheduled-Release-Trigger` 是 release 渠道服务端与客户端的每日调度入口,固定在每天 04:00(`0 4 * * *`)检查同一个源码分支。两条调度器都只用 `git ls-remote` 解析 `SOURCE_BRANCH`(默认 `master`)的远端 HEAD,不 checkout 工作区;解析出的完整 commit 与对应 lane 上一次成功的 revision 相同则标记 `NOT_BUILT` 并结束,不触发任何下游。 -dev 调度器发现 revision 变化时,把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy`、`Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,各条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。这些下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`(两个平台分区发布同一个发号 Job 下发的总版本号),纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。每日 release 调度器按服务端/客户端两个独立 scope 决定下游,固定使用 `DEPLOY_TARGET=release` 和 `CONFIRM_RELEASE_DEPLOY_AGENT=true`:服务端相关路径变化时触发 Full Build,`DATABASE_BACKUP_MODE` 默认 `async`,Stdb/API rollout 模式与审批人由调度参数显式透传;客户端相关路径变化时先经发号 Job 获取统一总号,再以同一个固定 commit 触发 AGC Windows 与 macOS 的 `release` 分区构建,macOS 仍带 `SKIP_IF_SUPERSEDED=true`。三类下游均 `wait: true` 并汇总结果,只有成功 lane 才推进 `.jenkins-last-release-full-revision` 或 `.jenkins-last-release-agc-revision`,失败 lane 下一轮单独补发,不重复已成功的服务端发布。dev 调度器的下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/`、`.codex/`、`jenkins/`、`apps/ai-game-creator-shell/`、`apps/mobile-shell/`、`apps/desktop-shell/`、`apps/preview-deployer-web/`、`tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。AGC 客户端版本号不再由渠道各自递增:唯一发号源是 OSS 对象 `agc/global-version.json`,发号收口到 `Genarrative-Agc-Global-Version-Issue`(`disableConcurrentBuilds()` + 写后回读校验;集群未装 `lockable-resources` 插件)。dev / release 两条调度管线与手动发布管线都先调用该 Job 发号,再用归档产物 `agc-global-version.txt` 读取总号并作为 `AGC_RELEASE_VERSION` 透传给客户端构建;统一构建只发一次号供各渠道共用,单渠道热修只把号传给该渠道。显式传入的号低于本渠道当前清单版本时构建失败关闭;`AGC_RELEASE_DRY_RUN` 只预览下一位,不写回、不烧号。一次性播种用 `SEED_ONLY`:基线取「仓库版本 / 各渠道清单 / 旧迁移指针」的最大值,播种本身不烧号。仓库里的 5 个版本文件仍由构建改写,只作构建输入参考,不是事实源。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;release 调度显式覆盖为 `release` 目标并透传备份、rollout、Pingora 和维护退出参数。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build` 结束后都会触发 `Genarrative-Notify-Email`;只有正式 OSS 发布成功时,通知正文才附加本次生成的 `latest.json` 首装包 URL,演练、失败或跳过构建时留空,避免把未写入或不完整的对象地址发给收件人。 +dev 调度器发现 revision 变化时,把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy`、`Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,各条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。这些下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`(两个平台分区发布同一个发号 Job 下发的总版本号),纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。每日 release 调度器按服务端/客户端两个独立 scope 决定下游,固定使用 `DEPLOY_TARGET=release` 和 `CONFIRM_RELEASE_DEPLOY_AGENT=true`:服务端相关路径变化时触发 Full Build,`DATABASE_BACKUP_MODE` 默认 `async`,Stdb/API rollout 模式与审批人由调度参数显式透传;客户端相关路径变化时先经发号 Job 获取统一总号,再以同一个固定 commit 触发 AGC Windows 与 macOS 的 `release` 分区构建,macOS 仍带 `SKIP_IF_SUPERSEDED=true`。三类下游均 `wait: true` 并汇总结果,只有成功 lane 才推进 `.jenkins-last-release-full-revision` 或 `.jenkins-last-release-agc-revision`,失败 lane 下一轮单独补发,不重复已成功的服务端发布。dev 调度器的下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/`、`.codex/`、`jenkins/`、`apps/ai-game-creator-shell/`、`apps/mobile-shell/`、`apps/desktop-shell/`、`apps/preview-deployer-web/`、`tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。AGC 客户端版本号不再由渠道各自递增:唯一发号源是 OSS 对象 `agc/global-version.json`,发号收口到 `Genarrative-Agc-Global-Version-Issue`(`disableConcurrentBuilds()` + 写后回读校验;集群未装 `lockable-resources` 插件)。dev / release 两条调度管线与手动发布管线都先调用该 Job 发号,再用归档产物 `agc-global-version.txt` 读取总号并作为 `AGC_RELEASE_VERSION` 透传给客户端构建;统一构建只发一次号供各渠道共用,单渠道热修只把号传给该渠道。显式传入的号低于本渠道当前清单版本时构建失败关闭;`AGC_RELEASE_DRY_RUN` 只预览下一位,不写回、不烧号。一次性播种用 `SEED_ONLY`:基线取「仓库版本 / 各渠道清单 / 旧迁移指针」的最大值,播种本身不烧号。仓库里的 5 个版本文件仍由构建改写,只作构建输入参考,不是事实源。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交消息第一行(标题)逐条写进 `notes`,忽略标题后的说明行(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;release 调度显式覆盖为 `release` 目标并透传备份、rollout、Pingora 和维护退出参数。三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build` 结束后都会触发 `Genarrative-Notify-Email`;只有正式 OSS 发布成功时,通知正文才附加本次生成的 `latest.json` 首装包 URL,演练、失败或跳过构建时留空,避免把未写入或不完整的对象地址发给收件人。 手工发布入口 `Genarrative-Manual-Build-And-Deploy` 的 `DEPLOY_TARGET` 与 AGC 更新渠道是两个独立维度。手工入口必须把 `release` 映射为 `AGC_UPDATE_CHANNEL=release`、把 `development` 映射为 `dev`,并把该参数同时透传给 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`;只传 `AGC_RELEASE_VERSION` 时下游会落回各自默认 `dev`。统一号 `0.1.95` 的 release 包应分别位于 `agc/release-win/0.1.95/` 与 `agc/release-mac/0.1.95/`,渠道清单为对应目录下的 `latest.json`,不存在 `agc/release/` 这一层。补发本轮已烧号的版本时,直接以相同 `AGC_RELEASE_VERSION` 重跑两条 AGC Job,不重新发号。