Sfoglia il codice sorgente

ci: retain shared pnpm store across runner instances

Tianyi Cui 6 giorni fa
parent
commit
4ee338532a

+ 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: ab88d49e21cdd42f4b2e9eb05967c43a7639ad16
-2026-09-06-pr-ci-runner-temporary-storage.zh.md: f4a9613b92530add458067017b81f651367d73ab
+2026-09-06-pr-ci-runner-temporary-storage.md: bab7f29604ec66a758564521e5022a577d661b93
+2026-09-06-pr-ci-runner-temporary-storage.zh.md: 5a391e2a5e9760df28007aa73941c5db84f84007

+ 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. 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 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. The pnpm store remains shared at `$HOME/.local/share/pnpm/store`, retaining cross-runner reuse under pnpm’s concurrent-access support. This change does not isolate its SQLite index or address capacity failures in that shared 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, weaken 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。消费者作业还将 Playwright 浏览器下载和安装锁放在 `RUNNER_TEMP` 旁;托管缓存恢复使用同一位置。持久化缓存的容量仍由运维人员负责。
+这三个 worker 还将 `npm_config_cache` 设为 `runner.temp/npm-cache`。[发布工作流](../../../../.github/workflows/release.yml) 在既有临时存储准备步骤中采用相同缓存位置,[vendor 演练](../../../../.github/workflows/release-vendor.yml) 也如此。否则,无论 `TMPDIR` 如何设置,npm 都会在共享 home 目录中缓存注册表响应;仅使用临时消费者目录不能隔离这些写入。pnpm store 保持共享于 `$HOME/.local/share/pnpm/store`,依靠 pnpm 的并发访问支持保留跨 runner 复用。本变更不隔离其 SQLite 索引,也不解决该共享 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。
 

+ 5 - 5
.github/workflows/ci.yml

@@ -79,7 +79,7 @@ jobs:
       - name: Configure pnpm store path
         id: pnpm-store
         run: |
-          store_root="${RUNNER_TEMP%/*}/pnpm-store"
+          store_root="$HOME/.local/share/pnpm/store"
           echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
           store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
           echo "path=$store_path" >> "$GITHUB_OUTPUT"
@@ -148,12 +148,12 @@ jobs:
       - name: Configure pnpm store path
         id: pnpm-store
         run: |
-          store_root="${RUNNER_TEMP%/*}/pnpm-store"
+          store_root="$HOME/.local/share/pnpm/store"
           echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
           store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
           echo "path=$store_path" >> "$GITHUB_OUTPUT"
 
-      # Self-hosted runners retain their own stores between jobs.
+      # Self-hosted runners reuse the shared persistent pnpm store.
       # Hosted runners restore the remote cache into their ephemeral store.
       - uses: actions/cache/restore@v4
         if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
@@ -274,7 +274,7 @@ jobs:
       - name: Configure pnpm store path
         id: pnpm-store
         run: |
-          store_root="${RUNNER_TEMP%/*}/pnpm-store"
+          store_root="$HOME/.local/share/pnpm/store"
           echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
           store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
           echo "path=$store_path" >> "$GITHUB_OUTPUT"
@@ -283,7 +283,7 @@ jobs:
       # compression and upload on the required path. No master job saves these
       # hosted cache keys, so each restore-keys fallback hits the matching archived
       # entry until it evicts, after which the store is cold. Self-hosted runners
-      # keep their own persistent stores instead of restoring the remote archive.
+      # reuse the shared persistent store instead of restoring the remote archive.
       - uses: actions/cache/restore@v4
         if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
         with:

+ 1 - 1
.github/workflows/release-vendor.yml

@@ -68,7 +68,7 @@ jobs:
       - name: Configure pnpm store path
         id: pnpm-store
         run: |
-          store_root="${RUNNER_TEMP%/*}/pnpm-store"
+          store_root="$HOME/.local/share/pnpm/store"
           echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
           store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
           echo "path=$store_path" >> "$GITHUB_OUTPUT"

