@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
|
@@ -15,12 +15,34 @@
|
|
|
15
15
|
* the one `chant operator` holds) named for `prApplyGroup`, renews it while
|
|
16
16
|
* it runs and releases it after. An apply that finds it held waits for a
|
|
17
17
|
* later poll, as a forge's concurrency group queues it.
|
|
18
|
+
* - Merged pull request. An apply asks the forge for the pull request whose
|
|
19
|
+
* merge commit the new head is (`pullRequestFor`, as pr-apply does) and
|
|
20
|
+
* passes it as `--pr`, so its commit status link and run index use
|
|
21
|
+
* `<pr>/<commit>`. With none found it runs without `--pr`.
|
|
18
22
|
* - Resume records. Each apply gets `<state>/resume/<prApplyRecordKey>.json`
|
|
19
23
|
* as its `--resume` record, so a re-run of a failed apply at the same
|
|
20
24
|
* commit finishes under the approval it has. This replaces the forge's CI
|
|
21
25
|
* cache.
|
|
26
|
+
* - A gate per wave (#4003). With `--wave-gate` the apply job runs
|
|
27
|
+
* `pr-apply --wave-gate`, which applies the waves in order and stops at
|
|
28
|
+
* the first whose moved plan waits on its own gate. The job fails, and the
|
|
29
|
+
* resume record keeps the waves that applied; once the wave's plan is
|
|
30
|
+
* approved, `rerunFailed` runs the apply again and it carries on from
|
|
31
|
+
* that wave.
|
|
22
32
|
* - Capacity. At most `maxJobs` jobs run at once. A job over the limit is not
|
|
23
33
|
* recorded, so a later poll starts it.
|
|
34
|
+
* - Fast-forwards only (#4122). An apply runs only when the target's new head
|
|
35
|
+
* descends from the head seen before it. Any other move, such as a force
|
|
36
|
+
* push back to an older commit, is recorded as seen and not applied.
|
|
37
|
+
* - `[skip ci]` (#4122). A target head or pull request head whose commit
|
|
38
|
+
* message says `[skip ci]` or `[ci skip]` is recorded as seen and not run,
|
|
39
|
+
* as Actions does.
|
|
40
|
+
* - The forge's ledger (#4121). Before each job, when no other job of this
|
|
41
|
+
* serve runs, the clone's `chant/lifecycle` follows the forge's: it is
|
|
42
|
+
* deleted when the forge has none, and reset to the forge's when neither
|
|
43
|
+
* contains the other. Each is a warning. A local branch ahead of the
|
|
44
|
+
* forge's keeps its facts, and the job's push sends them. Nothing here
|
|
45
|
+
* pushes the ledger.
|
|
24
46
|
*
|
|
25
47
|
* Host mode only: each job runs in a fresh worktree on this host as this
|
|
26
48
|
* user, with the clean environment `ciRun` gives it. A pull request from a
|
|
@@ -32,18 +54,20 @@
|
|
|
32
54
|
* turn a checked payload into the same triggers and schedule them.
|
|
33
55
|
*/
|
|
34
56
|
|
|
35
|
-
import { execFileSync } from "node:child_process";
|
|
57
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
36
58
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
37
|
-
import {
|
|
59
|
+
import { join, resolve } from "node:path";
|
|
38
60
|
import { prApplyGroup, prApplyRecordKey } from "../components/pr-pipeline";
|
|
39
61
|
import { forgeFromEnv, type OpenPullRequest } from "../pr-forge";
|
|
40
62
|
import type { CommandContext } from "../cli/registry";
|
|
41
63
|
import { formatError, formatWarning } from "../cli/format";
|
|
42
64
|
import { acquireLease, currentHolderId, releaseLease, DEFAULT_LEASE_TTL_MS, type LeaseRecord } from "../lifecycle/lease";
|
|
43
65
|
import { parseDuration } from "../op/duration";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
66
|
+
import { reportUrlOf } from "./ci-report-url";
|
|
67
|
+
import { CI_JOB_OPS, ciJobPlace, ciLoopOptions, ciSecretsOf, ciRun, ciStatusFromEnv, CiRunError, type CiJobName, type CiRunRequest, type CiRunResult } from "./ci-run";
|
|
68
|
+
import { RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green";
|
|
46
69
|
import { gitTop } from "./tree";
|
|
70
|
+
import { ciServeRunnerKey, formatRunnerKeySetup, setupRunnerKey } from "./ci-runner-key";
|
|
47
71
|
|
|
48
72
|
/** The forges `ci serve` polls. GitLab is later. */
|
|
49
73
|
export type CiServeForgeKind = "github" | "forgejo";
|
|
@@ -52,6 +76,8 @@ export type CiServeForgeKind = "github" | "forgejo";
|
|
|
52
76
|
export interface CiServeForge {
|
|
53
77
|
openPullRequests(base: string): Promise<OpenPullRequest[]>;
|
|
54
78
|
branchHead(branch: string): Promise<string | undefined>;
|
|
79
|
+
/** The merged pull request that produced a commit, as pr-apply finds it (`pullRequestFor`). Without it an apply gets no `--pr`. */
|
|
80
|
+
pullRequestFor?(sha: string): Promise<number | null>;
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
/** A job the forge's state calls for: the plan of a pull request's head, or the apply of a target-branch commit. */
|
|
@@ -85,9 +111,11 @@ export interface CiServeState {
|
|
|
85
111
|
env: string;
|
|
86
112
|
branch: string;
|
|
87
113
|
member?: string;
|
|
88
|
-
/** The target branch's head the last apply was measured to, or the head
|
|
114
|
+
/** The target branch's head the last apply was measured to, or the last head seen and not applied. */
|
|
89
115
|
target?: string;
|
|
90
116
|
jobs: Record<string, CiServeJobState>;
|
|
117
|
+
/** Plans seen and not run, by state key, with why (#4122). */
|
|
118
|
+
skipped?: Record<string, "skip-ci">;
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
/** What a pass did, in order, for the log. */
|
|
@@ -98,6 +126,8 @@ export type CiServeEvent =
|
|
|
98
126
|
| { type: "held"; job: CiJobName; commit: string; pr?: number; reason: "capacity" | "lease"; heldBy?: string }
|
|
99
127
|
| { type: "started"; job: CiJobName; commit: string; pr?: number; base: string; runId: string; attempt: number; resume?: string }
|
|
100
128
|
| { type: "settled"; job: CiJobName; commit: string; pr?: number; status: "ok" | "fail"; failure?: string; report?: string }
|
|
129
|
+
| { type: "not-run"; job: CiJobName; commit: string; pr?: number; reason: "not-fast-forward" | "skip-ci"; branch?: string; from?: string }
|
|
130
|
+
| { type: "ledger"; action: "deleted" | "reset"; local: string; forge?: string }
|
|
101
131
|
| { type: "error"; message: string };
|
|
102
132
|
|
|
103
133
|
/** The `ciRun` request fields `ci serve` passes through to every job: the generator's options and the runner's. */
|
|
@@ -152,6 +182,78 @@ function tryGit(cwd: string, args: string[]): string | undefined {
|
|
|
152
182
|
}
|
|
153
183
|
}
|
|
154
184
|
|
|
185
|
+
const LEDGER = "chant/lifecycle";
|
|
186
|
+
/** Where the forge's ledger tip is fetched to, a ref only this module writes. */
|
|
187
|
+
const FORGE_LEDGER = "refs/chant/ci-serve/forge-ledger";
|
|
188
|
+
|
|
189
|
+
/** `git` with its exit status, for the calls whose answer is the status. */
|
|
190
|
+
function gitStatus(cwd: string, args: string[]): { code: number; out: string } {
|
|
191
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LC_ALL: "C" } });
|
|
192
|
+
return { code: r.status ?? -1, out: (r.stdout ?? "").trim() };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Make the clone's `chant/lifecycle` follow the forge's (#4121). The clone's
|
|
197
|
+
* branch outlives the forge's, so a ledger the forge deleted or rewrote would
|
|
198
|
+
* make the next pr-apply find the two diverged and stall.
|
|
199
|
+
*
|
|
200
|
+
* - The forge has none: the local branch and its remote-tracking ref go.
|
|
201
|
+
* - Neither contains the other: the local branch and its remote-tracking ref
|
|
202
|
+
* are reset to the forge's tip.
|
|
203
|
+
* - The local branch is ahead of the forge's, or behind it, or the same: it
|
|
204
|
+
* is left, and an apply's push sends what it holds that the forge lacks.
|
|
205
|
+
*
|
|
206
|
+
* Nothing is pushed, so a rewritten ledger never goes back to the forge. A
|
|
207
|
+
* forge that can't be reached leaves everything as it is; the job's own
|
|
208
|
+
* fetch says why.
|
|
209
|
+
*/
|
|
210
|
+
export function reconcileCiServeLedger(top: string): CiServeEvent | undefined {
|
|
211
|
+
const local = tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/heads/${LEDGER}^{commit}`]);
|
|
212
|
+
const listed = gitStatus(top, ["ls-remote", "--exit-code", "origin", `refs/heads/${LEDGER}`]);
|
|
213
|
+
if (listed.code === 2) {
|
|
214
|
+
// A remote-tracking ref left from a deleted branch would be the next push's lease, and refuse it.
|
|
215
|
+
tryGit(top, ["update-ref", "-d", `refs/remotes/origin/${LEDGER}`]);
|
|
216
|
+
if (!local) return undefined;
|
|
217
|
+
tryGit(top, ["update-ref", "-d", `refs/heads/${LEDGER}`, local]);
|
|
218
|
+
return { type: "ledger", action: "deleted", local };
|
|
219
|
+
}
|
|
220
|
+
if (listed.code !== 0 || !local) return undefined;
|
|
221
|
+
const forge = listed.out.split(/\s+/)[0];
|
|
222
|
+
if (!forge || forge === local) return undefined;
|
|
223
|
+
if (gitStatus(top, ["fetch", "--quiet", "--no-tags", "origin", `+refs/heads/${LEDGER}:${FORGE_LEDGER}`]).code !== 0) return undefined;
|
|
224
|
+
try {
|
|
225
|
+
const ahead = gitStatus(top, ["merge-base", "--is-ancestor", forge, local]).code === 0;
|
|
226
|
+
const behind = gitStatus(top, ["merge-base", "--is-ancestor", local, forge]).code === 0;
|
|
227
|
+
if (ahead || behind) return undefined;
|
|
228
|
+
tryGit(top, ["update-ref", `refs/heads/${LEDGER}`, forge, local]);
|
|
229
|
+
tryGit(top, ["update-ref", `refs/remotes/origin/${LEDGER}`, forge]);
|
|
230
|
+
return { type: "ledger", action: "reset", local, forge };
|
|
231
|
+
} finally {
|
|
232
|
+
tryGit(top, ["update-ref", "-d", FORGE_LEDGER]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A commit message that asks for no CI run, as Actions and GitLab read it. */
|
|
237
|
+
const SKIP_CI = /\[(?:skip ci|ci skip)\]/i;
|
|
238
|
+
|
|
239
|
+
/** Whether `commit`'s message asks for no CI run. A commit this clone can't read is not skipped. */
|
|
240
|
+
function skipsCi(top: string, commit: string): boolean {
|
|
241
|
+
const body = tryGit(top, ["log", "-1", "--format=%B", commit]);
|
|
242
|
+
return body !== undefined && SKIP_CI.test(body);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* How the target branch moved from `from` to `head` (#4122): `fast-forward`
|
|
247
|
+
* when `head` descends from `from`, `other` when it does not, and `unknown`
|
|
248
|
+
* when `head` could not be fetched. Once `head` is here its history is too,
|
|
249
|
+
* so a `from` this clone lacks is not in that history.
|
|
250
|
+
*/
|
|
251
|
+
function targetMove(top: string, branch: string, from: string, head: string): "fast-forward" | "other" | "unknown" {
|
|
252
|
+
tryGit(top, ["fetch", "--quiet", "--no-tags", "origin", branch]);
|
|
253
|
+
if (gitStatus(top, ["cat-file", "-e", `${head}^{commit}`]).code !== 0) return "unknown";
|
|
254
|
+
return gitStatus(top, ["merge-base", "--is-ancestor", from, head]).code === 0 ? "fast-forward" : "other";
|
|
255
|
+
}
|
|
256
|
+
|
|
155
257
|
/** Where a serve keeps its state when `--state` names nowhere: beside the run events, in the repository's common git directory. */
|
|
156
258
|
export function defaultCiServeStateDir(cwd: string): string {
|
|
157
259
|
const common = tryGit(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
@@ -202,15 +304,23 @@ const failedEntry = (entry: CiServeJobState | undefined, key: string, rt: CiServ
|
|
|
202
304
|
|
|
203
305
|
/**
|
|
204
306
|
* One poll of the forge, as triggers: an apply when the target's head moved
|
|
205
|
-
* since the last apply (none on the first poll, which only
|
|
307
|
+
* since the last apply by a fast-forward (none on the first poll, which only
|
|
308
|
+
* notes the head, and none for a head whose message says `[skip ci]`),
|
|
206
309
|
* then a plan for each open pull request head not yet planned, oldest pull
|
|
207
310
|
* request first. A job already in the state file is left out unless it
|
|
208
|
-
* failed and `rerunFailed` is set. The first poll's head
|
|
209
|
-
* state file.
|
|
311
|
+
* failed and `rerunFailed` is set. The first poll's head, and a head seen
|
|
312
|
+
* and not applied, are written to the state file.
|
|
210
313
|
*/
|
|
211
314
|
export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime = newCiServeRuntime()): Promise<{ triggers: CiServeTrigger[]; events: CiServeEvent[] }> {
|
|
212
315
|
const place = servePlace(opts);
|
|
213
|
-
const
|
|
316
|
+
const fresh = { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) };
|
|
317
|
+
const state = readCiServeState(place.stateFile, fresh);
|
|
318
|
+
// Read again at the write: a job of this serve may have settled while the forge answered.
|
|
319
|
+
const update = (fn: (s: CiServeState) => void): void => {
|
|
320
|
+
const s = readCiServeState(place.stateFile, fresh);
|
|
321
|
+
fn(s);
|
|
322
|
+
writeState(place.stateFile, s);
|
|
323
|
+
};
|
|
214
324
|
const triggers: CiServeTrigger[] = [];
|
|
215
325
|
const events: CiServeEvent[] = [];
|
|
216
326
|
const [head, prs] = await Promise.all([opts.client.branchHead(place.branch), opts.client.openPullRequests(place.branch)]);
|
|
@@ -219,12 +329,23 @@ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime =
|
|
|
219
329
|
events.push({ type: "no-branch", branch: place.branch });
|
|
220
330
|
} else if (state.target === undefined) {
|
|
221
331
|
// What the branch held before this runner watched it was applied by whatever ran then.
|
|
222
|
-
|
|
223
|
-
|
|
332
|
+
update((s) => {
|
|
333
|
+
s.target = head;
|
|
334
|
+
});
|
|
224
335
|
events.push({ type: "baseline", commit: head });
|
|
225
336
|
} else if (head !== state.target) {
|
|
337
|
+
const from = state.target;
|
|
338
|
+
const move = targetMove(place.top, place.branch, from, head);
|
|
226
339
|
const entry = state.jobs[ciServeKey({ job: "apply", commit: head })];
|
|
227
|
-
if (
|
|
340
|
+
if (move === "unknown") {
|
|
341
|
+
events.push({ type: "error", message: `${place.branch} is at ${head.slice(0, 12)} on the forge, and that commit could not be fetched; the next poll tries again` });
|
|
342
|
+
} else if (move === "other" || (!entry && skipsCi(place.top, head))) {
|
|
343
|
+
// #4122: a move that is not a fast-forward, or a head that asks for no run, is seen and not applied.
|
|
344
|
+
update((s) => {
|
|
345
|
+
s.target = head;
|
|
346
|
+
});
|
|
347
|
+
events.push({ type: "not-run", job: "apply", commit: head, reason: move === "other" ? "not-fast-forward" : "skip-ci", ...(move === "other" ? { branch: place.branch, from } : {}) });
|
|
348
|
+
} else if (!entry) triggers.push({ job: "apply", commit: head, base: from });
|
|
228
349
|
else if (opts.rerunFailed && failedEntry(entry, ciServeKey(entry), rt)) triggers.push({ job: "apply", commit: head, base: entry.base, rerun: true });
|
|
229
350
|
} else {
|
|
230
351
|
const key = ciServeKey({ job: "apply", commit: head });
|
|
@@ -245,8 +366,17 @@ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime =
|
|
|
245
366
|
const key = ciServeKey({ job: "plan", commit: pr.headSha, pr: pr.number });
|
|
246
367
|
const entry = state.jobs[key];
|
|
247
368
|
const base = pr.baseSha ?? head;
|
|
248
|
-
if (!base) continue;
|
|
249
|
-
if (!entry)
|
|
369
|
+
if (!base || state.skipped?.[key]) continue;
|
|
370
|
+
if (!entry) {
|
|
371
|
+
// #4122: a head whose message asks for no run is seen and not planned.
|
|
372
|
+
if (gitStatus(place.top, ["cat-file", "-e", `${pr.headSha}^{commit}`]).code !== 0) tryGit(place.top, ["fetch", "--quiet", "--no-tags", "origin", `refs/pull/${pr.number}/head`]);
|
|
373
|
+
if (skipsCi(place.top, pr.headSha)) {
|
|
374
|
+
update((s) => {
|
|
375
|
+
s.skipped = { ...s.skipped, [key]: "skip-ci" };
|
|
376
|
+
});
|
|
377
|
+
events.push({ type: "not-run", job: "plan", commit: pr.headSha, pr: pr.number, reason: "skip-ci" });
|
|
378
|
+
} else triggers.push({ job: "plan", commit: pr.headSha, base, pr: pr.number });
|
|
379
|
+
}
|
|
250
380
|
else if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: "plan", commit: pr.headSha, base: entry.base, pr: pr.number, rerun: true });
|
|
251
381
|
}
|
|
252
382
|
return { triggers, events };
|
|
@@ -309,6 +439,12 @@ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly C
|
|
|
309
439
|
lease = got.lease;
|
|
310
440
|
}
|
|
311
441
|
|
|
442
|
+
// #4121: with no other job of this serve writing it, the clone's ledger follows the forge's.
|
|
443
|
+
if (rt.inflight.size === 0) {
|
|
444
|
+
const ledger = reconcileCiServeLedger(place.top);
|
|
445
|
+
if (ledger) emit(ledger);
|
|
446
|
+
}
|
|
447
|
+
|
|
312
448
|
const previous = readCiServeState(place.stateFile, fresh).jobs[key];
|
|
313
449
|
const attempt = (previous?.attempts ?? 0) + 1;
|
|
314
450
|
const runId = `chant-serve-${now().getTime()}-${t.job}-${t.commit.slice(0, 8)}`;
|
|
@@ -347,17 +483,20 @@ async function runJob(
|
|
|
347
483
|
t: CiServeTrigger,
|
|
348
484
|
paths: { runId: string; report: string; resume?: string },
|
|
349
485
|
held: { lease: LeaseRecord; holder: string; ttlMs: number; now: () => Date } | undefined,
|
|
350
|
-
): Promise<Pick<CiServeJobState, "status" | "failure" | "report">> {
|
|
486
|
+
): Promise<Pick<CiServeJobState, "status" | "failure" | "report" | "pr">> {
|
|
351
487
|
const renew = held
|
|
352
488
|
? setInterval(() => {
|
|
353
489
|
void acquireLease(place.group, held.holder, { cwd: opts.cwd, ttlMs: held.ttlMs, mode: "renew", token: held.lease.token, now: held.now }).catch(() => undefined);
|
|
354
490
|
}, Math.max(1000, Math.floor(held.ttlMs / 3)))
|
|
355
491
|
: undefined;
|
|
356
492
|
renew?.unref?.();
|
|
493
|
+
let merged: number | undefined;
|
|
357
494
|
try {
|
|
358
495
|
// The pull request's head is on the forge's refs/pull/<n>/head, the target's on the branch.
|
|
359
496
|
tryGit(place.top, ["fetch", "--quiet", "--no-tags", "origin", t.job === "plan" ? `refs/pull/${t.pr}/head` : place.branch]);
|
|
360
497
|
const hostEnv = { ...(opts.job?.hostEnv ?? process.env), GITHUB_REPOSITORY: opts.repo };
|
|
498
|
+
// An apply is for a merge: pass the pull request whose merge commit it is, so the commit status link and the run index use `<pr>/<commit>`. None found, or a forge that errs, is today's behaviour.
|
|
499
|
+
if (t.job === "apply" && opts.client.pullRequestFor) merged = (await opts.client.pullRequestFor(t.commit).catch(() => null)) ?? undefined;
|
|
361
500
|
const run = opts.run ?? ciRun;
|
|
362
501
|
const r = await run({
|
|
363
502
|
...opts.job,
|
|
@@ -367,16 +506,16 @@ async function runJob(
|
|
|
367
506
|
env: opts.env,
|
|
368
507
|
forge: opts.forge,
|
|
369
508
|
base: t.base,
|
|
370
|
-
...(t.job === "plan" ? { pr: t.pr } : {}),
|
|
509
|
+
...(t.job === "plan" ? { pr: t.pr } : merged !== undefined ? { pr: merged } : {}),
|
|
371
510
|
commit: t.commit,
|
|
372
511
|
output: paths.report,
|
|
373
512
|
runId: paths.runId,
|
|
374
513
|
...(paths.resume ? { resumeRecord: paths.resume } : {}),
|
|
375
514
|
});
|
|
376
515
|
const why = r.record.failure ? `${r.record.failure.code}: ${r.record.failure.reason}` : r.failure ? `${r.failure.phase}: ${r.failure.error.split("\n")[0]}` : undefined;
|
|
377
|
-
return { status: r.status === "ok" ? "ok" : "fail", ...(why && r.status !== "ok" ? { failure: why } : {}), ...(r.report ? { report: r.report } : {}) };
|
|
516
|
+
return { status: r.status === "ok" ? "ok" : "fail", ...(merged !== undefined ? { pr: merged } : {}), ...(why && r.status !== "ok" ? { failure: why } : {}), ...(r.report ? { report: r.report } : {}) };
|
|
378
517
|
} catch (err) {
|
|
379
|
-
return { status: "fail", failure: message(err) };
|
|
518
|
+
return { status: "fail", ...(merged !== undefined ? { pr: merged } : {}), failure: message(err) };
|
|
380
519
|
} finally {
|
|
381
520
|
if (renew) clearInterval(renew);
|
|
382
521
|
if (held) await releaseLease(place.group, held.holder, held.lease.token, { cwd: opts.cwd }).catch(() => false);
|
|
@@ -432,6 +571,14 @@ export function formatCiServeEvent(e: CiServeEvent): string {
|
|
|
432
571
|
return `${at(e.job, e.commit, e.pr)} started, run ${e.runId}, attempt ${e.attempt}${e.resume ? ", resuming" : ""}`;
|
|
433
572
|
case "settled":
|
|
434
573
|
return `${at(e.job, e.commit, e.pr)} ${e.status === "ok" ? "passed" : `failed${e.failure ? `: ${e.failure}` : ""}`}${e.report ? `; report ${e.report}` : ""}`;
|
|
574
|
+
case "not-run":
|
|
575
|
+
return e.reason === "skip-ci"
|
|
576
|
+
? `${at(e.job, e.commit, e.pr)} is not run: its commit message says [skip ci]`
|
|
577
|
+
: `${e.branch ?? "the target branch"} moved from ${e.from?.slice(0, 12) ?? "a commit"} to ${e.commit.slice(0, 12)}, which is not a fast-forward, so ${e.commit.slice(0, 12)} is recorded as seen and not applied`;
|
|
578
|
+
case "ledger":
|
|
579
|
+
return e.action === "deleted"
|
|
580
|
+
? `the forge has no ${LEDGER}, so this clone's, at ${e.local.slice(0, 12)}, was deleted`
|
|
581
|
+
: `the forge's ${LEDGER} at ${e.forge!.slice(0, 12)} does not contain this clone's at ${e.local.slice(0, 12)}, so this clone's was reset to the forge's and not pushed`;
|
|
435
582
|
case "error":
|
|
436
583
|
return `error: ${e.message}`;
|
|
437
584
|
}
|
|
@@ -439,10 +586,37 @@ export function formatCiServeEvent(e: CiServeEvent): string {
|
|
|
439
586
|
|
|
440
587
|
// ── The command ──────────────────────────────────────────────────────────────
|
|
441
588
|
|
|
589
|
+
/**
|
|
590
|
+
* `chant ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]` (#4099):
|
|
591
|
+
* make the host's runner key when there is none, print its public half on
|
|
592
|
+
* stdout, and say whether the target's head lists it. With --trust, commit
|
|
593
|
+
* the runner entry on a branch of its own and push it for review.
|
|
594
|
+
*/
|
|
595
|
+
export function runnerKeySetupCommand(ctx: CommandContext): number {
|
|
596
|
+
const { args } = ctx;
|
|
597
|
+
const cwd = process.cwd();
|
|
598
|
+
const top = gitTop(cwd);
|
|
599
|
+
if (!top) throw new CiRunError("the runner key is kept in the repository's git directory, and this directory is not in a git repository");
|
|
600
|
+
if (args.ciPrincipal !== undefined && !args.trustKey) throw new CiRunError("--principal names who --trust lists the key for; give --trust with it");
|
|
601
|
+
const branch = args.branch ?? "main";
|
|
602
|
+
const setup = setupRunnerKey({
|
|
603
|
+
top,
|
|
604
|
+
stateDir: args.statePath ? resolve(args.statePath) : defaultCiServeStateDir(cwd),
|
|
605
|
+
branch,
|
|
606
|
+
...(args.ciPrincipal ? { principal: args.ciPrincipal } : {}),
|
|
607
|
+
...(args.trustKey ? { trust: true } : {}),
|
|
608
|
+
...(args.by ? { by: args.by } : {}),
|
|
609
|
+
});
|
|
610
|
+
console.log(setup.key.publicKey);
|
|
611
|
+
console.error(formatRunnerKeySetup(setup, branch));
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
|
|
442
615
|
const USAGE =
|
|
616
|
+
"chant ci serve --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>] | " +
|
|
443
617
|
"chant ci serve --forge forgejo|github --repo <owner/repo> --env <env> [--interval <s>] [--max-jobs <n>] [--state <dir>] " +
|
|
444
|
-
"[--author <login>]... [--once] [--rerun-failed] [--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] " +
|
|
445
|
-
"[--image <image>] [--before-script <line>]... [--pr-build] [--pr-build-command <line>] [--chant <command>] [--pass-env <NAME>]... [--key <runner key.pem>]";
|
|
618
|
+
"[--author <login>]... [--once] [--rerun-failed] [--gate <name>] [--wave-gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--no-status] [--status-url <url template>] " +
|
|
619
|
+
"[--image <image>] [--before-script <line>]... [--pr-build] [--pr-build-command <line>] [--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [--unsigned]";
|
|
446
620
|
|
|
447
621
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
448
622
|
|
|
@@ -458,46 +632,69 @@ export function ciServeInterval(value: string | undefined): number {
|
|
|
458
632
|
export async function runCiServe(ctx: CommandContext): Promise<number> {
|
|
459
633
|
const { args } = ctx;
|
|
460
634
|
try {
|
|
635
|
+
if (args.setupKey) return runnerKeySetupCommand(ctx);
|
|
461
636
|
const forge = args.forge;
|
|
462
637
|
if (forge !== "forgejo" && forge !== "github") throw new CiRunError(`--forge names the forge ci serve polls: --forge forgejo|github${forge ? `, not ${JSON.stringify(forge)}` : ""}`);
|
|
463
638
|
if (!args.ciRepo) throw new CiRunError("--repo names the repository whose pull requests ci serve polls: --repo <owner/repo>");
|
|
464
639
|
if (!args.env) throw new CiRunError("--env names the environment the pipeline is for: --env <env>");
|
|
465
640
|
for (const name of args.passEnv ?? []) if (!ENV_NAME.test(name)) throw new CiRunError(`--pass-env takes a variable's name, not ${JSON.stringify(name)}`);
|
|
466
641
|
const intervalMs = ciServeInterval(args.interval);
|
|
467
|
-
const
|
|
468
|
-
const
|
|
642
|
+
const cwd = process.cwd();
|
|
643
|
+
const top = gitTop(cwd);
|
|
644
|
+
if (!top) throw new CiRunError("ci serve runs in a clone of the repository it serves, and this directory is not in a git repository");
|
|
645
|
+
const stateDir = args.statePath ? resolve(args.statePath) : defaultCiServeStateDir(cwd);
|
|
646
|
+
const branch = args.branch ?? "main";
|
|
647
|
+
// #4099: the host's key, checked against .chant/trust.json at the target's head before the first poll.
|
|
648
|
+
// #4156: signing is opt-in. Without ci.green.signedSince no key is quiet, and an untrusted one warns once and serves unsigned.
|
|
649
|
+
const keyed = ciServeRunnerKey({
|
|
650
|
+
top,
|
|
651
|
+
stateDir,
|
|
652
|
+
branch,
|
|
653
|
+
...(args.key !== undefined ? { keyFile: resolve(args.key) } : {}),
|
|
654
|
+
envKey: process.env[RUNNER_KEY_ENV] || undefined,
|
|
655
|
+
unsigned: args.unsigned,
|
|
656
|
+
required: signedRunsRequired(cwd),
|
|
657
|
+
});
|
|
658
|
+
const fromFlag = keyed.fromFlag;
|
|
659
|
+
const runnerKey = keyed.pem;
|
|
660
|
+
if (keyed.warning) console.error(formatWarning({ message: keyed.warning }));
|
|
661
|
+
if (runnerKey !== undefined) console.error(`ci serve: signing runs with the runner key from ${keyed.source}`);
|
|
662
|
+
else if (keyed.note && args.verbose) console.error(`ci serve: ${keyed.note}`);
|
|
663
|
+
// #4096: read once, so a loose or broken secrets file stops the serve before its first poll.
|
|
664
|
+
const secrets = ciSecretsOf(args.ciSecrets);
|
|
665
|
+
// #4160: without --report-url, the declaration's ci.reportUrl.
|
|
666
|
+
const reportUrl = await reportUrlOf(args.reportUrl, cwd);
|
|
667
|
+
if (reportUrl && !args.reportUrl) console.error(`ci serve: reports and the run index go to ${reportUrl}, the declaration's ci.reportUrl`);
|
|
469
668
|
const hostEnv = { ...process.env, GITHUB_REPOSITORY: args.ciRepo };
|
|
470
669
|
const client = forgeFromEnv(forge, hostEnv);
|
|
670
|
+
const { status, warning } = ciStatusFromEnv({ forge, hostEnv, noStatus: args.noStatus, statusUrl: args.statusUrl });
|
|
671
|
+
if (warning) console.error(formatWarning({ message: warning }));
|
|
471
672
|
if (!client.openPullRequests || !client.branchHead) throw new CiRunError(`the ${forge} client cannot list pull requests`);
|
|
472
673
|
const opts: CiServeOptions = {
|
|
473
|
-
cwd
|
|
674
|
+
cwd,
|
|
474
675
|
forge,
|
|
475
676
|
repo: args.ciRepo,
|
|
476
677
|
env: args.env,
|
|
477
|
-
client: { openPullRequests: client.openPullRequests.bind(client), branchHead: client.branchHead.bind(client) },
|
|
478
|
-
|
|
678
|
+
client: { openPullRequests: client.openPullRequests.bind(client), branchHead: client.branchHead.bind(client), pullRequestFor: client.pullRequestFor.bind(client) },
|
|
679
|
+
stateDir,
|
|
479
680
|
...(args.maxJobs ? { maxJobs: args.maxJobs } : {}),
|
|
480
681
|
...(args.ciAuthors?.length ? { authors: args.ciAuthors } : {}),
|
|
481
682
|
...(args.rerunFailed ? { rerunFailed: true } : {}),
|
|
482
683
|
job: {
|
|
483
|
-
loop: {
|
|
484
|
-
|
|
485
|
-
...(args.branch ? { branch: args.branch } : {}),
|
|
486
|
-
...(args.eventsJson ? { events: true } : {}),
|
|
487
|
-
...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
|
|
488
|
-
...(args.prBuild ? { build: true } : {}),
|
|
489
|
-
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
490
|
-
},
|
|
684
|
+
loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
|
|
685
|
+
...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
|
|
491
686
|
...(args.image ? { image: args.image } : {}),
|
|
492
687
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
493
688
|
...(args.ciChant ? { chant: args.ciChant } : {}),
|
|
494
689
|
...(args.passEnv?.length ? { passEnv: args.passEnv } : {}),
|
|
690
|
+
...(status ? { status } : {}),
|
|
691
|
+
...(secrets ? { secrets } : {}),
|
|
495
692
|
...(runnerKey !== undefined ? { runnerKey, runnerKeyFromEnv: !fromFlag } : {}),
|
|
496
693
|
...(args.eventsJson ? { runEvents: (line: string) => void process.stdout.write(line) } : {}),
|
|
497
694
|
hostEnv,
|
|
498
695
|
stepOutput: (line) => void process.stderr.write(`${line}\n`),
|
|
499
696
|
},
|
|
500
|
-
log: (e) => console.error(e.type === "error" ? formatWarning({ message: formatCiServeEvent(e) }) : `ci serve: ${formatCiServeEvent(e)}`),
|
|
697
|
+
log: (e) => console.error(e.type === "error" || e.type === "ledger" ? formatWarning({ message: formatCiServeEvent(e) }) : `ci serve: ${formatCiServeEvent(e)}`),
|
|
501
698
|
};
|
|
502
699
|
if (args.once) {
|
|
503
700
|
const pass = await ciServePass(opts);
|
|
@@ -177,6 +177,11 @@
|
|
|
177
177
|
"properties": {
|
|
178
178
|
"green": {
|
|
179
179
|
"$ref": "#/$defs/ciGreen"
|
|
180
|
+
},
|
|
181
|
+
"reportUrl": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"minLength": 1,
|
|
184
|
+
"description": "Where the pull-request jobs copy their reports and events, and where chant ci run keeps the run index, <reportUrl>/runs/index.json (#4160): an s3://<bucket>/<prefix> or file://<directory> URL, as --report-url takes. chant build --pr-loop, chant ci run, chant ci serve and chant ci runs use it when --report-url is not given, and the flag wins. Readers find it in chant workspace ls --json as ci.reportUrl. Added in schema 1 by chant 0.124.32, so a declaration that uses it sets minReader to 0.124.32 or newer."
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
},
|
|
@@ -581,7 +586,7 @@
|
|
|
581
586
|
},
|
|
582
587
|
"box": {
|
|
583
588
|
"type": "object",
|
|
584
|
-
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). A member that builds the fountain lexicon's Box declares the callback token fountain gives its
|
|
589
|
+
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). A member that builds the fountain lexicon's Box declares the callback token fountain gives its sandbox as the capability fountain-callback, brokered by fountain, with scope owner, or chant workspace check fails (WSP125, #2780). Its intent names the decision record that says what the box is for (#2850): chant workspace status --json reports the record's state and answer, and chant workspace check fails when no decision record has the id (WSP126) and warns when the record constrains no member or path of this workspace (WSP127, #2857). Its services list what the box runs under its supervisor (#2880), which the fly lexicon observes, restarts and applies through sprite-env, and which chant workspace status --json prints. Its factory declares what the box builds, how a build is checked, which member declares the builder agents and where a finished build is published (#3146, ws-077), its listing what the box shows of itself (#3146), and its replicate where the box's work in progress is pushed so a lost box loses none of it (#3172, ws-085). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
585
590
|
"properties": {
|
|
586
591
|
"intent": {
|
|
587
592
|
"type": "string",
|
|
@@ -1518,7 +1523,7 @@
|
|
|
1518
1523
|
"signedSince": {
|
|
1519
1524
|
"type": "string",
|
|
1520
1525
|
"pattern": "^[0-9a-f]{40}([0-9a-f]{24})?$",
|
|
1521
|
-
"description": "Added by #4031, read by chant 0.124.28 and newer. The full sha of the first commit on branch whose ci/green and ci/revoked tags must carry a DSSE statement in their annotation, signed by a key .chant/trust.json at base lists under runners for a principal holding the runner role. A tag on a strict first-parent ancestor of this commit counts unsigned, so existing tags keep working; a tag on any other commit, this one included, needs a statement. Position in history decides, never a date, which whoever makes a commit or a tag sets. A reader refuses a sha that names no commit in its clone with declaration-invalid. Absent, no tag needs a statement."
|
|
1526
|
+
"description": "Added by #4031, read by chant 0.124.28 and newer. The full sha of the first commit on branch whose ci/green and ci/revoked tags must carry a DSSE statement in their annotation, signed by a key .chant/trust.json at base lists under runners for a principal holding the runner role. A tag on a strict first-parent ancestor of this commit counts unsigned, so existing tags keep working; a tag on any other commit, this one included, needs a statement. Position in history decides, never a date, which whoever makes a commit or a tag sets. A reader refuses a sha that names no commit in its clone with declaration-invalid. Absent, no tag needs a statement. Since chant 0.124.32 (#4156) it covers Op run records the same way, when ci.green.source is runs: an unsigned run counts on a strict first-parent ancestor of this commit and needs a statement on any other, ci serve refuses to start without a runner key trusted at the target's head, and ci run and chant run warn about a run they leave unsigned. Absent, an unsigned run counts and nothing asks for a key."
|
|
1522
1527
|
},
|
|
1523
1528
|
"source": {
|
|
1524
1529
|
"enum": [
|
|
@@ -436,6 +436,29 @@ describe("retention (#4036)", () => {
|
|
|
436
436
|
});
|
|
437
437
|
});
|
|
438
438
|
|
|
439
|
+
describe("ci.reportUrl (#4160)", () => {
|
|
440
|
+
test("an s3:// or file:// URL is read without its trailing slash, beside green or alone, and is null when absent", () => {
|
|
441
|
+
expect(parse(base([])).ci).toBeNull();
|
|
442
|
+
expect(parse(base([], { ci: {} })).ci).toEqual({ green: null, reportUrl: null });
|
|
443
|
+
expect(parse(base([], { ci: { reportUrl: "s3://acme-reports/prod/" } })).ci).toEqual({ green: null, reportUrl: "s3://acme-reports/prod" });
|
|
444
|
+
const green = { branch: "main", phases: { build: ["build"] }, require: ["build"] };
|
|
445
|
+
const d = parse(base([], { ci: { green, reportUrl: "file:///srv/chant-reports" } }));
|
|
446
|
+
expect(d.ci?.reportUrl).toBe("file:///srv/chant-reports");
|
|
447
|
+
expect(d.ci?.green?.branch).toBe("main");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("a URL no job can upload to is declaration-invalid, at ci.reportUrl, with the reason --report-url gives", () => {
|
|
451
|
+
for (const reportUrl of ["https://reports.example/acme", "ftp://example/x", "reports/"]) {
|
|
452
|
+
const err = failure(base([], { ci: { reportUrl } }));
|
|
453
|
+
expect(err.code).toBe("declaration-invalid");
|
|
454
|
+
expect(err.message).toBe(`ci.reportUrl ${reportUrl}: takes an s3://<bucket>/<prefix> or file://<directory> URL`);
|
|
455
|
+
expect(err.location?.line).toBeGreaterThan(1);
|
|
456
|
+
}
|
|
457
|
+
expect(failure(base([], { ci: { reportUrl: "" } })).code).toBe("declaration-invalid");
|
|
458
|
+
expect(failure(base([], { ci: { reportUrl: 7 } })).code).toBe("declaration-invalid");
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
439
462
|
describe("metrics (#4039)", () => {
|
|
440
463
|
test("failureWindowDays is read as written, without x- keys, and the block is null when absent", () => {
|
|
441
464
|
expect(parse(base([])).metrics).toBeNull();
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { RETENTION_KINDS, type RetentionOverride } from "../lifecycle/retention-policy";
|
|
20
|
+
import { parseReportUrl } from "../pr-report-url";
|
|
20
21
|
import schema from "./declaration.schema.json" with { type: "json" };
|
|
21
22
|
import { expandGlob } from "./glob";
|
|
22
23
|
import { EXAMPLES_KIND, holdsChantProject } from "./kinds";
|
|
@@ -232,6 +233,14 @@ export type CiSource = "checks" | "runs";
|
|
|
232
233
|
/** The declaration's `ci` block (#3573). */
|
|
233
234
|
export interface CiDeclaration {
|
|
234
235
|
green: CiGreen | null;
|
|
236
|
+
/**
|
|
237
|
+
* `ci.reportUrl` (#4160): where the pull-request jobs copy their reports
|
|
238
|
+
* and `chant ci run` keeps the run index, `<reportUrl>/runs/index.json`.
|
|
239
|
+
* An `s3://` or `file://` URL, as `--report-url` takes, without a trailing
|
|
240
|
+
* slash. The commands that take `--report-url` default to it. Null when
|
|
241
|
+
* the declaration sets none.
|
|
242
|
+
*/
|
|
243
|
+
reportUrl: string | null;
|
|
235
244
|
}
|
|
236
245
|
|
|
237
246
|
export interface MemberRole {
|
|
@@ -1087,8 +1096,15 @@ function retentionOf(raw: unknown): RetentionOverride | null {
|
|
|
1087
1096
|
|
|
1088
1097
|
function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocation): CiDeclaration | null {
|
|
1089
1098
|
if (raw === undefined) return null;
|
|
1090
|
-
const block = raw as { green?: { branch: string; window?: string; phases: Record<string, string[] | { runs: string[]; skipped?: CiSkipped }>; require: string[]; signedSince?: string; source?: CiSource } };
|
|
1091
|
-
|
|
1099
|
+
const block = raw as { reportUrl?: string; green?: { branch: string; window?: string; phases: Record<string, string[] | { runs: string[]; skipped?: CiSkipped }>; require: string[]; signedSince?: string; source?: CiSource } };
|
|
1100
|
+
let reportUrl: string | null = null;
|
|
1101
|
+
if (block.reportUrl !== undefined) {
|
|
1102
|
+
// #4160: refused here as pr-plan and ci run refuse the flag, so a reader never gets a URL no job can write to.
|
|
1103
|
+
const target = parseReportUrl(block.reportUrl, "ci.reportUrl");
|
|
1104
|
+
if ("error" in target) throw new WorkspaceReadError("declaration-invalid", target.error, at("/ci/reportUrl"));
|
|
1105
|
+
reportUrl = target.url;
|
|
1106
|
+
}
|
|
1107
|
+
if (block.green === undefined) return { green: null, reportUrl };
|
|
1092
1108
|
const g = block.green;
|
|
1093
1109
|
const phases: CiPhase[] = Object.entries(g.phases).map(([name, p]) => ({
|
|
1094
1110
|
name,
|
|
@@ -1104,7 +1120,7 @@ function ciOf(raw: unknown, at: (pointer: string, key?: boolean) => ErrorLocatio
|
|
|
1104
1120
|
at(`/ci/green/require/${i}`),
|
|
1105
1121
|
);
|
|
1106
1122
|
}
|
|
1107
|
-
return { green: { branch: g.branch, window: g.window ?? DEFAULT_CI_WINDOW, phases, require: [...g.require], signedSince: g.signedSince ?? null, source: g.source ?? "checks", pointer: "/ci/green" } };
|
|
1123
|
+
return { green: { branch: g.branch, window: g.window ?? DEFAULT_CI_WINDOW, phases, require: [...g.require], signedSince: g.signedSince ?? null, source: g.source ?? "checks", pointer: "/ci/green" }, reportUrl };
|
|
1108
1124
|
}
|
|
1109
1125
|
|
|
1110
1126
|
function agentsOf(raw: unknown, members: Member[], at: (pointer: string, key?: boolean) => ErrorLocation): AgentDeclaration[] {
|
package/src/workspace/export.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { parseJsonText } from "./jsonc";
|
|
|
44
44
|
import { fileHash, LOCK_FILE, LockError, parseLock, renderLock, type Lineage, type LineageLock } from "./lineage-lock";
|
|
45
45
|
import { loadRecordKind } from "./records";
|
|
46
46
|
import { workingTree } from "./tree";
|
|
47
|
+
import { isWorkDir } from "../lifecycle/work-dir";
|
|
47
48
|
|
|
48
49
|
/** The role that marks the member an export is written into. */
|
|
49
50
|
export const EXPORT_ROLE = "export";
|
|
@@ -142,7 +143,7 @@ export function gitTopOf(dir: string): string | undefined {
|
|
|
142
143
|
/**
|
|
143
144
|
* The files under `dir` (absolute) that git tracks or would track, relative
|
|
144
145
|
* to `dir`, that exist in the working tree. Outside git, every file, leaving
|
|
145
|
-
* out `.git` and
|
|
146
|
+
* out `.git`, `node_modules` and `.chant/work`.
|
|
146
147
|
*/
|
|
147
148
|
export function listFiles(dir: string): string[] {
|
|
148
149
|
if (!existsSync(dir)) return [];
|
|
@@ -167,8 +168,10 @@ export function listFiles(dir: string): string[] {
|
|
|
167
168
|
for (const d of readdirSync(join(dir, rel), { withFileTypes: true })) {
|
|
168
169
|
if (d.name === ".git" || d.name === "node_modules") continue;
|
|
169
170
|
const p = rel ? `${rel}/${d.name}` : d.name;
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
// `.chant/work` holds work worktrees, each a copy of the checkout (#2893).
|
|
172
|
+
if (d.isDirectory()) {
|
|
173
|
+
if (!isWorkDir(p)) walk(p);
|
|
174
|
+
} else if (d.isFile()) out.push(p);
|
|
172
175
|
}
|
|
173
176
|
};
|
|
174
177
|
walk("");
|
|
@@ -103,7 +103,13 @@
|
|
|
103
103
|
{
|
|
104
104
|
"type": "object",
|
|
105
105
|
"required": ["green"],
|
|
106
|
-
"properties": {
|
|
106
|
+
"properties": {
|
|
107
|
+
"green": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/ciGreen" }] },
|
|
108
|
+
"reportUrl": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Added in contract 1 by chant 0.124.32 (#4160). The declaration's ci.reportUrl, an s3:// or file:// URL without a trailing slash, present only when it is set. The run index is at <reportUrl>/runs/index.json (ci-run-index), and each job's report under <reportUrl>/<pr>/<commit>[/<member>]/."
|
|
111
|
+
}
|
|
112
|
+
}
|
|
107
113
|
}
|
|
108
114
|
]
|
|
109
115
|
},
|
package/src/workspace/ls.ts
CHANGED
|
@@ -186,8 +186,12 @@ export type LsDocument =
|
|
|
186
186
|
groups: LsGroup[];
|
|
187
187
|
/** Every declared diagram, the workspace's own first, then each member's, in file order (#2764). */
|
|
188
188
|
diagrams: LsDiagram[];
|
|
189
|
-
/**
|
|
190
|
-
|
|
189
|
+
/**
|
|
190
|
+
* What the workspace's CI decides about its commits (#3573), or null
|
|
191
|
+
* with no `ci` block. `reportUrl` (#4160) is the declared report URL,
|
|
192
|
+
* where the run index is kept, present only when it is set.
|
|
193
|
+
*/
|
|
194
|
+
ci: { green: LsCiGreen | null; reportUrl?: string } | null;
|
|
191
195
|
summary: { members: number; unreadable: number; groups: number; matches: number };
|
|
192
196
|
}
|
|
193
197
|
| {
|
|
@@ -330,6 +334,7 @@ function readListing(query: LsQuery): { doc: LsDocument; declaration?: Declarati
|
|
|
330
334
|
...(declaration.ci.green.source === "runs" ? { source: "runs" as const } : {}),
|
|
331
335
|
}
|
|
332
336
|
: null,
|
|
337
|
+
...(declaration.ci.reportUrl !== null ? { reportUrl: declaration.ci.reportUrl } : {}),
|
|
333
338
|
}
|
|
334
339
|
: null,
|
|
335
340
|
summary: {
|
|
@@ -51,6 +51,11 @@ describe("integrityOf", () => {
|
|
|
51
51
|
expect(integrityOf(join(dir({ "d/a": "1", "d/b": "2", "d/e": "" }), "d")).integrity).not.toBe(base);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test("a work worktree under .chant/work is left out, and the rest of .chant is hashed (#2893)", () => {
|
|
55
|
+
const root = dir({ "d/a": "1", "d/.chant/work/W-1/a": "copy", "d/.chant/work/m/W-2/a": "copy", "d/.chant/notes": "n" });
|
|
56
|
+
expect(pinnedFiles(join(root, "d"))).toEqual([".chant/notes", "a"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
54
59
|
test("node_modules and .git are left out, and a symbolic link is refused", () => {
|
|
55
60
|
const root = dir({ "d/a": "1", "d/node_modules/x/index.js": "x", "d/.git/HEAD": "ref" });
|
|
56
61
|
expect(pinnedFiles(join(root, "d"))).toEqual(["a"]);
|