1- import { access , readFile , readdir } from 'node:fs/promises' ;
1+ import { access , readFile , readdir , writeFile } from 'node:fs/promises' ;
22
33import { program } from 'commander' ;
44import picocolors from 'picocolors' ;
@@ -100,8 +100,9 @@ type TaskKey = keyof typeof tasksMap;
100100const tasks = Object . keys ( tasksMap ) as TaskKey [ ] ;
101101
102102const CONFIG_YML_FILE = '../.circleci/config.yml' ;
103+ const WORKFLOWS_DIR = '../.circleci/src/workflows' ;
103104
104- async function checkParallelism ( cadence ?: Cadence , scriptName ?: TaskKey ) {
105+ async function checkParallelism ( cadence ?: Cadence , scriptName ?: TaskKey , fix : boolean = false ) {
105106 const configYml = await readFile ( CONFIG_YML_FILE , 'utf-8' ) ;
106107 const data = yaml . parse ( configYml ) ;
107108
@@ -110,6 +111,12 @@ async function checkParallelism(cadence?: Cadence, scriptName?: TaskKey) {
110111 const scripts = scriptName ? [ scriptName ] : tasks ;
111112 const summary = [ ] ;
112113 let isIncorrect = false ;
114+ const fixes : Array < {
115+ cadence : string ;
116+ job : string ;
117+ oldParallelism : number ;
118+ newParallelism : number ;
119+ } > = [ ] ;
113120
114121 cadences . forEach ( ( cad ) => {
115122 summary . push ( `\n${ picocolors . bold ( cad ) } ` ) ;
@@ -142,6 +149,12 @@ async function checkParallelism(cadence?: Cadence, scriptName?: TaskKey) {
142149 `(should be ${ newParallelism } )`
143150 ) } `
144151 ) ;
152+ fixes . push ( {
153+ cadence : cad ,
154+ job : tasksMap [ script ] ,
155+ oldParallelism : currentParallelism ,
156+ newParallelism,
157+ } ) ;
145158 isIncorrect = true ;
146159 } else {
147160 summary . push (
@@ -157,17 +170,131 @@ async function checkParallelism(cadence?: Cadence, scriptName?: TaskKey) {
157170 } ) ;
158171
159172 if ( isIncorrect ) {
160- summary . unshift (
161- 'The parellism count is incorrect for some jobs in .circleci/config.yml, you have to update them:'
162- ) ;
163- throw new Error ( summary . concat ( '\n' ) . join ( '\n' ) ) ;
173+ if ( fix ) {
174+ // Apply fixes to individual workflow files
175+ const fixesByFile : Record <
176+ string ,
177+ Array < { job : string ; oldParallelism : number ; newParallelism : number } >
178+ > = { } ;
179+
180+ // Group fixes by workflow file
181+ fixes . forEach ( ( { cadence : fixCadence , job, oldParallelism, newParallelism } ) => {
182+ const workflowFile = `${ fixCadence } .yml` ;
183+ if ( ! fixesByFile [ workflowFile ] ) {
184+ fixesByFile [ workflowFile ] = [ ] ;
185+ }
186+ fixesByFile [ workflowFile ] . push ( { job, oldParallelism, newParallelism } ) ;
187+ } ) ;
188+
189+ // Apply fixes to each workflow file
190+ for ( const [ workflowFile , fileFixes ] of Object . entries ( fixesByFile ) ) {
191+ const workflowPath = `${ WORKFLOWS_DIR } /${ workflowFile } ` ;
192+ let workflowContent = await readFile ( workflowPath , 'utf-8' ) ;
193+
194+ // Apply fixes using string manipulation to preserve comments and formatting
195+ fileFixes . forEach ( ( { job, newParallelism } ) => {
196+ // Find the job definition in the YAML content
197+ const jobRegex = new RegExp ( `^\\s*-\\s+${ job } :\\s*$` , 'm' ) ;
198+ const jobMatch = workflowContent . match ( jobRegex ) ;
199+
200+ if ( jobMatch ) {
201+ const jobStartIndex = jobMatch . index ! ;
202+ const jobStartLine = workflowContent . substring ( 0 , jobStartIndex ) . split ( '\n' ) . length - 1 ;
203+ const lines = workflowContent . split ( '\n' ) ;
204+
205+ // Find the parallelism line for this job
206+ let parallelismLineIndex = - 1 ;
207+ let indentLevel = 0 ;
208+
209+ for ( let i = jobStartLine + 1 ; i < lines . length ; i ++ ) {
210+ const line = lines [ i ] ;
211+ const trimmedLine = line . trim ( ) ;
212+
213+ // If we hit another job or top-level key, stop looking
214+ if (
215+ trimmedLine . startsWith ( '- ' ) ||
216+ ( trimmedLine && ! line . startsWith ( ' ' ) && ! trimmedLine . startsWith ( '#' ) )
217+ ) {
218+ break ;
219+ }
220+
221+ // Track indentation level
222+ if ( trimmedLine && ! trimmedLine . startsWith ( '#' ) ) {
223+ const currentIndent = line . length - line . trimStart ( ) . length ;
224+ if ( indentLevel === 0 ) {
225+ indentLevel = currentIndent ;
226+ }
227+ }
228+
229+ // Look for parallelism line
230+ if ( trimmedLine . startsWith ( 'parallelism:' ) ) {
231+ parallelismLineIndex = i ;
232+ break ;
233+ }
234+ }
235+
236+ if ( parallelismLineIndex !== - 1 ) {
237+ // Update existing parallelism line
238+ const indent = lines [ parallelismLineIndex ] . match ( / ^ ( \s * ) / ) ?. [ 1 ] || '' ;
239+ lines [ parallelismLineIndex ] = `${ indent } parallelism: ${ newParallelism } ` ;
240+ } else {
241+ // Add parallelism line after the job name
242+ const indent = lines [ jobStartLine ] . match ( / ^ ( \s * ) / ) ?. [ 1 ] || '' ;
243+ const jobIndent = indent + ' ' ;
244+ lines . splice ( jobStartLine + 1 , 0 , `${ jobIndent } parallelism: ${ newParallelism } ` ) ;
245+ }
246+
247+ workflowContent = lines . join ( '\n' ) ;
248+ }
249+ } ) ;
250+
251+ // Write the updated workflow file back with preserved comments and formatting
252+ await writeFile ( workflowPath , workflowContent , 'utf-8' ) ;
253+ }
254+
255+ summary . unshift (
256+ `🔧 ${ picocolors . green ( 'Fixed' ) } parallelism counts for ${ fixes . length } job${ fixes . length === 1 ? '' : 's' } in workflow files:`
257+ ) ;
258+ summary . push ( '' ) ;
259+ summary . push ( '✅ The parallelism of the following jobs was fixed:' ) ;
260+ fixes . forEach ( ( { job, oldParallelism, newParallelism, cadence } ) => {
261+ summary . push ( ` - ${ cadence } /${ job } : ${ oldParallelism } → ${ newParallelism } ` ) ;
262+ } ) ;
263+ summary . push ( '' ) ;
264+ summary . push (
265+ `${ picocolors . yellow ( '⚠️ Important:' ) } You must regenerate the main config file by running:`
266+ ) ;
267+ summary . push ( '' ) ;
268+ summary . push (
269+ `${ picocolors . cyan ( ' circleci config pack .circleci/src > .circleci/config.yml' ) } `
270+ ) ;
271+ summary . push ( `${ picocolors . cyan ( ' circleci config validate .circleci/config.yml' ) } ` ) ;
272+ summary . push ( '' ) ;
273+ summary . push (
274+ `${ picocolors . gray ( 'See .circleci/README.md for more details about the packing process.' ) } `
275+ ) ;
276+ console . log ( summary . concat ( '\n' ) . join ( '\n' ) ) ;
277+ } else {
278+ summary . unshift (
279+ 'The parallelism count is incorrect for some jobs in .circleci/config.yml, you have to update them:'
280+ ) ;
281+ summary . push ( '' ) ;
282+ summary . push (
283+ `${ picocolors . yellow ( '💡 Tip:' ) } Use the ${ picocolors . cyan ( '--fix' ) } flag to automatically fix these issues.`
284+ ) ;
285+ summary . push ( '' ) ;
286+ summary . push (
287+ `${ picocolors . gray ( 'Note: The fix will update the workflow files in .circleci/src/workflows/ and you will need to regenerate the main config.yml file. See .circleci/README.md for details.' ) } `
288+ ) ;
289+ throw new Error ( summary . concat ( '\n' ) . join ( '\n' ) ) ;
290+ }
164291 } else {
165292 summary . unshift ( '✅ The parallelism count is correct for all jobs in .circleci/config.yml:' ) ;
166293 console . log ( summary . concat ( '\n' ) . join ( '\n' ) ) ;
167294 }
168295
169296 const inDevelopmentTemplates = Object . entries ( allTemplates )
170- . filter ( ( [ _ , t ] ) => t . inDevelopment )
297+ . filter ( ( [ , t ] ) => t . inDevelopment )
171298 . map ( ( [ k ] ) => k ) ;
172299
173300 if ( inDevelopmentTemplates . length > 0 ) {
@@ -179,16 +306,21 @@ async function checkParallelism(cadence?: Cadence, scriptName?: TaskKey) {
179306 }
180307}
181308
182- type RunOptions = { cadence ?: Cadence ; task ?: TaskKey ; check : boolean } ;
183- async function run ( { cadence, task, check } : RunOptions ) {
184- if ( check ) {
309+ type RunOptions = {
310+ cadence ?: Cadence ;
311+ task ?: TaskKey ;
312+ check : boolean ;
313+ fix : boolean ;
314+ } ;
315+ async function run ( { cadence, task, check, fix } : RunOptions ) {
316+ if ( check || fix ) {
185317 if ( task && ! tasks . includes ( task ) ) {
186318 throw new Error (
187319 dedent `The "${ task } " task you provided is not valid. Valid tasks (found in .circleci/config.yml) are:
188320 ${ tasks . map ( ( v ) => `- ${ v } ` ) . join ( '\n' ) } `
189321 ) ;
190322 }
191- await checkParallelism ( cadence as Cadence , task ) ;
323+ await checkParallelism ( cadence as Cadence , task , fix ) ;
192324 return ;
193325 }
194326
@@ -211,11 +343,8 @@ if (esMain(import.meta.url)) {
211343 . description ( 'Retrieve the template to run for a given cadence and task' )
212344 . option ( '--cadence <cadence>' , 'Which cadence you want to run the script for' )
213345 . option ( '--task <task>' , 'Which task you want to run the script for' )
214- . option (
215- '--check' ,
216- 'Throws an error when the parallelism counts for tasks are incorrect' ,
217- false
218- ) ;
346+ . option ( '--check' , 'Throws an error when the parallelism counts for tasks are incorrect' , false )
347+ . option ( '--fix' , 'Automatically fix parallelism counts in .circleci/config.yml' , false ) ;
219348
220349 program . parse ( process . argv ) ;
221350
0 commit comments