88 * bun scripts/release-notes.ts strip-carried <body-file>
99 * bun scripts/release-notes.ts matching-preview-tag <version>
1010 * bun scripts/release-notes.ts matching-preview-tags <version>
11+ * bun scripts/release-notes.ts previous-release-tag <version>
1112 * bun scripts/release-notes.ts has-meaningful [body-file]
1213 * bun scripts/release-notes.ts assemble --npm-metadata ... --out ...
1314 */
1415
16+ type ParsedReleaseTag = {
17+ major : number ;
18+ minor : number ;
19+ patch : number ;
20+ /** null = stable release; otherwise the SemVer prerelease identifier string. */
21+ prerelease : string | null ;
22+ } ;
23+
24+ function parseReleaseTag ( tag : string ) : ParsedReleaseTag | null {
25+ const match = / ^ v ? ( \d + ) \. ( \d + ) \. ( \d + ) (?: - ( .+ ) ) ? $ / . exec ( tag . trim ( ) ) ;
26+ if ( ! match ) return null ;
27+ return {
28+ major : Number ( match [ 1 ] ) ,
29+ minor : Number ( match [ 2 ] ) ,
30+ patch : Number ( match [ 3 ] ) ,
31+ prerelease : match [ 4 ] ?? null ,
32+ } ;
33+ }
34+
35+ /** SemVer identifier compare: numeric parts by number; numeric < non-numeric. */
36+ function comparePrereleaseIds ( a : string , b : string ) : number {
37+ const aParts = a . split ( "." ) ;
38+ const bParts = b . split ( "." ) ;
39+ const len = Math . max ( aParts . length , bParts . length ) ;
40+ for ( let i = 0 ; i < len ; i += 1 ) {
41+ const ap = aParts [ i ] ;
42+ const bp = bParts [ i ] ;
43+ if ( ap === undefined ) return - 1 ;
44+ if ( bp === undefined ) return 1 ;
45+ const aNum = / ^ \d + $ / . test ( ap ) ;
46+ const bNum = / ^ \d + $ / . test ( bp ) ;
47+ if ( aNum && bNum ) {
48+ const diff = Number ( ap ) - Number ( bp ) ;
49+ if ( diff !== 0 ) return diff ;
50+ continue ;
51+ }
52+ if ( aNum !== bNum ) return aNum ? - 1 : 1 ;
53+ const cmp = ap . localeCompare ( bp ) ;
54+ if ( cmp !== 0 ) return cmp ;
55+ }
56+ return 0 ;
57+ }
58+
59+ /**
60+ * Ascending SemVer-aware tag compare. Stable ranks after prereleases with the
61+ * same core version (`v2.7.42-preview.*` < `v2.7.42`).
62+ */
63+ export function compareReleaseTags ( a : string , b : string ) : number {
64+ const pa = parseReleaseTag ( a ) ;
65+ const pb = parseReleaseTag ( b ) ;
66+ if ( ! pa || ! pb ) {
67+ return a . localeCompare ( b , undefined , { numeric : true , sensitivity : "base" } ) ;
68+ }
69+ if ( pa . major !== pb . major ) return pa . major - pb . major ;
70+ if ( pa . minor !== pb . minor ) return pa . minor - pb . minor ;
71+ if ( pa . patch !== pb . patch ) return pa . patch - pb . patch ;
72+ if ( pa . prerelease === null && pb . prerelease === null ) return 0 ;
73+ if ( pa . prerelease === null ) return 1 ;
74+ if ( pb . prerelease === null ) return - 1 ;
75+ return comparePrereleaseIds ( pa . prerelease , pb . prerelease ) ;
76+ }
77+
78+ function sortVersionTagsAscending ( tags : string [ ] ) : string [ ] {
79+ return [ ...tags ] . sort ( compareReleaseTags ) ;
80+ }
81+
1582/** Newest matching preview tag for a stable version, or null. */
1683export function matchingPreviewTag ( version : string , tags : string [ ] ) : string | null {
1784 const matches = matchingPreviewTags ( version , tags ) ;
@@ -29,8 +96,29 @@ export function matchingPreviewTags(version: string, tags: string[]): string[] {
2996 const matches = tags
3097 . map ( tag => tag . trim ( ) )
3198 . filter ( tag => tag . startsWith ( prefix ) ) ;
32- matches . sort ( ( a , b ) => a . localeCompare ( b , undefined , { numeric : true , sensitivity : "base" } ) ) ;
33- return matches ;
99+ return sortVersionTagsAscending ( matches ) ;
100+ }
101+
102+ /**
103+ * Previous release tag used as the generate-notes / changelog baseline.
104+ *
105+ * - Preview releases: newest prior tag of either channel (stable or preview).
106+ * Channel-isolated preview→preview baselines skip a shipped stable and restate
107+ * that stable's changelog (e.g. 2.7.41-preview → 2.7.43-preview after 2.7.42).
108+ * - Stable releases: newest prior stable only. Matching preview carry adjusts the
109+ * notes range start separately when assembling latest notes.
110+ */
111+ export function previousReleaseNotesTag ( version : string , tags : string [ ] ) : string | null {
112+ if ( ! version ) return null ;
113+ const releaseTag = version . startsWith ( "v" ) ? version : `v${ version } ` ;
114+ const candidates = tags
115+ . map ( tag => tag . trim ( ) )
116+ . filter ( tag => / ^ v \d / . test ( tag ) && compareReleaseTags ( tag , releaseTag ) < 0 ) ;
117+ const filtered = version . includes ( "-preview." )
118+ ? candidates
119+ : candidates . filter ( tag => ! tag . includes ( "-preview." ) ) ;
120+ const sorted = sortVersionTagsAscending ( filtered ) ;
121+ return sorted . length === 0 ? null : sorted [ sorted . length - 1 ] ! ;
34122}
35123
36124/** Drop npm blurb, Commits section, and Full Changelog link from a prior release body. */
@@ -212,7 +300,7 @@ async function main(argv: string[]): Promise<void> {
212300 return ;
213301 }
214302
215- if ( cmd === "matching-preview-tag" || cmd === "matching-preview-tags" ) {
303+ if ( cmd === "matching-preview-tag" || cmd === "matching-preview-tags" || cmd === "previous-release-tag" ) {
216304 const version = rest [ 0 ] ;
217305 if ( ! version ) {
218306 console . error ( `Usage: bun scripts/release-notes.ts ${ cmd } <version>` ) ;
@@ -225,6 +313,11 @@ async function main(argv: string[]): Promise<void> {
225313 if ( tag ) process . stdout . write ( tag + "\n" ) ;
226314 return ;
227315 }
316+ if ( cmd === "previous-release-tag" ) {
317+ const tag = previousReleaseNotesTag ( version , tags ) ;
318+ if ( tag ) process . stdout . write ( tag + "\n" ) ;
319+ return ;
320+ }
228321 for ( const tag of matchingPreviewTags ( version , tags ) ) {
229322 process . stdout . write ( tag + "\n" ) ;
230323 }
@@ -278,6 +371,7 @@ Usage:
278371 bun scripts/release-notes.ts join-carried --out <file> <part-file>...
279372 bun scripts/release-notes.ts matching-preview-tag <version> # tags on stdin
280373 bun scripts/release-notes.ts matching-preview-tags <version> # tags on stdin, oldest→newest
374+ bun scripts/release-notes.ts previous-release-tag <version> # tags on stdin
281375 bun scripts/release-notes.ts assemble --npm-metadata ... --out ...` ) ;
282376 process . exit ( 1 ) ;
283377}
0 commit comments