构建前按 OSS 版本递增 AGC patch
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Backend tests (pull_request) Failing after 9s
Project CI / Frontend tests (pull_request) Successful in 5m13s
Project CI / Native shell tests (pull_request) Failing after 17m40s

构建前读取 OSS latest.json 并选择较高版本递增 patch

同步更新 AGC package、Tauri 与 Cargo 版本字段

让一键上传复用版本准备流程并更新发布文档

补充版本计算、配置校验与清单生成测试
This commit is contained in:
2026-08-31 15:22:38 +08:00
parent 4a66677547
commit fea7a72246
6 changed files with 171 additions and 21 deletions
@@ -16,11 +16,131 @@ const bundleRoot = path.join(
'release',
'bundle',
);
const packageJson = JSON.parse(
fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
);
const packageJsonPath = path.join(appRoot, 'package.json');
const tauriConfigPath = path.join(appRoot, 'src-tauri', 'tauri.conf.json');
const cargoManifestPath = path.join(appRoot, 'src-tauri', 'Cargo.toml');
const cargoLockPath = path.join(appRoot, 'src-tauri', 'Cargo.lock');
const defaultOssBaseUrl =
'https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc';
const updateManifestUrl =
process.env.VITE_AGC_UPDATE_MANIFEST_URL?.trim() ||
`${process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl}/latest.json`;
function readPackageJson() {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
}
export function compareVersions(left, right) {
const leftParts = left.split('.').map(Number);
const rightParts = right.split('.').map(Number);
for (let index = 0; index < 3; index += 1) {
if (leftParts[index] !== rightParts[index]) {
return leftParts[index] > rightParts[index] ? 1 : -1;
}
}
return 0;
}
function parseVersion(value, label) {
if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/u.test(value)) {
throw new Error(`${label} 不是有效的三段版本号:${String(value)}`);
}
return value;
}
export function nextPatchVersion(localVersion, remoteVersion) {
const local = parseVersion(localVersion, '本地版本');
const remote =
remoteVersion == null ? null : parseVersion(remoteVersion, 'OSS版本');
const base = remote && compareVersions(remote, local) > 0 ? remote : local;
const [major, minor, patch] = base.split('.').map(Number);
if (patch === Number.MAX_SAFE_INTEGER) {
throw new Error(`版本号 patch 已达到上限:${base}`);
}
return `${major}.${minor}.${patch + 1}`;
}
async function readRemoteVersion() {
let response;
try {
response = await fetch(updateManifestUrl, {
headers: { Accept: 'application/json' },
});
} catch (error) {
throw new Error(`读取 OSS 版本清单失败:${error.message}`);
}
if (response.status === 404) return null;
if (!response.ok) {
throw new Error(`读取 OSS 版本清单失败:HTTP ${response.status}`);
}
let manifest;
try {
manifest = await response.json();
} catch (error) {
throw new Error(`OSS 版本清单不是有效 JSON${error.message}`);
}
return parseVersion(manifest?.version, 'OSS版本清单 version');
}
function replaceVersionLine(source, version, pattern, label) {
if (!pattern.test(source)) throw new Error(`未找到${label}版本字段`);
return source.replace(pattern, `$1${version}$3`);
}
export async function prepareReleaseVersion() {
const localVersion = parseVersion(readPackageJson().version, '本地版本');
const remoteVersion = await readRemoteVersion();
const nextVersion = nextPatchVersion(localVersion, remoteVersion);
const packageSource = fs.readFileSync(packageJsonPath, 'utf8');
fs.writeFileSync(
packageJsonPath,
replaceVersionLine(
packageSource,
nextVersion,
/("version"\s*:\s*")([^"]+)(")/u,
'package.json',
),
);
const tauriSource = fs.readFileSync(tauriConfigPath, 'utf8');
fs.writeFileSync(
tauriConfigPath,
replaceVersionLine(
tauriSource,
nextVersion,
/("version"\s*:\s*")([^"]+)(")/u,
'tauri.conf.json',
),
);
const cargoSource = fs.readFileSync(cargoManifestPath, 'utf8');
fs.writeFileSync(
cargoManifestPath,
replaceVersionLine(
cargoSource,
nextVersion,
/(^\[package\][\s\S]*?^version\s*=\s*")([^"]+)(")/mu,
'Cargo.toml',
),
);
const cargoLockSource = fs.readFileSync(cargoLockPath, 'utf8');
fs.writeFileSync(
cargoLockPath,
replaceVersionLine(
cargoLockSource,
nextVersion,
/(^name\s*=\s*"genarrative-ai-game-creator-shell"\s*\nversion\s*=\s*")([^"]+)(")/mu,
'Cargo.lock',
),
);
console.log(
`[ai-game-creator-shell] 版本 ${localVersion} / OSS ${remoteVersion ?? '不存在'} -> ${nextVersion}`,
);
return nextVersion;
}
export function runTauriBuild(args = []) {
const noBundle = args.includes('--no-bundle');
@@ -83,7 +203,7 @@ export function selectReleaseArtifact(files) {
export function createUpdateManifest(artifactPath) {
const bytes = fs.readFileSync(artifactPath);
const version = packageJson.version;
const version = readPackageJson().version;
const fileName = path.basename(artifactPath);
const baseUrl = (
process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl
@@ -118,6 +238,7 @@ if (
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const args = process.argv.slice(2);
if (!args.includes('--no-bundle')) await prepareReleaseVersion();
runTauriBuild(args);
if (!args.includes('--no-bundle')) generateUpdateManifest();
}
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
compareVersions,
createUpdateManifest,
nextPatchVersion,
selectReleaseArtifact,
} from './build-release.mjs';
@@ -29,8 +31,18 @@ test('manifest contains version, download URL and integrity fields', () => {
const manifest = createUpdateManifest(
new URL('../package.json', import.meta.url).pathname,
);
assert.equal(manifest.version, '0.1.12');
assert.match(manifest.downloadUrl, /\/agc\/0\.1\.12\/package\.json$/u);
assert.match(manifest.version, /^\d+\.\d+\.\d+$/u);
assert.match(
manifest.downloadUrl,
new RegExp(`/agc/${manifest.version}/package\\.json$`, 'u'),
);
assert.equal(manifest.sha256.length, 64);
assert.equal(typeof manifest.size, 'number');
});
test('next release version follows the higher local or OSS version', () => {
assert.equal(compareVersions('0.1.15', '0.1.12'), 1);
assert.equal(nextPatchVersion('0.1.12', '0.1.15'), '0.1.16');
assert.equal(nextPatchVersion('0.1.18', '0.1.15'), '0.1.19');
assert.equal(nextPatchVersion('0.1.12', null), '0.1.13');
});
@@ -1532,13 +1532,17 @@ if (
);
}
const releaseVersions = [
packageConfig.version,
tauriConfig.version,
cargoPackageVersion,
];
if (
tauriConfig.version !== '0.1.12' ||
packageConfig.version !== '0.1.12' ||
cargoPackageVersion !== '0.1.12'
releaseVersions.some((version) => !/^\d+\.\d+\.\d+$/u.test(version ?? '')) ||
new Set(releaseVersions).size !== 1
) {
throw new Error(
'AI game creator standard release must remain version 0.1.12',
'AI game creator release versions must be valid three-part semver and synchronized',
);
}
@@ -9,9 +9,8 @@ if (!/^[a-z0-9][a-z0-9.-]{1,62}$/u.test(bucket) || /[\r\n\0]/u.test(endpoint)) {
}
process.env.AGC_UPDATE_OSS_BASE_URL ||= `https://${bucket}.${endpoint}/agc`;
const { generateUpdateManifest, runTauriBuild } = await import(
'./build-release.mjs'
);
const { generateUpdateManifest, prepareReleaseVersion, runTauriBuild } =
await import('./build-release.mjs');
function runOssutil(args) {
const binary = process.env.OSSUTIL_BIN?.trim() || 'ossutil';
@@ -25,6 +24,7 @@ function runOssutil(args) {
if (result.status !== 0) process.exit(result.status ?? 1);
}
await prepareReleaseVersion();
runTauriBuild([]);
const { artifact, manifestPath, manifest } = generateUpdateManifest();
const artifactKey = `agc/${manifest.version}/${path.basename(artifact)}`;
@@ -29,8 +29,19 @@ AGC 每次启动时由根窗口检查一次公开 OSS 更新清单。清单默
## 发布约定
当前发布目标固定为 Windows x64 NSIS。执行 `npm run ai-game-creator-shell:build`向 Tauri 传入 `--target x86_64-pc-windows-msvc`,在构建完成后自动扫描 `.exe` 安装包,并在 `apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json` 生成包含版本、下载地址、大小和 SHA-256 的清单。可通过 `AGC_BUILD_TARGET` 显式覆盖目标(发布仍应使用 Windows x64),通过 `AGC_UPDATE_ARTIFACT` 指定要发布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定 OSS 前缀,通过 `AGC_UPDATE_RELEASE_NOTES` 写入发布说明;`--no-bundle` smoke 构建不会生成清单。
当前发布目标固定为 Windows x64 NSIS。执行 `npm run ai-game-creator-shell:build`先读取
`VITE_AGC_UPDATE_MANIFEST_URL`(默认 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com/agc/latest.json`)的
`latest.json`,取本地与 OSS 的较高版本并递增一个 patch,然后同步更新 package、Tauri 和 Cargo
版本后再向 Tauri 传入 `--target x86_64-pc-windows-msvc` 构建。OSS 清单首次不存在时按本地版本递增;
OSS 请求失败、清单格式错误或版本无效会终止发布,避免覆盖线上版本。构建完成后自动扫描 `.exe`
安装包,并在 `apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json`
生成包含版本、下载地址、大小和 SHA-256 的清单。可通过 `AGC_BUILD_TARGET` 显式覆盖目标(发布仍应使用
Windows x64),通过 `AGC_UPDATE_ARTIFACT` 指定要发布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定
OSS 前缀,通过 `AGC_UPDATE_RELEASE_NOTES` 写入发布说明;`--no-bundle` smoke 构建不会读取 OSS、修改版本或生成清单。
每次发布安装包上传完成后,再上传同一目录生成的 `latest.json`,确保 `downloadUrl` 指向已存在的 OSS 对象;清单和安装包均使用公开可读对象,不在清单中保存凭据、签名或本地路径。构建脚本不自动上传 OSS,避免把 AccessKey 写入本地配置或 CI 日志。
如需一键构建并上传,可执行 `npm run ai-game-creator-shell:release:upload`。该命令要求本机已安装并配置 `ossutil`先构建 Windows x64 NSIS,再上传安装包和 `latest.json`。默认上传到 `agc-dev` / `oss-rg-china-mainland.aliyuncs.com`,也可用 `AGC_OSS_BUCKET``AGC_OSS_ENDPOINT``OSSUTIL_BIN` 覆盖;凭据由 ossutil 本机配置读取,不能写入仓库或命令行参数。
如需一键构建并上传,可执行 `npm run ai-game-creator-shell:release:upload`。该命令要求本机已安装并配置 `ossutil`
先按上述规则比较 OSS 版本、递增 patch、构建 Windows x64 NSIS,再上传安装包和 `latest.json`。默认上传到
`agc-dev` / `oss-rg-china-mainland.aliyuncs.com`,也可用 `AGC_OSS_BUCKET``AGC_OSS_ENDPOINT``OSSUTIL_BIN`
覆盖;凭据由 ossutil 本机配置读取,不能写入仓库或命令行参数。
+8 -6
View File
@@ -173,12 +173,14 @@ export function collectNpmWorkspaceErrors(rootDir) {
`${manifestPath}: name must be ${WORKSPACE_NAMES[workspacePath]}, received ${String(manifest.name)}`,
);
}
const expectedWorkspaceVersion =
workspacePath === 'apps/ai-game-creator-shell' ? '0.1.12' : '0.1.0';
if (manifest.version !== expectedWorkspaceVersion) {
errors.push(
`${manifestPath}: workspace version must be ${expectedWorkspaceVersion}`,
);
if (workspacePath === 'apps/ai-game-creator-shell') {
if (!/^\d+\.\d+\.\d+$/u.test(manifest.version ?? '')) {
errors.push(
`${manifestPath}: workspace version must be a three-part semver`,
);
}
} else if (manifest.version !== '0.1.0') {
errors.push(`${manifestPath}: workspace version must be 0.1.0`);
}
for (const nestedLockfile of findNestedLockfiles(rootDir, workspacePath)) {