diff --git a/.github/scripts/release-notes.ts b/.github/scripts/release-notes.ts index d7c337e8..9c585848 100644 --- a/.github/scripts/release-notes.ts +++ b/.github/scripts/release-notes.ts @@ -55,8 +55,12 @@ if (!repoUrl) { console.error('CHANGELOG.md needs a "[Unreleased]: /compare/..." link ref at the bottom') process.exit(1) } -// The previous release is the next version heading down (the changelog is newest first). -const previous = /^## \[([^\]]+)\]/.exec(lines[end] ?? "")?.[1] +// The previous release is the next version heading down that has a tag (the changelog is newest first). +// A heading for a version that never shipped (its CI or Publish failed) has no tag, so it is skipped. +const previous = lines + .slice(end) + .map((l) => /^## \[([^\]]+)\]/.exec(l)?.[1]) + .find((v): v is string => v !== undefined && v !== "Unreleased" && refExists(`v${v}`)) // Before the tag exists (a dry run from main) the commits are the ones on HEAD. const tag = `v${version}` diff --git a/.github/scripts/release.ts b/.github/scripts/release.ts index c2301c3c..e0673267 100644 --- a/.github/scripts/release.ts +++ b/.github/scripts/release.ts @@ -37,7 +37,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync import { tmpdir } from "node:os" import { join } from "node:path" import semver from "semver" -import { changelogBlock, collectCommits } from "./commit-detail.ts" +import { changelogBlock, collectCommits, refExists } from "./commit-detail.ts" import { approvalState, judgeChecks, mergeRules, type CheckRow, type GhReview } from "./merge-gate.ts" import { bumpInstallPins, replaceSinceNext } from "./since-next.ts" @@ -516,7 +516,17 @@ const docRewrites = ["README.md", ...[...new Bun.Glob("docs/**/*.md").scanSync(" const linkAt = lines.findIndex((l) => l.startsWith("[Unreleased]: ")) const link = /^\[Unreleased\]: (.+)\/compare\/v(.+)\.\.\.HEAD$/.exec(lines[linkAt] ?? "") if (!link) fail('CHANGELOG.md needs a "[Unreleased]: /compare/v...HEAD" link ref at the bottom') -const [, repoUrl, previous] = link +const [, repoUrl, linkedPrevious] = link +// The link names the last version the changelog saw, but an unpublished version (a release whose CI or +// Publish failed) leaves a heading and link for a tag that does not exist, and the next release then fails +// on `git log v..HEAD`. So the previous version is the newest tag that exists below this release. +const previous = refExists(`v${linkedPrevious}`) + ? linkedPrevious + : git("tag", "--list", "v*", "--sort=-v:refname") + .split("\n") + .map((t) => t.slice(1)) + .find((v) => semver.valid(v) && semver.lt(v, version)) +if (!previous) fail(`no release tag below ${version} exists, and CHANGELOG.md links v${linkedPrevious}, which is not a tag`) if (!semver.gt(version, previous)) fail(`${version} does not increase on the last released version ${previous}`) // The commit-level detail: every commit since the previous tag, grouped by type, in a