Files
qwen-code/scripts/managed-runtime-release.js

228 lines
6.7 KiB
JavaScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import os from 'node:os';
import fs from 'node:fs';
import path from 'node:path';
import { Readable, Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { valid } from 'semver';
import { isMainModule, parseArgs, sha256File } from './release-script-utils.js';
export async function stageManagedRuntimeRelease({ jar, version, outDir }) {
if (valid(version) !== version)
throw new Error('An exact CLI release version is required.');
const input = fs.openSync(jar, 'r');
try {
const header = Buffer.alloc(4);
fs.readSync(input, header, 0, header.length, 0);
if (header.toString('hex') !== '504b0304')
throw new Error('Expected a packaged server JAR.');
} finally {
fs.closeSync(input);
}
const name = `qwen-managed-agent-server-${version}.jar`;
fs.mkdirSync(outDir, { recursive: true });
const destination = path.join(outDir, name);
fs.copyFileSync(jar, destination);
const sha256 = await sha256File(destination);
fs.writeFileSync(`${destination}.sha256`, `${sha256} ${name}\n`);
return { name, sha256 };
}
export async function checkManagedRuntimeAsset(url, file, fetchImpl = fetch) {
const parsed = new URL(url);
if (parsed.protocol !== 'https:' || parsed.username || parsed.password) {
throw new Error(
'Release artifact verification requires a public HTTPS URL.',
);
}
const response = await fetchImpl(url, {
signal: AbortSignal.timeout(300_000),
});
if (response.status === 404) {
await response.body?.cancel();
return 'missing';
}
if (!response.ok || !response.body) {
await response.body?.cancel();
throw new Error(
`Cannot verify existing release artifact: HTTP ${response.status}.`,
);
}
const size = fs.statSync(file).size;
const digest = createHash('sha256');
let received = 0;
await pipeline(
Readable.fromWeb(response.body),
new Transform({
transform(chunk, _encoding, callback) {
received += chunk.length;
if (received > size) {
callback(
new Error(
'Existing release artifact has different bytes; refusing overwrite.',
),
);
return;
}
callback(null, chunk);
},
}),
digest,
);
if (received !== size || digest.digest('hex') !== (await sha256File(file))) {
throw new Error(
'Existing release artifact has different bytes; refusing overwrite.',
);
}
return 'present';
}
export async function checkManagedRuntimeOssAsset(
url,
file,
config,
timeout = 300_000,
) {
const parsed = new URL(url);
if (
parsed.protocol !== 'oss:' ||
!parsed.hostname ||
parsed.username ||
parsed.password ||
parsed.port ||
parsed.pathname.length <= 1 ||
parsed.search ||
parsed.hash
) {
throw new Error(
'Release artifact verification requires an OSS object URL.',
);
}
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'managed-oss-'));
const download = path.join(temporary, 'artifact');
try {
const output = fs.openSync(download, 'w');
let result;
try {
// Unlike cp's HEAD precheck, cat uses GET and preserves OSS error codes.
result = spawnSync('ossutil', ['cat', url, '-c', path.resolve(config)], {
stdio: ['ignore', output, 'pipe'],
timeout,
killSignal: 'SIGKILL',
maxBuffer: 64 * 1024,
});
} finally {
fs.closeSync(output);
}
if (result.error || result.status !== 0) {
if (
!result.error &&
result.status === 1 &&
fs.statSync(download).size <= 16 * 1024 &&
/^Error: oss: service returned error: StatusCode=404, ErrorCode=NoSuchKey, ErrorMessage="[^\r\n]*", RequestId=[^\r\n]+\r?\n?$/.test(
fs.readFileSync(download, 'utf8'),
)
) {
return 'missing';
}
throw new Error(
'Cannot verify existing OSS artifact; refusing publication.',
);
}
const size = fs.statSync(file).size;
// cat appends a newline, then the 1.7.19 command wrapper prints elapsed time.
const trailer = Buffer.alloc(64);
const input = fs.openSync(download, 'r');
let length;
try {
length = fs.readSync(input, trailer, 0, trailer.length, size);
} finally {
fs.closeSync(input);
}
if (
fs.statSync(download).size !== size + length ||
!/^\n\n\d+\.\d{6}\(s\) elapsed\n$/.test(
trailer.toString('utf8', 0, length),
)
) {
throw new Error(
'Existing release artifact has different bytes; refusing overwrite.',
);
}
fs.truncateSync(download, size);
if ((await sha256File(download)) !== (await sha256File(file))) {
throw new Error(
'Existing release artifact has different bytes; refusing overwrite.',
);
}
return 'present';
} finally {
fs.rmSync(temporary, { recursive: true, force: true });
}
}
if (isMainModule(import.meta.url)) {
try {
const args = parseArgs(process.argv.slice(2), {
'--jar': { key: 'jar', type: 'value' },
'--version': { key: 'version', type: 'value' },
'--out-dir': { key: 'outDir', type: 'value' },
'--check-url': { key: 'url', type: 'value' },
'--check-oss': { key: 'oss', type: 'value' },
'--config': { key: 'config', type: 'value' },
'--file': { key: 'file', type: 'value' },
});
if (args.help) {
console.log(
'Stage: --jar PATH --version VERSION --out-dir DIR\nVerify without replacing: --check-url HTTPS_URL --file PATH (prints present or missing).\nAuthenticated OSS: --check-oss OSS_URL --config PATH --file PATH.',
);
} else if (
args.url &&
!args.oss &&
!args.config &&
args.file &&
!args.jar &&
!args.version &&
!args.outDir
) {
console.log(await checkManagedRuntimeAsset(args.url, args.file));
} else if (
args.oss &&
args.config &&
args.file &&
!args.url &&
!args.jar &&
!args.version &&
!args.outDir
) {
console.log(
await checkManagedRuntimeOssAsset(args.oss, args.file, args.config),
);
} else if (
args.jar &&
args.version &&
args.outDir &&
!args.url &&
!args.oss &&
!args.config &&
!args.file
) {
console.log(JSON.stringify(await stageManagedRuntimeRelease(args)));
} else {
throw new Error(
'Provide one complete stage or verification argument set; see --help.',
);
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}