From c3deff0bd727d23c9daff913d2fb9fec85d47d90 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:24:31 +0200 Subject: [PATCH 1/2] patch release --- ci/src/release/version.test.ts | 15 +++++++++++++++ ci/src/release/version.ts | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index 75d2e81..07ea743 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -252,6 +252,21 @@ describe('computeNextRcTag', () => { expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.0-rc.1') }) + it('starts patch RC from stable+1 when stable exists but no patch RCs yet', () => { + const tags = ['v6.1.0'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.1') + }) + + it('starts patch RC from highest stable+1 when multiple stables exist with no patch RCs', () => { + const tags = ['v6.1.0', 'v6.1.1'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.2-rc.1') + }) + + it('increments patch RC even when a stable also exists on the branch', () => { + const tags = ['v6.1.0', 'v6.1.1-rc.2', 'v6.1.1-rc.1'] + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.3') + }) + it('handles patch-level RC after a stable has been cut', () => { const tags = ['v6.1.0', 'v6.1.1-rc.1'] expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.2') diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index 251bd4a..455a822 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -97,8 +97,18 @@ export function computeNextRcTag(branchTags: string[], branchName: string): stri return m === major && n === minor }) .sort((a, b) => semver.rcompare(a, b)) - if (seriesRcs.length === 0) return `v${major}.${minor}.0-rc.1` - return `v${incrementRc(stripV(seriesRcs[0]))}` + if (seriesRcs.length > 0) return `v${incrementRc(stripV(seriesRcs[0]))}` + + const seriesStables = filterSemver(branchTags) + .filter((t) => !t.includes('-rc.')) + .filter((t) => { + const [m, n] = stripV(t).split('.') + return m === major && n === minor + }) + .sort((a, b) => semver.rcompare(a, b)) + if (seriesStables.length > 0) return `v${nextPatchRc(stripV(seriesStables[0]))}` + + return `v${major}.${minor}.0-rc.1` } export function computeDevVersion(highestTag: string, shortSha: string): string { From 1e8f53551d82f6c8b64d263397b41a8e0eebc545 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:12:46 +0200 Subject: [PATCH 2/2] patch release --- ci/src/release/compute-tag.test.ts | 4 ++++ ci/src/release/version.test.ts | 15 ++++++++++----- ci/src/release/version.ts | 24 ++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/ci/src/release/compute-tag.test.ts b/ci/src/release/compute-tag.test.ts index f6806c7..30e937b 100644 --- a/ci/src/release/compute-tag.test.ts +++ b/ci/src/release/compute-tag.test.ts @@ -13,6 +13,10 @@ describe('computeTag', () => { expect(computeTag(['v6.1.0-rc.3', 'v6.1.0-rc.2'], 'releases/v6.1', true)).toBe('v6.1.0') }) + it('starts next patch prerelease after stable promotion', () => { + expect(computeTag(['v0.2.0-rc.3', 'v0.2.0'], 'releases/v0.2', false)).toBe('v0.2.1-rc.0') + }) + it('throws when promoting to stable with no RC tags', () => { expect(() => computeTag([], 'releases/v6.1', true)).toThrow() }) diff --git a/ci/src/release/version.test.ts b/ci/src/release/version.test.ts index 07ea743..f4a026f 100644 --- a/ci/src/release/version.test.ts +++ b/ci/src/release/version.test.ts @@ -109,9 +109,9 @@ describe('promoteToStable', () => { describe('nextPatchRc', () => { it.each([ - ['1.4.0', '1.4.1-rc.1'], - ['1.4.5', '1.4.6-rc.1'], - ['6.0.0', '6.0.1-rc.1'], + ['1.4.0', '1.4.1-rc.0'], + ['1.4.5', '1.4.6-rc.0'], + ['6.0.0', '6.0.1-rc.0'], ])('nextPatchRc(%s) → %s', (version, expected) => { expect(nextPatchRc(version)).toBe(expected) }) @@ -254,12 +254,12 @@ describe('computeNextRcTag', () => { it('starts patch RC from stable+1 when stable exists but no patch RCs yet', () => { const tags = ['v6.1.0'] - expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.1') + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.0') }) it('starts patch RC from highest stable+1 when multiple stables exist with no patch RCs', () => { const tags = ['v6.1.0', 'v6.1.1'] - expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.2-rc.1') + expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.2-rc.0') }) it('increments patch RC even when a stable also exists on the branch', () => { @@ -272,6 +272,11 @@ describe('computeNextRcTag', () => { expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.1-rc.2') }) + it('starts next patch prerelease after promoting a patch to stable', () => { + const tags = ['v0.2.0-rc.3', 'v0.2.0'] + expect(computeNextRcTag(tags, 'releases/v0.2')).toBe('v0.2.1-rc.0') + }) + it('ignores RC tags from other series on the same branch', () => { const tags = ['v6.1.0-rc.3', 'v6.0.0-rc.8'] expect(computeNextRcTag(tags, 'releases/v6.1')).toBe('v6.1.0-rc.4') diff --git a/ci/src/release/version.ts b/ci/src/release/version.ts index 455a822..4e8d62c 100644 --- a/ci/src/release/version.ts +++ b/ci/src/release/version.ts @@ -35,7 +35,7 @@ export function promoteToStable(version: string): string { export function nextPatchRc(version: string): string { const [major, minor, patch] = version.split('.').map(Number) - return `${major}.${minor}.${patch + 1}-rc.1` + return `${major}.${minor}.${patch + 1}-rc.0` } export function nextMainVersion(version: string): string { @@ -97,7 +97,6 @@ export function computeNextRcTag(branchTags: string[], branchName: string): stri return m === major && n === minor }) .sort((a, b) => semver.rcompare(a, b)) - if (seriesRcs.length > 0) return `v${incrementRc(stripV(seriesRcs[0]))}` const seriesStables = filterSemver(branchTags) .filter((t) => !t.includes('-rc.')) @@ -106,6 +105,27 @@ export function computeNextRcTag(branchTags: string[], branchName: string): stri return m === major && n === minor }) .sort((a, b) => semver.rcompare(a, b)) + + if (seriesRcs.length > 0) { + if (seriesStables.length === 0) return `v${incrementRc(stripV(seriesRcs[0]))}` + + const rcTop = semver.parse(seriesRcs[0]) + const stableTop = semver.parse(seriesStables[0]) + + // If the top RC is for a patch that is already stable (or older), + // begin a new patch prerelease line from the highest stable tag. + if ( + rcTop && stableTop && + (rcTop.major < stableTop.major || + (rcTop.major === stableTop.major && rcTop.minor < stableTop.minor) || + (rcTop.major === stableTop.major && rcTop.minor === stableTop.minor && rcTop.patch <= stableTop.patch)) + ) { + return `v${nextPatchRc(stripV(seriesStables[0]))}` + } + + return `v${incrementRc(stripV(seriesRcs[0]))}` + } + if (seriesStables.length > 0) return `v${nextPatchRc(stripV(seriesStables[0]))}` return `v${major}.${minor}.0-rc.1`