From 4782427127c4aa60ba3996d96ee6118f8c5890ff Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Sun, 5 Jul 2026 01:28:48 +0100 Subject: [PATCH] fix(ci): retry prisma generate to survive Windows EPERM engine-DLL race `@trigger.dev/database` and `@internal/run-ops-database` pin the same prisma version, so pnpm gives them one shared package instance in the store. When `turbo run generate` runs their generate scripts concurrently, both prisma processes race to write the query-engine binary into that shared directory and on Windows the loser fails with `EPERM: operation not permitted, rename` on the `.tmp -> .dll.node` step, killing CI before any test runs. Wrap `prisma generate` in a small cross-platform node script that retries on transient filesystem contention (EPERM/EBUSY/EACCES) with backoff. On non-Windows the first attempt always succeeds, so this is a zero-cost no-op. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/e2e.yml | 2 + internal-packages/database/package.json | 2 +- .../run-ops-database/package.json | 2 +- scripts/retry-prisma-generate.mjs | 53 +++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 scripts/retry-prisma-generate.mjs diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d8aa9ec7d7d..2ec5ac470bc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,6 +42,8 @@ jobs: - name: 📥 Download deps run: pnpm install --frozen-lockfile --filter trigger.dev... + # Prisma clients generate concurrently and share one engine binary in the + # pnpm store; the generate script retries the transient Windows EPERM race. - name: 📀 Generate Prisma Client run: pnpm run generate diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index ec0bc950b8c..560cf229dd0 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -15,7 +15,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "prisma migrate deploy", "db:push": "prisma db push", diff --git a/internal-packages/run-ops-database/package.json b/internal-packages/run-ops-database/package.json index cbeb0279b05..7bfe0fe271c 100644 --- a/internal-packages/run-ops-database/package.json +++ b/internal-packages/run-ops-database/package.json @@ -25,7 +25,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "node scripts/migrate.mjs deploy", "db:migrate:status": "node scripts/migrate.mjs status", diff --git a/scripts/retry-prisma-generate.mjs b/scripts/retry-prisma-generate.mjs new file mode 100644 index 00000000000..f20fa4616ca --- /dev/null +++ b/scripts/retry-prisma-generate.mjs @@ -0,0 +1,53 @@ +// Retry wrapper around `prisma generate`. Our two prisma clients pin the same +// prisma version and so share one package instance in the pnpm store; when +// `turbo run generate` runs them concurrently both race to write the shared +// query-engine binary, and on Windows the loser fails with `EPERM ... rename`. +// Retrying lets it succeed once the engine file is present and unlocked. On +// non-Windows the first attempt succeeds, so this is a zero-cost no-op. +import { spawnSync } from "node:child_process"; + +const MAX_ATTEMPTS = 5; +const BASE_DELAY_MS = 500; + +// Transient, retryable filesystem contention on the shared engine binary. +const TRANSIENT = + /\b(EPERM|EBUSY|EACCES)\b|operation not permitted|resource busy or locked|being used by another process/i; + +const passthroughArgs = process.argv.slice(2); + +function sleepSync(ms) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); +} + +let lastStatus = 1; + +for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const result = spawnSync("prisma", ["generate", ...passthroughArgs], { + shell: true, + encoding: "utf8", + }); + + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + + if (result.status === 0) { + process.exit(0); + } + + lastStatus = result.status ?? 1; + + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + const isRetryable = TRANSIENT.test(output); + + if (!isRetryable || attempt === MAX_ATTEMPTS) { + break; + } + + const delay = BASE_DELAY_MS * attempt; + console.error( + `prisma generate hit a transient filesystem error (attempt ${attempt}/${MAX_ATTEMPTS}); retrying in ${delay}ms...` + ); + sleepSync(delay); +} + +process.exit(lastStatus);