mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* docs(managed-agent): generate the server configuration reference from its metadata
The server has about 180 settings, 84 of them mapped to QWEN_MANAGED_AGENT_*
variables, and no list of them with their variables, defaults and limits:
operators read a 2000-line developer README for them.
- The server module runs spring-boot-configuration-processor once more,
over ManagedAgentProperties only (the two Druid beans share a prefix,
which the processor refuses), and writes the configuration metadata.
The compiled classes are unchanged. runtime-broker.ssh is marked
@NestedConfigurationProperty, and STRICT_HARNESS_KEYS @Getter(NONE), so
the metadata lists the one and not the other.
- configuration-descriptions.json holds an English and a Chinese
description for each setting and section.
- scripts/generate-managed-agent-configuration-reference.mjs writes
docs/users/managed-agent-configuration-reference{,.zh-CN}.md from the
metadata, the packaged application.yml (variables and defaults), the
${qwen.managed-agent.*} placeholders in the main sources (settings read
outside the properties class) and the descriptions. --check fails on a
setting without a description, a description of no setting, or a stale
page; the reference workflow runs it after the Java build.
- The archive ships the pages as docs/configuration{,.zh-CN}.md, and the
deployment guide and the README link them.
* fix(managed-agent): link the shipped documents by their archive names
The archive renames each document it ships, so a link written against
the repository, such as the configuration reference's language switch or
the guide's link to it, named a file the archive does not have. The
packager now points every link between shipped documents at the
document's name in the archive and leaves every other link as written.
* build(managed-agent): take the configuration processor's version from the BOM
spring-boot.version is defined nowhere in this build, so the processor
path's <version> only resolved because the compiler plugin falls back to
dependency management. Say so by naming no version.
* test(managed-agent): name no tracked Markdown in a link fixture
classify-profile.test.mjs requires every Markdown file a test names to be
in TEST_READ_MARKDOWN, and the kept-link fixture's docs/runtime-broker.md
matched packages/managed-agent-server/docs/runtime-broker.md, which no
test reads. The fixture only needs a link to a file the archive does not
ship, so it names one that does not exist.
719 lines
22 KiB
JavaScript
719 lines
22 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}$/;
|
|
const MAIN_SOURCE_ROOT = 'packages/managed-agent-server/server/src/main/java';
|
|
const PERSISTENCE = 'com.alibaba.qwen.code.managedagent.maintenance';
|
|
const PREFLIGHT = 'com.alibaba.qwen.code.managedagent.preflight';
|
|
const WORKSPACE_RUNTIME = 'com.alibaba.qwen.code.managedagent.maintenance';
|
|
|
|
// The offline maintenance entry points, each a main class in the server jar.
|
|
// bin/qwen-managed-agent-admin runs them through Spring Boot's
|
|
// PropertiesLauncher, so the distribution ships no jar per command.
|
|
export const ADMIN_COMMANDS = Object.freeze([
|
|
[
|
|
'storage-registration',
|
|
`${PERSISTENCE}.WorkspaceStorageRegistrationMain`,
|
|
'Register, fence, restore or inspect a native Workspace storage root.',
|
|
],
|
|
[
|
|
'workspace-recovery',
|
|
`${PERSISTENCE}.WorkspaceRecoveryMain`,
|
|
'Capture, verify or inspect a Workspace recovery bundle.',
|
|
],
|
|
[
|
|
'workspace-migration',
|
|
`${WORKSPACE_RUNTIME}.WorkspaceMigrationMain`,
|
|
'Retire, prepare, promote, abort or inspect a Workspace migration.',
|
|
],
|
|
[
|
|
'maintenance-recovery',
|
|
`${PERSISTENCE}.WorkspaceMaintenanceRecoveryMain`,
|
|
'Inspect or reconcile a Workspace held by maintenance.',
|
|
],
|
|
[
|
|
'operator-recovery',
|
|
`${WORKSPACE_RUNTIME}.WorkspaceRecoveryCommand`,
|
|
'Prepare, complete or inspect a Hosted Shell lease recovery.',
|
|
],
|
|
[
|
|
'csi-registration',
|
|
`${PERSISTENCE}.WorkspaceCsiRegistrationMain`,
|
|
'Register or inspect a reviewed CSI Workspace registration.',
|
|
],
|
|
[
|
|
'csi-protection',
|
|
`${WORKSPACE_RUNTIME}.WorkspaceCsiProtectionMain`,
|
|
'Render the protection policy for a CSI registration.',
|
|
],
|
|
[
|
|
'csi-session',
|
|
`${PERSISTENCE}.WorkspaceCsiSessionMain`,
|
|
'Create a reviewed CSI Session offline.',
|
|
],
|
|
[
|
|
'csi-runtime',
|
|
`${PERSISTENCE}.WorkspaceCsiRuntimeMain`,
|
|
'Serve, prompt or retire one reviewed CSI Session Runtime.',
|
|
],
|
|
[
|
|
'csi-retirement',
|
|
`${PERSISTENCE}.WorkspaceCsiRetirementMain`,
|
|
'Inspect or begin a CSI Workspace retirement.',
|
|
],
|
|
[
|
|
'csi-worker-ack',
|
|
`${PERSISTENCE}.WorkspaceCsiWorkerAckMain`,
|
|
'Acknowledge or inspect a CSI worker publication.',
|
|
],
|
|
[
|
|
'resident-retirement',
|
|
`${PERSISTENCE}.ResidentWorkspaceRetirementMain`,
|
|
'Inspect, begin or delete the pod of a resident Workspace retirement.',
|
|
],
|
|
[
|
|
'resolved-configuration',
|
|
`${PREFLIGHT}.ResolvedConfiguration`,
|
|
'Print the Runtime state directory and auth mode the configuration resolves to.',
|
|
],
|
|
]);
|
|
|
|
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]}.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// What the archive ships from the repository, and under which name.
|
|
export const DOCUMENTATION = Object.freeze([
|
|
['docs/users/managed-agent-reference-deployment.md', 'docs/deployment.md'],
|
|
[
|
|
'docs/users/managed-agent-reference-deployment.zh-CN.md',
|
|
'docs/deployment.zh-CN.md',
|
|
],
|
|
[
|
|
'docs/users/managed-agent-configuration-reference.md',
|
|
'docs/configuration.md',
|
|
],
|
|
[
|
|
'docs/users/managed-agent-configuration-reference.zh-CN.md',
|
|
'docs/configuration.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'],
|
|
]);
|
|
|
|
/**
|
|
* `text`, a document shipped from `source`, with each link to another shipped
|
|
* document pointed at the name the archive gives it: the archive renames the
|
|
* documents, so a link written against the repository would name a file the
|
|
* archive does not have. Every other link is left as written: a URL or an
|
|
* anchor names no shipped path, and an absolute path is the reader's own.
|
|
*/
|
|
export function shippedDocumentLinks(
|
|
text,
|
|
source,
|
|
documentation = DOCUMENTATION,
|
|
) {
|
|
const shipped = new Map(documentation);
|
|
const directory = path.posix.dirname(shipped.get(source));
|
|
return text.replace(
|
|
/\]\(([^()\s#]+)(#[^()\s]*)?\)/g,
|
|
(link, target, anchor = '') => {
|
|
// path.posix.join would read it as relative.
|
|
if (target.startsWith('/')) {
|
|
return link;
|
|
}
|
|
const renamed = shipped.get(
|
|
path.posix.join(path.posix.dirname(source), target),
|
|
);
|
|
return renamed === undefined
|
|
? link
|
|
: `](${path.posix.relative(directory, renamed)}${anchor})`;
|
|
},
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Every launcher reads config/application.yml the same way, so the
|
|
// operator's configuration cannot depend on which one they ran.
|
|
const LAUNCHER_PREAMBLE = `#!/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
|
|
`;
|
|
|
|
function launcher(jar) {
|
|
return `${LAUNCHER_PREAMBLE}exec java -jar "$base/lib/${jar}" "$@"
|
|
`;
|
|
}
|
|
|
|
function adminLauncher(jar) {
|
|
const width = Math.max(...ADMIN_COMMANDS.map(([name]) => name.length));
|
|
const usage = ADMIN_COMMANDS.map(
|
|
([name, , summary]) => ` ${name.padEnd(width)} ${summary}`,
|
|
);
|
|
const cases = ADMIN_COMMANDS.map(
|
|
([name, main]) => ` ${name}) main=${main} ;;`,
|
|
);
|
|
return `${LAUNCHER_PREAMBLE}usage() {
|
|
cat <<'USAGE'
|
|
Usage: qwen-managed-agent-admin <command> [arguments]
|
|
|
|
Offline maintenance commands. Run one without arguments for its usage.
|
|
|
|
${usage.join('\n')}
|
|
USAGE
|
|
}
|
|
case "\${1:-}" in
|
|
${cases.join('\n')}
|
|
''|help|-h|--help) usage; exit 0 ;;
|
|
*) echo "qwen-managed-agent-admin: unknown command: $1" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
exec java "-Dloader.main=$main" -cp "$base/lib/${jar}" \\
|
|
org.springframework.boot.loader.launch.PropertiesLauncher "$@"
|
|
`;
|
|
}
|
|
|
|
// A renamed or removed entry point fails packaging instead of shipping a
|
|
// command that cannot start.
|
|
async function verifyAdminCommands(root) {
|
|
for (const [name, main] of ADMIN_COMMANDS) {
|
|
const source = path.join(
|
|
root,
|
|
MAIN_SOURCE_ROOT,
|
|
`${main.replaceAll('.', '/')}.java`,
|
|
);
|
|
let text;
|
|
try {
|
|
text = await readFile(source, 'utf8');
|
|
} catch {
|
|
throw new Error(`Admin command ${name} names a missing class ${main}.`);
|
|
}
|
|
if (!/public static void main\(String/.test(text)) {
|
|
throw new Error(`Admin command ${name}: ${main} has no main method.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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-parent',
|
|
);
|
|
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 brokerClient = await readFile(
|
|
path.join(root, 'packages/cli/src/serve/managed-runtime-broker-client.ts'),
|
|
'utf8',
|
|
);
|
|
const applicationConfiguration = await readFile(
|
|
path.join(
|
|
root,
|
|
'packages/managed-agent-server/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 !== 'hosted-workspace-shell/1') {
|
|
throw new Error(
|
|
'Hosted lifecycle v2 profiles must default to canonical Shell only.',
|
|
);
|
|
}
|
|
|
|
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/server/target/qwen-managed-agent-server-${serverVersion}-exec.jar`,
|
|
destination: `lib/${serverJar}`,
|
|
role: 'control-plane',
|
|
},
|
|
{
|
|
name: 'qwen-managed-agent-operator-recovery',
|
|
version: serverVersion,
|
|
source: `packages/managed-agent-server/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',
|
|
'qwen-managed-agent-upgrade',
|
|
'qwen-managed-agent-upgrade.mjs',
|
|
]) {
|
|
await copy(
|
|
root,
|
|
stage,
|
|
`packages/managed-agent-server/distribution/bin/${file}`,
|
|
`bin/${file}`,
|
|
);
|
|
}
|
|
for (const [source, destination] of DOCUMENTATION) {
|
|
if (!source.endsWith('.md')) {
|
|
await copy(root, stage, source, destination);
|
|
continue;
|
|
}
|
|
const target = path.join(stage, destination);
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await writeFile(
|
|
target,
|
|
shippedDocumentLinks(
|
|
await readFile(path.join(root, source), 'utf8'),
|
|
source,
|
|
),
|
|
);
|
|
}
|
|
|
|
const serverLauncher = path.join(stage, 'bin/qwen-managed-agent-server');
|
|
const recoveryLauncher = path.join(
|
|
stage,
|
|
'bin/qwen-managed-agent-operator-recovery',
|
|
);
|
|
const adminCommand = path.join(stage, 'bin/qwen-managed-agent-admin');
|
|
await verifyAdminCommands(root);
|
|
await mkdir(path.dirname(serverLauncher), { recursive: true });
|
|
await Promise.all([
|
|
writeFile(serverLauncher, launcher(serverJar)),
|
|
writeFile(recoveryLauncher, launcher(recoveryJar)),
|
|
writeFile(adminCommand, adminLauncher(serverJar)),
|
|
]);
|
|
await Promise.all([
|
|
chmod(serverLauncher, 0o755),
|
|
chmod(recoveryLauncher, 0o755),
|
|
chmod(adminCommand, 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(
|
|
brokerClient,
|
|
/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',
|
|
configuration: 'docs/configuration.md',
|
|
configurationChinese: 'docs/configuration.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-admin`,
|
|
// The two launchers exec these modules.
|
|
`${rootName}/bin/qwen-managed-agent-bootstrap`,
|
|
`${rootName}/bin/qwen-managed-agent-bootstrap.mjs`,
|
|
`${rootName}/bin/qwen-managed-agent-upgrade`,
|
|
`${rootName}/bin/qwen-managed-agent-upgrade.mjs`,
|
|
`${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;
|
|
});
|
|
}
|