11#!/usr/bin/env node
22import { execFile as execFileCallback } from "node:child_process" ;
3- import { mkdir , readFile , readdir , writeFile } from "node:fs/promises" ;
3+ import { mkdir , readFile , readdir , unlink , writeFile } from "node:fs/promises" ;
44import { join } from "node:path" ;
55import { promisify } from "node:util" ;
66
@@ -113,7 +113,10 @@ export async function inspectAlphaReleaseTrain(options = {}) {
113113 const tagName = `v${ version } ` ;
114114 const image = `${ SERVER_IMAGE_NAME } :${ version } ` ;
115115 const title = `ZITADEL Alpha ${ version } ` ;
116- const activeChangesets = await activeChangesetFiles ( cwd , readdirFn ) ;
116+ const activeChangesets = await activeChangesetFiles ( cwd , {
117+ readFile : readFileFn ,
118+ readdir : readdirFn ,
119+ } ) ;
117120 const headCommit = await gitOutput ( execFileFn , cwd , [ "rev-parse" , "HEAD" ] ) ;
118121 const tagCommit = await tagCommitFor ( tagName , execFileFn , cwd ) ;
119122 const tagExists = Boolean ( tagCommit ) ;
@@ -246,6 +249,26 @@ export async function tagExists(tagName, execFileFn = execFile, cwd = process.cw
246249 return Boolean ( await tagCommitFor ( tagName , execFileFn , cwd ) ) ;
247250}
248251
252+ export async function pruneEmptyChangesets ( options = { } ) {
253+ const cwd = options . cwd ?? process . cwd ( ) ;
254+ const readFileFn = options . readFile ?? readFile ;
255+ const readdirFn = options . readdir ?? readdir ;
256+ const unlinkFn = options . unlink ?? unlink ;
257+
258+ const removed = [ ] ;
259+ const files = await changesetMarkdownFiles ( cwd , readdirFn ) ;
260+ for ( const file of files ) {
261+ const path = join ( cwd , ".changeset" , file ) ;
262+ const content = await readFileFn ( path , "utf8" ) ;
263+ if ( ! changesetHasReleaseBump ( content ) ) {
264+ await unlinkFn ( path ) ;
265+ removed . push ( file ) ;
266+ }
267+ }
268+
269+ return removed . sort ( ) ;
270+ }
271+
249272export function renderAlphaReleaseNotes ( { title, version, image, packages } ) {
250273 const lines = [
251274 `# ${ title } ` ,
@@ -286,8 +309,10 @@ export function renderAlphaReleaseNotes({ title, version, image, packages }) {
286309
287310export function parseAlphaReleaseArgs ( args ) {
288311 const command = args [ 0 ] ;
289- if ( command !== "prepare" && command !== "status" ) {
290- throw new Error ( "Usage: release-alpha-train.mjs <status|prepare> [--out-dir <path>] [--published <true|false>]" ) ;
312+ if ( command !== "prepare" && command !== "status" && command !== "prune-empty-changesets" ) {
313+ throw new Error (
314+ "Usage: release-alpha-train.mjs <status|prepare|prune-empty-changesets> [--out-dir <path>] [--published <true|false>]" ,
315+ ) ;
291316 }
292317 const values = { } ;
293318 for ( let index = 1 ; index < args . length ; index += 1 ) {
@@ -310,7 +335,25 @@ function camelCase(value) {
310335 return value . replace ( / - ( [ a - z ] ) / g, ( _ , letter ) => letter . toUpperCase ( ) ) ;
311336}
312337
313- async function activeChangesetFiles ( cwd , readdirFn = readdir ) {
338+ async function activeChangesetFiles ( cwd , options = { } ) {
339+ const readFileFn = options . readFile ?? readFile ;
340+ const readdirFn = options . readdir ?? readdir ;
341+ const prereleaseChangesets = await prereleaseTrackedChangesets ( cwd , readFileFn ) ;
342+ const files = await changesetMarkdownFiles ( cwd , readdirFn ) ;
343+ const active = [ ] ;
344+ for ( const file of files ) {
345+ if ( prereleaseChangesets . has ( changesetSlug ( file ) ) ) {
346+ continue ;
347+ }
348+ const content = await readFileFn ( join ( cwd , ".changeset" , file ) , "utf8" ) ;
349+ if ( changesetHasReleaseBump ( content ) ) {
350+ active . push ( file ) ;
351+ }
352+ }
353+ return active . sort ( ) ;
354+ }
355+
356+ async function changesetMarkdownFiles ( cwd , readdirFn = readdir ) {
314357 let entries = [ ] ;
315358 try {
316359 entries = await readdirFn ( join ( cwd , ".changeset" ) , { withFileTypes : true } ) ;
@@ -327,6 +370,51 @@ async function activeChangesetFiles(cwd, readdirFn = readdir) {
327370 . sort ( ) ;
328371}
329372
373+ async function prereleaseTrackedChangesets ( cwd , readFileFn = readFile ) {
374+ let preState ;
375+ try {
376+ preState = JSON . parse ( await readFileFn ( join ( cwd , ".changeset/pre.json" ) , "utf8" ) ) ;
377+ } catch ( error ) {
378+ if ( isMissingPath ( error ) ) {
379+ return new Set ( ) ;
380+ }
381+ throw error ;
382+ }
383+ if ( ! Array . isArray ( preState . changesets ) ) {
384+ return new Set ( ) ;
385+ }
386+ return new Set ( preState . changesets . filter ( ( name ) => typeof name === "string" ) ) ;
387+ }
388+
389+ function changesetSlug ( file ) {
390+ return file . replace ( / \. m d $ / , "" ) ;
391+ }
392+
393+ function changesetHasReleaseBump ( content ) {
394+ const frontmatter = changesetFrontmatter ( content ) ;
395+ if ( frontmatter === undefined ) {
396+ return true ;
397+ }
398+ return frontmatter . split ( "\n" ) . some ( ( line ) => {
399+ const trimmed = line . trim ( ) ;
400+ return trimmed . length > 0 && ! trimmed . startsWith ( "#" ) ;
401+ } ) ;
402+ }
403+
404+ function changesetFrontmatter ( content ) {
405+ const normalized = content . replace ( / \r \n / g, "\n" ) ;
406+ if ( ! normalized . startsWith ( "---\n" ) ) {
407+ return undefined ;
408+ }
409+ const lines = normalized . split ( "\n" ) ;
410+ for ( let index = 1 ; index < lines . length ; index += 1 ) {
411+ if ( lines [ index ] === "---" ) {
412+ return lines . slice ( 1 , index ) . join ( "\n" ) ;
413+ }
414+ }
415+ return undefined ;
416+ }
417+
330418async function tagCommitFor ( tagName , execFileFn = execFile , cwd = process . cwd ( ) ) {
331419 try {
332420 return await gitOutput ( execFileFn , cwd , [ "rev-list" , "-n" , "1" , tagName ] ) ;
@@ -400,18 +488,26 @@ function printAlphaOutputs(result) {
400488 }
401489}
402490
491+ function printPrunedChangesets ( files ) {
492+ console . log ( `pruned_empty_changesets=${ files . join ( "," ) } ` ) ;
493+ }
494+
403495function isDirectRun ( url ) {
404496 return process . argv [ 1 ] && url === new URL ( `file://${ process . argv [ 1 ] } ` ) . href ;
405497}
406498
407499if ( isDirectRun ( import . meta. url ) ) {
408500 try {
409501 const { command, values } = parseAlphaReleaseArgs ( process . argv . slice ( 2 ) ) ;
410- const result =
411- command === "status"
412- ? await inspectAlphaReleaseTrain ( values )
413- : await prepareAlphaReleaseTrain ( values ) ;
414- printAlphaOutputs ( result ) ;
502+ if ( command === "prune-empty-changesets" ) {
503+ printPrunedChangesets ( await pruneEmptyChangesets ( values ) ) ;
504+ } else {
505+ const result =
506+ command === "status"
507+ ? await inspectAlphaReleaseTrain ( values )
508+ : await prepareAlphaReleaseTrain ( values ) ;
509+ printAlphaOutputs ( result ) ;
510+ }
415511 } catch ( error ) {
416512 console . error ( error instanceof Error ? error . message : String ( error ) ) ;
417513 process . exit ( 1 ) ;
0 commit comments