From fd2c1635551a31b16b1b1c053815a94e01183a23 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 19 May 2026 18:41:56 +0100 Subject: [PATCH 1/5] ci(release): reconcile lockfile before gated changeset commands verifyDepsBeforeRun: error gates pnpm run/exec. The frozen install in the release job does not refresh the lockfile's overrides hash, so a lockfile whose overrides drifted from pnpm-workspace.yaml passes the frozen install but trips the gate on `pnpm run changeset:version` / `pnpm changeset publish`, killing every release before it versions or publishes. A non-frozen install is not gated and reconciles the hash; changesets/action commits the result, self-healing the repo. --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d300fa77e4..d481d9677f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,12 @@ jobs: - name: Block 1.x releases (we are in 0.x) run: node .github/scripts/check-no-major.mjs + # Not redundant with the frozen install: frozen install does not + # refresh the lockfile's overrides hash, so a drifted lockfile trips + # verifyDepsBeforeRun on the gated changeset commands below. + - name: Reconcile lockfile + run: pnpm install --no-frozen-lockfile + - name: Create Release Pull Request or Publish if: ${{ !inputs.publish-only }} id: changesets From 29c912e7fc3d80cbc6d3373755d491f006491db5 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 19 May 2026 19:03:07 +0100 Subject: [PATCH 2/5] ci(release): reconcile lockfile inside the changeset command, not as a prior step changesets/action does git checkout changeset-release/main + git reset --hard right before running the version/publish command, so any reconcile placed in an earlier workflow step is discarded by that reset and the gated 'pnpm changeset' call still trips verifyDepsBeforeRun. Move the non-frozen install into a single non-gated node entrypoint the action invokes, so it runs after the action's git work and immediately before the gated pnpm call. Drop the now-unreferenced changeset:version script. --- .github/scripts/release.mjs | 23 +++++++++++++++++++++++ .github/workflows/release.yml | 12 +++--------- package.json | 3 +-- 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 .github/scripts/release.mjs diff --git a/.github/scripts/release.mjs b/.github/scripts/release.mjs new file mode 100644 index 0000000000..40988845e2 --- /dev/null +++ b/.github/scripts/release.mjs @@ -0,0 +1,23 @@ +import { execFileSync } from "node:child_process"; + +const mode = process.argv[2]; + +const run = (args) => execFileSync("pnpm", args, { stdio: "inherit" }); + +// changesets/action runs `git checkout changeset-release/main` + `git reset +// --hard` immediately before this, which can leave the lockfile's overrides +// hash out of sync with pnpm-workspace.yaml. The next gated pnpm call +// (verifyDepsBeforeRun: error) would then abort the release. `pnpm install` +// is not gated, so reconcile here, after the action's git work and before +// any `pnpm changeset` call. Do not hoist this into an earlier workflow +// step: the action's git reset runs after workflow steps and undoes it. +run(["install", "--no-frozen-lockfile"]); + +if (mode === "version") { + run(["changeset", "version"]); + run(["install", "--no-frozen-lockfile"]); +} else if (mode === "publish") { + run(["changeset", "publish"]); +} else { + throw new Error(`Unknown release mode: ${JSON.stringify(mode)} (expected "version" or "publish")`); +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d481d9677f..9ca4ac3bc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,19 +66,13 @@ jobs: - name: Block 1.x releases (we are in 0.x) run: node .github/scripts/check-no-major.mjs - # Not redundant with the frozen install: frozen install does not - # refresh the lockfile's overrides hash, so a drifted lockfile trips - # verifyDepsBeforeRun on the gated changeset commands below. - - name: Reconcile lockfile - run: pnpm install --no-frozen-lockfile - - name: Create Release Pull Request or Publish if: ${{ !inputs.publish-only }} id: changesets uses: changesets/action@63a615b9cd06ba9a3e6d13796c7fbcb080a60a0b # v1.8.0 with: - version: pnpm run changeset:version - publish: pnpm changeset publish + version: node .github/scripts/release.mjs version + publish: node .github/scripts/release.mjs publish commit: "ci: release" title: "ci: release" env: @@ -86,7 +80,7 @@ jobs: - name: Publish (manual) if: ${{ inputs.publish-only }} - run: pnpm changeset publish + run: node .github/scripts/release.mjs publish sync-templates: name: Sync Templates diff --git a/package.json b/package.json index 9f6655c70c..9ede6eb309 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,7 @@ "screenshots": "node scripts/screenshot-all-templates.mjs", "query-counts": "node scripts/query-counts.mjs", "locale:extract": "pnpm --filter @emdash-cms/admin locale:extract", - "locale:compile": "pnpm --filter @emdash-cms/admin locale:compile", - "changeset:version": "pnpm changeset version && pnpm install --no-frozen-lockfile" + "locale:compile": "pnpm --filter @emdash-cms/admin locale:compile" }, "keywords": [], "author": "Matt Kane", From 0eb4c6d16627d745ddf1e5eded1d107ecb5dd6b9 Mon Sep 17 00:00:00 2001 From: "emdashbot[bot]" Date: Tue, 19 May 2026 18:03:47 +0000 Subject: [PATCH 3/5] style: format --- .github/scripts/release.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/scripts/release.mjs b/.github/scripts/release.mjs index 40988845e2..c311e9c72a 100644 --- a/.github/scripts/release.mjs +++ b/.github/scripts/release.mjs @@ -19,5 +19,7 @@ if (mode === "version") { } else if (mode === "publish") { run(["changeset", "publish"]); } else { - throw new Error(`Unknown release mode: ${JSON.stringify(mode)} (expected "version" or "publish")`); + throw new Error( + `Unknown release mode: ${JSON.stringify(mode)} (expected "version" or "publish")`, + ); } From ee2a06e240d1cb749aa6d5d74beb7651e6195b28 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 19 May 2026 19:10:04 +0100 Subject: [PATCH 4/5] ci(release): use --prefer-frozen-lockfile for the reconcile changeset publish rebuilds packages via prepublishOnly at pack time. A non-frozen reconcile could re-resolve in-range transitive deps so shipped bits diverge from tested bits. prefer-frozen reconciles the deps state to satisfy verifyDepsBeforeRun without re-resolving when the lockfile is satisfiable, and falls back to a full install (e.g. after changeset version bumps workspace versions) when it isn't. --- .github/scripts/release.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/scripts/release.mjs b/.github/scripts/release.mjs index c311e9c72a..ba21354970 100644 --- a/.github/scripts/release.mjs +++ b/.github/scripts/release.mjs @@ -5,17 +5,20 @@ const mode = process.argv[2]; const run = (args) => execFileSync("pnpm", args, { stdio: "inherit" }); // changesets/action runs `git checkout changeset-release/main` + `git reset -// --hard` immediately before this, which can leave the lockfile's overrides -// hash out of sync with pnpm-workspace.yaml. The next gated pnpm call +// --hard` immediately before this, which can leave the deps state out of +// sync with pnpm-workspace.yaml. The next gated pnpm call // (verifyDepsBeforeRun: error) would then abort the release. `pnpm install` // is not gated, so reconcile here, after the action's git work and before // any `pnpm changeset` call. Do not hoist this into an earlier workflow // step: the action's git reset runs after workflow steps and undoes it. -run(["install", "--no-frozen-lockfile"]); +// prefer-frozen avoids re-resolving deps when the lockfile is already +// satisfiable (so published packages, which rebuild via prepublishOnly, +// match what was tested) but falls back to a full install when it isn't. +run(["install", "--prefer-frozen-lockfile"]); if (mode === "version") { run(["changeset", "version"]); - run(["install", "--no-frozen-lockfile"]); + run(["install", "--prefer-frozen-lockfile"]); } else if (mode === "publish") { run(["changeset", "publish"]); } else { From 8b49a6457fe16b3f347f354afb1e49f311da41d2 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Tue, 19 May 2026 19:48:58 +0100 Subject: [PATCH 5/5] ci(release): revert reconcile to --no-frozen-lockfile The PR's purpose is to reliably clear ERR_PNPM_VERIFY_DEPS_BEFORE_RUN. --no-frozen-lockfile is pnpm's documented remediation and unconditionally refreshes the deps-state hash. --prefer-frozen-lockfile's fast path is gated by a satisfiability check that may not include the settings hash, so it could skip the rewrite in exactly the stale-metadata case this fixes. The shipped-vs-tested concern that motivated prefer-frozen is low-probability, pre-existing, and negligible when the lockfile is satisfiable (no re-resolution occurs). --- .github/scripts/release.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/scripts/release.mjs b/.github/scripts/release.mjs index ba21354970..7330ed3ae4 100644 --- a/.github/scripts/release.mjs +++ b/.github/scripts/release.mjs @@ -11,14 +11,14 @@ const run = (args) => execFileSync("pnpm", args, { stdio: "inherit" }); // is not gated, so reconcile here, after the action's git work and before // any `pnpm changeset` call. Do not hoist this into an earlier workflow // step: the action's git reset runs after workflow steps and undoes it. -// prefer-frozen avoids re-resolving deps when the lockfile is already -// satisfiable (so published packages, which rebuild via prepublishOnly, -// match what was tested) but falls back to a full install when it isn't. -run(["install", "--prefer-frozen-lockfile"]); +// Must be --no-frozen-lockfile, not --prefer-frozen-lockfile: prefer-frozen +// can take the lockfile fast path and skip rewriting the stale deps-state +// hash, which is the exact condition that trips the gate. +run(["install", "--no-frozen-lockfile"]); if (mode === "version") { run(["changeset", "version"]); - run(["install", "--prefer-frozen-lockfile"]); + run(["install", "--no-frozen-lockfile"]); } else if (mode === "publish") { run(["changeset", "publish"]); } else {