fix(installer): pin versioned installer assets

This commit is contained in:
yiliang114 2026-05-05 22:13:28 +08:00
parent bbaa8ffc75
commit 6534e6f971
3 changed files with 113 additions and 4 deletions

View file

@ -26,6 +26,9 @@ if (isMainModule()) {
} else {
await buildInstallationAssets(
path.resolve(args.outDir || path.join(rootDir, 'dist', 'standalone')),
{
version: args.version,
},
);
}
} catch (error) {
@ -39,7 +42,7 @@ function isMainModule() {
}
async function buildInstallationAssets(outDir, options = {}) {
const { assets = INSTALLATION_ASSETS, root = rootDir } = options;
const { assets = INSTALLATION_ASSETS, root = rootDir, version } = options;
fs.mkdirSync(outDir, { recursive: true });
for (const asset of assets) {
@ -49,7 +52,11 @@ async function buildInstallationAssets(outDir, options = {}) {
}
const destination = path.join(outDir, asset.output);
fs.copyFileSync(source, destination);
const contents = fs.readFileSync(source, 'utf8');
fs.writeFileSync(
destination,
version ? stampInstallerVersion(contents, asset, version) : contents,
);
if (asset.mode !== undefined && process.platform !== 'win32') {
fs.chmodSync(destination, asset.mode);
}
@ -59,6 +66,56 @@ async function buildInstallationAssets(outDir, options = {}) {
await assertInstallationAssetChecksums(outDir, assets);
}
function stampInstallerVersion(contents, asset, version) {
validateReleaseVersion(version);
const sourceName = asset.sourcePath.at(-1);
if (sourceName.endsWith('.sh')) {
const stampedDefault = replaceRequired(
contents,
'VERSION="${QWEN_INSTALL_VERSION:-latest}"',
`VERSION="\${QWEN_INSTALL_VERSION:-${version}}"`,
asset.output,
);
return stampVersionHelpText(stampedDefault, asset.output, version);
}
if (sourceName.endsWith('.bat')) {
const stampedDefault = replaceRequired(
contents,
'set "VERSION=latest"',
`set "VERSION=${version}"`,
asset.output,
);
return stampVersionHelpText(stampedDefault, asset.output, version);
}
return contents;
}
function replaceRequired(contents, search, replacement, output) {
if (!contents.includes(search)) {
fail(`Unable to stamp release version in ${output}`);
}
return contents.replace(search, replacement);
}
function stampVersionHelpText(contents, output, version) {
return replaceRequired(
contents,
'Standalone release version. Defaults to latest.',
`Standalone release version. Defaults to ${version}.`,
output,
);
}
function validateReleaseVersion(version) {
if (/^v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$/.test(version)) {
return;
}
fail('--version must be a semver string');
}
async function assertInstallationAssetChecksums(
outDir,
assets = INSTALLATION_ASSETS,
@ -108,6 +165,7 @@ function parseArgs(argv) {
const args = {
help: false,
outDir: undefined,
version: undefined,
};
for (let index = 0; index < argv.length; index += 1) {
@ -121,6 +179,11 @@ function parseArgs(argv) {
args.outDir = readOptionValue(argv, index, arg);
index += 1;
break;
case '--version':
args.version = readOptionValue(argv, index, arg);
validateReleaseVersion(args.version);
index += 1;
break;
default:
fail(`Unknown option: ${arg}`);
}
@ -144,6 +207,8 @@ Usage:
Options:
--out-dir PATH Output directory. Defaults to dist/standalone.
--version VERSION
Stamp release installers so their default version is VERSION.
`);
}

View file

@ -301,6 +301,7 @@ describe('standalone release packaging', () => {
expect(output).toContain('package:installation-assets');
expect(output).toContain('--out-dir PATH');
expect(output).toContain('--version VERSION');
});
it('rejects invalid installation asset CLI arguments', () => {
@ -312,6 +313,14 @@ describe('standalone release packaging', () => {
['scripts/build-installation-assets.js', '--out-dir'],
/--out-dir requires a value/,
);
expectCommandFailure(
['scripts/build-installation-assets.js', '--version'],
/--version requires a value/,
);
expectCommandFailure(
['scripts/build-installation-assets.js', '--version', 'beta'],
/--version must be a semver string/,
);
});
it('shares release asset classification helpers', async () => {
@ -418,6 +427,37 @@ describe('standalone release packaging', () => {
}
});
it('stamps release versions into copied installation assets', async () => {
const { assertInstallationAssetChecksums, buildInstallationAssets } =
await import(installationAssetsScriptUrl);
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-install-assets-'));
try {
await buildInstallationAssets(tmpDir, { version: '0.16.0' });
const installSh = readScript(path.join(tmpDir, 'install-qwen.sh'));
const installBat = readScript(path.join(tmpDir, 'install-qwen.bat'));
expect(installSh).toContain('VERSION="${QWEN_INSTALL_VERSION:-0.16.0}"');
expect(installSh).toContain(
'Standalone release version. Defaults to 0.16.0.',
);
expect(installSh).toContain('--version)');
expect(installBat).toContain('set "VERSION=0.16.0"');
expect(installBat).toContain(
'Standalone release version. Defaults to 0.16.0.',
);
expect(installBat).toContain(
'if defined QWEN_INSTALL_VERSION set "VERSION=!QWEN_INSTALL_VERSION!"',
);
await expect(
assertInstallationAssetChecksums(tmpDir),
).resolves.not.toThrow();
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
});
it('rejects missing installation asset sources', async () => {
const { buildInstallationAssets } = await import(
installationAssetsScriptUrl
@ -633,7 +673,9 @@ describe('standalone release packaging', () => {
const workflow = readScript('.github/workflows/release.yml');
expect(workflow).toContain('npm run package:standalone:release --');
expect(workflow).toContain('npm run package:installation-assets --');
expect(workflow).toContain(
'npm run package:installation-assets -- --out-dir dist/standalone --version "${RELEASE_VERSION}"',
);
expect(workflow).not.toContain('verify_node_checksum()');
expect(workflow).not.toContain('download_node()');
expect(workflow).toContain('dist/standalone/qwen-code-*');