Skip to content
11 changes: 9 additions & 2 deletions .github/workflows/qwen-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -575,15 +575,16 @@ jobs:
sed -i "s/qwen3\.7-max/${ESCAPED}/g" "$TARGET"

- name: 'Ensure qwen CLI'
id: 'ensure_qwen'
run: |-
set -euo pipefail
if command -v qwen >/dev/null 2>&1; then
echo "qwen already installed:"
qwen --version
else
npm install -g --loglevel=error --no-audit @qwen-code/qwen-code@latest
qwen --version
fi
qwen --version
echo "version=$(qwen --version)" >> "${GITHUB_OUTPUT}"

- name: 'Run Qwen Triage'
id: 'triage'
Expand All @@ -600,6 +601,12 @@ jobs:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ vars.QWEN_TRIAGE_MODEL || vars.QWEN_PR_REVIEW_MODEL }}'
# Pin the action's unconditional global reinstall to the version
# this job already runs. Left at `latest`, the action installs with
# --prefer-offline and resolves the dist-tag from the runner's
# persistent npm cache, which lags npm publishes and has downgraded
# the shared ECS box back to a stale release.
qwen_cli_version: '${{ steps.ensure_qwen.outputs.version }}'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The triage version-pin — this PR's central bugfix — has no test, while the PR adds readFileSync/string-match tests for the other two workflow changes and the repo has an established convention of pinning workflow invariants this way (scripts/tests/qwen-triage-workflow.test.js, ~5500 lines, reads this same workflow into workflow). A repo-wide grep for qwen_cli_version / ensure_qwen / the version=$(qwen --version) capture matches only the action's docs — nothing in scripts/tests/. — Failure scenario: a future edit deletes the qwen_cli_version: line, or breaks the echo "version=$(qwen --version)" >> "${GITHUB_OUTPUT}" capture so the output resolves empty; the action's unconditional global reinstall then silently falls back to latest resolved through the runner's stale npm cache — the exact downgrade this PR fixes — with no failing test. Suggested fix (in scripts/tests/qwen-triage-workflow.test.js):

it('pins the action reinstall to the version the job already runs', () => {
  expect(workflow).toContain("id: 'ensure_qwen'");
  expect(workflow).toContain(
    'echo "version=$(qwen --version)" >> "${GITHUB_OUTPUT}"',
  );
  expect(workflow).toContain(
    "qwen_cli_version: '${{ steps.ensure_qwen.outputs.version }}'",
  );
});
中文说明

triage 的版本固定——本 PR 的核心修复——没有测试;而本 PR 为另外两处 workflow 改动都加了 readFileSync/字符串匹配测试,且仓库已有用这种方式固定 workflow 不变量的约定(scripts/tests/qwen-triage-workflow.test.js,约 5500 行,读取的正是同一个 workflow 并存入 workflow)。全仓库 grep qwen_cli_version / ensure_qwen / version=$(qwen --version) 捕获,只命中该 action 的文档,scripts/tests/ 中没有任何命中。— 失败场景:未来某次编辑删除 qwen_cli_version: 行,或破坏 echo "version=$(qwen --version)" >> "${GITHUB_OUTPUT}" 捕获使输出解析为空;action 的无条件全局重装会静默回退到经 runner 陈旧 npm 缓存解析的 latest——正是本 PR 修复的降级——且没有任何测试失败。建议的修复(在 scripts/tests/qwen-triage-workflow.test.js)见上方代码块。

— qwen3.8-max-preview via Qwen Code /review (v0.21.3)

# The input name is `settings` — this action version has no
# `settings_json` input. The old `settings_json:` block was silently
# dropped ("Unexpected input(s) 'settings_json'" in the run log), so
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,28 @@ jobs:
--notes-file "${NOTES_FILE}" \
${PRERELEASE_FLAG}

