UNPKG

circletron

Version:

circle orb for dealing with monorepos

165 lines 6.56 kB
#!/usr/bin/env node "use strict"; 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