فهرست منبع

fix(ci): isolate Playwright cache and installation locks per runner

Tianyi Cui 3 هفته پیش
والد
کامیت
cc69d5fcac

+ 2 - 2
.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.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/testing/2026-09-06-pr-ci-runner-temporary-storage.md
-2026-09-06-pr-ci-runner-temporary-storage.md: 93a631e3489f9cbf8b3aaee81cfdd7c170f6403b
-2026-09-06-pr-ci-runner-temporary-storage.zh.md: 48759b76263005b29399921ecd65878fcd6022b1
+2026-09-06-pr-ci-runner-temporary-storage.md: 29c4b52afdd366ba3b58b70371c10ab261a50b67
+2026-09-06-pr-ci-runner-temporary-storage.zh.md: b653c3b5bc17b9fadd3e1515b13c0f23b106e531

+ 1 - 1
.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.md

@@ -12,7 +12,7 @@ The Linux failover pool runs multiple runner instances on one VM. PR coverage an
 
 The static, coverage, and consumer jobs in [PR CI](../../../../.github/workflows/ci.yml) export `TMPDIR=runner.temp` through `GITHUB_ENV` in their first step before any setup or test process starts. Node, Vite, tsx, and temporary test consumers inherit the runner-owned location. Each runner owns its directory and GitHub Actions clears its removable contents at job start and completion; fixtures still allocate unique children and retain their own cleanup.
 
-The three workers also set `npm_config_cache` to `runner.temp/npm-cache`. The [release workflows](../../../../.github/workflows/release.yml) apply the same cache location in their existing temporary-storage setup, including [vendor rehearsals](../../../../.github/workflows/release-vendor.yml). npm otherwise caches registry responses under the shared home directory regardless of `TMPDIR`; a temporary consumer alone does not isolate those writes. Each worker keeps its persistent pnpm store beside `RUNNER_TEMP`, under its runner work root. This places the SQLite store index on the workspace volume and separates concurrent runner instances without deleting the shared home store. The first install per runner is cold; subsequent jobs on that runner reuse its store. Operators retain responsibility for persistent-store capacity.
+The three workers also set `npm_config_cache` to `runner.temp/npm-cache`. The [release workflows](../../../../.github/workflows/release.yml) apply the same cache location in their existing temporary-storage setup, including [vendor rehearsals](../../../../.github/workflows/release-vendor.yml). npm otherwise caches registry responses under the shared home directory regardless of `TMPDIR`; a temporary consumer alone does not isolate those writes. Each worker keeps its persistent pnpm store beside `RUNNER_TEMP`, under its runner work root. This places the SQLite store index on the workspace volume and separates concurrent runner instances without deleting the shared home store. The first install per runner is cold; subsequent jobs on that runner reuse its store. The consumer job also places Playwright browser downloads and installation locks beside `RUNNER_TEMP`; hosted cache restore uses that same location. Operators retain responsibility for persistent-cache capacity.
 
 The [release rehearsal decision](../process/2026-09-06-release-rehearsal-selfhosted.md) applies the same lifetime rule to release consumers. The [failover runbook](../process/2026-07-26-ci-failover-runbook.md) continues to own runner selection and shared-host capacity. This change does not retarget jobs, reduce concurrency, retry tests, change assertions, or modify master-only CI.
 

+ 1 - 1
.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.zh.md

@@ -12,7 +12,7 @@ Linux 故障切换池在同一台虚拟机上运行多个 runner 实例。PR 覆
 
 [PR CI](../../../../.github/workflows/ci.yml) 的静态检查、覆盖率和消费者作业在任何准备或测试进程启动前,在首个步骤通过 `GITHUB_ENV` 导出 `TMPDIR=runner.temp`。Node、Vite、tsx 和临时测试消费者继承 runner 管理的位置。每个 runner 管理自己的目录,GitHub Actions 在作业开始和完成时清除其中可删除的内容;测试夹具仍分配唯一子目录,并保留自身清理逻辑。
 
