diff --git a/docs/devops.md b/docs/devops.md index d4a800b..b27c63c 100644 --- a/docs/devops.md +++ b/docs/devops.md @@ -51,7 +51,7 @@ To release a new version (e.g., `v2.5.1`), follow these steps: git commit -m "release: prepare v2.5.1" npm run release:gate -- 2.5.1 --candidate ``` -2. Commit the release notes and prepared version changes, open a pull request, +2. Push the already committed and candidate-gated changes, open a pull request, and wait for required `verify`, `node20`, and `e2e` checks. 3. After the PR is merged, fast-forward local `main`, wait for `verify`, `node20`, and `e2e` on the merge commit, then run the final local gate. It diff --git a/scripts/release-local-gate.mjs b/scripts/release-local-gate.mjs index d4a73e5..e755b14 100644 --- a/scripts/release-local-gate.mjs +++ b/scripts/release-local-gate.mjs @@ -58,6 +58,12 @@ export function linuxGateCommand() { ].join(' && '); } +export function parseRemoteMain(text) { + const match = /^([a-f0-9]{40})\trefs\/heads\/main\s*$/u.exec(String(text)); + if (!match) throw new Error(`could not resolve origin main from: ${String(text).trim() || ''}`); + return match[1]; +} + function assertCleanTree() { const status = capture('git', ['status', '--porcelain=v1']); if (status) throw new Error(`release gate requires a clean working tree:\n${status}`); @@ -67,7 +73,9 @@ function assertFinalMainChecks() { const branch = capture('git', ['branch', '--show-current']); if (branch !== 'main') throw new Error(`final release gate requires branch main, found ${branch || ''}`); const head = capture('git', ['rev-parse', 'HEAD']); - const remoteMain = capture('git', ['rev-parse', 'origin/main']); + const remoteMain = parseRemoteMain(capture('git', [ + 'ls-remote', '--exit-code', 'origin', 'refs/heads/main' + ])); if (head !== remoteMain) throw new Error(`HEAD ${head} does not match origin/main ${remoteMain}`); const checksText = capture('gh', [ diff --git a/scripts/release-local-gate.test.mjs b/scripts/release-local-gate.test.mjs index 50461cc..1eadb23 100644 --- a/scripts/release-local-gate.test.mjs +++ b/scripts/release-local-gate.test.mjs @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { linuxGateCommand, parseGateArgs, + parseRemoteMain, playwrightImageFromLock } from './release-local-gate.mjs'; @@ -20,6 +21,13 @@ describe('local release gate contract', () => { expect(() => playwrightImageFromLock({ packages: {} })).toThrow('must pin'); }); + it('extracts main only from the exact remote branch record', () => { + const sha = '0123456789abcdef0123456789abcdef01234567'; + expect(parseRemoteMain(`${sha}\trefs/heads/main\n`)).toBe(sha); + expect(() => parseRemoteMain('')).toThrow('could not resolve origin main'); + expect(() => parseRemoteMain(`${sha}\trefs/heads/not-main`)).toThrow('could not resolve origin main'); + }); + it('runs the complete CI-equivalent dependency, verify, and browser sequence', () => { expect(linuxGateCommand()).toBe([ 'git clone --no-local /src /work',