@intentius/chant-lexicon-github 0.101.0 → 0.104.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/__fixtures__/pr-loop.github.golden.yml +68 -0
- 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 +30 -1
- package/src/components/generate-pipeline.ts +157 -14
- package/src/components/generate-pr-pipeline.test.ts +98 -0
- 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,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface NodeCIProps {
|
|
5
6
|
nodeVersion?: string;
|
|
@@ -28,12 +29,12 @@ export const NodeCI = Composite((props: NodeCIProps) => {
|
|
|
28
29
|
|
|
29
30
|
const checkoutStep = new Step({
|
|
30
31
|
name: "Checkout",
|
|
31
|
-
uses: "actions/checkout
|
|
32
|
+
uses: actionRef("actions/checkout"),
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
const setupNodeStep = new Step({
|
|
35
36
|
name: "Setup Node.js",
|
|
36
|
-
uses: "actions/setup-node
|
|
37
|
+
uses: actionRef("actions/setup-node"),
|
|
37
38
|
with: {
|
|
38
39
|
"node-version": nodeVersion,
|
|
39
40
|
cache: packageManager === "bun" ? undefined : packageManager,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, withDefaults, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface NodePipelineProps {
|
|
5
6
|
/** Node.js version. Default: "22" */
|
|
@@ -32,19 +33,19 @@ const cacheConfig = {
|
|
|
32
33
|
cache: "npm",
|
|
33
34
|
installCmd: "npm ci",
|
|
34
35
|
runPrefix: "npm run",
|
|
35
|
-
setupAction: "actions/setup-node
|
|
36
|
+
setupAction: actionRef("actions/setup-node"),
|
|
36
37
|
},
|
|
37
38
|
pnpm: {
|
|
38
39
|
cache: "pnpm",
|
|
39
40
|
installCmd: "pnpm install --frozen-lockfile",
|
|
40
41
|
runPrefix: "pnpm run",
|
|
41
|
-
setupAction: "actions/setup-node
|
|
42
|
+
setupAction: actionRef("actions/setup-node"),
|
|
42
43
|
},
|
|
43
44
|
yarn: {
|
|
44
45
|
cache: "yarn",
|
|
45
46
|
installCmd: "yarn install --frozen-lockfile",
|
|
46
47
|
runPrefix: "yarn",
|
|
47
|
-
setupAction: "actions/setup-node
|
|
48
|
+
setupAction: actionRef("actions/setup-node"),
|
|
48
49
|
},
|
|
49
50
|
bun: {
|
|
50
51
|
cache: undefined,
|
|
@@ -75,13 +76,13 @@ export const NodePipeline = Composite((props: NodePipelineProps) => {
|
|
|
75
76
|
const isBun = packageManager === "bun";
|
|
76
77
|
|
|
77
78
|
// ── Build job steps ────────────────────────────────────────────────
|
|
78
|
-
const buildCheckout = new Step({ name: "Checkout", uses: "actions/checkout
|
|
79
|
+
const buildCheckout = new Step({ name: "Checkout", uses: actionRef("actions/checkout") });
|
|
79
80
|
|
|
80
81
|
const buildSetup = isBun
|
|
81
82
|
? new Step({ name: "Setup Bun", uses: "oven-sh/setup-bun@v2" })
|
|
82
83
|
: new Step({
|
|
83
84
|
name: "Setup Node.js",
|
|
84
|
-
uses: "actions/setup-node
|
|
85
|
+
uses: actionRef("actions/setup-node"),
|
|
85
86
|
with: { "node-version": nodeVersion, cache: pm.cache },
|
|
86
87
|
});
|
|
87
88
|
|
|
@@ -105,13 +106,13 @@ export const NodePipeline = Composite((props: NodePipelineProps) => {
|
|
|
105
106
|
}, defaults?.buildJob));
|
|
106
107
|
|
|
107
108
|
// ── Test job steps ─────────────────────────────────────────────────
|
|
108
|
-
const testCheckout = new Step({ name: "Checkout", uses: "actions/checkout
|
|
109
|
+
const testCheckout = new Step({ name: "Checkout", uses: actionRef("actions/checkout") });
|
|
109
110
|
|
|
110
111
|
const testSetup = isBun
|
|
111
112
|
? new Step({ name: "Setup Bun", uses: "oven-sh/setup-bun@v2" })
|
|
112
113
|
: new Step({
|
|
113
114
|
name: "Setup Node.js",
|
|
114
|
-
uses: "actions/setup-node
|
|
115
|
+
uses: actionRef("actions/setup-node"),
|
|
115
116
|
with: { "node-version": nodeVersion, cache: pm.cache },
|
|
116
117
|
});
|
|
117
118
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Job, Step, Workflow } from "../generated/index";
|
|
3
|
+
import { actionRef } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface PythonCIProps {
|
|
5
6
|
/** Python version. Default: "3.12" */
|
|
@@ -46,7 +47,7 @@ export const PythonCI = Composite((props: PythonCIProps) => {
|
|
|
46
47
|
const testJob = new Job(mergeDefaults({
|
|
47
48
|
"runs-on": runsOn,
|
|
48
49
|
steps: [
|
|
49
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
50
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
50
51
|
new Step({
|
|
51
52
|
name: "Setup Python",
|
|
52
53
|
uses: "actions/setup-python@v5",
|
|
@@ -63,7 +64,7 @@ export const PythonCI = Composite((props: PythonCIProps) => {
|
|
|
63
64
|
? new Job(mergeDefaults({
|
|
64
65
|
"runs-on": runsOn,
|
|
65
66
|
steps: [
|
|
66
|
-
new Step({ name: "Checkout", uses: "actions/checkout
|
|
67
|
+
new Step({ name: "Checkout", uses: actionRef("actions/checkout") }),
|
|
67
68
|
new Step({
|
|
68
69
|
name: "Setup Python",
|
|
69
70
|
uses: "actions/setup-python@v5",
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Composite, mergeDefaults } from "@intentius/chant";
|
|
2
2
|
import { Step } from "../generated/index";
|
|
3
|
+
import { actionRef, type ActionPinMode } from "../action-pins";
|
|
3
4
|
|
|
4
5
|
export interface SetupNodeProps {
|
|
5
6
|
nodeVersion?: string;
|
|
6
7
|
registryUrl?: string;
|
|
7
8
|
cache?: string;
|
|
8
9
|
cacheFilePath?: string;
|
|
10
|
+
/**
|
|
11
|
+
* `"tag"` (default) emits the action's major tag. `"sha"` emits the commit
|
|
12
|
+
* SHA from the lexicon's pin table with the version as a YAML comment, which
|
|
13
|
+
* passes GHA021, GHA029 and GHA059.
|
|
14
|
+
*/
|
|
15
|
+
pin?: ActionPinMode;
|
|
9
16
|
defaults?: {
|
|
10
17
|
step?: Partial<ConstructorParameters<typeof Step>[0]>;
|
|
11
18
|
};
|
|
@@ -21,7 +28,7 @@ export const SetupNode = Composite((props: SetupNodeProps) => {
|
|
|
21
28
|
|
|
22
29
|
const step = new Step(mergeDefaults({
|
|
23
30
|
name: "Setup Node.js",
|
|
24
|
-
uses: "actions/setup-node
|
|
31
|
+
uses: actionRef("actions/setup-node", props.pin),
|
|
25
32
|
...(Object.keys(withObj).length > 0 ? { with: withObj } : {}),
|
|
26
33
|
}, defaults?.step));
|
|
27
34
|
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,10 @@ export { GitHub, Runner } from "./variables";
|
|
|
20
20
|
// After running `chant generate`, this re-exports all entity classes
|
|
21
21
|
export * from "./generated/index";
|
|
22
22
|
|
|
23
|
+
// Action versions the composites emit, and their pinned commits
|
|
24
|
+
export { ACTION_PINS, actionRef, actionPin } from "./action-pins";
|
|
25
|
+
export type { ActionPin, ActionPinMode, PinnedAction } from "./action-pins";
|
|
26
|
+
|
|
23
27
|
// Composites
|
|
24
28
|
export {
|
|
25
29
|
Checkout, SetupNode, SetupGo, SetupPython,
|
|
@@ -66,6 +66,7 @@ export const githubAuditCatalog: Record<string, RuleMeta> = {
|
|
|
66
66
|
GHA066: auditRule("GHA066", "report-only", "guidance", "Unbounded artifact retention", "Set a retention-days sized to how long the artifact is actually needed.", { category: "efficiency" }),
|
|
67
67
|
GHA067: auditRule("GHA067", "report-only", "guidance", "Unconditional heavy step with no path filter", "Scope the trigger's paths, or add an if: guard to the step.", { category: "efficiency" }),
|
|
68
68
|
GHA068: auditRule("GHA068", "report-only", "guidance", "Pull-request workflow missing a concurrency group", "Add a concurrency group with cancel-in-progress: true.", { category: "efficiency" }),
|
|
69
|
+
GHA069: auditRule("GHA069", "merge-worthy", "guidance", "Job permissions block drops the workflow's id-token: write", "Add `id-token: write` to the job's `permissions:` block; keep the job block rather than removing it.", { category: "correctness" }),
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* the registry, the relation vocabulary, and why this is credit rather than
|
|
5
5
|
* authority. Kept by hand; the prior-art sweep (scripts/prior-art-sweep.ts) reports
|
|
6
6
|
* when a credited tool's index no longer lists a rule cited here.
|
|
7
|
+
*
|
|
8
|
+
* GHA069 (a job-level permissions block drops the workflow's `id-token:
|
|
9
|
+
* write`, #2273) has no entry here. Checked 2026-10-02: zizmor's 41 audits
|
|
10
|
+
* (docs.zizmor.sh/audits) cover over-broad scopes (`excessive-permissions`)
|
|
11
|
+
* and undocumented ones (`undocumented-permissions`), not a scope a job
|
|
12
|
+
* block silently loses. actionlint's `permissions` check
|
|
13
|
+
* (github.com/rhysd/actionlint/blob/main/docs/checks.md#permissions)
|
|
14
|
+
* validates scope names and levels only. poutine's rules (opa/rego/rules)
|
|
15
|
+
* have `default_permissions_on_risky_events`, the GHA013 condition, and
|
|
16
|
+
* nothing about a dropped grant. octoscan's `debug-oidc-action` flags OIDC
|
|
17
|
+
* action use as a debug finding and does not read the permission blocks.
|
|
18
|
+
* Scorecard's Token-Permissions rewards narrower blocks, the opposite
|
|
19
|
+
* concern. Ship with no lineage rather than invent a credit.
|
|
7
20
|
*/
|
|
8
21
|
import type { Lineage } from "@intentius/chant/audit/catalog";
|
|
9
22
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
14
|
+
with:
|
|
15
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
16
|
+
aws-region: us-east-1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- run: npm test
|
|
16
|
+
deploy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
20
|
+
with:
|
|
21
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
22
|
+
aws-region: us-east-1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: write
|
|
14
|
+
id-token: write
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
18
|
+
with:
|
|
19
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
20
|
+
aws-region: us-east-1
|
|
21
|
+
- run: aws s3 sync ./dist s3://example-bucket
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
permissions:
|
|
6
|
+
id-token: write
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pull-requests: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
17
|
+
with:
|
|
18
|
+
role-to-assume: arn:aws:iam::123456789012:role/deploy
|
|
19
|
+
aws-region: us-east-1
|
|
20
|
+
- run: aws s3 sync ./dist s3://example-bucket
|
|
@@ -58,4 +58,20 @@ jobs:
|
|
|
58
58
|
const diags = gha011.check(makeCtx(yaml));
|
|
59
59
|
expect(diags).toHaveLength(0);
|
|
60
60
|
});
|
|
61
|
+
|
|
62
|
+
test("flags a scalar needs: naming a missing job (#3201)", () => {
|
|
63
|
+
const yaml = `name: CI
|
|
64
|
+
on: push
|
|
65
|
+
jobs:
|
|
66
|
+
test:
|
|
67
|
+
needs: biuld
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- name: Test
|
|
71
|
+
run: npm test
|
|
72
|
+
`;
|
|
73
|
+
const diags = gha011.check(makeCtx(yaml));
|
|
74
|
+
expect(diags).toHaveLength(1);
|
|
75
|
+
expect(diags[0].message).toContain('"biuld"');
|
|
76
|
+
});
|
|
61
77
|
});
|
|
@@ -63,4 +63,22 @@ jobs:
|
|
|
63
63
|
const diags = gha018.check(makeCtx(yaml));
|
|
64
64
|
expect(diags).toHaveLength(0);
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
test("flags a checkout step that starts with name: (#3201)", () => {
|
|
68
|
+
const yaml = `name: CI
|
|
69
|
+
on:
|
|
70
|
+
pull_request_target:
|
|
71
|
+
jobs:
|
|
72
|
+
build:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- name: Label
|
|
76
|
+
run: echo label
|
|
77
|
+
- name: Checkout
|
|
78
|
+
uses: actions/checkout@v7
|
|
79
|
+
`;
|
|
80
|
+
const diags = gha018.check(makeCtx(yaml));
|
|
81
|
+
expect(diags).toHaveLength(1);
|
|
82
|
+
expect(diags[0].entity).toBe("build");
|
|
83
|
+
});
|
|
66
84
|
});
|
|
@@ -59,6 +59,48 @@ jobs:
|
|
|
59
59
|
needs: [test]
|
|
60
60
|
steps:
|
|
61
61
|
- run: echo deploy
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha019.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("detects a cycle through scalar needs: (#3201)", () => {
|
|
68
|
+
const yaml = `name: CI
|
|
69
|
+
on: push
|
|
70
|
+
jobs:
|
|
71
|
+
a:
|
|
72
|
+
needs: b
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- run: echo a
|
|
76
|
+
b:
|
|
77
|
+
needs: a
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
steps:
|
|
80
|
+
- run: echo b
|
|
81
|
+
`;
|
|
82
|
+
const diags = gha019.check(makeCtx(yaml));
|
|
83
|
+
expect(diags).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("no false cycle when a later job id has an underscore (#3201)", () => {
|
|
87
|
+
// The line-based extractJobs did not split on "extra_thing", so its
|
|
88
|
+
// needs: landed on shared-alb and read as shared-alb -> shared-alb.
|
|
89
|
+
// generateGithubPipeline emits this shape for a component named Extra_Thing.
|
|
90
|
+
const yaml = `name: chant-components
|
|
91
|
+
on:
|
|
92
|
+
workflow_dispatch: {}
|
|
93
|
+
jobs:
|
|
94
|
+
shared-alb:
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@v7
|
|
98
|
+
extra_thing:
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
needs:
|
|
101
|
+
- shared-alb
|
|
102
|
+
steps:
|
|
103
|
+
- uses: actions/checkout@v7
|
|
62
104
|
`;
|
|
63
105
|
const diags = gha019.check(makeCtx(yaml));
|
|
64
106
|
expect(diags).toHaveLength(0);
|
|
@@ -79,4 +79,22 @@ jobs:
|
|
|
79
79
|
const diags = gha021.check(makeCtx(yaml));
|
|
80
80
|
expect(diags).toHaveLength(0);
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
test("flags a checkout step that starts with name: (the composite shape)", () => {
|
|
84
|
+
const yaml = `name: CI
|
|
85
|
+
on:
|
|
86
|
+
push:
|
|
87
|
+
jobs:
|
|
88
|
+
build:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- name: Checkout
|
|
92
|
+
uses: actions/checkout@v7
|
|
93
|
+
- name: Test
|
|
94
|
+
run: echo test
|
|
95
|
+
`;
|
|
96
|
+
const diags = gha021.check(makeCtx(yaml));
|
|
97
|
+
expect(diags).toHaveLength(1);
|
|
98
|
+
expect(diags[0].message).toContain('Checkout({ pin: "sha" })');
|
|
99
|
+
});
|
|
82
100
|
});
|
|
@@ -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
|
|
|
@@ -80,4 +80,19 @@ on:
|
|
|
80
80
|
const diags = gha022.check(makeCtx(yaml));
|
|
81
81
|
expect(diags).toHaveLength(0);
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
test("flags a job whose id has capitals or underscores (#3201)", () => {
|
|
85
|
+
const yaml = `name: CI
|
|
86
|
+
on: push
|
|
87
|
+
jobs:
|
|
88
|
+
Build_App:
|
|
89
|
+
runs-on: ubuntu-latest
|
|
90
|
+
steps:
|
|
91
|
+
- name: Build
|
|
92
|
+
run: npm run build
|
|
93
|
+
`;
|
|
94
|
+
const diags = gha022.check(makeCtx(yaml));
|
|
95
|
+
expect(diags).toHaveLength(1);
|
|
96
|
+
expect(diags[0].entity).toBe("Build_App");
|
|
97
|
+
});
|
|
83
98
|
});
|
|
@@ -49,4 +49,23 @@ jobs:
|
|
|
49
49
|
const diags = gha023.check(makeCtx(yaml));
|
|
50
50
|
expect(diags).toHaveLength(0);
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
test("flags ::set-output in a later name:-first step's block script (#3201)", () => {
|
|
54
|
+
const yaml = `name: CI
|
|
55
|
+
on: push
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- name: Checkout
|
|
61
|
+
uses: actions/checkout@v7
|
|
62
|
+
- name: Version
|
|
63
|
+
run: |
|
|
64
|
+
v=$(cat VERSION)
|
|
65
|
+
echo "::set-output name=version::$v"
|
|
66
|
+
`;
|
|
67
|
+
const diags = gha023.check(makeCtx(yaml));
|
|
68
|
+
expect(diags).toHaveLength(1);
|
|
69
|
+
expect(diags[0].message).toContain('step "Version"');
|
|
70
|
+
});
|
|
52
71
|
});
|
|
@@ -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
|
});
|
|
@@ -61,4 +61,24 @@ jobs:
|
|
|
61
61
|
const diags = gha038.check(makeCtx(yaml));
|
|
62
62
|
expect(diags).toHaveLength(0);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
test("flags a checkout step that starts with name: (#3201)", () => {
|
|
66
|
+
const yaml = `name: After CI
|
|
67
|
+
on:
|
|
68
|
+
workflow_run:
|
|
69
|
+
workflows: [CI]
|
|
70
|
+
types: [completed]
|
|
71
|
+
jobs:
|
|
72
|
+
report:
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
steps:
|
|
75
|
+
- name: Checkout
|
|
76
|
+
uses: actions/checkout@v7
|
|
77
|
+
- name: Report
|
|
78
|
+
run: ./report.sh
|
|
79
|
+
`;
|
|
80
|
+
const diags = gha038.check(makeCtx(yaml));
|
|
81
|
+
expect(diags).toHaveLength(1);
|
|
82
|
+
expect(diags[0].entity).toBe("report");
|
|
83
|
+
});
|
|
64
84
|
});
|
|
@@ -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 {
|