On reused GitHub-hosted runners, a previous job (e.g. verify/tmux on
the same runner pool) can leave .qwen/ files with restrictive
permissions (chmod -R a-w). The next job's checkout step then fails
with "error: unable to unlink old '.qwen/...': Permission denied"
because git cannot delete the read-only files during workspace
cleanup.
Add a pre-checkout step that chmods .qwen/ writable and removes it
before the checkout action runs. This is a no-op on fresh runners
where .qwen/ doesn't exist yet.
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: 易良 <1204183885@qq.com>
* fix(ci): stop cancelling in-progress E2E runs on main
Merges land on main roughly every 18 minutes (median) while a full E2E run takes about 40, so cancelling the in-flight run on every push starved the suite: across the last 100 push runs, 67 were cancelled and only 25 ever reported a result. The nightly regression was the only reliable signal, and each cancelled run still burned roughly 10 minutes on three runners before dying.
Turning cancellation off for main hands the coalescing to GitHub's concurrency queue, which keeps at most one pending run per group and cancels the previously pending one. The queue therefore collapses to the newest tree on its own while the in-flight run always finishes, so every result covers the batch of commits merged since the last one — bisect that range when it goes red. Dev branches keep cancelling superseded runs, where only the latest push matters.
* fix(ci): dedupe main CI failure issues by failing test
The autofix issue for a red main was deduped on the commit SHA, so a standing failure opened a brand new issue on every merge: one broken E2E test on 2026-07-26 produced six duplicate issues in twelve hours, each pointing the autofix agent at an unrelated commit.
The failing tests are now identified from the logs of the failed jobs and used as the dedupe key, with one marker per test so a failure set that grows still matches the issue that already tracks part of it. Later commits hitting the same failure are appended to that issue as recurrences (bounded, newest first) instead of opening another one, and notes written by a human or the agent are preserved when the machine-owned trailer is refreshed. Runs with no identifiable test — an install or build break — keep the previous per-commit behaviour.
* fix(ci): keep the failure analysis out of the bot-PAT job
Identifying the failing tests means running a helper from the repository, and the workflow deliberately checked out nothing so that the job holding the bot PAT could never execute repository code — an invariant its own test enforces.
Rather than weaken it, the work is split: a read-only job checks out the tree, reads the failed run's logs, finds any issue that already tracks the failure and renders the title and body, handing both over as outputs. The job with the PAT keeps checking out nothing and only writes what it was given. The invariant test now asserts that separation — the PAT job runs no repository code and holds only issue write — instead of banning checkout everywhere in the file.
* fix(ci): harden main CI failure dedupe per review (#7795)
- Match the `[run <id>]` link text instead of the run URL when deduping
recurrences: `/301` is a substring of `/3010`, so the URL match silently
deleted an unrelated run's line.
- Rebuild the machine-owned "## Also failing" section from the live failure
set on every merge so a test that has since been fixed drops out instead of
being listed forever.
- Assert the analyze checkout is SHA-pinned with persist-credentials disabled
and pin the failed-job log-download paths in the workflow test.
- Add an e2e workflow test guarding the cancel-in-progress expression that
keeps in-progress runs on main from being cancelled.
* fix(ci): bound the issue body and randomize the heredoc delimiter (#7795)
* fix(ci): test the runCli --existing merge path (#7795)
* fix(ci): address review feedback on e2e signal PR (#7795)
- Assert the full cancel-in-progress expression including && so a
mutation to || is caught by the e2e-workflow test
- Filter the capped-summary line from missingTests so it is not
rendered as a fake bullet under Also failing
---------
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Guards the security-critical invariants of qwen-triage.yml that broke silently
once already — the `settings_json:` input name was wrong, so the action dropped
it and the review agent ran with the full default toolset and no deny list.
A new `node:test` suite (wired into the shared HELPER_TESTS list both CI paths
run) asserts: the `settings:` input name (never `settings_json:`), the
tools.core registration whitelist and the deny list, the fork-PR runner routing
invariants, and the git exec-vector cleanup.
It also flips that cleanup from a best-effort denylist — which kept missing new
families (pager, filter.*, includeIf subsections, url.*, credential…) — to a
keep-known-safe allowlist: unset every local config key that isn't plumbing
actions/checkout needs (repo format, remote, branch, fetch/gc/pack/index,
safe.directory, extensions, submodule url/active/branch — not submodule.*.update,
which can be `!cmd`). This closes the whole exec-vector class, including knobs
not yet enumerated. The harness runs the workflow's actual allowlist pattern
against a scratch repo to prove it unsets every exec family and preserves the
checkout plumbing.
Co-authored-by: verify <verify@local>
* chore(vscode-ide-companion): regenerate NOTICES.txt and sync package-lock.json
NOTICES.txt had drifted out of sync with the dependency tree. It was
missing the third-party licenses pulled in transitively through
@qwen-code/core (notably @anthropic-ai/sdk and its dependencies), which
the notices generator reaches by following workspace links. Regenerated
from a clean install so it once again covers every resolved dependency.
Also sync the lockfile's ink spec to 7.0.3 to match
packages/cli/package.json, which pinned it without a caret but was never
reflected in package-lock.json.
* ci(vscode-ide-companion): guard NOTICES.txt against drift
Add a CI check that regenerates the companion notices and fails if the
committed file is stale, mirroring the existing settings-schema guard.
This stops the notices from silently rotting whenever a dependency
changes without a regeneration.
Also drop the validate:notices script entry, which pointed at a
non-existent file and was never functional.
* fix(vscode-ide-companion): make notices license lookup case-insensitive
The license-file lookup matched a fixed-case candidate list via fs.stat,
which resolves a `License` file on macOS's case-insensitive filesystem
but misses it on Linux's case-sensitive one. That made the generated
NOTICES.txt platform-dependent and broke the new CI up-to-date guard.
Scan the directory and compare lowercased names so the output is
identical on every platform.
* fix(vscode-ide-companion): match more license filename variants in notices
Extend the license-file candidate list with `LICENSE-MIT` and the
British-spelled `LICENCE.md` so packages that ship those (e.g. ignore,
bignumber.js) no longer emit a spurious "License text not found." in the
generated notices. Regenerates NOTICES.txt to include their licenses.
* test(vscode-ide-companion): cover case-insensitive notices license lookup
The Linux-only CI drift guard regenerates and compares on the same
case-sensitive filesystem, so a future revert to case-sensitive license
matching would produce consistent-but-wrong output and still pass. Extract
the lookup into an exported findLicenseFile() and add a regression test that
resolves a mixed-case `License` file regardless of platform.
---------
Co-authored-by: wenshao <wenshao@example.com>
* ci(web-shell): before/after visual previews, showing only changed views
The visual-preview bot posted the same fixed set of canned screenshots
on every web-shell PR, so it could not show what a PR actually changed
(a mermaid/split feature was invisible) and added noise on PRs that
touch the UI only trivially.
Render each scenario against BOTH the PR base (`main`) and the PR head,
pixel-diff them, and post a stitched "main | this PR" composite for only
the views that CHANGED. A PR with no visual impact composites nothing →
"no visual change". This makes the preview feature-aware with no per-PR
understanding: the diff finds exactly the surface the PR moved (and it
subsumes the backend-PR noise #6959 pre-filtered, at the content level).
- web-shell-visuals-compose.mjs: pixel-diff (canvas) + stitch a labelled
composite; pure helpers (parseShot/isChanged/planWork) unit-tested.
- web-shell-visuals.yml: also render the base — trusted `main` via
pull_request.base.sha, so no secret exposure in the untrusted-PR job —
then compose; composites replace the raw after-shots (same
`<view>-<theme>.png` name the publisher already expects).
- publish buildComment: list composites; "no visual change" when none.
Verified locally by overlaying #6881's real changes onto main + a new
mermaid scenario: the compositor flagged the mermaid view 6.5% changed
(its new zoom controls) and correctly skipped the unchanged transcript.
* ci(web-shell): address before/after review — lazy import, merge-base, robustness
Addresses the /review findings on the before/after preview:
- Lazy the @playwright/test import in the compositor so the pure exports
(parseShot/isChanged/planWork) load dependency-free, and wire
web-shell-visuals-compose.test.mjs into the github_ci_only test step —
it was never actually running in CI. (finding 1)
- Diff against the MERGE-BASE, not the base-branch tip, so a PR branch
behind main doesn't render others' already-landed changes reversed as
this PR's diff. (finding 2)
- continue-on-error on the base checkout + install so a flaky base
degrades to after-only instead of sinking the job; compose likewise
degrades to the raw after-shots on failure. (finding 3)
- timeout 20->30 (the job ~doubled) and scope the base render to
screenshots.spec.ts, skipping the discarded flow videos. (finding 4)
- diffPct: add img.onerror so a corrupt/truncated baseline PNG can't hang
page.evaluate to the job timeout. (finding 5)
- Lower CHANGED_PCT_THRESHOLD 0.1 -> 0.02 (~205px at 1280x800) so an icon
swap or one-word label change isn't classified "no change". (finding 6)
- Nits: correct the stdout comment, esc() the burned-in labels, and scope
the comment wording to "screenshots" (flows are always head-only).
* ci(web-shell): address before/after review round 2 (yiliang114)
- diffPct: a dimension change IS a visual change — comparing only the
overlapping rectangle hid it (a taller viewport with unchanged top
pixels read 0%). Short-circuit any size mismatch to changed. (Critical)
- Composite/comment label: "PR base (before)" not "main" — the workflow
also runs for release/**, whose base is not main. (Critical)
- Merge-base resolve: retry the compare API, then emit an EMPTY sha and
SKIP the base render (after-only) rather than falling back to the
base-branch tip, which reintroduces the reversed-diff bug. (Critical)
- Base steps get ids; the before render runs only when the base checkout
AND install both succeeded — else base/ (nested under head) resolves
node_modules up to head's and produces a hybrid before. (Critical)
- Publisher: a zero-change run now UPDATES the marker comment (image-less
"no screenshot changes") instead of exiting, so a prior preview's stale
images + SHA do not linger. (Critical)
Finding 6 (helper tests skip full CI) is a pre-existing repo-wide gap for
every .github/scripts test; left for a focused follow-up.
* ci(web-shell): close the compositor browser in a finally (leak on error)
A mid-loop rejection in diffPct (evaluate timeout / CDP disconnect on a
corrupt or oversized PNG) exited composeCli via the exception and skipped
browser.close(), leaking a ~200 MB Chromium child for the rest of the CI
job. Wrap the page + loop in try/finally so the browser always closes.
Also drop the stale "main (before)" labels from the docstring (the
composite/comment say "PR base" now, since the workflow also runs for
release/**).
* ci(web-shell): catch the compositor CLI promise for a clean exit
An unhandled composeCli() rejection (e.g. a missing @playwright/test)
printed an UnhandledPromiseRejectionWarning and exited without a
meaningful code; add a .catch that writes the error and exits 1.
* ci: run .github/scripts helper tests in full CI + test planWork nullish guards
- Finding 6: the compositor/publisher helper tests ran only in the
github_ci_only profile, which a `full` PR skips (and vitest test:ci
doesn't collect node:test files) — so a compositor change could pass CI
without its regression tests. Run them in the full ubuntu Test job too.
- Cover planWork's `?? []` guards with null/undefined inputs.
* ci: extract HELPER_TESTS list so both CI profiles share one source of truth
Round-3 F6 fix duplicated the .github/scripts node:test list across the
github_ci_only and full-profile steps; a missed edit would silently drop
coverage in one path. Hoist it to a workflow-level env var both reference.
---------
Co-authored-by: wenshao <wenshao@example.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
* ci(serve): daemon A/B before/after preview on response-surface PRs
The daemon analog of the web-shell visual before/after bot. For PRs that
touch the qwen serve response surface, build the CLI from BOTH the PR
base (main) and head, drive a fixed set of endpoints against each daemon,
and diff the JSON responses into a before/after field table posted on the
PR. A PR with no response change -> "no change".
- serve-ab-diff.mjs: structural JSON diff (set-aware arrays, volatile-
field masking) -> before/after table; buildComment assembles all
scenarios. Pure helpers unit-tested (12 tests).
- serve-ab-drive.mjs: boots the built daemon, drives GET /health,
/health?deep=1, /capabilities (no model -- dummy creds), captures JSON.
- serve-ab.yml / serve-ab-publish.yml: capture (untrusted, no secrets) +
privileged workflow_run publisher (validate+bind PR, dedup, TOCTOU),
mirroring the web-shell split.
Verified locally: real daemon boot + capture, the engine diffing real
capabilities -> the before/after table, and no-change detection. The
workflow wiring (2x build + drive in CI) is exercised on the next serve PR.
* ci(serve): address before/after review — merge-base, base fail-safe, CI-wire tests
Applies the transferable web-shell before/after review findings to the
daemon A/B:
- Diff against the MERGE-BASE, not the base-branch tip, so a PR branch
behind main doesn't show others' already-landed daemon changes reversed
as this PR's diff.
- continue-on-error on the base checkout so a flaky base degrades to
after-only instead of sinking the job (the base build/drive already was).
- Wire serve-ab-diff.test.mjs into the github_ci_only test step — it uses
only node builtins, so it runs there dependency-free (no npm ci needed).
* feat(serve): add a create-session deep-health A/B scenario
Add a `health-deep-with-session` scenario: the drive harness gains
setup-request support, creates one session (POST /session), then probes
GET /health?deep=1 — exercising the session lifecycle + cross-workspace
session aggregation (#6961's exact case). Broaden the volatile mask to
cover lastActivityAt/idleSinceMs/sessionId/clientId/workspaceCwd so the
capture is deterministic, while the meaningful counts (sessions,
activePrompts, pendingPermissions, connectedClients, channelAlive) stay
diffable.
Verified against the real daemon: two runs of the same build diff to "no
change" (deterministic), and a sessions 1->2 change surfaces as a
before/after table row.
* ci(serve): address review — narrow trigger, degraded-base marker, test the entrypoint
- Narrow the trigger from packages/core/src/** (~1100 mostly-unrelated
files) to packages/cli/src/serve/** — the daemon's actual response
surface (core has no serve/health subtree). (Suggestion)
- diffCaptureDirs signals baselineMissing when head captures exist but the
base build/drive produced none, so the comment says "diff skipped"
rather than misreporting every field as added or "no change". (Suggestion)
- Export + test diffCaptureDirs (the function the CI `comment` subcommand
actually invokes) with temp-fixture dirs, covering the diff + degraded
path. (Suggestion)
- Wording: "PR base" not "main" (the workflow also runs for release/**).
* ci(serve): address review round 2 — escape table values, guard setup/baseline/daemon
- fmt escapes `|` and backtick so an arbitrary daemon value can't split a
GFM table cell or close the code span.
- diffCaptureDirs surfaces base-only (removed/failed-to-capture) scenarios
instead of silently dropping them — otherwise a broken scenario lowers the
"across N" count and masks the regression.
- The drive checks each setup request's status and throws on non-2xx, so a
failed POST /session can't let health-deep-with-session capture wrong
state (0 sessions) and fake/mask a diff.
- The drive awaits daemon exit after SIGTERM (SIGKILL after 5s) so a hung
daemon can't linger.
* ci(serve): clear-error JSON parse for both capture sides
The after-side JSON.parse was unguarded while the before-side wasn't; a
readJson helper now gives a 'invalid JSON capture at <path>' error for
either side instead of a raw SyntaxError.
* ci(serve): review round 3 — merge-base skip, surface malformed base, escape path, kill dead stdout
- serve-ab.yml: retry the merge-base compare then SKIP the before build
(after-only) instead of falling back to the base-branch tip; gate the
base checkout/build on a resolved sha + successful checkout.
- diffCaptureDirs: an existing-but-malformed base capture now surfaces
(existsSync + readJson) instead of a bare catch treating it as {} (which
reported every field as "added"); covered by a new test.
- renderTable: escape backticks in the path field too (fmt already did for
values).
- Drop the dead stdout + its misleading "workflow reads it" comment (the
publisher posts whenever body.md exists); log the count to stderr.
* ci(serve): renderTable label 'PR base' not 'main' (release/** provenance)
buildComment already said 'PR base', but the per-scenario table header +
no-change line still said 'main'; align them (the workflow also runs for
release/**, whose base is not main).
---------
Co-authored-by: wenshao <wenshao@example.com>
* feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs
PRs that touch the web-shell UI now get an auto-updated comment with
light/dark screenshots of key views (transcript, slash menu, model/theme
dialogs, permission panel) and short GIF recordings of common flows,
rendered against the existing mock daemon — no real backend, no secrets.
Split into two workflows for security, since capture runs untrusted PR code:
- web-shell-visuals.yml (pull_request): checks out the PR head, builds and
renders it with Playwright, captures PNGs + webm, converts webm->GIF with
ffmpeg, and uploads an artifact. `contents: read` only, references no
secrets — fork PRs run with a read-only token and no secrets.
- web-shell-visuals-publish.yml (workflow_run): downloads the artifact,
binds it to its real PR by requiring the PR head SHA to equal the run's
authenticated head SHA, hosts the images on a per-PR `pr-assets/*` branch
(referenced by immutable commit SHA), and posts/updates one inline
comment. Never checks out or runs PR code; the write token lives only here.
Capture infra is self-contained in packages/web-shell
(playwright.visuals.config.ts + client/e2e/visuals/*), reusing the mock
daemon harness. Run locally with:
`npm run test:e2e:visuals --workspace=packages/web-shell`.
* fix(web-shell): guard empty gh api response in visuals publish
Addresses review feedback on #6880: if `gh api` returns empty (network
error / rate limit), jq on empty stdin errors and `set -e` kills the
publish job. Skip gracefully instead.
* fix(web-shell): address review nits on visuals capture
- harness recordFlow: wrap video saveAs/delete in try/catch so a video
I/O error (e.g. drive failed before navigation) can't mask the real
driveError.
- capture workflow: drop the unused head_sha.txt artifact field; the
publish job binds to the authenticated workflow_run.head_sha, and an
artifact-sourced SHA would be untrusted.
* fix(web-shell): address second review round on visuals capture
- context.close() in recordFlow's finally is now best-effort (try/catch)
so a close/crash error can't mask the real driveError.
- add a flows spec that asserts a throwing drive propagates its own error.
- trigger the capture workflow on playwright.visuals.config.ts changes too.
* fix(web-shell): address third review round on visuals capture
- harness: log (don't silently swallow) a video save/null when drive
succeeded; keep masking-suppression only when driveError is set.
- publish: HTML-escape interpolated values in the comment builder (defense
in depth, independent of the upstream filename sanitization); fix the
stale 'single pr-assets branch' comment and key concurrency on source
repo+branch so different PRs (incl. same-named fork branches) parallelize.
- capture: bump checkout to v6.0.3 (repo standard); surface ffmpeg's stderr
on GIF-conversion failure instead of discarding it.
* fix(web-shell): harden visuals publish/capture (review round 4)
Publish (privileged workflow_run):
- CRITICAL: capture basename before `tr` so its trailing newline isn't
turned into `_` (which broke the .png/.gif filter -> empty preview).
- dedup only against the bot's OWN comment (author + marker), not any
marker-bearing comment a participant can post.
- bound the pr-assets branch: force-push a single orphan snapshot per run
(previous snapshot GC'd) instead of appending unbounded untrusted content;
this also removes the rebase/retry path.
- cap EXAMINED candidates (not just accepted) before validation; tighten
per-file (3MiB) and accepted-image (14) caps.
- re-validate PR open + head-SHA immediately before the comment write
(TOCTOU); retry the comment listing and abort rather than POST a duplicate
when listing fails.
- esc() the runUrl for consistency with the self-defending HTML.
Capture (pull_request):
- upload raw recordings as a SEPARATE artifact the publisher never downloads,
so an untrusted multi-GB video can't exhaust the privileged job.
- also trigger on packages/webui/src and packages/sdk-typescript/src (the
visuals dev server aliases them).
- create screenshots/gifs dirs before the metadata counts (defensive).
Harness recordFlow:
- track drive failure with an explicit boolean (handles `throw undefined`);
discard the recording on failure so a failed flow leaves no bogus webm.
* refactor(web-shell): extract + unit-test the visuals publish staging/comment
Addresses the review's testability gap (the class of bug that let the
filename sanitizer break the whole preview slip through green CI). The image
validation (magic bytes, filename sanitization, examined/accepted/size caps)
and the comment builder (light/dark pairing, flow labels, HTML escaping) move
from inline workflow bash/node into .github/scripts/web-shell-visuals-publish
.mjs, covered by web-shell-visuals-publish.test.mjs (run in ci.yml's
node --test line). The publish workflow sparse-checks-out and calls the
script instead. Behaviour is unchanged; it just gained a test surface.
* fix(web-shell): retry the visuals asset force-push; drop stale comment
Round-4 switched hosting to a force-push but left a comment referencing a
'push-retry loop' that no longer existed, and the force-push was a single
call that set -e would abort on a transient failure. Add a bounded retry and
correct the comment.
* fix(web-shell): harden visuals publish/capture (review round 6)
Script (unit-tested):
- flow labels: own-property lookup so `toString.gif`/`constructor.gif` can't
leak Object.prototype members into the comment.
- per-kind image caps (screenshots vs gifs) so a large screenshot set can't
silently starve the flow GIFs from the preview.
- tests for both, plus the per-kind cap.
Publish:
- bind the artifact PR number to the run's authenticated head repo+branch
(not just head SHA), rejecting a sibling PR that shares the same commit.
- re-validate before the force-push and again right before the comment write
(close the download/stage/lookup TOCTOU windows).
Capture:
- bound artifact contents before upload (drop oversized / excess files) so an
untrusted spec can't bloat the published or video artifact.
- trigger on the capture workflow file itself.
- new close-trigger cleanup workflow deletes a PR's asset branch on close, so
pr-assets/* refs don't accumulate without bound.
- single-source the capture viewport (constants.ts) shared by config + harness.
- model-switch flow asserts the daemon model request actually fired.
* fix(web-shell): stricter visuals error handling (review round 7)
Harness recordFlow:
- when the drive SUCCEEDS, a failed context.close() or video.saveAs() (or a
missing recording) now FAILS the flow instead of a swallowed console.warn —
a silent pass with no .webm makes the downstream GIF step fail confusingly.
A drive FAILURE still discards the partial video and rethrows the original
error (unchanged).
Publish:
- validate_pr distinguishes a transient API failure (empty after retries ->
exit 1, re-triggerable) from a genuine invalid state (closed / head mismatch
-> skip), via a `gate` wrapper used at all three checkpoints.
- add a 2s backoff between comment-listing retries (matching the push retry).
---------
Co-authored-by: wenshao <wenshao@example.com>
* fix(cli,core): restore default debug log file output
* fix(cli): preserve debug log sandbox opt-out
* test(cli): stabilize flaky UI tests
* fix(cli): scope default debug log to debug mode
* fix(core): scope debug log default to --debug mode per #6600
Revert isDebugLogFileEnabled() guard from `value === undefined` back to
`!value` so the non-debug default stays opt-in as prescribed in #6600.
The --debug scoping in config.ts already sets QWEN_DEBUG_LOG_FILE=1 when
in debug mode, making the global default flip unnecessary.
Also adds a regression test for the opt-out edge case
(QWEN_DEBUG_LOG_FILE='0' + --debug should not overwrite).
* test(cli): remove unused voice keyterms import
* test(cli): isolate cli entry fallback test
* ci: avoid web-shell smoke port collisions
---------
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* Upgrade GitHub Actions to latest versions
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
* ci: restore ratchet annotations on upgraded actions
The action version bumps had replaced '# ratchet:owner/repo@<ver>'
annotations with bare '# vX.Y.Z' comments, which silently breaks
ratchet's ability to track and update these SHA pins. Restore the
annotations on every changed line (updated to the new major), matching
each line's pre-existing convention on main.
---------
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
After #5842, CodeQL and the E2E suite were the only things running on every push to `main` — the per-commit, post-merge backstop. With merges landing back-to-back, those runs (CodeQL ~30min, three E2E jobs ~25/36/36min, the latter never cancel-superseded) stacked on the scarce hosted Linux pool and were a main driver of the recent runner-saturation incident.
- CodeQL moves to its own scheduled codeql.yml (nightly + workflow_dispatch). It was never a required check and findings still surface in the Security tab, so per-commit scanning bought little. ci.yml loses its now-empty `push` trigger as a result (every remaining job is gated to pull_request / merge_group).
- E2E gets event-scoped concurrency so back-to-back pushes to `main` cancel superseded runs (only the latest tree matters and it covers every merged change), plus a nightly full regression as the guaranteed signal in case a busy merge window keeps cancelling the push run. Manual workflow_dispatch added.
The merge_group gate (ubuntu + integration + mac + win) still validates every PR before it lands; this only changes the non-gating post-merge work.
Follow-up (separate PR): alert on E2E/CodeQL failure (a deduped ci-failure issue) now that they run unattended.
* ci: route the merge queue's Linux jobs onto ECS
The merge queue runs in the base-repo context but classify_pr was PR-only, so its ubuntu_runner output was empty in the queue and the Ubuntu Test + Integration jobs fell back to the shared hosted pool — piling onto the scarce hosted Linux runners exactly when the queue is busiest. Run classify_pr on merge_group too and route both the Ubuntu gate and Integration onto the same in-repo ECS pool as PRs; the MAINTAINER_ECS_RUNNER_DISABLED kill-switch and the hosted fallback are intact, and fork PRs are unaffected.
Also extend the checkout-verification guard to the merge queue: now that the queue's Ubuntu checkout runs on ECS behind the squid egress proxy, a stale-ref checkout could silently test the wrong tree, and a wrong-tree pass in the queue would merge bad code. One sub-second merge-base check.
Routing approach carried forward from #5853 by @wenshao (closed); this adds the merge-queue checkout guard on top.
* ci: fix verify-step wiring test and guard integration_cli on ECS
The merge-queue ECS routing renamed the test job's checkout guard to "Verify
checkout includes expected head commit", but no-ak-integration-ci.test.js still
asserted the old "Verify PR checkout includes head commit" name, failing the
wiring test. Update the assertion.
integration_cli now also routes to ECS (via classify_pr on merge_group) yet
lacked the protections the Ubuntu gate has. Mirror them: fetch-depth 1 (nothing
walks history; a full clone is the heaviest transfer on the ECS runner) and the
same stale-checkout guard, keyed on merge_group.head_sha.
* ci: mirror the Node 22 version probe into integration_cli
The self-hosted Node step claimed to mirror the Ubuntu gate but omitted the
major-version probe, so a non-22 Node on the ECS runner would run integration
tests with no log signal. Add the same warning-only check.
Now that the merge queue is live, each job should run in exactly one place instead of doubling up.
- CodeQL and E2E leave the merge queue (CodeQL gains a merge_group guard, E2E loses its merge_group trigger). Neither is a required check, so neither gated a merge — CodeQL just ran ~30min and got cancelled at merge every time, and the three E2E jobs burned ~25/36/36min of hosted time, for nothing. They keep running post-merge on push to main.
- macOS/Windows move to merge_group only (they were also running on push to main), and the ubuntu Test job stops running on push too. The queue already tests the merged tree, so re-running these gate jobs on the post-merge push is redundant.
Net: PR runs ubuntu; the merge queue runs ubuntu + integration + mac + win (the gate); push to main runs CodeQL + E2E (the backstop). No job runs in two of those.
* ci: stop running push CI on release/** branches
release.yml pushes version + changelog commits to the release branch.
With 'release/**' under the push trigger, that fired a full push-event CI
run (mac/win/CodeQL all run since event != pull_request) on the release
PR's head, blocking it from main's merge queue and double-running the
matrix. Nothing gates on it: release branches have no protection and
publish happens inside release.yml. Keep 'release/**' under pull_request
for backport PRs.
* ci: split platform test matrix into named jobs to unblock merge queue
The macOS/Windows tests are required status checks that only run in the
merge queue (if: event_name != 'pull_request'). As a single matrix job, a
skipped run collapses to one check named 'Test (${{ matrix.os }}, Node
${{ matrix.node-version }})' — so the required contexts 'Test
(macos-latest, Node 22.x)' and 'Test (windows-latest, Node 22.x)' are never
reported on the PR head and sit 'Expected' forever, leaving every PR BLOCKED
and unable to enter the queue (observed on #5830/#5832).
Split into two named jobs. A skipped named job reports under its exact name
(conclusion: skipped), which satisfies the required check on the PR head —
exactly how the merge-queue-only Integration Tests job already behaves — so
PRs can enter the queue, where these jobs run for real and gate the merge.
No PR-stage runner cost (skipped jobs spin no runner); no ruleset change.
* test(ci): update no-AK wiring test for split platform jobs
The test looked up the 'test_platforms' matrix job, which this branch
split into named 'test_macos' / 'test_windows' jobs. Assert the same
properties (no no-AK script, immutable PR-head checkout) on both new jobs.
These three jobs reran on every PR push but rarely caught push-to-push
regressions: macOS/Windows are the slowest, costliest runners and platform
breakage is rare per iteration, and CodeQL findings seldom change between
pushes. Gate all three off the pull_request event so they no longer sit on
every PR's critical path. They still run on push to main (and in the merge
queue once enabled), so platform and security regressions are still caught
before release. The only required status check, Test (ubuntu-latest, Node
22.x), is unaffected and keeps running on PRs.
Two independent reliability fixes on the Linux CI path:
- test (ubuntu, ECS-routed): fetch-depth 0 -> 1. Nothing in the job walks
git history (the verify guard checks head.sha == HEAD; schema/tests only
touch the working tree), so the full-history clone is dead weight. On the
self-hosted ECS runner it is the heaviest transfer and chokes the squid
egress proxy, flaking checkout with "Operation too slow" / connect
timeouts. depth 1 is enough; coverage diffing runs in a separate hosted
job that checks out on its own.
- codeql: add timeout-minutes: 30. Analysis normally finishes in ~7-9 min
(22 min worst case observed). Without a cap, a hosted runner that drops
its heartbeat mid-analysis leaves the job in_progress for the default 6h,
holding a scarce hosted Linux slot the whole time and starving the queue.
* ci: collapse pr checks into ubuntu gate
* ci: keep platform PR tests in matrix
* ci: update no-ak gate wiring test
* ci: bypass stale self-hosted git cache
* ci: refresh self-hosted checkout cache
* ci: refresh pr refs after cached checkout
* ci: fetch fresh pr refs via github url
* ci: retry stale pr merge checkout
* ci: fetch stale pr merge ref directly
* test: isolate qwen serve streaming home
* test: keep qwen serve fake server off proxy
* ci: check out PR head ref to avoid merge-ref lag
The Ubuntu gate kept failing on the self-hosted runner: the PR checkout
used github.ref (refs/pull/N/merge), which GitHub rebuilds asynchronously
and can serve stale for minutes after a push, so the verify guard saw a
tree without the PR head. The retry/refresh machinery added to work
around this could not help — its direct GitHub fetch fallback times out
on the self-hosted squid proxy.
Check out refs/pull/N/head instead (immutable, published the instant the
branch is pushed) for pull_request events in both the test and
test_platforms jobs, and drop the 6-step retry/verify/refresh block. A
single sanity guard stays to fail loud if the head is missing. Non-PR
events keep github.ref; the merge queue validates the merged result.
* ci: check out PR head SHA to avoid missing merge-ref checkout failures
* ci: skip coverage comment on cancelled runs to free concurrency slot
post_coverage_comment used if: always(), so on a cancelled run it still ran
on the hosted pool. During rapid pushes the cancelled run's coverage job sat
queued for a hosted runner, keeping the run non-terminal and holding the
branch concurrency slot — so the next push's required Test (ubuntu-latest)
check waited behind it (observed ~9 min, with thrash). Switch to !cancelled():
coverage still posts on test failure, but a cancelled run releases the slot
immediately. Its coverage would be stale anyway.
* ci(triage): route /triage comment runs to the ECS pool
`/triage` comments trigger the authorize+triage jobs on hosted runners,
which queue behind CI during peak hours (~2 min vs 4 s off-peak observed).
Route the comment-triggered triage job to the ECS self-hosted pool, which
is not bound by the GitHub-hosted concurrency limit.
Safe scope: only `issue_comment` events; the job is read-only and checks
out the base ref (never PR-head code). Gated on the same
MAINTAINER_ECS_RUNNER_DISABLED kill-switch as `authorize`, so a disabled
ECS pool falls back to hosted. PR/issues/dispatch triggers stay hosted.
* ci: fetch PR head by ref name to fix 'not our ref' checkout failure
* ci: align Test checkout ref with Lint (add branch_ref fallback)
On workflow_dispatch the Test job runs (its if: !cancelled() overrides the
implicit success() gate when classify_pr is skipped), so its checkout must
honor the branch_ref input like Lint does. Without it, dispatch runs validated
github.ref in Test while Lint validated the user-supplied branch_ref.
* ci: retry merge-ref checkout instead of pinning PR head; drop /triage ECS routing
Per review feedback: checking out refs/pull/N/head dropped merge-result
validation, and merge queue isn't enabled on main to backstop it. Keep merge-ref
semantics (github.ref) for the required Lint/Test checkouts and retry once to
absorb the transient merge-ref lag the PR-head change was working around.
Also revert /triage routing to ECS: that job runs an agent (sandbox:false,
shell/write tools, repo tokens) over untrusted PR/issue text, so it stays on
ephemeral hosted runners, not the persistent self-hosted pool.
* ci: back off before the merge-ref checkout retry
The merge ref (refs/pull/N/merge) is built asynchronously, so a back-to-back
retry can hit the still-unbuilt ref again. Sleep 10s before retrying so the
retry absorbs the multi-second build lag, not just instantaneous blips.
* ci: harden self-hosted runner routing per #5620 review
Follow-up to #5620 addressing the review findings:
- authorize (pr-review + triage): add the same-repo guard so fork-triggered runs keep CI_BOT_PAT on ephemeral hosted runners; only in-repo PR events use the persistent ECS runner.
- delay-automatic-review: route back to hosted — its 30-minute environment wait timer would otherwise hold a self-hosted ECS slot idle for the whole wait (GitHub allocates the runner before the timer).
- Lint (self-hosted): warn when Node major != 22 (parity with Test) and use 'npm ci --prefer-offline --no-audit --progress=false' for proxy resilience (parity with Test).
Not included: keeping classify_pr on hosted (the SPOF concern) — deliberately left on ECS; the MAINTAINER_ECS_RUNNER_DISABLED kill switch reverts it within ~1 min if ECS is down.
* ci: guard github.event.pull_request before deref in authorize runner selection
Addresses Copilot's review note on #5644. GitHub Actions expression property access is already null-safe (a missing github.event.pull_request yields null, not an evaluation error), so non-PR events (issue_comment/workflow_dispatch) already fell back to hosted. This makes the fallback explicit and silences the recurring reviewer concern; no behavior change.
* ci: route in-repo PRs' Linux test to self-hosted runner
Send the required Linux Test job to the self-hosted ECS runner for PRs whose head branch lives in this repo (which implies the author had write access), while forks and push/merge_group runs stay on GitHub-hosted runners. This lets maintainer changes skip the shared hosted Linux queue without ever running untrusted fork code on self-hosted infrastructure. The MAINTAINER_ECS_RUNNER_DISABLED repo variable forces everything back to hosted if the ECS runner is unavailable.
* ci: reuse pre-installed Node on self-hosted runners, route Lint to ECS
The self-hosted ECS runners cannot reliably download the Node tarball from nodejs.org through the egress proxy (actions/setup-node aborts mid-download), which failed the Test job routed there. On self-hosted runners, reuse the machine's pre-installed Node instead of re-downloading it every run; hosted runners keep using actions/setup-node. Also route the Lint job to ECS for in-repo PRs — scripts/lint.js already skips installing actionlint/shellcheck/yamllint when they are on PATH, so no lint.js change is needed.
* ci: trim verbose runner-routing comments
* ci: run classify_pr gate on ECS for in-repo PRs
* ci: quote classify_pr/lint runs-on expressions
* ci: keep Lint on hosted runners (ECS proxy truncates linter downloads)
* ci: route Lint and review/triage gate jobs to self-hosted runner
Lint goes back to ECS (linters are now pre-installed on the runners, so scripts/lint.js skips downloading them). The pr-review gate jobs (authorize, review-config, delay-automatic-review, ack-review-request) and the triage authorize gate also move to ECS so the ECS heavy jobs (review-pr, tmux-testing) are no longer bottlenecked by the shared hosted queue. All respect the MAINTAINER_ECS_RUNNER_DISABLED kill-switch.
* feat(voice): voice dictation with native capture, streaming, and biasing
Add voice dictation for the prompt input:
- /voice [hold|tap|off|status] command + general.voice.{enabled,mode,language,protocol} settings; push-to-talk via Space, /model --voice to pick the model
- Native microphone capture (@qwen-code/audio-capture, miniaudio N-API) with arecord/SoX fallback, silence auto-stop, cold-start warm-up, and macOS permission query
- Batch transcription via DashScope Qwen-ASR (OpenAI-compatible chat/completions + input_audio) with language + keyterm biasing and an echo guard
- Live streaming over the DashScope realtime WebSocket (fun-asr-realtime / paraformer-realtime-v2) with interim text and an input-level waveform, behind voice.protocol=dashscope-realtime
- Rich VoiceIndicator UI (state, level meter, live partial transcript)
- Cross-platform prebuilds via prebuildify + node-gyp-build and a CI matrix
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* feat(cli): route voice ASR by model
* feat(cli): polish voice realtime parity
* ci: fix voice workflow checks
* fix(cli): address voice review blockers
* fix(cli): harden voice transcription failures
* fix(cli): address voice PR review blockers
* fix(cli): harden voice review follow-ups
* fix(voice): handle realtime review blockers
* fix(cli): add voice command i18n keys
* fix(cli): address voice review blockers
* fix(cli): address voice review follow-ups
* fix(voice): harden review edge cases
* fix(voice): address release and realtime review blockers
* fix(voice): address realtime suggestion followups
* fix(voice): handle realtime review followups
* fix(voice): address realtime review blockers
* fix(voice): address review feedback
* fix(voice): address recorder review feedback
* docs(voice): document ssrf guard boundary
* fix(voice): address review feedback
* fix(voice): preserve warm recorder session safety
* fix(voice): address stream review suggestions
* fix(voice): address multi-round review findings
Realtime/streaming:
- Salvage an already-committed transcript when the WebSocket closes right
after finish() instead of rejecting the whole dictation
(qwenAsrRealtimeSession, voiceStreamSession) + regression tests.
useVoiceInput state machine:
- Single-shot finalize guard so a tap-stop racing the silence auto-stop can't
double-stop the recorder and surface a spurious failure.
- Reset mountedRef on (re)mount so StrictMode (DEBUG) can't freeze the voice UI.
- Widen the hold-mode first-press release window above common key-repeat delays.
Model selection:
- Reject ids with no ASR transport at /model --voice and in the model dialog via
a new isSelectableVoiceModel; move resolveVoiceTransport into voiceModel so the
record-time config resolver stays transport-agnostic (+ tests).
macOS mic permission:
- Surface the not-determined state in voice warmup so the first dictation isn't
silently lost behind the TCC dialog.
Native packaging:
- Make the audio-capture native install non-fatal (falls back to SoX/arecord) so
a voice-only build failure can't break npm ci.
- Download audio-capture prebuilds before building standalone release archives.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(voice): address review blockers
* fix(voice): update batch recording audio level
* fix(voice): tap-mode transcript loss, stream leaks, and dead keyterm cleanup
- Tap-mode dictation submitted a stale empty buffer.text, wiping the just-
inserted transcript and sending nothing: thread the resulting prompt text
through onSubmit(text) instead of reading buffer.text back synchronously
after the async insert (useVoiceInput, InputPrompt).
- Streaming finalize leaked the WebSocket session when recorder.drain() threw:
abort the session before propagating the error.
- voiceStreamSession: reject the connect promise when 'task-finished' arrives
before 'task-started' instead of hanging forever in 'transcribing'.
- Remove dead keyterm enrichment (project/branch/recent-file paths were
unreachable after the privacy fix) and the now-inert CJK echo guard, plus the
tests that asserted that removed behavior; fix the misleading "OpenAI prompt
field" comment.
- Add the missing 'Voice Model' and macOS mic-permission i18n keys to
en/zh/zh-TW (they were falling back to English; check-i18n doesn't flag keys
absent from en).
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* fix(voice): reject partial stream transcripts
* fix(cli): let dialogs consume voice keys first
* fix(voice): reject incomplete qwen realtime transcripts
* fix(voice): handle stream review blockers
* fix(voice): salvage qwen realtime transcript on close
* fix(voice): sanitize streamed transcript text
* test(audio): run audio capture tests in CI
* fix(voice): address review blockers
* fix(voice): report stream close while recording
* fix(voice): reduce keyterm echo false positives
* fix(voice): handle realtime close diagnostics
* fix(voice): address realtime review blockers
* fix(lint): allow legacy voice filenames
* chore(cli): rename voice files to kebab-case
---------
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
* ci(release): report required Test checks on release PRs and auto-approve
Release-sync PRs skipped the matrix Test job at job level, so the required "Test (os, Node 22.x)" contexts were never reported and the PR could not merge. Move the skip to step level so the job still expands its matrix and concludes as a work-free pass. Also add a CI_DEV_BOT_PAT approval on the release PR to cover one of the two required reviews.
* ci: single-quote the test job if expression for yamllint
After dropping the inner 'true' comparison, double quotes are no longer required, so yamllint's quoted-strings rule wants single quotes.
Integration tests (integration-tests/) previously ran only in the nightly
Release pipeline, so a PR that broke them merged green and the failure stayed
hidden until release time. Add a merge_group-gated job that runs the existing
test:integration:cli:sandbox:none suite, catching regressions before the PR
lands on main.
Gated on github.event_name == 'merge_group' so it does not touch the per-PR
critical path, and is a no-op until merge queue is enabled on main. Reuses the
OPENAI_* secrets already wired for the release integration jobs.
Refs #5219
* ci(review): align automated review with bundled skill
* ci(review): trigger when bot review is requested
* ci(review): disable deployment records for review delay
* ci: sync actionlint version env
* ci(review): authorize requested review trigger
* ci(review): centralize review bot login
* ci(review): narrow review config trigger
* ci(review): isolate comment review concurrency
* ci(review): surface reviewer permission check failures
* fix(ci): use exit 0 on permission check API failure
When the GitHub API call to check requester permission fails, the job
should still complete successfully since should_review=false is already
set. Using exit 1 marks the job as failed, which blocks the downstream
review-pr job even with always() — a transient API outage would silently
prevent all reviews.
* fix(ci): only cancel-in-progress on synchronize events
Restrict cancel-in-progress to push (synchronize) events so that
review_requested events (adding a human reviewer) don't accidentally
cancel an in-progress bot review run sharing the same concurrency group.
* fix(i18n): sync mismatched keys between en.js and zh.js (#3503)
Add 4 keys missing from en.js that are actively used in source code,
add 5 missing Chinese translations to zh.js, integrate check-i18n
into CI to prevent future drift, and skip JSON file write in CI to
avoid dirtying the working tree.
---
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>