mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-19 16:28:28 +00:00
229 lines
6.6 KiB
JavaScript
229 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { RELEASE_TARGETS } from './build-standalone-release.js';
|
|
import { isStandaloneArchiveName } from './release-asset-config.js';
|
|
import {
|
|
fail,
|
|
isMainModule,
|
|
parseCliArgs,
|
|
parseSha256Sums,
|
|
sha256File,
|
|
} from './release-script-utils.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
|
|
const EXPECTED_STANDALONE_ARCHIVE_NAMES = RELEASE_TARGETS.map(
|
|
({ qwenTarget }) =>
|
|
`qwen-code-${qwenTarget}.${qwenTarget === 'win-x64' ? 'zip' : 'tar.gz'}`,
|
|
);
|
|
// Release artifacts that the installer chain expects in a GitHub Release.
|
|
// Hosted installer scripts (install-qwen.sh / install-qwen.bat) are served
|
|
// from a separate hosted endpoint and are intentionally not part of this set;
|
|
// they have their own staging path in `package:hosted-installation`.
|
|
const EXPECTED_RELEASE_ASSET_NAMES = [
|
|
...EXPECTED_STANDALONE_ARCHIVE_NAMES,
|
|
'SHA256SUMS',
|
|
];
|
|
|
|
const CLI_OPTIONS = {
|
|
'--help': { name: 'help', type: 'boolean' },
|
|
'-h': { name: 'help', type: 'boolean' },
|
|
'--dir': { name: 'dir' },
|
|
'--base-url': { name: 'baseUrl' },
|
|
};
|
|
|
|
if (isMainModule(import.meta.url)) {
|
|
try {
|
|
await main();
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseCliArgs(process.argv.slice(2), CLI_OPTIONS, {
|
|
help: false,
|
|
dir: undefined,
|
|
baseUrl: undefined,
|
|
});
|
|
if (args.help) {
|
|
printUsage();
|
|
return;
|
|
}
|
|
if (args.dir && args.baseUrl) {
|
|
fail('Pass --dir or --base-url, not both.');
|
|
}
|
|
if (args.baseUrl) {
|
|
await verifyReleaseBaseUrl(args.baseUrl);
|
|
return;
|
|
}
|
|
await verifyReleaseDirectory(
|
|
path.resolve(args.dir || path.join(rootDir, 'dist', 'standalone')),
|
|
);
|
|
}
|
|
|
|
function printUsage() {
|
|
console.log(`Usage: npm run verify:installation-release -- [options]
|
|
|
|
Verifies that an installation release directory contains the expected standalone
|
|
archives with matching SHA256SUMS entries. For a release URL, verifies that
|
|
SHA256SUMS is reachable, lists the expected archives, and that each archive URL
|
|
is reachable without downloading the full archive.
|
|
|
|
Options:
|
|
--dir PATH Verify a local release directory. Defaults to dist/standalone.
|
|
--base-url URL Verify a remote release URL (e.g. a GitHub release download
|
|
prefix). Cannot be combined with --dir.
|
|
-h, --help Show this help message.
|
|
`);
|
|
}
|
|
|
|
async function verifyReleaseDirectory(dir) {
|
|
const checksums = readReleaseChecksums(dir);
|
|
assertExpectedChecksumEntries(checksums);
|
|
assertExpectedArchiveFiles(dir);
|
|
|
|
for (const assetName of EXPECTED_STANDALONE_ARCHIVE_NAMES) {
|
|
const assetPath = path.join(dir, assetName);
|
|
if (!fs.existsSync(assetPath)) {
|
|
fail(`Missing release asset: ${assetName}`);
|
|
}
|
|
|
|
const actual = await sha256File(assetPath);
|
|
if (actual !== checksums.get(assetName)) {
|
|
fail(`Checksum verification failed for ${assetName}`);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`Verified ${EXPECTED_RELEASE_ASSET_NAMES.length} installation release assets in ${dir}`,
|
|
);
|
|
}
|
|
|
|
async function verifyReleaseBaseUrl(baseUrl, options = {}) {
|
|
const { fetchImpl = fetch } = options;
|
|
const normalizedBaseUrl = normalizeHttpsBaseUrl(baseUrl);
|
|
const checksumUrl = new URL('SHA256SUMS', normalizedBaseUrl).toString();
|
|
const checksums = parseSha256Sums(await fetchText(checksumUrl, fetchImpl));
|
|
assertExpectedChecksumEntries(checksums);
|
|
|
|
for (const assetName of EXPECTED_STANDALONE_ARCHIVE_NAMES) {
|
|
await assertRemoteAssetAvailable(
|
|
new URL(assetName, normalizedBaseUrl).toString(),
|
|
fetchImpl,
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`Verified ${EXPECTED_RELEASE_ASSET_NAMES.length} installation release asset URLs at ${baseUrl}`,
|
|
);
|
|
}
|
|
|
|
function readReleaseChecksums(dir) {
|
|
const checksumPath = path.join(dir, 'SHA256SUMS');
|
|
if (!fs.existsSync(checksumPath)) {
|
|
fail(`SHA256SUMS was not found at ${checksumPath}`);
|
|
}
|
|
|
|
return parseSha256Sums(fs.readFileSync(checksumPath, 'utf8'));
|
|
}
|
|
|
|
function assertExpectedChecksumEntries(checksums) {
|
|
const expected = new Set(EXPECTED_STANDALONE_ARCHIVE_NAMES);
|
|
const missing = EXPECTED_STANDALONE_ARCHIVE_NAMES.filter(
|
|
(assetName) => !checksums.has(assetName),
|
|
);
|
|
const extra = Array.from(checksums.keys()).filter(
|
|
(assetName) =>
|
|
isStandaloneArchiveName(assetName) && !expected.has(assetName),
|
|
);
|
|
|
|
if (missing.length > 0) {
|
|
fail(`Missing release asset checksum: ${missing.join(', ')}`);
|
|
}
|
|
if (extra.length > 0) {
|
|
fail(`Unexpected release asset checksum: ${extra.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
function assertExpectedArchiveFiles(dir) {
|
|
const expected = new Set(EXPECTED_STANDALONE_ARCHIVE_NAMES);
|
|
const extra = fs
|
|
.readdirSync(dir)
|
|
.filter(isStandaloneArchiveName)
|
|
.filter((assetName) => !expected.has(assetName))
|
|
.sort();
|
|
|
|
if (extra.length > 0) {
|
|
fail(`Unexpected release asset: ${extra.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
async function assertRemoteAssetAvailable(url, fetchImpl) {
|
|
let response = await fetchImpl(url, { method: 'HEAD' });
|
|
if (response.ok) {
|
|
await response.body?.cancel?.();
|
|
return;
|
|
}
|
|
await response.body?.cancel?.();
|
|
|
|
// Some object-storage hosts disable HEAD; fall back to a 1-byte ranged GET
|
|
// so the verifier can still confirm reachability without downloading the
|
|
// full archive.
|
|
response = await fetchImpl(url, {
|
|
headers: {
|
|
Range: 'bytes=0-0',
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
fail(`Release asset URL is not available: ${url}`);
|
|
}
|
|
await response.body?.cancel?.();
|
|
}
|
|
|
|
async function fetchText(url, fetchImpl) {
|
|
const response = await fetchImpl(url);
|
|
if (!response.ok) {
|
|
fail(
|
|
`Failed to download ${url}: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
return response.text();
|
|
}
|
|
|
|
function normalizeHttpsBaseUrl(baseUrl) {
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(baseUrl);
|
|
} catch {
|
|
fail(`--base-url must be a valid URL: ${baseUrl}`);
|
|
}
|
|
// Real release URLs are always HTTPS. Tests use injected fetchImpl, so
|
|
// they don't need a real protocol. Rejecting non-https early prevents an
|
|
// operator from accidentally pointing the verifier at a plain-http mirror.
|
|
if (parsed.protocol !== 'https:') {
|
|
fail(`--base-url must use https: ${baseUrl}`);
|
|
}
|
|
if (!parsed.pathname.endsWith('/')) {
|
|
parsed.pathname = `${parsed.pathname}/`;
|
|
}
|
|
return parsed.toString();
|
|
}
|
|
|
|
export {
|
|
EXPECTED_STANDALONE_ARCHIVE_NAMES,
|
|
verifyReleaseBaseUrl,
|
|
verifyReleaseDirectory,
|
|
};
|