diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3037b2f0e9..02f16b4fd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -168,7 +168,11 @@ jobs: echo "Cross-platform CI passed for ${GITHUB_SHA}: ${ci_url}" - # Notes / service baseline: + # Service baseline (lineage-relative): merged tags only, so the + # changed-files gate compares against the last release actually + # reachable from this commit. The release-notes baseline below uses the + # full tag set instead, so a stable on another lineage can anchor the + # changelog range. # - Preview: newest prior release of either channel (stable or preview). A # preview→preview-only baseline skips a shipped stable and restates it. # - Stable: newest prior stable only (matching preview carry adjusts the @@ -310,8 +314,14 @@ jobs: # Channel previous tag for Full Changelog + default notes baseline. # Preview baselines any prior release; stable baselines prior stable only. + # Read the FULL tag set: stable tags live on main's lineage, which the + # preview branch does not carry, and a trailing same-core preview + # (vX.Y.Z-preview.* shipped after vX.Y.Z) must not hide the stable from + # the compare range. The helper's semver ordering already ranks the + # stable above its own trailing preview, so the full tag list yields + # v2.9.1 → v2.10.0-preview instead of v2.9.1-preview → v2.10.0-preview. previous_tag="$( - git tag --merged HEAD --list 'v[0-9]*' | + git tag --list 'v[0-9]*' | bun scripts/release-notes.ts previous-release-tag "$RELEASE_VERSION" )" npm_metadata="Published to npm as \`@bitkyc08/opencodex@${RELEASE_VERSION}\` with dist-tag \`${NPM_DIST_TAG}\`." diff --git a/scripts/release-notes.ts b/scripts/release-notes.ts index 24d24a3d5f..98aa308731 100644 --- a/scripts/release-notes.ts +++ b/scripts/release-notes.ts @@ -109,6 +109,14 @@ export function matchingPreviewTags(version: string, tags: string[]): string[] { * that stable's changelog (e.g. 2.7.41-preview → 2.7.43-preview after 2.7.42). * - Stable releases: newest prior stable only. Matching preview carry adjusts the * notes range start separately when assembling latest notes. + * + * Callers must pass the FULL repo tag set, not `git tag --merged HEAD`. Stable + * tags live on main's lineage, which the preview branch does not carry, and a + * trailing same-core preview (vX.Y.Z-preview.* shipped after vX.Y.Z) must not + * hide the stable: for `2.10.0-preview.*` after `v2.9.1` + `v2.9.1-preview.*`, + * the baseline must be `v2.9.1`, not the trailing preview. Semver ordering + * already ranks the stable above its own trailing preview, so the full set is + * sufficient; restricting to merged tags is what reintroduces the bug. */ export function previousReleaseNotesTag(version: string, tags: string[]): string | null { if (!version) return null; diff --git a/tests/ci-workflows.test.ts b/tests/ci-workflows.test.ts index 2ca6f48f88..fe56a09ddd 100644 --- a/tests/ci-workflows.test.ts +++ b/tests/ci-workflows.test.ts @@ -629,6 +629,18 @@ describe("GitHub Actions hardening", () => { expect(createStep.indexOf("gh api")).toBeGreaterThan(-1); expect(createStep.indexOf('git tag "$release_tag"')).toBeGreaterThan(-1); expect(createStep.indexOf("gh api")).toBeLessThan(createStep.indexOf('git tag "$release_tag"')); + // The notes baseline must read the FULL tag set, not `--merged HEAD`: stable + // tags live on main's lineage, which the preview branch does not carry, and a + // trailing same-core preview must not hide the stable from the range + // (v2.9.1-preview → v2.10.0-preview is wrong; the range must start at v2.9.1). + expect(createStep).toContain("git tag --list 'v[0-9]*' |"); + expect(createStep).not.toContain("--merged HEAD"); + // The merged-only restriction remains on the service gate, whose + // changed-files comparison is deliberately lineage-relative. + const ciGateStep = workflow + .split("- name: Require successful Cross-platform CI for this commit")[1]! + .split(/\n {6}- name:/)[0]!; + expect(ciGateStep).toContain("--merged HEAD"); // First-channel releases must not call generate-notes without an explicit baseline // (GitHub would otherwise pick the newest repo tag, possibly from the other channel). // Scope to the single if-block that owns generate-notes; createStep has two diff --git a/tests/release-notes.test.ts b/tests/release-notes.test.ts index ed853635e9..e9a0562c82 100644 --- a/tests/release-notes.test.ts +++ b/tests/release-notes.test.ts @@ -105,6 +105,19 @@ describe("previousReleaseNotesTag", () => { "v2.7.42", ])).toBe("v2.7.42"); }); + + test("a trailing same-core preview does not hide the stable (2.9.1 → 2.10.0-preview)", () => { + // v2.9.1-preview.20260802 shipped after the v2.9.1 stable on another lineage; + // the next preview train must still baseline the stable, not the trailing + // preview. This is the workflow's `git tag --list` (full set) contract: the + // same input restricted to `--merged HEAD` would drop v2.9.1 and wrongly + // return v2.9.1-preview.20260802. + expect(previousReleaseNotesTag("2.10.0-preview.20260802", [ + "v2.9.1-preview.20260802", + "v2.9.1", + "v2.10.0-preview.20260802", + ])).toBe("v2.9.1"); + }); }); describe("stripCarriedReleaseNotes", () => {