Files
qwen-code/.github/workflows/update-ecs-runner-qwen.yml
易良 e2cdc5edd6 fix(ci): pin the fleet install to the /usr/local prefix (#11593)
* fix(ci): pin the fleet install to the /usr/local prefix

The fleet updater installs with `sudo env -u NPM_CONFIG_PREFIX npm
install -g`, which clears the environment variable but not npm's own
computed default prefix. On hk-4 and hk-5 that default is a custom Node
directory, `/usr/local/lib/nodejs/node-v22.23.2-linux-x64`, so the new
version installs to a bin directory nothing on the runner's PATH reads
while `/usr/local/bin/qwen` keeps pointing at the previous install.

The failure is silent in the only place it could be caught early: npm
reports "changed 16 packages" exactly as it does on the hosts where the
prefixes happen to agree, and only the verify step notices, by which
point the log offers no hint as to where the install went. hk-4 and hk-5
sat on 0.23.0 through 0.23.1, 0.23.2 and 0.23.3 that way, five
consecutive runs, each of them green through the install step.

`--prefix` is a command-line option and outranks the computed default,
root's npmrc and /etc/npmrc alike. Verified on hk-4: the same install
with the prefix pinned moved it from 0.23.0 to 0.23.3 immediately.

This restores c64936d6, which carried the same one-line change and was
dropped from #10921 to keep that PR's scope on the npm publish timeout.

* test(ci): pin the /usr/local prefix so it cannot be dropped again

Nothing asserted on this flag, which is how it left the tree the first
time: it was written, reviewed, and then scoped out of #10921 with no
test objecting, and four releases went to two pools that never received
them.

The existing case checks that NPM_CONFIG_PREFIX is cleared, and that
substring survives either way, so it passes with or without the flag.
This adds the assertion that actually fails on the version of the
workflow that shipped.
2026-09-11 06:44:21 +00:00

174 lines
7.2 KiB
YAML

name: 'Update ECS Runner Qwen'
on:
push:
branches: ['main']
paths: ['.github/workflows/update-ecs-runner-qwen.yml']
workflow_dispatch:
inputs:
version:
description: 'Optional. Empty means latest from npm.'
required: false
default: ''
type: 'string'
repository_dispatch:
types: ['npm-published']
permissions:
contents: 'read'
jobs:
# Resolving on a hosted runner, once, keeps the registry wait below off the
# ECS pools (which queue behind real review/triage work) and guarantees every
# pool installs the SAME version even when their jobs start hours apart.
resolve:
name: 'Resolve version'
if: "${{ github.repository == 'QwenLM/qwen-code' }}"
runs-on: 'ubuntu-latest'
timeout-minutes: 100
outputs:
version: '${{ steps.version.outputs.version }}'
steps:
- name: 'Resolve version'
id: 'version'
env:
INPUT_VERSION: '${{ inputs.version || github.event.client_payload.version }}'
# npm publishes asynchronously: `npm publish --provenance` returns
# "Your package is being processed and may take a few minutes to
# become available", and release.yml dispatches this workflow as soon
# as it returns. v0.23.0 took 55m22s to become resolvable, so the
# previous 25-minute budget expired while the package was still in
# flight. Wait up to 90 minutes and treat anything longer as an
# anomalous publish.
RESOLVE_TIMEOUT_SECONDS: '5400'
RESOLVE_INTERVAL_SECONDS: '30'
run: |-
set -euo pipefail
specifier="@qwen-code/qwen-code@${INPUT_VERSION#v}"
if [[ "${specifier}" == '@qwen-code/qwen-code@' ]]; then
specifier='@qwen-code/qwen-code@latest'
fi
# Per-attempt stderr is held back so ~50 identical 404 blocks do not
# bury the log; the last one is replayed when the wait gives up.
err_log="$(mktemp)"
deadline=$(( SECONDS + RESOLVE_TIMEOUT_SECONDS ))
while :; do
version="$(npm view "${specifier}" version 2>"${err_log}" | tail -n 1)" || true
if [[ -n "${version}" ]]; then
break
fi
if (( SECONDS >= deadline )); then
cat "${err_log}" >&2
echo "::error::No published qwen version matches '${INPUT_VERSION:-latest}' after ${RESOLVE_TIMEOUT_SECONDS}s."
exit 1
fi
echo "'${specifier}' is not on the registry yet; retrying in ${RESOLVE_INTERVAL_SECONDS}s."
sleep "${RESOLVE_INTERVAL_SECONDS}"
done
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "Resolved qwen version: ${version}"
update:
name: 'Update Qwen on ${{ matrix.runner }}'
needs: 'resolve'
if: "${{ github.repository == 'QwenLM/qwen-code' }}"
strategy:
matrix:
runner:
[
'ecs-update-hk-1',
'ecs-update-hk-2',
'ecs-update-hk-3',
'ecs-update-hk-4',
'ecs-update-hk-5',
]
fail-fast: false
runs-on: ['self-hosted', 'linux', 'x64', '${{ matrix.runner }}']
concurrency:
group: 'update-ecs-runner-qwen-${{ matrix.runner }}'
cancel-in-progress: false
timeout-minutes: 10
steps:
- name: 'Update qwen'
env:
VERSION: '${{ needs.resolve.outputs.version }}'
run: |-
set -euo pipefail
# The runner service resolves the system-wide qwen binary. Do not
# carry a runner user's custom npm prefix into the sudo install.
(
cd "${RUNNER_TEMP:?}"
# All runner processes of a region share one machine, so another
# job's concurrent `npm install -g` can race npm's rename of the
# package dir (ENOTEMPTY). Clear trash left by crashed installs
# and retry through the seconds-long race window.
PKG_DIR='/usr/local/lib/node_modules/@qwen-code'
for attempt in 1 2 3; do
sudo rm -rf "${PKG_DIR}"/.qwen-code-* 2>/dev/null || true
# --prefix pins the install to /usr/local: on the hk-4/hk-5
# machines root's npm global prefix is a custom Node directory
if sudo env -u NPM_CONFIG_PREFIX npm install -g --prefix /usr/local --registry=https://registry.npmjs.org "@qwen-code/qwen-code@${VERSION}"; then
exit 0
fi
if [[ "${attempt}" -lt 3 ]]; then
echo "::warning::npm install attempt ${attempt} failed; retrying"
sleep $((attempt * 10))
else
echo "::error::npm install of @qwen-code/qwen-code@${VERSION} failed after 3 attempts"
fi
done
exit 1
)
- name: 'Verify version'
env:
VERSION: '${{ needs.resolve.outputs.version }}'
run: |-
set -euo pipefail
qwen_path="$(command -v qwen)"
actual="$("${qwen_path}" --version)"
echo "qwen path: ${qwen_path}"
echo "qwen version: ${actual}"
test "${actual}" = "${VERSION}"
# A failed fleet update is otherwise invisible. `qwen-code-pr-review.yml` and
# `qwen-triage.yml` install the CLI only when it is absent, and on a
# self-hosted runner it never is, so a pool that misses an update keeps
# serving PRs on the old version indefinitely with nothing to distinguish it
# from a healthy one — on v0.22.3 that went unnoticed for a full day.
# `main-ci-failure-issue.yml` cannot cover this: it watches test suites on
# `main`, and this workflow runs off `repository_dispatch`.
report_failure:
name: 'Report a stale fleet'
needs: ['resolve', 'update']
# `cancelled` is deliberately not a failure: a newer dispatch supersedes an
# older one's still-pending legs through the per-pool concurrency group,
# which is routine and says nothing about the fleet's state.
if: "${{ always() && github.repository == 'QwenLM/qwen-code' && (needs.resolve.result == 'failure' || needs.update.result == 'failure') }}"
# Hosted, not the ECS pool: this job reports that the pools are broken, and
# queueing the report behind them would be the same trap it documents.
runs-on: 'ubuntu-latest'
timeout-minutes: 5
permissions:
# A job-level block replaces the workflow's, so checkout needs contents
# spelled out here even though the job only files an issue.
contents: 'read'
# Read this run's per-pool job conclusions.
actions: 'read'
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
with:
persist-credentials: false
- name: 'File or update the stale-fleet issue'
env:
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
REPO: '${{ github.repository }}'
RUN_ID: '${{ github.run_id }}'
RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
VERSION: '${{ needs.resolve.outputs.version }}'
DEDUP_LABEL: 'scope/ci-cd'
run: 'bash .github/scripts/ecs-fleet-update-failure-issue.sh'