diff --git a/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.i18n.yaml index 11c7a8935a..8bebda2bf5 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.md -2026-07-29-pnpm-setup-runner-isolation.md: 74b672b3f90ea445ad1a8e283a5904056059b2f8 -2026-07-29-pnpm-setup-runner-isolation.zh.md: fde4cb747e41ea7dafe87ca3ccd0e872c45dd3b1 +2026-07-29-pnpm-setup-runner-isolation.md: c7c076f34dcd4b905a6bb54411538d6cf61bc1d0 +2026-07-29-pnpm-setup-runner-isolation.zh.md: e0eec45d5c3ea53623e95b3ff5509df5ec22457f diff --git a/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.md b/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.md index 74b672b3f9..c7c076f34d 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.md +++ b/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.md @@ -10,9 +10,9 @@ English | [中文](2026-07-29-pnpm-setup-runner-isolation.zh.md) ## Decision -Every `pnpm/action-setup` step in [the primary CI workflow](../../../../.github/workflows/ci.yml) sets `dest: ${{ runner.temp }}/setup-pnpm`. Each runner service owns its temporary directory, so one setup cannot replace another runner's install directory. Persistent store reuse remains separate through `PNPM_CONFIG_STORE_DIR`, as established by the [pnpm provisioning decision](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md). +Every `pnpm/action-setup` step in [the primary CI workflow](../../../../.github/workflows/ci.yml) and [the master workflow](../../../../.github/workflows/ci-master.yml) sets `dest: ${{ runner.temp }}/setup-pnpm`. Each runner service owns its temporary directory, so one setup cannot replace another runner's install directory. Persistent store reuse remains separate through `PNPM_CONFIG_STORE_DIR`, as established by the [pnpm provisioning decision](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md). -[The workflow regression test](../../../../scripts/ci-workflow.spec.ts) discovers every `pnpm/action-setup` step in `ci.yml` and rejects one without the runner-private destination. This keeps newly added jobs inside the same isolation boundary. +[The workflow regression test](../../../../scripts/ci-workflow.spec.ts) discovers every `pnpm/action-setup` step in `ci.yml` and `ci-master.yml` and rejects one without the runner-private destination. This keeps newly added jobs inside the same isolation boundary. ## Alternatives considered diff --git a/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.zh.md b/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.zh.md index fde4cb747e..e0eec45d5c 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-29-pnpm-setup-runner-isolation.zh.md @@ -10,9 +10,9 @@ Status: implemented ## 决策 -[主 CI 工作流](../../../../.github/workflows/ci.yml)中的每个 `pnpm/action-setup` 步骤都设置 `dest: ${{ runner.temp }}/setup-pnpm`。每个 runner 服务独占自己的临时目录,因此一个设置过程无法替换另一个 runner 的安装目录。持久 store 的复用仍由 `PNPM_CONFIG_STORE_DIR` 独立处理,遵循 [pnpm 配置决策](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md)。 +[主 CI 工作流](../../../../.github/workflows/ci.yml)与 [CI master 工作流](../../../../.github/workflows/ci-master.yml)中的每个 `pnpm/action-setup` 步骤都设置 `dest: ${{ runner.temp }}/setup-pnpm`。每个 runner 服务独占自己的临时目录,因此一个设置过程无法替换另一个 runner 的安装目录。持久 store 的复用仍由 `PNPM_CONFIG_STORE_DIR` 独立处理,遵循 [pnpm 配置决策](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.md)。 -[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)会找出 `ci.yml` 中的每个 `pnpm/action-setup` 步骤,并拒绝缺少 runner 专属目标目录的步骤。这可确保后续新增的作业也处于同一隔离边界内。 +[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)会找出 `ci.yml` 与 `ci-master.yml` 中的每个 `pnpm/action-setup` 步骤,并拒绝缺少 runner 专属目标目录的步骤。这可确保后续新增的作业也处于同一隔离边界内。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml index fd079921f0..ee85a9b69c 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md -2026-07-26-ci-failover-runbook.md: 33fa8d4400d35aac120b9be23ce8b04f1ee11c2b +2026-07-26-ci-failover-runbook.md: bfed4e6e15311d0191c1379a5822b0daf46f4ed3 2026-07-26-ci-failover-runbook.zh.md: 64930954377e3c7caeec80a18e723807a7b3f371 diff --git a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md index 33fa8d4400..bfed4e6e15 100644 --- a/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md +++ b/.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md @@ -16,7 +16,7 @@ Each of the three required Linux worker jobs, the independent native Windows job The exemption is narrower than "a drill always finishes", in two ways. GitHub keeps a single pending entry per group, so a newer pending run displaces an older one and intermediate push runs still end as `cancelled` during busy periods. And the expression is evaluated against the *newly triggered* run, so a run whose own event is not `push` — a benchmark dispatched on master within `ci-master.yml`, sharing its group `CI master-` — evaluates to `true` and does cancel a drill that is mid-flight. That is a rare manual action and the next master push restores the evidence, so it does not warrant further mechanism. What the carve-out buys is that the lane periodically reaches a verdict at all, which is what makes it usable as evidence. -The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push carries only `wine-apt-cache` and these two drills; every other job is pull-request-gated, `workflow_dispatch`-gated, or `if: false`, and `scripts/ci-workflow.spec.ts` pins that set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. +The decision belongs at workflow level because cancellation applies to the whole superseded run: a job-level `concurrency` group does not exempt its job. The negated form is load-bearing rather than cosmetic: naming `pull_request` alone would also stop cancelling `workflow_dispatch`, and each runner benchmark fans out to twelve larger runners for up to fifteen minutes inside this same group on master, so a re-dispatch would queue ahead of a drill instead of replacing a stale measurement. What bounds the cost is that a master push in `ci-master.yml` carries only `wine-apt-cache` and these two drills; the pull-request jobs live in the separate `ci.yml` (which does not see `push`), and the benchmarks are `workflow_dispatch`-gated within `ci-master.yml`. `scripts/ci-workflow.spec.ts` pins that push-reachable set — classifying by exact condition, since a negated event test mentions the event it excludes — so a new push-reachable job cannot quietly start accumulating uncancelled runs. ### What the in-house pool is diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index d2df96fa4b..f324900c9e 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -62,6 +62,16 @@ jobs: cp /var/cache/apt/archives/*.deb "$HOME/wine-debs/" du -sh "$HOME/wine-debs" + # Hot-standby drill for the in-house self-hosted pool: every master move + # re-runs the complete unsharded aggregate on the persistent 64-core VM, + # continuously proving that environment can take over a required lane if + # the hosted pools degrade (the switch is then setting the writer-manageable + # DSH_CI_FAILOVER_LINUX variable — see the failover runbook, no merge required). + # Push-triggered, so this lane always executes the base branch's own + # workflow definition. This workflow never listens to pull_request, so the + # drill does not appear in PR checks. No cache steps because the VM's + # persistent pnpm store and tool caches make them redundant (and saving here + # would poison the hosted cache namespace with self-hosted paths). serial-linux-selfhosted: if: github.event_name == 'push' && github.ref == 'refs/heads/master' name: serial / linux (self-hosted standby) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d79953e9f7..91d346d281 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -447,7 +447,8 @@ jobs: # node versions evolve. Every blocking job in THIS workflow must be listed in # `needs`. The required Wine job is listed as `windows`; `windows-native` is # deliberately absent so its independent result never delays or changes this - # verdict. + # verdict. (`needs` cannot reach across workflow files; the master-only jobs in + # ci-master.yml are intentionally not part of this PR verdict.) # `if: always()` is load-bearing: without it a failed dependency # would SKIP this job, and GitHub counts a skipped required check as passing # — so this job always runs and fails on any non-success result, including