Skip to content

Commit bee15d5

Browse files
committed
feat: draft ljharb action parsing
1 parent 544bd31 commit bee15d5

File tree

1 file changed

+115
-28
lines changed

1 file changed

+115
-28
lines changed

lib/github-actions/index.js

+115-28
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,146 @@
11
'use strict';
22

3+
const _ = require('lodash');
34
const Nv = require('@pkgjs/nv');
45

5-
exports.detect = async (meta) => {
66

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

10-
if (!files.length) {
11-
return;
12-
}
139

14-
for (const file of files) {
10+
internals.parseActionsSetupNode = function * (workflow, file) {
1511

16-
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
17-
continue;
12+
for (const job of Object.values(workflow.jobs)) {
13+
14+
const nodeSteps = job.steps.filter(({ uses }) => uses && uses.startsWith('actions/setup-node'));
15+
for (const step of nodeSteps) {
16+
const nodeVersion = step.with && step.with['node-version'];
17+
18+
if (!nodeVersion) {
19+
// Docs say: "The node-version input is optional. If not supplied, the node version that is PATH will be used."
20+
// Therefore we cannot reliably detect a specific version, but we do want to let the user know
21+
yield 'not-set';
22+
continue;
23+
}
24+
25+
const matrixMatch = nodeVersion.match(/^\${{\s+matrix.(?<matrixVarName>.*)\s+}}$/);
26+
if (matrixMatch) {
27+
const matrix = job.strategy.matrix[matrixMatch.groups.matrixVarName];
28+
29+
yield * matrix;
30+
continue;
31+
}
32+
33+
const envMatch = nodeVersion.match(/^\${{\s+env.(?<envVarName>.*)\s+}}$/);
34+
if (envMatch) {
35+
const envValue = workflow.env[envMatch.groups.envVarName];
36+
37+
yield envValue;
38+
continue;
39+
}
40+
41+
yield nodeVersion;
1842
}
43+
}
44+
};
1945

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

22-
for (const job of Object.values(workflow.jobs)) {
47+
internals.parseLjharbActions = function * (workflow, file) {
48+
49+
for (const job of Object.values(workflow.jobs)) {
50+
51+
const nodeSteps = job.steps.filter(({ uses }) => {
52+
53+
if (!uses) {
54+
return false;
55+
}
56+
57+
return uses.startsWith('ljharb/actions/node/run') || uses.startsWith('ljharb/actions/node/install');
58+
});
59+
60+
for (const step of nodeSteps) {
61+
const nodeVersion = step.with && step.with['node-version'];
62+
63+
if (!nodeVersion) {
64+
yield 'lts/*'; // @todo: find ref which tells us that this is so
65+
continue;
66+
}
67+
68+
const matrixMatch = nodeVersion.match(/^\${{\s+matrix.(?<matrixVarName>.*)\s+}}$/);
69+
if (matrixMatch) {
2370

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

28-
if (!nodeVersion) {
29-
// @todo - no node version defined - use default? what is the default?
73+
const matrix = job.strategy.matrix[matrixMatch.groups.matrixVarName];
74+
75+
yield * matrix;
3076
continue;
3177
}
3278

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

37-
for (const version of matrix) {
38-
rawSet.add(version);
86+
if (!stepMatch) {
87+
throw new Error(`Unable to parse need output: ${needOutput} in ${file}`);
3988
}
4089

41-
continue;
42-
}
90+
const { needStepName/*, needStepOutputName*/ } = stepMatch.groups;
91+
const needStep = needJob.steps.find(({ id }) => id === needStepName);
4392

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

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

51-
rawSet.add(nodeVersion);
102+
throw new Error(`Unable to parse the job matrix: ${job.strategy.matrix} in ${file}`);
52103
}
104+
105+
yield nodeVersion;
106+
}
107+
}
108+
};
109+
110+
111+
exports.detect = async (meta) => {
112+
113+
const files = await meta.loadFolder('.github/workflows');
114+
const rawSet = new Set();
115+
const byFileSets = {};
116+
117+
if (!files.length) {
118+
// explicitly return no `githubActions` - this is different to finding actions and detecting no Node.js versions
119+
return;
120+
}
121+
122+
for (const file of files) {
123+
124+
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
125+
continue;
126+
}
127+
128+
const workflow = await meta.loadFile(`.github/workflows/${file}`, { yaml: true });
129+
byFileSets[file] = byFileSets[file] || new Set();
130+
131+
for (const version of internals.parseActionsSetupNode(workflow, file)) {
132+
rawSet.add(version);
133+
byFileSets[file].add(version);
134+
}
135+
136+
for (const version of internals.parseLjharbActions(workflow, file)) {
137+
rawSet.add(version);
138+
byFileSets[file].add(version);
53139
}
54140
}
55141

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

58145
const resolved = {};
59146

@@ -69,5 +156,5 @@ exports.detect = async (meta) => {
69156
}
70157
}
71158

72-
return { githubActions: { raw, resolved } };
159+
return { githubActions: { byFile, raw, resolved } };
73160
};

0 commit comments

Comments
 (0)