@intentius/chant 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +44 -16
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +15 -0
- package/dist/cli/registry.d.ts +13 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +10 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +39 -0
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +5 -5
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +12 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.js +6 -4
- package/dist/workspace/ci-run-index.schema.json +5 -3
- package/dist/workspace/ci-run.d.ts +75 -2
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +195 -53
- package/dist/workspace/ci-runner-key.d.ts +16 -3
- package/dist/workspace/ci-runner-key.d.ts.map +1 -1
- package/dist/workspace/ci-runner-key.js +30 -10
- package/dist/workspace/ci-serve.d.ts +54 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +175 -26
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +2 -2
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +18 -0
- package/src/cli/main.ts +43 -16
- package/src/cli/parse-args.ts +12 -0
- package/src/cli/registry.ts +19 -6
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +42 -0
- package/src/pr-forge.ts +40 -0
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +5 -5
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +5 -3
- package/src/workspace/ci-run-index.test.ts +42 -3
- package/src/workspace/ci-run-index.ts +18 -4
- package/src/workspace/ci-run.test.ts +185 -7
- package/src/workspace/ci-run.ts +224 -54
- package/src/workspace/ci-runner-key.test.ts +55 -17
- package/src/workspace/ci-runner-key.ts +36 -11
- package/src/workspace/ci-serve.test.ts +150 -6
- package/src/workspace/ci-serve.ts +169 -25
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.ts +2 -2
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
package/src/components/pr-run.ts
CHANGED
|
@@ -8,26 +8,42 @@
|
|
|
8
8
|
* everything before applying anything is what makes the two digests
|
|
9
9
|
* comparable: each member is planned against the same state both times.
|
|
10
10
|
*
|
|
11
|
-
* ##
|
|
11
|
+
* ## What a dependent is planned against
|
|
12
12
|
*
|
|
13
13
|
* A member that reads another one's output through `stackOutput()` is
|
|
14
|
-
* planned with
|
|
15
|
-
* `outputs` (`readComponentOutputs`). When the change moves that output, the
|
|
16
|
-
* dependent's approved plan still carries the old value; the apply applies
|
|
17
|
-
* it as approved and marks the member `inputsMoved`, and the next run plans
|
|
18
|
-
* it against the new value. A change that must carry a moved output through
|
|
19
|
-
* its dependents in one rollout is what gated waves are for (#3049): each
|
|
20
|
-
* wave is planned after the one before it applied.
|
|
14
|
+
* planned with one of three values (#3999):
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
16
|
+
* - the upstream is in the change too: the value its plan says it will
|
|
17
|
+
* expose (`CapabilityPlan.outputs`). The report lists these under
|
|
18
|
+
* `plannedOutputs`.
|
|
19
|
+
* - the upstream is not in the change (#4104): the value it has now, read
|
|
20
|
+
* through its capability's `outputs` (`readComponentOutputs`). The report
|
|
21
|
+
* lists these under `deployedOutputs`. A change to one cluster is planned
|
|
22
|
+
* against the network it sits on, which is why `components` is every
|
|
23
|
+
* component in the project and not the selection.
|
|
24
|
+
* - the upstream is in the change and its plan cannot say, or says the value
|
|
25
|
+
* is known only after apply: no value. The dependent's members list the
|
|
26
|
+
* reference under `unknownInputs`, and the approval does not cover what
|
|
27
|
+
* it will do with it.
|
|
28
|
+
*
|
|
29
|
+
* `--seed-outputs` wins over both. The apply stage's plan reads the same
|
|
30
|
+
* values, so on the same estate it gets the digest the plan stage showed.
|
|
31
|
+
* A resume plans against the outputs the approved plans read, from the
|
|
32
|
+
* record (`planOutputs`), and reads nothing those cover.
|
|
33
|
+
*
|
|
34
|
+
* ## Planning again once an upstream applied
|
|
35
|
+
*
|
|
36
|
+
* The apply runs each member's approved plan, except a member whose plan
|
|
37
|
+
* read an upstream output that is not the value standing when its turn
|
|
38
|
+
* comes (`movedWiringReferences`): the upstream applied as another value
|
|
39
|
+
* than its plan said, or the value was unknown. That member, and what is
|
|
40
|
+
* downstream of it, is held back. Once the rest settled, the held members
|
|
41
|
+
* are planned again against the outputs the apply left, and that set's
|
|
42
|
+
* digest is decided at its own gate, `<gate>-round-<n>` (`prRoundGate`), as
|
|
43
|
+
* a gated wave decides each wave (#3049). Approved, it applies, and a member
|
|
44
|
+
* that is held again goes to the next round. Not approved, the apply stops
|
|
45
|
+
* `waiting` with the new digest named, and the same apply with its
|
|
46
|
+
* `--resume` record carries on from there.
|
|
31
47
|
*
|
|
32
48
|
* ## Finishing an apply that failed partway
|
|
33
49
|
*
|
|
@@ -41,29 +57,48 @@
|
|
|
41
57
|
* the rest applies. Planning everything again would give the applied members
|
|
42
58
|
* empty plans and the set a new digest, and so ask for a second approval of
|
|
43
59
|
* work already approved.
|
|
60
|
+
*
|
|
61
|
+
* ## A gate per wave on merge
|
|
62
|
+
*
|
|
63
|
+
* `pr-apply --wave-gate` (#4003) applies the approved set a wave at a time.
|
|
64
|
+
* {@link prWavePlan} narrows the derivation to one wave, which is planned
|
|
65
|
+
* against the outputs the waves before it left, and {@link waveNotCovered}
|
|
66
|
+
* says whether the approved change set covers what it planned, by the same
|
|
67
|
+
* test a resume uses ({@link planCoveredBy}). Covered, the wave applies under
|
|
68
|
+
* the pull request's approval; not covered, it needs its own.
|
|
44
69
|
*/
|
|
45
70
|
|
|
46
71
|
import { composeChangeSet, verifyChangeSetDigest, type ChangeSetDocument, type ChangeSetEntry, type ChangeSetPart } from "../change-set";
|
|
47
72
|
import { evaluateGate, type GateCheck, type GateLedgerPort } from "../op/gate";
|
|
48
73
|
import {
|
|
49
74
|
memberCounts,
|
|
75
|
+
PR_APPLY_GATE,
|
|
76
|
+
prRoundGate,
|
|
50
77
|
PR_REPORT_CONTRACT,
|
|
51
78
|
PR_REPORT_SCHEMA_ID,
|
|
52
79
|
type PrApproval,
|
|
53
80
|
type PrMember,
|
|
54
81
|
type PrRefusal,
|
|
82
|
+
type PrRound,
|
|
55
83
|
type PrReport,
|
|
56
84
|
type PrStatus,
|
|
85
|
+
type PrWave,
|
|
57
86
|
} from "../pr-loop";
|
|
58
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
deployContext,
|
|
89
|
+
movedWiringReferences,
|
|
90
|
+
unresolvedWiringReferences,
|
|
91
|
+
type DriverComponent,
|
|
92
|
+
type DriverComponentResult,
|
|
93
|
+
} from "./driver";
|
|
59
94
|
import { deployUnits } from "./deploy-units";
|
|
60
95
|
import { runFanOut, type FanOutRunResult } from "./fan-out-run";
|
|
61
96
|
import { remainingFanOut, type ComponentChangeSignal, type FanOutPlan } from "./fan-out";
|
|
62
97
|
import type { PrApplyRecord } from "./fan-out-record";
|
|
63
|
-
import type { CapabilityRegistry } from "./capability";
|
|
98
|
+
import type { CapabilityRegistry, PlannedOutputs } from "./capability";
|
|
64
99
|
import type { RunProgressEvent } from "./run-progress";
|
|
65
100
|
import type { RunEventSink } from "../run-event-stream";
|
|
66
|
-
import { planWaveComponent, readDeployedOutputs } from "./wave-plan";
|
|
101
|
+
import { capabilitySteps, planWaveComponent, readDeployedOutputs } from "./wave-plan";
|
|
67
102
|
|
|
68
103
|
export interface PrPlanSetOptions {
|
|
69
104
|
/** Every component in the project, not the selection: a planned member's dependency outside it is read too (#4104). */
|
|
@@ -101,6 +136,12 @@ export interface PrPlanSet {
|
|
|
101
136
|
* nothing was read. The report's `deployedOutputs`.
|
|
102
137
|
*/
|
|
103
138
|
deployedOutputs?: Record<string, string[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Per dependency in the change that a member read (#3999), the names of
|
|
141
|
+
* the outputs its plan knows, sorted. Absent when there is none. The
|
|
142
|
+
* report's `plannedOutputs`.
|
|
143
|
+
*/
|
|
144
|
+
plannedOutputs?: Record<string, string[]>;
|
|
104
145
|
/** Whether any component failed to plan. */
|
|
105
146
|
failed: boolean;
|
|
106
147
|
}
|
|
@@ -115,36 +156,51 @@ function failedParts(component: DriverComponent, error: string): ChangeSetPart[]
|
|
|
115
156
|
}
|
|
116
157
|
|
|
117
158
|
/**
|
|
118
|
-
* Plan every component in `plan.order`, a wave at a time, each against
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
159
|
+
* Plan every component in `plan.order`, a wave at a time, each against its
|
|
160
|
+
* dependencies' planned outputs when the change plans them too, and against
|
|
161
|
+
* the outputs they have now otherwise. Never throws for a component that
|
|
162
|
+
* fails to plan: it becomes a failed member, and `failed` is set. Throws
|
|
163
|
+
* when a planned member's dependency is not among `components`, which means
|
|
164
|
+
* the caller passed the selection where the whole graph belongs.
|
|
123
165
|
*/
|
|
124
166
|
export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
|
|
125
167
|
const byName = new Map(options.components.map((c) => [c.name, c]));
|
|
126
168
|
const outputs: Record<string, Record<string, unknown>> = { ...(options.seededOutputs ?? {}) };
|
|
127
169
|
const read = new Set(Object.keys(outputs));
|
|
170
|
+
const inChange = new Set(options.plan.order);
|
|
171
|
+
/** What each component of the change planned to expose, once its wave planned. */
|
|
172
|
+
const plannedBy = new Map<string, PlannedOutputs | undefined>();
|
|
128
173
|
const deployed: Record<string, string[]> = {};
|
|
174
|
+
const planned: Record<string, string[]> = {};
|
|
129
175
|
const parts: ChangeSetPart[] = [];
|
|
130
176
|
const componentOf = new Map<string, string>();
|
|
177
|
+
const unknownOf = new Map<string, { unknown: string[]; build: string[] }>();
|
|
131
178
|
const plans: Record<string, Record<string, unknown>> = {};
|
|
132
179
|
let failed = false;
|
|
133
180
|
const emit = options.onRunEvent;
|
|
134
181
|
emit?.({ type: "phase-start", component: null, phase: PR_PLAN_PHASE });
|
|
135
182
|
|
|
136
183
|
for (const wave of options.plan.waves) {
|
|
137
|
-
// The dependencies' outputs first, each
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
184
|
+
// The dependencies' outputs first, each once. One the change plans is
|
|
185
|
+
// read from its plan, which an earlier wave made (#3999). Any other is
|
|
186
|
+
// read from the deployed estate (#4104). A dependency that cannot say
|
|
187
|
+
// leaves its references unresolved, which its dependent's plan reports
|
|
188
|
+
// if it needs them.
|
|
141
189
|
const deps = [...new Set(wave.flatMap((name) => byName.get(name)?.dependsOn ?? []))].filter((dep) => !read.has(dep));
|
|
190
|
+
const fromEstate: string[] = [];
|
|
142
191
|
for (const dep of deps) {
|
|
143
192
|
read.add(dep);
|
|
144
193
|
if (!byName.has(dep)) throw new Error(`planPrSet: ${dep}, a dependency of a planned member, is not among the components; pass every component in the project`);
|
|
194
|
+
if (!inChange.has(dep)) {
|
|
195
|
+
fromEstate.push(dep);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const known = plannedBy.get(dep)?.known ?? {};
|
|
199
|
+
outputs[dep] = { ...known };
|
|
200
|
+
planned[dep] = Object.keys(known).sort();
|
|
145
201
|
}
|
|
146
|
-
Object.assign(deployed, await readDeployedOutputs(
|
|
147
|
-
const
|
|
202
|
+
Object.assign(deployed, await readDeployedOutputs(fromEstate, byName, options.registry, (dep) => deployContext(options, dep), outputs));
|
|
203
|
+
const results = await Promise.all(
|
|
148
204
|
wave.map(async (name) => {
|
|
149
205
|
const at = { component: name, phase: PR_PLAN_PHASE, step: "plan" };
|
|
150
206
|
emit?.({ type: "step-start", ...at });
|
|
@@ -160,16 +216,20 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
|
|
|
160
216
|
return p;
|
|
161
217
|
}),
|
|
162
218
|
);
|
|
163
|
-
for (const p of
|
|
219
|
+
for (const p of results) {
|
|
164
220
|
const component = byName.get(p.component)!;
|
|
221
|
+
const unknown = unknownInputsOf(component, outputs, inChange);
|
|
222
|
+
if (unknown.unknown.length > 0 || unknown.build.length > 0) unknownOf.set(p.component, unknown);
|
|
165
223
|
if ("error" in p) {
|
|
166
224
|
failed = true;
|
|
225
|
+
plannedBy.set(p.component, undefined);
|
|
167
226
|
for (const part of failedParts(component, p.error)) {
|
|
168
227
|
parts.push(part);
|
|
169
228
|
componentOf.set(part.member.member, p.component);
|
|
170
229
|
}
|
|
171
230
|
continue;
|
|
172
231
|
}
|
|
232
|
+
plannedBy.set(p.component, p.outputs);
|
|
173
233
|
plans[p.component] = p.plans;
|
|
174
234
|
for (const part of p.parts) {
|
|
175
235
|
parts.push(part);
|
|
@@ -180,16 +240,78 @@ export async function planPrSet(options: PrPlanSetOptions): Promise<PrPlanSet> {
|
|
|
180
240
|
|
|
181
241
|
emit?.({ type: "phase-settle", component: null, phase: PR_PLAN_PHASE, status: failed ? "failed" : "ok" });
|
|
182
242
|
const doc = composeChangeSet(parts);
|
|
183
|
-
const members: PrMember[] = doc.members.map((m) =>
|
|
184
|
-
member
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
243
|
+
const members: PrMember[] = doc.members.map((m) => {
|
|
244
|
+
const component = componentOf.get(m.member) ?? m.member;
|
|
245
|
+
const unknown = unknownOf.get(component);
|
|
246
|
+
return {
|
|
247
|
+
member: m.member,
|
|
248
|
+
component,
|
|
249
|
+
planDigest: m.planDigest,
|
|
250
|
+
status: m.status === "failed" ? "plan-failed" : "planned",
|
|
251
|
+
counts: memberCounts(doc, m.member),
|
|
252
|
+
...(m.error ? { error: m.error } : {}),
|
|
253
|
+
...(unknown?.unknown.length ? { unknownInputs: unknown.unknown } : {}),
|
|
254
|
+
...(unknown?.build.length ? { buildInputs: unknown.build } : {}),
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
const sorted = (r: Record<string, string[]>) => Object.fromEntries(Object.entries(r).sort(([a], [b]) => a.localeCompare(b)));
|
|
258
|
+
const deployedOutputs = sorted(deployed);
|
|
259
|
+
const plannedOutputs = sorted(planned);
|
|
260
|
+
return {
|
|
261
|
+
doc,
|
|
262
|
+
members,
|
|
263
|
+
plans,
|
|
264
|
+
outputs,
|
|
265
|
+
failed,
|
|
266
|
+
...(Object.keys(deployedOutputs).length > 0 ? { deployedOutputs } : {}),
|
|
267
|
+
...(Object.keys(plannedOutputs).length > 0 ? { plannedOutputs } : {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The references `component`'s steps make to an output of a component in
|
|
273
|
+
* the change that `outputs` holds no value for (#3999), known only after
|
|
274
|
+
* that upstream applies, each sorted. `build` are the published artifact
|
|
275
|
+
* identities a build in the change produces (`WiringReference.publish`):
|
|
276
|
+
* the build's provenance binds those, so they are never approved again.
|
|
277
|
+
* `unknown` are the rest.
|
|
278
|
+
*/
|
|
279
|
+
function unknownInputsOf(
|
|
280
|
+
component: DriverComponent,
|
|
281
|
+
outputs: Record<string, Record<string, unknown>>,
|
|
282
|
+
inChange: ReadonlySet<string>,
|
|
283
|
+
): { unknown: string[]; build: string[] } {
|
|
284
|
+
const unknown = new Set<string>();
|
|
285
|
+
const build = new Set<string>();
|
|
286
|
+
for (const step of capabilitySteps(component.deploy)) {
|
|
287
|
+
const { kind: _kind, ...rest } = step;
|
|
288
|
+
for (const r of unresolvedWiringReferences(rest, outputs)) {
|
|
289
|
+
if (inChange.has(r.source)) (r.publish ? build : unknown).add(r.ref);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { unknown: [...unknown].sort(), build: [...build].sort() };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Why `component` cannot apply the plan that read `read` now that `now`
|
|
297
|
+
* stands (#3999): each reference its steps make that resolves to another
|
|
298
|
+
* value, said once. `undefined` when every one resolves as it did. A
|
|
299
|
+
* published artifact's identity from a build in `inChange` never holds a
|
|
300
|
+
* member back: the build's provenance binds it, not this approval.
|
|
301
|
+
*/
|
|
302
|
+
export function heldReason(
|
|
303
|
+
component: DriverComponent,
|
|
304
|
+
read: Record<string, Record<string, unknown>>,
|
|
305
|
+
now: Record<string, Record<string, unknown>>,
|
|
306
|
+
inChange: ReadonlySet<string> = new Set(),
|
|
307
|
+
): string | undefined {
|
|
308
|
+
const refs = new Set<string>();
|
|
309
|
+
for (const step of capabilitySteps(component.deploy)) {
|
|
310
|
+
const { kind: _kind, ...rest } = step;
|
|
311
|
+
for (const r of movedWiringReferences(rest, read, now)) if (!(r.publish && inChange.has(r.source))) refs.add(r.ref);
|
|
312
|
+
}
|
|
313
|
+
if (refs.size === 0) return undefined;
|
|
314
|
+
return `its plan read ${[...refs].sort().join(", ")} before ${refs.size === 1 ? "that output" : "those outputs"} applied as another value`;
|
|
193
315
|
}
|
|
194
316
|
|
|
195
317
|
/**
|
|
@@ -242,6 +364,18 @@ export function approversWithoutReview(approvedBy: readonly string[], reviewers:
|
|
|
242
364
|
return approvedBy.filter((by) => !allowed.has(norm(by)));
|
|
243
365
|
}
|
|
244
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Who approved the gate but wrote the change (#4000): each approval's
|
|
369
|
+
* `resolvedBy` that names one of `authors`, the forge's naming of the pull
|
|
370
|
+
* request's author and of its commits' authors and committers. Their own
|
|
371
|
+
* approval never counts, whether a forge review or a ledger line backs it.
|
|
372
|
+
*/
|
|
373
|
+
export function approversWhoAuthored(approvedBy: readonly string[], authors: readonly string[]): string[] {
|
|
374
|
+
const norm = (s: string) => s.trim().toLowerCase();
|
|
375
|
+
const wrote = new Set(authors.map(norm));
|
|
376
|
+
return approvedBy.filter((by) => wrote.has(norm(by)));
|
|
377
|
+
}
|
|
378
|
+
|
|
245
379
|
export interface PrApplyOptions {
|
|
246
380
|
components: DriverComponent[];
|
|
247
381
|
plan: FanOutPlan;
|
|
@@ -261,58 +395,151 @@ export interface PrApplyOptions {
|
|
|
261
395
|
resumed?: { completed: string[]; outputs: Record<string, Record<string, unknown>> };
|
|
262
396
|
/** Called as each component settles, so an attempt record survives a kill (`runFanOut`). */
|
|
263
397
|
onComponentSettled?: (result: DriverComponentResult, outputs: Record<string, unknown> | undefined) => void;
|
|
398
|
+
/** The pull request's gate, which names each later round's gate ({@link prRoundGate}). Default `pr-apply`. */
|
|
399
|
+
gate?: string;
|
|
400
|
+
/**
|
|
401
|
+
* Decide the gate over the plans of a round after the first (#3999): the
|
|
402
|
+
* members held back because an upstream they read applied as another value
|
|
403
|
+
* than their plan read, planned again against the outputs the apply left.
|
|
404
|
+
* The gate is {@link prRoundGate}. Without it, held members are left
|
|
405
|
+
* `waiting` and nothing is planned again.
|
|
406
|
+
*/
|
|
407
|
+
decideRound?: (round: { round: number; gate: string; set: PrPlanSet; plan: FanOutPlan }) => Promise<PrRoundDecision>;
|
|
264
408
|
}
|
|
265
409
|
|
|
266
|
-
|
|
410
|
+
/** How a round's gate was decided: whether it applies, and where its approval stands. */
|
|
411
|
+
export interface PrRoundDecision {
|
|
412
|
+
satisfied: boolean;
|
|
413
|
+
approval: PrApproval;
|
|
414
|
+
/** Why it does not apply, when an approval stands but does not count (`--require-review`). */
|
|
415
|
+
message?: string;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export interface PrApplyResult {
|
|
419
|
+
members: PrMember[];
|
|
420
|
+
/** `waiting`: some members wait on an approval of the plan a later round made. */
|
|
421
|
+
status: "applied" | "failed" | "waiting";
|
|
422
|
+
/** Every round's runs together: their results, components and outputs. */
|
|
423
|
+
run: FanOutRunResult;
|
|
424
|
+
/** Each round after the first, in order. Absent when nothing was held back. */
|
|
425
|
+
rounds?: PrRound[];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Two runs of one apply as one: `b` ran after `a`, against the outputs `a` left. */
|
|
429
|
+
function joinRuns(a: FanOutRunResult, b: FanOutRunResult): FanOutRunResult {
|
|
430
|
+
const status: FanOutRunResult["status"] = a.status === "fail" || b.status === "fail" ? "fail" : a.status === "gated" || b.status === "gated" ? "gated" : "ok";
|
|
431
|
+
const gate = a.gate ?? b.gate;
|
|
432
|
+
return {
|
|
433
|
+
plan: a.plan,
|
|
434
|
+
status,
|
|
435
|
+
results: [...a.results, ...b.results],
|
|
436
|
+
completed: [...new Set([...a.completed, ...b.completed])].sort(),
|
|
437
|
+
failed: [...new Set([...a.failed, ...b.failed])].sort(),
|
|
438
|
+
blocked: [...a.blocked, ...b.blocked],
|
|
439
|
+
componentOutputs: b.componentOutputs,
|
|
440
|
+
...(gate ? { gate } : {}),
|
|
441
|
+
...(b.held ? { held: b.held } : {}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
267
444
|
|
|
268
445
|
/**
|
|
269
446
|
* Apply the approved plans in dependency order. A failed component blocks
|
|
270
|
-
* its dependents and nothing else (`runFanOut`).
|
|
447
|
+
* its dependents and nothing else (`runFanOut`). A member whose plan read an
|
|
448
|
+
* upstream output that is not the value standing when its turn comes is held
|
|
449
|
+
* back with what is downstream of it, planned again once the rest settled,
|
|
450
|
+
* and applied when `decideRound` approves that plan (#3999).
|
|
271
451
|
*/
|
|
272
|
-
export async function applyPrSet(options: PrApplyOptions): Promise<
|
|
273
|
-
const
|
|
274
|
-
const run = await runFanOut(options.plan, options.components, options.registry, {
|
|
275
|
-
env: options.env,
|
|
276
|
-
...(options.vars ? { vars: options.vars } : {}),
|
|
277
|
-
plans: options.set.plans,
|
|
278
|
-
componentOutputs: { ...before },
|
|
279
|
-
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
280
|
-
...(options.onRunEvent ? { onRunEvent: options.onRunEvent } : {}),
|
|
281
|
-
...(options.onComponentSettled ? { onComponentSettled: options.onComponentSettled } : {}),
|
|
282
|
-
});
|
|
283
|
-
const completed = new Set(run.completed);
|
|
452
|
+
export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult> {
|
|
453
|
+
const byName = new Map(options.components.map((c) => [c.name, c]));
|
|
284
454
|
const priorCompleted = new Set(options.resumed?.completed ?? []);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
455
|
+
const inChange = new Set([...options.set.members.map((m) => m.component), ...options.plan.order]);
|
|
456
|
+
const toApply = new Set([...inChange].filter((name) => !priorCompleted.has(name)));
|
|
457
|
+
// The outputs standing as the apply starts: what the plans read of
|
|
458
|
+
// components outside the change, and what an earlier attempt's components
|
|
459
|
+
// left (the values the plans read, where its record kept none). A planned
|
|
460
|
+
// value of a component still to apply stands nowhere until it applies.
|
|
461
|
+
const standing: Record<string, Record<string, unknown>> = {
|
|
462
|
+
...Object.fromEntries(Object.entries(options.set.outputs).filter(([name]) => !toApply.has(name))),
|
|
463
|
+
...(options.resumed?.outputs ?? {}),
|
|
293
464
|
};
|
|
465
|
+
const runRound = (plan: FanOutPlan, plans: Record<string, Record<string, unknown>>, read: Record<string, Record<string, unknown>>, outputs: Record<string, Record<string, unknown>>) =>
|
|
466
|
+
runFanOut(plan, options.components, options.registry, {
|
|
467
|
+
env: options.env,
|
|
468
|
+
...(options.vars ? { vars: options.vars } : {}),
|
|
469
|
+
plans,
|
|
470
|
+
componentOutputs: outputs,
|
|
471
|
+
hold: (name, now) => heldReason(byName.get(name)!, read, now, inChange),
|
|
472
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
473
|
+
...(options.onRunEvent ? { onRunEvent: options.onRunEvent } : {}),
|
|
474
|
+
...(options.onComponentSettled ? { onComponentSettled: options.onComponentSettled } : {}),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
let run = await runRound(options.plan, options.set.plans, options.set.outputs, { ...standing });
|
|
478
|
+
const rounds: PrRound[] = [];
|
|
479
|
+
/** Per component, the round that applied it or holds it, and that round's set, after the first. */
|
|
480
|
+
const roundOf = new Map<string, { round: number; set: PrPlanSet }>();
|
|
481
|
+
let stopped: "waiting" | "plan-failed" | undefined;
|
|
482
|
+
while (run.held && run.held.length > 0 && run.status === "ok" && options.decideRound) {
|
|
483
|
+
const round = rounds.length + 2;
|
|
484
|
+
const gate = prRoundGate(options.gate ?? PR_APPLY_GATE, round);
|
|
485
|
+
const heldNames = new Set(run.held.map((h) => h.component));
|
|
486
|
+
const plan = remainingFanOut(options.plan, options.components, { completed: options.plan.order.filter((name) => !heldNames.has(name)) });
|
|
487
|
+
const set = await planPrSet({
|
|
488
|
+
components: options.components,
|
|
489
|
+
plan,
|
|
490
|
+
registry: options.registry,
|
|
491
|
+
env: options.env,
|
|
492
|
+
...(options.vars ? { vars: options.vars } : {}),
|
|
493
|
+
seededOutputs: run.componentOutputs,
|
|
494
|
+
});
|
|
495
|
+
for (const name of plan.order) roundOf.set(name, { round, set });
|
|
496
|
+
const record = (status: PrRound["status"], approval: PrApproval, message?: string): void => {
|
|
497
|
+
rounds.push({ round, gate, digest: set.doc.digest, components: plan.order, approval, status, ...(message ? { message } : {}), changeSet: set.doc });
|
|
498
|
+
};
|
|
499
|
+
if (set.failed) {
|
|
500
|
+
record("plan-failed", { status: "pending" });
|
|
501
|
+
stopped = "plan-failed";
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
const decision = await options.decideRound({ round, gate, set, plan });
|
|
505
|
+
if (!decision.satisfied) {
|
|
506
|
+
record("waiting", decision.approval, decision.message);
|
|
507
|
+
stopped = "waiting";
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
const next = await runRound(plan, set.plans, set.outputs, run.componentOutputs);
|
|
511
|
+
record(next.status === "fail" ? "failed" : "applied", decision.approval);
|
|
512
|
+
run = joinRuns(run, next);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const completed = new Set(run.completed);
|
|
516
|
+
const held = new Map((run.held ?? []).map((h) => [h.component, h]));
|
|
294
517
|
const blocked = new Set(run.blocked.map((b) => b.component));
|
|
295
518
|
const errorOf = (component: string): string | undefined => {
|
|
296
|
-
const result = run.results.find((r) => r.component === component);
|
|
519
|
+
const result = [...run.results].reverse().find((r) => r.component === component);
|
|
297
520
|
if (result?.status === "gated") return `stopped at its own gate ${result.gate?.gate ?? ""}`.trim();
|
|
298
521
|
return result?.records.find((r) => r.status === "fail")?.error;
|
|
299
522
|
};
|
|
300
|
-
const byName = new Map(options.components.map((c) => [c.name, c]));
|
|
301
523
|
const members = options.set.members.map((m): PrMember => {
|
|
302
524
|
if (priorCompleted.has(m.component)) return { ...m, status: "applied" };
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
if (
|
|
525
|
+
const later = roundOf.get(m.component);
|
|
526
|
+
const fresh = later?.set.members.find((x) => x.member === m.member);
|
|
527
|
+
const asPlanned: PrMember = later && fresh ? { ...m, planDigest: fresh.planDigest, counts: fresh.counts, round: later.round } : m;
|
|
528
|
+
if (completed.has(m.component)) return { ...asPlanned, status: "applied" };
|
|
529
|
+
const h = held.get(m.component);
|
|
530
|
+
if (h) return { ...asPlanned, status: "waiting", held: h.heldBy ? `${h.heldBy} is held: ${h.reason}` : h.reason };
|
|
531
|
+
if (fresh?.status === "plan-failed") return { ...asPlanned, status: "plan-failed", ...(fresh.error ? { error: fresh.error } : {}) };
|
|
532
|
+
if (blocked.has(m.component)) return { ...asPlanned, status: "blocked" };
|
|
310
533
|
const error = errorOf(m.component);
|
|
311
|
-
return { ...
|
|
534
|
+
return { ...asPlanned, status: "failed", ...(error ? { error } : {}) };
|
|
312
535
|
});
|
|
313
|
-
|
|
536
|
+
const status: PrApplyResult["status"] =
|
|
537
|
+
run.status !== "ok" || stopped === "plan-failed" ? "failed" : held.size > 0 || stopped === "waiting" ? "waiting" : "applied";
|
|
538
|
+
return { members, status, run, ...(rounds.length > 0 ? { rounds } : {}) };
|
|
314
539
|
}
|
|
315
540
|
|
|
541
|
+
const sameJson = (a: unknown, b: unknown): boolean => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
542
|
+
|
|
316
543
|
const entryKey = (e: Pick<ChangeSetEntry, "member" | "address" | "deposed">): string => `${e.member}\u0000${e.address}\u0000${e.deposed ?? ""}`;
|
|
317
544
|
|
|
318
545
|
/**
|
|
@@ -466,6 +693,49 @@ export async function resumePrSet(options: PrResumeOptions): Promise<PrResume> {
|
|
|
466
693
|
};
|
|
467
694
|
}
|
|
468
695
|
|
|
696
|
+
/**
|
|
697
|
+
* The fan-out narrowed to wave `wave` of `plan` (1-based), less the
|
|
698
|
+
* components in `completed` (#4003). Its one wave is what is left of that
|
|
699
|
+
* wave; a dependency outside it, an earlier wave's component among them, is
|
|
700
|
+
* a seed. The digest is the derivation's, as {@link remainingFanOut} keeps it.
|
|
701
|
+
*/
|
|
702
|
+
export function prWavePlan(plan: FanOutPlan, components: DriverComponent[], wave: number, completed: readonly string[] = []): FanOutPlan {
|
|
703
|
+
const done = new Set(completed);
|
|
704
|
+
const order = (plan.waves[wave - 1] ?? []).filter((name) => !done.has(name));
|
|
705
|
+
const inWave = new Set(order);
|
|
706
|
+
const byName = new Map(components.map((c) => [c.name, c]));
|
|
707
|
+
const seeds = [
|
|
708
|
+
...new Set([...plan.seeds, ...order.flatMap((name) => (byName.get(name)?.dependsOn ?? []).filter((dep) => !inWave.has(dep)))]),
|
|
709
|
+
].sort();
|
|
710
|
+
return { order, waves: order.length > 0 ? [order] : [], skipped: [], seeds, indeterminate: plan.indeterminate, digest: plan.digest };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Why the approved change set does not cover a wave's new plan, one line per
|
|
715
|
+
* member, or `[]` when it covers every member (#4003). A member is covered
|
|
716
|
+
* as {@link planCoveredBy} says, with one rule more: a member whose planner
|
|
717
|
+
* describes no changes (chant's own empty part, for a capability that
|
|
718
|
+
* returns no change set) is covered only by the digest it was approved
|
|
719
|
+
* with, since with no entries there is nothing to compare a moved plan by.
|
|
720
|
+
*/
|
|
721
|
+
export function waveNotCovered(approved: ChangeSetDocument, fresh: ChangeSetDocument, approvedMembers: readonly PrMember[] = []): string[] {
|
|
722
|
+
// #3999: a member whose approved plan waited only on an artifact a build in
|
|
723
|
+
// the change publishes is covered by that build's provenance, as it is
|
|
724
|
+
// without --wave-gate.
|
|
725
|
+
const byBuild = new Set(approvedMembers.filter((m) => m.buildInputs?.length && !m.unknownInputs?.length).map((m) => m.member));
|
|
726
|
+
return fresh.members
|
|
727
|
+
.map((m) => {
|
|
728
|
+
if (byBuild.has(m.member)) return undefined;
|
|
729
|
+
const was = approved.members.find((a) => a.member === m.member);
|
|
730
|
+
const opaque = m.planner === "chant" && m.lexicon === "chant" && m.status === "planned";
|
|
731
|
+
if (opaque && was && was.planDigest !== m.planDigest) {
|
|
732
|
+
return `${m.member} now plans to ${m.planDigest}, not the approved ${was.planDigest ?? "(none)"}, and its planner describes no changes to compare`;
|
|
733
|
+
}
|
|
734
|
+
return planCoveredBy(approved, fresh, m.member);
|
|
735
|
+
})
|
|
736
|
+
.filter((r): r is string => r !== undefined);
|
|
737
|
+
}
|
|
738
|
+
|
|
469
739
|
export interface PrReportInput {
|
|
470
740
|
stage: "plan" | "apply";
|
|
471
741
|
pr: number | null;
|
|
@@ -486,6 +756,11 @@ export interface PrReportInput {
|
|
|
486
756
|
message?: string;
|
|
487
757
|
/** Components an earlier attempt at the approved set applied (#3464). */
|
|
488
758
|
resumed?: string[];
|
|
759
|
+
/** `--wave-gate` (#4003): the wave gate's base name, and each wave reached so far. */
|
|
760
|
+
waveGate?: string;
|
|
761
|
+
waves?: PrWave[];
|
|
762
|
+
/** The rounds the apply planned after an upstream applied (#3999). */
|
|
763
|
+
rounds?: PrRound[];
|
|
489
764
|
}
|
|
490
765
|
|
|
491
766
|
/** The report a stage writes. */
|
|
@@ -508,7 +783,11 @@ export function prReport(input: PrReportInput): PrReport {
|
|
|
508
783
|
...(input.message ? { message: input.message } : {}),
|
|
509
784
|
...(input.resumed ? { resumed: { applied: [...input.resumed].sort() } } : {}),
|
|
510
785
|
...(input.set.deployedOutputs ? { deployedOutputs: input.set.deployedOutputs } : {}),
|
|
786
|
+
...(input.set.plannedOutputs ? { plannedOutputs: input.set.plannedOutputs } : {}),
|
|
787
|
+
...(input.rounds && input.rounds.length > 0 ? { rounds: input.rounds } : {}),
|
|
511
788
|
approval: input.approval,
|
|
789
|
+
...(input.waveGate ? { waveGate: input.waveGate } : {}),
|
|
790
|
+
...(input.waveGate && input.waves?.length ? { waves: [...input.waves].sort((a, b) => a.wave - b.wave) } : {}),
|
|
512
791
|
selection: {
|
|
513
792
|
changed: input.plan.order.filter((c) => changed.has(c)),
|
|
514
793
|
dependents: input.plan.order.filter((c) => !changed.has(c)),
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { resolveStepInput, unresolvedWiringSources, type DriverComponent, type DriverPhase, type DriverStep } from "./driver";
|
|
23
23
|
import { componentPlanDigest } from "./gate-plan";
|
|
24
|
-
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
24
|
+
import type { CapabilityRegistry, DeployContext, PlannedOutputs } from "./capability";
|
|
25
25
|
import type { WaveMember } from "../gated-waves";
|
|
26
26
|
import type { ChangeSetPart } from "../change-set";
|
|
27
27
|
|
|
@@ -29,10 +29,12 @@ import type { ChangeSetPart } from "../change-set";
|
|
|
29
29
|
* What planning one component produced: its members, what its steps hand to
|
|
30
30
|
* `run`, and each member's change-set part (#3183), or why it could not plan.
|
|
31
31
|
* A member whose capability returns no part gets one with no entries, so the
|
|
32
|
-
* parts always cover the members.
|
|
32
|
+
* parts always cover the members. `outputs` is what its steps' plans say the
|
|
33
|
+
* component will expose once applied (#3999), merged the way the driver
|
|
34
|
+
* merges a run's outputs; absent when no step's plan says.
|
|
33
35
|
*/
|
|
34
36
|
export type WaveComponentPlan =
|
|
35
|
-
| { component: string; members: WaveMember[]; plans: Record<string, unknown>; parts: ChangeSetPart[] }
|
|
37
|
+
| { component: string; members: WaveMember[]; plans: Record<string, unknown>; parts: ChangeSetPart[]; outputs?: PlannedOutputs }
|
|
36
38
|
| { component: string; error: string };
|
|
37
39
|
|
|
38
40
|
/** A part with no entries, for a member whose planner describes no changes. */
|
|
@@ -84,6 +86,7 @@ export async function planWaveComponent(
|
|
|
84
86
|
const members: WaveMember[] = [];
|
|
85
87
|
const plans: Record<string, unknown> = {};
|
|
86
88
|
const parts: ChangeSetPart[] = [];
|
|
89
|
+
let outputs: PlannedOutputs | undefined;
|
|
87
90
|
for (const step of capabilitySteps(component.deploy)) {
|
|
88
91
|
const capability = registry.resolve(step.kind);
|
|
89
92
|
if (!capability.plan) continue;
|
|
@@ -94,6 +97,7 @@ export async function planWaveComponent(
|
|
|
94
97
|
members.push(member);
|
|
95
98
|
parts.push(planned.changeSet ?? emptyPart(member));
|
|
96
99
|
if (planned.artifact !== undefined) plans[planned.member] = planned.artifact;
|
|
100
|
+
if (planned.outputs) outputs = mergePlannedOutputs(outputs, planned.outputs);
|
|
97
101
|
}
|
|
98
102
|
if (members.length === 0) {
|
|
99
103
|
const member = {
|
|
@@ -107,12 +111,20 @@ export async function planWaveComponent(
|
|
|
107
111
|
members.push(member);
|
|
108
112
|
parts.push(emptyPart(member));
|
|
109
113
|
}
|
|
110
|
-
return { component: component.name, members, plans, parts };
|
|
114
|
+
return { component: component.name, members, plans, parts, ...(outputs ? { outputs } : {}) };
|
|
111
115
|
} catch (err) {
|
|
112
116
|
return { component: component.name, error: err instanceof Error ? err.message : String(err) };
|
|
113
117
|
}
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/** `next` over `prior`, as a later step's outputs win over an earlier one's in a run. */
|
|
121
|
+
function mergePlannedOutputs(prior: PlannedOutputs | undefined, next: PlannedOutputs): PlannedOutputs {
|
|
122
|
+
const known = { ...(prior?.known ?? {}), ...next.known };
|
|
123
|
+
const unknown = new Set([...(prior?.unknown ?? []).filter((n) => !(n in next.known)), ...next.unknown]);
|
|
124
|
+
for (const n of unknown) delete known[n];
|
|
125
|
+
return { known, unknown: [...unknown].sort() };
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
/**
|
|
117
129
|
* The outputs `component` exposes now, read from each step whose capability
|
|
118
130
|
* has `outputs` (#3183), merged the way the driver merges a run's outputs.
|