diff --git a/scripts/check-version-coherence.mjs b/scripts/check-version-coherence.mjs index fa1b6861..98345813 100755 --- a/scripts/check-version-coherence.mjs +++ b/scripts/check-version-coherence.mjs @@ -225,10 +225,14 @@ function gitLines(args) { // exists — is skipped by the same rule that catches a merged one. const branch = process.env.GITHUB_REF_NAME ?? (gitLines(['rev-parse', '--abbrev-ref', 'HEAD']) ?? [null])[0]; +// Set only by `finish-release.mjs` around its own internal `qa:pre-release` +// spawn — the one caller genuinely seconds from creating this exact tag, not +// CI, not a PR check, not a plain manual run. See published-version.mjs. const publishedTag = checkMainDeclaresPublishedVersion({ branch, pkgVersion, tags: gitLines(['tag', '--list', 'v*']) ?? [], + aboutToTagThisVersion: process.env.MEMESH_FINISH_RELEASE_TAGGING === '1', }); findings.push(`main-declares-published-version: ${publishedTag.status} — ${publishedTag.message}`); if (publishedTag.status === 'error') { diff --git a/scripts/finish-release.mjs b/scripts/finish-release.mjs index e2643691..36a72a4d 100644 --- a/scripts/finish-release.mjs +++ b/scripts/finish-release.mjs @@ -180,10 +180,17 @@ const repoSlug = capture('gh', ['repo', 'view', '--json', 'nameWithOwner', '-q', // — a receipt can go stale the moment the next commit lands, and re-running a // few minutes of build+test is cheaper than trusting a stale one. console.log(`\n--- npm run qa:pre-release (build + verify:artifact + audit:memory; several minutes)`); +// MEMESH_FINISH_RELEASE_TAGGING=1 tells check-version-coherence.mjs's +// main-declares-published-version check that THIS run is the one about to +// create v seconds from now — the one caller for whom "main +// declares a version with no tag yet" is expected, not the five-day gap the +// check exists to catch. Set only here, at this one spawn; unset for CI, PR +// checks, and every other way qa:pre-release runs. See published-version.mjs. const qaPreReleaseResult = spawnSync('npm', ['run', 'qa:pre-release'], { cwd: repoRoot, stdio: 'inherit', shell: process.platform === 'win32', + env: { ...process.env, MEMESH_FINISH_RELEASE_TAGGING: '1' }, }); const qaPreReleaseStatus = qaPreReleaseResult.status; diff --git a/scripts/lib/published-version.mjs b/scripts/lib/published-version.mjs index 17528b82..94fd2769 100644 --- a/scripts/lib/published-version.mjs +++ b/scripts/lib/published-version.mjs @@ -25,13 +25,28 @@ // nobody break-tested is the defect class this repository keeps finding. /** + * `finish-release.mjs` runs `npm run qa:pre-release` as its own precondition, + * and that spawns this exact check, on `main`, before the tag it is about to + * create exists — the one moment this check cannot help but call an error. + * `aboutToTagThisVersion` is how the caller who is actually about to create + * `v` says so. It narrows to exactly the branch that condition + * can affect: tags ARE visible and simply do not include this one yet. It + * does nothing to the "no tags visible at all" branch above — a shallow + * checkout with no tag data is still a real failure, about-to-tag or not — + * and it does nothing to any of `check-version-coherence.mjs`'s other nine + * anchors, which this function never sees. + * * @param {object} input * @param {string|null} input.branch Current branch, or null if undiscoverable. * @param {string} input.pkgVersion `package.json` version. * @param {string[]} input.tags Every `v*` tag visible to the checkout. + * @param {boolean} [input.aboutToTagThisVersion] Set only by the caller that is + * itself seconds away from creating `v` (`finish-release.mjs`, + * via `MEMESH_FINISH_RELEASE_TAGGING=1`) — never by CI, a PR check, or a + * plain manual run. * @returns {{status: 'ok'|'skipped'|'error', message: string}} */ -export function checkMainDeclaresPublishedVersion({ branch, pkgVersion, tags }) { +export function checkMainDeclaresPublishedVersion({ branch, pkgVersion, tags, aboutToTagThisVersion = false }) { if (branch !== 'main') { return { status: 'skipped', @@ -59,6 +74,16 @@ export function checkMainDeclaresPublishedVersion({ branch, pkgVersion, tags }) return { status: 'ok', message: `v${pkgVersion} is tagged` }; } + if (aboutToTagThisVersion) { + return { + status: 'skipped', + message: + `main declares ${pkgVersion} with no \`v${pkgVersion}\` tag yet, but the ` + + 'caller running this check is finish-release.mjs itself, seconds from ' + + 'creating that exact tag — not a stale bump.', + }; + } + return { status: 'error', message: diff --git a/tests/main-declares-published-version.test.ts b/tests/main-declares-published-version.test.ts index dfda7722..3fa1801b 100644 --- a/tests/main-declares-published-version.test.ts +++ b/tests/main-declares-published-version.test.ts @@ -82,4 +82,49 @@ describe('main must declare a published version', () => { }); expect(r.status).toBe('skipped'); }); + + /** + * `finish-release.mjs` runs `qa:pre-release` as its own precondition, on + * `main`, before the tag it is about to create exists — the one moment + * this check cannot help but call an error against its own caller. + * `aboutToTagThisVersion` is how that one caller says so. + */ + describe('aboutToTagThisVersion (set only by finish-release.mjs)', () => { + it('skips instead of erroring when other tags are visible but not this one', () => { + const r = checkMainDeclaresPublishedVersion({ + branch: 'main', + pkgVersion: '4.2.12', + tags: ['v4.2.10', 'v4.2.11'], + aboutToTagThisVersion: true, + }); + expect(r.status).toBe('skipped'); + expect(r.message).toContain('4.2.12'); + }); + + it('still passes outright when the tag already exists', () => { + const r = checkMainDeclaresPublishedVersion({ + branch: 'main', + pkgVersion: '4.2.11', + tags: ['v4.2.10', 'v4.2.11'], + aboutToTagThisVersion: true, + }); + expect(r.status).toBe('ok'); + }); + + // Break-test: a shallow checkout with no tag data at all is still a real + // failure whether or not the caller is about to tag — the flag narrows to + // "this one tag is missing", not "trust me". If this ever went green, the + // flag would also paper over the exact defect `actions/checkout` fetching + // no tags reintroduces. + it('does NOT rescue a checkout where no tags are visible at all', () => { + const r = checkMainDeclaresPublishedVersion({ + branch: 'main', + pkgVersion: '4.2.11', + tags: [], + aboutToTagThisVersion: true, + }); + expect(r.status).toBe('error'); + expect(r.message).toContain('fetch-tags'); + }); + }); });