Keep restore-only and self-hosted stores outside pnpm/action-setup's replaceable install directory, resolving the configured path for every paired cache. Pin Yarn 4 for the generated-project e2e so runner images without Corepack do not fall back to Yarn Classic, and update the bilingual decision record.
scripts/wine-windows-gates.sh owns the whole lane — workspace snapshot,
concurrent Node/wineboot/install provisioning, entrypoint resolution, the
vue link, smoke, and the two concurrent blocking gates — so the ci.yml
windows job shrinks to runner provisioning plus one script call, and
pnpm run check:windows-wine runs the identical script locally. The local
path never mutates the working tree, persists the Wine prefix and the
checksum-verified Windows Node zip under .cache/wine-windows/, and falls
back to the cached zip offline. AGENTS.md ceiling 1680 -> 1695 for the
one-line command entry.
- serial-linux-selfhosted checks out fetch-depth 0: depth 2 misses
github.event.before on multi-commit or force pushes, failing the
archive verifier on a valid tree. Full fetch is cheap against the
VM's local mirror.
- Runbook (both languages): every remaining admin phrasing (problem
statement, switch heading, alternatives, consequences) now says
writer; and the 'composes with this mechanism' claim about a
master-ref-pinned runner group is replaced with the truth observed
live on 2026-07-27 — master-ref pinning blocks PR failover, and the
shipped posture is repository-scoped all-workflow group access.
Static gate green locally: 32 passed, 0 failed.
The standby lane itself is push-only, but under failover pull_request
jobs do reach these runners with the PR merge ref's workflow. The
workflow comment and the larger-runner note (both languages) now state
that plainly and name the actual boundary — repository membership
(private, forking disabled, Dependabot excluded) — matching the
runbook. Static gate green locally: 32 passed, 0 failed.
- Sweep every remaining 'admin-only' claim (workflow comments, runbook
lines 13/40, topology note, all zh pairs): the variable is
writer-manageable, and the boundary against untrusted code is
repository membership (private, forking disabled, Dependabot
excluded) — stated identically at every site instead of only in the
'who can flip' paragraph.
- Serial cross-platform reference note (both languages): master now
runs four references — the three hosted OS legs plus the self-hosted
standby drill, linked to the failover runbook.
Static gate green locally: 32 passed, 0 failed.
- serial-linux-selfhosted now fetches depth 2 and passes
DSH_ARCHIVE_BASE_REF=github.event.before, running the same
frozen-archive comparison as serial-linux instead of diffing the
new manifest against itself.
- Runbook (both languages): documents the deliberate dependabot
exception (queued-on-hosted during failover is expected, not a
failed switch); corrects the emergency-capacity bootstrap to
exclude .runner/.credentials when cloning a runner directory; and
replaces the 'admin-only' variable claim with the accurate
trust-model statement — repository variables are writer-manageable,
which in this private fork-disabled repo with an all-workflows
runner group is routing among members, not an escalation.
Static gate green locally: 32 passed, 0 failed.
- All four failover selectors (three workers + the verdict job) and the
paired env/cache expressions now exclude dependabot[bot]: under
failover, dependency-supplied code keeps queueing for the hosted pool
instead of executing on the persistent VM. A delayed Dependabot PR
during an outage is an acceptable cost; dependency code on the
privileged host is not.
- Runbook (both languages): records the shipped failover bounds
(coverage 8, snapshots 12, sized for six instances) and documents
that the verdict job follows the selector too — operators previously
had no explanation for a verdict queued after all workers passed.
- Local static gate green: 32 passed, 0 failed (translation pairing
519 pairs consistent).
- all-checks-passed now resolves its pool through the same
DSH_CI_FAILOVER expression as the worker jobs it aggregates.
Pinned to the hosted pool it would leave the branch-protection
verdict queued on the failed pool after every failover job passed —
observed live during the 2026-07-27 outage as a required check
looping against dead capacity.
- Coverage worker bound under failover drops 12 → 8 and snapshot
concurrency 16 → 12: the pool now runs six always-on instances (the
spare tier was retired), so worst case is 6 × 8 = 48 coverage
workers on the shared 64-core VM.
The required windows job moves from windows-2025 to ubuntu-latest, running
checksum-verified Windows Node under Wine at Linux-job wall clock (2m46s
warm vs 7-9min); master's serial-windows native-kernel reference is
untouched, and a new master-only wine-apt-cache job seeds the apt cache
every pull request restores. The experiment workflow folds into ci.yml,
the Agent Note moves to implemented with measured results, and the two CI
topology notes update to the shipped facts.
Review follow-up on the symmetric-cache conversion: provisioning and
caching are separable concerns, so the remaining 8 corepack enable
sites (the three enterprise restore-only PR jobs, the required Windows
job, serial-macos/serial-windows, the cache-less ci.yml jobs, and
sandbox.yml) now provision pnpm via pnpm/action-setup@v4 too — only
the provisioning line moves; every deliberate cache shape (enterprise
restore-only pairing, serial-linux producer, Windows cache-less) is
byte-preserved. corepack is gone from CI entirely.
Agent Note retitled to the decision's real shape (provisioning policy,
with caching as per-job policy on top), EN+ZH, pair re-recorded;
composite-action and stop-at-cache-workflows alternatives recorded.
Convert the hand-rolled corepack + store-path + actions/cache pnpm setups
to pnpm/action-setup@v4 + setup-node cache: pnpm (the landlock-run.yml
pattern) in e2e.yml, docs-pages.yml, pi-ai-provider-e2e.yml,
build-exe-for-python-sdk.yml, and ci.yml's node-compat and benchmark
jobs. The enterprise restore-only PR jobs and the Windows job keep their
documented asymmetries; serial-linux keeps its hand-rolled store cache
because it produces the exact key those restore-only jobs consume, and
only swaps corepack for the action.
Implements and moves the CI-caching Agent Note from PR #667 to
implemented/, recording the serial-linux producer exception and the
exact-key (no restore-keys prefix) behavior of setup-node's cache.
Review round on the pivoted design:
- node-24 (static) kept an unconditional hosted pnpm cache restore while the
coverage and consumers lanes skip it under failover. On the self-hosted VM
that restore downloads ~180 MB into /home/runner, a path pnpm never reads
there, adding latency and contention during an outage. Gate it with the same
`vars.DSH_CI_FAILOVER != 'selfhosted'` condition so all three lanes match.
- Runbook switch step 2 said "Re-run failed jobs", but the documented
indefinite-queue outage leaves jobs queued (not failed), which cannot be
re-run in place and do not retarget on variable change. Correct both language
sides to cancel the run and re-run all jobs, or push a new commit.
- The standby-lane comment still described the switch as a one-line runs-on
change; it is now setting the admin-only DSH_CI_FAILOVER variable.
The failover runbook landed as .agents/notes/implemented/process/ci-failover-runbook.md,
which fails three doc-sync gates: the classification/format gates require a
yyyy-mm-dd-topic.md filename and the implemented Agent Note skeleton
(Problem/Decision/Alternatives/Consequences), and the bilingual pairing gate
requires cross-note link targets to match between the two language sides.
Rename to 2026-07-26-ci-failover-runbook.md/.zh.md, reshape both sides into the
implemented skeleton (the runbook steps live in bespoke sections under Decision),
point the sibling topology note and the ci.yml comment at the dated filename, and
make both sides link the canonical .md per the bilingual convention. Re-recorded
the i18n pairing records.
The three required Linux jobs now resolve their pool through the
DSH_CI_FAILOVER repository variable. Unset, everything runs exactly as
today on the hosted enterprise pools. Setting it to 'selfhosted'
(repo-admin-only, not PR-editable, no merge required — a merge would be
deadlocked behind the failing checks themselves) retargets all three
onto the vm-backup pool, halves the coverage worker bound and snapshot
concurrency for the shared VM, and skips the hosted-path cache restores.
Adds a bilingual failover runbook (switch, capacity via the four
registered spare instances, switch-back, trust boundary) and links it
from the topology note. The push-triggered standby lane remains the
continuous proof that the failover target works.
Direction change after review discussion. Moving a REQUIRED check onto
a single in-house VM traded merge-path availability for modest savings
and accumulated trust/contention caveats (six review rounds' worth).
Revert every coverage-lane change: coverage stays on the enterprise
Ubuntu 24.04 32-core pool exactly as on master.
Instead, add serial-linux-selfhosted: on every master push the in-house
pool (vm-backup) runs the complete unsharded primary aggregate as a
hot-standby drill. It blocks nothing, yet continuously proves the
environment end to end, so any hosted-pool outage can be answered with
a one-line runs-on retarget onto continuously verified capacity.
Push-triggered lanes execute the base branch's own workflow definition,
so no PR-editable path selects these runners — the entire fork-trust
discussion is structurally moot for this lane. Topology notes (en/zh +
pairing records) describe the standby lane and the switch play.
The hosted 32-core runner is exclusive to one job, but the vm-backup
pool shares one 64-core VM across four runner instances; concurrent
PRs could stack 4×24 = 96 Vitest workers and re-trigger the documented
aggregate-contention failures in the timing-sensitive process suites.
Bound the self-hosted leg at 12 workers per job (48 host-wide fully
loaded) and keep 24 on the hosted leg, selected by the same expression
as the pool.
Sweep all remaining sources that still described coverage as an
enterprise 32-core job: the ci.yml jobs preamble, the three-job
paragraph of the larger-hosted-runners note, and the required-pool
sentence of the portable-recovery note — English and Chinese sides of
both notes, with their i18n pairing records re-recorded.