Skip to content

Commit

Permalink
feat: draft ljharb action parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
dominykas committed May 10, 2021
1 parent 544bd31 commit bee15d5
Showing 1 changed file with 115 additions and 28 deletions.
143 changes: 115 additions & 28 deletions lib/github-actions/index.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,146 @@
'use strict';

const _ = require('lodash');
const Nv = require('@pkgjs/nv');

exports.detect = async (meta) => {

const files = await meta.loadFolder('.github/workflows');
const rawSet = new Set();
const internals = {};

if (!files.length) {
return;
}

for (const file of files) {
internals.parseActionsSetupNode = function * (workflow, file) {

if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
continue;
for (const job of Object.values(workflow.jobs)) {

const nodeSteps = job.steps.filter(({ uses }) => uses && uses.startsWith('actions/setup-node'));
for (const step of nodeSteps) {
const nodeVersion = step.with && step.with['node-version'];

if (!nodeVersion) {
// Docs say: "The node-version input is optional. If not supplied, the node version that is PATH will be used."
// Therefore we cannot reliably detect a specific version, but we do want to let the user know
yield 'not-set';
continue;
}

const matrixMatch = nodeVersion.match(/^\${{\s+matrix.(?<matrixVarName>.*)\s+}}$/);
if (matrixMatch) {
const matrix = job.strategy.matrix[matrixMatch.groups.matrixVarName];

yield * matrix;
continue;
}

const envMatch = nodeVersion.match(/^\${{\s+env.(?<envVarName>.*)\s+}}$/);
if (envMatch) {
const envValue = workflow.env[envMatch.groups.envVarName];

yield envValue;
continue;
}

yield nodeVersion;
}
}
};

const workflow = await meta.loadFile(`.github/workflows/${file}`, { yaml: true });

for (const job of Object.values(workflow.jobs)) {
internals.parseLjharbActions = function * (workflow, file) {

for (const job of Object.values(workflow.jobs)) {

const nodeSteps = job.steps.filter(({ uses }) => {

if (!uses) {
return false;
}

return uses.startsWith('ljharb/actions/node/run') || uses.startsWith('ljharb/actions/node/install');
});

for (const step of nodeSteps) {
const nodeVersion = step.with && step.with['node-version'];

if (!nodeVersion) {
yield 'lts/*'; // @todo: find ref which tells us that this is so
continue;
}

const matrixMatch = nodeVersion.match(/^\${{\s+matrix.(?<matrixVarName>.*)\s+}}$/);
if (matrixMatch) {

const nodeSteps = job.steps.filter(({ uses }) => uses && uses.startsWith('actions/setup-node'));
for (const step of nodeSteps) {
const nodeVersion = step.with && step.with['node-version'];
if (typeof job.strategy.matrix !== 'string') {

if (!nodeVersion) {
// @todo - no node version defined - use default? what is the default?
const matrix = job.strategy.matrix[matrixMatch.groups.matrixVarName];

yield * matrix;
continue;
}

const matrixMatch = nodeVersion.match(/^\${{\s+matrix.(?<matrixVarName>.*)\s+}}$/);
if (matrixMatch) {
const matrix = job.strategy.matrix[matrixMatch.groups.matrixVarName];
const fromJsonMatch = job.strategy.matrix.match(/^\${{\s+fromJson\(needs\.(?<needJobName>.*)\.outputs\.(?<needOutputName>.*)\)\s+}}$/);
if (fromJsonMatch) {
const { needJobName, needOutputName } = fromJsonMatch.groups;
const needJob = workflow.jobs[needJobName];
const needOutput = needJob.outputs[needOutputName];
const stepMatch = needOutput.match(/^\${{\s+steps\.(?<needStepName>.*)\.outputs\.(?<needStepOutputName>.*)\s+}}$/);

for (const version of matrix) {
rawSet.add(version);
if (!stepMatch) {
throw new Error(`Unable to parse need output: ${needOutput} in ${file}`);
}

continue;
}
const { needStepName/*, needStepOutputName*/ } = stepMatch.groups;
const needStep = needJob.steps.find(({ id }) => id === needStepName);

const envMatch = nodeVersion.match(/^\${{\s+env.(?<envVarName>.*)\s+}}$/);
if (envMatch) {
rawSet.add(workflow.env[envMatch.groups.envVarName]);
if (!needStep || !needStep.uses.startsWith('ljharb/actions/node/matrix')) {
throw new Error(`Unrecognized action in ${needOutput} in ${file}`);
}

// @todo: with has more options - resolve to precise versions here and yield the full list
yield needStep.with.preset;
continue;
}

rawSet.add(nodeVersion);
throw new Error(`Unable to parse the job matrix: ${job.strategy.matrix} in ${file}`);
}

yield nodeVersion;
}
}
};


exports.detect = async (meta) => {

const files = await meta.loadFolder('.github/workflows');
const rawSet = new Set();
const byFileSets = {};

if (!files.length) {
// explicitly return no `githubActions` - this is different to finding actions and detecting no Node.js versions
return;
}

for (const file of files) {

if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
continue;
}

const workflow = await meta.loadFile(`.github/workflows/${file}`, { yaml: true });
byFileSets[file] = byFileSets[file] || new Set();

for (const version of internals.parseActionsSetupNode(workflow, file)) {
rawSet.add(version);
byFileSets[file].add(version);
}

for (const version of internals.parseLjharbActions(workflow, file)) {
rawSet.add(version);
byFileSets[file].add(version);
}
}

const raw = [...rawSet];
const byFile = _.mapValues(byFileSets, (set) => [...set]);

const resolved = {};

Expand All @@ -69,5 +156,5 @@ exports.detect = async (meta) => {
}
}

return { githubActions: { raw, resolved } };
return { githubActions: { byFile, raw, resolved } };
};

0 comments on commit bee15d5

Please sign in to comment.