- name: 'Trigger ECS runner qwen update'
# Stable releases only: nightly/preview must not move the fleet.
if: |-
${{ github.repository == 'QwenLM/qwen-code' &&
needs.prepare.outputs.is_dry_run == 'false' &&
needs.prepare.outputs.npm_tag == 'latest' }}
Comment on lines +617 to +619

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This if: gate is the sole protection preventing a nightly/preview/dry-run release from firing the fleet-moving npm-published dispatch, but nothing pins it. The repo has an established convention of testing analogous workflow safety invariants (e.g. scripts/tests/install-script.test.js asserts the OSS "latest" pointer flips only after every asset is verified; scripts/tests/qwen-fleet-shepherd-workflow.test.js executes extracted bash to prove gate/dry-run logic). — Failure scenario: a future edit that flips npm_tag == 'latest' to != 'latest', or drops the is_dry_run == 'false' clause, would pass review green and move the ECS fleet to a non-stable version — the exact outcome the step's own comment forbids; the triggered update workflow installs whatever version it is handed, so there is no downstream guard. Consider adding a workflow-content test (e.g. scripts/tests/release-workflow.test.js) asserting this step's if: contains all three clauses. (The gate as written is correct — per AGENTS.md a missing test is a Suggestion, not a blocker.)

中文说明

[Suggestion](建议)这个 if: 门控是阻止 nightly/preview/dry-run 发布触发会移动 runner 集群的 npm-published dispatch 的唯一防护,但没有任何测试 pin 住它。仓库已有测试同类工作流安全不变量的约定(例如 scripts/tests/install-script.test.js 断言 OSS "latest" 指针仅在全部资产校验后才翻转;scripts/tests/qwen-fleet-shepherd-workflow.test.js 会执行抽取出的 bash 以验证门控/dry-run 逻辑)。— 失败场景:未来某次编辑把 npm_tag == 'latest' 改成 != 'latest',或删掉 is_dry_run == 'false' 子句,都能绿通过审并把 ECS 集群切到非稳定版本——正是该步骤自身注释所禁止的结果;被触发的 update workflow 会安装它收到的任何版本,因此没有下游防护。建议新增一个 workflow 内容测试(如 scripts/tests/release-workflow.test.js),断言此步骤的 if: 包含全部三个子句。(门控本身是正确的——按 AGENTS.md,缺少测试属于 Suggestion,而非 blocker。)

— qwen3.8-max-preview via Qwen Code /review

# The packages are already published; a dispatch failure must not
# fail the release — report it and let maintainers re-run the update
# workflow manually instead.
continue-on-error: true
env:
GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'
Comment on lines +623 to +625

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] With continue-on-error: true, a dispatch failure is masked as a successful step, so the publish job completes success and the notify_failure job (gated on needs.publish.result == 'failure') never fires. — Failure scenario: if gh api .../dispatches fails (transient GitHub 5xx, or an expired/rotated CI_BOT_PAT), the package is published and the release shows green, but the npm-published dispatch never fires, so the ECS fleet silently stays on the previous qwen version indefinitely — reproducing the exact problem this PR set out to fix. The only signal is a ::warning:: annotation on a step inside an otherwise-green run, routed to no one, so the documented "re-run the update workflow manually" fallback is never prompted. Keeping continue-on-error is the right call (a dispatch failure must not fail an already-published release); consider making the fallback reachable — e.g. emit ::error:: so it surfaces in run-level failure digests, or record explicitly at the notify_failure condition that this step's failures are deliberately invisible and the fleet must be reconciled via dispatch history.

中文说明

[Suggestion](建议)由于 continue-on-error: true,dispatch 失败会被掩盖为成功步骤,因此 publish job 以 success 完成,而 notify_failure job(以 needs.publish.result == 'failure' 为门控)永远不会触发。— 失败场景:如果 gh api .../dispatches 失败(GitHub 瞬时 5xx,或 CI_BOT_PAT 过期/被轮换),包已发布、release 显示为绿色,但 npm-published dispatch 从未发出,于是 ECS 集群会无限期地静默停留在上一个 qwen 版本——恰好重现了本 PR 要解决的问题。唯一的信号是一个绿色 run 中某步骤上的 ::warning:: 标注,它不会通知任何人,因此文档中"手动重跑 update workflow"的兜底永远不会被触发。保留 continue-on-error 是对的(dispatch 失败不应拖败一个已发布的 release);建议让这个兜底可达——例如输出 ::error:: 使其出现在 run 级失败摘要中,或在 notify_failure 的门控处明确记录:此步骤的失败是刻意对告警路径不可见的,需要通过 dispatch 历史来核对集群版本。

