mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-10-26 00:39:51 +00:00
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>
229 lines
9.3 KiB
JavaScript
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"',
|
|
);
|
|
},
|
|
);
|
|
});
|