mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* refactor(managed-agent): Split runtime backends into optional modules Extract five backend implementations behind the shared provider SPI and support selected-backend executable distributions. Preserve SQL ownership, defer unavailable historical work, and make Spring own SSH connection cleanup. Co-authored-by: Qwen-Coder <[email protected]> * refactor(managed-agent): Narrow backend split to Maven modules Keep optional backend dependencies and safe assembly while deferring selective standalone distributions. Restore default packaging and document the provider configuration. Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]>
980 lines
34 KiB
JavaScript
980 lines
34 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { parse } from 'yaml';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
checkManagedRuntimeAsset,
|
|
checkManagedRuntimeOssAsset,
|
|
stageManagedRuntimeRelease,
|
|
} from '../managed-runtime-release.js';
|
|
|
|
const root = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../..',
|
|
);
|
|
const workflow = parse(
|
|
fs.readFileSync(
|
|
path.join(root, '.github/workflows/release-managed-runtime.yml'),
|
|
'utf8',
|
|
),
|
|
);
|
|
let temporary;
|
|
let jar;
|
|
const bytes = Buffer.concat([
|
|
Buffer.from('504b0304', 'hex'),
|
|
Buffer.from('release JAR fixture'),
|
|
]);
|
|
beforeEach(() => {
|
|
temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'managed-release-'));
|
|
jar = path.join(temporary, 'server.jar');
|
|
fs.writeFileSync(jar, bytes);
|
|
});
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
});
|
|
|
|
it('stages the exact server bytes and the checksum consumed by the first-use installer', async () => {
|
|
const outDir = path.join(temporary, 'release');
|
|
const result = await stageManagedRuntimeRelease({
|
|
jar,
|
|
version: '0.25.4-preview.1',
|
|
outDir,
|
|
});
|
|
const name = 'qwen-managed-agent-server-0.25.4-preview.1.jar';
|
|
expect(result).toEqual({
|
|
name,
|
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
});
|
|
expect(fs.readFileSync(path.join(outDir, name))).toEqual(bytes);
|
|
expect(fs.readFileSync(path.join(outDir, `${name}.sha256`), 'utf8')).toBe(
|
|
`${result.sha256} ${name}\n`,
|
|
);
|
|
expect(fs.readdirSync(outDir).sort()).toEqual([name, `${name}.sha256`]);
|
|
});
|
|
|
|
it.each(['latest', '../escape', 'v0.25.4'])(
|
|
'refuses non-exact version %s before staging',
|
|
async (version) => {
|
|
const outDir = path.join(temporary, 'release');
|
|
await expect(
|
|
stageManagedRuntimeRelease({ jar, version, outDir }),
|
|
).rejects.toThrow('exact CLI release');
|
|
expect(fs.existsSync(outDir)).toBe(false);
|
|
},
|
|
);
|
|
|
|
it('refuses non-JAR content rather than producing a downloadable payload', async () => {
|
|
fs.writeFileSync(jar, '<html>error</html>');
|
|
await expect(
|
|
stageManagedRuntimeRelease({
|
|
jar,
|
|
version: '0.25.4',
|
|
outDir: path.join(temporary, 'release'),
|
|
}),
|
|
).rejects.toThrow('packaged server JAR');
|
|
});
|
|
|
|
it('recognizes an already published identical artifact without replacing it', async () => {
|
|
const fetchImpl = vi.fn(async () => new Response(bytes));
|
|
await expect(
|
|
checkManagedRuntimeAsset('https://example.com/server.jar', jar, fetchImpl),
|
|
).resolves.toBe('present');
|
|
expect(fetchImpl.mock.calls[0][1].signal).toBeInstanceOf(AbortSignal);
|
|
});
|
|
|
|
it('treats only a genuine 404 as an unpublished artifact', async () => {
|
|
await expect(
|
|
checkManagedRuntimeAsset(
|
|
'https://example.com/server.jar',
|
|
jar,
|
|
async () => new Response('', { status: 404 }),
|
|
),
|
|
).resolves.toBe('missing');
|
|
for (const status of [403, 429, 500]) {
|
|
await expect(
|
|
checkManagedRuntimeAsset(
|
|
'https://example.com/server.jar',
|
|
jar,
|
|
async () => new Response('', { status }),
|
|
),
|
|
).rejects.toThrow(`HTTP ${status}`);
|
|
}
|
|
});
|
|
|
|
it.each(['different bytes of the same length', 'truncated', 'oversized'])(
|
|
'refuses an existing %s artifact',
|
|
async (kind) => {
|
|
const content =
|
|
kind === 'truncated'
|
|
? bytes.subarray(0, 4)
|
|
: kind === 'oversized'
|
|
? Buffer.concat([bytes, Buffer.from('extra')])
|
|
: Buffer.alloc(bytes.length, 1);
|
|
await expect(
|
|
checkManagedRuntimeAsset(
|
|
'https://example.com/server.jar',
|
|
jar,
|
|
async () => new Response(content),
|
|
),
|
|
).rejects.toThrow('refusing overwrite');
|
|
},
|
|
);
|
|
|
|
it('does not disguise network failure as a missing release', async () => {
|
|
await expect(
|
|
checkManagedRuntimeAsset(
|
|
'https://example.com/server.jar',
|
|
jar,
|
|
async () => {
|
|
throw new Error('network unavailable');
|
|
},
|
|
),
|
|
).rejects.toThrow('network unavailable');
|
|
});
|
|
|
|
it.each([
|
|
'http://example.com/server.jar',
|
|
'https://user:[email protected]/server.jar',
|
|
])('requires a public HTTPS source: %s', async (url) => {
|
|
const fetchImpl = vi.fn();
|
|
await expect(checkManagedRuntimeAsset(url, jar, fetchImpl)).rejects.toThrow(
|
|
'public HTTPS',
|
|
);
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe.skipIf(process.platform === 'win32')(
|
|
'authenticated OSS verification',
|
|
() => {
|
|
function check({
|
|
content = bytes,
|
|
error = '',
|
|
status = 0,
|
|
url = 'oss://release-bucket/releases/server.jar',
|
|
extra = [],
|
|
script = path.join(root, 'scripts/managed-runtime-release.js'),
|
|
} = {}) {
|
|
const bin = path.join(temporary, 'bin');
|
|
fs.mkdirSync(bin, { recursive: true });
|
|
fs.writeFileSync(path.join(bin, 'package.json'), '{"type":"commonjs"}');
|
|
fs.writeFileSync(
|
|
path.join(bin, 'ossutil'),
|
|
`#!${process.execPath}
|
|
const fs = require('node:fs');
|
|
fs.writeFileSync(process.env.OSS_ARGS, JSON.stringify(process.argv.slice(2)));
|
|
process.stdout.write(Buffer.from(${JSON.stringify(content.toString('base64'))}, 'base64'));
|
|
process.stdout.write(${JSON.stringify(error || '\n\n0.001234(s) elapsed\n')});
|
|
process.exit(${status});
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
fs.realpathSync(script),
|
|
'--check-oss',
|
|
url,
|
|
'--config',
|
|
path.join(temporary, 'config'),
|
|
'--file',
|
|
jar,
|
|
...extra,
|
|
],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
OSS_ARGS: path.join(temporary, 'args'),
|
|
TMPDIR: temporary,
|
|
},
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
expect(
|
|
fs
|
|
.readdirSync(temporary)
|
|
.filter((name) => name.startsWith('managed-oss-')),
|
|
).toEqual([]);
|
|
return result;
|
|
}
|
|
|
|
it('downloads authenticated bytes using only the configured read command', () => {
|
|
const result = check();
|
|
expect(result.stderr).toBe('');
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toBe('present\n');
|
|
expect(
|
|
JSON.parse(fs.readFileSync(path.join(temporary, 'args'), 'utf8')),
|
|
).toEqual([
|
|
'cat',
|
|
'oss://release-bucket/releases/server.jar',
|
|
'-c',
|
|
path.join(temporary, 'config'),
|
|
]);
|
|
expect(fs.readFileSync(jar)).toEqual(bytes);
|
|
});
|
|
|
|
it('runs current workflow tooling alongside an unchanged old-tag source tree', () => {
|
|
const command = workflow.jobs.release.steps
|
|
.find((item) => item.id === 'publication')
|
|
.run.match(/node (\S+) --check-oss/)[1];
|
|
const script = path.join(temporary, command);
|
|
fs.mkdirSync(path.dirname(script), { recursive: true });
|
|
for (const name of [
|
|
'managed-runtime-release.js',
|
|
'release-script-utils.js',
|
|
]) {
|
|
fs.copyFileSync(
|
|
path.join(root, 'scripts', name),
|
|
path.join(path.dirname(script), name),
|
|
);
|
|
}
|
|
fs.writeFileSync(
|
|
path.join(temporary, 'package.json'),
|
|
'{"type":"module"}',
|
|
);
|
|
fs.symlinkSync(
|
|
path.join(root, 'node_modules'),
|
|
path.join(temporary, 'node_modules'),
|
|
'dir',
|
|
);
|
|
const oldSource = path.join(
|
|
temporary,
|
|
'scripts/managed-runtime-release.js',
|
|
);
|
|
fs.mkdirSync(path.dirname(oldSource), { recursive: true });
|
|
fs.writeFileSync(
|
|
oldSource,
|
|
'throw new Error("old tag has no --check-oss");',
|
|
);
|
|
const result = check({ script });
|
|
expect(result.stderr).toBe('');
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toBe('present\n');
|
|
expect(fs.readFileSync(oldSource, 'utf8')).toBe(
|
|
'throw new Error("old tag has no --check-oss");',
|
|
);
|
|
});
|
|
|
|
it('preserves an artifact terminal newline when removing the CLI trailer', () => {
|
|
const content = Buffer.concat([bytes, Buffer.from('\n')]);
|
|
fs.writeFileSync(jar, content);
|
|
expect(check({ content }).stdout).toBe('present\n');
|
|
});
|
|
|
|
it('refuses unexpected successful CLI output instead of discarding arbitrary bytes', () => {
|
|
const result = check({ error: '\n' });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('refusing overwrite');
|
|
});
|
|
|
|
it('recognizes only the canonical structured NoSuchKey 404 as missing', () => {
|
|
const result = check({
|
|
content: Buffer.alloc(0),
|
|
status: 1,
|
|
error:
|
|
'Error: oss: service returned error: StatusCode=404, ErrorCode=NoSuchKey, ErrorMessage="The specified key does not exist.", RequestId=request-id\n',
|
|
});
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toBe('missing\n');
|
|
});
|
|
|
|
it.each([
|
|
'Error: oss: service returned error: StatusCode=403, ErrorCode=AccessDenied, ErrorMessage="Access denied", RequestId=id\n',
|
|
'Error: oss: service returned error: StatusCode=404, ErrorCode=NoSuchBucket, ErrorMessage="Missing bucket", RequestId=id\n',
|
|
'Error: oss: service returned error: StatusCode=403, ErrorCode=NoSuchKey, ErrorMessage="Denied", RequestId=id\n',
|
|
'Error: oss: service returned error: StatusCode=404, ErrorCode=AccessDenied, ErrorMessage="NoSuchKey", RequestId=id\n',
|
|
'Error: HTTP 404 NoSuchKey\n',
|
|
'Error: network unavailable\n',
|
|
])('fails closed for inaccessible or unconfirmed absence: %s', (error) => {
|
|
const result = check({ content: Buffer.alloc(0), status: 1, error });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toBe('');
|
|
expect(result.stderr).toContain('Cannot verify existing OSS artifact');
|
|
});
|
|
|
|
it.each([
|
|
Buffer.alloc(bytes.length, 1),
|
|
bytes.subarray(0, 4),
|
|
Buffer.concat([bytes, Buffer.from('extra')]),
|
|
])('refuses different bytes and cleans its download', (content) => {
|
|
const result = check({ content });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain('refusing overwrite');
|
|
});
|
|
|
|
it.each([
|
|
'https://example.com/object',
|
|
'oss://user:password@bucket/object',
|
|
'oss://bucket/',
|
|
'oss://bucket/key?versionId=old',
|
|
])('rejects invalid OSS sources before invoking ossutil: %s', (url) => {
|
|
expect(check({ url }).status).toBe(1);
|
|
expect(fs.existsSync(path.join(temporary, 'args'))).toBe(false);
|
|
});
|
|
|
|
it('kills a stalled reader and removes its partial download', async () => {
|
|
fs.writeFileSync(
|
|
path.join(temporary, 'ossutil'),
|
|
`#!${process.execPath}
|
|
setInterval(() => {}, 1000);
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
vi.stubEnv('PATH', temporary);
|
|
vi.stubEnv('TMPDIR', temporary);
|
|
await expect(
|
|
checkManagedRuntimeOssAsset('oss://bucket/key', jar, 'config', 100),
|
|
).rejects.toThrow('Cannot verify existing OSS artifact');
|
|
expect(
|
|
fs
|
|
.readdirSync(temporary)
|
|
.filter((name) => name.startsWith('managed-oss-')),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it('fails closed when ossutil cannot start', async () => {
|
|
vi.stubEnv('PATH', temporary);
|
|
await expect(
|
|
checkManagedRuntimeOssAsset('oss://bucket/key', jar, 'config'),
|
|
).rejects.toThrow('Cannot verify existing OSS artifact');
|
|
});
|
|
|
|
it('rejects mixed public and authenticated verification arguments', () => {
|
|
expect(
|
|
check({ extra: ['--check-url', 'https://example.com/server.jar'] })
|
|
.status,
|
|
).toBe(1);
|
|
expect(fs.existsSync(path.join(temporary, 'args'))).toBe(false);
|
|
});
|
|
},
|
|
);
|
|
|
|
describe('Managed runtime publication gates', () => {
|
|
const steps = workflow.jobs.release.steps;
|
|
const step = (name) => steps.find((item) => item.name === name);
|
|
it('stages the executable classifier while retaining legacy fallback and runtime download names', () => {
|
|
const verification = step(
|
|
'Verify the executable JAR and publication profile',
|
|
).run;
|
|
expect(verification).toContain(
|
|
'qwen-managed-agent-server-${CLI_VERSION}-exec.jar',
|
|
);
|
|
expect(verification).toContain(
|
|
'Main-Class: org.springframework.boot.loader.launch.JarLauncher',
|
|
);
|
|
expect(verification).toContain(
|
|
'if [[ ! -f "${runtime_jar}" ]]; then runtime_jar="packages/managed-agent-server/target/qwen-managed-agent-server-${CLI_VERSION}.jar"; fi',
|
|
);
|
|
expect(verification).toContain(
|
|
'--jar "${runtime_jar}" --version "${CLI_VERSION}" --out-dir dist/managed-runtime',
|
|
);
|
|
expect(step('Verify both public download channels').run).toContain(
|
|
'jar="qwen-managed-agent-server-${CLI_VERSION}.jar"',
|
|
);
|
|
});
|
|
|
|
it('defaults manual runs to verification and restricts publication to the protected official repository', () => {
|
|
expect(workflow.on.release.types).toEqual(['published']);
|
|
expect(workflow.on.workflow_dispatch.inputs.dry_run.default).toBe(true);
|
|
expect(workflow.jobs.prepare.if).toContain(
|
|
"github.repository == 'QwenLM/qwen-code'",
|
|
);
|
|
expect(workflow.jobs.release.environment.name).toBe('production-release');
|
|
expect(workflow.concurrency['cancel-in-progress']).toBe(false);
|
|
expect(workflow.jobs.release.if).toContain(
|
|
"needs.prepare.outputs.eligible == 'true'",
|
|
);
|
|
for (const name of [
|
|
'Recheck the immutable tag and existing publication bytes',
|
|
'Attach only missing GitHub release assets',
|
|
'Install the verified OSS uploader',
|
|
'Configure authenticated OSS access',
|
|
'Upload only missing OSS release assets',
|
|
]) {
|
|
expect(step(name).if).toContain(
|
|
"github.event_name == 'release' || !inputs.dry_run",
|
|
);
|
|
}
|
|
expect(step('Attach only missing GitHub release assets').run).not.toContain(
|
|
'--clobber',
|
|
);
|
|
const build = steps.findIndex(
|
|
(item) =>
|
|
item.name === 'Verify the executable JAR and publication profile',
|
|
);
|
|
for (const name of [
|
|
'Attach only missing GitHub release assets',
|
|
'Upload only missing OSS release assets',
|
|
]) {
|
|
expect(build).toBeLessThan(steps.indexOf(step(name)));
|
|
}
|
|
});
|
|
|
|
it('distributes the verified runtime without Central publication access or signing credentials', () => {
|
|
const definition = JSON.stringify(workflow);
|
|
for (const dependency of [
|
|
'CENTRAL_USERNAME',
|
|
'CENTRAL_PASSWORD',
|
|
'MAVEN_GPG_PRIVATE_KEY',
|
|
'MAVEN_GPG_PASSPHRASE',
|
|
'repo.maven.apache.org',
|
|
'gpg-private-key',
|
|
]) {
|
|
expect(definition).not.toContain(dependency);
|
|
}
|
|
expect(steps.some((item) => /\bmvn\b.*\bdeploy\b/u.test(item.run))).toBe(
|
|
false,
|
|
);
|
|
expect(workflow.on.workflow_dispatch.inputs.dry_run.description).toBe(
|
|
'Build and verify without publishing to GitHub or OSS.',
|
|
);
|
|
});
|
|
|
|
it('configures the pinned OSS reader before preflight and reuses the upload destination and credentials', () => {
|
|
const install = step('Install the verified OSS uploader');
|
|
const configure = step('Configure authenticated OSS access');
|
|
const preflight = step(
|
|
'Recheck the immutable tag and existing publication bytes',
|
|
);
|
|
const upload = step('Upload only missing OSS release assets');
|
|
expect(steps.indexOf(install)).toBeLessThan(steps.indexOf(configure));
|
|
expect(steps.indexOf(configure)).toBeLessThan(steps.indexOf(preflight));
|
|
expect(steps.indexOf(preflight)).toBeLessThan(
|
|
steps.indexOf(step('Attach only missing GitHub release assets')),
|
|
);
|
|
expect(steps.indexOf(preflight)).toBeLessThan(steps.indexOf(upload));
|
|
expect(install.env.OSSUTIL_URL).toContain('/1.7.19/');
|
|
expect(install.env.OSSUTIL_SHA256).toBe(
|
|
'dcc512e4a893e16bbee63bc769339d8e56b21744fd83c8212a9d8baf28767343',
|
|
);
|
|
expect(configure.env.ALIYUN_OSS_ACCESS_KEY_ID).toBe(
|
|
'${{ secrets.ALIYUN_OSS_ACCESS_KEY_ID }}',
|
|
);
|
|
expect(configure.env.ALIYUN_OSS_ACCESS_KEY_SECRET).toBe(
|
|
'${{ secrets.ALIYUN_OSS_ACCESS_KEY_SECRET }}',
|
|
);
|
|
expect(configure.run).toContain('umask 077');
|
|
expect(configure.run).toContain(
|
|
'-L EN -c "${RUNNER_TEMP}/managed-ossutilconfig"',
|
|
);
|
|
expect(workflow.jobs.release.env.ALIYUN_OSS_BUCKET).toContain(
|
|
'vars.ALIYUN_OSS_BUCKET',
|
|
);
|
|
expect(preflight.run).toContain(
|
|
'oss="oss://${ALIYUN_OSS_BUCKET}/releases/qwen-code/${RELEASE_TAG}/${asset}"',
|
|
);
|
|
expect(preflight.run).toContain(
|
|
'--check-oss "${oss}" --config "${RUNNER_TEMP}/managed-ossutilconfig"',
|
|
);
|
|
expect(upload.run).toContain(
|
|
'--bucket "${ALIYUN_OSS_BUCKET}" --config "${RUNNER_TEMP}/managed-ossutilconfig"',
|
|
);
|
|
expect(upload.env).toBeUndefined();
|
|
expect(step('Verify both public download channels').run).toContain(
|
|
'--check-url "${base}/${asset}"',
|
|
);
|
|
expect(step('Verify both public download channels').run).toContain(
|
|
'OSS_PUBLIC_BASE_URL',
|
|
);
|
|
});
|
|
|
|
it('pins checkout and reproducible timestamps, and verifies the SDK before packaging the server', () => {
|
|
expect(steps[0].with.ref).toBe('${{ needs.prepare.outputs.sha }}');
|
|
expect(steps[0].with['persist-credentials']).toBe(false);
|
|
const tools = step('Checkout release tooling from the workflow revision');
|
|
expect(tools.with).toEqual({
|
|
ref: '${{ github.workflow_sha }}',
|
|
path: '.qwen/managed-release-tools',
|
|
'sparse-checkout': 'scripts',
|
|
'persist-credentials': false,
|
|
});
|
|
expect(tools.uses).toBe(steps[0].uses);
|
|
for (const item of steps.filter((item) =>
|
|
item.run?.includes('managed-runtime-release.js'),
|
|
)) {
|
|
expect(item.run).not.toContain('node scripts/managed-runtime-release.js');
|
|
expect(item.run).toContain(
|
|
'node .qwen/managed-release-tools/scripts/managed-runtime-release.js',
|
|
);
|
|
}
|
|
|
|
expect(
|
|
step('Verify and install the SDK from the same source').run,
|
|
).toContain('clean install');
|
|
expect(
|
|
step('Verify and install the SDK from the same source').run,
|
|
).toContain('project.build.outputTimestamp="${BUILD_TIMESTAMP}"');
|
|
expect(
|
|
step('Verify the executable JAR and publication profile').run,
|
|
).toContain('-Pcentral-release');
|
|
expect(
|
|
step('Verify the executable JAR and publication profile').run,
|
|
).toContain('clean verify');
|
|
});
|
|
|
|
it('isolates server test classes without filtering or bypassing release verification', () => {
|
|
const verification = step(
|
|
'Verify the executable JAR and publication profile',
|
|
).run;
|
|
expect(verification).toContain('-DforkCount=1');
|
|
expect(verification).toContain('-DreuseForks=false');
|
|
expect(verification).toContain('clean verify -Pcentral-release');
|
|
expect(verification).not.toMatch(
|
|
/-D(?:test=|groups=|excludedGroups=|skipTests|maven\.test\.(?:skip|failure\.ignore)|surefire\.(?:includes|excludes|rerunFailingTestsCount))/,
|
|
);
|
|
expect(verification).not.toContain('-DargLine=');
|
|
});
|
|
|
|
it('keeps auxiliary executable JARs out of Central without changing their ordinary build attachment', () => {
|
|
const pom = fs.readFileSync(
|
|
path.join(root, 'packages/managed-agent-server/pom.xml'),
|
|
'utf8',
|
|
);
|
|
expect(pom).toContain(
|
|
'<managed.operator-tools.attach>true</managed.operator-tools.attach>',
|
|
);
|
|
const profile = pom.match(
|
|
/<profile>\s*<id>central-release<\/id>([\s\S]*?)<\/profile>/,
|
|
)[1];
|
|
expect(profile).toContain(
|
|
'<managed.operator-tools.attach>false</managed.operator-tools.attach>',
|
|
);
|
|
for (const plugin of [
|
|
'central-publishing-maven-plugin',
|
|
'maven-source-plugin',
|
|
'maven-javadoc-plugin',
|
|
'maven-gpg-plugin',
|
|
])
|
|
expect(profile).toContain(plugin);
|
|
const serverPom = fs.readFileSync(
|
|
path.join(root, 'packages/managed-agent-server/server/pom.xml'),
|
|
'utf8',
|
|
);
|
|
expect(
|
|
serverPom.match(/<attach>\$\{managed.operator-tools.attach\}<\/attach>/g),
|
|
).toHaveLength(6);
|
|
});
|
|
});
|
|
|
|
describe.skipIf(process.platform === 'win32')('release gate execution', () => {
|
|
function executable(name, body) {
|
|
const bin = path.join(temporary, 'bin');
|
|
fs.mkdirSync(bin, { recursive: true });
|
|
fs.writeFileSync(path.join(bin, name), `#!/bin/sh\n${body}\n`, {
|
|
mode: 0o755,
|
|
});
|
|
return bin;
|
|
}
|
|
function run(script, env = {}) {
|
|
return spawnSync('bash', ['--noprofile', '--norc', '-e', '-c', script], {
|
|
cwd: temporary,
|
|
env: {
|
|
...process.env,
|
|
HOME: temporary,
|
|
BASH_ENV: '/dev/null',
|
|
PATH: `${path.join(temporary, 'bin')}${path.delimiter}${process.env.PATH}`,
|
|
RUNNER_TEMP: temporary,
|
|
GITHUB_OUTPUT: path.join(temporary, 'output'),
|
|
RELEASE_TAG: 'v0.25.4',
|
|
EVENT_NAME: 'workflow_dispatch',
|
|
WORKFLOW_REF: 'refs/heads/main',
|
|
...env,
|
|
},
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
const gate = workflow.jobs.prepare.steps.find(
|
|
(item) => item.id === 'gate',
|
|
).run;
|
|
const preflight = workflow.jobs.release.steps.find(
|
|
(item) => item.id === 'publication',
|
|
).run;
|
|
const repair = workflow.jobs.release.steps.find(
|
|
(item) => item.name === 'Repair the 0.26.3 ownership test fixture',
|
|
);
|
|
const repairSource = 'eeee1443a7e36ab22b2f1dcded8dec64b621a7ee';
|
|
const repairedTest =
|
|
'packages/managed-agent-server/src/test/java/com/alibaba/qwen/code/managedagent/Issue13180HardenedVerificationTest.java';
|
|
const repairPatch = 'scripts/patches/managed-runtime-v0.26.3-tests.patch';
|
|
|
|
function repairFixture() {
|
|
const fixed = fs.readFileSync(
|
|
path.join(
|
|
root,
|
|
repairedTest.replace('/src/test/', '/verification/src/test/'),
|
|
),
|
|
'utf8',
|
|
);
|
|
const target = path.join(temporary, repairedTest);
|
|
const patch = path.join(
|
|
temporary,
|
|
'.qwen/managed-release-tools',
|
|
repairPatch,
|
|
);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.mkdirSync(path.dirname(patch), { recursive: true });
|
|
fs.writeFileSync(target, fixed);
|
|
fs.copyFileSync(path.join(root, repairPatch), patch);
|
|
const git = (...args) => {
|
|
const result = spawnSync(
|
|
'git',
|
|
[
|
|
'-c',
|
|
'core.hooksPath=/dev/null',
|
|
'-c',
|
|
'commit.gpgsign=false',
|
|
'-c',
|
|
'user.name=Release test',
|
|
'-c',
|
|
'[email protected]',
|
|
...args,
|
|
],
|
|
{ cwd: temporary, encoding: 'utf8' },
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
};
|
|
git('init', '--quiet');
|
|
git('apply', '--reverse', patch);
|
|
const original = fs.readFileSync(target, 'utf8');
|
|
fs.writeFileSync(path.join(temporary, 'product.java'), 'original product');
|
|
git('add', repairedTest, 'product.java');
|
|
git('commit', '--quiet', '-m', 'Release test fixture');
|
|
return { fixed, original, target, patch };
|
|
}
|
|
|
|
it('repairs only the 0.26.3 test while retaining the original ownership assertions and product bytes', () => {
|
|
expect(repair.if).toBe("${{ env.RELEASE_TAG == 'v0.26.3' }}");
|
|
const { fixed, target } = repairFixture();
|
|
const result = run(repair.run, {
|
|
RELEASE_TAG: 'v0.26.3',
|
|
SOURCE_SHA: repairSource,
|
|
GITHUB_STEP_SUMMARY: path.join(temporary, 'summary'),
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(fs.readFileSync(target, 'utf8')).toBe(fixed);
|
|
expect(fs.readFileSync(path.join(temporary, 'product.java'), 'utf8')).toBe(
|
|
'original product',
|
|
);
|
|
});
|
|
|
|
it.each(['wrong source', 'production patch'])(
|
|
'refuses the 0.26.3 repair with %s before changing files',
|
|
(failure) => {
|
|
const { original, target, patch } = repairFixture();
|
|
if (failure === 'production patch') {
|
|
fs.writeFileSync(
|
|
patch,
|
|
fs
|
|
.readFileSync(patch, 'utf8')
|
|
.replaceAll(repairedTest, 'product.java'),
|
|
);
|
|
}
|
|
const result = run(repair.run, {
|
|
RELEASE_TAG: 'v0.26.3',
|
|
SOURCE_SHA: failure === 'wrong source' ? 'other-sha' : repairSource,
|
|
GITHUB_STEP_SUMMARY: path.join(temporary, 'summary'),
|
|
});
|
|
expect(result.status).not.toBe(0);
|
|
expect(fs.readFileSync(target, 'utf8')).toBe(original);
|
|
expect(
|
|
fs.readFileSync(path.join(temporary, 'product.java'), 'utf8'),
|
|
).toBe('original product');
|
|
},
|
|
);
|
|
|
|
const steps = workflow.jobs.release.steps;
|
|
const step = (name) => steps.find((item) => item.name === name);
|
|
const releaseRepair = step('Repair the 0.26.5 release inputs');
|
|
const verificationPom = 'packages/managed-agent-server/verification/pom.xml';
|
|
const repairPaths = [
|
|
'packages/managed-agent-server/adapters/src/main/java/com/alibaba/qwen/code/runtimebroker/provisioning/ExternalRuntimeProvisioner.java',
|
|
'packages/managed-agent-server/contracts/src/main/java/com/alibaba/qwen/code/managedagent/contracts/event/ManagedEventEnvelopes.java',
|
|
verificationPom,
|
|
];
|
|
|
|
function releaseFixture() {
|
|
// The patches still target the released layout after source moves.
|
|
const currentPaths = [
|
|
'packages/managed-agent-server/runtime-external/src/main/java/com/alibaba/qwen/code/runtimebroker/provisioning/external/ExternalRuntimeProvisioner.java',
|
|
...repairPaths.slice(1),
|
|
];
|
|
const fixed = currentPaths.map((file) =>
|
|
fs.readFileSync(path.join(root, file), 'utf8'),
|
|
);
|
|
for (const [index, file] of repairPaths.entries()) {
|
|
const target = path.join(temporary, file);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.writeFileSync(target, fixed[index]);
|
|
}
|
|
const patches = ['javadoc', 'verification'].map((kind) => {
|
|
const relative = `scripts/patches/managed-runtime-v0.26.5-${kind}.patch`;
|
|
const patch = path.join(
|
|
temporary,
|
|
'.qwen/managed-release-tools',
|
|
relative,
|
|
);
|
|
fs.mkdirSync(path.dirname(patch), { recursive: true });
|
|
fs.copyFileSync(path.join(root, relative), patch);
|
|
return patch;
|
|
});
|
|
const git = (...args) => {
|
|
const result = spawnSync(
|
|
'git',
|
|
[
|
|
'-c',
|
|
'core.hooksPath=/dev/null',
|
|
'-c',
|
|
'commit.gpgsign=false',
|
|
'-c',
|
|
'user.name=Release test',
|
|
'-c',
|
|
'[email protected]',
|
|
...args,
|
|
],
|
|
{ cwd: temporary, encoding: 'utf8' },
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
};
|
|
git('init', '--quiet');
|
|
git('apply', '--reverse', ...patches);
|
|
fs.writeFileSync(path.join(temporary, 'product.java'), 'original product');
|
|
git('add', ...repairPaths, 'product.java');
|
|
git('commit', '--quiet', '-m', 'Pinned release input fixture');
|
|
return {
|
|
fixed,
|
|
patches,
|
|
originalPom: fs.readFileSync(
|
|
path.join(temporary, verificationPom),
|
|
'utf8',
|
|
),
|
|
};
|
|
}
|
|
|
|
it('repairs the pinned 0.26.5 release inputs while preserving every specialist test profile', () => {
|
|
expect(releaseRepair.if).toBe("${{ env.RELEASE_TAG == 'v0.26.5' }}");
|
|
expect(steps.indexOf(releaseRepair)).toBeLessThan(
|
|
steps.indexOf(step('Build the matching CLI prerequisites')),
|
|
);
|
|
const { fixed, patches, originalPom } = releaseFixture();
|
|
const [patch] = patches;
|
|
const additions = fs
|
|
.readFileSync(patch, 'utf8')
|
|
.split('\n')
|
|
.filter((line) => line.startsWith('+') && !line.startsWith('+++'));
|
|
expect(additions).toEqual([
|
|
'+import com.alibaba.qwen.code.runtimebroker.protocol.ExternalRuntimeKinds;',
|
|
'+ * decimal form while 1e-6 ≤ |x| < 1e21, exponential outside it.',
|
|
]);
|
|
expect(
|
|
fs
|
|
.readFileSync(patch, 'utf8')
|
|
.split('\n')
|
|
.filter((line) => line.startsWith('-') && !line.startsWith('---')),
|
|
).toEqual([
|
|
'- * decimal form while 1e-6 ≤ |x| < 1e21, exponential outside it.',
|
|
]);
|
|
const result = run(releaseRepair.run, {
|
|
RELEASE_TAG: 'v0.26.5',
|
|
SOURCE_SHA: 'db0e4e24a0d89b211b2a15f4af2be22adac71dfc',
|
|
GITHUB_STEP_SUMMARY: path.join(temporary, 'summary'),
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
for (const [index, file] of repairPaths.entries()) {
|
|
expect(fs.readFileSync(path.join(temporary, file), 'utf8')).toBe(
|
|
fixed[index],
|
|
);
|
|
}
|
|
expect(fs.readFileSync(path.join(temporary, 'product.java'), 'utf8')).toBe(
|
|
'original product',
|
|
);
|
|
const pom = fs.readFileSync(path.join(temporary, verificationPom), 'utf8');
|
|
expect(pom.slice(pom.indexOf('<profiles>'))).toBe(
|
|
originalPom.slice(originalPom.indexOf('<profiles>')),
|
|
);
|
|
const defaultBuild = pom
|
|
.slice(pom.indexOf('<build>'), pom.indexOf('<profiles>'))
|
|
.replace(/<pluginManagement>[\s\S]*?<\/pluginManagement>/, '');
|
|
expect(defaultBuild).not.toContain(
|
|
'<artifactId>maven-failsafe-plugin</artifactId>',
|
|
);
|
|
const packaging = (xml) =>
|
|
xml.match(
|
|
/<execution>\s*<id>artifact-packaging<\/id>[\s\S]*?<\/execution>/,
|
|
)?.[0];
|
|
expect(packaging(pom)).toBeTruthy();
|
|
expect(packaging(pom)).toBe(packaging(originalPom));
|
|
expect(fs.readFileSync(path.join(temporary, 'summary'), 'utf8')).toContain(
|
|
'original tag unchanged',
|
|
);
|
|
expect(JSON.stringify(workflow)).not.toMatch(
|
|
/-D(?:maven\.javadoc\.skip|doclint=|additionalJOption=)/,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
'wrong source',
|
|
'changed Javadoc patch',
|
|
'changed verification patch',
|
|
'dirty checkout',
|
|
])(
|
|
'refuses the 0.26.5 release repair with %s before applying either patch',
|
|
(failure) => {
|
|
const { patches } = releaseFixture();
|
|
if (failure.startsWith('changed ')) {
|
|
const patch = patches[failure === 'changed Javadoc patch' ? 0 : 1];
|
|
fs.writeFileSync(patch, `${fs.readFileSync(patch, 'utf8')}\n`);
|
|
}
|
|
if (failure === 'dirty checkout') {
|
|
const target = path.join(temporary, repairPaths[0]);
|
|
fs.writeFileSync(target, `${fs.readFileSync(target, 'utf8')}\n`);
|
|
}
|
|
const before = repairPaths.map((file) =>
|
|
fs.readFileSync(path.join(temporary, file), 'utf8'),
|
|
);
|
|
const result = run(releaseRepair.run, {
|
|
RELEASE_TAG: 'v0.26.5',
|
|
SOURCE_SHA:
|
|
failure === 'wrong source'
|
|
? 'other-sha'
|
|
: 'db0e4e24a0d89b211b2a15f4af2be22adac71dfc',
|
|
GITHUB_STEP_SUMMARY: path.join(temporary, 'summary'),
|
|
});
|
|
expect(result.status).not.toBe(0);
|
|
for (const [index, file] of repairPaths.entries()) {
|
|
expect(fs.readFileSync(path.join(temporary, file), 'utf8')).toBe(
|
|
before[index],
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it('requires both credentials before creating a private shared OSS configuration', () => {
|
|
const configure = workflow.jobs.release.steps.find(
|
|
(item) => item.name === 'Configure authenticated OSS access',
|
|
).run;
|
|
executable(
|
|
'ossutil',
|
|
'touch oss-called; for value do :; done; : > "$value"',
|
|
);
|
|
const env = {
|
|
ALIYUN_OSS_ACCESS_KEY_ID: 'fixture-id',
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET: '',
|
|
ALIYUN_OSS_ENDPOINT: 'https://example.invalid',
|
|
};
|
|
for (const missing of [
|
|
'ALIYUN_OSS_ACCESS_KEY_ID',
|
|
'ALIYUN_OSS_ACCESS_KEY_SECRET',
|
|
]) {
|
|
const result = run(configure, {
|
|
...env,
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET: 'fixture-secret',
|
|
[missing]: '',
|
|
});
|
|
expect(result.status, result.stderr).not.toBe(0);
|
|
}
|
|
expect(fs.existsSync(path.join(temporary, 'oss-called'))).toBe(false);
|
|
expect(
|
|
run(configure, { ...env, ALIYUN_OSS_ACCESS_KEY_SECRET: 'fixture-secret' })
|
|
.status,
|
|
).toBe(0);
|
|
expect(
|
|
fs.statSync(path.join(temporary, 'managed-ossutilconfig')).mode & 0o777,
|
|
).toBe(0o600);
|
|
});
|
|
|
|
it('rejects a non-main manual run before consulting the release', () => {
|
|
executable('gh', 'touch gh-called; exit 1');
|
|
expect(run(gate, { WORKFLOW_REF: 'refs/heads/untrusted' }).status).not.toBe(
|
|
0,
|
|
);
|
|
expect(fs.existsSync(path.join(temporary, 'gh-called'))).toBe(false);
|
|
});
|
|
|
|
it('ignores an unrelated published release without attempting publication', () => {
|
|
executable('gh', 'touch gh-called; exit 1');
|
|
expect(
|
|
run(gate, { EVENT_NAME: 'release', RELEASE_TAG: 'vscode-v0.25.4' })
|
|
.status,
|
|
).toBe(0);
|
|
expect(fs.readFileSync(path.join(temporary, 'output'), 'utf8')).toBe(
|
|
'eligible=false\n',
|
|
);
|
|
expect(fs.existsSync(path.join(temporary, 'gh-called'))).toBe(false);
|
|
});
|
|
|
|
it('rejects a changed release tag before checking or uploading artifacts', () => {
|
|
executable('git', "printf '%s\\tref\\n' different-sha");
|
|
executable('gh', 'touch gh-called; exit 1');
|
|
expect(
|
|
run(preflight, { SOURCE_SHA: 'original-sha', CLI_VERSION: '0.25.4' })
|
|
.status,
|
|
).not.toBe(0);
|
|
expect(fs.existsSync(path.join(temporary, 'gh-called'))).toBe(false);
|
|
});
|
|
|
|
it('prepares both distribution channels when Central publication is unavailable and its credentials are absent', () => {
|
|
executable('git', "printf '%s\\tref\\n' original-sha");
|
|
executable('gh', "printf '%s\\n' '{\"draft\":false}'");
|
|
executable(
|
|
'node',
|
|
'case "$*" in *repo.maven.apache.org*) echo unexpected-central >&2; exit 7 ;; *) echo missing ;; esac',
|
|
);
|
|
const result = run(preflight, {
|
|
SOURCE_SHA: 'original-sha',
|
|
CLI_VERSION: '0.25.4',
|
|
ALIYUN_OSS_BUCKET: 'release-bucket',
|
|
CENTRAL_USERNAME: '',
|
|
CENTRAL_PASSWORD: '',
|
|
MAVEN_USERNAME: '',
|
|
MAVEN_PASSWORD: '',
|
|
MAVEN_GPG_PRIVATE_KEY: '',
|
|
MAVEN_GPG_PASSPHRASE: '',
|
|
});
|
|
expect(result.status).toBe(0);
|
|
for (const channel of ['github', 'oss']) {
|
|
expect(
|
|
fs.readFileSync(
|
|
path.join(temporary, `managed-${channel}-missing`),
|
|
'utf8',
|
|
),
|
|
).toBe(
|
|
'dist/managed-runtime/qwen-managed-agent-server-0.25.4.jar\n' +
|
|
'dist/managed-runtime/qwen-managed-agent-server-0.25.4.jar.sha256\n',
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each(['github.com', 'oss://'])(
|
|
'aborts preflight on a %s artifact-verification error instead of treating it as present',
|
|
(channel) => {
|
|
executable('git', "printf '%s\\tref\\n' original-sha");
|
|
executable('gh', "printf '%s\\n' '{\"draft\":false}'");
|
|
executable(
|
|
'node',
|
|
'case "$*" in *"$MISMATCH_HOST"*) echo mismatch >&2; exit 7 ;; *) echo missing ;; esac',
|
|
);
|
|
const result = run(preflight, {
|
|
SOURCE_SHA: 'original-sha',
|
|
CLI_VERSION: '0.25.4',
|
|
OSS_PUBLIC_BASE_URL: 'https://example.com',
|
|
ALIYUN_OSS_BUCKET: 'release-bucket',
|
|
MISMATCH_HOST: channel,
|
|
});
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain('mismatch');
|
|
},
|
|
);
|
|
});
|