mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-01 20:22:33 +00:00
* feat(managed-agent): ship a configuration template in the reference distribution The reference archive carried no application.yml, so the Runtime credential key ring — which cannot be expressed through QWEN_MANAGED_AGENT_* variables because relaxed binding normalises map keys — forced every rotating deployment to create a file and pass --spring.config.additional-location by hand. Ship config/application.example.yml and have both launchers load config/application.yml from the bundle root when it exists, leaving an operator-supplied SPRING_CONFIG_ADDITIONAL_LOCATION in effect. * feat(managed-agent): add a read-only --check preflight The reference distribution listed its prerequisites in prose, but the server evaluated them only at startup or on the first Turn: MySQL grants, the credential key ring, auth/bind combinations, Harness reachability, broker credentials, directories and ports. Intercept --check before Spring starts and report each input as PASS/WARN/FAIL/SKIP, exiting 1 on any failure. The command binds the same configuration as the server, probes /capabilities, and compares the Flyway version with the release manifest when one is present. * feat(managed-agent): ship systemd units for the single-host topology The server reference required a supervisor that leaves Runtime workers running across a Broker restart, but the reference distribution shipped no unit that did so. Add control-plane and Hosted Harness units, environment templates, and an install guide. The control-plane unit uses KillMode=process and StateDirectory; the Harness uses KillMode=mixed. Include the templates in the archive and guard their required directives with a structural test. * feat(managed-agent): bootstrap the shared deployment credentials Every deployment had to mint a Harness token, a capability digest, a broker token and a 32-byte credential key, then keep the token and digest identical across two files and the key ring in a third. Add bin/qwen-managed-agent-bootstrap, which generates all four values and writes harness.env, server.env and application.yml with mode 0600, refuses to overwrite without --force, and prints paths only. Ship it in the archive. * fix(managed-agent): pass the distribution config through the environment The shared launcher injected --spring.config.additional-location before the operator's arguments, so the operator-recovery entry, which dispatches on args[0] and the exact argument count, could no longer run inspect, prepare or complete. Export SPRING_CONFIG_ADDITIONAL_LOCATION instead and leave the argument vector untouched for both launchers. Also make the template's active key pair use the mapped placeholders, so a copied example still honours the documented environment override. * docs(managed-agent): match the preflight design to the launcher fix The preflight design still said the launcher adds a command-line config option. The launcher now exports SPRING_CONFIG_ADDITIONAL_LOCATION, so update both language versions. * test(managed-agent): isolate the launcher test from an ambient config variable The test spread process.env, so a machine that already exported SPRING_CONFIG_ADDITIONAL_LOCATION changed the launcher's behaviour and failed the new assertions. Remove the variable from the base environment; the explicit operator-override case still sets it. * fix(managed-agent): align the preflight with startup validation The preflight could report PASS for a database without the performance_schema grant, ignored command-line overrides, dropped earlier external YAML files when several were listed, and could not find the release manifest in the repackaged executable JAR. It also hand-copied auth and Workspace tool-path rules that disagreed with the startup validators. Delegate auth to BrokerSecurity, mirror the Workspace tool-path admission, fail on a missing grant, load command-line arguments and every external YAML with unique property sources, and resolve the manifest from the outer JAR. * fix(managed-agent): reject the static provisioner for Workspace files validateWorkspaceFiles refuses Workspace files on the static provisioner, but tool-path reported only WARN. Make it FAIL and keep the not-evaluated warning for other provisioners. * fix(managed-agent): close the remaining preflight review gaps Resolve relative class-path entries before locating the manifest, bind the full server and spring.mvc properties before the auth guard, and require durable local-process execution plus verified recovery for a Workspace creation root. * fix(managed-agent): wire the Harness Broker flags and keep EnvironmentFile semantics Expose QWEN_HOSTED_HARNESS_BROKER_ARGS in the Harness unit so the tool path can pass the Broker URL and token, and run the preflight through systemd-run so it reads the same EnvironmentFile as the service. * fix(managed-agent): harden the bootstrap entry point and writes Resolve symlinked entry points before running main, quote the configuration path for shell sourcing and reject paths that cannot be quoted, and hold an exclusive lock across the existence check and the three writes. * fix(managed-agent): run the startup validation in the preflight The preflight bound ManagedAgentProperties with Binder, which never runs the @PostConstruct startup validation. Configurations that the server rejects at startup -- an approval timeout below the ten-second floor, an unknown bound-session isolation class, SSH enabled without Workspace files -- printed PASS and exited 0. ManagedAgentProperties now exposes the validation as a callable method and the preflight reports its verdict as a `validation` row. * fix(ci): keep the systemd READMEs on the full profile The packaging test names distribution/systemd/README.md and its Chinese counterpart, so classify-profile.test.mjs requires them in TEST_READ_MARKDOWN. Without the entry the helper battery failed with both files listed as unregistered lightweight Markdown, reddening Lint & Static for the systemd stack. * fix(managed-agent): boot the systemd template and authenticate the preflight The shipped server.env.example enabled the Hosted Harness without the Managed Session Store, which startup refuses, so following the README restart-looped. The template now enables the store and sets its base URL and the Runtime workspace ID. The preflight probed /capabilities without the Harness token, so --check printed FAIL 401 against a healthy authenticated Harness. It now sends the configured bearer token and reports a rejected token separately from an unreachable Harness. The README installs application.yml as 0600 owned by the service account instead of 0640 root:group: the control plane reads it as that account and the preflight rejects group-readable secret files. It also documents the ProtectHome constraint on the CLI location and the port-advance case. * fix(managed-agent): close the systemd review gaps The Harness unit expands QWEN_HOSTED_HARNESS_BROKER_ARGS into argv, so the broker token is readable through /proc/<pid>/cmdline. Say so in the env template and the unit comment, and keep that path to single-tenant hosts. KillMode=process means `systemctl stop` signals only the main process: the unit reports inactive while Runtime workers keep the state directory and Workspace. Document the symptom and the reap command in both READMEs and the deployment guide. Derive the archive's required systemd entries from systemdFiles, so a dropped env template or Chinese README fails packaging instead of only breaking install step 3. * fix(managed-agent): point the generated store at the server and reject commas The bootstrap generator's server.env still omitted the Managed Session Store, so the generated deployment failed the preflight's validation row. It now emits the store enablement, its base URL and the Runtime workspace ID. The store URL is this Java control plane's listener, not the Harness: the template and the generator now use http://127.0.0.1:8080, matching the server reference. The systemd READMEs name both variables so a Harness port change does not leave the store behind. A comma in --directory splits SPRING_CONFIG_ADDITIONAL_LOCATION into two locations; the guard now rejects it with the quote and newline cases, and tests pin the generator's store lines and the 8080 value.
525 lines
16 KiB
JavaScript
525 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
import {
|
|
chmod,
|
|
copyFile,
|
|
mkdir,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
stat,
|
|
utimes,
|
|
writeFile,
|
|
} from 'node:fs/promises';
|
|
import { execFileSync } from 'node:child_process';
|
|
import path from 'node:path';
|
|
import { pipeline } from 'node:stream/promises';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { parseArgs } from 'node:util';
|
|
import { createGzip } from 'node:zlib';
|
|
|
|
import {
|
|
CONTRACT_DOCUMENT,
|
|
documentVersion,
|
|
} from './contract-version-utils.js';
|
|
import {
|
|
LOCATIONS,
|
|
migrationFiles,
|
|
migrationVersion,
|
|
} from './flyway-migration-utils.js';
|
|
|
|
const DISTRIBUTION_NAME = 'qwen-managed-agent-reference';
|
|
const PRERELEASE_IDENTIFIER =
|
|
'(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)';
|
|
const VERSION_PATTERN = new RegExp(
|
|
`^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)-${PRERELEASE_IDENTIFIER}(?:\\.${PRERELEASE_IDENTIFIER})*$`,
|
|
);
|
|
const REVISION_PATTERN = /^[0-9a-f]{40}$/;
|
|
|
|
async function sha256(file) {
|
|
return createHash('sha256')
|
|
.update(await readFile(file))
|
|
.digest('hex');
|
|
}
|
|
|
|
function match(text, pattern, label) {
|
|
const result = text.match(pattern);
|
|
if (!result) throw new Error(`Cannot read ${label}.`);
|
|
return result[1];
|
|
}
|
|
|
|
function uniqueMatch(text, pattern, label) {
|
|
const results = [...text.matchAll(pattern)];
|
|
if (results.length !== 1) {
|
|
throw new Error(`Cannot uniquely read ${label}.`);
|
|
}
|
|
return results[0][1];
|
|
}
|
|
|
|
function withoutComments(text) {
|
|
let output = '';
|
|
let block = false;
|
|
for (let index = 0; index < text.length; index += 1) {
|
|
if (block) {
|
|
if (text.startsWith('*/', index)) {
|
|
block = false;
|
|
index += 1;
|
|
} else if (text[index] === '\n') {
|
|
output += '\n';
|
|
}
|
|
} else if (text.startsWith('/*', index)) {
|
|
block = true;
|
|
index += 1;
|
|
} else if (text.startsWith('//', index)) {
|
|
index = text.indexOf('\n', index) - 1;
|
|
if (index < -1) break;
|
|
} else {
|
|
output += text[index];
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
async function pomVersion(file, artifactId) {
|
|
const text = await readFile(file, 'utf8');
|
|
return match(
|
|
text,
|
|
new RegExp(
|
|
`<artifactId>${artifactId}</artifactId>\\s*<version>([^<]+)</version>`,
|
|
),
|
|
`${artifactId} version`,
|
|
);
|
|
}
|
|
|
|
function compareMigrationVersions(left, right) {
|
|
const a = left.split('.').map(Number);
|
|
const b = right.split('.').map(Number);
|
|
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
if (difference !== 0) return difference;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function migrationRange(root) {
|
|
const versions = LOCATIONS.flatMap(({ dir, suffix }) =>
|
|
migrationFiles(
|
|
path.join(root, 'packages/managed-agent-server', ...dir),
|
|
suffix,
|
|
).map((file) => migrationVersion(path.basename(file))),
|
|
);
|
|
if (versions.length === 0) throw new Error('No Flyway migrations found.');
|
|
versions.sort(compareMigrationVersions);
|
|
return { minimum: versions[0], latest: versions.at(-1) };
|
|
}
|
|
|
|
async function listFiles(directory, prefix = '') {
|
|
const files = [];
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
const relative = path.posix.join(prefix, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(
|
|
...(await listFiles(path.join(directory, entry.name), relative)),
|
|
);
|
|
} else if (entry.isFile()) {
|
|
files.push(relative);
|
|
}
|
|
}
|
|
return files.sort();
|
|
}
|
|
|
|
async function writeChecksums(directory, files, destination) {
|
|
const lines = [];
|
|
for (const relative of files) {
|
|
lines.push(`${await sha256(path.join(directory, relative))} ${relative}`);
|
|
}
|
|
await writeFile(destination, `${lines.join('\n')}\n`);
|
|
}
|
|
|
|
async function verifyChecksums(directory, checksumFile) {
|
|
const lines = (await readFile(checksumFile, 'utf8')).trim().split('\n');
|
|
for (const line of lines) {
|
|
const found = line.match(/^([0-9a-f]{64}) {2}(.+)$/);
|
|
if (!found) throw new Error(`Invalid checksum line: ${line}`);
|
|
const actual = await sha256(path.join(directory, found[2]));
|
|
if (actual !== found[1]) {
|
|
throw new Error(`Checksum mismatch for ${found[2]}.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function copy(root, stage, source, destination) {
|
|
const target = path.join(stage, destination);
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await copyFile(path.join(root, source), target);
|
|
return target;
|
|
}
|
|
|
|
function launcher(jar) {
|
|
return `#!/usr/bin/env sh
|
|
set -eu
|
|
base="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
|
config="$base/config/application.yml"
|
|
if [ -f "$config" ] && [ -z "\${SPRING_CONFIG_ADDITIONAL_LOCATION:-}" ]; then
|
|
SPRING_CONFIG_ADDITIONAL_LOCATION="file:\${config}"
|
|
export SPRING_CONFIG_ADDITIONAL_LOCATION
|
|
fi
|
|
exec java -jar "$base/lib/${jar}" "$@"
|
|
`;
|
|
}
|
|
|
|
async function createDeterministicArchive(output, rootName, stage, archive) {
|
|
const files = await listFiles(stage);
|
|
const timestamp = new Date(0);
|
|
for (const relative of files) {
|
|
const file = path.join(stage, relative);
|
|
await chmod(file, relative.startsWith('bin/') ? 0o755 : 0o644);
|
|
await utimes(file, timestamp, timestamp);
|
|
}
|
|
|
|
const tarVersion = execFileSync('tar', ['--version'], { encoding: 'utf8' });
|
|
const ownershipArguments = tarVersion.toLowerCase().includes('bsdtar')
|
|
? ['--uid', '0', '--gid', '0', '--uname', 'root', '--gname', 'root']
|
|
: ['--owner=0', '--group=0'];
|
|
const tarFile = `${archive}.tmp`;
|
|
try {
|
|
execFileSync(
|
|
'tar',
|
|
[
|
|
'-c',
|
|
'--format=ustar',
|
|
...ownershipArguments,
|
|
'-f',
|
|
tarFile,
|
|
'-C',
|
|
output,
|
|
...files.map((file) => path.posix.join(rootName, file)),
|
|
],
|
|
{ stdio: 'inherit' },
|
|
);
|
|
await pipeline(
|
|
createReadStream(tarFile),
|
|
createGzip({ level: 9, mtime: 0 }),
|
|
createWriteStream(archive),
|
|
);
|
|
} finally {
|
|
await rm(tarFile, { force: true });
|
|
}
|
|
}
|
|
|
|
export async function createReferenceDistribution({
|
|
repoRoot,
|
|
outputDirectory,
|
|
version,
|
|
sourceRevision,
|
|
}) {
|
|
if (!VERSION_PATTERN.test(version)) {
|
|
throw new Error(
|
|
'Version must be an experimental SemVer with a prerelease.',
|
|
);
|
|
}
|
|
if (!REVISION_PATTERN.test(sourceRevision)) {
|
|
throw new Error(
|
|
'Source revision must be a lowercase 40-character Git SHA.',
|
|
);
|
|
}
|
|
|
|
const root = path.resolve(repoRoot);
|
|
const output = path.resolve(outputDirectory);
|
|
const rootName = `${DISTRIBUTION_NAME}-${version}`;
|
|
const stage = path.join(output, rootName);
|
|
const archive = path.join(output, `${rootName}.tar.gz`);
|
|
const detachedManifest = path.join(output, `${rootName}.manifest.json`);
|
|
const detachedChecksums = path.join(output, 'SHA256SUMS.txt');
|
|
|
|
await mkdir(output, { recursive: true });
|
|
await Promise.all([
|
|
rm(stage, { recursive: true, force: true }),
|
|
rm(archive, { force: true }),
|
|
rm(detachedManifest, { force: true }),
|
|
rm(detachedChecksums, { force: true }),
|
|
]);
|
|
await mkdir(stage, { recursive: true });
|
|
|
|
const serverPom = path.join(root, 'packages/managed-agent-server/pom.xml');
|
|
const serverVersion = await pomVersion(
|
|
serverPom,
|
|
'qwen-managed-agent-server',
|
|
);
|
|
const packageJson = JSON.parse(
|
|
await readFile(path.join(root, 'package.json')),
|
|
);
|
|
const publicApi = documentVersion(
|
|
await readFile(path.join(root, CONTRACT_DOCUMENT), 'utf8'),
|
|
);
|
|
if (publicApi.error) {
|
|
throw new Error(`${CONTRACT_DOCUMENT} ${publicApi.error}.`);
|
|
}
|
|
const capabilities = await readFile(
|
|
path.join(root, 'packages/cli/src/serve/capabilities.ts'),
|
|
'utf8',
|
|
);
|
|
const brokerProvider = await readFile(
|
|
path.join(
|
|
root,
|
|
'packages/cli/src/serve/broker-managed-runtime-provider.ts',
|
|
),
|
|
'utf8',
|
|
);
|
|
const applicationConfiguration = await readFile(
|
|
path.join(
|
|
root,
|
|
'packages/managed-agent-server/src/main/resources/application.yml',
|
|
),
|
|
'utf8',
|
|
);
|
|
const hostedHarnessContract = await readFile(
|
|
path.join(root, 'packages/cli/src/serve/hosted-harness-contract.ts'),
|
|
'utf8',
|
|
);
|
|
const serverPomText = await readFile(serverPom, 'utf8');
|
|
const schema = migrationRange(root);
|
|
const publicShellDefault = uniqueMatch(
|
|
applicationConfiguration,
|
|
/^[ \t]*workspace-shell-enabled:[ \t]*'\$\{QWEN_MANAGED_AGENT_WORKSPACE_SHELL_ENABLED:([^}\r\n]+)\}'[ \t]*$/gm,
|
|
'public Shell default',
|
|
);
|
|
if (publicShellDefault !== 'false') {
|
|
throw new Error('Public Shell must default to false.');
|
|
}
|
|
const lifecycleV2ProfilesDefault = uniqueMatch(
|
|
withoutComments(hostedHarnessContract),
|
|
/^[ \t]*process\.env\[HOSTED_LIFECYCLE_V2_PROFILES_ENV\][ \t]*\?\?[ \t]*'([^'\r\n]*)'[ \t]*$/gm,
|
|
'Hosted lifecycle v2 profile default',
|
|
);
|
|
if (lifecycleV2ProfilesDefault !== '') {
|
|
throw new Error('Hosted lifecycle v2 profiles must default to empty.');
|
|
}
|
|
|
|
const serverJar = `qwen-managed-agent-server-${serverVersion}.jar`;
|
|
const recoveryJar = `qwen-managed-agent-server-${serverVersion}-operator-recovery.jar`;
|
|
const componentSources = [
|
|
{
|
|
name: 'qwen-managed-agent-server',
|
|
version: serverVersion,
|
|
source: `packages/managed-agent-server/target/${serverJar}`,
|
|
destination: `lib/${serverJar}`,
|
|
role: 'control-plane',
|
|
},
|
|
{
|
|
name: 'qwen-managed-agent-operator-recovery',
|
|
version: serverVersion,
|
|
source: `packages/managed-agent-server/target/${recoveryJar}`,
|
|
destination: `lib/${recoveryJar}`,
|
|
role: 'operator-recovery',
|
|
},
|
|
];
|
|
|
|
for (const component of componentSources) {
|
|
await copy(root, stage, component.source, component.destination);
|
|
}
|
|
await copy(
|
|
root,
|
|
stage,
|
|
'packages/managed-agent-server/distribution/config/application.example.yml',
|
|
'config/application.example.yml',
|
|
);
|
|
const systemdFiles = [
|
|
'qwen-hosted-harness.service',
|
|
'qwen-managed-agent-server.service',
|
|
'harness.env.example',
|
|
'server.env.example',
|
|
'README.md',
|
|
'README.zh-CN.md',
|
|
];
|
|
for (const file of systemdFiles) {
|
|
await copy(
|
|
root,
|
|
stage,
|
|
`packages/managed-agent-server/distribution/systemd/${file}`,
|
|
`systemd/${file}`,
|
|
);
|
|
}
|
|
for (const file of [
|
|
'qwen-managed-agent-bootstrap',
|
|
'qwen-managed-agent-bootstrap.mjs',
|
|
]) {
|
|
await copy(
|
|
root,
|
|
stage,
|
|
`packages/managed-agent-server/distribution/bin/${file}`,
|
|
`bin/${file}`,
|
|
);
|
|
}
|
|
const documentation = [
|
|
['docs/users/managed-agent-reference-deployment.md', 'docs/deployment.md'],
|
|
[
|
|
'docs/users/managed-agent-reference-deployment.zh-CN.md',
|
|
'docs/deployment.zh-CN.md',
|
|
],
|
|
['packages/managed-agent-server/README.md', 'docs/server-reference.md'],
|
|
[
|
|
'docs/users/hosted-workspace-recovery.md',
|
|
'docs/hosted-workspace-recovery.md',
|
|
],
|
|
[
|
|
'docs/users/hosted-workspace-recovery.zh-CN.md',
|
|
'docs/hosted-workspace-recovery.zh-CN.md',
|
|
],
|
|
['LICENSE', 'LICENSE'],
|
|
];
|
|
for (const [source, destination] of documentation) {
|
|
await copy(root, stage, source, destination);
|
|
}
|
|
|
|
const serverLauncher = path.join(stage, 'bin/qwen-managed-agent-server');
|
|
const recoveryLauncher = path.join(
|
|
stage,
|
|
'bin/qwen-managed-agent-operator-recovery',
|
|
);
|
|
await mkdir(path.dirname(serverLauncher), { recursive: true });
|
|
await Promise.all([
|
|
writeFile(serverLauncher, launcher(serverJar)),
|
|
writeFile(recoveryLauncher, launcher(recoveryJar)),
|
|
]);
|
|
await Promise.all([
|
|
chmod(serverLauncher, 0o755),
|
|
chmod(recoveryLauncher, 0o755),
|
|
]);
|
|
|
|
const components = [];
|
|
for (const component of componentSources) {
|
|
const file = path.join(stage, component.destination);
|
|
components.push({
|
|
name: component.name,
|
|
version: component.version,
|
|
role: component.role,
|
|
path: component.destination,
|
|
size: (await stat(file)).size,
|
|
sha256: await sha256(file),
|
|
});
|
|
}
|
|
|
|
const manifest = {
|
|
schemaVersion: 1,
|
|
name: DISTRIBUTION_NAME,
|
|
version,
|
|
status: 'experimental',
|
|
source: {
|
|
repository: 'https://github.com/QwenLM/qwen-code',
|
|
revision: sourceRevision,
|
|
},
|
|
components,
|
|
compatibility: {
|
|
qwenCodeVersion: packageJson.version,
|
|
hostedHarnessProfile: 'hosted-harness',
|
|
serveProtocolVersion: match(
|
|
capabilities,
|
|
/SERVE_PROTOCOL_VERSION = '([^']+)'/,
|
|
'Serve protocol version',
|
|
),
|
|
managedRuntimeBrokerProtocolVersion: Number(
|
|
match(
|
|
brokerProvider,
|
|
/MANAGED_RUNTIME_BROKER_PROTOCOL_VERSION = (\d+)/,
|
|
'Runtime Broker protocol version',
|
|
),
|
|
),
|
|
managedAgentPublicApiVersion: publicApi.version,
|
|
java: `>=${match(
|
|
serverPomText,
|
|
/<java\.version>([^<]+)<\/java\.version>/,
|
|
'Java version',
|
|
)}`,
|
|
node: packageJson.engines.node,
|
|
databaseSchema: {
|
|
migrationTool: 'Flyway',
|
|
minimumMigrationVersion: schema.minimum,
|
|
latestMigrationVersion: schema.latest,
|
|
},
|
|
},
|
|
referenceTopology: {
|
|
operatingSystem: 'Linux',
|
|
javaControlPlanes: 1,
|
|
hostTrust: 'trusted-single-host',
|
|
runtimeBroker: 'embedded',
|
|
database: 'MySQL 8',
|
|
},
|
|
defaults: {
|
|
publicShellEnabled: publicShellDefault === 'true',
|
|
v2CapabilityAdvertisementEnabled: lifecycleV2ProfilesDefault !== '',
|
|
},
|
|
documentation: {
|
|
deployment: 'docs/deployment.md',
|
|
deploymentChinese: 'docs/deployment.zh-CN.md',
|
|
serverReference: 'docs/server-reference.md',
|
|
operatorRecovery: 'docs/hosted-workspace-recovery.md',
|
|
},
|
|
};
|
|
const manifestFile = path.join(stage, 'manifest.json');
|
|
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
const payload = await listFiles(stage);
|
|
await writeChecksums(stage, payload, path.join(stage, 'SHA256SUMS'));
|
|
await verifyChecksums(stage, path.join(stage, 'SHA256SUMS'));
|
|
await copyFile(manifestFile, detachedManifest);
|
|
|
|
await createDeterministicArchive(output, rootName, stage, archive);
|
|
const entries = execFileSync('tar', ['-tzf', archive], { encoding: 'utf8' });
|
|
for (const required of [
|
|
`${rootName}/manifest.json`,
|
|
`${rootName}/SHA256SUMS`,
|
|
`${rootName}/bin/qwen-managed-agent-server`,
|
|
`${rootName}/bin/qwen-managed-agent-operator-recovery`,
|
|
`${rootName}/bin/qwen-managed-agent-bootstrap`,
|
|
`${rootName}/config/application.example.yml`,
|
|
...systemdFiles.map((file) => `${rootName}/systemd/${file}`),
|
|
]) {
|
|
if (!entries.split('\n').includes(required)) {
|
|
throw new Error(`Archive is missing ${required}.`);
|
|
}
|
|
}
|
|
|
|
await writeChecksums(
|
|
output,
|
|
[path.basename(archive), path.basename(detachedManifest)],
|
|
detachedChecksums,
|
|
);
|
|
await verifyChecksums(output, detachedChecksums);
|
|
await rm(stage, { recursive: true, force: true });
|
|
|
|
return { archive, detachedManifest, detachedChecksums, manifest };
|
|
}
|
|
|
|
async function main() {
|
|
const { values } = parseArgs({
|
|
options: {
|
|
version: { type: 'string' },
|
|
'source-revision': { type: 'string' },
|
|
output: { type: 'string' },
|
|
},
|
|
});
|
|
if (!values.version || !values['source-revision'] || !values.output) {
|
|
throw new Error(
|
|
'Usage: package-managed-agent-reference.mjs --version <experimental-semver> --source-revision <sha> --output <directory>',
|
|
);
|
|
}
|
|
const result = await createReferenceDistribution({
|
|
repoRoot: process.cwd(),
|
|
outputDirectory: values.output,
|
|
version: values.version,
|
|
sourceRevision: values['source-revision'],
|
|
});
|
|
console.log(result.archive);
|
|
console.log(result.detachedManifest);
|
|
console.log(result.detachedChecksums);
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|