@intentius/chant-lexicon-github 0.101.0 → 0.102.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/dist/action-pins.d.ts +53 -0
- package/dist/action-pins.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/components/generate-op-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/composites/checkout.d.ts +7 -0
- package/dist/composites/checkout.d.ts.map +1 -1
- package/dist/composites/dependabot.d.ts.map +1 -1
- package/dist/composites/deploy-environment.d.ts.map +1 -1
- package/dist/composites/docker-build.d.ts.map +1 -1
- package/dist/composites/go-ci.d.ts.map +1 -1
- package/dist/composites/node-ci.d.ts.map +1 -1
- package/dist/composites/node-pipeline.d.ts +30 -30
- package/dist/composites/node-pipeline.d.ts.map +1 -1
- package/dist/composites/python-ci.d.ts.map +1 -1
- package/dist/composites/setup-node.d.ts +7 -0
- package/dist/composites/setup-node.d.ts.map +1 -1
- package/dist/expression.d.ts.map +1 -1
- package/dist/generated/index.d.ts +22 -22
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +7 -6
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +13 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/gha021.d.ts.map +1 -1
- package/dist/lint/post-synth/gha029.d.ts.map +1 -1
- package/dist/lint/post-synth/gha059.d.ts.map +1 -1
- package/dist/lint/post-synth/gha069.d.ts +33 -0
- package/dist/lint/post-synth/gha069.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/yaml-helpers.d.ts +20 -0
- package/dist/lint/post-synth/yaml-helpers.d.ts.map +1 -1
- package/dist/lint/rules/data/deprecated-versions.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/GHA069.md +16 -0
- package/dist/okf/types/Job.md +1 -0
- package/dist/okf/types/Workflow.md +1 -0
- package/dist/rules/gha021.ts +24 -24
- package/dist/rules/gha029.ts +3 -2
- package/dist/rules/gha059.ts +10 -2
- package/dist/rules/gha069.ts +111 -0
- package/dist/rules/yaml-helpers.ts +41 -47
- package/dist/variables.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/action-pins.test.ts +87 -0
- package/src/action-pins.ts +75 -0
- package/src/codegen/docs.ts +1 -1
- package/src/components/generate-op-pipeline.test.ts +4 -4
- package/src/components/generate-op-pipeline.ts +8 -7
- package/src/components/generate-pipeline.test.ts +1 -1
- package/src/components/generate-pipeline.ts +8 -14
- package/src/composites/checkout.ts +8 -1
- package/src/composites/composites.test.ts +4 -4
- package/src/composites/deploy-environment.ts +3 -2
- package/src/composites/docker-build.ts +2 -1
- package/src/composites/go-ci.ts +4 -3
- package/src/composites/node-ci.ts +3 -2
- package/src/composites/node-pipeline.ts +8 -7
- package/src/composites/python-ci.ts +3 -2
- package/src/composites/setup-node.ts +8 -1
- package/src/index.ts +4 -0
- package/src/lint/audit-catalog.ts +1 -0
- package/src/lint/audit-lineage.ts +13 -0
- package/src/lint/post-synth/fixtures/GHA069/negative-no-job-block.yml +16 -0
- package/src/lint/post-synth/fixtures/GHA069/negative-no-oidc-step.yml +22 -0
- package/src/lint/post-synth/fixtures/GHA069/negative.yml +21 -0
- package/src/lint/post-synth/fixtures/GHA069/positive.yml +20 -0
- package/src/lint/post-synth/gha011.test.ts +16 -0
- package/src/lint/post-synth/gha018.test.ts +18 -0
- package/src/lint/post-synth/gha019.test.ts +42 -0
- package/src/lint/post-synth/gha021.test.ts +18 -0
- package/src/lint/post-synth/gha021.ts +24 -24
- package/src/lint/post-synth/gha022.test.ts +15 -0
- package/src/lint/post-synth/gha023.test.ts +19 -0
- package/src/lint/post-synth/gha029.ts +3 -2
- package/src/lint/post-synth/gha038.test.ts +20 -0
- package/src/lint/post-synth/gha059.ts +10 -2
- package/src/lint/post-synth/gha069.test.ts +173 -0
- package/src/lint/post-synth/gha069.ts +111 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/yaml-helpers.test.ts +83 -0
- package/src/lint/post-synth/yaml-helpers.ts +41 -47
- package/src/lint/rules/data/deprecated-versions.ts +5 -2
- package/src/lint/rules/deprecated-action-version.test.ts +1 -1
- package/src/lint/rules/rules.test.ts +1 -1
- package/src/mcp/context-tools.test.ts +17 -0
- package/src/plugin.test.ts +1 -1
- package/src/serializer.ts +15 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yaml-helpers.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/yaml-helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEpE;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAGvF;AAED,iEAAiE;AACjE,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAS/F;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED
|
|
1
|
+
{"version":3,"file":"yaml-helpers.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/yaml-helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deprecated-versions.d.ts","sourceRoot":"","sources":["../../../../src/lint/rules/data/deprecated-versions.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"deprecated-versions.d.ts","sourceRoot":"","sources":["../../../../src/lint/rules/data/deprecated-versions.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAS5F,CAAC"}
|
package/dist/manifest.json
CHANGED
package/dist/okf/index.md
CHANGED
|
@@ -78,3 +78,4 @@ okf_version: '0.2'
|
|
|
78
78
|
* [GHA066](/rules/GHA066.md) - Uploaded artifact has no explicit retention-days
|
|
79
79
|
* [GHA067](/rules/GHA067.md) - Unconditional docker build with no path filter or guard
|
|
80
80
|
* [GHA068](/rules/GHA068.md) - Pull-request workflow missing a concurrency group
|
|
81
|
+
* [GHA069](/rules/GHA069.md) - Job-level permissions block drops the workflow's id-token: write that an OIDC credential step needs
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: post-synth-check
|
|
3
|
+
title: GHA069
|
|
4
|
+
description: 'Job-level permissions block drops the workflow''s id-token: write that an OIDC credential step needs'
|
|
5
|
+
id: GHA069
|
|
6
|
+
severity: error
|
|
7
|
+
category: post-synth
|
|
8
|
+
lexicon: github
|
|
9
|
+
docs: https://intentius.io/chant/lexicons/github/rules/
|
|
10
|
+
---
|
|
11
|
+
Job-level permissions block drops the workflow's id-token: write that an OIDC credential step needs
|
|
12
|
+
|
|
13
|
+
## Applies to
|
|
14
|
+
|
|
15
|
+
- [Job](/types/Job.md)
|
|
16
|
+
- [Workflow](/types/Workflow.md)
|
package/dist/okf/types/Job.md
CHANGED
|
@@ -77,3 +77,4 @@ resource_type: GitHub::Actions::Job
|
|
|
77
77
|
- [GHA065](/rules/GHA065.md): Matrix combines dimensions into an uncapped, large fan-out
|
|
78
78
|
- [GHA066](/rules/GHA066.md): Uploaded artifact has no explicit retention-days
|
|
79
79
|
- [GHA067](/rules/GHA067.md): Unconditional docker build with no path filter or guard
|
|
80
|
+
- [GHA069](/rules/GHA069.md): Job-level permissions block drops the workflow's id-token: write that an OIDC credential step needs
|
|
@@ -36,3 +36,4 @@ resource_type: GitHub::Actions::Workflow
|
|
|
36
36
|
- [GHA053](/rules/GHA053.md): Re-enables unsafe set-env / add-path workflow commands
|
|
37
37
|
- [GHA056](/rules/GHA056.md): Workflow without a name
|
|
38
38
|
- [GHA068](/rules/GHA068.md): Pull-request workflow missing a concurrency group
|
|
39
|
+
- [GHA069](/rules/GHA069.md): Job-level permissions block drops the workflow's id-token: write that an OIDC credential step needs
|
package/dist/rules/gha021.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
-
import { getPrimaryOutput,
|
|
9
|
+
import { getPrimaryOutput, extractActionRefs, stripUsesComment } from "./yaml-helpers";
|
|
10
|
+
import { pinFixHint } from "../../action-pins";
|
|
10
11
|
|
|
11
12
|
export const gha021: PostSynthCheck = {
|
|
12
13
|
id: "GHA021",
|
|
@@ -17,29 +18,28 @@ export const gha021: PostSynthCheck = {
|
|
|
17
18
|
|
|
18
19
|
for (const [, output] of ctx.outputs) {
|
|
19
20
|
const yaml = getPrimaryOutput(output);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
21
|
+
// The structural parser sees every step. The line-based extractJobs
|
|
22
|
+
// only reads a step's first line, so it missed `uses:` whenever a step
|
|
23
|
+
// starts with `name:`, which is how every composite emits it.
|
|
24
|
+
for (const { job: jobName, ref: uses, level } of extractActionRefs(yaml)) {
|
|
25
|
+
if (level !== "step") continue;
|
|
26
|
+
|
|
27
|
+
// A pinned ref carries its version as a trailing comment
|
|
28
|
+
// (`actions/checkout@<sha> # v7.0.1`); only the ref is judged.
|
|
29
|
+
const match = stripUsesComment(uses).match(/^actions\/checkout@(.+)$/);
|
|
30
|
+
if (!match) continue;
|
|
31
|
+
|
|
32
|
+
const ref = match[1];
|
|
33
|
+
// A pinned SHA is 40 hex characters
|
|
34
|
+
if (/^[0-9a-f]{40}$/.test(ref)) continue;
|
|
35
|
+
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
checkId: "GHA021",
|
|
38
|
+
severity: "warning",
|
|
39
|
+
message: `Job "${jobName}" uses actions/checkout@${ref} — pin to a full commit SHA for supply-chain security.${pinFixHint("actions/checkout")}`,
|
|
40
|
+
entity: jobName,
|
|
41
|
+
lexicon: "github",
|
|
42
|
+
});
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
package/dist/rules/gha029.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
15
15
|
import { getPrimaryOutput, extractActionRefs, parseActionUses } from "./yaml-helpers";
|
|
16
16
|
import { TRUSTED_ACTION_OWNERS } from "../rules/data/trusted-action-owners";
|
|
17
|
+
import { pinFixHint } from "../../action-pins";
|
|
17
18
|
|
|
18
19
|
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
19
20
|
|
|
@@ -52,11 +53,11 @@ export const gha029: PostSynthCheck = {
|
|
|
52
53
|
|
|
53
54
|
for (const [, output] of ctx.outputs) {
|
|
54
55
|
const yaml = getPrimaryOutput(output);
|
|
55
|
-
for (const { job, ref } of findUnpinnedActions(yaml)) {
|
|
56
|
+
for (const { job, ref, slug } of findUnpinnedActions(yaml)) {
|
|
56
57
|
diagnostics.push({
|
|
57
58
|
checkId: "GHA029",
|
|
58
59
|
severity: "warning",
|
|
59
|
-
message: `Job "${job}" uses ${ref} pinned to a tag or branch — pin to a full commit SHA for supply-chain security
|
|
60
|
+
message: `Job "${job}" uses ${ref} pinned to a tag or branch — pin to a full commit SHA for supply-chain security.${pinFixHint(slug)}`,
|
|
60
61
|
entity: job,
|
|
61
62
|
lexicon: "github",
|
|
62
63
|
});
|
package/dist/rules/gha059.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
24
|
-
import { getPrimaryOutput, extractActionRefs, extractUsesComment, parseActionUses } from "./yaml-helpers";
|
|
24
|
+
import { getPrimaryOutput, extractActionRefs, extractUsesComment, parseActionUses, usesLineComments } from "./yaml-helpers";
|
|
25
25
|
|
|
26
26
|
const SHA_RE = /^[0-9a-f]{40}$/;
|
|
27
27
|
|
|
@@ -49,9 +49,17 @@ export function findStalePinAnnotations(yaml: string): StalePinFinding[] {
|
|
|
49
49
|
const findings: StalePinFinding[] = [];
|
|
50
50
|
|
|
51
51
|
// First pass: missing annotations.
|
|
52
|
+
// The parsed value carries no comment, so match each ref to its `uses:`
|
|
53
|
+
// line, in document order, to read the label.
|
|
54
|
+
const lines = usesLineComments(yaml);
|
|
55
|
+
const commentFor = (ref: string): string | undefined => {
|
|
56
|
+
const k = lines.findIndex((l) => l.ref === ref);
|
|
57
|
+
if (k === -1) return extractUsesComment(ref);
|
|
58
|
+
return lines.splice(k, 1)[0].comment;
|
|
59
|
+
};
|
|
52
60
|
const withComment: Array<{ job: string; ref: string; slug: string; sha: string; comment: string }> = [];
|
|
53
61
|
for (const { job, ref, parsed } of refs) {
|
|
54
|
-
const comment =
|
|
62
|
+
const comment = commentFor(ref);
|
|
55
63
|
if (!comment) {
|
|
56
64
|
findings.push({ job, ref, slug: parsed.slug, sha: parsed.gitRef, kind: "missing" });
|
|
57
65
|
} else {
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
|
|
23
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
24
|
+
import {
|
|
25
|
+
getPrimaryOutput,
|
|
26
|
+
extractWorkflowPermissions,
|
|
27
|
+
extractJobPermissions,
|
|
28
|
+
extractStepsByJob,
|
|
29
|
+
parseActionUses,
|
|
30
|
+
type PermissionsValue,
|
|
31
|
+
} from "./yaml-helpers";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Actions that exchange the job's OIDC token for cloud credentials, keyed by
|
|
35
|
+
* lowercased `owner/repo`, each with the inputs that select a static
|
|
36
|
+
* credential instead (when one is set, the step does not need `id-token`).
|
|
37
|
+
*/
|
|
38
|
+
export const OIDC_CREDENTIAL_ACTIONS: ReadonlyMap<string, readonly string[]> = new Map([
|
|
39
|
+
["aws-actions/configure-aws-credentials", ["aws-access-key-id", "web-identity-token-file"]],
|
|
40
|
+
["azure/login", ["creds"]],
|
|
41
|
+
["google-github-actions/auth", ["credentials_json"]],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/** True when the permissions value grants `id-token: write`. */
|
|
45
|
+
export function grantsIdTokenWrite(perms: PermissionsValue): boolean {
|
|
46
|
+
if (typeof perms === "string") return perms.trim() === "write-all";
|
|
47
|
+
return perms["id-token"] === "write";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The OIDC credential action a step uses, as written, or undefined. */
|
|
51
|
+
function oidcActionOf(step: Record<string, unknown>): string | undefined {
|
|
52
|
+
if (typeof step.uses !== "string") return undefined;
|
|
53
|
+
const parsed = parseActionUses(step.uses);
|
|
54
|
+
if (!parsed) return undefined;
|
|
55
|
+
const staticInputs = OIDC_CREDENTIAL_ACTIONS.get(parsed.slug.toLowerCase());
|
|
56
|
+
if (!staticInputs) return undefined;
|
|
57
|
+
const withInputs = step.with;
|
|
58
|
+
if (withInputs && typeof withInputs === "object" && !Array.isArray(withInputs)) {
|
|
59
|
+
const inputs = withInputs as Record<string, unknown>;
|
|
60
|
+
if (staticInputs.some((k) => inputs[k] !== undefined && inputs[k] !== null && inputs[k] !== "")) return undefined;
|
|
61
|
+
}
|
|
62
|
+
return parsed.slug;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function describeJobBlock(perms: PermissionsValue): string {
|
|
66
|
+
if (typeof perms === "string") return `\`permissions: ${perms}\``;
|
|
67
|
+
if (Object.keys(perms).length === 0) return "`permissions: {}`";
|
|
68
|
+
return "its own `permissions:` block";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const gha069: PostSynthCheck = {
|
|
72
|
+
id: "GHA069",
|
|
73
|
+
description: "Job-level permissions block drops the workflow's id-token: write that an OIDC credential step needs",
|
|
74
|
+
|
|
75
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
76
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
77
|
+
|
|
78
|
+
for (const [, output] of ctx.outputs) {
|
|
79
|
+
const yaml = getPrimaryOutput(output);
|
|
80
|
+
|
|
81
|
+
const wf = extractWorkflowPermissions(yaml);
|
|
82
|
+
if (!wf || !grantsIdTokenWrite(wf)) continue;
|
|
83
|
+
|
|
84
|
+
const jobPermissions = extractJobPermissions(yaml);
|
|
85
|
+
if (jobPermissions.size === 0) continue;
|
|
86
|
+
|
|
87
|
+
const stepsByJob = extractStepsByJob(yaml);
|
|
88
|
+
for (const [job, perms] of jobPermissions) {
|
|
89
|
+
if (grantsIdTokenWrite(perms)) continue;
|
|
90
|
+
|
|
91
|
+
const actions = [
|
|
92
|
+
...new Set((stepsByJob.get(job) ?? []).map(oidcActionOf).filter((a): a is string => a !== undefined)),
|
|
93
|
+
];
|
|
94
|
+
if (actions.length === 0) continue;
|
|
95
|
+
|
|
96
|
+
diagnostics.push({
|
|
97
|
+
checkId: "GHA069",
|
|
98
|
+
severity: "warning",
|
|
99
|
+
message:
|
|
100
|
+
`Job "${job}" declares ${describeJobBlock(perms)}, which replaces the workflow-level block instead of merging with it, ` +
|
|
101
|
+
`so the workflow's \`id-token: write\` does not reach the job and ${actions.join(", ")} cannot request an OIDC token. ` +
|
|
102
|
+
`Add \`id-token: write\` to job "${job}"'s permissions block; keep the job block rather than removing it.`,
|
|
103
|
+
entity: job,
|
|
104
|
+
lexicon: "github",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return diagnostics;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -37,66 +37,45 @@ export interface ParsedJob {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Extract jobs from a serialized GitHub Actions workflow YAML.
|
|
40
|
+
*
|
|
41
|
+
* Built on the structural parser ({@link jobEntries}), so every step of every
|
|
42
|
+
* job is read whatever key the step starts with, and `needs:` is read in all
|
|
43
|
+
* three forms (a scalar, a flow list, a block list). The line-based version
|
|
44
|
+
* this replaced stopped after the first line of a job's first step, so a step
|
|
45
|
+
* that starts with `name:` (the shape chant's serializer emits) had no `uses:`
|
|
46
|
+
* or `run:` and later steps were never seen (#3201).
|
|
47
|
+
*
|
|
48
|
+
* `run` is the whole script, block scalars included. `permissions` is not
|
|
49
|
+
* filled here; use {@link extractJobPermissions}.
|
|
40
50
|
*/
|
|
41
51
|
export function extractJobs(yaml: string): Map<string, ParsedJob> {
|
|
42
52
|
const jobs = new Map<string, ParsedJob>();
|
|
43
|
-
|
|
44
|
-
// Find the jobs: section — take everything after `jobs:\n`
|
|
45
|
-
const jobsIdx = yaml.search(/^jobs:\s*$/m);
|
|
46
|
-
if (jobsIdx === -1) return jobs;
|
|
47
|
-
|
|
48
|
-
// Content after the `jobs:` line. Stop at the next top-level key (non-indented) or EOF.
|
|
49
|
-
const afterJobs = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
|
|
50
|
-
const endMatch = afterJobs.search(/^[a-z]/m);
|
|
51
|
-
const jobsContent = endMatch === -1 ? afterJobs : afterJobs.slice(0, endMatch);
|
|
52
|
-
|
|
53
|
-
// Split into individual jobs by finding lines with exactly 2 spaces of indent followed by a key
|
|
54
|
-
const jobSections = jobsContent.split(/\n(?= [a-z][a-z0-9-]*:)/);
|
|
55
|
-
|
|
56
|
-
for (const section of jobSections) {
|
|
57
|
-
const nameMatch = section.match(/^\s{2}([a-z][a-z0-9-]*):/);
|
|
58
|
-
if (!nameMatch) continue;
|
|
59
|
-
|
|
60
|
-
const name = nameMatch[1];
|
|
53
|
+
for (const [name, jobObj] of jobEntries(yaml)) {
|
|
61
54
|
const job: ParsedJob = { name };
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const needsList = section.match(/^\s{4}needs:\n((?:\s{6}- .+\n?)+)/m);
|
|
69
|
-
if (needsList) {
|
|
70
|
-
job.needs = [];
|
|
71
|
-
for (const line of needsList[1].split("\n")) {
|
|
72
|
-
const item = line.match(/^\s{6}- (.+)$/);
|
|
73
|
-
if (item) job.needs.push(item[1].trim().replace(/^'|'$/g, "").replace(/^"|"$/g, ""));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
56
|
+
const needs = jobObj.needs;
|
|
57
|
+
if (typeof needs === "string") {
|
|
58
|
+
job.needs = [needs];
|
|
59
|
+
} else if (Array.isArray(needs)) {
|
|
60
|
+
job.needs = needs.filter((n) => typeof n === "string" || typeof n === "number").map(String);
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (stepsMatch) {
|
|
63
|
+
const steps = jobObj.steps;
|
|
64
|
+
if (Array.isArray(steps)) {
|
|
81
65
|
job.steps = [];
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
run: runMatch?.[1]?.trim(),
|
|
91
|
-
name: stepNameMatch?.[1]?.trim().replace(/^'|'$/g, ""),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
66
|
+
for (const raw of steps) {
|
|
67
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
68
|
+
const step = raw as Record<string, unknown>;
|
|
69
|
+
job.steps.push({
|
|
70
|
+
uses: typeof step.uses === "string" ? stripUsesComment(step.uses) : undefined,
|
|
71
|
+
run: typeof step.run === "string" ? step.run : undefined,
|
|
72
|
+
name: typeof step.name === "string" ? step.name : undefined,
|
|
73
|
+
});
|
|
94
74
|
}
|
|
95
75
|
}
|
|
96
76
|
|
|
97
77
|
jobs.set(name, job);
|
|
98
78
|
}
|
|
99
|
-
|
|
100
79
|
return jobs;
|
|
101
80
|
}
|
|
102
81
|
|
|
@@ -497,6 +476,21 @@ export function extractUsesComment(rawUses: string): string | undefined {
|
|
|
497
476
|
return comment ? comment : undefined;
|
|
498
477
|
}
|
|
499
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Every `uses:` line in the workflow text, in document order, with its
|
|
481
|
+
* trailing comment. The parsed document has no comments (core's parseYAML
|
|
482
|
+
* drops them, #3006), so a check that reads the pin label (GHA059) takes it
|
|
483
|
+
* from here.
|
|
484
|
+
*/
|
|
485
|
+
export function usesLineComments(yaml: string): Array<{ ref: string; comment: string | undefined }> {
|
|
486
|
+
const out: Array<{ ref: string; comment: string | undefined }> = [];
|
|
487
|
+
for (const line of yaml.split(/\r?\n/)) {
|
|
488
|
+
const m = line.match(/^\s*(?:-\s+)?uses:\s*(["']?)([^\s"'#]+)\1(\s+#.*)?\s*$/);
|
|
489
|
+
if (m) out.push({ ref: m[2], comment: m[3] ? extractUsesComment(m[3]) : undefined });
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
|
|
500
494
|
export function parseActionUses(rawUses: string): { owner: string; repo: string; slug: string; gitRef: string } | undefined {
|
|
501
495
|
const uses = stripUsesComment(rawUses);
|
|
502
496
|
if (uses.startsWith("./") || uses.startsWith("../") || uses.startsWith("docker://")) return undefined;
|
package/dist/variables.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../src/variables.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"variables.d.ts","sourceRoot":"","sources":["../src/variables.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,eAAO,MAAM,MAAM;aACjB,GAAG;aACH,OAAO;aACP,OAAO;aACP,GAAG;aACH,KAAK;aACL,eAAe;aACf,UAAU;aACV,eAAe;aACf,SAAS;aACT,KAAK;aACL,KAAK;aACL,SAAS;aACT,UAAU;aACV,QAAQ;aACR,WAAW;aACX,SAAS;aACT,KAAK;aACL,GAAG;aACH,OAAO;aACP,OAAO;aACP,SAAS;aACT,MAAM;aACN,UAAU;aACV,MAAM;aACN,UAAU;CACF,CAAC;AAEX,eAAO,MAAM,MAAM;aACjB,EAAE;aACF,IAAI;aACJ,IAAI;aACJ,IAAI;aACJ,SAAS;CACD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-github",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.102.0",
|
|
4
4
|
"description": "GitHub Actions lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
"validate": "tsx src/validate-cli.ts",
|
|
55
55
|
"docs": "tsx src/codegen/docs-cli.ts",
|
|
56
56
|
"prepack": "npm run generate && npm run bundle && npm run validate && npm run build",
|
|
57
|
-
"build": "
|
|
57
|
+
"build": "../../scripts/tsc7.sh -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && find dist -type f \\( -name \"*.js\" -o -name \"*.js.map\" \\) -delete"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@intentius/chant": "*",
|
|
61
61
|
"typescript": "^5.9.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
|
-
"@intentius/chant": "^0.
|
|
64
|
+
"@intentius/chant": "^0.102.0",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
5
|
+
import { Workflow, Job } from "./generated/index";
|
|
6
|
+
import { Checkout } from "./composites/checkout";
|
|
7
|
+
import { SetupNode } from "./composites/setup-node";
|
|
8
|
+
import { githubSerializer } from "./serializer";
|
|
9
|
+
import { ACTION_PINS, actionRef } from "./action-pins";
|
|
10
|
+
import { gha021 } from "./lint/post-synth/gha021";
|
|
11
|
+
import { gha029 } from "./lint/post-synth/gha029";
|
|
12
|
+
import { gha059 } from "./lint/post-synth/gha059";
|
|
13
|
+
|
|
14
|
+
// #2510: the composites emit a current major by default, `pin: "sha"` emits
|
|
15
|
+
// the pinned commit, and the pinning checks agree with both.
|
|
16
|
+
|
|
17
|
+
function workflowYaml(pin?: "tag" | "sha"): string {
|
|
18
|
+
const entities = new Map<string, unknown>([
|
|
19
|
+
["ci", new Workflow({ name: "CI", on: { push: {} } })],
|
|
20
|
+
[
|
|
21
|
+
"test",
|
|
22
|
+
new Job({
|
|
23
|
+
"runs-on": "ubuntu-latest",
|
|
24
|
+
steps: [Checkout({ pin }).step, SetupNode({ nodeVersion: "22.x", cache: "npm", pin }).step],
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
]);
|
|
28
|
+
return githubSerializer.serialize(entities as never) as string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ctx(yaml: string): PostSynthContext {
|
|
32
|
+
return {
|
|
33
|
+
outputs: new Map([["github", yaml]]),
|
|
34
|
+
entities: new Map(),
|
|
35
|
+
buildResult: { outputs: new Map([["github", yaml]]), entities: new Map(), warnings: [], errors: [], sourceFileCount: 1 },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pinningFindings = (yaml: string) =>
|
|
40
|
+
[gha021, gha029, gha059].flatMap((check) => check.check(ctx(yaml)));
|
|
41
|
+
|
|
42
|
+
describe("action pins (#2510)", () => {
|
|
43
|
+
test("every pin is a 40-character commit SHA on the major the composites emit", () => {
|
|
44
|
+
for (const pin of Object.values(ACTION_PINS)) {
|
|
45
|
+
expect(pin.sha).toMatch(/^[0-9a-f]{40}$/);
|
|
46
|
+
expect(pin.version.startsWith(`${pin.major}.`)).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("Checkout and SetupNode default to the current major, not v4", () => {
|
|
51
|
+
expect((Checkout({}).step.props as { uses?: string }).uses).toBe("actions/checkout@v7");
|
|
52
|
+
expect((SetupNode({}).step.props as { uses?: string }).uses).toBe("actions/setup-node@v7");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("pin: \"sha\" writes the SHA as the ref and the version as a YAML comment", () => {
|
|
56
|
+
const yaml = workflowYaml("sha");
|
|
57
|
+
expect(yaml).toContain(`uses: actions/checkout@${ACTION_PINS["actions/checkout"].sha} # v7.0.1\n`);
|
|
58
|
+
expect(yaml).toContain(`uses: actions/setup-node@${ACTION_PINS["actions/setup-node"].sha} # v7.0.0\n`);
|
|
59
|
+
expect(yaml).not.toMatch(/uses: '/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a SHA-pinned workflow passes GHA021, GHA029 and GHA059", () => {
|
|
63
|
+
expect(pinningFindings(workflowYaml("sha"))).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the default tag warning names the exact fix", () => {
|
|
67
|
+
const messages = pinningFindings(workflowYaml()).map((d) => d.message);
|
|
68
|
+
expect(messages).toHaveLength(2);
|
|
69
|
+
expect(messages[0]).toContain(`\`uses: ${actionRef("actions/checkout", "sha")}\``);
|
|
70
|
+
expect(messages[0]).toContain(`Checkout({ pin: "sha" })`);
|
|
71
|
+
expect(messages[1]).toContain(`\`uses: ${actionRef("actions/setup-node", "sha")}\``);
|
|
72
|
+
expect(messages[1]).toContain(`SetupNode({ pin: "sha" })`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an action outside the pin table gets no fix hint", () => {
|
|
76
|
+
const yaml = "jobs:\n a:\n runs-on: ubuntu-latest\n steps:\n - uses: acme/deploy@v1\n";
|
|
77
|
+
const [finding] = gha029.check(ctx(yaml));
|
|
78
|
+
expect(finding.message).toMatch(/security\.$/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("the github-pr-preview example's literal pins match the table", () => {
|
|
82
|
+
// The example writes its pins as literals so the project still folds.
|
|
83
|
+
const source = readFileSync(join(__dirname, "../../../examples/github-pr-preview/src/ci/setup.ts"), "utf8");
|
|
84
|
+
expect(source).toContain(`"${actionRef("actions/checkout", "sha")}"`);
|
|
85
|
+
expect(source).toContain(`"${actionRef("actions/setup-node", "sha")}"`);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The action versions the github lexicon emits, and the commit each release
|
|
3
|
+
* tag points at.
|
|
4
|
+
*
|
|
5
|
+
* Composites emit `<slug>@<major>` by default. With `pin: "sha"` they emit
|
|
6
|
+
* `<slug>@<sha> # <version>`, the form GHA021 and GHA029 accept and GHA059
|
|
7
|
+
* reads the version label from. The serializer writes the `# <version>` part
|
|
8
|
+
* as a YAML comment, so GitHub sees only the SHA.
|
|
9
|
+
*
|
|
10
|
+
* Refresh a row by resolving the release tag to its commit:
|
|
11
|
+
*
|
|
12
|
+
* gh api repos/actions/checkout/commits/v7.0.1 --jq .sha
|
|
13
|
+
*
|
|
14
|
+
* Keep `major` on a version whose action runs on a Node runtime GitHub still
|
|
15
|
+
* supports.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface ActionPin {
|
|
19
|
+
/** The major tag composites emit by default, such as `v7`. */
|
|
20
|
+
major: string;
|
|
21
|
+
/** The release the SHA belongs to, written as the pin's comment. */
|
|
22
|
+
version: string;
|
|
23
|
+
/** The full commit SHA of `version`. */
|
|
24
|
+
sha: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ACTION_PINS = {
|
|
28
|
+
"actions/checkout": {
|
|
29
|
+
major: "v7",
|
|
30
|
+
version: "v7.0.1",
|
|
31
|
+
sha: "3d3c42e5aac5ba805825da76410c181273ba90b1",
|
|
32
|
+
},
|
|
33
|
+
"actions/setup-node": {
|
|
34
|
+
major: "v7",
|
|
35
|
+
version: "v7.0.0",
|
|
36
|
+
sha: "820762786026740c76f36085b0efc47a31fe5020",
|
|
37
|
+
},
|
|
38
|
+
} as const satisfies Record<string, ActionPin>;
|
|
39
|
+
|
|
40
|
+
export type PinnedAction = keyof typeof ACTION_PINS;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* How a composite refers to its action: `"tag"` emits the major tag
|
|
44
|
+
* (`actions/checkout@v7`), `"sha"` emits the pinned commit with its version
|
|
45
|
+
* as a comment (`actions/checkout@3d3c…90b1 # v7.0.1`).
|
|
46
|
+
*/
|
|
47
|
+
export type ActionPinMode = "tag" | "sha";
|
|
48
|
+
|
|
49
|
+
/** The `uses:` value for a pinned action. */
|
|
50
|
+
export function actionRef(slug: PinnedAction, pin: ActionPinMode = "tag"): string {
|
|
51
|
+
const entry: ActionPin = ACTION_PINS[slug];
|
|
52
|
+
return pin === "sha" ? `${slug}@${entry.sha} # ${entry.version}` : `${slug}@${entry.major}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The pin table entry for an action slug, if the lexicon maintains one. */
|
|
56
|
+
export function actionPin(slug: string): ActionPin | undefined {
|
|
57
|
+
return Object.prototype.hasOwnProperty.call(ACTION_PINS, slug)
|
|
58
|
+
? ACTION_PINS[slug as PinnedAction]
|
|
59
|
+
: undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const COMPOSITE_FOR: Record<PinnedAction, string> = {
|
|
63
|
+
"actions/checkout": "Checkout",
|
|
64
|
+
"actions/setup-node": "SetupNode",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The exact fix a pinning warning names for an action in the pin table, or
|
|
69
|
+
* an empty string for any other action.
|
|
70
|
+
*/
|
|
71
|
+
export function pinFixHint(slug: string): string {
|
|
72
|
+
if (!actionPin(slug)) return "";
|
|
73
|
+
const pinned = slug as PinnedAction;
|
|
74
|
+
return ` Use \`uses: ${actionRef(pinned, "sha")}\`, which ${COMPOSITE_FOR[pinned]}({ pin: "sha" }) emits.`;
|
|
75
|
+
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -55,7 +55,7 @@ The generated file includes:
|
|
|
55
55
|
| \`export const buildApp = new Job({...})\` | \`jobs: build-app:\` | Export name → kebab-case job key |
|
|
56
56
|
| \`"runs-on": "ubuntu-latest"\` | \`runs-on: ubuntu-latest\` | Property names match GitHub spec |
|
|
57
57
|
| \`timeoutMinutes: 15\` | \`timeout-minutes: 15\` | camelCase → kebab-case for job properties |
|
|
58
|
-
| \`new Step({ uses: "actions/checkout@
|
|
58
|
+
| \`new Step({ uses: "actions/checkout@v7" })\` | \`- uses: actions/checkout@v7\` | Steps serialize as sequence entries |
|
|
59
59
|
|
|
60
60
|
## Validating locally
|
|
61
61
|
|