actions-warden 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +199 -0
- package/CONTRIBUTING.md +109 -0
- package/README.md +284 -224
- package/RELEASING.md +338 -0
- package/SECURITY.md +30 -3
- package/docs/AI-AGENTS.md +474 -0
- package/docs/CLI.md +474 -0
- package/docs/CONFIGURATION.md +340 -0
- package/docs/DEVELOPMENT.md +373 -0
- package/docs/GITHUB-ACTION.md +285 -0
- package/docs/JAVASCRIPT-API.md +357 -0
- package/docs/OUTPUTS.md +414 -0
- package/docs/README.md +27 -0
- package/examples/org-scan.yml +42 -0
- package/examples/upgrade-pr.yml +57 -0
- package/llms.txt +38 -0
- package/package.json +32 -10
- package/skills/actions-warden/SKILL.md +151 -40
- package/src/action.js +306 -0
- package/src/cli.js +494 -56
- package/src/commands/audit.js +189 -36
- package/src/commands/org-scan.js +544 -0
- package/src/commands/pin.js +59 -56
- package/src/commands/report.js +122 -10
- package/src/commands/upgrade.js +102 -62
- package/src/commands/verify.js +193 -0
- package/src/index.js +21 -4
- package/src/lib/action-status.js +27 -0
- package/src/lib/agent-mode.js +174 -0
- package/src/lib/annotations.js +250 -0
- package/src/lib/baseline.js +103 -0
- package/src/lib/cache.js +47 -10
- package/src/lib/concurrency.js +27 -0
- package/src/lib/config.js +185 -0
- package/src/lib/execution.js +71 -0
- package/src/lib/formatter.js +127 -8
- package/src/lib/github-org.js +374 -0
- package/src/lib/identity.js +62 -0
- package/src/lib/ignore.js +7 -6
- package/src/lib/org-checkpoint.js +461 -0
- package/src/lib/org-progress.js +60 -0
- package/src/lib/parser.js +326 -52
- package/src/lib/patcher.js +199 -0
- package/src/lib/path-equality.js +30 -0
- package/src/lib/paths.js +35 -12
- package/src/lib/redact.js +65 -4
- package/src/lib/resolver.js +225 -43
- package/src/lib/targets.js +28 -0
- package/src/lib/triggers.js +12 -0
- package/src/lib/writer.js +48 -8
- package/src/rules/excessive-permissions.js +24 -33
- package/src/rules/index.js +19 -1
- package/src/rules/pull-request-target-checkout.js +149 -18
- package/src/rules/reusable-workflow-secrets.js +32 -0
- package/src/rules/script-injection.js +77 -12
- package/src/rules/secrets-in-env.js +101 -18
- package/src/rules/unpinned-action.js +3 -2
- package/src/rules/unpinned-container-image.js +39 -0
- package/src/rules/unpinned-docker-action.js +30 -0
- package/src/rules/untrusted-self-hosted-runner.js +109 -0
- package/src/rules/workflow-run-artifact-execution.js +122 -0
- package/src/rules/workflow-structure.js +396 -0
- package/src/version.js +3 -0
package/src/lib/parser.js
CHANGED
|
@@ -7,7 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFile } from 'node:fs/promises';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
parseDocument,
|
|
12
|
+
isAlias,
|
|
13
|
+
isMap,
|
|
14
|
+
isSeq,
|
|
15
|
+
isPair,
|
|
16
|
+
isScalar,
|
|
17
|
+
} from 'yaml';
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* @typedef {object} ActionRef
|
|
@@ -16,30 +23,71 @@ import { parseDocument, isMap, isSeq, isPair, isScalar } from 'yaml';
|
|
|
16
23
|
* @property {string|null} repo
|
|
17
24
|
* @property {string|null} subpath - reusable-workflow sub-path
|
|
18
25
|
* @property {string|null} ref - tag, branch, or SHA after `@`
|
|
19
|
-
* @property {'external'|'reusable-workflow'|'local'|'docker'|'unknown'} kind
|
|
26
|
+
* @property {'external'|'reusable-workflow'|'local'|'self'|'docker'|'unknown'} kind
|
|
20
27
|
* @property {number} line
|
|
28
|
+
* @property {number} column
|
|
29
|
+
* @property {number} start
|
|
30
|
+
* @property {number} end
|
|
31
|
+
* @property {number} lineStart
|
|
32
|
+
* @property {number} lineEnd
|
|
33
|
+
* @property {boolean} alias
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} ImageRef
|
|
38
|
+
* @property {string} raw
|
|
39
|
+
* @property {number} line
|
|
40
|
+
* @property {number} start
|
|
41
|
+
* @property {number} end
|
|
42
|
+
* @property {string} context
|
|
43
|
+
* @property {string|null} jobName
|
|
21
44
|
*/
|
|
22
45
|
|
|
23
46
|
/**
|
|
24
47
|
* @typedef {object} StepNode
|
|
25
48
|
* @property {string|null} name
|
|
26
49
|
* @property {string|null} id
|
|
50
|
+
* @property {boolean} ifDeclared
|
|
27
51
|
* @property {ActionRef|null} uses
|
|
52
|
+
* @property {boolean} usesDeclared
|
|
28
53
|
* @property {string|null} run
|
|
29
|
-
* @property {
|
|
30
|
-
* @property {
|
|
54
|
+
* @property {boolean} runDeclared
|
|
55
|
+
* @property {unknown} env
|
|
56
|
+
* @property {unknown} with_
|
|
57
|
+
* @property {string|null} workingDirectory
|
|
58
|
+
* @property {string|null} shell
|
|
59
|
+
* @property {boolean} shellDeclared
|
|
60
|
+
* @property {'wait'|'wait-all'|'cancel'|'parallel'|null} control
|
|
61
|
+
* @property {unknown} controlValue
|
|
62
|
+
* @property {number} primaryCount
|
|
63
|
+
* @property {unknown} background
|
|
64
|
+
* @property {boolean} backgroundDeclared
|
|
65
|
+
* @property {boolean} parallelValid
|
|
66
|
+
* @property {number} parallelDepth
|
|
31
67
|
* @property {number} line
|
|
68
|
+
* @property {number} runLine
|
|
69
|
+
* @property {number} envLine
|
|
32
70
|
*/
|
|
33
71
|
|
|
34
72
|
/**
|
|
35
73
|
* @typedef {object} JobNode
|
|
36
74
|
* @property {string} name
|
|
37
|
-
* @property {
|
|
38
|
-
* @property {
|
|
39
|
-
* @property {
|
|
40
|
-
* @property {
|
|
75
|
+
* @property {unknown} permissions
|
|
76
|
+
* @property {boolean} permissionsDeclared
|
|
77
|
+
* @property {unknown} runsOn
|
|
78
|
+
* @property {boolean} runsOnDeclared
|
|
79
|
+
* @property {unknown} env
|
|
80
|
+
* @property {ActionRef|null} uses
|
|
81
|
+
* @property {boolean} usesDeclared
|
|
82
|
+
* @property {unknown} with_
|
|
83
|
+
* @property {unknown} secrets
|
|
41
84
|
* @property {StepNode[]} steps
|
|
85
|
+
* @property {boolean} stepsDeclared
|
|
86
|
+
* @property {boolean} stepsValid
|
|
87
|
+
* @property {boolean} validMapping
|
|
42
88
|
* @property {number} line
|
|
89
|
+
* @property {number} permissionsLine
|
|
90
|
+
* @property {number} envLine
|
|
43
91
|
*/
|
|
44
92
|
|
|
45
93
|
/**
|
|
@@ -49,19 +97,24 @@ import { parseDocument, isMap, isSeq, isPair, isScalar } from 'yaml';
|
|
|
49
97
|
* @property {'workflow'|'composite-action'|'unknown'} kind
|
|
50
98
|
* @property {string|null} name
|
|
51
99
|
* @property {unknown} on
|
|
52
|
-
* @property {
|
|
100
|
+
* @property {boolean} onDeclared
|
|
101
|
+
* @property {unknown} permissions
|
|
102
|
+
* @property {boolean} permissionsDeclared
|
|
53
103
|
* @property {object|null} env
|
|
54
104
|
* @property {JobNode[]} jobs
|
|
55
|
-
* @property {
|
|
105
|
+
* @property {ImageRef[]} images
|
|
106
|
+
* @property {number} permissionsLine
|
|
107
|
+
* @property {number} envLine
|
|
56
108
|
* @property {object} raw - the parsed plain object (for rules to query)
|
|
57
109
|
*/
|
|
58
110
|
|
|
59
111
|
/**
|
|
60
112
|
* @param {string} raw e.g. `actions/checkout@v3` or `./local`
|
|
61
113
|
* @param {number} line
|
|
114
|
+
* @param {{column?: number, start?: number, end?: number, lineStart?: number, lineEnd?: number}} [location]
|
|
62
115
|
* @returns {ActionRef}
|
|
63
116
|
*/
|
|
64
|
-
export function parseActionRef(raw, line) {
|
|
117
|
+
export function parseActionRef(raw, line, location = {}) {
|
|
65
118
|
/** @type {ActionRef} */
|
|
66
119
|
const base = {
|
|
67
120
|
raw,
|
|
@@ -71,11 +124,20 @@ export function parseActionRef(raw, line) {
|
|
|
71
124
|
ref: null,
|
|
72
125
|
kind: 'unknown',
|
|
73
126
|
line,
|
|
127
|
+
column: location.column ?? 0,
|
|
128
|
+
start: location.start ?? 0,
|
|
129
|
+
end: location.end ?? 0,
|
|
130
|
+
lineStart: location.lineStart ?? 0,
|
|
131
|
+
lineEnd: location.lineEnd ?? 0,
|
|
132
|
+
alias: Boolean(location.alias),
|
|
74
133
|
};
|
|
75
134
|
if (typeof raw !== 'string' || raw.length === 0) return base;
|
|
76
135
|
if (raw.startsWith('./') || raw.startsWith('../')) {
|
|
77
136
|
return { ...base, kind: 'local' };
|
|
78
137
|
}
|
|
138
|
+
if (raw.startsWith('$/')) {
|
|
139
|
+
return { ...base, kind: 'self' };
|
|
140
|
+
}
|
|
79
141
|
if (raw.startsWith('docker://')) {
|
|
80
142
|
return { ...base, kind: 'docker' };
|
|
81
143
|
}
|
|
@@ -109,6 +171,27 @@ function lineFromRange(source, node) {
|
|
|
109
171
|
return line;
|
|
110
172
|
}
|
|
111
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Return exact source offsets for a scalar node.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} source
|
|
178
|
+
* @param {{range?: number[]}} node
|
|
179
|
+
*/
|
|
180
|
+
function locationFromRange(source, node) {
|
|
181
|
+
const start = node?.range?.[0] ?? 0;
|
|
182
|
+
const end = node?.range?.[1] ?? start;
|
|
183
|
+
const lineStart = source.lastIndexOf('\n', Math.max(start - 1, 0)) + 1;
|
|
184
|
+
const newline = source.indexOf('\n', end);
|
|
185
|
+
const lineEnd = newline === -1 ? source.length : newline;
|
|
186
|
+
return {
|
|
187
|
+
start,
|
|
188
|
+
end,
|
|
189
|
+
lineStart,
|
|
190
|
+
lineEnd,
|
|
191
|
+
column: start - lineStart + 1,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
112
195
|
/**
|
|
113
196
|
* Read a Pair value into a JS scalar where possible.
|
|
114
197
|
*
|
|
@@ -139,53 +222,108 @@ function findPair(map, key) {
|
|
|
139
222
|
}
|
|
140
223
|
|
|
141
224
|
/**
|
|
142
|
-
* Extract a `steps` sequence from a workflow job or composite action `runs`
|
|
143
|
-
* mapping. Both locations use the same step-level `uses` syntax.
|
|
144
|
-
*
|
|
145
225
|
* @param {string} source
|
|
146
|
-
* @param {object}
|
|
226
|
+
* @param {object} stepsNode
|
|
227
|
+
* @param {number} [parallelDepth]
|
|
147
228
|
* @returns {StepNode[]}
|
|
148
229
|
*/
|
|
149
|
-
function extractSteps(source,
|
|
230
|
+
function extractSteps(source, stepsNode, doc, parallelDepth = 0) {
|
|
231
|
+
if (!isSeq(stepsNode)) return [];
|
|
150
232
|
const steps = [];
|
|
151
|
-
const
|
|
152
|
-
if (!stepsPair || !isSeq(stepsPair.value)) return steps;
|
|
153
|
-
|
|
154
|
-
for (const stepNode of stepsPair.value.items) {
|
|
233
|
+
for (const stepNode of stepsNode.items) {
|
|
155
234
|
if (!isMap(stepNode)) continue;
|
|
156
235
|
const usesPair = findPair(stepNode, 'uses');
|
|
157
236
|
const runPair = findPair(stepNode, 'run');
|
|
158
237
|
const namePair = findPair(stepNode, 'name');
|
|
159
238
|
const idPair = findPair(stepNode, 'id');
|
|
239
|
+
const ifPair = findPair(stepNode, 'if');
|
|
160
240
|
const envPair = findPair(stepNode, 'env');
|
|
161
241
|
const withPair = findPair(stepNode, 'with');
|
|
162
|
-
|
|
242
|
+
const workingDirectoryPair = findPair(stepNode, 'working-directory');
|
|
243
|
+
const shellPair = findPair(stepNode, 'shell');
|
|
244
|
+
const waitPair = findPair(stepNode, 'wait');
|
|
245
|
+
const waitAllPair = findPair(stepNode, 'wait-all');
|
|
246
|
+
const cancelPair = findPair(stepNode, 'cancel');
|
|
247
|
+
const parallelPair = findPair(stepNode, 'parallel');
|
|
248
|
+
const backgroundPair = findPair(stepNode, 'background');
|
|
249
|
+
const controlPairs = [
|
|
250
|
+
['wait', waitPair],
|
|
251
|
+
['wait-all', waitAllPair],
|
|
252
|
+
['cancel', cancelPair],
|
|
253
|
+
['parallel', parallelPair],
|
|
254
|
+
].filter(([, pair]) => Boolean(pair));
|
|
255
|
+
const [control, controlPair] = controlPairs[0] ?? [null, null];
|
|
256
|
+
const stepLine = lineFromRange(source, stepNode);
|
|
257
|
+
/** @type {StepNode} */
|
|
163
258
|
const step = {
|
|
164
259
|
name: namePair && isScalar(namePair.value) ? String(namePair.value.value) : null,
|
|
165
260
|
id: idPair && isScalar(idPair.value) ? String(idPair.value.value) : null,
|
|
261
|
+
ifDeclared: Boolean(ifPair),
|
|
166
262
|
uses: null,
|
|
263
|
+
usesDeclared: Boolean(usesPair),
|
|
167
264
|
run: runPair && isScalar(runPair.value) ? String(runPair.value.value) : null,
|
|
265
|
+
runDeclared: Boolean(runPair),
|
|
168
266
|
env: envPair ? toJs(envPair.value) : null,
|
|
169
267
|
with_: withPair ? toJs(withPair.value) : null,
|
|
170
|
-
|
|
268
|
+
workingDirectory: workingDirectoryPair && isScalar(workingDirectoryPair.value)
|
|
269
|
+
? String(workingDirectoryPair.value.value)
|
|
270
|
+
: null,
|
|
271
|
+
shell: shellPair && isScalar(shellPair.value) ? String(shellPair.value.value) : null,
|
|
272
|
+
shellDeclared: Boolean(shellPair),
|
|
273
|
+
control,
|
|
274
|
+
controlValue: controlPair ? toJs(controlPair.value) : null,
|
|
275
|
+
primaryCount: Number(Boolean(usesPair)) + Number(Boolean(runPair)) + controlPairs.length,
|
|
276
|
+
background: backgroundPair ? toJs(backgroundPair.value) : null,
|
|
277
|
+
backgroundDeclared: Boolean(backgroundPair),
|
|
278
|
+
parallelValid: Boolean(
|
|
279
|
+
parallelPair
|
|
280
|
+
&& isSeq(parallelPair.value)
|
|
281
|
+
&& parallelPair.value.items.length > 0
|
|
282
|
+
&& parallelPair.value.items.every(item => isMap(item)),
|
|
283
|
+
),
|
|
284
|
+
parallelDepth,
|
|
285
|
+
line: stepLine,
|
|
286
|
+
runLine: runPair ? lineFromRange(source, runPair.key) : 0,
|
|
287
|
+
envLine: envPair ? lineFromRange(source, envPair.key) : 0,
|
|
171
288
|
};
|
|
172
|
-
|
|
289
|
+
const usesValue = usesPair ? resolveScalar(usesPair.value, doc) : null;
|
|
290
|
+
if (usesValue) {
|
|
291
|
+
const usesLine = lineFromRange(source, usesValue.location);
|
|
173
292
|
step.uses = parseActionRef(
|
|
174
|
-
String(
|
|
175
|
-
|
|
293
|
+
String(usesValue.scalar.value),
|
|
294
|
+
usesLine,
|
|
295
|
+
{
|
|
296
|
+
...locationFromRange(source, usesValue.location),
|
|
297
|
+
alias: usesValue.alias,
|
|
298
|
+
},
|
|
176
299
|
);
|
|
177
300
|
}
|
|
178
301
|
steps.push(step);
|
|
302
|
+
if (parallelPair && isSeq(parallelPair.value)) {
|
|
303
|
+
steps.push(...extractSteps(source, parallelPair.value, doc, parallelDepth + 1));
|
|
304
|
+
}
|
|
179
305
|
}
|
|
180
306
|
return steps;
|
|
181
307
|
}
|
|
182
308
|
|
|
309
|
+
function imageRef(source, scalarValue, context, jobName = null) {
|
|
310
|
+
const location = locationFromRange(source, scalarValue.location);
|
|
311
|
+
return {
|
|
312
|
+
raw: String(scalarValue.scalar.value),
|
|
313
|
+
line: lineFromRange(source, scalarValue.location),
|
|
314
|
+
start: location.start,
|
|
315
|
+
end: location.end,
|
|
316
|
+
context,
|
|
317
|
+
jobName,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
183
321
|
/**
|
|
184
322
|
* @param {string} source
|
|
185
323
|
* @param {object} doc - yaml Document
|
|
186
324
|
* @returns {JobNode[]}
|
|
187
325
|
*/
|
|
188
|
-
function extractJobs(source, doc) {
|
|
326
|
+
function extractJobs(source, doc, images) {
|
|
189
327
|
/** @type {JobNode[]} */
|
|
190
328
|
const jobs = [];
|
|
191
329
|
const jobsPair = findPair(doc.contents, 'jobs');
|
|
@@ -201,28 +339,90 @@ function extractJobs(source, doc) {
|
|
|
201
339
|
const job = {
|
|
202
340
|
name: jobName,
|
|
203
341
|
permissions: null,
|
|
342
|
+
permissionsDeclared: false,
|
|
204
343
|
runsOn: null,
|
|
344
|
+
runsOnDeclared: false,
|
|
205
345
|
env: null,
|
|
206
346
|
uses: null,
|
|
347
|
+
usesDeclared: false,
|
|
348
|
+
with_: null,
|
|
349
|
+
secrets: null,
|
|
207
350
|
steps: [],
|
|
351
|
+
stepsDeclared: false,
|
|
352
|
+
stepsValid: false,
|
|
353
|
+
validMapping: isMap(jobNode),
|
|
208
354
|
line: jobLine,
|
|
355
|
+
permissionsLine: 0,
|
|
356
|
+
envLine: 0,
|
|
209
357
|
};
|
|
210
358
|
|
|
211
359
|
if (isMap(jobNode)) {
|
|
212
360
|
const perms = findPair(jobNode, 'permissions');
|
|
213
|
-
if (perms)
|
|
361
|
+
if (perms) {
|
|
362
|
+
job.permissions = toJs(perms.value);
|
|
363
|
+
job.permissionsDeclared = true;
|
|
364
|
+
job.permissionsLine = lineFromRange(source, perms.key);
|
|
365
|
+
}
|
|
214
366
|
const runsOn = findPair(jobNode, 'runs-on');
|
|
215
|
-
if (runsOn)
|
|
367
|
+
if (runsOn) {
|
|
368
|
+
job.runsOn = toJs(runsOn.value);
|
|
369
|
+
job.runsOnDeclared = true;
|
|
370
|
+
}
|
|
216
371
|
const env = findPair(jobNode, 'env');
|
|
217
|
-
if (env)
|
|
218
|
-
|
|
219
|
-
|
|
372
|
+
if (env) {
|
|
373
|
+
job.env = toJs(env.value);
|
|
374
|
+
job.envLine = lineFromRange(source, env.key);
|
|
375
|
+
}
|
|
376
|
+
const jobUses = findPair(jobNode, 'uses');
|
|
377
|
+
job.usesDeclared = Boolean(jobUses);
|
|
378
|
+
const jobUsesValue = jobUses ? resolveScalar(jobUses.value, doc) : null;
|
|
379
|
+
if (jobUsesValue) {
|
|
380
|
+
const usesLine = lineFromRange(source, jobUsesValue.location);
|
|
220
381
|
job.uses = parseActionRef(
|
|
221
|
-
String(
|
|
222
|
-
|
|
382
|
+
String(jobUsesValue.scalar.value),
|
|
383
|
+
usesLine,
|
|
384
|
+
{
|
|
385
|
+
...locationFromRange(source, jobUsesValue.location),
|
|
386
|
+
alias: jobUsesValue.alias,
|
|
387
|
+
},
|
|
223
388
|
);
|
|
224
389
|
}
|
|
225
|
-
|
|
390
|
+
const jobWith = findPair(jobNode, 'with');
|
|
391
|
+
if (jobWith) job.with_ = toJs(jobWith.value);
|
|
392
|
+
const jobSecrets = findPair(jobNode, 'secrets');
|
|
393
|
+
if (jobSecrets) job.secrets = toJs(jobSecrets.value);
|
|
394
|
+
|
|
395
|
+
const containerPair = findPair(jobNode, 'container');
|
|
396
|
+
const containerValue = containerPair ? resolveScalar(containerPair.value, doc) : null;
|
|
397
|
+
if (containerValue) {
|
|
398
|
+
images.push(imageRef(source, containerValue, 'job-container', jobName));
|
|
399
|
+
} else if (containerPair && isMap(containerPair.value)) {
|
|
400
|
+
const imagePair = findPair(containerPair.value, 'image');
|
|
401
|
+
const imageValue = imagePair ? resolveScalar(imagePair.value, doc) : null;
|
|
402
|
+
if (imageValue) {
|
|
403
|
+
images.push(imageRef(source, imageValue, 'job-container', jobName));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const servicesPair = findPair(jobNode, 'services');
|
|
408
|
+
if (servicesPair && isMap(servicesPair.value)) {
|
|
409
|
+
for (const servicePair of servicesPair.value.items) {
|
|
410
|
+
if (!isPair(servicePair) || !isMap(servicePair.value)) continue;
|
|
411
|
+
const imagePair = findPair(servicePair.value, 'image');
|
|
412
|
+
const imageValue = imagePair ? resolveScalar(imagePair.value, doc) : null;
|
|
413
|
+
if (imageValue) {
|
|
414
|
+
images.push(imageRef(source, imageValue, 'service-container', jobName));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const stepsPair = findPair(jobNode, 'steps');
|
|
420
|
+
if (stepsPair) {
|
|
421
|
+
job.stepsDeclared = true;
|
|
422
|
+
job.stepsValid = isSeq(stepsPair.value)
|
|
423
|
+
&& stepsPair.value.items.every(item => isMap(item));
|
|
424
|
+
job.steps = extractSteps(source, stepsPair.value, doc);
|
|
425
|
+
}
|
|
226
426
|
}
|
|
227
427
|
jobs.push(job);
|
|
228
428
|
}
|
|
@@ -249,32 +449,85 @@ export function parseWorkflowSource(source, path) {
|
|
|
249
449
|
kind: 'unknown',
|
|
250
450
|
name: null,
|
|
251
451
|
on: null,
|
|
452
|
+
onDeclared: false,
|
|
252
453
|
permissions: null,
|
|
454
|
+
permissionsDeclared: false,
|
|
253
455
|
env: null,
|
|
254
456
|
jobs: [],
|
|
255
|
-
|
|
457
|
+
images: [],
|
|
458
|
+
permissionsLine: 0,
|
|
459
|
+
envLine: 0,
|
|
256
460
|
raw: doc.toJS() ?? {},
|
|
257
461
|
};
|
|
258
462
|
if (!doc.contents || !isMap(doc.contents)) return result;
|
|
259
463
|
const namePair = findPair(doc.contents, 'name');
|
|
260
464
|
if (namePair && isScalar(namePair.value)) result.name = String(namePair.value.value);
|
|
261
465
|
const onPair = findPair(doc.contents, 'on');
|
|
262
|
-
if (onPair)
|
|
466
|
+
if (onPair) {
|
|
467
|
+
result.on = toJs(onPair.value);
|
|
468
|
+
result.onDeclared = true;
|
|
469
|
+
}
|
|
263
470
|
const permPair = findPair(doc.contents, 'permissions');
|
|
264
|
-
if (permPair)
|
|
471
|
+
if (permPair) {
|
|
472
|
+
result.permissions = toJs(permPair.value);
|
|
473
|
+
result.permissionsDeclared = true;
|
|
474
|
+
result.permissionsLine = lineFromRange(source, permPair.key);
|
|
475
|
+
}
|
|
265
476
|
const envPair = findPair(doc.contents, 'env');
|
|
266
|
-
if (envPair)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
477
|
+
if (envPair) {
|
|
478
|
+
result.env = toJs(envPair.value);
|
|
479
|
+
result.envLine = lineFromRange(source, envPair.key);
|
|
480
|
+
}
|
|
270
481
|
const runsPair = findPair(doc.contents, 'runs');
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
result.
|
|
482
|
+
const usingPair = runsPair && isMap(runsPair.value) ? findPair(runsPair.value, 'using') : null;
|
|
483
|
+
if (usingPair?.value?.value === 'composite') {
|
|
484
|
+
result.kind = 'composite-action';
|
|
485
|
+
const stepsPair = findPair(runsPair.value, 'steps');
|
|
486
|
+
result.jobs = [{
|
|
487
|
+
name: 'composite',
|
|
488
|
+
permissions: null,
|
|
489
|
+
permissionsDeclared: false,
|
|
490
|
+
runsOn: null,
|
|
491
|
+
runsOnDeclared: false,
|
|
492
|
+
env: null,
|
|
493
|
+
uses: null,
|
|
494
|
+
usesDeclared: false,
|
|
495
|
+
with_: null,
|
|
496
|
+
secrets: null,
|
|
497
|
+
steps: stepsPair ? extractSteps(source, stepsPair.value, doc) : [],
|
|
498
|
+
stepsDeclared: Boolean(stepsPair),
|
|
499
|
+
stepsValid: Boolean(
|
|
500
|
+
stepsPair
|
|
501
|
+
&& isSeq(stepsPair.value)
|
|
502
|
+
&& stepsPair.value.items.every(item => isMap(item)),
|
|
503
|
+
),
|
|
504
|
+
validMapping: true,
|
|
505
|
+
line: lineFromRange(source, runsPair.key),
|
|
506
|
+
permissionsLine: 0,
|
|
507
|
+
envLine: 0,
|
|
508
|
+
}];
|
|
509
|
+
} else {
|
|
510
|
+
result.kind = findPair(doc.contents, 'jobs') ? 'workflow' : 'unknown';
|
|
511
|
+
result.jobs = extractJobs(source, doc, result.images);
|
|
512
|
+
if (usingPair?.value?.value === 'docker') {
|
|
513
|
+
result.kind = 'unknown';
|
|
514
|
+
const imagePair = findPair(runsPair.value, 'image');
|
|
515
|
+
const imageValue = imagePair ? resolveScalar(imagePair.value, doc) : null;
|
|
516
|
+
if (imageValue) {
|
|
517
|
+
result.images.push(imageRef(source, imageValue, 'docker-action'));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
274
520
|
}
|
|
275
521
|
return result;
|
|
276
522
|
}
|
|
277
523
|
|
|
524
|
+
function resolveScalar(node, doc) {
|
|
525
|
+
if (isScalar(node)) return { scalar: node, location: node, alias: false };
|
|
526
|
+
if (!isAlias(node)) return null;
|
|
527
|
+
const scalar = node.resolve(doc);
|
|
528
|
+
return isScalar(scalar) ? { scalar, location: node, alias: true } : null;
|
|
529
|
+
}
|
|
530
|
+
|
|
278
531
|
/**
|
|
279
532
|
* @param {string} path
|
|
280
533
|
* @returns {Promise<WorkflowDoc>}
|
|
@@ -288,20 +541,41 @@ export async function parseWorkflowFile(path) {
|
|
|
288
541
|
* Iterate every action reference in a workflow.
|
|
289
542
|
*
|
|
290
543
|
* @param {WorkflowDoc} workflow
|
|
291
|
-
* @returns {Array<{ref: ActionRef, jobName: string|null, stepIndex: number, location: 'job'|'step'|'action-step'}>}
|
|
544
|
+
* @returns {Array<{ref: ActionRef, jobName: string|null, stepIndex: number, target: 'job'|'step', location: 'job'|'step'|'action-step'}>}
|
|
292
545
|
*/
|
|
293
546
|
export function collectUses(workflow) {
|
|
294
547
|
const out = [];
|
|
295
|
-
for (const job of workflow.jobs
|
|
548
|
+
for (const job of workflow.jobs) {
|
|
296
549
|
if (job.uses) {
|
|
297
|
-
out.push({
|
|
550
|
+
out.push({
|
|
551
|
+
ref: job.uses,
|
|
552
|
+
jobName: job.name,
|
|
553
|
+
stepIndex: -1,
|
|
554
|
+
target: 'job',
|
|
555
|
+
location: 'job',
|
|
556
|
+
});
|
|
298
557
|
}
|
|
299
|
-
|
|
300
|
-
if (step.uses)
|
|
558
|
+
job.steps.forEach((step, i) => {
|
|
559
|
+
if (!step.uses) return;
|
|
560
|
+
const composite = workflow.kind === 'composite-action';
|
|
561
|
+
out.push({
|
|
562
|
+
ref: step.uses,
|
|
563
|
+
jobName: composite ? null : job.name,
|
|
564
|
+
stepIndex: i,
|
|
565
|
+
target: 'step',
|
|
566
|
+
location: composite ? 'action-step' : 'step',
|
|
567
|
+
});
|
|
301
568
|
});
|
|
302
569
|
}
|
|
303
|
-
(workflow.actionSteps ?? []).forEach((step, i) => {
|
|
304
|
-
if (step.uses) out.push({ ref: step.uses, jobName: null, stepIndex: i, location: 'action-step' });
|
|
305
|
-
});
|
|
306
570
|
return out;
|
|
307
571
|
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Return container images referenced by jobs, services, and Docker actions.
|
|
575
|
+
*
|
|
576
|
+
* @param {WorkflowDoc} workflow
|
|
577
|
+
* @returns {ImageRef[]}
|
|
578
|
+
*/
|
|
579
|
+
export function collectImages(workflow) {
|
|
580
|
+
return [...workflow.images];
|
|
581
|
+
}
|