1- #!/usr/bin/env node
1+ #!/usr/bin/env zx
2+ import 'zx/globals' ;
3+
24/**
35 * Validate changesets in CI
46 *
57 * Checks:
68 * 1. Changesets are present (PRs require changesets)
79 * 2. No major version bumps (breaking changes disallowed)
810 * 3. Changeset status passes (validates format, config, dependencies)
9- *
10- * Usage: node .github/scripts/validate-changesets.mts
1111 */
1212
13- import { execSync } from 'child_process' ;
14- import { readFileSync , readdirSync } from 'fs' ;
15- import { join } from 'path' ;
16-
1713const CHANGESETS_DIR = '.changeset' ;
18- const RED = '\x1b[31m' ;
19- const GREEN = '\x1b[32m' ;
20- const YELLOW = '\x1b[33m' ;
21- const RESET = '\x1b[0m' ;
14+
15+ // ANSI color codes
16+ const colors = {
17+ red : ( msg : string ) => `\x1b[31m${ msg } \x1b[0m` ,
18+ green : ( msg : string ) => `\x1b[32m${ msg } \x1b[0m` ,
19+ yellow : ( msg : string ) => `\x1b[33m${ msg } \x1b[0m` ,
20+ } ;
21+
22+ // Logging helpers
23+ const log = {
24+ error : ( msg : string ) => echo ( colors . red ( msg ) ) ,
25+ success : ( msg : string ) => echo ( colors . green ( msg ) ) ,
26+ warn : ( msg : string ) => echo ( colors . yellow ( msg ) ) ,
27+ info : ( msg : string ) => echo ( msg ) ,
28+ } ;
2229
2330interface ChangesetFrontmatter {
2431 [ packageName : string ] : 'major' | 'minor' | 'patch' ;
2532}
2633
2734function parseChangesetForMajorCheck ( filePath : string ) : ChangesetFrontmatter | null {
2835 try {
29- const content = readFileSync ( filePath , 'utf-8' ) ;
30-
31- // Extract frontmatter between --- markers
36+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
3237 const frontmatterMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
33- if ( ! frontmatterMatch ) {
34- return null ;
35- }
38+ if ( ! frontmatterMatch ) return null ;
3639
3740 const frontmatter = frontmatterMatch [ 1 ] ;
38- const result : ChangesetFrontmatter = { } ;
41+ const result = { } ;
3942
40- // Parse YAML-like frontmatter
41- // Format: "@scope/package": minor
4243 const lines = frontmatter . split ( '\n' ) . filter ( line => line . trim ( ) ) ;
4344 for ( const line of lines ) {
4445 const match = line . match ( / ^ [ " ' ] ? ( [ ^ " ' : ] + ) [ " ' ] ? \s * : \s * ( m a j o r | m i n o r | p a t c h ) / ) ;
4546 if ( match ) {
4647 const [ , packageName , bumpType ] = match ;
47- result [ packageName . trim ( ) ] = bumpType as 'major' | 'minor' | 'patch' ;
48+ result [ packageName . trim ( ) ] = bumpType ;
4849 }
4950 }
5051
5152 return Object . keys ( result ) . length > 0 ? result : null ;
52- } catch ( error ) {
53- // Parsing errors will be caught by changeset status
53+ } catch {
5454 return null ;
5555 }
5656}
5757
58- function checkChangesetPresence ( ) : boolean {
59- console . log ( '\n🔍 Checking for changeset presence...\n' ) ;
58+ async function checkChangesetPresence ( ) {
59+ log . info ( '\n🔍 Checking for changeset presence...\n' ) ;
6060
6161 try {
62- const statusOutput = execSync ( 'yarn changeset status --since=origin/main 2>&1' , {
63- encoding : 'utf-8'
64- } ) ;
65-
66- if ( statusOutput . includes ( 'No changesets present' ) ) {
67- console . log ( `${ RED } ❌ No changesets found${ RESET } \n` ) ;
68- console . log ( `${ YELLOW } This PR requires a changeset to document the changes.${ RESET } ` ) ;
69- console . log ( `${ YELLOW } To create a changeset, run: yarn changeset${ RESET } \n` ) ;
62+ const { stdout } = await $ `yarn changeset status --since=origin/main 2>&1` ;
63+
64+ if ( stdout . includes ( 'No changesets present' ) ) {
65+ log . error ( '❌ No changesets found\n' ) ;
66+ log . warn ( 'This PR requires a changeset to document the changes.' ) ;
67+ log . warn ( 'To create a changeset, run: yarn changeset\n' ) ;
7068 return false ;
7169 }
7270
73- console . log ( ` ${ GREEN } ✅ Changesets found${ RESET } ` ) ;
71+ log . success ( ' ✅ Changesets found' ) ;
7472 return true ;
7573 } catch ( error : any ) {
76- console . log ( ` ${ RED } ❌ Failed to check changeset status${ RESET } \n` ) ;
77- console . log ( error . message ) ;
74+ log . error ( ' ❌ Failed to check changeset status\n' ) ;
75+ log . info ( error . message ) ;
7876 return false ;
7977 }
8078}
8179
82- function checkForMajorBumps ( ) : boolean {
83- console . log ( '\n🔍 Checking for major version bumps...\n' ) ;
80+ async function checkForMajorBumps ( ) {
81+ log . info ( '\n🔍 Checking for major version bumps...\n' ) ;
8482
85- const changesetFiles = readdirSync ( CHANGESETS_DIR )
83+ const files = fs . readdirSync ( CHANGESETS_DIR ) ;
84+ const changesetFiles = files
8685 . filter ( file => file . endsWith ( '.md' ) && file !== 'README.md' )
87- . map ( file => join ( CHANGESETS_DIR , file ) ) ;
86+ . map ( file => path . join ( CHANGESETS_DIR , file ) ) ;
8887
8988 if ( changesetFiles . length === 0 ) {
90- console . log ( ` ${ YELLOW } No changesets found (skipping major check)${ RESET } ` ) ;
89+ log . warn ( ' No changesets found (skipping major check)' ) ;
9190 return true ;
9291 }
9392
9493 let hasMajor = false ;
95- const majorBumps : Array < { file : string ; packages: string [ ] } > = [ ] ;
94+ const majorBumps = [ ] ;
9695
9796 for ( const file of changesetFiles ) {
9897 const frontmatter = parseChangesetForMajorCheck ( file ) ;
@@ -109,80 +108,69 @@ function checkForMajorBumps(): boolean {
109108 }
110109
111110 if ( hasMajor ) {
112- console . log ( ` ${ RED } ❌ Major version bumps detected!${ RESET } \n` ) ;
111+ log . error ( ' ❌ Major version bumps detected!\n' ) ;
113112 for ( const { file, packages } of majorBumps ) {
114- console . log ( ` ${ RED } ${ file } :${ RESET } `) ;
113+ log . error ( ` ${ file } :`) ;
115114 for ( const pkg of packages ) {
116- console . log ( ` ${ RED } - ${ pkg } : major${ RESET } `) ;
115+ log . error ( ` - ${ pkg } : major`) ;
117116 }
118117 }
119- console . log ( `\n ${ RED } Major version bumps are not allowed.${ RESET } ` ) ;
120- console . log ( ` ${ YELLOW } If you need to make a breaking change, please discuss with the team first.${ RESET } \n` ) ;
118+ log . error ( '\nMajor version bumps are not allowed.' ) ;
119+ log . warn ( ' If you need to make a breaking change, please discuss with the team first.\n' ) ;
121120 return false ;
122121 }
123122
124- console . log ( ` ${ GREEN } ✅ No major version bumps found${ RESET } ` ) ;
123+ log . success ( ' ✅ No major version bumps found' ) ;
125124 return true ;
126125}
127126
128- function validateChangesetStatus ( ) : boolean {
129- console . log ( '\n🔍 Validating changesets with changeset status...\n' ) ;
127+ async function validateChangesetStatus ( ) {
128+ log . info ( '\n🔍 Validating changesets with changeset status...\n' ) ;
130129
131130 try {
132- // This validates:
133- // - Changeset file format
134- // - Package references
135- // - Dependency graph
136- // - Config validity
137- const statusOutput = execSync ( 'yarn changeset status --since=origin/main 2>&1' , {
138- encoding : 'utf-8'
139- } ) ;
140-
141- // Check for errors (but "No changesets present" is not an error here)
142- if ( statusOutput . toLowerCase ( ) . includes ( 'error' ) && ! statusOutput . includes ( 'No changesets present' ) ) {
143- console . log ( `${ RED } ❌ Changeset validation failed!${ RESET } \n` ) ;
144- console . log ( statusOutput ) ;
131+ const { stdout } = await $ `yarn changeset status --since=origin/main 2>&1` ;
132+
133+ if ( stdout . toLowerCase ( ) . includes ( 'error' ) && ! stdout . includes ( 'No changesets present' ) ) {
134+ log . error ( '❌ Changeset validation failed!\n' ) ;
135+ log . info ( stdout ) ;
145136 return false ;
146137 }
147138
148- console . log ( ` ${ GREEN } ✅ Changeset validation passed${ RESET } ` ) ;
139+ log . success ( ' ✅ Changeset validation passed' ) ;
149140 return true ;
150141 } catch ( error : any ) {
151- console . log ( ` ${ RED } ❌ Changeset validation failed!${ RESET } \n` ) ;
152- console . log ( ` ${ RED } Error:${ RESET } ` , error . message ) ;
153- if ( error . stdout ) console . log ( error . stdout ) ;
154- if ( error . stderr ) console . log ( error . stderr ) ;
142+ log . error ( ' ❌ Changeset validation failed!\n' ) ;
143+ log . error ( ` Error: ${ error . message } ` ) ;
144+ if ( error . stdout ) log . info ( error . stdout ) ;
145+ if ( error . stderr ) log . info ( error . stderr ) ;
155146 return false ;
156147 }
157148}
158149
159- function main ( ) : void {
160- console . log ( `\n${ '=' . repeat ( 60 ) } ` ) ;
161- console . log ( 'Changesets Validation' ) ;
162- console . log ( `${ '=' . repeat ( 60 ) } ` ) ;
150+ // Main execution
151+ log . info ( `\n${ '=' . repeat ( 60 ) } ` ) ;
152+ log . info ( 'Changesets Validation' ) ;
153+ log . info ( `${ '=' . repeat ( 60 ) } ` ) ;
163154
164- const results = {
165- presence : checkChangesetPresence ( ) ,
166- majorBumps : checkForMajorBumps ( ) ,
167- validation : validateChangesetStatus ( )
168- } ;
155+ const results = {
156+ presence : await checkChangesetPresence ( ) ,
157+ majorBumps : await checkForMajorBumps ( ) ,
158+ validation : await validateChangesetStatus ( )
159+ } ;
169160
170- console . log ( `\n${ '=' . repeat ( 60 ) } ` ) ;
171- console . log ( 'Validation Results:' ) ;
172- console . log ( `${ '=' . repeat ( 60 ) } \n` ) ;
161+ log . info ( `\n${ '=' . repeat ( 60 ) } ` ) ;
162+ log . info ( 'Validation Results:' ) ;
163+ log . info ( `${ '=' . repeat ( 60 ) } \n` ) ;
173164
174- console . log ( `Changeset presence: ${ results . presence ? GREEN + '✅ PASS' : RED + '❌ FAIL' } ${ RESET } ` ) ;
175- console . log ( `Major version check: ${ results . majorBumps ? GREEN + '✅ PASS' : RED + '❌ FAIL' } ${ RESET } ` ) ;
176- console . log ( `Changeset validation: ${ results . validation ? GREEN + '✅ PASS' : RED + '❌ FAIL' } ${ RESET } \n` ) ;
165+ log . info ( `Changeset presence: ${ results . presence ? '✅ PASS' : '❌ FAIL' } ` ) ;
166+ log . info ( `Major version check: ${ results . majorBumps ? '✅ PASS' : '❌ FAIL' } ` ) ;
167+ log . info ( `Changeset validation: ${ results . validation ? '✅ PASS' : '❌ FAIL' } \n` ) ;
177168
178- const allPassed = results . presence && results . majorBumps && results . validation ;
179-
180- if ( ! allPassed ) {
181- console . log ( `${ RED } Validation failed!${ RESET } \n` ) ;
182- process . exit ( 1 ) ;
183- }
169+ const allPassed = results . presence && results . majorBumps && results . validation ;
184170
185- console . log ( `${ GREEN } All validations passed! ✅${ RESET } \n` ) ;
171+ if ( ! allPassed ) {
172+ log . error ( 'Validation failed!\n' ) ;
173+ throw new Error ( 'Validation failed' ) ;
186174}
187175
188- main ( ) ;
176+ log . success ( 'All validations passed! ✅\n' ) ;
0 commit comments