修复渠道版本链回退并记录高水位规则
Project CI / AI game creator shell Rust shard 2/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 3/4 (push) Has been cancelled
Project CI / AI game creator shell Rust shard 4/4 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend 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) Has been cancelled

- 发布脚本版本来源改为渠道清单与旧协议迁移指针取较大值,避免渠道启用初期把版本链改小
- 旧迁移指针只服务 dev-win,其它渠道不参与比较,旧指针 404 后自动只剩渠道清单
- 补充三条定向用例:旧指针高水位、两者取高、非 Windows 渠道不读旧指针
- 主规范记录版本高水位规则与 2026-09-17 首次渠道发布 0.1.57 退回 0.1.48、后以 0.1.60 纠偏的事实
This commit is contained in:
kdletters
2026-09-17 18:47:45 +08:00
parent 1a7abfd059
commit 55af6014ba
3 changed files with 96 additions and 7 deletions
@@ -143,27 +143,57 @@ export function resolveManifestPlatformKeys(target = releaseTarget) {
throw new Error(`不支持的发布目标:${target}`);
}
async function readRemoteVersion(channel = resolveReleaseChannel()) {
const manifestUrl = updateManifestUrl(channel);
/** 旧协议迁移指针:只在迁移窗口内存在,是历史版本高水位的来源。 */
function legacyBridgeManifestUrl() {
return `${ossBaseUrl()}/latest.json`;
}
async function readManifestVersion(manifestUrl, label) {
let response;
try {
response = await fetch(manifestUrl, {
headers: { Accept: 'application/json' },
});
} catch (error) {
throw new Error(`读取 OSS 渠道清单失败:${error.message}`);
throw new Error(`读取 ${label} 失败:${error.message}`);
}
if (response.status === 404) return null;
if (!response.ok) {
throw new Error(`读取 OSS 渠道清单失败:HTTP ${response.status}`);
throw new Error(`读取 ${label} 失败:HTTP ${response.status}`);
}
let manifest;
try {
manifest = await response.json();
} catch (error) {
throw new Error(`OSS 渠道清单不是有效 JSON${error.message}`);
throw new Error(`${label} 不是有效 JSON${error.message}`);
}
return parseVersion(manifest?.version, 'OSS渠道清单 version');
return parseVersion(manifest?.version, `${label} version`);
}
/**
* 版本高水位:渠道清单与旧协议迁移指针取较大值。
*
* 只看渠道清单会在「渠道刚启用、旧指针还停在更高版本」时把版本链改小 ——
* 2026-09-17 首次渠道发布就是这样把 0.1.57 退回 0.1.48 的。旧指针只服务
* Windows 渠道,其它渠道不参与比较;旧指针 404(迁移窗口结束)后自动只剩渠道清单。
*/
export async function resolveRemoteHighWaterVersion(
channel = resolveReleaseChannel(),
) {
const channelVersion = await readManifestVersion(
updateManifestUrl(channel),
'OSS 渠道清单',
);
if (channel !== 'dev-win') return channelVersion;
const legacyVersion = await readManifestVersion(
legacyBridgeManifestUrl(),
'OSS 迁移指针',
);
if (channelVersion == null) return legacyVersion;
if (legacyVersion == null) return channelVersion;
return compareVersions(channelVersion, legacyVersion) >= 0
? channelVersion
: legacyVersion;
}
function replaceVersionLine(source, version, pattern, label) {
@@ -174,7 +204,7 @@ function replaceVersionLine(source, version, pattern, label) {
export async function prepareReleaseVersion() {
const channel = resolveReleaseChannel();
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await readRemoteVersion(channel);
const remoteVersion = await resolveRemoteHighWaterVersion(channel);
const requestedVersion = process.env.AGC_RELEASE_VERSION?.trim();
const nextVersion = requestedVersion
? parseVersion(requestedVersion, '指定版本')
@@ -13,6 +13,7 @@ import {
nextPatchVersion,
resolveManifestPlatformKeys,
resolveReleaseChannel,
resolveRemoteHighWaterVersion,
selectReleaseArtifact,
updateManifestUrl,
} from './build-release.mjs';
@@ -49,6 +50,22 @@ function withSignedArtifact(fileName, run) {
}
}
function jsonResponse(body, status = 200) {
return {
status,
ok: status >= 200 && status < 300,
json: async () => body,
};
}
function withStubbedFetch(handler, run) {
const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => handler(String(url));
return Promise.resolve(run()).finally(() => {
globalThis.fetch = originalFetch;
});
}
test('selects an explicit release artifact when configured', () => {
const artifactPath = fileURLToPath(
new URL('../package.json', import.meta.url),
@@ -173,6 +190,47 @@ test('next release version follows the higher local or channel version', () => {
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
});
test('version high water keeps the legacy pointer during the migration window', async () => {
await withStubbedFetch(
(url) =>
url.endsWith('/agc/dev-win/latest.json')
? jsonResponse({}, 404)
: jsonResponse({ version: '0.1.57' }),
async () => {
assert.equal(await resolveRemoteHighWaterVersion('dev-win'), '0.1.57');
// 旧指针 0.1.57 已是高水位,下一次发布必须是 0.1.58,不能退回渠道本地版本。
assert.equal(nextPatchVersion('0.1.47', '0.1.57'), '0.1.58');
},
);
});
test('version high water takes the higher of channel and legacy pointer', async () => {
await withStubbedFetch(
(url) =>
url.endsWith('/agc/dev-win/latest.json')
? jsonResponse({ version: '0.1.60' })
: jsonResponse({ version: '0.1.57' }),
async () => {
assert.equal(await resolveRemoteHighWaterVersion('dev-win'), '0.1.60');
},
);
});
test('version high water ignores the windows migration pointer for other channels', async () => {
await withStubbedFetch(
(url) => {
assert.ok(
!url.endsWith('/agc/latest.json'),
'non-windows channel must not read the windows migration pointer',
);
return jsonResponse({ version: '0.1.12' });
},
async () => {
assert.equal(await resolveRemoteHighWaterVersion('dev-mac'), '0.1.12');
},
);
});
test('release upload forces overwrite for artifact, signature and channel pointers', () => {
const source = readFileSync(
new URL('./release-upload.mjs', import.meta.url),
@@ -82,6 +82,7 @@
- 上一条的两个键不能合成单一 `darwin-universal` 键:更新插件按运行时实际架构解析清单键(Apple Silicon 命中 `darwin-aarch64`Intel 命中 `darwin-x86_64`),不存在自动命中 `darwin-universal` 的情形。将来真要单独发该键,必须在客户端同时设置自定义 target,否则清单里这一项永远不会被读取。
- 构建期要求:打开 `bundle.createUpdaterArtifacts` 以生成 `.sig`;构建环境提供签名私钥与密码(私钥内容不得入库);公钥写入客户端配置。公钥在首个带更新能力的版本发布后不可更换,更换等于放弃自动更新(只能手动重装)。
- 版本递增按渠道独立进行:发布脚本读取该渠道远端 `latest.json``version`,与本地版本取较高者递增 patch;两个渠道的版本号互不影响。
- 版本高水位:发布脚本取「渠道清单版本」与「旧协议迁移指针版本」(迁移窗口内)中的较大值再递增。只看渠道清单会在渠道启用初期把版本链改小 —— 2026-09-17 首次渠道发布即把旧指针的 0.1.57 退回 0.1.48,随后以显式 0.1.60 纠偏;迁移窗口结束(旧指针 404)后自动只剩渠道清单,`dev-mac` 不参与旧指针比较。
- 迁移(旧协议 → 渠道清单):
- 迁移起点:已发布客户端(含当前线上版本)内置自研清单地址 `agc/latest.json`(sha256 格式),下载与安装由自研 Rust 命令完成。
- 迁移策略见「未决问题与决策」。迁移完成后,自研清单解析、下载命令、下载进度事件以及为此放行的 CSP / HTTP 白名单条目按「四不写」整条删除,不留兼容分支与墓碑说明。