circletron
Version:
circle orb for dealing with monorepos
165 lines • 6.56 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.triggerCiJobs = exports.getCircleLernaConfig = void 0;
const child_process_1 = require("child_process");
const fs_extra_1 = require("fs-extra");
const util_1 = require("util");
const axios_1 = require("axios");
const yaml_1 = require("yaml");
const path_1 = require("path");
const CONTINUATION_API_URL = `https://circleci.com/api/v2/pipeline/continue`;
const pExec = util_1.promisify(child_process_1.exec);
const requireEnv = (varName) => {
const value = process.env[varName];
if (!value) {
throw new Error(`Environment variable ${varName} must be set`);
}
return value;
};
async function getPackages() {
const packageOutput = await pExec(`lerna list --parseable --all --long`);
const allPackages = await Promise.all(packageOutput.stdout
.trim()
.split('\n')
.map(async (line) => {
const [fullPath, name] = line.split(':');
let circleConfig = '';
try {
circleConfig = (await fs_extra_1.readFile(path_1.join(fullPath, 'circle.yml'))).toString();
}
catch (e) {
// no circle config, filter below
}
return { circleConfig, name };
}));
return allPackages.filter((pkg) => pkg.circleConfig !== '');
}
/**
* Get the names of the packages which builds should be triggered for by
* determing which packages have changed in this branch and consulting
* .circleci/lerna.yml to packages that should be run due to a dependency
* changing.
*/
const getTriggerPackages = async (packages, config, branch) => {
// run all jobs when the source is the release/develop branches directly
const runAll = branch === 'develop' || branch.startsWith('release/');
const changedPackages = new Set();
if (runAll) {
console.log(`Detected a push from ${branch}, running all pipelines`);
}
else {
const parentBranchOutput = await pExec('get-branchpoint-commit.sh');
// have to prepend origin, when `develop` is used directly, circle incorrectly thinks
// `develop` points to the tip of the current branch. they are doing something weird
// with their git checkout I guess.
const branchpointCommit = parentBranchOutput.stdout.trim();
console.log("Looking for changes since `%s'", branchpointCommit);
const changeOutput = await pExec(`lerna list --parseable --all --long --since ${branchpointCommit}`);
const changesStr = changeOutput.stdout.trim();
if (!changesStr) {
console.log('Found no changed packages');
}
else {
for (const pkg of changesStr.split('\n')) {
changedPackages.add(pkg.split(':', 2)[1]);
}
console.log('Found changes: %O', changedPackages);
}
}
const allPackageNames = new Set(packages.map((pkg) => pkg.name));
if (runAll) {
return allPackageNames;
}
return new Set(Array.from(changedPackages)
.flatMap((changedPackage) => [
changedPackage,
...Object.entries(config.dependencies)
.filter(([, deps]) => deps.includes(changedPackage))
.map(([pkgName]) => pkgName),
])
.filter((pkg) => allPackageNames.has(pkg)));
};
const SKIP_JOB = {
docker: [{ image: 'busybox:stable' }],
steps: [
{
run: {
name: 'Jobs not required',
command: 'echo "Jobs not required"',
},
},
],
};
async function buildConfiguration(packages, triggerPackages) {
const config = yaml_1.parse((await fs_extra_1.readFile('circle.yml')).toString());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mergeObject = (path, projectYaml) => {
var _a;
for (const [name, value] of Object.entries((_a = projectYaml[path]) !== null && _a !== void 0 ? _a : {})) {
if (config[path][name]) {
throw new Error(`Two ${path} with the same name: ${name}`);
}
config[path][name] = value;
}
};
const jobsConfig = config.jobs;
for (const pkg of packages) {
const projectYaml = yaml_1.parse(pkg.circleConfig);
mergeObject('workflows', projectYaml);
mergeObject('orbs', projectYaml);
mergeObject('executors', projectYaml);
mergeObject('commands', projectYaml);
const jobs = projectYaml.jobs;
for (const [jobName, jobData] of Object.entries(jobs)) {
if (jobsConfig[jobName]) {
throw new Error(`Two jobs with the same name: ${jobName}`);
}
if ('conditional' in jobData) {
const { conditional } = jobData;
delete jobData.conditional;
if (conditional === false) {
// these jobs are triggered no matter what
jobsConfig[jobName] = jobData;
continue;
}
}
jobsConfig[jobName] = triggerPackages.has(pkg.name) ? jobData : SKIP_JOB;
}
}
return yaml_1.stringify(config);
}
async function getCircleLernaConfig() {
var _a;
let rawConfig = {};
try {
rawConfig = yaml_1.parse((await fs_extra_1.readFile(path_1.join('.circleci', 'lerna.yml'))).toString());
}
catch (e) {
// lerna.yml is not mandatory
}
return { dependencies: (_a = rawConfig.dependencies) !== null && _a !== void 0 ? _a : {} };
}
exports.getCircleLernaConfig = getCircleLernaConfig;
async function triggerCiJobs(branch, continuationKey) {
const lernaConfig = await getCircleLernaConfig();
const packages = await getPackages();
const triggerPackages = await getTriggerPackages(packages, lernaConfig, branch);
const body = {
'continuation-key': continuationKey,
configuration: await buildConfiguration(packages, triggerPackages),
};
console.log('CircleCI request to %s: %O', CONTINUATION_API_URL, body);
const response = await axios_1.default.post(CONTINUATION_API_URL, body);
console.log('CircleCI response: %O', response.data);
}
exports.triggerCiJobs = triggerCiJobs;
if (require.main === module) {
const branch = requireEnv('CIRCLE_BRANCH');
const continuationKey = requireEnv('CIRCLE_CONTINUATION_KEY');
triggerCiJobs(branch, continuationKey).catch((err) => {
console.warn('Got error: %O', err);
process.exit(1);
});
}
//# sourceMappingURL=index.js.map