diff --git a/src/commands/githubActions/bumpUpstream/github/fetchGithubUpstreamVersion.ts b/src/commands/githubActions/bumpUpstream/github/fetchGithubUpstreamVersion.ts index 12217196..dee7324c 100644 --- a/src/commands/githubActions/bumpUpstream/github/fetchGithubUpstreamVersion.ts +++ b/src/commands/githubActions/bumpUpstream/github/fetchGithubUpstreamVersion.ts @@ -1,14 +1,14 @@ import { Github } from "../../../../providers/github/Github.js"; -import { isValidRelease } from "./isValidRelease.js"; +import { pickLatestRelease } from "./pickLatestRelease.js"; export async function fetchGithubUpstreamVersion( repo: string ): Promise { try { const newVersion = await fetchGithubLatestTag(repo); - if (!isValidRelease(newVersion)) { + if (!newVersion) { console.log( - `This is not a valid release (probably a release candidate) - ${repo}: ${newVersion}` + `No stable release found (only drafts, pre-releases or release candidates) - ${repo}` ); return null; } @@ -21,13 +21,12 @@ export async function fetchGithubUpstreamVersion( } } -async function fetchGithubLatestTag(repo: string): Promise { +async function fetchGithubLatestTag(repo: string): Promise { const [owner, repoName] = repo.split("/"); const githubRepo = new Github({ owner, repo: repoName }); const releases = await githubRepo.listReleases(); - const latestRelease = releases[0]; - if (!latestRelease) throw Error(`No release found for ${repo}`); + if (releases.length === 0) throw Error(`No release found for ${repo}`); - return latestRelease.tag_name; + return pickLatestRelease(releases); } diff --git a/src/commands/githubActions/bumpUpstream/github/index.ts b/src/commands/githubActions/bumpUpstream/github/index.ts index 58c03364..1cc496fb 100644 --- a/src/commands/githubActions/bumpUpstream/github/index.ts +++ b/src/commands/githubActions/bumpUpstream/github/index.ts @@ -3,3 +3,4 @@ export * from "./getBumpPrBody.js"; export * from "./getGithubSettings.js"; export * from "./isBranchNew.js"; export * from "./isValidRelease.js"; +export * from "./pickLatestRelease.js"; diff --git a/src/commands/githubActions/bumpUpstream/github/pickLatestRelease.ts b/src/commands/githubActions/bumpUpstream/github/pickLatestRelease.ts new file mode 100644 index 00000000..42ab0053 --- /dev/null +++ b/src/commands/githubActions/bumpUpstream/github/pickLatestRelease.ts @@ -0,0 +1,21 @@ +import { isValidRelease } from "./isValidRelease.js"; + +interface Release { + tag_name: string; + draft: boolean; + prerelease: boolean; +} + +/** + * Returns the tag of the newest release that is not a draft, not flagged as a + * pre-release and passes isValidRelease, or null when there is none. + * `releases` comes newest first, as GitHub lists them. A release candidate + * published after a stable release must not hide that stable release. + */ +export function pickLatestRelease(releases: Release[]): string | null { + for (const { tag_name, draft, prerelease } of releases) { + if (draft || prerelease || !isValidRelease(tag_name)) continue; + return tag_name; + } + return null; +} diff --git a/test/commands/gaBumpUpstream/pickLatestRelease.test.ts b/test/commands/gaBumpUpstream/pickLatestRelease.test.ts new file mode 100644 index 00000000..47e2998e --- /dev/null +++ b/test/commands/gaBumpUpstream/pickLatestRelease.test.ts @@ -0,0 +1,49 @@ +import { expect } from "chai"; +import { pickLatestRelease } from "../../../src/commands/githubActions/bumpUpstream/github/index.js"; + +const release = ( + tag_name: string, + { draft = false, prerelease = false } = {} +) => ({ tag_name, draft, prerelease }); + +describe("command / gaBumpUpstream / pickLatestRelease", () => { + it("returns the newest release when it is stable", () => { + expect(pickLatestRelease([release("v1.2.0"), release("v1.1.0")])).to.equal( + "v1.2.0" + ); + }); + + it("skips a newer release flagged as pre-release", () => { + expect( + pickLatestRelease([ + release("0.3.5-rc.1", { prerelease: true }), + release("0.3.4") + ]) + ).to.equal("0.3.4"); + }); + + it("skips a newer release candidate tag that is not flagged", () => { + expect( + pickLatestRelease([release("v1.2.0-rc.1"), release("v1.1.0")]) + ).to.equal("v1.1.0"); + }); + + it("skips drafts and nightly builds", () => { + expect( + pickLatestRelease([ + release("v2.0.0", { draft: true }), + release("nightly-2026-11-10"), + release("v1.9.0") + ]) + ).to.equal("v1.9.0"); + }); + + it("returns null when no release is stable", () => { + expect( + pickLatestRelease([ + release("v2.0.0-rc.2", { prerelease: true }), + release("v2.0.0-rc.1") + ]) + ).to.equal(null); + }); +});