更新摘要降级为不阻断发布
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m22s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m20s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 6m42s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 6m1s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m4s
Project CI / Native shell tests (push) Failing after 1m29s
Project CI / AI game creator shell Rust crates (push) Successful in 2m15s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 6m22s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 6m20s
Project CI / AI game creator shell Rust shard 2/4 (push) Failing after 6m42s
Project CI / AI game creator shell Rust shard 4/4 (push) Failing after 6m1s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m4s
Project CI / Native shell tests (push) Failing after 1m29s
Project CI / AI game creator shell Rust crates (push) Successful in 2m15s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
- 摘要锚点读取失败时降级为无锚点并告警,不再让整个发布失败 - 无锚点时列出最近 8 条客户端相关提交,并注明可能与上一版重复 - 新增锚点读取失败降级与最近提交兜底的定向用例 - 技术方案补充摘要不阻断发布的约束
This commit is contained in:
@@ -211,10 +211,18 @@ export async function resolvePreviousReleaseCommit(
|
||||
if (explicit && /^[0-9a-f]{7,40}$/u.test(explicit)) {
|
||||
return explicit;
|
||||
}
|
||||
const manifest = await readRemoteChannelManifest(channel);
|
||||
const commit =
|
||||
typeof manifest?.commit === 'string' ? manifest.commit.trim() : '';
|
||||
return /^[0-9a-f]{7,40}$/u.test(commit) ? commit : null;
|
||||
try {
|
||||
const manifest = await readRemoteChannelManifest(channel);
|
||||
const commit =
|
||||
typeof manifest?.commit === 'string' ? manifest.commit.trim() : '';
|
||||
return /^[0-9a-f]{7,40}$/u.test(commit) ? commit : null;
|
||||
} catch (error) {
|
||||
// 摘要只是附注:清单读不到(网络抖动等)不能把发布带崩,降级为「没有锚点」。
|
||||
console.warn(
|
||||
`[ai-game-creator-shell] 读取摘要锚点失败,本次不写自动摘要:${error.message}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,6 +554,39 @@ export function formatReleaseNotes(
|
||||
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text;
|
||||
}
|
||||
|
||||
/** 无锚点时的兜底:列出最近的客户端相关提交,并注明可能与上一版重复。 */
|
||||
export function collectRecentReleaseCommits({
|
||||
cwd = repoRoot,
|
||||
paths = agcReleasePathPatterns,
|
||||
limit = 8,
|
||||
} = {}) {
|
||||
let output;
|
||||
try {
|
||||
output = execFileSync(
|
||||
'git',
|
||||
['log', '--no-merges', `-n${limit}`, '--format=%h%x09%s', '--', ...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') };
|
||||
});
|
||||
return commits.length > 0 ? commits : null;
|
||||
}
|
||||
|
||||
export function formatRecentReleaseNotes(commits) {
|
||||
const notes = formatReleaseNotes(commits, { limit: 8 });
|
||||
if (!notes) return '';
|
||||
return `最近客户端改动(未定位到上一次发布提交,可能与上一版重复):\n${notes}`;
|
||||
}
|
||||
|
||||
/** 旧协议(sha256)清单:只用于把已发布客户端带到新渠道协议,一个版本周期后整条删除。 */
|
||||
export function createLegacyUpdateManifest(
|
||||
artifactPath,
|
||||
@@ -572,10 +613,14 @@ export async function generateUpdateManifest() {
|
||||
const manualNotes = readReleaseNotes();
|
||||
const previousCommit = await resolvePreviousReleaseCommit(channel);
|
||||
const commits = collectReleaseCommits(previousCommit);
|
||||
const notes = manualNotes || formatReleaseNotes(commits);
|
||||
const recentCommits = previousCommit ? null : collectRecentReleaseCommits();
|
||||
const notes =
|
||||
manualNotes ||
|
||||
formatReleaseNotes(commits) ||
|
||||
formatRecentReleaseNotes(recentCommits);
|
||||
if (!manualNotes && !notes) {
|
||||
console.log(
|
||||
`[ai-game-creator-shell] 未生成自动更新摘要(上一发布 commit=${previousCommit ?? '未知'},客户端相关提交=${commits ? commits.length : '不可判定'})`,
|
||||
`[ai-game-creator-shell] 未生成自动更新摘要(上一发布 commit=${previousCommit ?? '未知'},客户端相关提交=${commits ? commits.length : '不可判定'},最近提交=${recentCommits ? recentCommits.length : '不可判定'})`,
|
||||
);
|
||||
}
|
||||
const manifest = createUpdateManifest(artifact, { channel, notes });
|
||||
@@ -606,7 +651,9 @@ export async function generateUpdateManifest() {
|
||||
console.log(
|
||||
manualNotes
|
||||
? '[ai-game-creator-shell] 更新摘要:使用 AGC_UPDATE_RELEASE_NOTES 手动文案'
|
||||
: `[ai-game-creator-shell] 更新摘要:自动汇总 ${commits ? commits.length : 0} 条客户端相关提交(起点 ${previousCommit ?? '无'})`,
|
||||
: notes && !previousCommit
|
||||
? `[ai-game-creator-shell] 更新摘要:无锚点,列出最近 ${recentCommits ? recentCommits.length : 0} 条客户端相关提交`
|
||||
: `[ai-game-creator-shell] 更新摘要:自动汇总 ${commits ? commits.length : 0} 条客户端相关提交(起点 ${previousCommit ?? '无'})`,
|
||||
);
|
||||
console.log(`[ai-game-creator-shell] 更新摘要文件:${notesPath}`);
|
||||
if (legacyManifestPath) {
|
||||
|
||||
@@ -14,11 +14,13 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
agcReleasePathPatterns,
|
||||
collectRecentReleaseCommits,
|
||||
collectReleaseCommits,
|
||||
compareVersions,
|
||||
createChannelConfig,
|
||||
createLegacyUpdateManifest,
|
||||
createUpdateManifest,
|
||||
formatRecentReleaseNotes,
|
||||
formatReleaseNotes,
|
||||
nextPatchVersion,
|
||||
resolveManifestPlatformKeys,
|
||||
@@ -277,6 +279,61 @@ test('release notes anchor prefers the explicit commit and falls back to the man
|
||||
);
|
||||
});
|
||||
|
||||
test('release notes anchor degrades to null when the manifest cannot be read', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error('fetch failed');
|
||||
};
|
||||
try {
|
||||
assert.equal(
|
||||
await resolvePreviousReleaseCommit('dev-win', { override: undefined }),
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('recent commit fallback marks that entries may repeat the previous release', () => {
|
||||
const directory = mkdtempSync(path.join(os.tmpdir(), 'agc-recent-git-'));
|
||||
const git = (...args) =>
|
||||
execFileSync('git', args, { cwd: directory, encoding: 'utf8' });
|
||||
try {
|
||||
git('init', '--quiet');
|
||||
git('config', 'user.email', 'release@example.test');
|
||||
git('config', 'user.name', 'release test');
|
||||
mkdirSync(path.join(directory, 'apps/ai-game-creator-shell'), {
|
||||
recursive: true,
|
||||
});
|
||||
for (const name of ['one', 'two']) {
|
||||
writeFileSync(
|
||||
path.join(directory, `apps/ai-game-creator-shell/${name}.rs`),
|
||||
`fn ${name}() {}\n`,
|
||||
);
|
||||
git('add', '.');
|
||||
git('commit', '--quiet', '-m', `客户端:${name}`);
|
||||
}
|
||||
writeFileSync(path.join(directory, 'README.md'), '# 文档\n');
|
||||
git('add', '.');
|
||||
git('commit', '--quiet', '-m', '文档:说明');
|
||||
|
||||
const recent = collectRecentReleaseCommits({ cwd: directory, limit: 5 });
|
||||
assert.deepEqual(
|
||||
recent.map((entry) => entry.subject),
|
||||
['客户端:two', '客户端:one'],
|
||||
);
|
||||
const notes = formatRecentReleaseNotes(recent);
|
||||
assert.match(
|
||||
notes,
|
||||
/^最近客户端改动(未定位到上一次发布提交,可能与上一版重复):\n- 客户端:two/u,
|
||||
);
|
||||
assert.equal(formatRecentReleaseNotes([]), '');
|
||||
assert.equal(formatRecentReleaseNotes(null), '');
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('release upload forces overwrite for artifact, signature and channel pointers', () => {
|
||||
const source = readFileSync(
|
||||
new URL('./release-upload.mjs', import.meta.url),
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
- 发布入口:`npm run ai-game-creator-shell:release:upload`(构建 + 按渠道上传);仅构建不发布的 smoke 使用 `--no-bundle` 分支,不读远端版本、不改版本、不生成清单。
|
||||
- 渠道由构建参数显式指定,并按目标平台校验:Windows 目标只允许 `dev-win`,macOS 目标只允许 `dev-mac`;未显式指定时按目标平台取默认渠道。
|
||||
- 定时调度只在本轮到达的提交包含 AGC 相关路径(客户端、共享包、`server-rs/crates`、AGC 插件、桌面壳图标、根依赖清单)时才触发渠道发布;纯文档或流水线自身的提交只跑 Full Build,不推高客户端版本号。判定失败或勾选强制触发时按"需要发布"处理。
|
||||
- 更新摘要自动生成:发布脚本用渠道清单里的 `commit` 字段(上一次发布的提交)到本次提交之间、且只覆盖客户端相关路径的提交列表生成 `notes`(每条 `- 提交标题(短 SHA)`,最多 12 条、主题 80 字、整体 900 字,超出折叠或截断),同时写入旧协议清单的 `releaseNotes` 和归档文件 `release-notes.txt`。`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准;无法判定起点(缺少上次 `commit` 或本地没有该提交)时不写摘要。清单缺少 `commit` 时回退用上一次成功构建的 `COMMIT_HASH`(CI 通过 `AGC_UPDATE_PREVIOUS_COMMIT` 传入)作为锚点,因此首次启用摘要或更换渠道后也能立即产出摘要。
|
||||
- 更新摘要自动生成:发布脚本用渠道清单里的 `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` 是非标准字段:更新插件忽略未知字段,发布脚本用它定位下一次摘要的起点。
|
||||
- 上传:安装包与 `.sig` 上传到 `agc/<channel>/<version>/`,清单以 `--force` 覆盖上传到 `agc/<channel>/latest.json`,保证 latest 指针与清单内 URL 指向已存在的对象。
|
||||
- Jenkins 流水线需要新增渠道参数与签名凭据;签名私钥与密码只以受保护凭据注入当前进程,不写入 workspace、日志或归档产物。
|
||||
|
||||
Reference in New Issue
Block a user