@intentius/chant-lexicon-github 0.124.31 → 0.124.32
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/dist/action-pins.js +53 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +142 -0
- package/dist/codegen/generate-cli.js +26 -0
- package/dist/codegen/generate-lexicon.js +36 -0
- package/dist/codegen/generate-typescript.js +108 -0
- package/dist/codegen/generate.js +111 -0
- package/dist/codegen/naming.js +49 -0
- package/dist/codegen/package.js +40 -0
- package/dist/codegen/parse.js +595 -0
- package/dist/codegen/patches.js +36 -0
- package/dist/components/ci-jobs.js +84 -0
- package/dist/components/generate-op-pipeline.js +440 -0
- package/dist/components/generate-op-waves-pipeline.js +158 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +476 -0
- package/dist/composites/cache.js +17 -0
- package/dist/composites/catalog.js +859 -0
- package/dist/composites/checkout.js +25 -0
- package/dist/composites/dependabot.js +43 -0
- package/dist/composites/deploy-environment.js +38 -0
- package/dist/composites/docker-build.js +70 -0
- package/dist/composites/download-artifact.js +18 -0
- package/dist/composites/go-ci.js +62 -0
- package/dist/composites/index.js +15 -0
- package/dist/composites/node-ci.js +44 -0
- package/dist/composites/node-pipeline.js +96 -0
- package/dist/composites/pr-plan-report.js +129 -0
- package/dist/composites/python-ci.js +60 -0
- package/dist/composites/setup-go.js +18 -0
- package/dist/composites/setup-node.js +21 -0
- package/dist/composites/setup-python.js +18 -0
- package/dist/composites/upload-artifact.js +19 -0
- package/dist/coverage.js +32 -0
- package/dist/expression.js +177 -0
- package/dist/generated/index.js +26 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/graph-edges.js +174 -0
- package/dist/import/generator.js +108 -0
- package/dist/import/parser.js +69 -0
- package/dist/index.js +22 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +69 -0
- package/dist/lint/audit-lineage.js +168 -0
- package/dist/lint/post-synth/gha006.js +50 -0
- package/dist/lint/post-synth/gha009.js +35 -0
- package/dist/lint/post-synth/gha011.js +34 -0
- package/dist/lint/post-synth/gha013.js +40 -0
- package/dist/lint/post-synth/gha017.js +25 -0
- package/dist/lint/post-synth/gha018.js +33 -0
- package/dist/lint/post-synth/gha019.js +58 -0
- package/dist/lint/post-synth/gha021.js +42 -0
- package/dist/lint/post-synth/gha022.js +44 -0
- package/dist/lint/post-synth/gha023.js +37 -0
- package/dist/lint/post-synth/gha024.js +32 -0
- package/dist/lint/post-synth/gha025.js +33 -0
- package/dist/lint/post-synth/gha026.js +32 -0
- package/dist/lint/post-synth/gha027.js +46 -0
- package/dist/lint/post-synth/gha028.js +28 -0
- package/dist/lint/post-synth/gha029.js +56 -0
- package/dist/lint/post-synth/gha030.js +35 -0
- package/dist/lint/post-synth/gha031.js +75 -0
- package/dist/lint/post-synth/gha032.js +35 -0
- package/dist/lint/post-synth/gha033.js +39 -0
- package/dist/lint/post-synth/gha034.js +35 -0
- package/dist/lint/post-synth/gha035.js +50 -0
- package/dist/lint/post-synth/gha036.js +37 -0
- package/dist/lint/post-synth/gha037.js +41 -0
- package/dist/lint/post-synth/gha038.js +35 -0
- package/dist/lint/post-synth/gha039.js +34 -0
- package/dist/lint/post-synth/gha040.js +36 -0
- package/dist/lint/post-synth/gha041.js +32 -0
- package/dist/lint/post-synth/gha042.js +33 -0
- package/dist/lint/post-synth/gha043.js +35 -0
- package/dist/lint/post-synth/gha044.js +36 -0
- package/dist/lint/post-synth/gha045.js +33 -0
- package/dist/lint/post-synth/gha046.js +55 -0
- package/dist/lint/post-synth/gha047.js +44 -0
- package/dist/lint/post-synth/gha048.js +34 -0
- package/dist/lint/post-synth/gha049.js +36 -0
- package/dist/lint/post-synth/gha050.js +37 -0
- package/dist/lint/post-synth/gha051.js +38 -0
- package/dist/lint/post-synth/gha052.js +35 -0
- package/dist/lint/post-synth/gha053.js +34 -0
- package/dist/lint/post-synth/gha054.js +35 -0
- package/dist/lint/post-synth/gha055.js +44 -0
- package/dist/lint/post-synth/gha056.js +32 -0
- package/dist/lint/post-synth/gha057.js +35 -0
- package/dist/lint/post-synth/gha058.js +42 -0
- package/dist/lint/post-synth/gha059.js +119 -0
- package/dist/lint/post-synth/gha060.js +129 -0
- package/dist/lint/post-synth/gha061.js +78 -0
- package/dist/lint/post-synth/gha062.js +65 -0
- package/dist/lint/post-synth/gha063.js +56 -0
- package/dist/lint/post-synth/gha064.js +47 -0
- package/dist/lint/post-synth/gha065.js +125 -0
- package/dist/lint/post-synth/gha066.js +40 -0
- package/dist/lint/post-synth/gha067.js +75 -0
- package/dist/lint/post-synth/gha068.js +47 -0
- package/dist/lint/post-synth/gha069.js +99 -0
- package/dist/lint/post-synth/index.js +114 -0
- package/dist/lint/post-synth/yaml-helpers.js +465 -0
- package/dist/lint/rules/data/action-usage-policy.js +16 -0
- package/dist/lint/rules/data/advisory-feed.js +15 -0
- package/dist/lint/rules/data/deprecated-versions.js +14 -0
- package/dist/lint/rules/data/flagged-actions.js +18 -0
- package/dist/lint/rules/data/known-action-slugs.js +38 -0
- package/dist/lint/rules/data/known-actions.js +12 -0
- package/dist/lint/rules/data/preinstalled-tools.js +36 -0
- package/dist/lint/rules/data/recommended-inputs.js +9 -0
- package/dist/lint/rules/data/risky-features.js +22 -0
- package/dist/lint/rules/data/secret-patterns.js +30 -0
- package/dist/lint/rules/data/trusted-action-owners.js +12 -0
- package/dist/lint/rules/data/untrusted-contexts.js +44 -0
- package/dist/lint/rules/deprecated-action-version.js +44 -0
- package/dist/lint/rules/detect-secrets.js +45 -0
- package/dist/lint/rules/extract-inline-structs.js +55 -0
- package/dist/lint/rules/file-job-limit.js +43 -0
- package/dist/lint/rules/index.js +16 -0
- package/dist/lint/rules/job-timeout.js +53 -0
- package/dist/lint/rules/missing-recommended-inputs.js +51 -0
- package/dist/lint/rules/no-hardcoded-secrets.js +39 -0
- package/dist/lint/rules/no-raw-expressions.js +44 -0
- package/dist/lint/rules/suggest-cache.js +60 -0
- package/dist/lint/rules/use-condition-builders.js +37 -0
- package/dist/lint/rules/use-matrix-builder.js +36 -0
- package/dist/lint/rules/use-typed-actions.js +41 -0
- package/dist/lint/rules/validate-concurrency.js +59 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +33 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +152 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +344 -0
- package/dist/serializer.js +572 -0
- package/dist/spec/fetch.js +46 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +38 -0
- package/dist/variables.js +41 -0
- package/package.json +6 -6
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.github.golden.yml +6 -2
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +178 -0
- package/src/components/__fixtures__/pr-loop.github.golden.yml +6 -2
- package/src/components/generate-pipeline.ts +66 -29
- package/src/components/generate-pr-pipeline.test.ts +44 -20
- package/src/components/generate-pr-waves-pipeline.test.ts +117 -0
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA065: Unbounded Matrix Fan-Out
|
|
3
|
+
*
|
|
4
|
+
* Flags a `strategy.matrix` that combines two or more dimensions into more
|
|
5
|
+
* jobs than a `max-parallel:` cap bounds. Two orthogonal axes each with a
|
|
6
|
+
* handful of values multiply, not add — `os` x `node` x `arch` can produce
|
|
7
|
+
* dozens of jobs from a config that reads like a short list. Unbounded, that
|
|
8
|
+
* is capacity spent on a combination nobody asked for, not a correctness or
|
|
9
|
+
* security issue (efficiency, #444).
|
|
10
|
+
*
|
|
11
|
+
* Parses the `matrix:` block by indentation, not the structural YAML parser
|
|
12
|
+
* — a flow list of bare words (`os: [ubuntu-latest, macos-latest]`, the
|
|
13
|
+
* overwhelmingly common hand-written form) is not one it round-trips, only
|
|
14
|
+
* a flow list of numbers. Mirrors GHA009's regex-based matrix handling for
|
|
15
|
+
* the same reason.
|
|
16
|
+
*/
|
|
17
|
+
import { getPrimaryOutput, extractJobs } from "./yaml-helpers.js";
|
|
18
|
+
/** Above this many combinations, an uncapped matrix is flagged. */
|
|
19
|
+
const FANOUT_THRESHOLD = 12;
|
|
20
|
+
/** The raw text of one job's block within `jobs:`, or undefined if not found. */
|
|
21
|
+
function jobSection(yaml, jobName) {
|
|
22
|
+
const jobsIdx = yaml.search(/^jobs:\s*$/m);
|
|
23
|
+
if (jobsIdx === -1)
|
|
24
|
+
return undefined;
|
|
25
|
+
const afterJobs = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
|
|
26
|
+
const header = ` ${jobName}:\n`;
|
|
27
|
+
const start = afterJobs.indexOf(header);
|
|
28
|
+
if (start === -1)
|
|
29
|
+
return undefined;
|
|
30
|
+
const rest = afterJobs.slice(start + header.length);
|
|
31
|
+
const nextJobMatch = rest.search(/\n {2}\S/);
|
|
32
|
+
return nextJobMatch === -1 ? rest : rest.slice(0, nextJobMatch);
|
|
33
|
+
}
|
|
34
|
+
/** Parse a job section's `strategy.matrix` dimensions and whether max-parallel caps it. */
|
|
35
|
+
function parseMatrix(section) {
|
|
36
|
+
const lines = section.split("\n");
|
|
37
|
+
const matrixLineIdx = lines.findIndex((l) => /^\s*matrix:\s*$/.test(l));
|
|
38
|
+
if (matrixLineIdx === -1)
|
|
39
|
+
return undefined;
|
|
40
|
+
const matrixIndent = lines[matrixLineIdx].search(/\S/);
|
|
41
|
+
const maxParallel = lines.some((l) => {
|
|
42
|
+
const m = l.match(/^(\s*)max-parallel:/);
|
|
43
|
+
return !!m && m[1].length === matrixIndent;
|
|
44
|
+
});
|
|
45
|
+
const dims = [];
|
|
46
|
+
let i = matrixLineIdx + 1;
|
|
47
|
+
while (i < lines.length) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
if (line.trim() === "") {
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const indent = line.search(/\S/);
|
|
54
|
+
if (indent <= matrixIndent)
|
|
55
|
+
break; // dedented out of the matrix: block
|
|
56
|
+
const kv = line.match(/^\s*([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
57
|
+
if (!kv) {
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const [, key, rest] = kv;
|
|
62
|
+
const value = rest.trim();
|
|
63
|
+
if (value.startsWith("[")) {
|
|
64
|
+
const inner = value.replace(/^\[|\]\s*$/g, "").trim();
|
|
65
|
+
dims.push({ key, size: inner === "" ? 0 : inner.split(",").length });
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (value !== "") {
|
|
70
|
+
dims.push({ key, size: 1 }); // a bare scalar dimension
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Block-list value on subsequent, deeper-indented lines.
|
|
75
|
+
let size = 0;
|
|
76
|
+
i++;
|
|
77
|
+
while (i < lines.length) {
|
|
78
|
+
const l2 = lines[i];
|
|
79
|
+
if (l2.trim() === "") {
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const ind2 = l2.search(/\S/);
|
|
84
|
+
if (ind2 <= indent)
|
|
85
|
+
break;
|
|
86
|
+
if (/^\s*-\s/.test(l2))
|
|
87
|
+
size++;
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
dims.push({ key, size });
|
|
91
|
+
}
|
|
92
|
+
return { dims, maxParallel };
|
|
93
|
+
}
|
|
94
|
+
export const gha065 = {
|
|
95
|
+
id: "GHA065",
|
|
96
|
+
description: "Matrix combines dimensions into an uncapped, large fan-out",
|
|
97
|
+
check(ctx) {
|
|
98
|
+
const diagnostics = [];
|
|
99
|
+
for (const [, output] of ctx.outputs) {
|
|
100
|
+
const yaml = getPrimaryOutput(output);
|
|
101
|
+
for (const [jobName] of extractJobs(yaml)) {
|
|
102
|
+
const section = jobSection(yaml, jobName);
|
|
103
|
+
if (!section)
|
|
104
|
+
continue;
|
|
105
|
+
const matrix = parseMatrix(section);
|
|
106
|
+
if (!matrix)
|
|
107
|
+
continue;
|
|
108
|
+
const dims = matrix.dims.filter((d) => d.key !== "include" && d.key !== "exclude" && d.size > 0);
|
|
109
|
+
if (dims.length < 2)
|
|
110
|
+
continue; // a single axis isn't "combined" fan-out
|
|
111
|
+
const combos = dims.reduce((a, d) => a * d.size, 1);
|
|
112
|
+
if (combos <= FANOUT_THRESHOLD || matrix.maxParallel)
|
|
113
|
+
continue;
|
|
114
|
+
diagnostics.push({
|
|
115
|
+
checkId: "GHA065",
|
|
116
|
+
severity: "info",
|
|
117
|
+
message: `Job "${jobName}"'s matrix combines ${dims.length} dimensions (${dims.map((d) => d.key).join(" x ")}) into ${combos} jobs with no \`max-parallel:\` cap. Confirm the full cross-product is intended, trim it with \`include\`/\`exclude\`, or add \`max-parallel:\`.`,
|
|
118
|
+
entity: jobName,
|
|
119
|
+
lexicon: "github",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return diagnostics;
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA066: Unbounded Artifact Retention
|
|
3
|
+
*
|
|
4
|
+
* Flags an `actions/upload-artifact` step with no `retention-days`, which
|
|
5
|
+
* falls back to the repository's default (up to 90 days). Build logs, test
|
|
6
|
+
* output, and intermediate artifacts rarely need to outlive the PR they came
|
|
7
|
+
* from; an unset retention silently accumulates storage. Efficiency (#444),
|
|
8
|
+
* not a correctness or security issue.
|
|
9
|
+
*/
|
|
10
|
+
import { getPrimaryOutput, jobEntries, parseActionUses } from "./yaml-helpers.js";
|
|
11
|
+
export const gha066 = {
|
|
12
|
+
id: "GHA066",
|
|
13
|
+
description: "Uploaded artifact has no explicit retention-days",
|
|
14
|
+
check(ctx) {
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
for (const [, output] of ctx.outputs) {
|
|
17
|
+
const yaml = getPrimaryOutput(output);
|
|
18
|
+
for (const [jobName, jobObj] of jobEntries(yaml)) {
|
|
19
|
+
const steps = Array.isArray(jobObj.steps) ? jobObj.steps : [];
|
|
20
|
+
for (const step of steps) {
|
|
21
|
+
const uses = typeof step.uses === "string" ? step.uses : undefined;
|
|
22
|
+
if (!uses || parseActionUses(uses)?.slug !== "actions/upload-artifact")
|
|
23
|
+
continue;
|
|
24
|
+
const withBlock = step.with;
|
|
25
|
+
if (withBlock && withBlock["retention-days"] !== undefined)
|
|
26
|
+
continue;
|
|
27
|
+
const name = typeof withBlock?.name === "string" ? withBlock.name : undefined;
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "GHA066",
|
|
30
|
+
severity: "info",
|
|
31
|
+
message: `Job "${jobName}" uploads${name ? ` artifact "${name}"` : " an artifact"} with no \`retention-days\` — it falls back to the repository's default (up to 90 days). Set a \`retention-days\` sized to how long the artifact is actually needed.`,
|
|
32
|
+
entity: jobName,
|
|
33
|
+
lexicon: "github",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return diagnostics;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA067: Unconditional Heavy Step With No Path Filter
|
|
3
|
+
*
|
|
4
|
+
* Flags a `docker build`/`docker buildx build` step that has no `if:` guard,
|
|
5
|
+
* in a workflow that triggers on `push`/`pull_request` with no `paths:` or
|
|
6
|
+
* `paths-ignore:` filter. A docs-only or unrelated change still pays for a
|
|
7
|
+
* full image build because nothing scopes the trigger or guards the step.
|
|
8
|
+
* Efficiency (#444), not a correctness or security issue.
|
|
9
|
+
*/
|
|
10
|
+
import { getPrimaryOutput, parseDoc, jobEntries } from "./yaml-helpers.js";
|
|
11
|
+
const DOCKER_BUILD_RE = /\bdocker(\s+buildx)?\s+build\b/i;
|
|
12
|
+
function triggerObject(on, key) {
|
|
13
|
+
if (!on || typeof on !== "object" || Array.isArray(on))
|
|
14
|
+
return undefined;
|
|
15
|
+
const trig = on[key];
|
|
16
|
+
return trig && typeof trig === "object" && !Array.isArray(trig) ? trig : undefined;
|
|
17
|
+
}
|
|
18
|
+
function hasPushOrPRTrigger(on) {
|
|
19
|
+
if (typeof on === "string")
|
|
20
|
+
return on === "push" || on === "pull_request";
|
|
21
|
+
if (Array.isArray(on))
|
|
22
|
+
return on.includes("push") || on.includes("pull_request");
|
|
23
|
+
if (on && typeof on === "object") {
|
|
24
|
+
const keys = Object.keys(on);
|
|
25
|
+
return keys.includes("push") || keys.includes("pull_request");
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function hasPathFilter(on) {
|
|
30
|
+
for (const key of ["push", "pull_request"]) {
|
|
31
|
+
const trig = triggerObject(on, key);
|
|
32
|
+
if (!trig)
|
|
33
|
+
continue;
|
|
34
|
+
if (Array.isArray(trig.paths) && trig.paths.length > 0)
|
|
35
|
+
return true;
|
|
36
|
+
if (Array.isArray(trig["paths-ignore"]) && trig["paths-ignore"].length > 0)
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
export const gha067 = {
|
|
42
|
+
id: "GHA067",
|
|
43
|
+
description: "Unconditional docker build with no path filter or guard",
|
|
44
|
+
check(ctx) {
|
|
45
|
+
const diagnostics = [];
|
|
46
|
+
for (const [, output] of ctx.outputs) {
|
|
47
|
+
const yaml = getPrimaryOutput(output);
|
|
48
|
+
const doc = parseDoc(yaml);
|
|
49
|
+
if (!doc)
|
|
50
|
+
continue;
|
|
51
|
+
if (!hasPushOrPRTrigger(doc.on))
|
|
52
|
+
continue;
|
|
53
|
+
if (hasPathFilter(doc.on))
|
|
54
|
+
continue;
|
|
55
|
+
for (const [jobName, jobObj] of jobEntries(yaml)) {
|
|
56
|
+
const steps = Array.isArray(jobObj.steps) ? jobObj.steps : [];
|
|
57
|
+
for (const step of steps) {
|
|
58
|
+
const run = typeof step.run === "string" ? step.run : undefined;
|
|
59
|
+
if (!run || !DOCKER_BUILD_RE.test(run))
|
|
60
|
+
continue;
|
|
61
|
+
if (step.if !== undefined)
|
|
62
|
+
continue; // already guarded
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
checkId: "GHA067",
|
|
65
|
+
severity: "info",
|
|
66
|
+
message: `Job "${jobName}" runs a Docker build on every push/pull_request with no \`paths:\`/\`paths-ignore:\` filter and no \`if:\` guard — unrelated changes (docs, etc.) still pay for a full image build. Scope the trigger's paths or add a guard.`,
|
|
67
|
+
entity: jobName,
|
|
68
|
+
lexicon: "github",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return diagnostics;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA068: Pull-Request Workflow Missing a Concurrency Group
|
|
3
|
+
*
|
|
4
|
+
* Flags a workflow triggered on `pull_request` with no top-level
|
|
5
|
+
* `concurrency:` block. Without one, a superseded push keeps its old run
|
|
6
|
+
* going instead of cancelling it, so every commit on a PR pays for the full
|
|
7
|
+
* pipeline instead of just the latest. Deploy workflows are left to GHA024,
|
|
8
|
+
* which already covers them (overlapping-deployment risk, not capacity).
|
|
9
|
+
* Efficiency (#444), not a correctness or security issue.
|
|
10
|
+
*/
|
|
11
|
+
import { getPrimaryOutput, parseDoc, extractJobs, extractWorkflowName } from "./yaml-helpers.js";
|
|
12
|
+
function hasPullRequestTrigger(on) {
|
|
13
|
+
if (typeof on === "string")
|
|
14
|
+
return on === "pull_request";
|
|
15
|
+
if (Array.isArray(on))
|
|
16
|
+
return on.includes("pull_request");
|
|
17
|
+
if (on && typeof on === "object")
|
|
18
|
+
return "pull_request" in on;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export const gha068 = {
|
|
22
|
+
id: "GHA068",
|
|
23
|
+
description: "Pull-request workflow missing a concurrency group",
|
|
24
|
+
check(ctx) {
|
|
25
|
+
const diagnostics = [];
|
|
26
|
+
for (const [, output] of ctx.outputs) {
|
|
27
|
+
const yaml = getPrimaryOutput(output);
|
|
28
|
+
const doc = parseDoc(yaml);
|
|
29
|
+
if (!doc || !hasPullRequestTrigger(doc.on))
|
|
30
|
+
continue;
|
|
31
|
+
const workflowName = extractWorkflowName(yaml) ?? "";
|
|
32
|
+
const jobNames = [...extractJobs(yaml).keys()];
|
|
33
|
+
const isDeployWorkflow = /deploy/i.test(workflowName) || jobNames.some((n) => /deploy/i.test(n));
|
|
34
|
+
if (isDeployWorkflow)
|
|
35
|
+
continue; // GHA024 already covers deploy workflows
|
|
36
|
+
if (!/^\s*concurrency:/m.test(yaml)) {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
checkId: "GHA068",
|
|
39
|
+
severity: "info",
|
|
40
|
+
message: `Workflow${workflowName ? ` "${workflowName}"` : ""} triggers on pull_request with no \`concurrency:\` group — a new push doesn't cancel the superseded run, so every commit on the PR consumes a full run's worth of runner capacity. Add a \`concurrency:\` group with \`cancel-in-progress: true\`.`,
|
|
41
|
+
lexicon: "github",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return diagnostics;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA069: Job-Level Permissions Block Drops the Workflow's `id-token: write`
|
|
3
|
+
*
|
|
4
|
+
* A job-level `permissions:` block replaces the workflow-level one; GitHub
|
|
5
|
+
* does not merge the two. A workflow that grants `id-token: write` at the top
|
|
6
|
+
* and then gives a job its own block without that scope leaves the job with
|
|
7
|
+
* no OIDC token, and the job's cloud-credential action fails with an error
|
|
8
|
+
* that names neither the scope nor the job block (#2273).
|
|
9
|
+
*
|
|
10
|
+
* Fires when all three hold:
|
|
11
|
+
* - the workflow-level block grants `id-token: write` (directly, or through
|
|
12
|
+
* `write-all`);
|
|
13
|
+
* - a job declares its own block that does not (a map without
|
|
14
|
+
* `id-token: write`, `{}`, `read-all`, or any other preset but `write-all`);
|
|
15
|
+
* - that job has a step using a known OIDC credential action, matched on the
|
|
16
|
+
* action path whatever the ref (tag, major or full SHA), with no static
|
|
17
|
+
* credential input that would make the step skip OIDC.
|
|
18
|
+
*
|
|
19
|
+
* Only `id-token` is checked. The reasoning for not generalising to every
|
|
20
|
+
* scope a job block narrows is recorded on #2273.
|
|
21
|
+
*/
|
|
22
|
+
import { getPrimaryOutput, extractWorkflowPermissions, extractJobPermissions, extractStepsByJob, parseActionUses, } from "./yaml-helpers.js";
|
|
23
|
+
/**
|
|
24
|
+
* Actions that exchange the job's OIDC token for cloud credentials, keyed by
|
|
25
|
+
* lowercased `owner/repo`, each with the inputs that select a static
|
|
26
|
+
* credential instead (when one is set, the step does not need `id-token`).
|
|
27
|
+
*/
|
|
28
|
+
export const OIDC_CREDENTIAL_ACTIONS = new Map([
|
|
29
|
+
["aws-actions/configure-aws-credentials", ["aws-access-key-id", "web-identity-token-file"]],
|
|
30
|
+
["azure/login", ["creds"]],
|
|
31
|
+
["google-github-actions/auth", ["credentials_json"]],
|
|
32
|
+
]);
|
|
33
|
+
/** True when the permissions value grants `id-token: write`. */
|
|
34
|
+
export function grantsIdTokenWrite(perms) {
|
|
35
|
+
if (typeof perms === "string")
|
|
36
|
+
return perms.trim() === "write-all";
|
|
37
|
+
return perms["id-token"] === "write";
|
|
38
|
+
}
|
|
39
|
+
/** The OIDC credential action a step uses, as written, or undefined. */
|
|
40
|
+
function oidcActionOf(step) {
|
|
41
|
+
if (typeof step.uses !== "string")
|
|
42
|
+
return undefined;
|
|
43
|
+
const parsed = parseActionUses(step.uses);
|
|
44
|
+
if (!parsed)
|
|
45
|
+
return undefined;
|
|
46
|
+
const staticInputs = OIDC_CREDENTIAL_ACTIONS.get(parsed.slug.toLowerCase());
|
|
47
|
+
if (!staticInputs)
|
|
48
|
+
return undefined;
|
|
49
|
+
const withInputs = step.with;
|
|
50
|
+
if (withInputs && typeof withInputs === "object" && !Array.isArray(withInputs)) {
|
|
51
|
+
const inputs = withInputs;
|
|
52
|
+
if (staticInputs.some((k) => inputs[k] !== undefined && inputs[k] !== null && inputs[k] !== ""))
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return parsed.slug;
|
|
56
|
+
}
|
|
57
|
+
function describeJobBlock(perms) {
|
|
58
|
+
if (typeof perms === "string")
|
|
59
|
+
return `\`permissions: ${perms}\``;
|
|
60
|
+
if (Object.keys(perms).length === 0)
|
|
61
|
+
return "`permissions: {}`";
|
|
62
|
+
return "its own `permissions:` block";
|
|
63
|
+
}
|
|
64
|
+
export const gha069 = {
|
|
65
|
+
id: "GHA069",
|
|
66
|
+
description: "Job-level permissions block drops the workflow's id-token: write that an OIDC credential step needs",
|
|
67
|
+
check(ctx) {
|
|
68
|
+
const diagnostics = [];
|
|
69
|
+
for (const [, output] of ctx.outputs) {
|
|
70
|
+
const yaml = getPrimaryOutput(output);
|
|
71
|
+
const wf = extractWorkflowPermissions(yaml);
|
|
72
|
+
if (!wf || !grantsIdTokenWrite(wf))
|
|
73
|
+
continue;
|
|
74
|
+
const jobPermissions = extractJobPermissions(yaml);
|
|
75
|
+
if (jobPermissions.size === 0)
|
|
76
|
+
continue;
|
|
77
|
+
const stepsByJob = extractStepsByJob(yaml);
|
|
78
|
+
for (const [job, perms] of jobPermissions) {
|
|
79
|
+
if (grantsIdTokenWrite(perms))
|
|
80
|
+
continue;
|
|
81
|
+
const actions = [
|
|
82
|
+
...new Set((stepsByJob.get(job) ?? []).map(oidcActionOf).filter((a) => a !== undefined)),
|
|
83
|
+
];
|
|
84
|
+
if (actions.length === 0)
|
|
85
|
+
continue;
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
checkId: "GHA069",
|
|
88
|
+
severity: "warning",
|
|
89
|
+
message: `Job "${job}" declares ${describeJobBlock(perms)}, which replaces the workflow-level block instead of merging with it, ` +
|
|
90
|
+
`so the workflow's \`id-token: write\` does not reach the job and ${actions.join(", ")} cannot request an OIDC token. ` +
|
|
91
|
+
`Add \`id-token: write\` to job "${job}"'s permissions block; keep the job block rather than removing it.`,
|
|
92
|
+
entity: job,
|
|
93
|
+
lexicon: "github",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return diagnostics;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { gha006 } from "./gha006.js";
|
|
2
|
+
import { gha009 } from "./gha009.js";
|
|
3
|
+
import { gha011 } from "./gha011.js";
|
|
4
|
+
import { gha013 } from "./gha013.js";
|
|
5
|
+
import { gha017 } from "./gha017.js";
|
|
6
|
+
import { gha018 } from "./gha018.js";
|
|
7
|
+
import { gha019 } from "./gha019.js";
|
|
8
|
+
import { gha021 } from "./gha021.js";
|
|
9
|
+
import { gha022 } from "./gha022.js";
|
|
10
|
+
import { gha023 } from "./gha023.js";
|
|
11
|
+
import { gha024 } from "./gha024.js";
|
|
12
|
+
import { gha025 } from "./gha025.js";
|
|
13
|
+
import { gha026 } from "./gha026.js";
|
|
14
|
+
import { gha027 } from "./gha027.js";
|
|
15
|
+
import { gha028 } from "./gha028.js";
|
|
16
|
+
import { gha029 } from "./gha029.js";
|
|
17
|
+
import { gha030 } from "./gha030.js";
|
|
18
|
+
import { gha031 } from "./gha031.js";
|
|
19
|
+
import { gha032 } from "./gha032.js";
|
|
20
|
+
import { gha033 } from "./gha033.js";
|
|
21
|
+
import { gha034 } from "./gha034.js";
|
|
22
|
+
import { gha035 } from "./gha035.js";
|
|
23
|
+
import { gha036 } from "./gha036.js";
|
|
24
|
+
import { gha037 } from "./gha037.js";
|
|
25
|
+
import { gha038 } from "./gha038.js";
|
|
26
|
+
import { gha039 } from "./gha039.js";
|
|
27
|
+
import { gha040 } from "./gha040.js";
|
|
28
|
+
import { gha041 } from "./gha041.js";
|
|
29
|
+
import { gha042 } from "./gha042.js";
|
|
30
|
+
import { gha043 } from "./gha043.js";
|
|
31
|
+
import { gha044 } from "./gha044.js";
|
|
32
|
+
import { gha045 } from "./gha045.js";
|
|
33
|
+
import { gha046 } from "./gha046.js";
|
|
34
|
+
import { gha047 } from "./gha047.js";
|
|
35
|
+
import { gha048 } from "./gha048.js";
|
|
36
|
+
import { gha049 } from "./gha049.js";
|
|
37
|
+
import { gha050 } from "./gha050.js";
|
|
38
|
+
import { gha051 } from "./gha051.js";
|
|
39
|
+
import { gha052 } from "./gha052.js";
|
|
40
|
+
import { gha053 } from "./gha053.js";
|
|
41
|
+
import { gha054 } from "./gha054.js";
|
|
42
|
+
import { gha055 } from "./gha055.js";
|
|
43
|
+
import { gha056 } from "./gha056.js";
|
|
44
|
+
import { gha057 } from "./gha057.js";
|
|
45
|
+
import { gha058 } from "./gha058.js";
|
|
46
|
+
import { gha059 } from "./gha059.js";
|
|
47
|
+
import { gha060 } from "./gha060.js";
|
|
48
|
+
import { gha061 } from "./gha061.js";
|
|
49
|
+
import { gha062 } from "./gha062.js";
|
|
50
|
+
import { gha063 } from "./gha063.js";
|
|
51
|
+
import { gha064 } from "./gha064.js";
|
|
52
|
+
import { gha065 } from "./gha065.js";
|
|
53
|
+
import { gha066 } from "./gha066.js";
|
|
54
|
+
import { gha067 } from "./gha067.js";
|
|
55
|
+
import { gha068 } from "./gha068.js";
|
|
56
|
+
import { gha069 } from "./gha069.js";
|
|
57
|
+
export const postSynthChecks = [
|
|
58
|
+
gha006,
|
|
59
|
+
gha009,
|
|
60
|
+
gha011,
|
|
61
|
+
gha013,
|
|
62
|
+
gha017,
|
|
63
|
+
gha018,
|
|
64
|
+
gha019,
|
|
65
|
+
gha021,
|
|
66
|
+
gha022,
|
|
67
|
+
gha023,
|
|
68
|
+
gha024,
|
|
69
|
+
gha025,
|
|
70
|
+
gha026,
|
|
71
|
+
gha027,
|
|
72
|
+
gha028,
|
|
73
|
+
gha029,
|
|
74
|
+
gha030,
|
|
75
|
+
gha031,
|
|
76
|
+
gha032,
|
|
77
|
+
gha033,
|
|
78
|
+
gha034,
|
|
79
|
+
gha035,
|
|
80
|
+
gha036,
|
|
81
|
+
gha037,
|
|
82
|
+
gha038,
|
|
83
|
+
gha039,
|
|
84
|
+
gha040,
|
|
85
|
+
gha041,
|
|
86
|
+
gha042,
|
|
87
|
+
gha043,
|
|
88
|
+
gha044,
|
|
89
|
+
gha045,
|
|
90
|
+
gha046,
|
|
91
|
+
gha047,
|
|
92
|
+
gha048,
|
|
93
|
+
gha049,
|
|
94
|
+
gha050,
|
|
95
|
+
gha051,
|
|
96
|
+
gha052,
|
|
97
|
+
gha053,
|
|
98
|
+
gha054,
|
|
99
|
+
gha055,
|
|
100
|
+
gha056,
|
|
101
|
+
gha057,
|
|
102
|
+
gha058,
|
|
103
|
+
gha059,
|
|
104
|
+
gha060,
|
|
105
|
+
gha061,
|
|
106
|
+
gha062,
|
|
107
|
+
gha063,
|
|
108
|
+
gha064,
|
|
109
|
+
gha065,
|
|
110
|
+
gha066,
|
|
111
|
+
gha067,
|
|
112
|
+
gha068,
|
|
113
|
+
gha069,
|
|
114
|
+
];
|