Files
qwen-code/.github/scripts/run-e2e-tests.sh
45f71b3714 fix(ci): apply the apt-mirror probe to the release docker lane too (#16278)
* 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 at 315733fe showed 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 on 9b3d131cdb because main moved
.github/workflows/ci.yml at 2b9c4a98f4, 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 of 6218478b62 went 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 on 6218478b62).

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]>
2026-11-06 15:42:59 +08:00

169 lines
6.9 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# shellcheck source=.github/scripts/apt-mirror-probe.sh
source "$(dirname "${BASH_SOURCE[0]}")/apt-mirror-probe.sh"
sandbox="${1:?usage: run-e2e-tests.sh <sandbox> <shard>}"
shard="${2:?usage: run-e2e-tests.sh <sandbox> <shard>}"
cleanup_e2e_job() {
if [ "$sandbox" = 'sandbox:docker' ]; then
container_ids="$(timeout 30 docker ps -aq --filter "label=org.qwen-code.ci.owner=${E2E_CONTAINER_OWNER}" 2>/dev/null)" || container_ids=''
if [ -n "$container_ids" ]; then
printf '%s\n' "$container_ids" | xargs -r timeout 60 docker rm -f > /dev/null 2>&1 || echo "::warning::failed to remove E2E containers for ${E2E_CONTAINER_OWNER}"
fi
fi
if [ -n "${QWEN_CI_TMPDIR:-}" ]; then
rm -rf "$QWEN_CI_TMPDIR" 2>/dev/null || true
fi
}
trap cleanup_e2e_job EXIT
trap 'exit 1' INT TERM
if [ "$sandbox" = 'sandbox:docker' ]; then
sandbox_image="$(node -p "require('./packages/cli/package.json').config.sandboxImageUri")-e2e-${GITHUB_SHA}"
# 7>&- 8>&- 9>&- here and on the test command below: a lock
# lives on the open file description, so a descendant that
# inherits the descriptor and outlives this job keeps holding it.
# Closing it in the child costs nothing — this shell keeps the
# lock — and leaves no way to leak one onto the host.
build_image() {
npm run build:sandbox -- -s --no-prune -i "$sandbox_image" 7>&- 8>&- 9>&-
}
if [ "$RUNNER_ENVIRONMENT" = 'self-hosted' ]; then
mkdir -p "${HOME}/.cache/qwen-code-ci"
# Host daemon lock, shared for the whole step and never
# upgraded: it only keeps the age-based prune (which takes it
# exclusively, non-blocking) off a daemon with Docker work in
# flight. #10605 upgraded it to exclusive to build an image,
# which starves that shard behind the test-phase readers of
# every other run on the host: in run 33637097713 shard 1/3
# gave up on the write lock after 30 minutes while a shard of
# run 33638984513 held the shared lock through its tests, and
# shard 2/3 then timed out on the coordinator lock shard 1/3
# was still holding.
exec 9>"${HOME}/.cache/qwen-code-ci/docker-sandbox-daemon.lock"
if ! flock --shared --wait 1800 9; then
echo "::error::docker daemon read lock not acquired within 30 minutes"
exit 1
fi
# Per-commit coordinator: one job builds the image; a concurrent
# run at the same SHA waits here and then finds it present.
exec 8>"${HOME}/.cache/qwen-code-ci/docker-sandbox-build-e2e-${GITHUB_SHA}.lock"
if ! flock --wait 1800 8; then
echo "::error::docker build coordinator lock not acquired within 30 minutes"
exit 1
fi
fi
if ! docker image inspect "$sandbox_image" > /dev/null 2>&1; then
if [ "$RUNNER_ENVIRONMENT" = 'self-hosted' ]; then
# Host build mutex: keeps concurrent image builds off one
# daemon. Held only while an image is prepared, never while
# tests run, so the wait is bounded by a build.
exec 7>"${HOME}/.cache/qwen-code-ci/docker-sandbox-build.lock"
if ! flock --wait 1800 7; then
echo "::error::docker build lock not acquired within 30 minutes"
exit 1
fi
fi
# Label- and age-filtered, so it cannot touch the image a
# concurrent shard is testing.
timeout 20m docker image prune --all --force --filter 'label=org.qwen-code.ci.sandbox=true' --filter 'until=24h' || echo "::warning::old CI sandbox image cleanup failed on ${RUNNER_NAME:-this runner}"
apply_apt_mirror
if ! build_image; then
echo "::warning::sandbox image build failed; retrying once"
build_image
fi
if [ "$RUNNER_ENVIRONMENT" = 'self-hosted' ]; then
flock --unlock 7
exec 7>&-
fi
else
echo "Reusing sandbox image for ${GITHUB_SHA}"
fi
sandbox_image_id="$(docker image inspect --format '{{.Id}}' "$sandbox_image")"
export QWEN_SANDBOX_IMAGE="$sandbox_image_id"
if [ "$RUNNER_ENVIRONMENT" = 'self-hosted' ]; then
flock --unlock 8
exec 8>&-
fi
fi
export TMPDIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
if [ "${RUNNER_OS:-}" = 'Linux' ]; then
QWEN_CI_TMPDIR="$(mktemp -d /var/tmp/qwen-ci-XXXXXX 2>/dev/null || true)"
if [ -n "$QWEN_CI_TMPDIR" ]; then
TMPDIR="$QWEN_CI_TMPDIR"
export TMPDIR
fi
fi
# QWEN_E2E_RENDERER=ink pins the baseline leg of the renderer
# matrix; the opentui leg runs in e2e-interactive-opentui below.
# The docker leg runs vitest directly instead of through
# test:integration:sandbox:docker: that script would rebuild the image
# the step above just built.
# Keep suites that launch many real CLI/daemon subprocesses out of
# the three-fork batch so they cannot starve each other's local control
# and HTTP requests.
run_vitest() {
if [ "$sandbox" = 'sandbox:docker' ]; then
npx cross-env QWEN_E2E_RENDERER=ink QWEN_SANDBOX=docker vitest run --root ./integration-tests "$@" 9>&-
else
QWEN_E2E_RENDERER=ink npm run test:integration:sandbox:none -- "$@"
fi
}
bulk_args=(
--exclude '**/interactive/cron-interactive.test.ts'
--exclude '**/channel-plugin.test.ts'
--exclude '**/chat-transcript-document.test.ts'
--exclude '**/qwen-serve-routes.test.ts'
--exclude '**/sdk-typescript/**'
--poolOptions.forks.maxForks=3
--shard="$shard"
)
run_shard() {
run_vitest "${bulk_args[@]}" &&
run_vitest sdk-typescript cli/qwen-serve-routes.test.ts --poolOptions.forks.maxForks=1
}
if [ "$sandbox" = 'sandbox:docker' ]; then
run_shard
else
# One bounded retry: pool runners' sandbox:none shards die under
# shared-host pressure with every test green and no vitest FAIL
# line — runs 33293739505, 33302550436 and 33317457036 failed
# this leg on three different hosts while sibling shards of the
# same runs passed, and the shard passes on re-run. The same
# transient class the sandbox-image build retry above covers
# (#10355). A deterministic test failure fails both attempts and
# keeps the shard red. The retry is budget-gated on job-elapsed
# time: two of those three runs died with ~10-12 of the 60 job
# minutes left, less than one shard-time, where a retry can only
# be cancelled mid-flight by timeout-minutes. The docker leg
# keeps no retry: two ~30min attempts would outrun the job's
# timeout-minutes.
run_shard || {
elapsed=$(( $(date +%s) - ${E2E_JOB_START_EPOCH:-0} ))
# Budget gate: a retry needs a full shard-time inside the job's
# timeout-minutes. 3600s minus a 25-minute reserve — the worst
# measured shard is ~21min under shared-host pressure — leaves
# 2100s; past that, GitHub cancels the retry mid-flight, burns
# pool time, and the failure degrades to a timeout signature.
if (( elapsed > 2100 )); then
echo "::error::sandbox:none shard failed on ${RUNNER_NAME:-this runner} after ${elapsed}s of the 3600s job budget — not enough left for a retry"
exit 1
fi
echo "::warning::sandbox:none shard failed on ${RUNNER_NAME:-this runner} after ${elapsed}s; retrying once (transient shared-host pressure class)"
run_shard
}
fi