mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* fix(ci): apply the apt-mirror probe to the release docker lane too The release docker integration lane builds the same sandbox image as the E2E leg but never selected an apt mirror, and it prunes the builder cache on every run so its apt layers are always cold. On mainland pool hosts that rebuilt against the throttled deb.debian.org path (11-45 kB/s) — the same defect #16214 fixed for E2E, left unfixed here because it was out of scope (#16271). Extract the probe (apt_mirror_probe_rate / select_apt_mirror) and a new apply_apt_mirror helper into a shared .github/scripts/apt-mirror-probe.sh, sourced by both run-e2e-tests.sh and run-release-docker-integration.sh. Each calls apply_apt_mirror before build:sandbox, so a mainland host builds against the VPC intranet mirror (6-9 MB/s, verified for E2E) while a healthy default is left untouched. The Dockerfile APT_MIRROR ARG already exists from #16218. Contract tests read the shared probe and pin the candidate loop and the source statement (not substrings the comments also satisfy); both callers must apply the probe before the build. Mutation-verified: removing the release call, a candidate, or a source line each reddens the suite. * fix(ci): carry the apt-mirror probe into the shard-retry test rig run-e2e-tests.sh now sources its sibling apt-mirror-probe.sh at startup. The shard-retry meta-test copies only run-e2e-tests.sh into a temp dir, so the source resolved to a missing file and 'set -e' aborted before any shard ran — all eight cases saw zero npm calls. Copy the probe file alongside the script in the rig so the source resolves; sourcing only defines functions (no network at source time) and the sandbox:none path never calls apply_apt_mirror, so the rig stays hermetic. Witness: CI at315733feshowed all 8 e2e-shard-retry cases failing (npmCalls expected 2, got 0); with the probe carried into the rig all 8 pass locally. eslint exit 0, prettier clean. * fix(ci): address #16278 review — hermetic probe test, shebang, scope header R1-1: the release-lane exec harness now stubs curl (per-URL rates) so the suite no longer probes the live network; the npm stub logs BUILD_SANDBOX_FLAGS and two execution tests assert the fast mirror is passed as --build-arg APT_MIRROR with the CI label preserved, and that an all-slow probe adds no mirror arg. Flipping the mirror-selection guard reddens them. R1-2: add the inert shebang so file(1) reports text/x-shellscript and the shellcheck lane enrols the sourced probe; a lint test pins the mime type. R1-3: narrow the header to the two covered build sites and record the third cold caller (release-sdk.yml build-sdk) as out of scope, tracked in #16317. * test(ci): drive the shellcheck lane for the probe; copy scripts dir in shard harness R2-1: the shebang test now runs the lane's own candidate selection with only shellcheck stubbed, so a filter change dropping the probe is caught (removing the shebang reddens it). R2-2: the shard-retry harness copies the whole .github/scripts dir so any sourced sibling comes along instead of a hand-maintained file list. * merge(ci): sync origin/main to pick up the pinned COREPACK_HOME gate The lint-gate-freshness lane went red on9b3d131cdbbecause main moved .github/workflows/ci.yml at2b9c4a98f4, and the shard-4 Linux lane died on the very corepack 'pnpm.mjs' miss that commit fixes. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw2t0at7dy * docs(ci): stop promising the release lane a benefit its routed host cannot get `integration_docker` is pinned to `ecs-qwen-hk4-host`, or `ubuntu-latest` when the ECS pool is unavailable. On the Hong Kong host `deb.debian.org` already answers at MB/s and the VPC mirror is unreachable, so `select_apt_mirror` correctly keeps the built-in source; on a hosted runner the same is true. The call-site comment asserted a mainland benefit this call site cannot obtain, and the always-cold builder cache it sits next to is unchanged. The comment now says what the call does: apply the same probe the E2E leg uses, so a host whose default is throttled builds against a faster endpoint, while a host whose default is already fast keeps it. The `Fixes #16271` claim is narrowed to `Refs #16271` in the PR body — the always-cold half stays open. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw420o4bfo * fix(ci): keep the release docker lane script executable The previous delivery of6218478b62went through the Git Data API, which recorded .github/scripts/run-release-docker-integration.sh as 100644 while the base has it 100755. release.yml runs these by bare path, so the mode is load-bearing, and scripts/tests/release-workflow.test.js > keeps the extracted release scripts executable already fails on it (Linux unit tests shard 3/8 on6218478b62). Restore the recorded mode to 100755. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw48g5gafx * test(ci): stage the probe fixture at its real path (#16278 R2-1) The rewritten case kept its fixture at the temp-repo root, where the lane's `^([^.]+)` candidate alternative already matches, so deleting the `.*\.(sh|zsh|bash)` arm left it green. Put the file at `.github/scripts/apt-mirror-probe.sh` — a leading dot makes the `.sh` arm the only way it can be selected — teach `setup()` to create parent directories for nested names, and assert the exact shellcheck argv. Two-arm check on the lane's own run string: as authored, exit 0 with `... --color=never .github/scripts/apt-mirror-probe.sh`; with the `.sh` arm deleted, exit 1 ("matched no shell-script candidates"). Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmw4cqh0dg3 --------- Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: yiliang114 <[email protected]>
184 lines
6.2 KiB
JavaScript
184 lines
6.2 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import {
|
|
chmodSync,
|
|
cpSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
// Executes the E2E runner with npm stubbed and the clock pinned, so the
|
|
// retry's exit-code semantics and budget gate are witnessed by bash rather
|
|
// than by shape assertions alone. Bash-driven, so it is excluded from the
|
|
// Windows lanes in vitest.config.ts.
|
|
describe('e2e workflow sandbox:none shard retry execution', () => {
|
|
const script = readFileSync('.github/scripts/run-e2e-tests.sh', 'utf8');
|
|
|
|
function runStepScript({ failCalls, elapsedSeconds }) {
|
|
const dir = mkdtempSync(join(tmpdir(), 'qwen-e2e-retry-'));
|
|
try {
|
|
// Copy the whole scripts dir so any sibling the runner sources comes
|
|
// along, instead of hand-maintaining a per-file list (#16278 R2-2). The
|
|
// probe lands at dir/apt-mirror-probe.sh, where dirname BASH_SOURCE looks.
|
|
cpSync('.github/scripts', dir, { recursive: true });
|
|
const callCountFile = join(dir, 'npm-call-count');
|
|
const npmStub = join(dir, 'npm');
|
|
writeFileSync(callCountFile, '0');
|
|
writeFileSync(
|
|
npmStub,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'call=$(( $(cat "$NPM_CALL_COUNT_FILE") + 1 ))',
|
|
'printf "%s" "$call" > "$NPM_CALL_COUNT_FILE"',
|
|
'for bad in $NPM_FAIL_CALLS; do',
|
|
' [ "$call" = "$bad" ] && exit 1',
|
|
'done',
|
|
'exit 0',
|
|
].join('\n'),
|
|
);
|
|
chmodSync(npmStub, 0o755);
|
|
// The clock is pinned so the budget-gate boundary cases are exact:
|
|
// with the real clock, elapsed only ever grows between this process
|
|
// and the script's `date +%s`, which would race the 2100s threshold.
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const dateStub = join(dir, 'date');
|
|
writeFileSync(
|
|
dateStub,
|
|
['#!/usr/bin/env bash', `printf '%s' '${now}'`].join('\n'),
|
|
);
|
|
chmodSync(dateStub, 0o755);
|
|
const scriptFile = join(dir, 'run-e2e-tests.sh');
|
|
writeFileSync(scriptFile, script);
|
|
let exitCode = 0;
|
|
let output = '';
|
|
try {
|
|
output = execFileSync(
|
|
'bash',
|
|
['-e', scriptFile, 'sandbox:none', '1/3'],
|
|
{
|
|
env: {
|
|
...process.env,
|
|
PATH: `${dir}:${process.env.PATH}`,
|
|
NPM_CALL_COUNT_FILE: callCountFile,
|
|
NPM_FAIL_CALLS: failCalls,
|
|
E2E_JOB_START_EPOCH: String(now - elapsedSeconds),
|
|
},
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
} catch (err) {
|
|
exitCode = err.status;
|
|
output = `${err.stdout ?? ''}${err.stderr ?? ''}`;
|
|
}
|
|
return {
|
|
exitCode,
|
|
output,
|
|
npmCalls: Number(readFileSync(callCountFile, 'utf8')),
|
|
};
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
it('runs a green shard straight through with no retry noise', () => {
|
|
// The green first-attempt path needs its own witness: without one, an
|
|
// unconditional pre-gate side effect (a spurious ::warning:: before
|
|
// `run_shard || {`) ships with every other witness green.
|
|
const { exitCode, npmCalls, output } = runStepScript({
|
|
failCalls: '',
|
|
elapsedSeconds: 1200,
|
|
});
|
|
expect(npmCalls).toBe(2);
|
|
expect(output).not.toContain('::warning::');
|
|
expect(output).not.toContain('::error::');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it('retries a shard that dies once and passes on the second attempt', () => {
|
|
// The transient class the retry exists for: first attempt dead, re-run
|
|
// green (runs 33293739505, 33302550436, 33317457036).
|
|
const { exitCode, npmCalls, output } = runStepScript({
|
|
failCalls: '1',
|
|
elapsedSeconds: 1200,
|
|
});
|
|
expect(npmCalls).toBe(3);
|
|
expect(output).toContain('::warning::');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it('retries when the isolated suites die once', () => {
|
|
const { exitCode, npmCalls, output } = runStepScript({
|
|
failCalls: '2',
|
|
elapsedSeconds: 1200,
|
|
});
|
|
expect(npmCalls).toBe(4);
|
|
expect(output).toContain('::warning::');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it('keeps the step red when the shard fails both attempts', () => {
|
|
// A deterministic failure must not be absorbed: appending `|| true` to
|
|
// the brace group turns this red.
|
|
const { exitCode, npmCalls } = runStepScript({
|
|
failCalls: '1 2',
|
|
elapsedSeconds: 1200,
|
|
});
|
|
expect(npmCalls).toBe(2);
|
|
expect(exitCode).not.toBe(0);
|
|
});
|
|
|
|
it('keeps the step red when the isolated suites fail both attempts', () => {
|
|
const { exitCode, npmCalls } = runStepScript({
|
|
failCalls: '2 4',
|
|
elapsedSeconds: 1200,
|
|
});
|
|
expect(npmCalls).toBe(4);
|
|
expect(exitCode).not.toBe(0);
|
|
});
|
|
|
|
it('retries at exactly the 2100s budget-gate threshold', () => {
|
|
// The gate admits a retry at elapsed <= 2100. Threshold mutations in
|
|
// either direction must not ship silently between the 1200/3000 probes.
|
|
const { exitCode, npmCalls, output } = runStepScript({
|
|
failCalls: '1',
|
|
elapsedSeconds: 2100,
|
|
});
|
|
expect(npmCalls).toBe(3);
|
|
expect(output).toContain('::warning::');
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it('refuses a retry one second past the 2100s budget-gate threshold', () => {
|
|
const { exitCode, npmCalls, output } = runStepScript({
|
|
failCalls: '1',
|
|
elapsedSeconds: 2101,
|
|
});
|
|
expect(npmCalls).toBe(1);
|
|
expect(output).toContain('::error::');
|
|
expect(exitCode).not.toBe(0);
|
|
});
|
|
|
|
it('fails fast when the remaining job budget cannot fit a retried shard', () => {
|
|
// 3000s spent of the 3600s job budget leaves 10 minutes — the exact
|
|
// shape of runs 33293739505 and 33302550436. An unconditional retry
|
|
// would call npm again and be cancelled mid-flight by timeout-minutes.
|
|
const { exitCode, npmCalls, output } = runStepScript({
|
|
failCalls: '1',
|
|
elapsedSeconds: 3000,
|
|
});
|
|
expect(npmCalls).toBe(1);
|
|
expect(output).toContain('::error::');
|
|
expect(exitCode).not.toBe(0);
|
|
});
|
|
});
|