@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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant ci runner-start sign|verify` and `chant ci run --require-runner-start`
|
|
3
|
+
* (#4147): the runner-start statement (../runner-start.ts) checked against
|
|
4
|
+
* `.chant/trust.json` at base.
|
|
5
|
+
*
|
|
6
|
+
* - `sign` is for the provisioner that starts a runner for one job: it signs
|
|
7
|
+
* the job's ids with a runner key the job never holds and prints the
|
|
8
|
+
* envelope, which the provisioner puts where the job looks for it.
|
|
9
|
+
* - `verify` is for the job: it names itself from the forge's variables,
|
|
10
|
+
* reads the statement, and fails unless a runner key `.chant/trust.json`
|
|
11
|
+
* at base lists, whose principal holds the `runner` role, signed it for
|
|
12
|
+
* this job.
|
|
13
|
+
* - `ci run --require-runner-start` makes the same check before the job's
|
|
14
|
+
* first step, so an apply refuses before it trades for any credentials.
|
|
15
|
+
*
|
|
16
|
+
* Requiring the statement is opt-in (#4156). With nothing asking for it,
|
|
17
|
+
* nothing reads it, and a runner with none never refuses, warns or hides
|
|
18
|
+
* anything.
|
|
19
|
+
*
|
|
20
|
+
* {@link runnerStartReadiness} says whether a repository could require it
|
|
21
|
+
* at base, and what is missing, for the isolated-runners security group
|
|
22
|
+
* (#4168, section 8), whose WSP165 check reports these gaps.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { execFileSync } from "node:child_process";
|
|
26
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { resolve } from "node:path";
|
|
28
|
+
import type { CommandContext } from "../cli/registry";
|
|
29
|
+
import { formatError } from "../cli/format";
|
|
30
|
+
import {
|
|
31
|
+
ciJobFromEnv,
|
|
32
|
+
RUNNER_START_SIGNER_ROLE,
|
|
33
|
+
signRunnerStart,
|
|
34
|
+
verifyRunnerStartForJob,
|
|
35
|
+
type CiJobIdentity,
|
|
36
|
+
type CiSystem,
|
|
37
|
+
type RunnerStartVerdict,
|
|
38
|
+
} from "../runner-start";
|
|
39
|
+
import { gitRevisionSource } from "./record-source";
|
|
40
|
+
import { gitTop } from "./tree";
|
|
41
|
+
import { loadRunnerKey } from "./trust/dsse";
|
|
42
|
+
import { emptyPolicy, readTrustPolicy, type TrustPolicy } from "./trust/policy";
|
|
43
|
+
|
|
44
|
+
/** The policy at `base` in the repository around `cwd`. */
|
|
45
|
+
function policyAt(top: string, base: string): TrustPolicy {
|
|
46
|
+
return readTrustPolicy(gitRevisionSource(top, base), base);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check this job's runner-start statement against `.chant/trust.json` at
|
|
51
|
+
* `base`. `commit` is the commit the job runs, in place of the forge's
|
|
52
|
+
* variable. Never throws.
|
|
53
|
+
*/
|
|
54
|
+
export function checkRunnerStart(input: { top: string; base: string; env?: Record<string, string | undefined>; commit?: string; readFile?: (path: string) => string }): RunnerStartVerdict {
|
|
55
|
+
const policy = policyAt(input.top, input.base);
|
|
56
|
+
if (policy.problems.length > 0) return { status: "untrusted", reason: `.chant/trust.json at ${input.base.slice(0, 12)} cannot be read: ${policy.problems.join("; ")}` };
|
|
57
|
+
return verifyRunnerStartForJob({
|
|
58
|
+
runners: policy.runners,
|
|
59
|
+
roles: policy.roles,
|
|
60
|
+
...(input.env ? { env: input.env } : {}),
|
|
61
|
+
...(input.commit ? { commit: input.commit } : {}),
|
|
62
|
+
...(input.readFile ? { readFile: input.readFile } : {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One thing a repository lacks before it can require runner-start statements. */
|
|
67
|
+
export interface RunnerStartGap {
|
|
68
|
+
/** Not a read-contract reason code: the isolated-runners group (#4168) maps each to its own security code. */
|
|
69
|
+
gap: "no-runner-key" | "no-runner-role" | "trust-unreadable";
|
|
70
|
+
reason: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether `.chant/trust.json` in `policy` lets a job require a runner-start
|
|
75
|
+
* statement: a key under `runners` whose principal holds the `runner` role.
|
|
76
|
+
* `configured` is true when there are no gaps. For the isolated-runners
|
|
77
|
+
* security group (#4168); nothing here refuses or warns.
|
|
78
|
+
*/
|
|
79
|
+
export function runnerStartReadiness(policy: TrustPolicy): { configured: boolean; gaps: RunnerStartGap[] } {
|
|
80
|
+
const gaps: RunnerStartGap[] = [];
|
|
81
|
+
if (policy.problems.length > 0) {
|
|
82
|
+
gaps.push({ gap: "trust-unreadable", reason: `.chant/trust.json cannot be read: ${policy.problems.join("; ")}` });
|
|
83
|
+
return { configured: false, gaps };
|
|
84
|
+
}
|
|
85
|
+
if (policy.runners.length === 0) {
|
|
86
|
+
gaps.push({ gap: "no-runner-key", reason: ".chant/trust.json lists no key under runners for a provisioner to sign with: chant workspace trust runner add <principal> --key <ssh-ed25519 line>" });
|
|
87
|
+
} else {
|
|
88
|
+
const holders = new Set((policy.roles[RUNNER_START_SIGNER_ROLE] ?? []).map((p) => p.normalize("NFKC").trim().toLowerCase()));
|
|
89
|
+
if (!policy.runners.some((r) => holders.has(r.principal.normalize("NFKC").trim().toLowerCase()))) {
|
|
90
|
+
gaps.push({ gap: "no-runner-role", reason: `no principal under runners holds the ${RUNNER_START_SIGNER_ROLE} role in .chant/trust.json: "roles": { "${RUNNER_START_SIGNER_ROLE}": ["<principal>"] }` });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { configured: gaps.length === 0, gaps };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** {@link runnerStartReadiness} for the repository around `cwd` at `base` (default `HEAD`). */
|
|
97
|
+
export function runnerStartReadinessAt(cwd: string, base = "HEAD"): { configured: boolean; gaps: RunnerStartGap[] } {
|
|
98
|
+
const top = gitTop(cwd);
|
|
99
|
+
if (!top) return runnerStartReadiness(emptyPolicy(null, [`${cwd} is not in a git repository`]));
|
|
100
|
+
const commit = revParse(top, base);
|
|
101
|
+
return runnerStartReadiness(commit ? policyAt(top, commit) : emptyPolicy(null, [`${base} is not a commit`]));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function revParse(top: string, rev: string): string | undefined {
|
|
105
|
+
try {
|
|
106
|
+
return execFileSync("git", ["rev-parse", "--verify", "-q", `${rev}^{commit}`], { cwd: top, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The base `verify` reads trust at: `--base`, else the remote's default branch, else `HEAD`. */
|
|
113
|
+
function verifyBase(top: string, base?: string): string {
|
|
114
|
+
const rev = base ?? (revParse(top, "refs/remotes/origin/HEAD") ? "refs/remotes/origin/HEAD" : "HEAD");
|
|
115
|
+
const commit = revParse(top, rev);
|
|
116
|
+
if (!commit) throw new Error(`--base ${rev} is not a commit in this repository`);
|
|
117
|
+
return commit;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The job named by `--job`: JSON, or `@<file>` holding it. */
|
|
121
|
+
export function parseJobSpec(spec: string): CiJobIdentity {
|
|
122
|
+
const text = spec.startsWith("@") ? readFileSync(resolve(spec.slice(1)), "utf8") : spec;
|
|
123
|
+
let raw: unknown;
|
|
124
|
+
try {
|
|
125
|
+
raw = JSON.parse(text);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error("--job takes the job as JSON, or @<file> holding it: {\"system\":\"github\",\"repo\":\"owner/name\",\"run\":\"<id>\",\"attempt\":\"1\",\"job\":\"apply\",\"commit\":\"<sha>\",\"runner\":\"<name>\"}");
|
|
128
|
+
}
|
|
129
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("--job takes a JSON object");
|
|
130
|
+
const o = raw as Record<string, unknown>;
|
|
131
|
+
const str = (k: string): string | undefined => (o[k] === undefined ? undefined : String(o[k]));
|
|
132
|
+
const known = ["system", "repo", "run", "attempt", "job", "commit", "runner"];
|
|
133
|
+
const extra = Object.keys(o).filter((k) => !known.includes(k));
|
|
134
|
+
if (extra.length > 0) throw new Error(`--job has fields chant does not know: ${extra.join(", ")}`);
|
|
135
|
+
return {
|
|
136
|
+
system: (str("system") ?? "") as CiSystem,
|
|
137
|
+
repo: str("repo") ?? "",
|
|
138
|
+
run: str("run") ?? "",
|
|
139
|
+
attempt: str("attempt") ?? "1",
|
|
140
|
+
job: str("job") ?? "",
|
|
141
|
+
commit: str("commit") ?? "",
|
|
142
|
+
...(str("runner") ? { runner: str("runner")! } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const USAGE = "chant ci runner-start sign --key <runner key.pem> --principal <name> --job <json|@file> [-o <file>]\n chant ci runner-start verify [--base <rev>] [--commit <sha>] [--json]";
|
|
147
|
+
|
|
148
|
+
/** `chant ci runner-start sign|verify`. */
|
|
149
|
+
export async function runCiRunnerStart(ctx: CommandContext): Promise<number> {
|
|
150
|
+
const { args } = ctx;
|
|
151
|
+
const action = args.extraPositional;
|
|
152
|
+
try {
|
|
153
|
+
if (action === "sign") {
|
|
154
|
+
if (!args.key) throw new Error("--key names the provisioner's runner key, an Ed25519 private key in PEM, which the job never holds");
|
|
155
|
+
if (!args.ciPrincipal) throw new Error("--principal names the principal .chant/trust.json lists the key for under runners");
|
|
156
|
+
if (!args.ciJob) throw new Error("--job names the job the runner was started for, as JSON or @<file>");
|
|
157
|
+
const job = parseJobSpec(args.ciJob);
|
|
158
|
+
const pem = readFileSync(resolve(args.key), "utf8");
|
|
159
|
+
const loaded = loadRunnerKey(pem);
|
|
160
|
+
const envelope = signRunnerStart(job, { principal: args.ciPrincipal, key: loaded.key, publicKey: loaded.publicKey });
|
|
161
|
+
const text = `${JSON.stringify(envelope)}\n`;
|
|
162
|
+
if (args.output) writeFileSync(resolve(args.output), text, { mode: 0o644 });
|
|
163
|
+
else process.stdout.write(text);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
if (action === "verify") {
|
|
167
|
+
const top = gitTop(process.cwd());
|
|
168
|
+
if (!top) throw new Error("verify reads .chant/trust.json at a commit, and this directory is not in a git repository");
|
|
169
|
+
const base = verifyBase(top, args.base);
|
|
170
|
+
const verdict = checkRunnerStart({ top, base, ...(args.commit ? { commit: args.commit } : {}) });
|
|
171
|
+
if (args.json) process.stdout.write(`${JSON.stringify({ ...verdict, base })}\n`);
|
|
172
|
+
else console.error(`ci runner-start: ${verdict.status}: ${verdict.reason}`);
|
|
173
|
+
return verdict.status === "verified" ? 0 : 1;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`ci runner-start takes sign or verify${action ? `, not ${JSON.stringify(action)}` : ""}`);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(formatError({ message: `chant ci runner-start: ${err instanceof Error ? err.message : String(err)}`, hint: USAGE }));
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The check `ci run --require-runner-start` makes before the job's first
|
|
184
|
+
* step: the statement for this job, verified against `.chant/trust.json` at
|
|
185
|
+
* `base`. Throws with the verdict's reason unless it verified.
|
|
186
|
+
*/
|
|
187
|
+
export function requireRunnerStart(input: { top: string; base: string; commit: string; env: Record<string, string | undefined> }): Extract<RunnerStartVerdict, { status: "verified" }> {
|
|
188
|
+
const named = ciJobFromEnv(input.env);
|
|
189
|
+
if (!named.ok) throw new Error(`--require-runner-start needs a forge job to check: ${named.reason}`);
|
|
190
|
+
const verdict = checkRunnerStart({ top: input.top, base: input.base, env: input.env, commit: input.commit });
|
|
191
|
+
if (verdict.status !== "verified") throw new Error(`--require-runner-start: ${verdict.status}: ${verdict.reason}`);
|
|
192
|
+
return verdict;
|
|
193
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4169 — `ci serve --build-command` serves each `ci/build/<sha>` tag once,
|
|
3
|
+
* within `maxJobs`, and records how each build ended.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
9
|
+
import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
10
|
+
import { buildRequestAnnotation, requestedBuild } from "./ci-builds";
|
|
11
|
+
import { ciServeBuilds, ciServePass, formatCiServeEvent, newCiServeRuntime, type CiServeEvent, type CiServeOptions } from "./ci-serve";
|
|
12
|
+
import type { CiGreen } from "./declaration";
|
|
13
|
+
|
|
14
|
+
afterAll(cleanScratch);
|
|
15
|
+
|
|
16
|
+
const GREEN = { branch: "main", phases: [], require: ["suites"] } as unknown as CiGreen;
|
|
17
|
+
|
|
18
|
+
function clone(): { root: string; shas: string[] } {
|
|
19
|
+
const origin = scratchDir("chant-serve-builds-origin-");
|
|
20
|
+
git(origin, "init", "-q", "--bare");
|
|
21
|
+
const root = scratchDir("chant-serve-builds-");
|
|
22
|
+
git(root, "init", "-q");
|
|
23
|
+
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
24
|
+
git(root, "config", "user.name", "t");
|
|
25
|
+
git(root, "config", "user.email", "t@example.com");
|
|
26
|
+
git(root, "remote", "add", "origin", origin);
|
|
27
|
+
const shas: string[] = [];
|
|
28
|
+
for (const n of ["a", "b"]) {
|
|
29
|
+
writeFiles(root, { [`${n}.txt`]: `${n}\n` });
|
|
30
|
+
git(root, "add", "-A");
|
|
31
|
+
git(root, "commit", "-q", "-m", n);
|
|
32
|
+
shas.push(git(root, "rev-parse", "HEAD"));
|
|
33
|
+
const r = requestedBuild(GREEN, shas.at(-1)!, ["suites"], new Date(`2026-10-20T1${shas.length}:00:00Z`));
|
|
34
|
+
git(root, "tag", "-a", "-m", buildRequestAnnotation(r), r.tag, r.commit);
|
|
35
|
+
}
|
|
36
|
+
git(root, "push", "-q", "origin", "main", "--tags");
|
|
37
|
+
// The serve's clone learns the tags from the forge.
|
|
38
|
+
for (const s of shas) git(root, "tag", "-d", `ci/build/${s}`);
|
|
39
|
+
return { root, shas };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const options = (root: string, built: string[], log: CiServeEvent[], extra: Partial<CiServeOptions> = {}): CiServeOptions => ({
|
|
43
|
+
cwd: root,
|
|
44
|
+
forge: "github",
|
|
45
|
+
repo: "acme/app",
|
|
46
|
+
env: "prod",
|
|
47
|
+
client: { openPullRequests: async () => [], branchHead: async () => undefined },
|
|
48
|
+
stateDir: join(root, ".serve"),
|
|
49
|
+
buildCommand: "true",
|
|
50
|
+
build: async ({ request }) => (built.push(request.commit), request.commit === built[0] ? 0 : 2),
|
|
51
|
+
log: (e) => log.push(e),
|
|
52
|
+
...extra,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("ci serve --build-command (#4169)", () => {
|
|
56
|
+
test("serves each request once, oldest first, within maxJobs, and records how it ended", async () => {
|
|
57
|
+
const { root, shas } = clone();
|
|
58
|
+
const built: string[] = [];
|
|
59
|
+
const log: CiServeEvent[] = [];
|
|
60
|
+
const rt = newCiServeRuntime();
|
|
61
|
+
const first = ciServeBuilds(options(root, built, log), rt);
|
|
62
|
+
expect(first.events.map((e) => e.type === "build" && e.status)).toEqual(["started", "held"]);
|
|
63
|
+
await Promise.all(first.started);
|
|
64
|
+
const second = ciServeBuilds(options(root, built, log), rt);
|
|
65
|
+
await Promise.all(second.started);
|
|
66
|
+
expect(built).toEqual(shas);
|
|
67
|
+
const state = JSON.parse(readFileSync(join(root, ".serve", "chant-apply-prod.json"), "utf-8"));
|
|
68
|
+
expect(Object.fromEntries(Object.entries(state.builds).map(([k, v]) => [k, (v as { status: string }).status]))).toEqual({ [`build:${shas[0]}`]: "ok", [`build:${shas[1]}`]: "fail" });
|
|
69
|
+
// Served once: another pass starts nothing.
|
|
70
|
+
expect(ciServeBuilds(options(root, built, log), rt).started).toEqual([]);
|
|
71
|
+
expect(log.map((e) => formatCiServeEvent(e))).toContain(`the build of suites at ${shas[1].slice(0, 12)} failed with exit code 2`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("without --build-command a pass leaves the tags alone; the default build runs the command in a worktree of the commit", async () => {
|
|
75
|
+
const { root, shas } = clone();
|
|
76
|
+
const log: CiServeEvent[] = [];
|
|
77
|
+
await ciServePass({ ...options(root, [], log), buildCommand: undefined });
|
|
78
|
+
expect(log.filter((e) => e.type === "build")).toEqual([]);
|
|
79
|
+
const out = join(root, ".serve-out");
|
|
80
|
+
await ciServePass({ ...options(root, [], log), build: undefined, buildCommand: `git rev-parse HEAD >> ${out}; echo "$CHANT_BUILD_PHASES $CHANT_BUILD_REASON" >> ${out}`, maxJobs: 2 });
|
|
81
|
+
expect(existsSync(out)).toBe(true);
|
|
82
|
+
const lines = readFileSync(out, "utf-8").trim().split("\n");
|
|
83
|
+
expect(lines.filter((l) => /^[0-9a-f]{40}$/.test(l)).sort()).toEqual([...shas].sort());
|
|
84
|
+
expect(lines.filter((l) => l === "suites requested")).toHaveLength(2);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -143,6 +143,8 @@ function serve(app: string, client: CiServeForge, extra: Partial<CiServeOptions>
|
|
|
143
143
|
client,
|
|
144
144
|
stateDir: state,
|
|
145
145
|
holder: "serve-test",
|
|
146
|
+
// Most tests here cover the apply on merge, which #4168 made opt-in; the record tests pass false.
|
|
147
|
+
applyOnMerge: true,
|
|
146
148
|
// A job image of its own, so no setup line downloads OpenTofu.
|
|
147
149
|
job: { image: "chant-ci-serve-test:local", chant: fakeChant(), hostEnv: env, passEnv: Object.keys(hostEnv ?? {}) },
|
|
148
150
|
log: (e) => events.push(e),
|
|
@@ -177,6 +179,63 @@ const isAncestor = (cwd: string, a: string, b: string): boolean => {
|
|
|
177
179
|
const argsOf = (report: string, stage: string): string[] => readFileSync(join(report, `${stage}.args`), "utf-8").trimEnd().split("\n");
|
|
178
180
|
const ofType = <T extends CiServeEvent["type"]>(events: CiServeEvent[], type: T) => events.filter((e): e is Extract<CiServeEvent, { type: T }> => e.type === type);
|
|
179
181
|
|
|
182
|
+
describe("ci serve records each merge unless apply on merge is opted in (#4168)", () => {
|
|
183
|
+
test("a merge to the target branch gets the record job, measured from the head before it, and no resume record", async () => {
|
|
184
|
+
const { root, app, main } = workspace();
|
|
185
|
+
const forge = fakeForge(main);
|
|
186
|
+
const { opts, events, state } = serve(app, forge, { applyOnMerge: false });
|
|
187
|
+
await ciServePass(opts);
|
|
188
|
+
const merged = commit(root, "app/merged.txt");
|
|
189
|
+
git(root, "push", "-q", "origin", "main");
|
|
190
|
+
forge.head = merged;
|
|
191
|
+
forge.merges.set(merged, 21);
|
|
192
|
+
|
|
193
|
+
const pass = await ciServePass(opts);
|
|
194
|
+
expect(pass.jobs).toEqual([expect.objectContaining({ job: "record", commit: merged, base: main, pr: 21, status: "ok" })]);
|
|
195
|
+
const argv = argsOf(pass.jobs[0]!.report!, "pr-record");
|
|
196
|
+
expect(argv.slice(0, 4)).toEqual(["components", "pr-record", "--base", main]);
|
|
197
|
+
expect(argv).not.toContain("--resume");
|
|
198
|
+
expect(ofType(events, "started")[0]).not.toHaveProperty("resume");
|
|
199
|
+
expect(existsSync(join(state, "resume"))).toBe(false);
|
|
200
|
+
const saved = readCiServeState(join(state, `${prApplyGroup("prod", "app")}.json`), { forge: "forgejo", repo: "demo/estate", env: "prod", branch: "main" });
|
|
201
|
+
expect(saved.target).toBe(merged);
|
|
202
|
+
expect(saved.jobs[ciServeKey({ job: "record", commit: merged })]).toMatchObject({ status: "ok" });
|
|
203
|
+
expect((await ciServePass(opts)).jobs).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("each settled job runs the tick, and a tick that fails is an error event, never a failed job", async () => {
|
|
207
|
+
const { root, app, main } = workspace();
|
|
208
|
+
const forge = fakeForge(main);
|
|
209
|
+
const ticks: string[] = [];
|
|
210
|
+
let fail = false;
|
|
211
|
+
const { opts, events } = serve(app, forge, {
|
|
212
|
+
applyOnMerge: false,
|
|
213
|
+
tick: async () => {
|
|
214
|
+
ticks.push("tick");
|
|
215
|
+
if (fail) throw new Error("forge said no");
|
|
216
|
+
return ["ci/green/abc"];
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
await ciServePass(opts);
|
|
220
|
+
expect(ticks).toEqual([]);
|
|
221
|
+
const merged = commit(root, "app/merged.txt");
|
|
222
|
+
git(root, "push", "-q", "origin", "main");
|
|
223
|
+
forge.head = merged;
|
|
224
|
+
await ciServePass(opts);
|
|
225
|
+
expect(ticks).toEqual(["tick"]);
|
|
226
|
+
expect(ofType(events, "tick")).toEqual([{ type: "tick", made: ["ci/green/abc"] }]);
|
|
227
|
+
expect(formatCiServeEvent({ type: "tick", made: ["ci/green/abc"] })).toBe("ci tick made ci/green/abc");
|
|
228
|
+
|
|
229
|
+
fail = true;
|
|
230
|
+
const next = commit(root, "app/next.txt");
|
|
231
|
+
git(root, "push", "-q", "origin", "main");
|
|
232
|
+
forge.head = next;
|
|
233
|
+
const pass = await ciServePass(opts);
|
|
234
|
+
expect(pass.jobs[0]).toMatchObject({ job: "record", status: "ok" });
|
|
235
|
+
expect(ofType(events, "error").map((e) => e.message)).toContain("ci tick after a job settled: forge said no");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
180
239
|
describe("chant ci serve (#4095)", () => {
|
|
181
240
|
test("one poll plans a new pull request head once, and skips forks and authors not given", async () => {
|
|
182
241
|
const { root, app, main } = workspace();
|