diff --git a/.changeset/release-pr-ci-gates.md b/.changeset/release-pr-ci-gates.md new file mode 100644 index 0000000000..c9740d36c6 --- /dev/null +++ b/.changeset/release-pr-ci-gates.md @@ -0,0 +1,45 @@ +--- +--- + +ci(release): unblock the two gates that fail every `chore: version packages` PR (#4894) + +Both failures on #4422 were the gates themselves, not the release PR's content, +and both recur on every release PR. + +**`Check Changeset` was structurally unsatisfiable for the release PR.** The +gate counts changesets a PR *adds* (`git diff --diff-filter=A` against the base) +— the right question for an ordinary PR, and the fix #3373 landed after a global +`find | wc -l` proved unable to ever go red in RC mode. But the Changesets +release PR is the *consuming* side: it applies pending changesets into versions +and CHANGELOGs and adds none, by construction. Nobody labels a bot-authored PR +`skip-changeset`, so the release sat blocked on a check that could only be red. +`changeset-release/main` is now exempt at the job level, pinned to the bot author +as well as the branch name so a hand-pushed branch of that name cannot borrow the +exemption. + +**`Scaffold E2E` skewed the protocol major against itself during an RC window.** +The install step already falls back to `latest` when the repo's version is not +yet published (`@objectstack/cli@^17.0.0-rc.2` → ETARGET → retry as `latest`). +That fallback rewrote the generated project's dependencies but not its manifest, +and the template stamps the repo's protocol major (`engines: { protocol: '^17' }`, +written at version time by `sync-template-versions.mjs`) while `latest` still +pointed at 16.x. The ADR-0087 D1 handshake then correctly refused to boot the +artifact — the gate working, on a skew the step had introduced: + +``` +✗ package 'e2e-app' targets protocol ^17 (engines.protocol) but this runtime is + protocol 16.0.0 +``` + +The fallback now re-stamps `engines.protocol` to the major actually installed, +read off `node_modules/@objectstack/spec` (`PROTOCOL_VERSION` is kept in lockstep +with that package's own major, asserted by `protocol-version.test.ts`), and logs +a `::notice` so the run's true protocol is visible rather than silently rewritten. +It is confined to the fallback branch: on the normal path the project installs +the repo's own version, the majors agree by construction, and a template stamping +the wrong major must still fail — which is what `template-consistency.test.ts` is +for. Re-stamping happens before `npm run build`, so the artifact and the Docker +image (already pinned to the resolved CLI version by the same reasoning) stay in +step. + +CI configuration only; releases nothing. diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index face29ce72..b1e2f7060e 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -49,7 +49,20 @@ jobs: changeset-check: name: Check Changeset runs-on: ubuntu-latest - if: "!contains(github.event.pull_request.labels.*.name, 'skip-changeset')" + # Two exemptions, both meaning "this PR declares no release of its own": + # - the `skip-changeset` label — the author's explicit opt-out; + # - the Changesets release PR (`changeset-release/main`, pushed by + # changesets/action). That PR is the CONSUMING side: it applies pending + # changesets into versions and CHANGELOGs and adds none, so the gate + # below can only ever fail it. It did, on every `chore: version + # packages` PR (#4422 / #4894), leaving the release blocked on a check + # that was structurally unsatisfiable. + # Pin the author as well as the branch name, so a hand-pushed branch of + # that name cannot borrow the exemption as an escape hatch. + if: >- + !contains(github.event.pull_request.labels.*.name, 'skip-changeset') + && !(github.head_ref == 'changeset-release/main' + && github.event.pull_request.user.login == 'github-actions[bot]') permissions: contents: read pull-requests: write diff --git a/.github/workflows/scaffold-e2e.yml b/.github/workflows/scaffold-e2e.yml index 5948b1eabf..4814828826 100644 --- a/.github/workflows/scaffold-e2e.yml +++ b/.github/workflows/scaffold-e2e.yml @@ -90,6 +90,47 @@ jobs: fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); ' npm install --no-fund --no-audit + # The fallback just swapped the repo's unpublished version for + # whatever `latest` points at — which during an RC window is the + # PREVIOUS major (17.0.0-rc.N unpublished → latest is 16.x). The + # template's manifest still stamps the repo's protocol major + # (`engines: { protocol: '^17' }`, written by + # scripts/sync-template-versions.mjs), so the ADR-0087 D1 handshake + # in `os start` correctly refuses to boot the artifact: + # ✗ package 'e2e-app' targets protocol ^17 (engines.protocol) + # but this runtime is protocol 16.0.0 + # That is the gate working, on a skew this step introduced (#4894). + # Re-stamp the manifest to the protocol major actually installed so + # the rest of the job exercises the template against a coherent + # runtime. Same alignment the Docker step below already does by + # reading the resolved CLI version; done here BEFORE `npm run + # build`, so the artifact carries the corrected range too. + # + # The major is read off the installed @objectstack/spec package: + # PROTOCOL_VERSION is kept in lockstep with that package's own major + # (packages/spec/src/kernel/protocol-version.ts, asserted by + # protocol-version.test.ts), so the two cannot drift. + # + # Deliberately confined to the fallback branch: on the normal path + # the project installs the repo's own version and the majors agree + # by construction — a template that stamped the wrong major would + # still fail, which is what template-consistency.test.ts is for. + node -e ' + const fs = require("fs"); + const major = JSON.parse( + fs.readFileSync("node_modules/@objectstack/spec/package.json", "utf8"), + ).version.split(".")[0]; + const path = "objectstack.config.ts"; + const src = fs.readFileSync(path, "utf8"); + const stamp = /engines:\s*\{\s*protocol:\s*[\x27"][^\x27"]*[\x27"]\s*\}/; + if (!stamp.test(src)) { + console.log("::error::fallback cannot re-stamp engines.protocol — no stamp found in " + path); + process.exit(1); + } + const out = src.replace(stamp, "engines: { protocol: \x27^" + major + "\x27 }"); + fs.writeFileSync(path, out); + console.log("::notice::fallback re-stamped engines.protocol to ^" + major + " (installed @objectstack/spec major) — this run exercises the template against protocol " + major + ", not the repo\x27s"); + ' fi - name: Validate and build the generated project