@intentius/chant 0.124.32 → 0.124.34
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/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -3
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/pr.d.ts +2 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +124 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +277 -74
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +99 -22
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +30 -1
- package/dist/cli/registry.d.ts +21 -5
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +9 -0
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +33 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +42 -11
- package/dist/components/pr-run.d.ts +1 -1
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +10 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +32 -4
- package/dist/graph-declared.d.ts +4 -0
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +11 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon.d.ts +27 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +14 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +15 -5
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/wave-runtime.mjs +236 -229
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +88 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +123 -0
- package/dist/workspace/checks/security.d.ts +50 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +8 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +12 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +89 -4
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +310 -23
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +35 -1
- package/dist/workspace/ci-run-index.d.ts +1 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.schema.json +1 -1
- package/dist/workspace/ci-run.d.ts +12 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +72 -16
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +76 -7
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +153 -19
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +35 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +12 -2
- package/dist/workspace/declaration.schema.json +51 -0
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/ls.d.ts +2 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -1
- package/dist/workspace/ls.schema.json +2 -1
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pr-report.schema.json +3 -3
- package/dist/workspace/reason-codes.d.ts +26 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +28 -0
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/handlers/build.test.ts +36 -0
- package/src/cli/handlers/build.ts +18 -3
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr.test.ts +22 -0
- package/src/cli/handlers/pr.ts +127 -4
- package/src/cli/handlers/promote.test.ts +17 -0
- package/src/cli/handlers/promote.ts +296 -84
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/main.ts +99 -22
- package/src/cli/parse-args.ts +22 -1
- package/src/cli/registry.ts +21 -5
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.ts +11 -0
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.ts +56 -15
- package/src/components/pr-run.ts +1 -1
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +32 -4
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.ts +9 -1
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon.ts +41 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/pr-loop.test.ts +29 -0
- package/src/pr-loop.ts +26 -7
- package/src/pr-report-url.ts +2 -2
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +88 -2
- package/src/workspace/checks/ci-workflow.test.ts +150 -0
- package/src/workspace/checks/ci-workflow.ts +129 -0
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +8 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +24 -0
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +5 -2
- package/src/workspace/ci-green.ts +359 -27
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -1
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run-index.schema.json +1 -1
- package/src/workspace/ci-run-index.ts +1 -1
- package/src/workspace/ci-run.test.ts +24 -7
- package/src/workspace/ci-run.ts +78 -16
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +59 -0
- package/src/workspace/ci-serve.ts +177 -22
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +51 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +53 -3
- package/src/workspace/init.ts +2 -2
- package/src/workspace/ls.schema.json +2 -1
- package/src/workspace/ls.ts +3 -2
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pr-report.schema.json +3 -3
- package/src/workspace/reason-codes.test.ts +4 -0
- package/src/workspace/reason-codes.ts +28 -0
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
|
@@ -284,9 +284,10 @@
|
|
|
284
284
|
"pass",
|
|
285
285
|
"fail",
|
|
286
286
|
"pending",
|
|
287
|
+
"unbuilt",
|
|
287
288
|
"unrecorded"
|
|
288
289
|
],
|
|
289
|
-
"description": "pass and fail come from the tags (a green tag records the phases that passed, a revoked tag the one that failed) or, for an undecided commit, from the forge under --forge, or from the signed Op runs on the local chant/lifecycle when the source is chant runs (#4044). pending is such a read with runs still going or not yet reported. unrecorded means no tag holds the result and nothing was read."
|
|
290
|
+
"description": "pass and fail come from the tags (a green tag records the phases that passed, a revoked tag the one that failed) or, for an undecided commit, from the forge under --forge, or from the signed Op runs on the local chant/lifecycle when the source is chant runs (#4044). pending is such a read with runs still going or not yet reported. unbuilt (#4169, chant 0.124.33 and newer) is such a read whose runs were cancelled. unrecorded means no tag holds the result and nothing was read."
|
|
290
291
|
},
|
|
291
292
|
"runs": {
|
|
292
293
|
"type": "array",
|
|
@@ -406,6 +407,39 @@
|
|
|
406
407
|
}
|
|
407
408
|
]
|
|
408
409
|
},
|
|
410
|
+
"record": {
|
|
411
|
+
"description": "Added within contract version 1 by #4169, written by chant 0.124.33 and newer. What the commit's _ci/<sha>.json record on the local chant/lifecycle says, the tick's last word on it, or null when it has none. state is green, red, unbuilt or pending. reason, coveredBy and culprit come from the required phases: why a phase is unbuilt or still pending (cancelled, superseded, retrying, requested, request-unserved or aged-out), the newer commit whose run covers a superseded phase, and the commit a bisect found broke a phase. Each is absent when no phase has one.",
|
|
412
|
+
"oneOf": [
|
|
413
|
+
{
|
|
414
|
+
"type": "null"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"type": "object",
|
|
418
|
+
"required": [
|
|
419
|
+
"state"
|
|
420
|
+
],
|
|
421
|
+
"properties": {
|
|
422
|
+
"state": {
|
|
423
|
+
"enum": [
|
|
424
|
+
"green",
|
|
425
|
+
"red",
|
|
426
|
+
"unbuilt",
|
|
427
|
+
"pending"
|
|
428
|
+
]
|
|
429
|
+
},
|
|
430
|
+
"reason": {
|
|
431
|
+
"type": "string"
|
|
432
|
+
},
|
|
433
|
+
"coveredBy": {
|
|
434
|
+
"type": "string"
|
|
435
|
+
},
|
|
436
|
+
"culprit": {
|
|
437
|
+
"type": "string"
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
},
|
|
409
443
|
"ignored": {
|
|
410
444
|
"type": "array",
|
|
411
445
|
"description": "Added within contract version 1 by #4031, written by chant 0.124.28 and newer. The commit's ci tags that do not count for want of a valid statement signed by a runner key. A tag on any commit that is not a strict first-parent ancestor of ci.green.signedSince needs one; empty when signedSince is not set. state, green and revoked count only the tags that are not ignored.",
|
|
@@ -44,6 +44,8 @@ const repoTemplate = repoFixture(() => {
|
|
|
44
44
|
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
45
45
|
git(root, "config", "user.name", "t");
|
|
46
46
|
git(root, "config", "user.email", "t@example.com");
|
|
47
|
+
git(root, "config", "gc.auto", "0");
|
|
48
|
+
git(root, "config", "maintenance.auto", "false");
|
|
47
49
|
writeFiles(root, { "chant.workspace.json": declaration(MEMBERS), ".gitignore": ".chant/pr/\n", "app/a.txt": "a\n" });
|
|
48
50
|
const commit = (msg: string) => {
|
|
49
51
|
execFileSync("git", ["-c", "commit.gpgsign=false", "add", "-A"], { cwd: root, stdio: "ignore" });
|
|
@@ -77,10 +79,13 @@ async function cli(cwd: string, argv: string[], run: (ctx: CommandContext) => Pr
|
|
|
77
79
|
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// A job image of its own, so the job gets no setup lines: the default image's
|
|
83
|
+
// OpenTofu install downloads from the network on a runner without tofu, as
|
|
84
|
+
// in ci-run.test.ts.
|
|
80
85
|
const runPlan = (w: ReturnType<typeof workspace>, extra: string[] = []) =>
|
|
81
86
|
cli(
|
|
82
87
|
w.app,
|
|
83
|
-
["ci", "run", "plan", "--env", "prod", "--base", w.base, "--pr", "7", "--forge", "forgejo", "--no-status", "--chant", w.chant, "-o", join(scratchDir("chant-report-url-out-"), "pr"), ...extra],
|
|
88
|
+
["ci", "run", "plan", "--image", "chant-ci-report-url-test:local", "--env", "prod", "--base", w.base, "--pr", "7", "--forge", "forgejo", "--no-status", "--chant", w.chant, "-o", join(scratchDir("chant-report-url-out-"), "pr"), ...extra],
|
|
84
89
|
runCiRun,
|
|
85
90
|
);
|
|
86
91
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"if": { "properties": { "status": { "const": "fail" } } },
|
|
24
24
|
"then": { "required": ["failure"], "properties": { "failure": { "type": "object" } } },
|
|
25
25
|
"properties": {
|
|
26
|
-
"job": { "enum": ["plan", "apply"] },
|
|
26
|
+
"job": { "enum": ["plan", "apply", "record"], "description": "record (#4168, chant 0.124.33 and newer): the merge record job." },
|
|
27
27
|
"pr": { "type": ["integer", "null"], "minimum": 1, "description": "The pull or merge request number: the one the job's report names, else --pr. null when neither has one." },
|
|
28
28
|
"commit": { "type": "string", "pattern": "^[0-9a-f]{40,64}$", "description": "The full commit the job ran at." },
|
|
29
29
|
"status": { "enum": ["ok", "fail"] },
|
|
@@ -44,7 +44,7 @@ export const CI_RUN_INDEX_LIMIT = 200;
|
|
|
44
44
|
|
|
45
45
|
/** One job the runner ran. */
|
|
46
46
|
export interface CiRunIndexEntry {
|
|
47
|
-
job: "plan" | "apply";
|
|
47
|
+
job: "plan" | "apply" | "record";
|
|
48
48
|
/** The pull request, or null when the job found none. */
|
|
49
49
|
pr: number | null;
|
|
50
50
|
commit: string;
|
|
@@ -206,7 +206,11 @@ describe("the job as an Op (#4094)", () => {
|
|
|
206
206
|
expect(ciLoopOptions(args)).toEqual({ gate: "infra", waveGate: "release" });
|
|
207
207
|
}
|
|
208
208
|
const [plan, apply] = prLoopJobs("forgejo", "prod", ciLoopOptions(parseArgs(["ci", "run", "apply", "--env", "prod", "--wave-gate", "release"])));
|
|
209
|
-
|
|
209
|
+
// #4168: no --require-review unless asked for.
|
|
210
|
+
expect(apply!.command).toContain(" --wave-gate release --resume .chant/pr-resume/pr-apply.json");
|
|
211
|
+
expect(apply!.command).not.toContain("--require-review");
|
|
212
|
+
const reviewed = prLoopJobs("forgejo", "prod", ciLoopOptions(parseArgs(["ci", "run", "apply", "--env", "prod", "--wave-gate", "release", "--require-review"])))[1]!;
|
|
213
|
+
expect(reviewed.command).toContain("--require-review --wave-gate release --resume .chant/pr-resume/pr-apply.json");
|
|
210
214
|
expect(apply!.command).not.toContain("--wave ");
|
|
211
215
|
expect(plan!.command).toContain("--wave-gate release");
|
|
212
216
|
expect(plan!.command).not.toContain("--wave ");
|
|
@@ -294,7 +298,7 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
294
298
|
|
|
295
299
|
expect(apply.status).toBe("ok");
|
|
296
300
|
const applyArgs = readFileSync(join(reports, "apply", "pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
297
|
-
expect(applyArgs).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--
|
|
301
|
+
expect(applyArgs).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
298
302
|
expect(formatCiRun(apply)).toContain("signed by chant-runner");
|
|
299
303
|
});
|
|
300
304
|
|
|
@@ -310,6 +314,17 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
310
314
|
expect(record.phases.apply![0]).toMatchObject({ name: "pr-apply", opRun: { id: apply.record.id, signer: "chant-runner" } });
|
|
311
315
|
});
|
|
312
316
|
|
|
317
|
+
test("under the isolated-runners security group an apply job requires a runner-start statement, and a plan job does not (#4168)", async () => {
|
|
318
|
+
const { root, app, base } = workspace();
|
|
319
|
+
writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app", security: ["isolated-runners"] }], { ci: { green: GREEN } }) });
|
|
320
|
+
git(root, "commit", "-qam", "isolated-runners");
|
|
321
|
+
git(root, "push", "-q", "origin", "main");
|
|
322
|
+
const job = (j: "plan" | "apply") =>
|
|
323
|
+
ciRun({ image: JOB_IMAGE, cwd: app, job: j, env: "prod", forge: "github", base, ...(j === "plan" ? { pr: 7 } : {}), chant: fakeChant(), hostEnv: HOST, output: join(scratchDir("chant-ci-run-out-"), "pr"), push: false });
|
|
324
|
+
await expect(job("apply")).rejects.toThrow(/--require-runner-start needs a forge job to check/);
|
|
325
|
+
expect((await job("plan")).status).toBe("ok");
|
|
326
|
+
});
|
|
327
|
+
|
|
313
328
|
test("a failing job is recorded failed, and a key from the environment that trust.json does not list leaves it unsigned, quietly", async () => {
|
|
314
329
|
const { root, app, base } = workspace();
|
|
315
330
|
const stranger = runnerKey();
|
|
@@ -746,12 +761,14 @@ describe("a generated forge job runs ci run --in-place (#4156)", () => {
|
|
|
746
761
|
// What a GitHub or Forgejo job has in its environment: the forge's run, job and attempt, and the secret.
|
|
747
762
|
const FORGE = { GITHUB_RUN_ID: "4242", GITHUB_RUN_ATTEMPT: "2", GITHUB_JOB: "apply", GITHUB_REPOSITORY: "acme/estate", GITHUB_SERVER_URL: "http://forgejo.localhost" };
|
|
748
763
|
|
|
749
|
-
test("the generated job's line parses to the same job, and --in-place, --no-push, --
|
|
750
|
-
const [plan, apply] = prLoopJobs("forgejo", "prod", { events: true, reportUrl: "s3://b/p", requireReview:
|
|
764
|
+
test("the generated job's line parses to the same job, and --in-place, --no-push, --require-review and --wave reach it", () => {
|
|
765
|
+
const [plan, apply] = prLoopJobs("forgejo", "prod", { events: true, reportUrl: "s3://b/p", requireReview: true, waveGate: "release", branch: "trunk" });
|
|
751
766
|
expect(plan!.run.command).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo --wave-gate release --events-json --report-url "s3://b/p"');
|
|
752
767
|
const args = parseArgs(apply!.run.command.replace(/"\$BASE_SHA"/, "b".repeat(40)).replace(/"/g, "").split(" ").slice(1).concat(["--wave", "2"]));
|
|
753
|
-
expect(args).toMatchObject({ ciInPlace: true, requireReview:
|
|
754
|
-
expect(ciLoopOptions(args)).toEqual({ gate: "pr-apply", branch: "trunk", events: true, reportUrl: "s3://b/p", waveGate: "release", requireReview:
|
|
768
|
+
expect(args).toMatchObject({ ciInPlace: true, requireReview: true, wave: 2, waveGate: "release", branch: "trunk", eventsJson: true, reportUrl: "s3://b/p" });
|
|
769
|
+
expect(ciLoopOptions(args)).toEqual({ gate: "pr-apply", branch: "trunk", events: true, reportUrl: "s3://b/p", waveGate: "release", requireReview: true });
|
|
770
|
+
// --no-require-review, which pipelines generated before #4168 pass, is the default now.
|
|
771
|
+
expect(ciLoopOptions(parseArgs(["ci", "run", "apply", "--no-require-review"]))).toEqual({});
|
|
755
772
|
expect(parseArgs(["ci", "run", "plan", "--no-push"]).noPush).toBe(true);
|
|
756
773
|
// ci run rebuilds pr-apply from those options: the very command the job ran before #4156.
|
|
757
774
|
expect(prLoopJobs("forgejo", "prod", ciLoopOptions(args))[1]!.command).toBe(apply!.command);
|
|
@@ -774,7 +791,7 @@ describe("a generated forge job runs ci run --in-place (#4156)", () => {
|
|
|
774
791
|
expect(r.report).toBe(join(realpathSync(app), ".chant/pr"));
|
|
775
792
|
expect(r.record.phases.map((p) => p.name)).toEqual(["apply"]);
|
|
776
793
|
const argv = readFileSync(join(app, ".chant/pr/pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
777
|
-
expect(argv).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--
|
|
794
|
+
expect(argv).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
778
795
|
// The forge job's whole environment reaches the step, but never the runner key.
|
|
779
796
|
const env = envFile(join(app, ".chant/pr"), "pr-apply");
|
|
780
797
|
expect(env).toMatchObject({ NOT_FOR_JOBS: "leak", GITHUB_RUN_ID: "4242", BASE_SHA: base, GITHUB_SHA: sha });
|
package/src/workspace/ci-run.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant ci run plan|apply` (#4094, epic #4101): run one pull-request job as
|
|
2
|
+
* `chant ci run plan|record|apply` (#4094, epic #4101, record #4168): run one pull-request job as
|
|
3
3
|
* a pipeline Op, the first slice of the chant runner.
|
|
4
4
|
*
|
|
5
5
|
* The job is the typed value `prLoopJobs` (../components/pr-pipeline.ts)
|
|
@@ -99,13 +99,22 @@ import { resolveMemberContext } from "./member-pipeline";
|
|
|
99
99
|
import { signOpRunRecord } from "./trust/op-run-statement";
|
|
100
100
|
import { gitTop } from "./tree";
|
|
101
101
|
import { hostRunnerKey } from "./ci-runner-key";
|
|
102
|
+
import { requireRunnerStart } from "./ci-runner-start";
|
|
103
|
+
import { securityGroupsAt } from "./security";
|
|
102
104
|
|
|
103
|
-
/** The
|
|
105
|
+
/** The jobs a pull-request pipeline has: plan, and on merge record (#4168) or, opted in, apply. */
|
|
104
106
|
export type CiJobName = PrLoopJob["jobName"];
|
|
105
|
-
export const CI_JOB_NAMES: readonly CiJobName[] = ["plan", "apply"];
|
|
107
|
+
export const CI_JOB_NAMES: readonly CiJobName[] = ["plan", "apply", "record"];
|
|
106
108
|
|
|
107
109
|
/** The Op each job is recorded as, which `ci.green` phases match. */
|
|
108
|
-
export const CI_JOB_OPS: Record<CiJobName, string> = { plan: "pr-plan", apply: "pr-apply" };
|
|
110
|
+
export const CI_JOB_OPS: Record<CiJobName, string> = { plan: "pr-plan", apply: "pr-apply", record: "pr-record" };
|
|
111
|
+
|
|
112
|
+
/** What each job's Op says it does. */
|
|
113
|
+
const CI_JOB_OVERVIEWS: Record<CiJobName, string> = {
|
|
114
|
+
plan: "Plan the members this pull request reaches",
|
|
115
|
+
apply: "Apply the plan a reviewer approved",
|
|
116
|
+
record: "Record the plan on the merged commit; nothing applies",
|
|
117
|
+
};
|
|
109
118
|
|
|
110
119
|
/** Where a job the chant runner ran says it ran (#4094). */
|
|
111
120
|
export const CHANT_RUNNER_ORIGIN: RunOrigin = { forge: "chant" };
|
|
@@ -255,7 +264,7 @@ export function ciJobOp(input: CiJobOpInput): OpConfig {
|
|
|
255
264
|
const op = CI_JOB_OPS[input.job.jobName];
|
|
256
265
|
return {
|
|
257
266
|
name: op,
|
|
258
|
-
overview: input.job.jobName
|
|
267
|
+
overview: CI_JOB_OVERVIEWS[input.job.jobName],
|
|
259
268
|
labels: { Env: input.env, Pipeline: "pr", Job: input.job.jobName },
|
|
260
269
|
phases,
|
|
261
270
|
};
|
|
@@ -353,12 +362,12 @@ const STATUS_DESCRIPTION = 140;
|
|
|
353
362
|
const cut = (text: string, max = STATUS_DESCRIPTION): string => (text.length > max ? `${text.slice(0, max - 3)}...` : text);
|
|
354
363
|
|
|
355
364
|
/**
|
|
356
|
-
* A job's status context: `chant/pr-plan` for the plan, and for the apply
|
|
365
|
+
* A job's status context: `chant/pr-plan` for the plan, `chant/pr-record` for the record (#4168), and for the apply
|
|
357
366
|
* `chant/<gate>`, which is `chant/pr-apply` unless `--gate` renamed the gate.
|
|
358
367
|
* pr-plan's own status is `chant/plan` (../pr-loop.ts), a different context.
|
|
359
368
|
*/
|
|
360
369
|
export function ciStatusContext(job: CiJobName, gate?: string): string {
|
|
361
|
-
return job === "apply" ? `chant/${gate ?? PR_APPLY_GATE}` : `chant/${CI_JOB_OPS
|
|
370
|
+
return job === "apply" ? `chant/${gate ?? PR_APPLY_GATE}` : `chant/${CI_JOB_OPS[job]}`;
|
|
362
371
|
}
|
|
363
372
|
|
|
364
373
|
/**
|
|
@@ -483,6 +492,12 @@ export interface CiRunRequest {
|
|
|
483
492
|
* `runOrigin: { forge: "local" }` and `runtime: "local"`.
|
|
484
493
|
*/
|
|
485
494
|
local?: boolean;
|
|
495
|
+
/**
|
|
496
|
+
* `--require-runner-start` (#4147): refuse the job before its first step
|
|
497
|
+
* unless a runner-start statement, signed by a runner key
|
|
498
|
+
* `.chant/trust.json` at `base` lists, names this forge job.
|
|
499
|
+
*/
|
|
500
|
+
requireRunnerStart?: boolean;
|
|
486
501
|
/**
|
|
487
502
|
* `--wave <k>` (#4003, #4156): the one wave a generated apply job runs, as
|
|
488
503
|
* `pr-apply --wave <k>`. Needs `--wave-gate`.
|
|
@@ -648,6 +663,10 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
648
663
|
const found = jobs.find((j) => j.jobName === req.job);
|
|
649
664
|
if (!found) throw new CiRunError(`the pull-request pipeline has no ${req.job} job; it has ${jobs.map((j) => j.jobName).join(" and ")}`);
|
|
650
665
|
if (req.wave !== undefined && (req.job !== "apply" || !req.loop?.waveGate)) throw new CiRunError("--wave names the one wave an apply runs, and takes --wave-gate: ci run apply --wave-gate <gate> --wave <k>");
|
|
666
|
+
// #4147: before anything runs or any secret reaches a step, the provisioner's word that this runner was started for this job.
|
|
667
|
+
// #4168: under the isolated-runners security group an apply job requires the statement too, unless it runs on this machine.
|
|
668
|
+
const isolated = !req.requireRunnerStart && !req.local && req.job === "apply" && securityGroupsAt(cwd).has("isolated-runners");
|
|
669
|
+
const started = req.requireRunnerStart || isolated ? runnerStartFor(req, { top, commit, hostEnv }) : undefined;
|
|
651
670
|
// A generated wave job's command, as prLoopWaveJobs writes it. #4161: a local job's command posts no status or note.
|
|
652
671
|
const command = `${found.command}${req.wave !== undefined ? ` --wave ${req.wave}` : ""}${req.local ? " --no-status" : ""}`;
|
|
653
672
|
const job = { ...found, command, ...(req.wave !== undefined ? { wave: req.wave } : {}) };
|
|
@@ -666,7 +685,8 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
666
685
|
// In place the forge's own variables come too; one whose value is short, such as a flag, is left out, so it can't blank common words.
|
|
667
686
|
const redact = ciRedactor(req.inPlace ? { ...longValues(jobEnv), ...own } : jobEnv, req.secrets);
|
|
668
687
|
try {
|
|
669
|
-
const
|
|
688
|
+
const ran = await runCiJob(req, { cwd, top, commit, place, job, jobEnv, ...(redact ? { redact } : {}) });
|
|
689
|
+
const result = started ? { ...ran, notes: [started, ...ran.notes] } : ran;
|
|
670
690
|
return redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
|
|
671
691
|
} catch (err) {
|
|
672
692
|
if (redact && err instanceof Error) err.message = redact(err.message);
|
|
@@ -674,6 +694,22 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
674
694
|
}
|
|
675
695
|
}
|
|
676
696
|
|
|
697
|
+
/**
|
|
698
|
+
* `--require-runner-start` (#4147): the runner-start statement for this
|
|
699
|
+
* forge job, checked against `.chant/trust.json` at `--base` (the commit
|
|
700
|
+
* before the change, as a push's `before` is), or at the job's commit
|
|
701
|
+
* without one. Throws {@link CiRunError} unless it verifies.
|
|
702
|
+
*/
|
|
703
|
+
function runnerStartFor(req: CiRunRequest, at: { top: string; commit: string; hostEnv: Record<string, string | undefined> }): string {
|
|
704
|
+
if (req.local) throw new CiRunError("--require-runner-start checks a forge job's runner, and --local runs the job on this machine");
|
|
705
|
+
const base = req.base ? resolveJobCommit(at.top, req.base) : at.commit;
|
|
706
|
+
try {
|
|
707
|
+
return requireRunnerStart({ top: at.top, base, commit: at.commit, env: at.hostEnv }).reason;
|
|
708
|
+
} catch (err) {
|
|
709
|
+
throw new CiRunError(message(err));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
677
713
|
/** The generator's options for a `--local` job (#4161): the same, with no report URL. */
|
|
678
714
|
function localLoop(loop: PrLoopPipelineOptions | undefined): PrLoopPipelineOptions {
|
|
679
715
|
const { reportUrl: _dropped, ...rest } = loop ?? {};
|
|
@@ -899,8 +935,8 @@ export function formatCiRun(r: CiRunResult): string {
|
|
|
899
935
|
* The generator's options from `ci run`'s and `ci serve`'s flags, as `chant
|
|
900
936
|
* build --components --generate <lexicon> --pr-loop` takes them: `--gate`,
|
|
901
937
|
* `--branch`, `--events-json`, `--report-url`, `--pr-build`,
|
|
902
|
-
* `--pr-build-command`, `--wave-gate` (#4003) and `--
|
|
903
|
-
* (#
|
|
938
|
+
* `--pr-build-command`, `--wave-gate` (#4003) and `--require-review`
|
|
939
|
+
* (#4168; `--no-require-review`, the default, is still accepted).
|
|
904
940
|
*/
|
|
905
941
|
export function ciLoopOptions(args: CommandContext["args"]): PrLoopPipelineOptions {
|
|
906
942
|
return {
|
|
@@ -911,15 +947,15 @@ export function ciLoopOptions(args: CommandContext["args"]): PrLoopPipelineOptio
|
|
|
911
947
|
...(args.prBuild ? { build: true } : {}),
|
|
912
948
|
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
913
949
|
...(args.waveGate ? { waveGate: args.waveGate } : {}),
|
|
914
|
-
...(args.requireReview ===
|
|
950
|
+
...(args.requireReview === true ? { requireReview: true } : {}),
|
|
915
951
|
};
|
|
916
952
|
}
|
|
917
953
|
|
|
918
954
|
const USAGE =
|
|
919
|
-
"chant ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
|
|
955
|
+
"chant ci run plan|record|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
|
|
920
956
|
"[--gate <name>] [--wave-gate <name>] [--wave <k>] [--branch <name>] [--events-json] [--report-url <url>] [--run-index-limit <n>] [--image <image>] [--before-script <line>]... " +
|
|
921
|
-
"[--pr-build] [--pr-build-command <line>] [--
|
|
922
|
-
"[--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [-o <dir>] [--in-place | --local] [--no-push] [--verbose] | chant ci run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]";
|
|
957
|
+
"[--pr-build] [--pr-build-command <line>] [--require-review] [--no-status] [--status-url <url template>] " +
|
|
958
|
+
"[--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [-o <dir>] [--in-place | --local] [--no-push] [--verbose] | chant ci run --at <sha> | chant ci run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]";
|
|
923
959
|
|
|
924
960
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
925
961
|
|
|
@@ -942,13 +978,38 @@ export function ciSecretsOf(flag: string | undefined, env: Record<string, string
|
|
|
942
978
|
return file === undefined ? undefined : readCiSecrets(resolve(file));
|
|
943
979
|
}
|
|
944
980
|
|
|
945
|
-
/**
|
|
981
|
+
/**
|
|
982
|
+
* `chant ci run --at <sha>` (#4169): ask for every required phase to be built
|
|
983
|
+
* at a commit of the green branch. On GitHub, with a token in the
|
|
984
|
+
* environment, the workflows holding the phases start at once.
|
|
985
|
+
*/
|
|
986
|
+
async function requestBuildCommand(at: string): Promise<number> {
|
|
987
|
+
const { requestBuild } = await import("./ci-green");
|
|
988
|
+
const { ciForgeFromEnv } = await import("./ci-green-forge");
|
|
989
|
+
let forge: Awaited<ReturnType<typeof ciForgeFromEnv>> | undefined;
|
|
990
|
+
try {
|
|
991
|
+
forge = process.env.GITHUB_REPOSITORY ? ciForgeFromEnv("github") : undefined;
|
|
992
|
+
} catch {
|
|
993
|
+
forge = undefined;
|
|
994
|
+
}
|
|
995
|
+
const r = await requestBuild({ cwd: process.cwd(), at, ...(forge ? { forge } : {}) });
|
|
996
|
+
for (const w of r.warnings) console.error(formatWarning({ message: w }));
|
|
997
|
+
const what = `${r.request.phases.join(", ")} at ${r.request.commit.slice(0, 12)}`;
|
|
998
|
+
console.log(
|
|
999
|
+
`${r.existed ? `${r.request.tag} already asks for a build` : `Asked for ${what}: ${r.request.tag}`}. ` +
|
|
1000
|
+
(r.dispatched.length > 0 ? `Started ${r.dispatched.join(", ")}.` : "The next ci tick starts it on GitHub, and ci serve --build-command serves it for chant runs."),
|
|
1001
|
+
);
|
|
1002
|
+
return 0;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** `chant ci run plan|record|apply` (#4094, #4168). */
|
|
946
1006
|
export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
947
1007
|
const { args } = ctx;
|
|
948
1008
|
try {
|
|
949
1009
|
if (args.setupKey) return (await import("./ci-serve")).runnerKeySetupCommand(ctx);
|
|
1010
|
+
if (args.at !== undefined && args.extraPositional === undefined) return await requestBuildCommand(args.at);
|
|
950
1011
|
const job = args.extraPositional;
|
|
951
|
-
if (job !== "plan" && job !== "apply") throw new CiRunError(`ci run takes the job to run, plan or apply${job ? `, not ${JSON.stringify(job)}` : ""}`);
|
|
1012
|
+
if (job !== "plan" && job !== "apply" && job !== "record") throw new CiRunError(`ci run takes the job to run, plan, record or apply${job ? `, not ${JSON.stringify(job)}` : ""}`);
|
|
952
1013
|
if (!args.env) throw new CiRunError("--env names the environment the pipeline is for, as chant build --components --generate <lexicon> --pr-loop took it: --env <env>");
|
|
953
1014
|
const forge = args.forge ?? detectForge(process.env);
|
|
954
1015
|
if (!forge) throw new CiRunError("--forge names the forge the job posts to: --forge github|gitlab|forgejo");
|
|
@@ -980,6 +1041,7 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
980
1041
|
...(args.local ? { local: true } : {}),
|
|
981
1042
|
...(args.noPush ? { push: false } : {}),
|
|
982
1043
|
...(args.wave !== undefined ? { wave: args.wave } : {}),
|
|
1044
|
+
...(args.requireRunnerStart ? { requireRunnerStart: true } : {}),
|
|
983
1045
|
...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
|
|
984
1046
|
...(args.image ? { image: args.image } : {}),
|
|
985
1047
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant ci runner-start` and `chant ci run --require-runner-start` (#4147):
|
|
3
|
+
* the statement checked against `.chant/trust.json` at base, and the
|
|
4
|
+
* readiness the isolated-runners security group reads (#4168).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterAll, describe, expect, test, vi } from "vitest";
|
|
11
|
+
import { parseArgs } from "../cli/parse-args";
|
|
12
|
+
import type { CommandContext } from "../cli/registry";
|
|
13
|
+
import { signRunnerStart, type CiJobIdentity } from "../runner-start";
|
|
14
|
+
import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
15
|
+
import { checkRunnerStart, parseJobSpec, requireRunnerStart, runCiRunnerStart, runnerStartReadiness, runnerStartReadinessAt } from "./ci-runner-start";
|
|
16
|
+
import { ciRun, CiRunError } from "./ci-run";
|
|
17
|
+
import { sshFromEd25519 } from "./trust/dsse";
|
|
18
|
+
import { emptyPolicy } from "./trust/policy";
|
|
19
|
+
|
|
20
|
+
afterAll(cleanScratch);
|
|
21
|
+
|
|
22
|
+
function key(principal: string): { principal: string; pem: string; ssh: string; signer: Parameters<typeof signRunnerStart>[1] } {
|
|
23
|
+
const { privateKey } = generateKeyPairSync("ed25519");
|
|
24
|
+
const jwk = createPublicKey(privateKey).export({ format: "jwk" }) as { x: string };
|
|
25
|
+
const ssh = sshFromEd25519(Buffer.from(jwk.x, "base64url"));
|
|
26
|
+
return { principal, pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), ssh, signer: { principal, key: privateKey, publicKey: ssh } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PROVISIONER = key("provisioner");
|
|
30
|
+
const LATER = key("late-provisioner");
|
|
31
|
+
|
|
32
|
+
/** A repository whose first commit lists PROVISIONER as a runner, and whose second adds LATER. */
|
|
33
|
+
function repo(): { root: string; base: string; head: string } {
|
|
34
|
+
const root = scratchDir("chant-runner-start-");
|
|
35
|
+
git(root, "init", "-q", "-b", "main");
|
|
36
|
+
git(root, "config", "user.name", "t");
|
|
37
|
+
git(root, "config", "user.email", "t@example.com");
|
|
38
|
+
git(root, "config", "commit.gpgsign", "false");
|
|
39
|
+
const trust = (runners: Array<{ principal: string; ssh: string }>) =>
|
|
40
|
+
JSON.stringify({ schema: 1, runners: runners.map((r) => ({ principal: r.principal, class: "runner", key: r.ssh })), roles: { runner: runners.map((r) => r.principal) } });
|
|
41
|
+
writeFiles(root, { ".chant/trust.json": trust([PROVISIONER]) });
|
|
42
|
+
git(root, "add", "-A");
|
|
43
|
+
git(root, "commit", "-q", "-m", "trust");
|
|
44
|
+
const base = git(root, "rev-parse", "HEAD");
|
|
45
|
+
writeFiles(root, { ".chant/trust.json": trust([PROVISIONER, LATER]) });
|
|
46
|
+
git(root, "add", "-A");
|
|
47
|
+
git(root, "commit", "-q", "-m", "a pull request adds its own provisioner");
|
|
48
|
+
return { root, base, head: git(root, "rev-parse", "HEAD") };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const R = repo();
|
|
52
|
+
|
|
53
|
+
const job = (commit: string): CiJobIdentity => ({ system: "github", repo: "acme/infra", run: "4242", attempt: "1", job: "apply", commit, runner: "jit-1" });
|
|
54
|
+
const env = (commit: string, statement?: unknown): Record<string, string> => ({
|
|
55
|
+
GITHUB_ACTIONS: "true",
|
|
56
|
+
GITHUB_REPOSITORY: "acme/infra",
|
|
57
|
+
GITHUB_RUN_ID: "4242",
|
|
58
|
+
GITHUB_RUN_ATTEMPT: "1",
|
|
59
|
+
GITHUB_JOB: "apply",
|
|
60
|
+
GITHUB_SHA: commit,
|
|
61
|
+
RUNNER_NAME: "jit-1",
|
|
62
|
+
CHANT_RUNNER_START_FILE: "/nonexistent/runner-start.json",
|
|
63
|
+
...(statement ? { CHANT_RUNNER_START: JSON.stringify(statement) } : {}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("checkRunnerStart reads the runner keys at base", () => {
|
|
67
|
+
test("a statement by a provisioner listed at base verifies", () => {
|
|
68
|
+
const v = checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, signRunnerStart(job(R.head), PROVISIONER.signer)) });
|
|
69
|
+
expect(v).toMatchObject({ status: "verified", signer: "provisioner" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("a provisioner the change itself lists does not count at the base before it", () => {
|
|
73
|
+
const statement = signRunnerStart(job(R.head), LATER.signer);
|
|
74
|
+
expect(checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, statement) }).status).toBe("untrusted");
|
|
75
|
+
expect(checkRunnerStart({ top: R.root, base: R.head, env: env(R.head, statement) }).status).toBe("verified");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("the job's commit stands in for the forge's variable, so a statement for another commit is refused", () => {
|
|
79
|
+
const statement = signRunnerStart(job(R.head), PROVISIONER.signer);
|
|
80
|
+
expect(checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, statement), commit: R.base }).status).toBe("mismatch");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("requireRunnerStart throws with the reason, and a missing statement is named", () => {
|
|
84
|
+
expect(() => requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: env(R.head) })).toThrow(/--require-runner-start: missing: .*has no runner-start statement/);
|
|
85
|
+
expect(() => requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: {} })).toThrow(/needs a forge job to check/);
|
|
86
|
+
expect(requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: env(R.head, signRunnerStart(job(R.head), PROVISIONER.signer)) }).signer).toBe("provisioner");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("runnerStartReadiness, for the isolated-runners group", () => {
|
|
91
|
+
test("a policy with a runner key whose principal holds the runner role is configured", () => {
|
|
92
|
+
expect(runnerStartReadinessAt(R.root, R.base)).toEqual({ configured: true, gaps: [] });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("no runner key, a key with no runner role, and an unreadable policy each name their gap", () => {
|
|
96
|
+
expect(runnerStartReadiness(emptyPolicy("x")).gaps.map((g) => g.gap)).toEqual(["no-runner-key"]);
|
|
97
|
+
const noRole = { ...emptyPolicy("x"), runners: [{ principal: "p", class: "runner" as const, key: PROVISIONER.ssh }] };
|
|
98
|
+
expect(runnerStartReadiness(noRole)).toMatchObject({ configured: false, gaps: [{ gap: "no-runner-role" }] });
|
|
99
|
+
expect(runnerStartReadiness(emptyPolicy("x", ["bad json"])).gaps.map((g) => g.gap)).toEqual(["trust-unreadable"]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("chant ci runner-start sign and verify", () => {
|
|
104
|
+
test("--job takes JSON or @file, and refuses fields chant does not know", () => {
|
|
105
|
+
const dir = scratchDir("chant-runner-start-job-");
|
|
106
|
+
writeFiles(dir, { "job.json": JSON.stringify(job(R.head)) });
|
|
107
|
+
expect(parseJobSpec(`@${join(dir, "job.json")}`)).toEqual(job(R.head));
|
|
108
|
+
expect(parseJobSpec('{"system":"gitlab","repo":"g/s/p","run":9,"job":77,"commit":"x"}')).toEqual({ system: "gitlab", repo: "g/s/p", run: "9", attempt: "1", job: "77", commit: "x" });
|
|
109
|
+
expect(() => parseJobSpec('{"system":"github","pr":7}')).toThrow(/fields chant does not know: pr/);
|
|
110
|
+
expect(parseArgs(["ci", "runner-start", "sign", "--key", "k.pem", "--principal", "provisioner", "--job", "@job.json"])).toMatchObject({ path: "runner-start", extraPositional: "sign", key: "k.pem", ciPrincipal: "provisioner", ciJob: "@job.json" });
|
|
111
|
+
expect(parseArgs(["ci", "run", "apply", "--env", "prod", "--require-runner-start"])).toMatchObject({ requireRunnerStart: true });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("sign writes an envelope the job then verifies, and verify exits 1 for another attempt", async () => {
|
|
115
|
+
const dir = scratchDir("chant-runner-start-cli-");
|
|
116
|
+
writeFiles(dir, { "key.pem": PROVISIONER.pem, "job.json": JSON.stringify(job(R.head)) });
|
|
117
|
+
const out = join(dir, "runner-start.json");
|
|
118
|
+
const ctx = (argv: string[]) => ({ args: parseArgs(argv) }) as unknown as CommandContext;
|
|
119
|
+
const quiet = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
120
|
+
try {
|
|
121
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "sign", "--key", join(dir, "key.pem"), "--principal", "provisioner", "--job", `@${join(dir, "job.json")}`, "-o", out]))).toBe(0);
|
|
122
|
+
const statement = JSON.parse(readFileSync(out, "utf8"));
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
process.chdir(R.root);
|
|
125
|
+
try {
|
|
126
|
+
vi.stubEnv("GITHUB_ACTIONS", "true");
|
|
127
|
+
for (const [k, v] of Object.entries(env(R.head))) vi.stubEnv(k, v);
|
|
128
|
+
vi.stubEnv("CHANT_RUNNER_START_FILE", out);
|
|
129
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "verify", "--base", R.base]))).toBe(0);
|
|
130
|
+
vi.stubEnv("GITHUB_RUN_ATTEMPT", "2");
|
|
131
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "verify", "--base", R.base]))).toBe(1);
|
|
132
|
+
} finally {
|
|
133
|
+
vi.unstubAllEnvs();
|
|
134
|
+
process.chdir(cwd);
|
|
135
|
+
}
|
|
136
|
+
expect(statement.payloadType).toBe("application/vnd.in-toto+json");
|
|
137
|
+
expect(await runCiRunnerStart(ctx(["ci", "runner-start", "nope"]))).toBe(1);
|
|
138
|
+
} finally {
|
|
139
|
+
quiet.mockRestore();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("ci run --require-runner-start", () => {
|
|
145
|
+
test("a job with no statement is refused before anything runs, and --local is refused with it", async () => {
|
|
146
|
+
const run = (extra: Partial<Parameters<typeof ciRun>[0]>) => ciRun({ cwd: R.root, job: "apply", env: "prod", forge: "github", base: R.base, hostEnv: env(R.head), push: false, requireRunnerStart: true, ...extra });
|
|
147
|
+
await expect(run({})).rejects.toThrow(CiRunError);
|
|
148
|
+
await expect(run({})).rejects.toThrow(/has no runner-start statement/);
|
|
149
|
+
await expect(run({ local: true, hostEnv: {} })).rejects.toThrow(/--local runs the job on this machine/);
|
|
150
|
+
});
|
|
151
|
+
});
|