— qwen3.8-max-preview via Qwen Code /review

RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}'
run: |-
gh api "repos/${GITHUB_REPOSITORY}/dispatches" \
--method POST \
-f 'event_type=npm-published' \
-f "client_payload[version]=${RELEASE_VERSION}" || {
echo "::error::npm-published dispatch failed; run the 'Update ECS Runner Qwen' workflow manually."
exit 1
}

notify_failure:
name: 'Notify Release Failure'
runs-on: 'ubuntu-latest'
Expand Down
19 changes: 18 additions & 1 deletion .github/workflows/update-ecs-runner-qwen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,24 @@ jobs:
# carry a runner user's custom npm prefix into the sudo install.
(
cd "${RUNNER_TEMP:?}"
sudo env -u NPM_CONFIG_PREFIX npm install -g --registry=https://registry.npmjs.org "@qwen-code/qwen-code@${VERSION}"
# 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
if sudo env -u NPM_CONFIG_PREFIX npm install -g --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'
Expand Down
10 changes: 10 additions & 0 deletions scripts/tests/qwen-triage-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ describe('qwen-triage tmux workflow', () => {
expect(runStep).toContain("QWEN_HOME: '${{ runner.temp }}/qwen-home'");
});

it('pins the action reinstall to the version the job already runs', () => {
expect(workflow).toContain("id: 'ensure_qwen'");
expect(workflow).toContain(
'echo "version=$(qwen --version)" >> "${GITHUB_OUTPUT}"',
);
expect(workflow).toContain(
"qwen_cli_version: '${{ steps.ensure_qwen.outputs.version }}'",
);
});

it('passes triage output through env before bash reads it', () => {
const checkStep = step('Check triage response');

Expand Down
42 changes: 42 additions & 0 deletions scripts/tests/release-workflow.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';

const workflow = readFileSync('.github/workflows/release.yml', 'utf8');

describe('release workflow', () => {
it('fires the fleet-moving npm-published dispatch on stable releases only', () => {
// This gate is the sole protection keeping a nightly/preview/dry-run
// release from moving the ECS fleet; the triggered update workflow
// installs whatever version it is handed, so there is no downstream
// guard. Pin all three clauses together so dropping or inverting one
// fails review instead of silently shipping a non-stable fleet.
expect(workflow).toContain(
'if: |-\n' +
" ${{ github.repository == 'QwenLM/qwen-code' &&\n" +
" needs.prepare.outputs.is_dry_run == 'false' &&\n" +
" needs.prepare.outputs.npm_tag == 'latest' }}",
);
expect(workflow).toContain("-f 'event_type=npm-published'");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This dispatch test pins the event type but not the version payload. The payload line -f "client_payload[version]=${RELEASE_VERSION}" (release.yml:631) is the load-bearing half of the release→update integration: the update workflow reads github.event.client_payload.version and falls back to @qwen-code/qwen-code@latest when it is empty (update-ecs-runner-qwen.yml:36-39). A probe confirmed it: deleting the payload line leaves this test green. — Failure scenario: a future edit drops the payload line; the test stays green (event type still pinned) but the dispatch no longer carries the released version, so the fleet installs whatever latest resolves to instead of the just-published release — a wrong-version fleet update whenever the two diverge.

Suggested change
expect(workflow).toContain("-f 'event_type=npm-published'");
expect(workflow).toContain("-f 'event_type=npm-published'");
expect(workflow).toContain(
'-f "client_payload[version]=${RELEASE_VERSION}"',
);
中文说明

该 dispatch 测试只固定了事件类型,未固定版本 payload。payload 行 -f "client_payload[version]=${RELEASE_VERSION}"(release.yml:631)是 release→update 集成的承重一半:update workflow 读取 github.event.client_payload.version,为空时回退到 @qwen-code/qwen-code@latest(update-ecs-runner-qwen.yml:36-39)。探针已证实:删除该 payload 行后测试仍为绿色。— 失败场景:未来某次编辑删除 payload 行,测试仍绿(事件类型仍被固定),但 dispatch 不再携带发布版本,runner 集群会安装 latest 解析出的版本而非刚发布的版本——两者不一致时即一次错误版本的集群更新。上面的 suggestion 同时固定事件类型与版本 payload。

— qwen3.8-max-preview via Qwen Code /review (v0.21.3)

expect(workflow).toContain(
'-f "client_payload[version]=${RELEASE_VERSION}"',
);
});

it('keeps a dispatch failure from failing an already-published release', () => {
// The packages are published before this step runs, so it must not fail
// the release; but the failure must still surface (as an error, not a
// warning) so the fleet can be reconciled via a manual re-run.
expect(workflow).toContain(
'continue-on-error: true\n' +
' env:\n' +
" GITHUB_TOKEN: '${{ secrets.CI_BOT_PAT }}'",
);
expect(workflow).toContain('echo "::error::npm-published dispatch failed;');
});
});
14 changes: 14 additions & 0 deletions scripts/tests/update-ecs-runner-qwen-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,18 @@ describe('ECS runner qwen update workflow', () => {
expect(workflow).toContain('cd "${RUNNER_TEMP:?}"');
expect(workflow).toContain('sudo env -u NPM_CONFIG_PREFIX npm install -g');
});

it('annotates a retry and a terminal failure distinctly', () => {
// The final attempt must not log a "retrying" warning that never
// retries; a sustained failure ends with an explicit exhausted error.
Comment on lines +21 to +23

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This test's comment claims to guard that "the final attempt must not log a 'retrying' warning that never retries", but the two toContain assertions only pin the two echo strings as substrings anywhere in the file — the loop bound, the -lt 3 guard placement, and the trash-cleanup line are unpinned. — Failure scenario: moving echo "::warning::...retrying" above the if [[ "${attempt}" -lt 3 ]] guard makes attempt 3 log a "retrying" warning that never retries (the exact bug the comment names) while both assertions stay green; for attempt in 1 2 31 2 ships green while the error text still says "after 3 attempts"; and deleting sudo rm -rf "${PKG_DIR}"/.qwen-code-* is caught by no test. All three mutants were verified to ship green by a mutation probe. Pin the structure alongside the messages:

expect(workflow).toContain('for attempt in 1 2 3; do');
expect(workflow).toContain('if [[ "${attempt}" -lt 3 ]]; then');
expect(workflow).toContain('sudo rm -rf "${PKG_DIR}"/.qwen-code-*');
中文说明

此测试的注释声称要保护“最后一次尝试不会记录一个永不重试的 'retrying' 警告”,但两个 toContain 断言仅把两条 echo 字符串作为文件任意位置的子串固定下来——循环上界、-lt 3 守卫的位置以及垃圾清理行都未被固定。— 失败场景:把 echo "::warning::...retrying" 移到 if [[ "${attempt}" -lt 3 ]] 守卫之上,会使第 3 次尝试记录一个永不重试的 "retrying" 警告(正是注释所指出的 bug),而两个断言仍为绿;for attempt in 1 2 31 2 在错误文案仍写 "after 3 attempts" 的情况下绿灯通过;删除 sudo rm -rf "${PKG_DIR}"/.qwen-code-* 也没有任何测试能捕获。以上三种变异均经变异探针验证可绿灯通过。建议在固定消息的同时固定结构:

expect(workflow).toContain('for attempt in 1 2 3; do');
expect(workflow).toContain('if [[ "${attempt}" -lt 3 ]]; then');
expect(workflow).toContain('sudo rm -rf "${PKG_DIR}"/.qwen-code-*');

— qwen3.8-max-preview via Qwen Code /review

expect(workflow).toContain(
'echo "::warning::npm install attempt ${attempt} failed; retrying"',
);
expect(workflow).toContain(
'echo "::error::npm install of @qwen-code/qwen-code@${VERSION} failed after 3 attempts"',
);
expect(workflow).toContain('for attempt in 1 2 3; do');
expect(workflow).toContain('if [[ "${attempt}" -lt 3 ]]; then');
expect(workflow).toContain('sudo rm -rf "${PKG_DIR}"/.qwen-code-*');
});
});
Loading