qwen-code/scripts/tests/sdk-java-workflow.test.js
qwen-code-dev-bot f3642d4e45
fix(ci): sweep stale pool residue before sdk-java checkouts (#13506) (#13507)
A main-push SDK Java run failed its 'ubuntu-latest / Java 21' leg in the
Checkout step before any test ran (run 37423974280). The leg runs on the
shared self-hosted ECS pool, whose reused workspace accumulates residue
from earlier jobs: ci.yml's incident history records review/autofix
leftovers (.qwen, .qwen.root-orig, review worktrees and branches) failing
actions/checkout's clean, and a runner-user-owned tree failing every
later git call with 'detected dubious ownership' (#12648). sdk-java.yml's
pool-routed jobs carried only the bare ownership restore, without the two
countermeasures ci.yml grew for exactly these failures.

Port both, byte-identical to the pinned ci.yml copies, to the three
pool-routed jobs (test, flyway-migrations, daemon-e2e): the
safe.directory trust joins the restore step, and the stale-.qwen sweep
(with its quarantine ladder and review worktree/branch cleanup) runs
between restore and checkout, gated to self-hosted so the hosted
macOS/Windows legs stay untouched.

The sdk-java workflow suite pins the restore -> sweep -> checkout order
in all three jobs and pins both step bodies byte-identical to ci.yml's,
so the copies cannot drift; mutation-probed by stashing the workflow
change (all 9 new pins red) and restoring it (green). The size baseline
records the copied recipe's growth in the same commit, per the ratchet.

Co-authored-by: 易良 <1204183885@qq.com>
2026-10-06 14:36:05 +00:00

229 lines
9.3 KiB
JavaScript

import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { parse } from 'yaml';
import { describe, expect, it } from 'vitest';
const workflow = readFileSync('.github/workflows/sdk-java.yml', 'utf8');
const job = (name) => {
const start = workflow.indexOf(` ${name}:`);
const next = workflow.slice(start + 1).search(/\n {2}[a-z0-9-]+:\n/);
return workflow.slice(start, next < 0 ? undefined : start + 1 + next);
};
const step = (block, name) => {
const marker = ` - name: '${name}'`;
const start = block.indexOf(marker);
if (start < 0) throw new Error(`Missing workflow step: ${name}`);
const next = block.slice(start + 1).search(/\n {6}- name:/);
return block.slice(start, next < 0 ? undefined : start + 1 + next);
};
describe('SDK Java self-hosted workflow guards', () => {
it.each(['test', 'daemon-e2e'])('protects the %s job', (name) => {
const block = job(name);
for (const fragment of [
"github.repository == ''QwenLM/qwen-code''",
'github.event.pull_request.head.repo.full_name == github.repository',
"vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true''",
// Write-access fork authors route to ECS too; the association list is
// the repo's established trusted set. Negative associations (CONTRIBUTOR,
// NONE, '') fail contains() and stay hosted.
'contains(fromJSON(\'\'["OWNER","MEMBER","COLLABORATOR"]\'\'), github.event.pull_request.author_association)',
'fromJSON(\'\'["self-hosted", "linux", "x64", "ecs-qwen"]\'\')',
"format('refs/pull/{0}/head', github.event.pull_request.number)",
"EXPECTED_SHA: '${{ github.event.pull_request.head.sha }}'",
'git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD',
'exit 1',
]) {
expect(block).toContain(fragment);
}
});
it('serializes latency-sensitive tests on each physical ECS host', () => {
const block = job('test');
expect(block).toContain(
'if: "${{ runner.environment == \'self-hosted\' }}"',
);
expect(block).toContain(
'exec 9>"${HOME}/.cache/qwen-code-ci/sdk-java-tests.lock"',
);
expect(block).toContain('flock --wait 1200 9');
expect(block).toContain(
'::error::sdk-java host lock not acquired within 20 minutes',
);
expect(block).toContain(
'if: "${{ runner.environment == \'github-hosted\' }}"',
);
});
it('runs Runtime Broker tests from the sibling module on self-hosted Java 21', () => {
const block = step(job('test'), 'Run Java SDK tests (self-hosted)');
expect(block).toContain("working-directory: 'packages/sdk-java/qwencode'");
expect(block).toContain("MATRIX_JAVA: '${{ matrix.java }}'");
expect(block).toContain(
'mvn --batch-mode --no-transfer-progress clean test\n' +
' if [ "${MATRIX_JAVA}" = "21" ]; then\n' +
' cd ../runtime-broker\n' +
' mvn --batch-mode --no-transfer-progress clean test\n' +
' fi',
);
});
it.each(['test', 'daemon-e2e'])(
'keeps setup-java Maven files job-local in the %s job',
(name) => {
const block = job(name);
expect(block).toContain(
"settings-path: '${{ runner.temp }}/setup-java-m2'",
);
expect(
block.match(
/MAVEN_ARGS: '--settings \$\{\{ runner\.temp \}\}\/setup-java-m2\/settings\.xml --toolchains \$\{\{ runner\.temp \}\}\/setup-java-m2\/toolchains\.xml'/g,
),
).toHaveLength(name === 'test' ? 6 : 1);
expect(block).not.toContain('Drop shared Maven toolchains.xml');
expect(block).not.toContain('rm -f "${HOME}/.m2/toolchains.xml"');
},
);
});
// #12940: the duplicate-version guard is the fast lane for a collision two
// green PRs can only produce in the merge result, so it runs on every
// trigger — no job-level condition, no Java, no database. #13245 moved its
// trusted runs onto the ECS pool (a seconds-long scan sat 26 minutes in the
// hosted queue); untrusted fork PRs stay hosted.
describe('SDK Java Flyway migration version guard', () => {
it('runs the uniqueness check as an unconditional job', () => {
const block = job('flyway-migrations');
const parsed = parse(workflow).jobs['flyway-migrations'];
expect(parsed.if).toBeUndefined();
for (const fragment of [
"github.event_name != ''pull_request''",
'github.event.pull_request.head.repo.full_name == github.repository',
"vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true''",
'fromJSON(\'\'["self-hosted", "linux", "x64", "ecs-qwen"]\'\')',
"fromJSON(''[\"ubuntu-latest\"]'')",
]) {
expect(block).toContain(fragment);
}
// The merge result is the point: keep the default merge-ref checkout on
// the pool too, never the refs/pull/N/head the build lanes use.
expect(block).toContain('actions/checkout@');
expect(block).not.toContain('refs/pull/');
const steps = parsed.steps.map((s) => s.name);
expect(steps.indexOf('Restore workspace ownership')).toBeLessThan(
steps.indexOf('Checkout'),
);
expect(block).toContain(
"run: 'node scripts/check-flyway-migrations.js packages/sdk-java/managed-agent-server packages/sdk-java/runtime-broker packages/sdk-java/qwencode'",
);
});
it('scans every sdk-java module that owns a db/migration directory', () => {
// The guard's premise is the shared classpath:db/migration namespace —
// managed-agent-server depends on runtime-broker, so one invocation must
// name every module that owns a migration sequence. Derive the
// expectation from the tree: a module that grows a db/migration without
// joining the invocation turns this red.
const owners = new Set();
for (const entry of readdirSync('packages/sdk-java', {
withFileTypes: true,
})) {
if (!entry.isDirectory()) continue;
for (const location of [
'src/main/resources/db/migration',
'src/main/java/db/migration',
]) {
if (existsSync(join('packages/sdk-java', entry.name, location))) {
owners.add(`packages/sdk-java/${entry.name}`);
}
}
}
expect(owners.size).toBeGreaterThan(0);
const block = job('flyway-migrations');
for (const owner of owners) {
expect(block).toContain(owner);
}
});
it('triggers the workflow when the guard script itself changes', () => {
const yml = parse(workflow);
for (const event of ['pull_request', 'push']) {
expect(yml.on[event].paths).toContain(
'scripts/check-flyway-migrations.js',
);
}
});
});
// #13506: the pool-routed legs inherited only the bare ownership restore
// while ci.yml grew the rest of its pre-checkout hygiene across
// recorded incidents — the safe.directory trust after #12648 and the
// stale-.qwen sweep after the 33146730771 checkout poisoning. A leftover
// review/autofix residue on a shared ECS host failed this workflow's
// Checkout step on main. Pin every pool job to restore → sweep → checkout
// and pin both step bodies byte-identical to the ci.yml copies so the two
// files cannot drift apart (ci.yml's own copies are pinned the same way in
// scripts/tests/review-worktree-cleanup-workflow.test.js).
describe('SDK Java pre-checkout hygiene on the ECS pool', () => {
const parsed = parse(workflow);
const ci = parse(readFileSync('.github/workflows/ci.yml', 'utf8'));
const ciSteps = Object.values(ci.jobs).flatMap((job) => job.steps ?? []);
const ciSweep = ciSteps.find(
(s) => s.name === 'Clean stale .qwen before checkout',
)?.run;
const ciRestore = ciSteps.find(
(s) => s.name === 'Restore workspace ownership',
)?.run;
const poolJobs = ['test', 'flyway-migrations', 'daemon-e2e'];
it.each(poolJobs)(
'restores ownership, sweeps stale .qwen, then checks out in the %s job',
(name) => {
const steps = parsed.jobs[name].steps;
const names = steps.map((s) => s.name);
const restoreIdx = names.indexOf('Restore workspace ownership');
const sweepIdx = names.indexOf('Clean stale .qwen before checkout');
const checkoutIdx = steps.findIndex((s) =>
String(s.uses ?? '').includes('actions/checkout'),
);
expect(restoreIdx, name).toBeGreaterThanOrEqual(0);
expect(sweepIdx, name).toBeGreaterThan(restoreIdx);
expect(sweepIdx, name).toBeLessThan(checkoutIdx);
// The matrix legs on fresh hosted VMs never take this branch, and
// Windows would run it under pwsh — the recipe is bash.
expect(steps[sweepIdx].if).toBe(
"${{ runner.environment == 'self-hosted' }}",
);
},
);
it.each(poolJobs)(
'keeps the %s restore and sweep bodies byte-identical to ci.yml',
(name) => {
const steps = parsed.jobs[name].steps;
const restore = steps.find(
(s) => s.name === 'Restore workspace ownership',
)?.run;
const sweep = steps.find(
(s) => s.name === 'Clean stale .qwen before checkout',
)?.run;
expect(ciRestore).toBeDefined();
expect(ciSweep).toBeDefined();
expect(restore, name).toBe(ciRestore);
expect(sweep, name).toBe(ciSweep);
},
);
it.each(poolJobs)(
'trusts the workspace as a git safe.directory in the %s job',
(name) => {
const restore = parsed.jobs[name].steps.find(
(s) => s.name === 'Restore workspace ownership',
)?.run;
expect(restore).toContain(
'git config --global --add safe.directory "$GITHUB_WORKSPACE"',
);
},
);
});