mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
228 lines
6.7 KiB
JavaScript
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;
|
|
}
|
|
}
|