-这三个 worker 还将 `npm_config_cache` 设为 `runner.temp/npm-cache`。[发布工作流](../../../../.github/workflows/release.yml) 在既有临时存储准备步骤中采用相同缓存位置,[vendor 演练](../../../../.github/workflows/release-vendor.yml) 也如此。否则,无论 `TMPDIR` 如何设置,npm 都会在共享 home 目录中缓存注册表响应;仅使用临时消费者目录不能隔离这些写入。每个 worker 将持久化 pnpm store 放在 `RUNNER_TEMP` 旁的 runner 工作根目录下。这使 SQLite store 索引位于 workspace 所在卷,并隔离并发 runner 实例,而不删除共享 home store。每个 runner 的首次安装是冷启动;该 runner 的后续作业复用其 store。持久化 store 的容量仍由运维人员负责。
+这三个 worker 还将 `npm_config_cache` 设为 `runner.temp/npm-cache`。[发布工作流](../../../../.github/workflows/release.yml) 在既有临时存储准备步骤中采用相同缓存位置,[vendor 演练](../../../../.github/workflows/release-vendor.yml) 也如此。否则,无论 `TMPDIR` 如何设置,npm 都会在共享 home 目录中缓存注册表响应;仅使用临时消费者目录不能隔离这些写入。每个 worker 将持久化 pnpm store 放在 `RUNNER_TEMP` 旁的 runner 工作根目录下。这使 SQLite store 索引位于 workspace 所在卷,并隔离并发 runner 实例,而不删除共享 home store。每个 runner 的首次安装是冷启动;该 runner 的后续作业复用其 store。消费者作业还将 Playwright 浏览器下载和安装锁放在 `RUNNER_TEMP` 旁;托管缓存恢复使用同一位置。持久化缓存的容量仍由运维人员负责。
 
 [发布演练决策](../process/2026-09-06-release-rehearsal-selfhosted.zh.md) 对发布消费者采用相同的生命周期规则。[故障切换运行手册](../process/2026-07-26-ci-failover-runbook.zh.md) 继续负责 runner 选择和共享主机容量。本变更不调整作业目标、不降低并发、不重试测试、不修改断言,也不修改仅在 master 上执行的 CI。
 

+ 3 - 2
.github/workflows/ci.yml

@@ -236,6 +236,7 @@ jobs:
         run: |
           echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"
           echo "npm_config_cache=${{ runner.temp }}/npm-cache" >> "$GITHUB_ENV"
+          echo "PLAYWRIGHT_BROWSERS_PATH=${RUNNER_TEMP%/*}/ms-playwright" >> "$GITHUB_ENV"
 
       - uses: actions/checkout@v6
         with:
@@ -270,11 +271,11 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-
 
-      # Skipped under failover: the VM's persistent browser cache is already warm.
+      # Self-hosted runners retain their own browser cache between jobs.
       - uses: actions/cache/restore@v4
         if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
         with:
-          path: ~/.cache/ms-playwright
+          path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
           key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
           restore-keys: |
             ${{ runner.os }}-playwright-

+ 8 - 0
scripts/ci-workflow.spec.ts

@@ -46,9 +46,17 @@ describe('CI workflow', () => {
         run: [
           'echo "TMPDIR=${{ runner.temp }}" >> "$GITHUB_ENV"',
           'echo "npm_config_cache=${{ runner.temp }}/npm-cache" >> "$GITHUB_ENV"',
+          ...(jobName === 'node-24-consumers'
+            ? ['echo "PLAYWRIGHT_BROWSERS_PATH=${RUNNER_TEMP%/*}/ms-playwright" >> "$GITHUB_ENV"']
+            : []),
           '',
         ].join('\n'),
       })
+      if (jobName === 'node-24-consumers') {
+        const browserCache: unknown = job.steps.find(step => isRecord(step) && isRecord(step.with)
+          && step.with.path === '${{ env.PLAYWRIGHT_BROWSERS_PATH }}')
+        expect(browserCache).toMatchObject({ uses: 'actions/cache/restore@v4' })
+      }
       const store: unknown = job.steps.find(step => isRecord(step) && step.name === 'Configure pnpm store path')
       expect(store).toMatchObject({
         run: [