From 6534e6f971a9ecd142fa2aee0e324cb4b003b12e Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Tue, 5 May 2026 22:13:28 +0800 Subject: [PATCH] fix(installer): pin versioned installer assets --- .github/workflows/release.yml | 4 +- scripts/build-installation-assets.js | 69 +++++++++++++++++++++++++++- scripts/tests/install-script.test.js | 44 +++++++++++++++++- 3 files changed, 113 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80abd142c..67117f9d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -387,7 +387,9 @@ jobs: # Rewrites SHA256SUMS after copying installer scripts so the release # checksum file covers both standalone archives and installer assets. - name: 'Build Installation Assets' - run: 'npm run package:installation-assets -- --out-dir dist/standalone' + env: + RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' + run: 'npm run package:installation-assets -- --out-dir dist/standalone --version "${RELEASE_VERSION}"' - name: 'Publish @qwen-code/qwen-code' working-directory: 'dist' diff --git a/scripts/build-installation-assets.js b/scripts/build-installation-assets.js index 2fd453d9c..48c5aa843 100644 --- a/scripts/build-installation-assets.js +++ b/scripts/build-installation-assets.js @@ -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. `); } diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js index 207ba5539..98a96e4c0 100644 --- a/scripts/tests/install-script.test.js +++ b/scripts/tests/install-script.test.js @@ -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-*');