@intentius/chant 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +44 -16
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli/registry.d.ts +13 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +10 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +39 -0
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +5 -5
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +12 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.js +6 -4
- package/dist/workspace/ci-run-index.schema.json +5 -3
- package/dist/workspace/ci-run.d.ts +75 -2
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +195 -53
- package/dist/workspace/ci-runner-key.d.ts +16 -3
- package/dist/workspace/ci-runner-key.d.ts.map +1 -1
- package/dist/workspace/ci-runner-key.js +30 -10
- package/dist/workspace/ci-serve.d.ts +54 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +175 -26
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -2
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +18 -0
- package/src/cli/main.ts +43 -16
- package/src/cli/parse-args.ts +12 -0
- package/src/cli/registry.ts +19 -6
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +42 -0
- package/src/pr-forge.ts +40 -0
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +5 -5
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +5 -3
- package/src/workspace/ci-run-index.test.ts +42 -3
- package/src/workspace/ci-run-index.ts +18 -4
- package/src/workspace/ci-run.test.ts +185 -7
- package/src/workspace/ci-run.ts +224 -54
- package/src/workspace/ci-runner-key.test.ts +55 -17
- package/src/workspace/ci-runner-key.ts +36 -11
- package/src/workspace/ci-serve.test.ts +150 -6
- package/src/workspace/ci-serve.ts +169 -25
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.ts +2 -2
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
package/src/workspace/ci-run.ts
CHANGED
|
@@ -44,6 +44,30 @@
|
|
|
44
44
|
* With a forge client (#4097) the job also sets a commit status at its
|
|
45
45
|
* commit: `pending` when it starts, `success` or `failure` when it settles.
|
|
46
46
|
* A status the forge refuses is a warning and never fails the job.
|
|
47
|
+
*
|
|
48
|
+
* Signing is opt-in (#4156). With no runner key the record is unsigned, and
|
|
49
|
+
* a reader counts it with no signer. A key `.chant/trust.json` does not list
|
|
50
|
+
* leaves it unsigned with a note `--verbose` prints, unless the declaration
|
|
51
|
+
* sets `ci.green.signedSince`; then a run left unsigned is a warning.
|
|
52
|
+
*
|
|
53
|
+
* In place (`--in-place`, #4156) the job runs in the checkout it is started
|
|
54
|
+
* in, as a generated forge job does: no worktree, no setup phase and no
|
|
55
|
+
* commit status of its own (the forge shows the job). The steps get the
|
|
56
|
+
* job's whole environment less `CHANT_RUNNER_KEY`, since the forge job is
|
|
57
|
+
* already the boundary, and the record's `runOrigin` names the forge's run,
|
|
58
|
+
* job and attempt (`resolveCiJob`).
|
|
59
|
+
*
|
|
60
|
+
* Local (`--local`, #4161) runs the job the same way on this machine and
|
|
61
|
+
* leaves nothing outside it. It implies `--no-push` and `--no-status`, and
|
|
62
|
+
* writes nothing under a report URL, given or declared: the command gets no
|
|
63
|
+
* `--report-url`, and gets `--no-status`, so pr-plan and pr-apply post no
|
|
64
|
+
* status or note either. The record is still written to the local
|
|
65
|
+
* `chant/lifecycle`, where a reader on this machine finds it beside CI's for
|
|
66
|
+
* the same commit. It says `runOrigin: { forge: "local" }` and its events say
|
|
67
|
+
* `runtime: "local"`. `ci tick --source runs` and `ci list` never count it.
|
|
68
|
+
* A pr-apply still pushes its own release records and gate facts, as the
|
|
69
|
+
* command always does, since a local apply changes the same infrastructure
|
|
70
|
+
* a CI one does.
|
|
47
71
|
*/
|
|
48
72
|
|
|
49
73
|
import { execFileSync } from "node:child_process";
|
|
@@ -55,8 +79,8 @@ import { detectForge, ForgeEnvironmentError, forgeFromEnv, FORGE_KINDS, type Com
|
|
|
55
79
|
import { PR_APPLY_GATE } from "../pr-loop";
|
|
56
80
|
import type { CommandContext } from "../cli/registry";
|
|
57
81
|
import { formatError, formatWarning } from "../cli/format";
|
|
58
|
-
import type
|
|
59
|
-
import { pushLifecycleReport } from "../lifecycle/git";
|
|
82
|
+
import { resolveCiJob, type RunOrigin } from "../lifecycle/release-ledger";
|
|
83
|
+
import { fetchLifecycleStatus, pushLifecycleReport } from "../lifecycle/git";
|
|
60
84
|
import { ACTIVITY_PROFILES } from "../op/activity-profiles";
|
|
61
85
|
import type { ActivityFn, ActivityProfile } from "../op/activity-registry";
|
|
62
86
|
import { shellCmd } from "../op/activities/shell";
|
|
@@ -66,7 +90,9 @@ import type { ActivityStep, OpConfig, PhaseDefinition } from "../op/types";
|
|
|
66
90
|
import { redactText, type Redact } from "../op/redact";
|
|
67
91
|
import { findWorkspaceRoot } from "../project-root";
|
|
68
92
|
import { parseReportUrl, reportKey } from "../pr-report-url";
|
|
69
|
-
import {
|
|
93
|
+
import { reportUrlOf } from "./ci-report-url";
|
|
94
|
+
import { ciTagSigner, RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green";
|
|
95
|
+
import { loadRunnerKey } from "./trust/dsse";
|
|
70
96
|
import { ciRedactor, ciSecretsFor, CI_SECRETS_ENV, readCiSecrets, type CiSecrets } from "./ci-secrets";
|
|
71
97
|
import { updateCiRunIndex, type CiRunIndexEntry } from "./ci-run-index";
|
|
72
98
|
import { resolveMemberContext } from "./member-pipeline";
|
|
@@ -84,6 +110,9 @@ export const CI_JOB_OPS: Record<CiJobName, string> = { plan: "pr-plan", apply: "
|
|
|
84
110
|
/** Where a job the chant runner ran says it ran (#4094). */
|
|
85
111
|
export const CHANT_RUNNER_ORIGIN: RunOrigin = { forge: "chant" };
|
|
86
112
|
|
|
113
|
+
/** Where a job `chant ci run --local` ran says it ran (#4161): this machine, outside CI. */
|
|
114
|
+
export const CHANT_LOCAL_ORIGIN: RunOrigin = { forge: "local" };
|
|
115
|
+
|
|
87
116
|
/** The runner's variables every job gets, when the runner has them. */
|
|
88
117
|
export const CI_RUN_BASE_ENV = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TZ"] as const;
|
|
89
118
|
|
|
@@ -241,6 +270,17 @@ function exitCodeOf(error: string | undefined): number | undefined {
|
|
|
241
270
|
return m ? Number(m[1]) : undefined;
|
|
242
271
|
}
|
|
243
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The code `chant ci run` exits with for a settled job: 0 when it passed,
|
|
275
|
+
* else its command's own code when that is 1 to 255, such as 3 for a moved
|
|
276
|
+
* plan or a waiting wave (#4156), else 1. The run index records the same
|
|
277
|
+
* code (#4165).
|
|
278
|
+
*/
|
|
279
|
+
export function ciJobExitCode(result: { status: OpRunResult["status"]; exitCode?: number }): number {
|
|
280
|
+
if (result.status === "ok") return 0;
|
|
281
|
+
return result.exitCode && result.exitCode > 0 && result.exitCode < 256 ? result.exitCode : 1;
|
|
282
|
+
}
|
|
283
|
+
|
|
244
284
|
/** The first line of a step's error, for a reason. */
|
|
245
285
|
const firstLine = (text: string): string => text.split("\n").find((l) => l.trim() !== "")?.trim() ?? text;
|
|
246
286
|
|
|
@@ -378,7 +418,12 @@ export interface CiRunRequest {
|
|
|
378
418
|
base?: string;
|
|
379
419
|
pr?: number;
|
|
380
420
|
commit?: string;
|
|
381
|
-
/**
|
|
421
|
+
/**
|
|
422
|
+
* The generator's options: `--gate`, `--branch`, `--events-json`,
|
|
423
|
+
* `--report-url`, `--pr-build` and `--pr-build-command`, and `--wave-gate`
|
|
424
|
+
* (#4003), with which the apply job runs the waves in order and stops at
|
|
425
|
+
* the first whose moved plan waits on its own gate.
|
|
426
|
+
*/
|
|
382
427
|
loop?: PrLoopPipelineOptions;
|
|
383
428
|
/** `--image`: the image a generated pipeline names. Only the default gets setup lines. */
|
|
384
429
|
image?: string;
|
|
@@ -425,6 +470,24 @@ export interface CiRunRequest {
|
|
|
425
470
|
resumeRecord?: string;
|
|
426
471
|
/** Set a commit status at the job's commit (#4097). Absent: none is set. `chant ci run` builds it from the environment. */
|
|
427
472
|
status?: CiStatusConfig;
|
|
473
|
+
/**
|
|
474
|
+
* `--in-place` (#4156): run in the checkout `cwd` is in, at its `HEAD`, as
|
|
475
|
+
* a generated forge job does, with the runner's whole environment less
|
|
476
|
+
* `CHANT_RUNNER_KEY`, no setup phase, and the forge's run as `runOrigin`.
|
|
477
|
+
*/
|
|
478
|
+
inPlace?: boolean;
|
|
479
|
+
/**
|
|
480
|
+
* `--local` (#4161): run on this machine and write nothing beyond it. It
|
|
481
|
+
* implies `push: false` and no status (`status` is ignored), writes
|
|
482
|
+
* nothing under `loop.reportUrl`, and the record and events say
|
|
483
|
+
* `runOrigin: { forge: "local" }` and `runtime: "local"`.
|
|
484
|
+
*/
|
|
485
|
+
local?: boolean;
|
|
486
|
+
/**
|
|
487
|
+
* `--wave <k>` (#4003, #4156): the one wave a generated apply job runs, as
|
|
488
|
+
* `pr-apply --wave <k>`. Needs `--wave-gate`.
|
|
489
|
+
*/
|
|
490
|
+
wave?: number;
|
|
428
491
|
signal?: AbortSignal;
|
|
429
492
|
}
|
|
430
493
|
|
|
@@ -443,8 +506,14 @@ export interface CiRunResult {
|
|
|
443
506
|
pushWarning?: string;
|
|
444
507
|
/** What went wrong beside the job, such as a record the key could not sign. */
|
|
445
508
|
warnings: string[];
|
|
509
|
+
/** What `--verbose` prints: such as a run left unsigned when nothing asks for a signature (#4156). */
|
|
510
|
+
notes: string[];
|
|
511
|
+
/** The exit code of the job's command when it failed with one, such as pr-apply's 3 for a moved plan. */
|
|
512
|
+
exitCode?: number;
|
|
446
513
|
/** The checkout the job ran in, removed by now. */
|
|
447
514
|
checkout: string;
|
|
515
|
+
/** The job ran with `local` (#4161): its record stays on this machine. */
|
|
516
|
+
local?: true;
|
|
448
517
|
}
|
|
449
518
|
|
|
450
519
|
const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
@@ -516,6 +585,11 @@ async function recordInIndex(input: {
|
|
|
516
585
|
member?: string;
|
|
517
586
|
pr?: number;
|
|
518
587
|
record: OpRunRecord;
|
|
588
|
+
runOrigin: RunOrigin;
|
|
589
|
+
/** The runner that signed the record, or null (#4156). */
|
|
590
|
+
signer: string | null;
|
|
591
|
+
/** The code `chant ci run` exits with for the job (#4165). */
|
|
592
|
+
exitCode: number;
|
|
519
593
|
reportDir: string;
|
|
520
594
|
limit?: number;
|
|
521
595
|
/** The run's redactor (#4096): every string written to the index goes through it, as the copied report does. */
|
|
@@ -547,7 +621,9 @@ async function recordInIndex(input: {
|
|
|
547
621
|
ended: input.record.ended,
|
|
548
622
|
runId: clean(input.record.id),
|
|
549
623
|
key: clean(reportKey(pr, input.commit, input.member)),
|
|
550
|
-
runOrigin:
|
|
624
|
+
runOrigin: input.runOrigin,
|
|
625
|
+
signer: input.signer,
|
|
626
|
+
exitCode: input.exitCode,
|
|
551
627
|
};
|
|
552
628
|
await updateCiRunIndex({ target, entry, ...(input.limit ? { limit: input.limit } : {}), warn: input.warn });
|
|
553
629
|
}
|
|
@@ -565,11 +641,17 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
565
641
|
const top = gitTop(cwd);
|
|
566
642
|
if (!top) throw new CiRunError("a job runs in a fresh checkout of a commit, and this directory is not in a git repository");
|
|
567
643
|
const commit = resolveJobCommit(top, req.commit ?? "HEAD");
|
|
644
|
+
if (req.inPlace && commit !== resolveJobCommit(top, "HEAD")) throw new CiRunError(`--in-place runs the checkout's HEAD, and --commit names ${commit.slice(0, 12)}`);
|
|
645
|
+
if (req.local && req.inPlace) throw new CiRunError("--in-place runs a forge job in the forge's checkout, and --local runs one on this machine; pass one of them");
|
|
568
646
|
const place = ciJobPlace(cwd, top);
|
|
569
|
-
const jobs = prLoopJobs(req.forge, req.env, req.loop ?? {}, place.member);
|
|
570
|
-
const
|
|
571
|
-
if (!
|
|
572
|
-
|
|
647
|
+
const jobs = prLoopJobs(req.forge, req.env, req.local ? localLoop(req.loop) : (req.loop ?? {}), place.member);
|
|
648
|
+
const found = jobs.find((j) => j.jobName === req.job);
|
|
649
|
+
if (!found) throw new CiRunError(`the pull-request pipeline has no ${req.job} job; it has ${jobs.map((j) => j.jobName).join(" and ")}`);
|
|
650
|
+
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>");
|
|
651
|
+
// A generated wave job's command, as prLoopWaveJobs writes it. #4161: a local job's command posts no status or note.
|
|
652
|
+
const command = `${found.command}${req.wave !== undefined ? ` --wave ${req.wave}` : ""}${req.local ? " --no-status" : ""}`;
|
|
653
|
+
const job = { ...found, command, ...(req.wave !== undefined ? { wave: req.wave } : {}) };
|
|
654
|
+
const own = ciJobEnv(job, {
|
|
573
655
|
forge: req.forge,
|
|
574
656
|
env: req.env,
|
|
575
657
|
values: { base: req.base, pr: req.pr, commit },
|
|
@@ -577,37 +659,90 @@ export async function ciRun(req: CiRunRequest): Promise<CiRunResult> {
|
|
|
577
659
|
passEnv: req.passEnv,
|
|
578
660
|
secrets: ciSecretsFor(req.secrets, req.job),
|
|
579
661
|
});
|
|
662
|
+
// #4156: in place, the forge job's environment is the job's, less the runner key.
|
|
663
|
+
const jobEnv = req.inPlace ? { ...definedEnv(hostEnv), ...own } : own;
|
|
664
|
+
delete jobEnv[RUNNER_KEY_ENV];
|
|
580
665
|
// #4096: the secrets and the job's secret-looking variables, redacted from everything the run writes and returns.
|
|
581
|
-
|
|
666
|
+
// 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
|
+
const redact = ciRedactor(req.inPlace ? { ...longValues(jobEnv), ...own } : jobEnv, req.secrets);
|
|
582
668
|
try {
|
|
583
669
|
const result = await runCiJob(req, { cwd, top, commit, place, job, jobEnv, ...(redact ? { redact } : {}) });
|
|
584
|
-
return redact ? { ...result, warnings: result.warnings.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
|
|
670
|
+
return redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
|
|
585
671
|
} catch (err) {
|
|
586
672
|
if (redact && err instanceof Error) err.message = redact(err.message);
|
|
587
673
|
throw err;
|
|
588
674
|
}
|
|
589
675
|
}
|
|
590
676
|
|
|
677
|
+
/** The generator's options for a `--local` job (#4161): the same, with no report URL. */
|
|
678
|
+
function localLoop(loop: PrLoopPipelineOptions | undefined): PrLoopPipelineOptions {
|
|
679
|
+
const { reportUrl: _dropped, ...rest } = loop ?? {};
|
|
680
|
+
return rest;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** The variables of `env` whose value is long enough to be a credential, 8 characters or more. */
|
|
684
|
+
function longValues(env: Record<string, string>): Record<string, string> {
|
|
685
|
+
return Object.fromEntries(Object.entries(env).filter(([, value]) => value.length >= 8));
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** The variables of `env` that have a value. */
|
|
689
|
+
function definedEnv(env: Record<string, string | undefined>): Record<string, string> {
|
|
690
|
+
const out: Record<string, string> = {};
|
|
691
|
+
for (const [name, value] of Object.entries(env)) if (value !== undefined) out[name] = value;
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Who signs the run (#4156): the runner key, checked against
|
|
697
|
+
* `.chant/trust.json` at the commit. A key that isn't an Ed25519 PEM is an
|
|
698
|
+
* error, and so is a `--key` the file does not list. A key from the
|
|
699
|
+
* environment that it does not list, and no key at all, leave the run
|
|
700
|
+
* unsigned: a warning when the declaration's `ci.green.signedSince` asks
|
|
701
|
+
* for signed runs, a note for `--verbose` otherwise.
|
|
702
|
+
*/
|
|
703
|
+
function ciRunSigner(req: CiRunRequest, at: { cwd: string; top: string; commit: string }): { signer?: ReturnType<typeof ciTagSigner>; warnings: string[]; notes: string[] } {
|
|
704
|
+
// #4161: a local run never counts, signed or not, so nothing about its signature warns.
|
|
705
|
+
const required = req.local ? null : signedRunsRequired(at.cwd);
|
|
706
|
+
const say = (text: string) => (required ? { warnings: [`${text}, and ci.green.signedSince is ${required}, so it will not count`], notes: [] } : { warnings: [], notes: [text] });
|
|
707
|
+
if (req.runnerKey === undefined) return say("there is no runner key, so the run's record is unsigned");
|
|
708
|
+
try {
|
|
709
|
+
loadRunnerKey(req.runnerKey);
|
|
710
|
+
} catch (err) {
|
|
711
|
+
throw new CiRunError(`the runner key${req.runnerKeyFromEnv ? ` in ${RUNNER_KEY_ENV}` : ""} is not an Ed25519 private key in PEM: ${message(err)}`);
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
return { signer: ciTagSigner(at.top, at.commit, req.runnerKey, "the runs it signed"), warnings: [], notes: [] };
|
|
715
|
+
} catch (err) {
|
|
716
|
+
if (!req.runnerKeyFromEnv) throw new CiRunError(`the run would not be signed: ${message(err)}`);
|
|
717
|
+
return say(`${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message(err)}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Where the job runs: a fresh worktree of the commit, or with `inPlace` the checkout itself, which is left as it is. */
|
|
722
|
+
function jobCheckout(top: string, commit: string, inPlace?: boolean): { dir: string; remove(): void } {
|
|
723
|
+
return inPlace ? { dir: top, remove() {} } : freshCheckout(top, commit);
|
|
724
|
+
}
|
|
725
|
+
|
|
591
726
|
/** {@link ciRun} once the job and its environment are known. */
|
|
592
727
|
async function runCiJob(
|
|
593
728
|
req: CiRunRequest,
|
|
594
729
|
at: { cwd: string; top: string; commit: string; place: { member?: string; dir: string }; job: PrLoopJob; jobEnv: Record<string, string>; redact?: Redact },
|
|
595
730
|
): Promise<CiRunResult> {
|
|
596
731
|
const { cwd, top, commit, place, job, jobEnv, redact } = at;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const checkout =
|
|
732
|
+
const local = req.local === true;
|
|
733
|
+
const status = local ? undefined : req.status;
|
|
734
|
+
const reportUrl = local ? undefined : req.loop?.reportUrl;
|
|
735
|
+
|
|
736
|
+
// The record is signed against .chant/trust.json at the commit the job runs, so a --key that commit does not list is refused before anything runs.
|
|
737
|
+
const signing = ciRunSigner(req, { cwd, top, commit });
|
|
738
|
+
const signer = signing.signer;
|
|
739
|
+
const early = signing.warnings;
|
|
740
|
+
const notes = signing.notes;
|
|
741
|
+
const origin: RunOrigin = local ? CHANT_LOCAL_ORIGIN : req.inPlace ? (resolveCiJob(req.hostEnv ?? process.env)?.runOrigin ?? CHANT_RUNNER_ORIGIN) : CHANT_RUNNER_ORIGIN;
|
|
742
|
+
// In place, the job writes its record on the checkout's chant/lifecycle, which a forge checkout may not have fetched.
|
|
743
|
+
if (req.inPlace && req.push !== false) await fetchLifecycleStatus({ cwd: top }).catch(() => undefined);
|
|
744
|
+
|
|
745
|
+
const checkout = jobCheckout(top, commit, req.inPlace);
|
|
611
746
|
try {
|
|
612
747
|
const jobDir = place.dir === "." ? checkout.dir : join(checkout.dir, place.dir);
|
|
613
748
|
const op = ciJobOp({
|
|
@@ -615,7 +750,7 @@ async function runCiJob(
|
|
|
615
750
|
env: req.env,
|
|
616
751
|
cwd: jobDir,
|
|
617
752
|
jobEnv,
|
|
618
|
-
setup: prLoopSetup(req.image ?? PR_LOOP_IMAGE),
|
|
753
|
+
setup: req.inPlace ? [] : prLoopSetup(req.image ?? PR_LOOP_IMAGE),
|
|
619
754
|
build: [...(req.beforeScript ?? []), ...(job.build ?? [])],
|
|
620
755
|
...(req.chant ? { chant: req.chant } : {}),
|
|
621
756
|
});
|
|
@@ -627,13 +762,13 @@ async function runCiJob(
|
|
|
627
762
|
copyFileSync(resume.host, resume.job);
|
|
628
763
|
}
|
|
629
764
|
const statusContext = ciStatusContext(req.job, req.loop?.gate);
|
|
630
|
-
const statusUrl =
|
|
631
|
-
? ciStatusUrl({ ...(
|
|
765
|
+
const statusUrl = status
|
|
766
|
+
? ciStatusUrl({ ...(status.urlTemplate ? { urlTemplate: status.urlTemplate } : {}), ...(reportUrl ? { reportUrl } : {}), ...(req.pr !== undefined ? { pr: req.pr } : {}), commit, ...(place.member ? { member: place.member } : {}) })
|
|
632
767
|
: undefined;
|
|
633
768
|
const postStatus = async (state: CommitStatus["state"], description: string): Promise<void> => {
|
|
634
|
-
if (!
|
|
769
|
+
if (!status) return;
|
|
635
770
|
try {
|
|
636
|
-
await
|
|
771
|
+
await status.client.setCommitStatus(commit, { context: statusContext, state, description: cut(redact ? redact(description) : description), ...(statusUrl ? { targetUrl: statusUrl } : {}) });
|
|
637
772
|
} catch (err) {
|
|
638
773
|
warnings.push(`could not set the ${statusContext} status: ${message(err)}`);
|
|
639
774
|
}
|
|
@@ -649,7 +784,7 @@ async function runCiJob(
|
|
|
649
784
|
cwd: jobDir,
|
|
650
785
|
commit,
|
|
651
786
|
ledger: { cwd: jobDir },
|
|
652
|
-
runner: { kind: "pipeline", runtime: "ci", runOrigin:
|
|
787
|
+
runner: { kind: "pipeline", runtime: local ? "local" : "ci", runOrigin: origin },
|
|
653
788
|
describeFailure: (records) => ciJobFailure({ job: req.job, op, records, reportDir: join(jobDir, PR_LOOP_REPORT_DIR) }),
|
|
654
789
|
...(req.runEvents ? { runEvents: { write: req.runEvents } } : {}),
|
|
655
790
|
...(req.stepOutput ? { stepOutput: req.stepOutput } : {}),
|
|
@@ -687,26 +822,37 @@ async function runCiJob(
|
|
|
687
822
|
let report: string | null = null;
|
|
688
823
|
if (existsSync(written)) {
|
|
689
824
|
report = resolve(cwd, req.output ?? PR_LOOP_REPORT_DIR);
|
|
690
|
-
|
|
691
|
-
if (
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
825
|
+
// In place the report is already where the forge keeps it, and stays as the command wrote it.
|
|
826
|
+
if (!(existsSync(report) && realpathSync(report) === realpathSync(written))) {
|
|
827
|
+
cpSync(written, report, { recursive: true });
|
|
828
|
+
if (redact) redactTree(report, redact);
|
|
829
|
+
}
|
|
695
830
|
}
|
|
696
|
-
if (ledgerError) throw new CiRunError(ledgerError);
|
|
697
|
-
const pushed: { pushed: boolean; pushWarning?: string } = req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
|
|
698
831
|
const failed = result.records.find((r) => r.status === "fail");
|
|
832
|
+
const exitCode = failed?.phase === req.job ? exitCodeOf(failed.error) : undefined;
|
|
833
|
+
if (reportUrl) {
|
|
834
|
+
await recordInIndex({ url: reportUrl, job: req.job, commit, member: place.member, pr: req.pr, record: result.record, runOrigin: origin, signer: result.record.statement && signer ? signer.principal : null, exitCode: ciJobExitCode({ status: result.status, ...(exitCode !== undefined ? { exitCode } : {}) }), reportDir: written, limit: req.runIndexLimit, ...(redact ? { redact } : {}), warn: (w) => warnings.push(w) });
|
|
835
|
+
}
|
|
836
|
+
// In place the forge job's outcome is the command's, so a record that could not be written is a warning there.
|
|
837
|
+
if (ledgerError && req.inPlace) warnings.push(ledgerError);
|
|
838
|
+
else if (ledgerError) throw new CiRunError(ledgerError);
|
|
839
|
+
const pushed: { pushed: boolean; pushWarning?: string } = local
|
|
840
|
+
? { pushed: false }
|
|
841
|
+
: req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
|
|
699
842
|
return {
|
|
700
843
|
op: op.name,
|
|
701
844
|
status: result.status,
|
|
702
845
|
record: result.record,
|
|
703
846
|
...(failed ? { failure: { phase: failed.phase, error: failed.error ?? "the step failed" } } : {}),
|
|
847
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
704
848
|
report,
|
|
705
|
-
signer: signer
|
|
849
|
+
signer: result.record.statement && signer ? signer.principal : null,
|
|
706
850
|
pushed: pushed.pushed,
|
|
707
851
|
...(pushed.pushWarning ? { pushWarning: pushed.pushWarning } : {}),
|
|
708
852
|
warnings,
|
|
853
|
+
notes,
|
|
709
854
|
checkout: checkout.dir,
|
|
855
|
+
...(local ? { local: true as const } : {}),
|
|
710
856
|
};
|
|
711
857
|
} finally {
|
|
712
858
|
checkout.remove();
|
|
@@ -740,19 +886,40 @@ export function formatCiRun(r: CiRunResult): string {
|
|
|
740
886
|
const why = r.record.failure;
|
|
741
887
|
if (why) lines.push(` failure: ${why.code}${why.phase ? ` in ${why.phase}` : ""}${why.exitCode !== undefined ? `, exit ${why.exitCode}` : ""}: ${why.reason}`);
|
|
742
888
|
else if (r.failure) lines.push(` ${r.failure.phase}: ${r.failure.error}`);
|
|
743
|
-
lines.push(` record: ${r.record.env}/runs__${r.op}.jsonl on chant/lifecycle, ${r.signer ? `signed by ${r.signer}` : "unsigned
|
|
889
|
+
lines.push(` record: ${r.record.env}/runs__${r.op}.jsonl on chant/lifecycle, ${r.signer ? `signed by ${r.signer}` : "unsigned"}`);
|
|
744
890
|
if (r.report) lines.push(` report: ${r.report}`);
|
|
745
|
-
if (
|
|
891
|
+
if (r.local) lines.push(" local run: the record stays on this machine, with no push, commit status or report upload");
|
|
892
|
+
else if (!r.pushed) lines.push(` ${r.pushWarning ?? "chant/lifecycle was not pushed"}`);
|
|
746
893
|
return lines.join("\n");
|
|
747
894
|
}
|
|
748
895
|
|
|
749
896
|
// ── The command ──────────────────────────────────────────────────────────────
|
|
750
897
|
|
|
898
|
+
/**
|
|
899
|
+
* The generator's options from `ci run`'s and `ci serve`'s flags, as `chant
|
|
900
|
+
* build --components --generate <lexicon> --pr-loop` takes them: `--gate`,
|
|
901
|
+
* `--branch`, `--events-json`, `--report-url`, `--pr-build`,
|
|
902
|
+
* `--pr-build-command`, `--wave-gate` (#4003) and `--no-require-review`
|
|
903
|
+
* (#4156).
|
|
904
|
+
*/
|
|
905
|
+
export function ciLoopOptions(args: CommandContext["args"]): PrLoopPipelineOptions {
|
|
906
|
+
return {
|
|
907
|
+
...(args.gate ? { gate: args.gate } : {}),
|
|
908
|
+
...(args.branch ? { branch: args.branch } : {}),
|
|
909
|
+
...(args.eventsJson ? { events: true } : {}),
|
|
910
|
+
...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
|
|
911
|
+
...(args.prBuild ? { build: true } : {}),
|
|
912
|
+
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
913
|
+
...(args.waveGate ? { waveGate: args.waveGate } : {}),
|
|
914
|
+
...(args.requireReview === false ? { requireReview: false } : {}),
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
751
918
|
const USAGE =
|
|
752
919
|
"chant ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
|
|
753
|
-
"[--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--run-index-limit <n>] [--image <image>] [--before-script <line>]... " +
|
|
754
|
-
"[--pr-build] [--pr-build-command <line>] [--no-status] [--status-url <url template>] " +
|
|
755
|
-
"[--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [-o <dir>] | chant ci run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]";
|
|
920
|
+
"[--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>] [--no-require-review] [--no-status] [--status-url <url template>] " +
|
|
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>]";
|
|
756
923
|
|
|
757
924
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
758
925
|
|
|
@@ -792,9 +959,13 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
792
959
|
const hostKey = fromFlag || process.env[RUNNER_KEY_ENV] ? undefined : hostRunnerKeyAt(process.cwd(), args.statePath);
|
|
793
960
|
const runnerKey = fromFlag ? readFileSync(resolve(args.key!), "utf-8") : process.env[RUNNER_KEY_ENV] || hostKey?.pem;
|
|
794
961
|
const hostEnv = process.env;
|
|
795
|
-
|
|
962
|
+
// #4156: in place the forge shows the job, so ci run sets no status of its own.
|
|
963
|
+
// #4161: a local run sets no status, so it builds no client and warns about none.
|
|
964
|
+
const { status, warning } = ciStatusFromEnv({ forge: forge as ForgeKind, hostEnv, noStatus: args.noStatus || args.ciInPlace || args.local, statusUrl: args.statusUrl });
|
|
796
965
|
if (warning) console.error(formatWarning({ message: warning }));
|
|
797
966
|
const secrets = ciSecretsOf(args.ciSecrets);
|
|
967
|
+
// #4160: without --report-url, the declaration's ci.reportUrl.
|
|
968
|
+
const reportUrl = await reportUrlOf(args.reportUrl, process.cwd());
|
|
798
969
|
const result = await ciRun({
|
|
799
970
|
cwd: process.cwd(),
|
|
800
971
|
...(status ? { status } : {}),
|
|
@@ -804,14 +975,11 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
804
975
|
...(args.base ? { base: args.base } : {}),
|
|
805
976
|
...(args.pr !== undefined ? { pr: args.pr } : {}),
|
|
806
977
|
...(args.commit ? { commit: args.commit } : {}),
|
|
807
|
-
loop: {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
...(args.prBuild ? { build: true } : {}),
|
|
813
|
-
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
814
|
-
},
|
|
978
|
+
loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
|
|
979
|
+
...(args.ciInPlace ? { inPlace: true } : {}),
|
|
980
|
+
...(args.local ? { local: true } : {}),
|
|
981
|
+
...(args.noPush ? { push: false } : {}),
|
|
982
|
+
...(args.wave !== undefined ? { wave: args.wave } : {}),
|
|
815
983
|
...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
|
|
816
984
|
...(args.image ? { image: args.image } : {}),
|
|
817
985
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
@@ -824,8 +992,10 @@ export async function runCiRun(ctx: CommandContext): Promise<number> {
|
|
|
824
992
|
stepOutput: (line) => void process.stderr.write(`${line}\n`),
|
|
825
993
|
});
|
|
826
994
|
for (const w of result.warnings) console.error(formatWarning({ message: w }));
|
|
995
|
+
if (args.verbose) for (const n of result.notes) console.error(`ci run: ${n}`);
|
|
827
996
|
console.error(formatCiRun(result));
|
|
828
|
-
|
|
997
|
+
// #4156: a failed command's own exit code, such as 3 for a moved plan, is the job's, as it was before the job ran through ci run.
|
|
998
|
+
return ciJobExitCode(result);
|
|
829
999
|
} catch (err) {
|
|
830
1000
|
console.error(formatError({ message: `chant ci run: ${message(err)}`, hint: USAGE }));
|
|
831
1001
|
return 1;
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* #4099: the runner key on a `chant ci serve` or `chant ci run` host. The
|
|
3
3
|
* key is made in the state directory with mode 0600, `--setup-key --trust`
|
|
4
4
|
* proposes its runner entry on a branch through `trust runner add` (#3510)
|
|
5
|
-
* and never writes the target branch
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* and never writes the target branch. Signing is opt-in (#4156): `ci serve`
|
|
6
|
+
* starts with no key and says nothing about it, and serves unsigned with one
|
|
7
|
+
* warning when `.chant/trust.json` at the target's head does not list its
|
|
8
|
+
* key. With `ci.green.signedSince` it refuses to start in either case.
|
|
8
9
|
*
|
|
9
10
|
* Every test works in its own temp repository with a bare origin and its own
|
|
10
11
|
* state directory. The jobs' `chant` is a stub script, and no job runs: the
|
|
@@ -126,9 +127,12 @@ describe("the host's runner key (#4099)", () => {
|
|
|
126
127
|
expect(trust.roles.runner).toEqual(["chant-runner:demo"]);
|
|
127
128
|
expect(git(origin, "rev-parse", `${branch}^`)).toBe(main);
|
|
128
129
|
|
|
129
|
-
// Until it merges, ci serve refuses the
|
|
130
|
+
// Until it merges, ci serve serves unsigned with a warning (refuses with the opt-in); once it has, the key signs as the principal.
|
|
130
131
|
const key = hostRunnerKey(state)!;
|
|
131
|
-
|
|
132
|
+
const pending = ciServeRunnerKey({ top: root, stateDir: state, branch: "main" });
|
|
133
|
+
expect(pending.pem).toBeUndefined();
|
|
134
|
+
expect(pending.warning).toMatch(/not trusted at main/);
|
|
135
|
+
expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main", required: main })).toThrow(/not trusted at main/);
|
|
132
136
|
git(root, "push", "-q", "origin", `refs/remotes/origin/${branch}:refs/heads/main`);
|
|
133
137
|
expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main" })).toMatchObject({ pem: key.pem, source: join(state, RUNNER_KEY_FILE) });
|
|
134
138
|
expect(ciTagSigner(root, git(root, "rev-parse", "refs/remotes/origin/main"), key.pem).principal).toBe("chant-runner:demo");
|
|
@@ -138,29 +142,63 @@ describe("the host's runner key (#4099)", () => {
|
|
|
138
142
|
expect(listed.err).toContain("lists it for chant-runner:demo, with the runner role");
|
|
139
143
|
});
|
|
140
144
|
|
|
141
|
-
test("ci serve
|
|
145
|
+
test("ci serve starts with no key and says nothing, and serves unsigned with one warning for a key trust.json does not list (#4156)", async () => {
|
|
142
146
|
const { root } = workspace();
|
|
143
147
|
const state = scratchDir("chant-runner-key-state-");
|
|
144
148
|
|
|
149
|
+
// No key: nothing about keys is printed. The serve goes on to the forge, which this test has no token for.
|
|
145
150
|
const none = await cli(root, SERVE(state));
|
|
146
|
-
expect(none.
|
|
147
|
-
expect(none.err).toContain("
|
|
148
|
-
|
|
151
|
+
expect(none.err.split("\n")).toHaveLength(2);
|
|
152
|
+
expect(none.err.split("\n")[0]).toContain("chant ci serve: --forge forgejo needs GITHUB_TOKEN");
|
|
153
|
+
const keyless = ciServeRunnerKey({ top: root, stateDir: state, branch: "main" });
|
|
154
|
+
expect(keyless.pem).toBeUndefined();
|
|
155
|
+
expect(keyless.warning).toBeUndefined();
|
|
156
|
+
expect(keyless.note).toContain("not signed");
|
|
157
|
+
// --verbose prints that one line.
|
|
158
|
+
const verbose = await cli(root, [...SERVE(state), "--verbose"]);
|
|
159
|
+
expect(verbose.err).toContain("ci serve: runs are not signed: there is no runner key");
|
|
160
|
+
|
|
161
|
+
// A key the head does not list: one warning at startup, and no key goes to the jobs.
|
|
149
162
|
ensureHostRunnerKey(state);
|
|
150
163
|
const unlisted = await cli(root, SERVE(state));
|
|
151
|
-
expect(unlisted.
|
|
152
|
-
expect(unlisted.err).
|
|
164
|
+
expect(unlisted.err.match(/not trusted at main [0-9a-f]{12}/g)).toHaveLength(1);
|
|
165
|
+
expect(unlisted.err).toContain("Runs are not signed until it is");
|
|
153
166
|
expect(unlisted.err).toContain("chant ci serve --setup-key --trust");
|
|
154
|
-
expect(unlisted.err).toContain("
|
|
167
|
+
expect(unlisted.err).not.toContain("signing runs with");
|
|
155
168
|
|
|
156
|
-
// A key listed only in the working tree, not at the head, is
|
|
169
|
+
// A key listed only in the working tree, not at the head, is not trusted either.
|
|
157
170
|
const other = generateKeyPairSync("ed25519").privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
158
171
|
writeFileSync(join(state, "other.pem"), other, { mode: 0o600 });
|
|
159
172
|
expect(trustWrite({ cwd: root, action: "runner add", name: "ci-bot", key: join(state, "other.pem") })).toMatchObject({ changed: true });
|
|
160
|
-
const flagged =
|
|
161
|
-
expect(flagged.
|
|
162
|
-
expect(flagged.
|
|
173
|
+
const flagged = ciServeRunnerKey({ top: root, stateDir: state, branch: "main", keyFile: join(state, "other.pem") });
|
|
174
|
+
expect(flagged.pem).toBeUndefined();
|
|
175
|
+
expect(flagged.warning).toContain("lists no runner with this key");
|
|
176
|
+
|
|
177
|
+
// A key file that is not a key is an error either way.
|
|
178
|
+
writeFileSync(join(state, "bad.pem"), "not a key", { mode: 0o600 });
|
|
179
|
+
expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main", keyFile: join(state, "bad.pem") })).toThrow(/not an Ed25519 private key in PEM/);
|
|
180
|
+
|
|
181
|
+
// --unsigned is accepted and serves unsigned.
|
|
182
|
+
expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main", unsigned: true })).toMatchObject({ fromFlag: false, source: "none (--unsigned)" });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("with ci.green.signedSince, ci serve refuses to start without a trusted key, --unsigned or not (#4156)", async () => {
|
|
186
|
+
const { root, main } = workspace();
|
|
187
|
+
const state = scratchDir("chant-runner-key-state-");
|
|
188
|
+
const green = { branch: "main", phases: { plan: ["pr-plan"] }, require: ["plan"], source: "runs", signedSince: main };
|
|
189
|
+
writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green } }) });
|
|
163
190
|
|
|
164
|
-
|
|
191
|
+
const none = await cli(root, SERVE(state));
|
|
192
|
+
expect(none.code).toBe(1);
|
|
193
|
+
expect(none.err).toContain(`ci.green.signedSince is ${main}, so every run must be signed, and there is no runner key`);
|
|
194
|
+
const unsigned = await cli(root, [...SERVE(state), "--unsigned"]);
|
|
195
|
+
expect(unsigned.code).toBe(1);
|
|
196
|
+
expect(unsigned.err).toContain("there is no runner key");
|
|
197
|
+
|
|
198
|
+
ensureHostRunnerKey(state);
|
|
199
|
+
const unlisted = await cli(root, SERVE(state));
|
|
200
|
+
expect(unlisted.code).toBe(1);
|
|
201
|
+
expect(unlisted.err).toMatch(/not trusted at main [0-9a-f]{12}/);
|
|
202
|
+
expect(unlisted.err).toContain("chant workspace trust runner add");
|
|
165
203
|
});
|
|
166
204
|
});
|
|
@@ -217,17 +217,38 @@ export function formatRunnerKeySetup(s: RunnerKeySetup, branch: string): string
|
|
|
217
217
|
/**
|
|
218
218
|
* The key `ci serve` signs with, checked before its first poll (#4099): the
|
|
219
219
|
* `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
|
|
220
|
-
* directory.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* directory. Signing is opt-in (#4156):
|
|
221
|
+
*
|
|
222
|
+
* - With no key, `ci serve` serves unsigned and says so only in `note`.
|
|
223
|
+
* - A key `.chant/trust.json` at the target's head does not list with the
|
|
224
|
+
* runner role, or a target branch it can't find to check against, leaves
|
|
225
|
+
* it serving unsigned, with one `warning`.
|
|
226
|
+
* - With `required` (the declaration's `ci.green.signedSince`) each of
|
|
227
|
+
* those refuses to start, as before #4156.
|
|
228
|
+
*
|
|
229
|
+
* A key that is not an Ed25519 private key in PEM, or a `--key` file that
|
|
230
|
+
* can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
|
|
231
|
+
* key without `required` and is ignored with it.
|
|
223
232
|
*/
|
|
224
|
-
export function ciServeRunnerKey(req: {
|
|
225
|
-
|
|
233
|
+
export function ciServeRunnerKey(req: {
|
|
234
|
+
top: string;
|
|
235
|
+
stateDir: string;
|
|
236
|
+
branch: string;
|
|
237
|
+
keyFile?: string;
|
|
238
|
+
envKey?: string;
|
|
239
|
+
unsigned?: boolean;
|
|
240
|
+
/** The declaration's `ci.green.signedSince`, when it sets one: every run must then be signed. */
|
|
241
|
+
required?: string | null;
|
|
242
|
+
}): { pem?: string; fromFlag: boolean; source: string; warning?: string; note?: string } {
|
|
243
|
+
const required = req.required ?? null;
|
|
244
|
+
const optIn = `ci.green.signedSince is ${required}, so every run must be signed`;
|
|
245
|
+
if (req.unsigned && !required) return { fromFlag: false, source: "none (--unsigned)", note: "runs are not signed (--unsigned)" };
|
|
226
246
|
const host = req.keyFile === undefined && !req.envKey ? hostRunnerKey(req.stateDir) : undefined;
|
|
227
247
|
const pem = req.keyFile !== undefined ? readFileSync(req.keyFile, "utf-8") : req.envKey || host?.pem;
|
|
228
248
|
const source = req.keyFile !== undefined ? req.keyFile : req.envKey ? "CHANT_RUNNER_KEY" : (host?.path ?? join(req.stateDir, RUNNER_KEY_FILE));
|
|
229
249
|
if (pem === undefined) {
|
|
230
|
-
|
|
250
|
+
if (!required) return { fromFlag: false, source: "none", note: "runs are not signed: there is no runner key. To verify who ran a job, add one with chant ci serve --setup-key --trust" };
|
|
251
|
+
throw new CiRunError(`${optIn}, and there is no runner key: no --key, no CHANT_RUNNER_KEY and no ${source}. Make one with chant ci serve --setup-key and list it with --trust`);
|
|
231
252
|
}
|
|
232
253
|
let publicKey: string;
|
|
233
254
|
try {
|
|
@@ -236,13 +257,17 @@ export function ciServeRunnerKey(req: { top: string; stateDir: string; branch: s
|
|
|
236
257
|
throw new CiRunError(`the runner key from ${source} is not an Ed25519 private key in PEM: ${message(err)}`);
|
|
237
258
|
}
|
|
238
259
|
const head = targetHead(req.top, req.branch);
|
|
239
|
-
if (!head)
|
|
260
|
+
if (!head) {
|
|
261
|
+
const why = `there is no branch ${req.branch} here or on origin, so the runner key can't be checked against its .chant/trust.json`;
|
|
262
|
+
if (required) throw new CiRunError(`${optIn}, and ${why}; fetch it`);
|
|
263
|
+
return { fromFlag: false, source, warning: `${why}, so runs are not signed; fetch it to sign them` };
|
|
264
|
+
}
|
|
240
265
|
const listed = runnerKeyListed(req.top, head.commit, pem);
|
|
241
266
|
if (!listed.ok) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
267
|
+
const why = `the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}`;
|
|
268
|
+
const fix = `List it on a branch for review with chant ci serve --setup-key --trust, or run ${trustCommand(defaultRunnerPrincipal(), publicKey)} in a checkout of ${req.branch} and merge the change.`;
|
|
269
|
+
if (required) throw new CiRunError(`${optIn}, and ${why}\n ${fix}`);
|
|
270
|
+
return { fromFlag: false, source, warning: `${why}. Runs are not signed until it is. ${fix}` };
|
|
246
271
|
}
|
|
247
272
|
return { pem, fromFlag: req.keyFile !== undefined, source };
|
|
248
273
|
}
|