构建时自动生成 AGC 更新清单
新增发布构建包装脚本并扫描平台安装包 自动计算下载地址、文件大小和 SHA-256 补充构建脚本测试与发布文档
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"dev": "node scripts/start-tauri-dev.mjs",
|
||||
"dev-server": "node scripts/start-dev-server.mjs",
|
||||
"dev-stack": "node scripts/start-dev-stack.mjs",
|
||||
"build": "npm --prefix ../.. exec tauri -- build",
|
||||
"build": "node scripts/build-release.mjs",
|
||||
"skill-pack:check": "node scripts/check-skill-pack.mjs",
|
||||
"skill-pack:sync": "node scripts/check-skill-pack.mjs --write",
|
||||
"skill-pack:test": "node --test scripts/check-skill-pack.test.mjs",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appRoot = fileURLToPath(new URL('..', import.meta.url));
|
||||
const bundleRoot = path.join(
|
||||
appRoot,
|
||||
'src-tauri',
|
||||
'target',
|
||||
'release',
|
||||
'bundle',
|
||||
);
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
|
||||
);
|
||||
const defaultOssBaseUrl =
|
||||
'https://genarrative-assets.oss-cn-shanghai.aliyuncs.com/agc';
|
||||
|
||||
function runTauriBuild(args) {
|
||||
const result = spawnSync(
|
||||
'npm',
|
||||
['--prefix', '../..', 'exec', 'tauri', '--', 'build', ...args],
|
||||
{ cwd: appRoot, stdio: 'inherit' },
|
||||
);
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
function listFiles(root) {
|
||||
if (!fs.existsSync(root)) return [];
|
||||
return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
|
||||
const fullPath = path.join(root, entry.name);
|
||||
return entry.isDirectory() ? listFiles(fullPath) : [fullPath];
|
||||
});
|
||||
}
|
||||
|
||||
function artifactPriority(filePath) {
|
||||
const name = path.basename(filePath).toLowerCase();
|
||||
if (process.platform === 'win32') return name.endsWith('.exe') ? 0 : 99;
|
||||
if (process.platform === 'darwin') return name.endsWith('.dmg') ? 0 : 99;
|
||||
if (name.endsWith('.appimage')) return 0;
|
||||
if (name.endsWith('.deb')) return 1;
|
||||
if (name.endsWith('.rpm')) return 2;
|
||||
return 99;
|
||||
}
|
||||
|
||||
export function selectReleaseArtifact(files) {
|
||||
const explicit = process.env.AGC_UPDATE_ARTIFACT?.trim();
|
||||
if (explicit) {
|
||||
const resolved = path.resolve(explicit);
|
||||
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
|
||||
throw new Error(`AGC_UPDATE_ARTIFACT 不是有效文件:${resolved}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
return (
|
||||
[...files]
|
||||
.filter((filePath) => artifactPriority(filePath) < 99)
|
||||
.sort((left, right) => {
|
||||
const priority = artifactPriority(left) - artifactPriority(right);
|
||||
return priority || left.localeCompare(right);
|
||||
})[0] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function createUpdateManifest(artifactPath) {
|
||||
const bytes = fs.readFileSync(artifactPath);
|
||||
const version = packageJson.version;
|
||||
const fileName = path.basename(artifactPath);
|
||||
const baseUrl = (
|
||||
process.env.AGC_UPDATE_OSS_BASE_URL?.trim() || defaultOssBaseUrl
|
||||
).replace(/\/+$/u, '');
|
||||
const encodedFileName = encodeURIComponent(fileName).replace(/%2F/giu, '/');
|
||||
return {
|
||||
version,
|
||||
downloadUrl: `${baseUrl}/${encodeURIComponent(version)}/${encodedFileName}`,
|
||||
sha256: createHash('sha256').update(bytes).digest('hex'),
|
||||
size: bytes.length,
|
||||
...(process.env.AGC_UPDATE_RELEASE_NOTES?.trim()
|
||||
? { releaseNotes: process.env.AGC_UPDATE_RELEASE_NOTES.trim() }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function generateUpdateManifest() {
|
||||
const artifact = selectReleaseArtifact(listFiles(bundleRoot));
|
||||
if (!artifact) {
|
||||
throw new Error(`未找到可发布的 AGC 安装包:${bundleRoot}`);
|
||||
}
|
||||
const manifest = createUpdateManifest(artifact);
|
||||
const manifestPath = path.join(bundleRoot, 'latest.json');
|
||||
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`[ai-game-creator-shell] 已生成 ${manifestPath}`);
|
||||
console.log(`[ai-game-creator-shell] 安装包:${artifact}`);
|
||||
return { artifact, manifestPath, manifest };
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
||||
) {
|
||||
const args = process.argv.slice(2);
|
||||
runTauriBuild(args);
|
||||
if (!args.includes('--no-bundle')) generateUpdateManifest();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
|
||||
import {
|
||||
createUpdateManifest,
|
||||
selectReleaseArtifact,
|
||||
} from './build-release.mjs';
|
||||
|
||||
test('selects an explicit release artifact when configured', () => {
|
||||
const artifactPath = new URL('../package.json', import.meta.url).pathname;
|
||||
const previous = process.env.AGC_UPDATE_ARTIFACT;
|
||||
process.env.AGC_UPDATE_ARTIFACT = artifactPath;
|
||||
try {
|
||||
assert.equal(selectReleaseArtifact([]), artifactPath);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.AGC_UPDATE_ARTIFACT;
|
||||
else process.env.AGC_UPDATE_ARTIFACT = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test('does not select unsupported files', () => {
|
||||
assert.equal(
|
||||
selectReleaseArtifact(['/tmp/latest.json', '/tmp/readme.txt']),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
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.equal(manifest.sha256.length, 64);
|
||||
assert.equal(typeof manifest.size, 'number');
|
||||
});
|
||||
@@ -29,4 +29,6 @@ AGC 每次启动时由根窗口检查一次公开 OSS 更新清单。清单默
|
||||
|
||||
## 发布约定
|
||||
|
||||
每次发布安装包上传完成后,再上传同一版本的 `agc/latest.json`,确保 `downloadUrl` 指向已存在的 OSS 对象;清单和安装包均使用公开可读对象,不在清单中保存凭据、签名或本地路径。
|
||||
执行 `npm run ai-game-creator-shell:build` 会在 Tauri 构建完成后自动扫描平台安装包,并在 `apps/ai-game-creator-shell/src-tauri/target/release/bundle/latest.json` 生成包含版本、下载地址、大小和 SHA-256 的清单。可通过 `AGC_UPDATE_ARTIFACT` 指定要发布的安装包,通过 `AGC_UPDATE_OSS_BASE_URL` 指定 OSS 前缀,通过 `AGC_UPDATE_RELEASE_NOTES` 写入发布说明;`--no-bundle` smoke 构建不会生成清单。
|
||||
|
||||
每次发布安装包上传完成后,再上传同一目录生成的 `latest.json`,确保 `downloadUrl` 指向已存在的 OSS 对象;清单和安装包均使用公开可读对象,不在清单中保存凭据、签名或本地路径。构建脚本不自动上传 OSS,避免把 AccessKey 写入本地配置或 CI 日志。
|
||||
|
||||
Reference in New Issue
Block a user