+ 2 - 2
.github/workflows/release.yml

@@ -65,7 +65,7 @@ jobs:
       - name: Configure pnpm store path
         id: pnpm-store
         run: |
-          store_root="${RUNNER_TEMP%/*}/pnpm-store"
+          store_root="$HOME/.local/share/pnpm/store"
           echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
           store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
           echo "path=$store_path" >> "$GITHUB_OUTPUT"
@@ -128,7 +128,7 @@ jobs:
       - name: Configure pnpm store path
         id: pnpm-store
         run: |
-          store_root="${RUNNER_TEMP%/*}/pnpm-store"
+          store_root="$HOME/.local/share/pnpm/store"
           echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"
           store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)
           echo "path=$store_path" >> "$GITHUB_OUTPUT"

+ 1 - 1
scripts/ci-workflow.spec.ts

@@ -60,7 +60,7 @@ describe('CI workflow', () => {
       const store: unknown = job.steps.find(step => isRecord(step) && step.name === 'Configure pnpm store path')
       expect(store).toMatchObject({
         run: [
-          'store_root="${RUNNER_TEMP%/*}/pnpm-store"',
+          'store_root="$HOME/.local/share/pnpm/store"',
           'echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"',
           'store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)',
           'echo "path=$store_path" >> "$GITHUB_OUTPUT"',

+ 8 - 8
scripts/tests/ci-release-selfhosted.spec.ts

@@ -47,8 +47,8 @@ function assertEarlyNpmCacheExport(steps: Step[]): void {
   expect(steps[cacheIndex]?.if).toBeUndefined()
 }
 
-function assertRunnerPrivateStore(run: string | undefined): void {
-  expect(run).toContain('store_root="${RUNNER_TEMP%/*}/pnpm-store"')
+function assertSharedPersistentStore(run: string | undefined): void {
+  expect(run).toContain('store_root="$HOME/.local/share/pnpm/store"')
   expect(run).toContain('echo "PNPM_CONFIG_STORE_DIR=$store_root" >> "$GITHUB_ENV"')
   expect(run).toContain('store_path=$(PNPM_CONFIG_STORE_DIR="$store_root" pnpm store path --silent)')
 }
@@ -147,15 +147,15 @@ for (const [file, jobIds] of [['release.yml', ['dependencies', 'pack']], ['relea
           steps.push({ run: npmCacheExport })
           expect(() => { assertEarlyNpmCacheExport(steps) }).toThrow()
         })
-        it.each(['', 'store_root="$HOME/.local/share/pnpm/store"', 'store_root="$RUNNER_TEMP/pnpm-store"'])(
-          'rejects missing, shared-home, or job-temporary store placement: %s', (replacement) => {
+        it.each(['', 'store_root="${RUNNER_TEMP%/*}/pnpm-store"', 'store_root="$RUNNER_TEMP/pnpm-store"'])(
+          'rejects missing, runner-private, or job-temporary store placement: %s', (replacement) => {
             const run = job.steps.find(step => step.name === 'Configure pnpm store path')?.run
-              ?.replace('store_root="${RUNNER_TEMP%/*}/pnpm-store"', replacement)
-            expect(() => { assertRunnerPrivateStore(run) }).toThrow()
+              ?.replace('store_root="$HOME/.local/share/pnpm/store"', replacement)
+            expect(() => { assertSharedPersistentStore(run) }).toThrow()
           },
         )
-        it('uses a runner-private persistent store without remote cache reads or writes on self-hosted', () => {
-          assertRunnerPrivateStore(job.steps.find(step => step.name === 'Configure pnpm store path')?.run)
+        it('uses the shared persistent store without remote cache reads or writes on self-hosted', () => {
+          assertSharedPersistentStore(job.steps.find(step => step.name === 'Configure pnpm store path')?.run)
           const caches = job.steps.filter(step => step.uses?.startsWith('actions/cache'))
           expect(caches.map(step => step.uses)).toEqual(['actions/cache/restore@v4'])
           for (const step of caches) {