Skip to content

Commit 0dc8429

Browse files
committed
Build: Add automatic parallelism fixing in CircleCI workflows
- Introduced a `--fix` option to the script for automatically correcting parallelism counts in workflow files. - Implemented logic to read and update parallelism values in the corresponding YAML files while preserving formatting and comments. - Enhanced error messages to guide users on regenerating the main config file after fixes.
1 parent a47e297 commit 0dc8429

File tree

1 file changed

+145
-16
lines changed

1 file changed

+145
-16
lines changed

scripts/get-template.ts

Lines changed: 145 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { access, readFile, readdir } from 'node:fs/promises';
1+
import { access, readFile, readdir, writeFile } from 'node:fs/promises';
22

33
import { program } from 'commander';
44
import picocolors from 'picocolors';
@@ -100,8 +100,9 @@ type TaskKey = keyof typeof tasksMap;
100100
const tasks = Object.keys(tasksMap) as TaskKey[];
101101

102102
const 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

Comments
 (0)