From 4ee338532ad1b735d9e18315c727767a3a00a393 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:28:12 +0800 Subject: [PATCH] ci: retain shared pnpm store across runner instances --- ...9-06-pr-ci-runner-temporary-storage.i18n.yaml | 4 ++-- .../2026-09-06-pr-ci-runner-temporary-storage.md | 2 +- ...26-09-06-pr-ci-runner-temporary-storage.zh.md | 2 +- .github/workflows/ci.yml | 10 +++++----- .github/workflows/release-vendor.yml | 2 +- .github/workflows/release.yml | 4 ++-- scripts/ci-workflow.spec.ts | 2 +- scripts/tests/ci-release-selfhosted.spec.ts | 16 ++++++++-------- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.i18n.yaml b/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.i18n.yaml index 1a38a6e430..d734e73fff 100644 --- a/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.i18n.yaml +++ b/.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 diff --git a/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.md b/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.md index ab88d49e21..bab7f29604 100644 --- a/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.md +++ b/.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. diff --git a/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.zh.md b/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.zh.md index f4a9613b92..5a391e2a5e 100644 --- a/.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.zh.md +++ b/.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。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ee6be1ca0..5f4bdacb4e 100644 --- a/.github/workflows/ci.yml +++ b/.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: diff --git a/.github/workflows/release-vendor.yml b/.github/workflows/release-vendor.yml index 50dedc6542..194874f35a 100644 --- a/.github/workflows/release-vendor.yml +++ b/.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" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78b557f279..ab9c3f82db 100644 --- a/.github/workflows/release.yml +++ b/.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" diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index e52e87e079..7228f1a636 100644 --- a/scripts/ci-workflow.spec.ts +++ b/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"', diff --git a/scripts/tests/ci-release-selfhosted.spec.ts b/scripts/tests/ci-release-selfhosted.spec.ts index ea158caa68..98423292ce 100644 --- a/scripts/tests/ci-release-selfhosted.spec.ts +++ b/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) {