ci: isolate the Windows pnpm setup destination per job

The windows-* jobs keep a separate standalone pnpm executable under
runner.temp/setup-pnpm-js. A previous job on the same self-hosted runner
can leave a locked @reflink native module there, so the next job's
pnpm/action-setup fails with EPERM during unlink before any test runs.
Suffix the destination with run_id, run_attempt, and job so every job
gets a fresh directory even when sequential jobs land on the same
runner; apply the same to the python SDK exe build. Update the pnpm
setup isolation note to record the Windows-specific destination.
This commit is contained in:
Chinesezjc
2026-08-26 16:39:30 +08:00
parent 5661b7a972
commit e87a47692d
6 changed files with 29 additions and 10 deletions
@@ -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: c7c076f34dcd4b905a6bb54411538d6cf61bc1d0
2026-07-29-pnpm-setup-runner-isolation.zh.md: 2dd866404a5a799fe33e8b9c70c17787dfed0c2a
2026-07-29-pnpm-setup-runner-isolation.md: 14a609c56eb45f706c7451d2659ab397c2886411
2026-07-29-pnpm-setup-runner-isolation.zh.md: cdb13f9cfb7e2abf7d1563c39526ea2d9e1ca562
@@ -10,7 +10,7 @@ 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) 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).
Every non-Windows `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. The Windows native jobs use a separate pnpm executable under `setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}` (not `standalone: true`; the destination keeps that executable apart): the run/attempt/job suffix gives every job a fresh directory even when sequential jobs land on the same self-hosted runner and a previous job leaves a locked @reflink native module. 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 `ci-master.yml` and rejects one without the runner-private destination. This keeps newly added jobs inside the same isolation boundary.
@@ -10,7 +10,7 @@ Status: implemented
## 决策
[主 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.zh.md)。
[主 CI 工作流](../../../../.github/workflows/ci.yml)与 [CI master 工作流](../../../../.github/workflows/ci-master.yml)中的每个**非 Windows** `pnpm/action-setup` 步骤都设置 `dest: ${{ runner.temp }}/setup-pnpm`。每个 runner 服务独占自己的临时目录,因此一个设置过程无法替换另一个 runner 的安装目录。Windows 原生作业在 `setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}` 下使用独立的 pnpm 可执行文件(非 `standalone: true`,目录本身起分离作用):run/attempt/job 后缀让每次作业都使用全新目录,即使顺序作业落到同一自托管 runner、且前一作业留下被锁定的 @reflink 原生模块。持久 store 的复用仍由 `PNPM_CONFIG_STORE_DIR` 独立处理,遵循 [pnpm 配置决策](../process/2026-07-26-pnpm-action-setup-for-symmetric-ci-caching.zh.md)。
[工作流回归测试](../../../../scripts/ci-workflow.spec.ts)会找出 `ci.yml``ci-master.yml` 中的每个 `pnpm/action-setup` 步骤,并拒绝缺少 runner 专属目标目录的步骤。这可确保后续新增的作业也处于同一隔离边界内。
@@ -161,7 +161,7 @@ jobs:
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- name: Enable Windows Developer Mode (symlink support)
if: runner.os == 'Windows'
+4 -4
View File
@@ -430,7 +430,7 @@ jobs:
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
@@ -470,7 +470,7 @@ jobs:
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
@@ -508,7 +508,7 @@ jobs:
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
@@ -554,7 +554,7 @@ jobs:
run: if (Test-Path "$env:RUNNER_TEMP/setup-pnpm-js") { Remove-Item -Recurse -Force "$env:RUNNER_TEMP/setup-pnpm-js" -ErrorAction SilentlyContinue }
- uses: pnpm/action-setup@v4
with:
dest: ${{ runner.temp }}/setup-pnpm-js
dest: ${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}
- uses: actions/setup-node@v6
with:
node-version: ${{ env.PRIMARY_NODE_VERSION }}
+20 -1
View File
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'
const root = resolve(import.meta.dirname, '..')
const runnerPrivatePnpmDestination = '${{ runner.temp }}/setup-pnpm'
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js'
const nativeWindowsPnpmDestination = '${{ runner.temp }}/setup-pnpm-js-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}'
describe('CI workflow', () => {
it('isolates every pnpm action setup destination per runner', () => {
@@ -36,6 +36,25 @@ describe('CI workflow', () => {
}
})
it('isolates the python SDK exe pnpm setup destination per job', () => {
const workflow: unknown = yaml.load(readFileSync(resolve(root, '.github/workflows/build-exe-for-python-sdk.yml'), 'utf8'))
if (!isRecord(workflow) || !isRecord(workflow.jobs)) throw new TypeError('build-exe-for-python-sdk.yml must define jobs')
const setups: Array<{ step: unknown }> = []
for (const job of Object.values(workflow.jobs)) {
if (!isRecord(job) || !Array.isArray(job.steps)) continue
for (const step of job.steps) {
if (!isRecord(step) || typeof step.uses !== 'string' || !step.uses.startsWith('pnpm/action-setup@')) continue
setups.push({ step })
}
}
expect(setups.length).toBeGreaterThan(0)
for (const { step } of setups) {
expect(step).toMatchObject({
with: { dest: nativeWindowsPnpmDestination },
})
}
})
it('keeps required Wine and split native Windows jobs with failover, plus a master-only standby', () => {
const workflow = loadWorkflow('.github/workflows/ci.yml')
const masterWorkflow = loadWorkflow('.github/workflows/ci-master.yml')