@intentius/chant 0.124.30 → 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 +74 -12
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +65 -0
- package/dist/cli/registry.d.ts +29 -2
- 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/local-executor.d.ts +12 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -3
- 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/redact.d.ts +34 -0
- package/dist/op/redact.d.ts.map +1 -0
- package/dist/op/redact.js +94 -0
- package/dist/op/step-console.d.ts +2 -0
- package/dist/op/step-console.d.ts.map +1 -1
- package/dist/op/step-console.js +5 -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 +27 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +51 -6
- 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 +10 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +52 -6
- 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 +108 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +164 -0
- package/dist/workspace/ci-run-index.schema.json +52 -0
- package/dist/workspace/ci-run.d.ts +159 -9
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +419 -50
- package/dist/workspace/ci-runner-key.d.ts +133 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +237 -0
- package/dist/workspace/ci-secrets.d.ts +65 -0
- package/dist/workspace/ci-secrets.d.ts.map +1 -0
- package/dist/workspace/ci-secrets.js +211 -0
- package/dist/workspace/ci-serve.d.ts +67 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +240 -35
- 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 +7 -3
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +9 -4
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/trust/trust-write.d.ts +109 -0
- package/dist/workspace/trust/trust-write.d.ts.map +1 -0
- package/dist/workspace/trust/trust-write.js +320 -0
- package/dist/workspace/trust-write.schema.json +265 -0
- 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 +19 -0
- package/src/cli/main.ts +73 -12
- package/src/cli/parse-args.ts +48 -0
- package/src/cli/registry.ts +35 -4
- 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/local-executor.ts +31 -3
- 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/redact.ts +93 -0
- package/src/op/step-console.ts +7 -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 +71 -0
- package/src/pr-forge.ts +70 -6
- 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 +58 -6
- 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 +52 -0
- package/src/workspace/ci-run-index.test.ts +392 -0
- package/src/workspace/ci-run-index.ts +206 -0
- package/src/workspace/ci-run.test.ts +520 -42
- package/src/workspace/ci-run.ts +483 -51
- package/src/workspace/ci-runner-key.test.ts +204 -0
- package/src/workspace/ci-runner-key.ts +275 -0
- package/src/workspace/ci-secrets.test.ts +106 -0
- package/src/workspace/ci-secrets.ts +213 -0
- package/src/workspace/ci-serve.test.ts +282 -39
- package/src/workspace/ci-serve.ts +231 -34
- 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.test.ts +2 -0
- package/src/workspace/reason-codes.ts +9 -4
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/trust/trust-write.test.ts +179 -0
- package/src/workspace/trust/trust-write.ts +377 -0
- package/src/workspace/trust-write.schema.json +265 -0
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
package/dist/workspace/ci-run.js
CHANGED
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* own variables. The forge values a generated job reads from `${{ ... }}`
|
|
23
23
|
* expressions or GitLab's predefined variables come from flags here:
|
|
24
24
|
* `BASE_SHA` from `--base`, `PR_NUMBER` from `--pr`, and the commit from
|
|
25
|
-
* `--commit`.
|
|
25
|
+
* `--commit`. The values a forge job gets from repository secrets come from
|
|
26
|
+
* the host's secrets file (`--secrets`, ./ci-secrets.ts, #4096), and every
|
|
27
|
+
* secret value is redacted from the run's events, record, copied report and
|
|
28
|
+
* output by the executor's `redact` option.
|
|
26
29
|
*
|
|
27
30
|
* The steps run in a fresh checkout of the commit: a detached `git worktree`
|
|
28
31
|
* in a temporary directory outside the repository, removed when the run
|
|
@@ -37,28 +40,67 @@
|
|
|
37
40
|
* key is given, so `chant ci tick --source runs` (#4044) judges the commit
|
|
38
41
|
* from it. Its events are `kind: "pipeline"` and `runtime: "ci"`, and the
|
|
39
42
|
* record and the events carry `runOrigin: { forge: "chant" }`.
|
|
43
|
+
*
|
|
44
|
+
* With a forge client (#4097) the job also sets a commit status at its
|
|
45
|
+
* commit: `pending` when it starts, `success` or `failure` when it settles.
|
|
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.
|
|
40
71
|
*/
|
|
41
72
|
import { execFileSync } from "node:child_process";
|
|
42
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
73
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
43
74
|
import { tmpdir } from "node:os";
|
|
44
75
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
45
76
|
import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prLoopJobs, prLoopSetup, PR_LOOP_IMAGE } from "../components/pr-pipeline.js";
|
|
46
|
-
import { detectForge, FORGE_KINDS } from "../pr-forge.js";
|
|
77
|
+
import { detectForge, ForgeEnvironmentError, forgeFromEnv, FORGE_KINDS } from "../pr-forge.js";
|
|
78
|
+
import { PR_APPLY_GATE } from "../pr-loop.js";
|
|
47
79
|
import { formatError, formatWarning } from "../cli/format.js";
|
|
48
|
-
import {
|
|
80
|
+
import { resolveCiJob } from "../lifecycle/release-ledger.js";
|
|
81
|
+
import { fetchLifecycleStatus, pushLifecycleReport } from "../lifecycle/git.js";
|
|
49
82
|
import { ACTIVITY_PROFILES } from "../op/activity-profiles.js";
|
|
50
83
|
import { shellCmd } from "../op/activities/shell.js";
|
|
51
84
|
import { OpRunFailure, runOpLocally } from "../op/local-executor.js";
|
|
85
|
+
import { redactText } from "../op/redact.js";
|
|
52
86
|
import { findWorkspaceRoot } from "../project-root.js";
|
|
53
|
-
import {
|
|
87
|
+
import { parseReportUrl, reportKey } from "../pr-report-url.js";
|
|
88
|
+
import { reportUrlOf } from "./ci-report-url.js";
|
|
89
|
+
import { ciTagSigner, RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green.js";
|
|
90
|
+
import { loadRunnerKey } from "./trust/dsse.js";
|
|
91
|
+
import { ciRedactor, ciSecretsFor, CI_SECRETS_ENV, readCiSecrets } from "./ci-secrets.js";
|
|
92
|
+
import { updateCiRunIndex } from "./ci-run-index.js";
|
|
54
93
|
import { resolveMemberContext } from "./member-pipeline.js";
|
|
55
94
|
import { signOpRunRecord } from "./trust/op-run-statement.js";
|
|
56
95
|
import { gitTop } from "./tree.js";
|
|
96
|
+
import { hostRunnerKey } from "./ci-runner-key.js";
|
|
57
97
|
export const CI_JOB_NAMES = ["plan", "apply"];
|
|
58
98
|
/** The Op each job is recorded as, which `ci.green` phases match. */
|
|
59
99
|
export const CI_JOB_OPS = { plan: "pr-plan", apply: "pr-apply" };
|
|
60
100
|
/** Where a job the chant runner ran says it ran (#4094). */
|
|
61
101
|
export const CHANT_RUNNER_ORIGIN = { forge: "chant" };
|
|
102
|
+
/** Where a job `chant ci run --local` ran says it ran (#4161): this machine, outside CI. */
|
|
103
|
+
export const CHANT_LOCAL_ORIGIN = { forge: "local" };
|
|
62
104
|
/** The runner's variables every job gets, when the runner has them. */
|
|
63
105
|
export const CI_RUN_BASE_ENV = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TZ"];
|
|
64
106
|
/**
|
|
@@ -89,19 +131,27 @@ export class CiRunError extends Error {
|
|
|
89
131
|
}
|
|
90
132
|
/**
|
|
91
133
|
* The job's environment: the runner's base variables, the forge variables
|
|
92
|
-
* and `passEnv` names it has,
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
134
|
+
* and `passEnv` names it has, the job's secrets from the host's secrets file
|
|
135
|
+
* (#4096), `CHANT_ENV` as a generated workflow sets it, the commit as the
|
|
136
|
+
* forge spells it, and the job's own variables. A job variable the forge
|
|
137
|
+
* fills from an expression comes from a flag, or from a secret or the
|
|
138
|
+
* runner's environment for a token such as `GITHUB_TOKEN`; a literal one,
|
|
139
|
+
* such as `CHANT_RUN_EVENTS_DIR`, is kept as it is. A secret replaces the
|
|
140
|
+
* runner's variable of the same name, and none replaces a variable the
|
|
141
|
+
* runner sets for the job. `CHANT_RUNNER_KEY` is never in it.
|
|
97
142
|
*/
|
|
98
143
|
export function ciJobEnv(job, input) {
|
|
144
|
+
if (input.passEnv?.includes(RUNNER_KEY_ENV))
|
|
145
|
+
throw new CiRunError(`--pass-env names ${RUNNER_KEY_ENV}, and the runner key is never passed to a job`);
|
|
99
146
|
const out = {};
|
|
100
147
|
for (const name of [...CI_RUN_BASE_ENV, ...CI_RUN_FORGE_ENV, ...(input.passEnv ?? [])]) {
|
|
101
148
|
const value = input.hostEnv[name];
|
|
102
149
|
if (value !== undefined)
|
|
103
150
|
out[name] = value;
|
|
104
151
|
}
|
|
152
|
+
for (const [name, value] of Object.entries(input.secrets ?? {}))
|
|
153
|
+
if (name !== RUNNER_KEY_ENV)
|
|
154
|
+
out[name] = value;
|
|
105
155
|
out.CHANT_ENV = input.env;
|
|
106
156
|
out[input.forge === "gitlab" ? "CI_COMMIT_SHA" : "GITHUB_SHA"] = input.values.commit;
|
|
107
157
|
const flags = {
|
|
@@ -118,10 +168,14 @@ export function ciJobEnv(job, input) {
|
|
|
118
168
|
else if (!value.includes("$")) {
|
|
119
169
|
out[name] = value;
|
|
120
170
|
}
|
|
171
|
+
else if (input.secrets?.[name] !== undefined) {
|
|
172
|
+
out[name] = input.secrets[name];
|
|
173
|
+
}
|
|
121
174
|
else if (input.hostEnv[name] !== undefined) {
|
|
122
175
|
out[name] = input.hostEnv[name];
|
|
123
176
|
}
|
|
124
177
|
}
|
|
178
|
+
delete out[RUNNER_KEY_ENV];
|
|
125
179
|
return out;
|
|
126
180
|
}
|
|
127
181
|
/** `line` with a `chant` at its start replaced by the command the caller names, when it names one. */
|
|
@@ -165,6 +219,17 @@ function exitCodeOf(error) {
|
|
|
165
219
|
const m = /command exited (\d+)/.exec(error ?? "");
|
|
166
220
|
return m ? Number(m[1]) : undefined;
|
|
167
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The code `chant ci run` exits with for a settled job: 0 when it passed,
|
|
224
|
+
* else its command's own code when that is 1 to 255, such as 3 for a moved
|
|
225
|
+
* plan or a waiting wave (#4156), else 1. The run index records the same
|
|
226
|
+
* code (#4165).
|
|
227
|
+
*/
|
|
228
|
+
export function ciJobExitCode(result) {
|
|
229
|
+
if (result.status === "ok")
|
|
230
|
+
return 0;
|
|
231
|
+
return result.exitCode && result.exitCode > 0 && result.exitCode < 256 ? result.exitCode : 1;
|
|
232
|
+
}
|
|
168
233
|
/** The first line of a step's error, for a reason. */
|
|
169
234
|
const firstLine = (text) => text.split("\n").find((l) => l.trim() !== "")?.trim() ?? text;
|
|
170
235
|
/**
|
|
@@ -220,6 +285,67 @@ export function ciJobFailure(input) {
|
|
|
220
285
|
}
|
|
221
286
|
return { code: "command-failed", reason: firstLine(failed.error ?? "the command failed"), phase: failed.phase, ...exit };
|
|
222
287
|
}
|
|
288
|
+
/** A status description is cut to what GitHub and Forgejo keep. */
|
|
289
|
+
const STATUS_DESCRIPTION = 140;
|
|
290
|
+
const cut = (text, max = STATUS_DESCRIPTION) => (text.length > max ? `${text.slice(0, max - 3)}...` : text);
|
|
291
|
+
/**
|
|
292
|
+
* A job's status context: `chant/pr-plan` for the plan, and for the apply
|
|
293
|
+
* `chant/<gate>`, which is `chant/pr-apply` unless `--gate` renamed the gate.
|
|
294
|
+
* pr-plan's own status is `chant/plan` (../pr-loop.ts), a different context.
|
|
295
|
+
*/
|
|
296
|
+
export function ciStatusContext(job, gate) {
|
|
297
|
+
return job === "apply" ? `chant/${gate ?? PR_APPLY_GATE}` : `chant/${CI_JOB_OPS.plan}`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* The link a job's status carries: the `--status-url` template with `{key}`
|
|
301
|
+
* filled, else the https `--report-url` followed by the key and a slash, else
|
|
302
|
+
* none (an `s3://` or `file://` URL is not one a browser opens).
|
|
303
|
+
*/
|
|
304
|
+
export function ciStatusUrl(input) {
|
|
305
|
+
const key = reportKey(input.pr ?? null, input.commit, input.member);
|
|
306
|
+
if (input.urlTemplate)
|
|
307
|
+
return input.urlTemplate.split("{key}").join(key);
|
|
308
|
+
const url = input.reportUrl?.trim();
|
|
309
|
+
if (url && /^https:\/\//.test(url))
|
|
310
|
+
return `${url.replace(/\/+$/, "")}/${key}/`;
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
/** What the settled status says: a short summary, or on failure the record's code and reason. */
|
|
314
|
+
export function ciStatusSettled(op, result, seconds) {
|
|
315
|
+
if (result.status === "ok")
|
|
316
|
+
return { state: "success", description: cut(`${op} passed in ${seconds}s`) };
|
|
317
|
+
const why = result.record.failure;
|
|
318
|
+
const text = why ? `${why.code}: ${why.reason}` : result.failure ? `${result.failure.phase}: ${firstLine(result.failure.error)}` : `${op} failed`;
|
|
319
|
+
return { state: "failure", description: cut(text) };
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* The status client for `chant ci run` and `chant ci serve` (#4097), or none:
|
|
323
|
+
* off with `--no-status`, on GitLab (its client has no commit status call
|
|
324
|
+
* here), and without a forge token. A token with the rest of the forge's
|
|
325
|
+
* variables missing gives a warning instead.
|
|
326
|
+
*/
|
|
327
|
+
export function ciStatusFromEnv(input) {
|
|
328
|
+
if (input.noStatus || input.forge === "gitlab")
|
|
329
|
+
return {};
|
|
330
|
+
if (input.statusUrl !== undefined && !/^https?:\/\/\S+$/.test(input.statusUrl)) {
|
|
331
|
+
throw new CiRunError(`--status-url takes an http or https URL template such as https://host/reports/{key}, not ${JSON.stringify(input.statusUrl)}`);
|
|
332
|
+
}
|
|
333
|
+
const env = input.hostEnv;
|
|
334
|
+
const hasToken = Boolean(env.CHANT_FORGE_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN);
|
|
335
|
+
if (!hasToken)
|
|
336
|
+
return {};
|
|
337
|
+
try {
|
|
338
|
+
const client = forgeFromEnv(input.forge, env);
|
|
339
|
+
if (!client.setCommitStatus)
|
|
340
|
+
return {};
|
|
341
|
+
return { status: { client: { setCommitStatus: client.setCommitStatus.bind(client) }, ...(input.statusUrl ? { urlTemplate: input.statusUrl } : {}) } };
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
if (err instanceof ForgeEnvironmentError)
|
|
345
|
+
return { warning: `no commit status will be set: ${err.message}` };
|
|
346
|
+
throw err;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
223
349
|
const message = (err) => (err instanceof Error ? err.message : String(err));
|
|
224
350
|
function git(cwd, args) {
|
|
225
351
|
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -275,6 +401,47 @@ function freshCheckout(top, commit) {
|
|
|
275
401
|
},
|
|
276
402
|
};
|
|
277
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Add the settled job to the run index at its report URL (#4098). The pull
|
|
406
|
+
* request is the one the job's own report names, since a pr-apply finds it
|
|
407
|
+
* itself, else `--pr`. A warning, never a failure, when the index can't be
|
|
408
|
+
* updated.
|
|
409
|
+
*/
|
|
410
|
+
async function recordInIndex(input) {
|
|
411
|
+
const target = parseReportUrl(input.url);
|
|
412
|
+
if ("error" in target)
|
|
413
|
+
return input.warn(`the run index is not updated: ${target.error}`);
|
|
414
|
+
let pr = input.pr ?? null;
|
|
415
|
+
try {
|
|
416
|
+
const file = join(input.reportDir, `${CI_JOB_OPS[input.job]}.json`);
|
|
417
|
+
if (existsSync(file)) {
|
|
418
|
+
const said = JSON.parse(readFileSync(file, "utf-8")).pr;
|
|
419
|
+
if (Number.isInteger(said) && said > 0)
|
|
420
|
+
pr = said;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
// The report is not readable; the flag stands.
|
|
425
|
+
}
|
|
426
|
+
const failure = input.record.failure;
|
|
427
|
+
const clean = (text) => (input.redact ? input.redact(text) : text);
|
|
428
|
+
const entry = {
|
|
429
|
+
job: input.job,
|
|
430
|
+
pr,
|
|
431
|
+
commit: input.commit,
|
|
432
|
+
status: input.record.status === "ok" ? "ok" : "fail",
|
|
433
|
+
...(input.record.status !== "ok" ? { failure: { code: clean(failure?.code ?? "command-failed") } } : {}),
|
|
434
|
+
...(input.member ? { member: clean(input.member) } : {}),
|
|
435
|
+
started: input.record.started,
|
|
436
|
+
ended: input.record.ended,
|
|
437
|
+
runId: clean(input.record.id),
|
|
438
|
+
key: clean(reportKey(pr, input.commit, input.member)),
|
|
439
|
+
runOrigin: input.runOrigin,
|
|
440
|
+
signer: input.signer,
|
|
441
|
+
exitCode: input.exitCode,
|
|
442
|
+
};
|
|
443
|
+
await updateCiRunIndex({ target, entry, ...(input.limit ? { limit: input.limit } : {}), warn: input.warn });
|
|
444
|
+
}
|
|
278
445
|
const ACTIVITIES = new Map([["shellCmd", shellCmd]]);
|
|
279
446
|
/**
|
|
280
447
|
* Run one pull-request job as a pipeline Op in a fresh checkout of the
|
|
@@ -288,26 +455,110 @@ export async function ciRun(req) {
|
|
|
288
455
|
if (!top)
|
|
289
456
|
throw new CiRunError("a job runs in a fresh checkout of a commit, and this directory is not in a git repository");
|
|
290
457
|
const commit = resolveJobCommit(top, req.commit ?? "HEAD");
|
|
458
|
+
if (req.inPlace && commit !== resolveJobCommit(top, "HEAD"))
|
|
459
|
+
throw new CiRunError(`--in-place runs the checkout's HEAD, and --commit names ${commit.slice(0, 12)}`);
|
|
460
|
+
if (req.local && req.inPlace)
|
|
461
|
+
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");
|
|
291
462
|
const place = ciJobPlace(cwd, top);
|
|
292
|
-
const jobs = prLoopJobs(req.forge, req.env, req.loop ?? {}, place.member);
|
|
293
|
-
const
|
|
294
|
-
if (!
|
|
463
|
+
const jobs = prLoopJobs(req.forge, req.env, req.local ? localLoop(req.loop) : (req.loop ?? {}), place.member);
|
|
464
|
+
const found = jobs.find((j) => j.jobName === req.job);
|
|
465
|
+
if (!found)
|
|
295
466
|
throw new CiRunError(`the pull-request pipeline has no ${req.job} job; it has ${jobs.map((j) => j.jobName).join(" and ")}`);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
467
|
+
if (req.wave !== undefined && (req.job !== "apply" || !req.loop?.waveGate))
|
|
468
|
+
throw new CiRunError("--wave names the one wave an apply runs, and takes --wave-gate: ci run apply --wave-gate <gate> --wave <k>");
|
|
469
|
+
// A generated wave job's command, as prLoopWaveJobs writes it. #4161: a local job's command posts no status or note.
|
|
470
|
+
const command = `${found.command}${req.wave !== undefined ? ` --wave ${req.wave}` : ""}${req.local ? " --no-status" : ""}`;
|
|
471
|
+
const job = { ...found, command, ...(req.wave !== undefined ? { wave: req.wave } : {}) };
|
|
472
|
+
const own = ciJobEnv(job, {
|
|
473
|
+
forge: req.forge,
|
|
474
|
+
env: req.env,
|
|
475
|
+
values: { base: req.base, pr: req.pr, commit },
|
|
476
|
+
hostEnv,
|
|
477
|
+
passEnv: req.passEnv,
|
|
478
|
+
secrets: ciSecretsFor(req.secrets, req.job),
|
|
479
|
+
});
|
|
480
|
+
// #4156: in place, the forge job's environment is the job's, less the runner key.
|
|
481
|
+
const jobEnv = req.inPlace ? { ...definedEnv(hostEnv), ...own } : own;
|
|
482
|
+
delete jobEnv[RUNNER_KEY_ENV];
|
|
483
|
+
// #4096: the secrets and the job's secret-looking variables, redacted from everything the run writes and returns.
|
|
484
|
+
// 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.
|
|
485
|
+
const redact = ciRedactor(req.inPlace ? { ...longValues(jobEnv), ...own } : jobEnv, req.secrets);
|
|
486
|
+
try {
|
|
487
|
+
const result = await runCiJob(req, { cwd, top, commit, place, job, jobEnv, ...(redact ? { redact } : {}) });
|
|
488
|
+
return redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
if (redact && err instanceof Error)
|
|
492
|
+
err.message = redact(err.message);
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** The generator's options for a `--local` job (#4161): the same, with no report URL. */
|
|
497
|
+
function localLoop(loop) {
|
|
498
|
+
const { reportUrl: _dropped, ...rest } = loop ?? {};
|
|
499
|
+
return rest;
|
|
500
|
+
}
|
|
501
|
+
/** The variables of `env` whose value is long enough to be a credential, 8 characters or more. */
|
|
502
|
+
function longValues(env) {
|
|
503
|
+
return Object.fromEntries(Object.entries(env).filter(([, value]) => value.length >= 8));
|
|
504
|
+
}
|
|
505
|
+
/** The variables of `env` that have a value. */
|
|
506
|
+
function definedEnv(env) {
|
|
507
|
+
const out = {};
|
|
508
|
+
for (const [name, value] of Object.entries(env))
|
|
509
|
+
if (value !== undefined)
|
|
510
|
+
out[name] = value;
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Who signs the run (#4156): the runner key, checked against
|
|
515
|
+
* `.chant/trust.json` at the commit. A key that isn't an Ed25519 PEM is an
|
|
516
|
+
* error, and so is a `--key` the file does not list. A key from the
|
|
517
|
+
* environment that it does not list, and no key at all, leave the run
|
|
518
|
+
* unsigned: a warning when the declaration's `ci.green.signedSince` asks
|
|
519
|
+
* for signed runs, a note for `--verbose` otherwise.
|
|
520
|
+
*/
|
|
521
|
+
function ciRunSigner(req, at) {
|
|
522
|
+
// #4161: a local run never counts, signed or not, so nothing about its signature warns.
|
|
523
|
+
const required = req.local ? null : signedRunsRequired(at.cwd);
|
|
524
|
+
const say = (text) => (required ? { warnings: [`${text}, and ci.green.signedSince is ${required}, so it will not count`], notes: [] } : { warnings: [], notes: [text] });
|
|
525
|
+
if (req.runnerKey === undefined)
|
|
526
|
+
return say("there is no runner key, so the run's record is unsigned");
|
|
527
|
+
try {
|
|
528
|
+
loadRunnerKey(req.runnerKey);
|
|
529
|
+
}
|
|
530
|
+
catch (err) {
|
|
531
|
+
throw new CiRunError(`the runner key${req.runnerKeyFromEnv ? ` in ${RUNNER_KEY_ENV}` : ""} is not an Ed25519 private key in PEM: ${message(err)}`);
|
|
309
532
|
}
|
|
310
|
-
|
|
533
|
+
try {
|
|
534
|
+
return { signer: ciTagSigner(at.top, at.commit, req.runnerKey, "the runs it signed"), warnings: [], notes: [] };
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
if (!req.runnerKeyFromEnv)
|
|
538
|
+
throw new CiRunError(`the run would not be signed: ${message(err)}`);
|
|
539
|
+
return say(`${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message(err)}`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** Where the job runs: a fresh worktree of the commit, or with `inPlace` the checkout itself, which is left as it is. */
|
|
543
|
+
function jobCheckout(top, commit, inPlace) {
|
|
544
|
+
return inPlace ? { dir: top, remove() { } } : freshCheckout(top, commit);
|
|
545
|
+
}
|
|
546
|
+
/** {@link ciRun} once the job and its environment are known. */
|
|
547
|
+
async function runCiJob(req, at) {
|
|
548
|
+
const { cwd, top, commit, place, job, jobEnv, redact } = at;
|
|
549
|
+
const local = req.local === true;
|
|
550
|
+
const status = local ? undefined : req.status;
|
|
551
|
+
const reportUrl = local ? undefined : req.loop?.reportUrl;
|
|
552
|
+
// 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.
|
|
553
|
+
const signing = ciRunSigner(req, { cwd, top, commit });
|
|
554
|
+
const signer = signing.signer;
|
|
555
|
+
const early = signing.warnings;
|
|
556
|
+
const notes = signing.notes;
|
|
557
|
+
const origin = local ? CHANT_LOCAL_ORIGIN : req.inPlace ? (resolveCiJob(req.hostEnv ?? process.env)?.runOrigin ?? CHANT_RUNNER_ORIGIN) : CHANT_RUNNER_ORIGIN;
|
|
558
|
+
// In place, the job writes its record on the checkout's chant/lifecycle, which a forge checkout may not have fetched.
|
|
559
|
+
if (req.inPlace && req.push !== false)
|
|
560
|
+
await fetchLifecycleStatus({ cwd: top }).catch(() => undefined);
|
|
561
|
+
const checkout = jobCheckout(top, commit, req.inPlace);
|
|
311
562
|
try {
|
|
312
563
|
const jobDir = place.dir === "." ? checkout.dir : join(checkout.dir, place.dir);
|
|
313
564
|
const op = ciJobOp({
|
|
@@ -315,7 +566,7 @@ export async function ciRun(req) {
|
|
|
315
566
|
env: req.env,
|
|
316
567
|
cwd: jobDir,
|
|
317
568
|
jobEnv,
|
|
318
|
-
setup: prLoopSetup(req.image ?? PR_LOOP_IMAGE),
|
|
569
|
+
setup: req.inPlace ? [] : prLoopSetup(req.image ?? PR_LOOP_IMAGE),
|
|
319
570
|
build: [...(req.beforeScript ?? []), ...(job.build ?? [])],
|
|
320
571
|
...(req.chant ? { chant: req.chant } : {}),
|
|
321
572
|
});
|
|
@@ -326,6 +577,22 @@ export async function ciRun(req) {
|
|
|
326
577
|
mkdirSync(dirname(resume.job), { recursive: true });
|
|
327
578
|
copyFileSync(resume.host, resume.job);
|
|
328
579
|
}
|
|
580
|
+
const statusContext = ciStatusContext(req.job, req.loop?.gate);
|
|
581
|
+
const statusUrl = status
|
|
582
|
+
? ciStatusUrl({ ...(status.urlTemplate ? { urlTemplate: status.urlTemplate } : {}), ...(reportUrl ? { reportUrl } : {}), ...(req.pr !== undefined ? { pr: req.pr } : {}), commit, ...(place.member ? { member: place.member } : {}) })
|
|
583
|
+
: undefined;
|
|
584
|
+
const postStatus = async (state, description) => {
|
|
585
|
+
if (!status)
|
|
586
|
+
return;
|
|
587
|
+
try {
|
|
588
|
+
await status.client.setCommitStatus(commit, { context: statusContext, state, description: cut(redact ? redact(description) : description), ...(statusUrl ? { targetUrl: statusUrl } : {}) });
|
|
589
|
+
}
|
|
590
|
+
catch (err) {
|
|
591
|
+
warnings.push(`could not set the ${statusContext} status: ${message(err)}`);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const startedAt = Date.now();
|
|
595
|
+
await postStatus("pending", `${op.name} running`);
|
|
329
596
|
let ledgerError;
|
|
330
597
|
let result;
|
|
331
598
|
try {
|
|
@@ -335,22 +602,28 @@ export async function ciRun(req) {
|
|
|
335
602
|
cwd: jobDir,
|
|
336
603
|
commit,
|
|
337
604
|
ledger: { cwd: jobDir },
|
|
338
|
-
runner: { kind: "pipeline", runtime: "ci", runOrigin:
|
|
605
|
+
runner: { kind: "pipeline", runtime: local ? "local" : "ci", runOrigin: origin },
|
|
339
606
|
describeFailure: (records) => ciJobFailure({ job: req.job, op, records, reportDir: join(jobDir, PR_LOOP_REPORT_DIR) }),
|
|
340
607
|
...(req.runEvents ? { runEvents: { write: req.runEvents } } : {}),
|
|
341
608
|
...(req.stepOutput ? { stepOutput: req.stepOutput } : {}),
|
|
342
609
|
...(signer ? { signRecord: (record) => signOpRunRecord(record, signer) } : {}),
|
|
343
610
|
onSignError: (err) => warnings.push(`the run was not signed: ${message(err)}`),
|
|
611
|
+
...(redact ? { redact } : {}),
|
|
344
612
|
onLedgerError: (err) => {
|
|
345
613
|
ledgerError = `the job ran, and its record could not be written to chant/lifecycle: ${message(err)}`;
|
|
346
614
|
},
|
|
347
615
|
});
|
|
348
616
|
}
|
|
349
617
|
catch (err) {
|
|
350
|
-
if (!(err instanceof OpRunFailure))
|
|
618
|
+
if (!(err instanceof OpRunFailure)) {
|
|
619
|
+
await postStatus("error", `${op.name} stopped: ${firstLine(message(err))}`);
|
|
351
620
|
throw err;
|
|
621
|
+
}
|
|
352
622
|
result = err.result;
|
|
353
623
|
}
|
|
624
|
+
const failedStep = result.records.find((r) => r.status === "fail");
|
|
625
|
+
const settled = ciStatusSettled(op.name, { status: result.status, record: result.record, ...(failedStep ? { failure: { phase: failedStep.phase, error: failedStep.error ?? "the step failed" } } : {}) }, Math.round((Date.now() - startedAt) / 1000));
|
|
626
|
+
await postStatus(settled.state, settled.description);
|
|
354
627
|
if (resume && existsSync(resume.job)) {
|
|
355
628
|
try {
|
|
356
629
|
mkdirSync(dirname(resume.host), { recursive: true });
|
|
@@ -364,29 +637,69 @@ export async function ciRun(req) {
|
|
|
364
637
|
let report = null;
|
|
365
638
|
if (existsSync(written)) {
|
|
366
639
|
report = resolve(cwd, req.output ?? PR_LOOP_REPORT_DIR);
|
|
367
|
-
|
|
640
|
+
// In place the report is already where the forge keeps it, and stays as the command wrote it.
|
|
641
|
+
if (!(existsSync(report) && realpathSync(report) === realpathSync(written))) {
|
|
642
|
+
cpSync(written, report, { recursive: true });
|
|
643
|
+
if (redact)
|
|
644
|
+
redactTree(report, redact);
|
|
645
|
+
}
|
|
368
646
|
}
|
|
369
|
-
if (ledgerError)
|
|
370
|
-
throw new CiRunError(ledgerError);
|
|
371
|
-
const pushed = req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
|
|
372
647
|
const failed = result.records.find((r) => r.status === "fail");
|
|
648
|
+
const exitCode = failed?.phase === req.job ? exitCodeOf(failed.error) : undefined;
|
|
649
|
+
if (reportUrl) {
|
|
650
|
+
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) });
|
|
651
|
+
}
|
|
652
|
+
// In place the forge job's outcome is the command's, so a record that could not be written is a warning there.
|
|
653
|
+
if (ledgerError && req.inPlace)
|
|
654
|
+
warnings.push(ledgerError);
|
|
655
|
+
else if (ledgerError)
|
|
656
|
+
throw new CiRunError(ledgerError);
|
|
657
|
+
const pushed = local
|
|
658
|
+
? { pushed: false }
|
|
659
|
+
: req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
|
|
373
660
|
return {
|
|
374
661
|
op: op.name,
|
|
375
662
|
status: result.status,
|
|
376
663
|
record: result.record,
|
|
377
664
|
...(failed ? { failure: { phase: failed.phase, error: failed.error ?? "the step failed" } } : {}),
|
|
665
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
378
666
|
report,
|
|
379
|
-
signer: signer
|
|
667
|
+
signer: result.record.statement && signer ? signer.principal : null,
|
|
380
668
|
pushed: pushed.pushed,
|
|
381
669
|
...(pushed.pushWarning ? { pushWarning: pushed.pushWarning } : {}),
|
|
382
670
|
warnings,
|
|
671
|
+
notes,
|
|
383
672
|
checkout: checkout.dir,
|
|
673
|
+
...(local ? { local: true } : {}),
|
|
384
674
|
};
|
|
385
675
|
}
|
|
386
676
|
finally {
|
|
387
677
|
checkout.remove();
|
|
388
678
|
}
|
|
389
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Redact every text file under `dir` in place (#4096): the job's report, its
|
|
682
|
+
* change set and its component events, as `ci run` copied them out. A
|
|
683
|
+
* JSON file stays JSON. A file with a NUL byte is taken as binary and left.
|
|
684
|
+
*/
|
|
685
|
+
export function redactTree(dir, redact) {
|
|
686
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
687
|
+
const path = join(dir, entry.name);
|
|
688
|
+
if (entry.isDirectory()) {
|
|
689
|
+
redactTree(path, redact);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (!entry.isFile())
|
|
693
|
+
continue;
|
|
694
|
+
const bytes = readFileSync(path);
|
|
695
|
+
if (bytes.includes(0))
|
|
696
|
+
continue;
|
|
697
|
+
const text = bytes.toString("utf-8");
|
|
698
|
+
const clean = redactText(text, redact);
|
|
699
|
+
if (clean !== text)
|
|
700
|
+
writeFileSync(path, clean);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
390
703
|
/** The text `chant ci run` prints when the run settles. */
|
|
391
704
|
export function formatCiRun(r) {
|
|
392
705
|
const lines = [`${r.op} ${r.status === "ok" ? "passed" : r.status === "fail" ? "failed" : r.status} at ${r.record.gitSha?.slice(0, 12) ?? "an unknown commit"}, run ${r.record.id} in ${r.record.env}`];
|
|
@@ -395,23 +708,64 @@ export function formatCiRun(r) {
|
|
|
395
708
|
lines.push(` failure: ${why.code}${why.phase ? ` in ${why.phase}` : ""}${why.exitCode !== undefined ? `, exit ${why.exitCode}` : ""}: ${why.reason}`);
|
|
396
709
|
else if (r.failure)
|
|
397
710
|
lines.push(` ${r.failure.phase}: ${r.failure.error}`);
|
|
398
|
-
lines.push(` record: ${r.record.env}/runs__${r.op}.jsonl on chant/lifecycle, ${r.signer ? `signed by ${r.signer}` : "unsigned
|
|
711
|
+
lines.push(` record: ${r.record.env}/runs__${r.op}.jsonl on chant/lifecycle, ${r.signer ? `signed by ${r.signer}` : "unsigned"}`);
|
|
399
712
|
if (r.report)
|
|
400
713
|
lines.push(` report: ${r.report}`);
|
|
401
|
-
if (
|
|
714
|
+
if (r.local)
|
|
715
|
+
lines.push(" local run: the record stays on this machine, with no push, commit status or report upload");
|
|
716
|
+
else if (!r.pushed)
|
|
402
717
|
lines.push(` ${r.pushWarning ?? "chant/lifecycle was not pushed"}`);
|
|
403
718
|
return lines.join("\n");
|
|
404
719
|
}
|
|
405
720
|
// ── The command ──────────────────────────────────────────────────────────────
|
|
721
|
+
/**
|
|
722
|
+
* The generator's options from `ci run`'s and `ci serve`'s flags, as `chant
|
|
723
|
+
* build --components --generate <lexicon> --pr-loop` takes them: `--gate`,
|
|
724
|
+
* `--branch`, `--events-json`, `--report-url`, `--pr-build`,
|
|
725
|
+
* `--pr-build-command`, `--wave-gate` (#4003) and `--no-require-review`
|
|
726
|
+
* (#4156).
|
|
727
|
+
*/
|
|
728
|
+
export function ciLoopOptions(args) {
|
|
729
|
+
return {
|
|
730
|
+
...(args.gate ? { gate: args.gate } : {}),
|
|
731
|
+
...(args.branch ? { branch: args.branch } : {}),
|
|
732
|
+
...(args.eventsJson ? { events: true } : {}),
|
|
733
|
+
...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
|
|
734
|
+
...(args.prBuild ? { build: true } : {}),
|
|
735
|
+
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
736
|
+
...(args.waveGate ? { waveGate: args.waveGate } : {}),
|
|
737
|
+
...(args.requireReview === false ? { requireReview: false } : {}),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
406
740
|
const USAGE = "chant ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
|
|
407
|
-
"[--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--image <image>] [--before-script <line>]... " +
|
|
408
|
-
"[--pr-build] [--pr-build-command <line>] " +
|
|
409
|
-
"[--chant <command>] [--pass-env <NAME>]... [--key <runner key.pem>] [-o <dir>]";
|
|
741
|
+
"[--gate <name>] [--wave-gate <name>] [--wave <k>] [--branch <name>] [--events-json] [--report-url <url>] [--run-index-limit <n>] [--image <image>] [--before-script <line>]... " +
|
|
742
|
+
"[--pr-build] [--pr-build-command <line>] [--no-require-review] [--no-status] [--status-url <url template>] " +
|
|
743
|
+
"[--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>]";
|
|
410
744
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
745
|
+
/** The host's runner key in `--state`, or in ci serve's default state directory, when `--setup-key` made one there (#4099). */
|
|
746
|
+
function hostRunnerKeyAt(cwd, statePath) {
|
|
747
|
+
const common = statePath ? null : (() => {
|
|
748
|
+
try {
|
|
749
|
+
return execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
})();
|
|
755
|
+
const dir = statePath ? resolve(statePath) : common ? join(common, "chant", "ci-serve") : null;
|
|
756
|
+
return dir ? hostRunnerKey(dir) : undefined;
|
|
757
|
+
}
|
|
758
|
+
/** The secrets file `--secrets` names, else the one `CHANT_CI_SECRETS` names, read; undefined when neither names one (#4096). */
|
|
759
|
+
export function ciSecretsOf(flag, env = process.env) {
|
|
760
|
+
const file = flag ?? (env[CI_SECRETS_ENV] || undefined);
|
|
761
|
+
return file === undefined ? undefined : readCiSecrets(resolve(file));
|
|
762
|
+
}
|
|
411
763
|
/** `chant ci run plan|apply` (#4094). */
|
|
412
764
|
export async function runCiRun(ctx) {
|
|
413
765
|
const { args } = ctx;
|
|
414
766
|
try {
|
|
767
|
+
if (args.setupKey)
|
|
768
|
+
return (await import("./ci-serve.js")).runnerKeySetupCommand(ctx);
|
|
415
769
|
const job = args.extraPositional;
|
|
416
770
|
if (job !== "plan" && job !== "apply")
|
|
417
771
|
throw new CiRunError(`ci run takes the job to run, plan or apply${job ? `, not ${JSON.stringify(job)}` : ""}`);
|
|
@@ -426,27 +780,38 @@ export async function runCiRun(ctx) {
|
|
|
426
780
|
if (!ENV_NAME.test(name))
|
|
427
781
|
throw new CiRunError(`--pass-env takes a variable's name, not ${JSON.stringify(name)}`);
|
|
428
782
|
const fromFlag = args.key !== undefined;
|
|
429
|
-
|
|
783
|
+
// #4099: without --key or CHANT_RUNNER_KEY, the host's key that --setup-key made, if there is one. Like the variable, an unlisted one only warns.
|
|
784
|
+
const hostKey = fromFlag || process.env[RUNNER_KEY_ENV] ? undefined : hostRunnerKeyAt(process.cwd(), args.statePath);
|
|
785
|
+
const runnerKey = fromFlag ? readFileSync(resolve(args.key), "utf-8") : process.env[RUNNER_KEY_ENV] || hostKey?.pem;
|
|
786
|
+
const hostEnv = process.env;
|
|
787
|
+
// #4156: in place the forge shows the job, so ci run sets no status of its own.
|
|
788
|
+
// #4161: a local run sets no status, so it builds no client and warns about none.
|
|
789
|
+
const { status, warning } = ciStatusFromEnv({ forge: forge, hostEnv, noStatus: args.noStatus || args.ciInPlace || args.local, statusUrl: args.statusUrl });
|
|
790
|
+
if (warning)
|
|
791
|
+
console.error(formatWarning({ message: warning }));
|
|
792
|
+
const secrets = ciSecretsOf(args.ciSecrets);
|
|
793
|
+
// #4160: without --report-url, the declaration's ci.reportUrl.
|
|
794
|
+
const reportUrl = await reportUrlOf(args.reportUrl, process.cwd());
|
|
430
795
|
const result = await ciRun({
|
|
431
796
|
cwd: process.cwd(),
|
|
797
|
+
...(status ? { status } : {}),
|
|
432
798
|
job,
|
|
433
799
|
env: args.env,
|
|
434
800
|
forge: forge,
|
|
435
801
|
...(args.base ? { base: args.base } : {}),
|
|
436
802
|
...(args.pr !== undefined ? { pr: args.pr } : {}),
|
|
437
803
|
...(args.commit ? { commit: args.commit } : {}),
|
|
438
|
-
loop: {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
445
|
-
},
|
|
804
|
+
loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
|
|
805
|
+
...(args.ciInPlace ? { inPlace: true } : {}),
|
|
806
|
+
...(args.local ? { local: true } : {}),
|
|
807
|
+
...(args.noPush ? { push: false } : {}),
|
|
808
|
+
...(args.wave !== undefined ? { wave: args.wave } : {}),
|
|
809
|
+
...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
|
|
446
810
|
...(args.image ? { image: args.image } : {}),
|
|
447
811
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
448
812
|
...(args.ciChant ? { chant: args.ciChant } : {}),
|
|
449
813
|
...(args.passEnv?.length ? { passEnv: args.passEnv } : {}),
|
|
814
|
+
...(secrets ? { secrets } : {}),
|
|
450
815
|
...(args.output ? { output: args.output } : {}),
|
|
451
816
|
...(runnerKey !== undefined ? { runnerKey, runnerKeyFromEnv: !fromFlag } : {}),
|
|
452
817
|
...(args.eventsJson ? { runEvents: (line) => void process.stdout.write(line) } : {}),
|
|
@@ -454,8 +819,12 @@ export async function runCiRun(ctx) {
|
|
|
454
819
|
});
|
|
455
820
|
for (const w of result.warnings)
|
|
456
821
|
console.error(formatWarning({ message: w }));
|
|
822
|
+
if (args.verbose)
|
|
823
|
+
for (const n of result.notes)
|
|
824
|
+
console.error(`ci run: ${n}`);
|
|
457
825
|
console.error(formatCiRun(result));
|
|
458
|
-
|
|
826
|
+
// #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.
|
|
827
|
+
return ciJobExitCode(result);
|
|
459
828
|
}
|
|
460
829
|
catch (err) {
|
|
461
830
|
console.error(formatError({ message: `chant ci run: ${message(err)}`, hint: USAGE }));
|