构建前按 OSS 版本递增 AGC patch
构建前读取 OSS latest.json 并选择较高版本递增 patch 同步更新 AGC package、Tauri 与 Cargo 版本字段 让一键上传复用版本准备流程并更新发布文档 补充版本计算、配置校验与清单生成测试
This commit is contained in:
@@ -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 本机配置读取,不能写入仓库或命令行参数。
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user