@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/src/cli/handlers/pr.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* chant components pr-plan --base <ref> --pr <n> --env <env> [--forge github|gitlab|forgejo]
|
|
7
7
|
* chant approve pr-<n> pr-apply --plan <digest> --approver github:<login> --sign
|
|
8
8
|
* chant components pr-apply --base <ref> --env <env> [--pr <n>] [--forge <kind>] [--require-review] [--resume <file>]
|
|
9
|
+
* [--wave-gate <gate> [--wave <k>]]
|
|
9
10
|
* ```
|
|
10
11
|
*
|
|
11
12
|
* Both measure the change from the merge base of `--base` and `HEAD`, select
|
|
@@ -27,6 +28,13 @@
|
|
|
27
28
|
* approval still stands, applies the rest without a fresh approval. When
|
|
28
29
|
* they are not covered it says why and runs as it would without a record.
|
|
29
30
|
*
|
|
31
|
+
* `--wave-gate <gate>` (#4003) applies the approved set a wave at a time,
|
|
32
|
+
* each wave planned after the one before it applied (`prApplyWaves`). A
|
|
33
|
+
* wave whose plan the approved change set covers applies under the pull
|
|
34
|
+
* request's approval; one whose plan moved waits at `<gate>-wave-<k>` under
|
|
35
|
+
* the pull request's op (exit 3). `--wave <k>` runs one wave, reading the
|
|
36
|
+
* approved set and the waves that applied from the `--resume` record.
|
|
37
|
+
*
|
|
30
38
|
* Every run writes `pr-plan.json` or `pr-apply.json` (`pr-report.schema.json`),
|
|
31
39
|
* `change-set.json` and `pr-note.md` under `--output` (default `.chant/pr`). *
|
|
32
40
|
* `--events-json` (#4041, #4007) streams the stage's run events to stdout,
|
|
@@ -48,6 +56,12 @@
|
|
|
48
56
|
* recorded under `pr-<n>-<member>`, and the note and the statuses carry the
|
|
49
57
|
* member's name, so each member's loop on one pull request stands apart.
|
|
50
58
|
* The name must be the member that owns the current directory.
|
|
59
|
+
*
|
|
60
|
+
* `--no-status` (#4161) posts neither the stage's status nor its note: the
|
|
61
|
+
* report and the note are written under `--output` only. `chant ci run
|
|
62
|
+
* --local` passes it. pr-plan then builds no forge client, since posting is
|
|
63
|
+
* all it uses one for; pr-apply still reads the pull request and its reviews
|
|
64
|
+
* through `--forge`.
|
|
51
65
|
*/
|
|
52
66
|
|
|
53
67
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -58,14 +72,19 @@ import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support";
|
|
|
58
72
|
import {
|
|
59
73
|
applyPrSet,
|
|
60
74
|
approvalOf,
|
|
75
|
+
approversWhoAuthored,
|
|
61
76
|
approversWithoutReview,
|
|
62
77
|
decidePrGate,
|
|
63
78
|
planPrSet,
|
|
64
79
|
prReport,
|
|
65
80
|
readOnlyLedger,
|
|
81
|
+
prWavePlan,
|
|
66
82
|
resumePrSet,
|
|
83
|
+
waveNotCovered,
|
|
67
84
|
type PrPlanSet,
|
|
68
85
|
} from "../../components/pr-run";
|
|
86
|
+
import { composeChangeSet, verifyChangeSetDigest } from "../../change-set";
|
|
87
|
+
import { waveGateName } from "../../gated-waves";
|
|
69
88
|
import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt, type PrApplyRecord } from "../../components/fan-out-record";
|
|
70
89
|
import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
|
|
71
90
|
import { samePlanDigest } from "../../lifecycle/plan-digest";
|
|
@@ -85,18 +104,22 @@ import {
|
|
|
85
104
|
prStatusState,
|
|
86
105
|
renderPrNote,
|
|
87
106
|
type PrApproval,
|
|
107
|
+
type PrMember,
|
|
88
108
|
type PrReport,
|
|
109
|
+
type PrWave,
|
|
110
|
+
type PrRound,
|
|
89
111
|
} from "../../pr-loop";
|
|
90
112
|
import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf, type ForgeKind, type PrForge } from "../../pr-forge";
|
|
91
113
|
import { GITHUB_COMMENT_LIMIT, GITLAB_NOTE_LIMIT } from "../../plan-summary";
|
|
92
114
|
import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
|
|
93
115
|
import { formatError, formatInfo, formatSuccess, formatWarning } from "../format";
|
|
116
|
+
import { reportUninstalledDependencies } from "../dependencies";
|
|
94
117
|
import { findWorkspaceRoot } from "../../project-root";
|
|
95
118
|
import { resolveChangedUnits } from "./fan-out";
|
|
96
119
|
import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run";
|
|
97
120
|
import type { CommandContext } from "../registry";
|
|
98
121
|
import type { ComponentChangeSignal, FanOutPlan } from "../../components/fan-out";
|
|
99
|
-
import type
|
|
122
|
+
import { failedRunExitCode, type DriverComponent } from "../../components/driver";
|
|
100
123
|
import type { CapabilityRegistry } from "../../components/capability";
|
|
101
124
|
|
|
102
125
|
|
|
@@ -161,7 +184,7 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply"): Promise<Pr
|
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
let forge: PrForge | undefined;
|
|
164
|
-
if (args.forge) {
|
|
187
|
+
if (args.forge && !(stage === "plan" && args.noStatus)) {
|
|
165
188
|
try {
|
|
166
189
|
forge = forgeFromEnv(args.forge as ForgeKind);
|
|
167
190
|
} catch (err) {
|
|
@@ -191,9 +214,15 @@ async function prepare(ctx: CommandContext, stage: "plan" | "apply"): Promise<Pr
|
|
|
191
214
|
config,
|
|
192
215
|
});
|
|
193
216
|
} catch (err) {
|
|
194
|
-
|
|
217
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
218
|
+
reportUninstalledDependencies([message], projectPath); // #4116
|
|
219
|
+
return fail(message);
|
|
220
|
+
}
|
|
221
|
+
if (!derived.success) {
|
|
222
|
+
// #4116: component files that import packages the project has not installed.
|
|
223
|
+
if (derived.error) reportUninstalledDependencies([derived.error], projectPath);
|
|
224
|
+
return fail(derived.error ?? "Could not discover components");
|
|
195
225
|
}
|
|
196
|
-
if (!derived.success) return fail(derived.error ?? "Could not discover components");
|
|
197
226
|
|
|
198
227
|
const seededOutputs: Record<string, Record<string, unknown>> = {};
|
|
199
228
|
for (const file of args.seedOutputs ?? []) {
|
|
@@ -271,6 +300,7 @@ function settleOf(status: PrReport["status"]): RunEventSettleStatus {
|
|
|
271
300
|
case "applied":
|
|
272
301
|
return "ok";
|
|
273
302
|
case "refused":
|
|
303
|
+
case "waiting":
|
|
274
304
|
return "gated";
|
|
275
305
|
default:
|
|
276
306
|
return "failed";
|
|
@@ -334,7 +364,7 @@ function printMembers(report: PrReport): void {
|
|
|
334
364
|
async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload?: PrReportUpload): Promise<void> {
|
|
335
365
|
const note = noteFor(report, p.forge);
|
|
336
366
|
writeOutputs(ctx, report, note, upload);
|
|
337
|
-
await publish(p.forge, report, note);
|
|
367
|
+
if (!ctx.args.noStatus) await publish(p.forge, report, note);
|
|
338
368
|
if (ctx.args.json) console.log(JSON.stringify(report, null, 2));
|
|
339
369
|
}
|
|
340
370
|
|
|
@@ -432,6 +462,7 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
432
462
|
set,
|
|
433
463
|
approval,
|
|
434
464
|
status,
|
|
465
|
+
...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
|
|
435
466
|
});
|
|
436
467
|
await finish(ctx, p, report, upload);
|
|
437
468
|
// The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
|
|
@@ -455,6 +486,8 @@ export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
|
|
|
455
486
|
async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
|
|
456
487
|
const { args } = ctx;
|
|
457
488
|
if (args.requireReview && !args.forge) return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
|
|
489
|
+
if (args.wave !== undefined && !args.waveGate) return fail("--wave runs one wave of a --wave-gate apply, so it needs --wave-gate <name>");
|
|
490
|
+
if (args.wave !== undefined && args.wave < 1) return fail(`--wave counts from 1, not ${args.wave}`);
|
|
458
491
|
const p = await prepare(ctx, "apply");
|
|
459
492
|
if (typeof p === "number") return p;
|
|
460
493
|
|
|
@@ -474,6 +507,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
474
507
|
const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
|
|
475
508
|
stage.stream = events;
|
|
476
509
|
events?.emit({ type: "run-start", waves: p.plan.waves });
|
|
510
|
+
if (args.waveGate) return prApplyWaves(ctx, p, { pr, op, release, upload, events });
|
|
477
511
|
|
|
478
512
|
// `--resume` (#3464): an earlier attempt at this change on this commit
|
|
479
513
|
// applied part of an approved set. When the members left are covered by
|
|
@@ -506,14 +540,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
506
540
|
const set = resumed?.set ?? (await planAll(p, events?.emit));
|
|
507
541
|
const base = { stage: "apply" as const, pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, set };
|
|
508
542
|
|
|
509
|
-
const stop =
|
|
510
|
-
events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
|
|
511
|
-
await finish(ctx, p, report, upload);
|
|
512
|
-
console.error((code === 0 ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
|
|
513
|
-
if (report.message) console.error(report.message);
|
|
514
|
-
printMembers(report);
|
|
515
|
-
return code;
|
|
516
|
-
};
|
|
543
|
+
const stop = stopper(ctx, p, events, upload);
|
|
517
544
|
|
|
518
545
|
if (p.plan.order.length === 0) {
|
|
519
546
|
return stop(prReport({ ...base, approval: { status: "pending" }, status: "nothing" }), 0);
|
|
@@ -582,22 +609,12 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
582
609
|
}
|
|
583
610
|
}
|
|
584
611
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
const strangers = approversWithoutReview(approval.approvedBy ?? [], reviewers);
|
|
593
|
-
if (strangers.length > 0) {
|
|
594
|
-
const message =
|
|
595
|
-
`The plan is approved by ${strangers.join(", ")}, who ${strangers.length === 1 ? "has" : "have"} no approving review on #${pr}` +
|
|
596
|
-
` (approving reviews: ${reviewers.join(", ") || "none"}). Nothing was applied. ` +
|
|
597
|
-
`An approval counts when it names the reviewer the way the forge does: ${p.forge.principalOf("<login>")}.`;
|
|
598
|
-
emitReviewStop(events, message);
|
|
599
|
-
return stop(prReport({ ...base, approval, status: "refused", refusal: "not-a-reviewer", message }), GATED_EXIT_CODE);
|
|
600
|
-
}
|
|
612
|
+
const reviewOf = reviewCheck(args, p, pr);
|
|
613
|
+
const review = await reviewOf(approval.approvedBy ?? [], "The plan");
|
|
614
|
+
if (review && "error" in review) return fail(review.error);
|
|
615
|
+
if (review) {
|
|
616
|
+
emitReviewStop(events, review.message);
|
|
617
|
+
return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
|
|
601
618
|
}
|
|
602
619
|
|
|
603
620
|
// The record is written before the first component runs and again as each
|
|
@@ -632,12 +649,38 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
632
649
|
};
|
|
633
650
|
recordAttempt();
|
|
634
651
|
|
|
652
|
+
// #3999: a round planned after an upstream applied waits on a gate of its
|
|
653
|
+
// own, under the same identity.gates rule and the same review checks.
|
|
654
|
+
const roundLedger = gitGateLedgerPort();
|
|
655
|
+
const decideRound = async ({ round, gate, set: planned }: { round: number; gate: string; set: PrPlanSet }) => {
|
|
656
|
+
console.error(formatInfo(`round ${round}: ${planned.members.map((m) => m.component).filter((c, i, all) => all.indexOf(c) === i).join(", ")} planned again after an upstream applied, ${planned.doc.digest}`));
|
|
657
|
+
await storeChangeSetForGate(ctx, p, planned.doc);
|
|
658
|
+
const port = {
|
|
659
|
+
...roundLedger,
|
|
660
|
+
...(roundLedger.approvalRule ? { approvalRule: (g: string, at?: { base?: string }) => roundLedger.approvalRule!(g === gate ? p.gate : g, at) } : {}),
|
|
661
|
+
};
|
|
662
|
+
const check = await emitGateStep(events?.emit, { op, gate }, () =>
|
|
663
|
+
decidePrGate(port, { op, gate, digest: planned.doc.digest, description: `pull request #${pr}, round ${round}: planned again after an upstream applied` }),
|
|
664
|
+
);
|
|
665
|
+
const roundApproval = approvalOf(check);
|
|
666
|
+
if (!check.satisfied) return { satisfied: false, approval: roundApproval };
|
|
667
|
+
const refused = await reviewOf(roundApproval.approvedBy ?? [], `Round ${round}'s plan`);
|
|
668
|
+
if (refused) {
|
|
669
|
+
const message = "error" in refused ? refused.error : refused.message;
|
|
670
|
+
emitReviewStop(events, message);
|
|
671
|
+
return { satisfied: false, approval: roundApproval, message };
|
|
672
|
+
}
|
|
673
|
+
return { satisfied: true, approval: roundApproval };
|
|
674
|
+
};
|
|
675
|
+
|
|
635
676
|
const applied = await applyPrSet({
|
|
636
677
|
components: p.components,
|
|
637
678
|
plan: resumed?.plan ?? p.plan,
|
|
638
679
|
registry: p.registry,
|
|
639
680
|
env: p.env,
|
|
640
681
|
set,
|
|
682
|
+
gate: p.gate,
|
|
683
|
+
decideRound,
|
|
641
684
|
...(resumed ? { resumed: prior } : {}),
|
|
642
685
|
// The apply is this run's waves, so the fan-out's own run framing is
|
|
643
686
|
// dropped. The log's lines go to stderr with or without --events-json.
|
|
@@ -668,13 +711,92 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
|
|
|
668
711
|
status: applied.status,
|
|
669
712
|
members: applied.members,
|
|
670
713
|
...(resumed ? { resumed: resumed.completed } : {}),
|
|
714
|
+
...(applied.rounds ? { rounds: applied.rounds } : {}),
|
|
715
|
+
...(applied.status === "waiting" ? { message: waitingMessage(op, applied.rounds) } : {}),
|
|
671
716
|
});
|
|
672
|
-
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
673
|
-
|
|
717
|
+
// A component that stopped at a gate of its own leaves the apply waiting on
|
|
718
|
+
// a person. A step that refused its plan because the target moved since
|
|
719
|
+
// the approval exits 3, as a moved digest does before anything applies (#3997).
|
|
720
|
+
const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
|
|
721
|
+
const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
|
|
674
722
|
if (applied.status === "applied") console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
|
|
675
723
|
return code;
|
|
676
724
|
}
|
|
677
725
|
|
|
726
|
+
/** How a stage stops: settle its events, write and publish the report, say how it ended, and return `code`. */
|
|
727
|
+
function stopper(ctx: CommandContext, p: Prepared, events: RunEventStream | undefined, upload: PrReportUpload | undefined) {
|
|
728
|
+
return async (report: PrReport, code: number, settle?: RunEventSettleStatus): Promise<number> => {
|
|
729
|
+
events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
|
|
730
|
+
await finish(ctx, p, report, upload);
|
|
731
|
+
console.error((code === 0 ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
|
|
732
|
+
if (report.message) console.error(report.message);
|
|
733
|
+
printMembers(report);
|
|
734
|
+
return code;
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Why `--require-review` does not count an approval, or why the reviews could not be read. */
|
|
739
|
+
type ReviewStop = { refusal: "author-approved" | "not-a-reviewer"; message: string } | { error: string } | undefined;
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* The `--require-review` check (#3183, #4000) of an approval's approvers,
|
|
743
|
+
* `what` naming the plan they approved. The pull request's reviews and
|
|
744
|
+
* authors are read once, at the first call. Without `--require-review` every
|
|
745
|
+
* approval counts.
|
|
746
|
+
*/
|
|
747
|
+
function reviewCheck(args: CommandContext["args"], p: Prepared, pr: number): (approvedBy: readonly string[], what: string) => Promise<ReviewStop> {
|
|
748
|
+
let read: Promise<{ reviewers: string[]; authors: string[] }> | undefined;
|
|
749
|
+
return async (approvedBy, what) => {
|
|
750
|
+
const forge = p.forge;
|
|
751
|
+
if (!args.requireReview || !forge) return undefined;
|
|
752
|
+
read ??= (async () => ({
|
|
753
|
+
reviewers: (await forge.approvers(pr)).map((login) => forge.principalOf(login)),
|
|
754
|
+
authors: (await forge.authors(pr)).map((login) => forge.principalOf(login)),
|
|
755
|
+
}))();
|
|
756
|
+
let reviewers: string[];
|
|
757
|
+
let authors: string[];
|
|
758
|
+
try {
|
|
759
|
+
({ reviewers, authors } = await read);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
return { error: `could not read the reviews and authors of #${pr}: ${err instanceof Error ? err.message : String(err)}` };
|
|
762
|
+
}
|
|
763
|
+
// #4000: whoever wrote the change never counts toward its gate, through a forge review or a ledger line.
|
|
764
|
+
const writers = approversWhoAuthored(approvedBy, authors);
|
|
765
|
+
if (writers.length > 0) {
|
|
766
|
+
return {
|
|
767
|
+
refusal: "author-approved",
|
|
768
|
+
message:
|
|
769
|
+
`${what} is approved by ${writers.join(", ")}, who wrote #${pr} ` +
|
|
770
|
+
`(its author and the authors and committers of its commits: ${authors.join(", ")}). ` +
|
|
771
|
+
`An approval from someone who wrote the change never counts, so nothing was applied. ` +
|
|
772
|
+
`Have someone else approve the pull request and the plan, then run the apply again.`,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const strangers = approversWithoutReview(approvedBy, reviewers);
|
|
776
|
+
if (strangers.length > 0) {
|
|
777
|
+
return {
|
|
778
|
+
refusal: "not-a-reviewer",
|
|
779
|
+
message:
|
|
780
|
+
`${what} is approved by ${strangers.join(", ")}, who ${strangers.length === 1 ? "has" : "have"} no approving review on #${pr}` +
|
|
781
|
+
` (approving reviews: ${reviewers.join(", ") || "none"}). Nothing was applied. ` +
|
|
782
|
+
`An approval counts when it names the reviewer the way the forge does: ${forge.principalOf("<login>")}.`,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return undefined;
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** What a `waiting` apply prints (#3999): the round's gate and digest to approve, and how to carry on. */
|
|
790
|
+
function waitingMessage(op: string, rounds: PrRound[] | undefined): string {
|
|
791
|
+
const last = rounds?.at(-1);
|
|
792
|
+
if (!last) return "Some members were held back after an upstream applied.";
|
|
793
|
+
return (
|
|
794
|
+
`${last.components.join(", ")} ${last.components.length === 1 ? "was" : "were"} planned again after an upstream applied, ` +
|
|
795
|
+
`and no approval stands for that plan (${last.digest}), so ${last.components.length === 1 ? "it" : "they"} did not apply. ` +
|
|
796
|
+
`Approve it: ${prApproveCommand({ op, gate: last.gate, digest: last.digest })}; then run the apply again with the same --resume record.`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
678
800
|
/** What a component's settle says in the job log. */
|
|
679
801
|
function settleWords(body: RunEventComponentSettle): string {
|
|
680
802
|
switch (body.status) {
|
|
@@ -809,3 +931,373 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
|
|
|
809
931
|
console.error(formatInfo(`resume : ${completed.join(", ")} applied in an earlier attempt; applying ${resumed.plan.order.join(", ") || "nothing more"} under the approval of ${record.digest}`));
|
|
810
932
|
return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed, outputs };
|
|
811
933
|
}
|
|
934
|
+
|
|
935
|
+
// ── A gate per wave on merge (#4003) ─────────────────────────────────────────
|
|
936
|
+
|
|
937
|
+
/** What an earlier wave's job left for this one (#4003): the approved set, the pull request's gate decided against it, and the progress that carries. */
|
|
938
|
+
interface WaveCarry {
|
|
939
|
+
record: PrApplyRecord;
|
|
940
|
+
check: GateCheck;
|
|
941
|
+
approval: PrApproval;
|
|
942
|
+
completed: string[];
|
|
943
|
+
outputs: Record<string, Record<string, unknown>>;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Whether `attempt` carries into this wave's job. It does when it was made
|
|
948
|
+
* by an apply of this pull request's gate, on this commit, for this
|
|
949
|
+
* derivation; when its change set is the one its digest names; and when the
|
|
950
|
+
* pull request's approval still stands for that digest. Of the components it
|
|
951
|
+
* applied, those whose input is the same now carry (#4081). Otherwise it says
|
|
952
|
+
* why on stderr, and the job runs as it would with no record.
|
|
953
|
+
*/
|
|
954
|
+
async function carryWaves(p: Prepared, attempt: FanOutAttempt, file: string, op: string, inputsNow: Record<string, string>): Promise<WaveCarry | undefined> {
|
|
955
|
+
const skip = (why: string): undefined => {
|
|
956
|
+
console.error(formatWarning({
|
|
957
|
+
message: `--resume: "${file}" does not carry: ${why}`,
|
|
958
|
+
hint: "Wave 1 plans every member again and needs the pull request's approval of that plan; a later wave refuses until wave 1 applied.",
|
|
959
|
+
}));
|
|
960
|
+
return undefined;
|
|
961
|
+
};
|
|
962
|
+
const record = attempt.prApply;
|
|
963
|
+
if (!record) return skip("it was not written by pr-apply");
|
|
964
|
+
if (record.op !== op || record.gate !== p.gate) return skip(`it records ${record.op} ${record.gate}, not ${op} ${p.gate}`);
|
|
965
|
+
if (record.head !== p.head) return skip(`it records an apply of ${record.head.slice(0, 12)}, not ${p.head.slice(0, 12)}`);
|
|
966
|
+
if (!samePlanDigest(attempt.digest, p.plan.digest)) return skip("the change selects other components than it did");
|
|
967
|
+
if (record.changeSet.digest !== record.digest || !verifyChangeSetDigest(record.changeSet)) return skip(`its change set is not the one its digest ${record.digest} names`);
|
|
968
|
+
const split = progressThatCarries(attempt, inputsNow);
|
|
969
|
+
if (split.unrecorded && attempt.completed.length > 0) return skip("it does not record what each component applied, since it was written before chant recorded component inputs");
|
|
970
|
+
for (const name of split.changed) console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
|
|
971
|
+
let check: GateCheck;
|
|
972
|
+
try {
|
|
973
|
+
check = await decidePrGate(readOnlyLedger(gitGateLedgerPort()), { op, gate: p.gate, digest: record.digest });
|
|
974
|
+
} catch (err) {
|
|
975
|
+
return skip(`could not read the gate ledger: ${err instanceof Error ? err.message : String(err)}`);
|
|
976
|
+
}
|
|
977
|
+
if (!check.satisfied) return skip(`no approval stands for the plan it applied (${record.digest}) any more`);
|
|
978
|
+
const completed = split.carried;
|
|
979
|
+
const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => completed.includes(name)));
|
|
980
|
+
return { record, check, approval: approvalOf(check), completed, outputs };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** A run event with its wave renumbered to `wave`: the fan-out that applies one wave numbers it 1. */
|
|
984
|
+
function inWave(body: RunEventBody, wave: number): RunEventBody {
|
|
985
|
+
return "wave" in body && typeof (body as { wave?: unknown }).wave === "number" ? ({ ...body, wave } as RunEventBody) : body;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* `pr-apply --wave-gate <gate>` (#4003): apply the pull request's approved
|
|
990
|
+
* set a wave at a time, each wave planned after the one before it applied.
|
|
991
|
+
*
|
|
992
|
+
* The first job plans every member, as pr-apply does, and decides the pull
|
|
993
|
+
* request's gate against that digest. Approved, wave 1 applies with those
|
|
994
|
+
* plans. Each later wave is planned again against the outputs the waves
|
|
995
|
+
* before it left. When the approved change set covers its plan
|
|
996
|
+
* (`planCoveredBy`, as on a resume) it applies under the pull request's
|
|
997
|
+
* approval; when it does not, it waits at `<gate>-wave-<k>` under the pull
|
|
998
|
+
* request's op for an approval of its new plan, and exits 3.
|
|
999
|
+
*
|
|
1000
|
+
* With `--wave <k>` the job runs wave k and no other, reading the approved
|
|
1001
|
+
* set and the progress from the `--resume` record the job before it left.
|
|
1002
|
+
* Without it the waves run in order until one stops.
|
|
1003
|
+
*/
|
|
1004
|
+
async function prApplyWaves(
|
|
1005
|
+
ctx: CommandContext,
|
|
1006
|
+
p: Prepared,
|
|
1007
|
+
at: { pr: number | null; op: string; release: ReturnType<typeof autoReleaseRun>; upload?: PrReportUpload; events?: RunEventStream },
|
|
1008
|
+
): Promise<number> {
|
|
1009
|
+
const { args } = ctx;
|
|
1010
|
+
const { pr, op, events, upload } = at;
|
|
1011
|
+
const waveGate = args.waveGate!;
|
|
1012
|
+
const only = args.wave;
|
|
1013
|
+
const total = p.plan.waves.length;
|
|
1014
|
+
const stop = stopper(ctx, p, events, upload);
|
|
1015
|
+
const log = applyLog();
|
|
1016
|
+
log({ type: "run-start", waves: p.plan.waves });
|
|
1017
|
+
const reportBase = { stage: "apply" as const, pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, waveGate };
|
|
1018
|
+
|
|
1019
|
+
if (p.plan.order.length === 0 || pr === null) {
|
|
1020
|
+
const set = await planAll(p, events?.emit);
|
|
1021
|
+
if (p.plan.order.length === 0) return stop(prReport({ ...reportBase, set, approval: { status: "pending" }, status: "nothing" }), 0);
|
|
1022
|
+
return stop(
|
|
1023
|
+
prReport({
|
|
1024
|
+
...reportBase,
|
|
1025
|
+
set,
|
|
1026
|
+
approval: { status: "pending" },
|
|
1027
|
+
status: "refused",
|
|
1028
|
+
refusal: "no-pull-request",
|
|
1029
|
+
message: `No pull request merged ${p.head}, so no approval answers for this change. Nothing was applied. Pass --pr <n>, or land the change through a pull request.`,
|
|
1030
|
+
}),
|
|
1031
|
+
GATED_EXIT_CODE,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const resumePath = args.resume ? resolve(args.resume) : undefined;
|
|
1036
|
+
let attempt: FanOutAttempt | undefined;
|
|
1037
|
+
if (resumePath) {
|
|
1038
|
+
try {
|
|
1039
|
+
attempt = readFanOutAttempt(resumePath);
|
|
1040
|
+
} catch (err) {
|
|
1041
|
+
return fail(`--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const inputsNow = resumePath ? componentInputDigests(p.components, { projectRoot: resolve("."), env: p.env }) : {};
|
|
1045
|
+
const carry = attempt ? await carryWaves(p, attempt, args.resume!, op, inputsNow) : undefined;
|
|
1046
|
+
|
|
1047
|
+
const completed = new Set(carry?.completed ?? []);
|
|
1048
|
+
const outputsSoFar: Record<string, Record<string, unknown>> = { ...(carry?.outputs ?? {}) };
|
|
1049
|
+
const inputsSoFar: Record<string, string> = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]!]));
|
|
1050
|
+
const failedSoFar = new Set<string>();
|
|
1051
|
+
let waves: PrWave[] = carry?.record.waves ?? [];
|
|
1052
|
+
const after = new Map<string, PrMember>();
|
|
1053
|
+
const leftIn = (k: number): string[] => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
|
|
1054
|
+
const firstLeft = (): number => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
|
|
1055
|
+
|
|
1056
|
+
// The approved set: carried from the record, or planned here and decided against the pull request's gate.
|
|
1057
|
+
let approved: PrPlanSet | undefined = carry
|
|
1058
|
+
? { doc: carry.record.changeSet, members: carry.record.members, plans: {}, outputs: carry.record.planOutputs, failed: false }
|
|
1059
|
+
: undefined;
|
|
1060
|
+
let approval: PrApproval = carry?.approval ?? { status: "pending" };
|
|
1061
|
+
const report = (input: { status: PrReport["status"]; refusal?: PrReport["refusal"]; message?: string }) => {
|
|
1062
|
+
const set = approved ?? { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
|
|
1063
|
+
const members = set.members.map((m): PrMember => {
|
|
1064
|
+
const now = after.get(m.member);
|
|
1065
|
+
if (now) return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
|
|
1066
|
+
return completed.has(m.component) ? { ...m, status: "applied" } : m;
|
|
1067
|
+
});
|
|
1068
|
+
return prReport({ ...reportBase, set, approval, members, waves, ...input });
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
|
|
1072
|
+
const left = firstLeft();
|
|
1073
|
+
if (only !== undefined && left > 0 && left < only) {
|
|
1074
|
+
return stop(
|
|
1075
|
+
report({
|
|
1076
|
+
status: "refused",
|
|
1077
|
+
refusal: "earlier-wave",
|
|
1078
|
+
message:
|
|
1079
|
+
`Wave ${only} cannot run yet: wave ${left} has not applied (${leftIn(left).join(", ")})${carry ? "" : ", and no --resume record of this apply says otherwise"}. ` +
|
|
1080
|
+
`Nothing was applied. Run wave ${left}'s job first, with the same --resume record.`,
|
|
1081
|
+
}),
|
|
1082
|
+
1,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
let planned: PrPlanSet | undefined;
|
|
1087
|
+
if (carry) {
|
|
1088
|
+
// What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
|
|
1089
|
+
p.plan.waves.forEach((names, i) => {
|
|
1090
|
+
for (const component of names.filter((name) => completed.has(name))) {
|
|
1091
|
+
const body: RunEventComponentSettle = { type: "component-settle", wave: i + 1, component, status: "already-applied" };
|
|
1092
|
+
events?.emit(body);
|
|
1093
|
+
log(body);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
emitResumedGate(events, op, p.gate, carry.check);
|
|
1097
|
+
} else {
|
|
1098
|
+
const set = await planAll(p, events?.emit);
|
|
1099
|
+
approved = set;
|
|
1100
|
+
if (set.failed) {
|
|
1101
|
+
return stop(report({ status: "plan-failed", message: "A member failed to plan, so there is no plan to compare with the approved one. Nothing was applied." }), 1);
|
|
1102
|
+
}
|
|
1103
|
+
await storeChangeSetForGate(ctx, p, set.doc);
|
|
1104
|
+
const check = await emitGateStep(
|
|
1105
|
+
events?.emit,
|
|
1106
|
+
{ op, gate: p.gate },
|
|
1107
|
+
() => decidePrGate(gitGateLedgerPort(), { op, gate: p.gate, digest: set.doc.digest, description: `pull request #${pr}: ${p.plan.order.join(", ")}` }),
|
|
1108
|
+
(unapproved) => {
|
|
1109
|
+
const a = approvalOf(unapproved);
|
|
1110
|
+
return a.status === "changed"
|
|
1111
|
+
? describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval: a })
|
|
1112
|
+
: `No approval stands for this plan. Approve it: ${prApproveCommand({ op, gate: p.gate, digest: set.doc.digest })}`;
|
|
1113
|
+
},
|
|
1114
|
+
);
|
|
1115
|
+
approval = approvalOf(check);
|
|
1116
|
+
if (!check.satisfied) {
|
|
1117
|
+
return stop(
|
|
1118
|
+
approval.status === "changed"
|
|
1119
|
+
? report({ status: "refused", refusal: "plan-changed", message: describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval }) })
|
|
1120
|
+
: report({
|
|
1121
|
+
status: "refused",
|
|
1122
|
+
refusal: "not-approved",
|
|
1123
|
+
message: `No approval stands for this plan, so nothing was applied. Approve it: ${prApproveCommand({ op, gate: p.gate, digest: set.doc.digest })}; then run the apply again.`,
|
|
1124
|
+
}),
|
|
1125
|
+
GATED_EXIT_CODE,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
planned = set;
|
|
1129
|
+
}
|
|
1130
|
+
const approvedSet = approved!;
|
|
1131
|
+
|
|
1132
|
+
const review = reviewCheck(args, p, pr);
|
|
1133
|
+
const prReview = await review(approval.approvedBy ?? [], "The plan");
|
|
1134
|
+
if (prReview && "error" in prReview) return fail(prReview.error);
|
|
1135
|
+
if (prReview) {
|
|
1136
|
+
emitReviewStop(events, prReview.message);
|
|
1137
|
+
return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const recordAttempt = (): void => {
|
|
1141
|
+
if (!resumePath) return;
|
|
1142
|
+
writeFanOutAttempt(resumePath, {
|
|
1143
|
+
digest: p.plan.digest,
|
|
1144
|
+
completed: [...completed].sort(),
|
|
1145
|
+
failed: [...failedSoFar].sort(),
|
|
1146
|
+
outputs: outputsSoFar,
|
|
1147
|
+
inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
|
|
1148
|
+
prApply: {
|
|
1149
|
+
op,
|
|
1150
|
+
gate: p.gate,
|
|
1151
|
+
head: p.head,
|
|
1152
|
+
digest: approvedSet.doc.digest,
|
|
1153
|
+
approvedBy: approval.approvedBy ?? [],
|
|
1154
|
+
members: approvedSet.members,
|
|
1155
|
+
changeSet: approvedSet.doc,
|
|
1156
|
+
planOutputs: approvedSet.outputs,
|
|
1157
|
+
...(waves.length > 0 ? { waves } : {}),
|
|
1158
|
+
},
|
|
1159
|
+
});
|
|
1160
|
+
};
|
|
1161
|
+
const setWave = (wave: PrWave): void => {
|
|
1162
|
+
waves = [...waves.filter((w) => w.wave !== wave.wave), wave].sort((a, b) => a.wave - b.wave);
|
|
1163
|
+
recordAttempt();
|
|
1164
|
+
};
|
|
1165
|
+
recordAttempt();
|
|
1166
|
+
|
|
1167
|
+
const from = only ?? (left || total + 1);
|
|
1168
|
+
const to = only ?? total;
|
|
1169
|
+
for (let k = from; k <= to; k++) {
|
|
1170
|
+
const wavePlan = prWavePlan(p.plan, p.components, k, [...completed]);
|
|
1171
|
+
if (wavePlan.order.length === 0) continue;
|
|
1172
|
+
const inThisWave = new Set(wavePlan.order);
|
|
1173
|
+
|
|
1174
|
+
// Wave 1 of the apply that just planned every member applies those plans:
|
|
1175
|
+
// they are the approved set's. Every other wave is planned again, against
|
|
1176
|
+
// the outputs the waves before it left.
|
|
1177
|
+
let set: PrPlanSet;
|
|
1178
|
+
let entry: PrWave;
|
|
1179
|
+
if (planned && k === 1) {
|
|
1180
|
+
set = {
|
|
1181
|
+
...planned,
|
|
1182
|
+
members: planned.members.filter((m) => inThisWave.has(m.component)),
|
|
1183
|
+
plans: Object.fromEntries(Object.entries(planned.plans).filter(([name]) => inThisWave.has(name))),
|
|
1184
|
+
};
|
|
1185
|
+
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
|
|
1186
|
+
} else {
|
|
1187
|
+
const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
|
|
1188
|
+
const fresh = await planPrSet({
|
|
1189
|
+
components: p.components,
|
|
1190
|
+
plan: wavePlan,
|
|
1191
|
+
registry: p.registry,
|
|
1192
|
+
env: p.env,
|
|
1193
|
+
seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
|
|
1194
|
+
...(events ? { onRunEvent: (body: RunEventBody) => events.emit(body) } : {}),
|
|
1195
|
+
});
|
|
1196
|
+
if (fresh.failed) {
|
|
1197
|
+
for (const m of fresh.members) after.set(m.member, m);
|
|
1198
|
+
setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
|
|
1199
|
+
return stop(report({ status: "plan-failed", message: `A member of wave ${k} failed to plan, so nothing in wave ${k} was applied.` }), 1);
|
|
1200
|
+
}
|
|
1201
|
+
const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
|
|
1202
|
+
set = fresh;
|
|
1203
|
+
if (moved.length === 0) {
|
|
1204
|
+
console.error(formatInfo(`wave ${k}: the approved plan covers what it plans now, so it applies under the pull request's approval`));
|
|
1205
|
+
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
|
|
1206
|
+
} else {
|
|
1207
|
+
const gate = waveGateName(waveGate, k);
|
|
1208
|
+
const digest = fresh.doc.digest;
|
|
1209
|
+
console.error(formatInfo(`wave ${k}: its plan moved since the pull request's (${moved.join("; ")}), so it needs an approval of ${gate}`));
|
|
1210
|
+
await storeChangeSetForGate(ctx, p, fresh.doc);
|
|
1211
|
+
writeWaveChangeSet(ctx, k, fresh.doc, upload);
|
|
1212
|
+
const approveLine = prApproveCommand({ op, gate, digest });
|
|
1213
|
+
const check = await emitGateStep(
|
|
1214
|
+
events?.emit,
|
|
1215
|
+
{ op, gate },
|
|
1216
|
+
() => decidePrGate(gitGateLedgerPort(), { op, gate, digest, description: `pull request #${pr}, wave ${k} of ${total}: ${wavePlan.order.join(", ")}` }),
|
|
1217
|
+
(unapproved) =>
|
|
1218
|
+
unapproved.mismatch
|
|
1219
|
+
? `wave ${k}'s plan changed after its approval. Approve the plan now: ${approveLine}`
|
|
1220
|
+
: `wave ${k}'s plan is not the one the pull request approved. Approve it: ${approveLine}`,
|
|
1221
|
+
);
|
|
1222
|
+
const waveApproval = approvalOf(check);
|
|
1223
|
+
const waiting: PrWave = {
|
|
1224
|
+
wave: k,
|
|
1225
|
+
components: wavePlan.order,
|
|
1226
|
+
status: "waiting",
|
|
1227
|
+
approval: "wave",
|
|
1228
|
+
gate,
|
|
1229
|
+
digest,
|
|
1230
|
+
moved,
|
|
1231
|
+
...(waveApproval.approved ? { approved: waveApproval.approved } : {}),
|
|
1232
|
+
};
|
|
1233
|
+
if (!check.satisfied) {
|
|
1234
|
+
setWave(waiting);
|
|
1235
|
+
const message =
|
|
1236
|
+
waveApproval.status === "changed"
|
|
1237
|
+
? `Wave ${k}'s plan changed after its approval, so nothing in it was applied. approved: ${waveApproval.approved ?? "(none)"}; planned now: ${digest}. ` +
|
|
1238
|
+
`Read the new plan, and if it is right, approve it: ${approveLine}; then run the wave's apply again.`
|
|
1239
|
+
: `Wave ${k} was planned after the waves before it applied, and the pull request's approved plan does not cover it: ${moved.join("; ")}. ` +
|
|
1240
|
+
`Nothing in wave ${k} was applied. Read its plan, and if it is right, approve it: ${approveLine}; then run the wave's apply again.`;
|
|
1241
|
+
return stop(report({ status: "refused", refusal: waveApproval.status === "changed" ? "wave-plan-changed" : "wave-not-approved", message }), GATED_EXIT_CODE);
|
|
1242
|
+
}
|
|
1243
|
+
const waveReview = await review(waveApproval.approvedBy ?? [], `Wave ${k}'s plan`);
|
|
1244
|
+
if (waveReview && "error" in waveReview) return fail(waveReview.error);
|
|
1245
|
+
if (waveReview) {
|
|
1246
|
+
setWave(waiting);
|
|
1247
|
+
emitReviewStop(events, waveReview.message);
|
|
1248
|
+
return stop(report({ status: "refused", refusal: waveReview.refusal, message: waveReview.message }), GATED_EXIT_CODE);
|
|
1249
|
+
}
|
|
1250
|
+
entry = { wave: k, components: wavePlan.order, status: "applied", approval: "wave", gate, digest, moved, ...(waveApproval.approvedBy ? { approvedBy: waveApproval.approvedBy } : {}) };
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const applied = await applyPrSet({
|
|
1255
|
+
components: p.components,
|
|
1256
|
+
plan: wavePlan,
|
|
1257
|
+
registry: p.registry,
|
|
1258
|
+
env: p.env,
|
|
1259
|
+
set,
|
|
1260
|
+
// One wave's fan-out numbers its wave 1; the events and the log carry the derivation's number.
|
|
1261
|
+
onRunEvent: (body: RunEventBody) => {
|
|
1262
|
+
const numbered = inWave(body, k);
|
|
1263
|
+
events?.nested(numbered);
|
|
1264
|
+
if (numbered.type !== "run-start") log(numbered);
|
|
1265
|
+
},
|
|
1266
|
+
onComponentSettled: (settled, outputs) => {
|
|
1267
|
+
if (settled.status === "ok") {
|
|
1268
|
+
completed.add(settled.component);
|
|
1269
|
+
if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
|
|
1270
|
+
if (outputs) outputsSoFar[settled.component] = outputs;
|
|
1271
|
+
} else if (settled.status === "fail") {
|
|
1272
|
+
failedSoFar.add(settled.component);
|
|
1273
|
+
}
|
|
1274
|
+
recordAttempt();
|
|
1275
|
+
},
|
|
1276
|
+
});
|
|
1277
|
+
for (const m of applied.members) after.set(m.member, m);
|
|
1278
|
+
for (const name of applied.run.completed) {
|
|
1279
|
+
completed.add(name);
|
|
1280
|
+
const outputs = applied.run.componentOutputs[name];
|
|
1281
|
+
if (outputs) outputsSoFar[name] = outputs;
|
|
1282
|
+
}
|
|
1283
|
+
setWave({ ...entry, status: applied.status === "applied" ? "applied" : "failed" });
|
|
1284
|
+
await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin);
|
|
1285
|
+
if (applied.status !== "applied") {
|
|
1286
|
+
return stop(report({ status: "failed" }), failedRunExitCode(applied.run.results), applied.run.status === "gated" ? "gated" : undefined);
|
|
1287
|
+
}
|
|
1288
|
+
console.error(formatSuccess(`applied wave ${k} of ${total} under ${entry.approval === "pull-request" ? `the pull request's approval of ${approvedSet.doc.digest}` : `${entry.gate}, approved for ${entry.digest}`} by ${(entry.approvedBy ?? []).join(", ")}`));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const done = p.plan.order.every((name) => completed.has(name));
|
|
1292
|
+
return stop(report({ status: done ? "applied" : "wave-applied" }), 0);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/** A wave's new plan (#4003), beside the report as `change-set-wave-<k>.json`, for whoever approves its gate. */
|
|
1296
|
+
function writeWaveChangeSet(ctx: CommandContext, wave: number, doc: PrPlanSet["doc"], upload?: PrReportUpload): void {
|
|
1297
|
+
const dir = resolve(ctx.args.output ?? PR_REPORT_DIR);
|
|
1298
|
+
mkdirSync(dir, { recursive: true });
|
|
1299
|
+
const name = `change-set-wave-${wave}.json`;
|
|
1300
|
+
const body = JSON.stringify(doc, null, 2) + "\n";
|
|
1301
|
+
writeFileSync(join(dir, name), body);
|
|
1302
|
+
upload?.file(name, body);
|
|
1303
|
+
}
|