@intentius/chant 0.124.33 → 0.124.34

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.
Files changed (75) hide show
  1. package/dist/cli/handlers/promote.d.ts.map +1 -1
  2. package/dist/cli/handlers/promote.js +261 -73
  3. package/dist/cli/main.d.ts.map +1 -1
  4. package/dist/cli/main.js +18 -1
  5. package/dist/cli/parse-args.d.ts.map +1 -1
  6. package/dist/cli/parse-args.js +7 -0
  7. package/dist/cli/registry.d.ts +3 -1
  8. package/dist/cli/registry.d.ts.map +1 -1
  9. package/dist/components/promote-rounds.d.ts +102 -0
  10. package/dist/components/promote-rounds.d.ts.map +1 -0
  11. package/dist/components/promote-rounds.js +174 -0
  12. package/dist/components/release-gate.d.ts +30 -0
  13. package/dist/components/release-gate.d.ts.map +1 -1
  14. package/dist/components/release-gate.js +92 -42
  15. package/dist/graph-declared.d.ts +4 -0
  16. package/dist/graph-declared.d.ts.map +1 -1
  17. package/dist/graph-declared.js +11 -1
  18. package/dist/wave-runtime.mjs +39 -39
  19. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  20. package/dist/workspace/__fixtures__/contract-repo.js +13 -2
  21. package/dist/workspace/check.schema.json +20 -2
  22. package/dist/workspace/checks/ci-workflow.d.ts +17 -0
  23. package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
  24. package/dist/workspace/checks/ci-workflow.js +123 -0
  25. package/dist/workspace/checks/security.d.ts +16 -1
  26. package/dist/workspace/checks/security.d.ts.map +1 -1
  27. package/dist/workspace/checks/security.js +4 -4
  28. package/dist/workspace/checks.d.ts +1 -1
  29. package/dist/workspace/checks.d.ts.map +1 -1
  30. package/dist/workspace/checks.js +4 -1
  31. package/dist/workspace/ci-init.d.ts +100 -0
  32. package/dist/workspace/ci-init.d.ts.map +1 -0
  33. package/dist/workspace/ci-init.js +434 -0
  34. package/dist/workspace/ci-workflow.d.ts +91 -0
  35. package/dist/workspace/ci-workflow.d.ts.map +1 -0
  36. package/dist/workspace/ci-workflow.js +33 -0
  37. package/dist/workspace/declaration.d.ts +18 -0
  38. package/dist/workspace/declaration.d.ts.map +1 -1
  39. package/dist/workspace/declaration.js +9 -2
  40. package/dist/workspace/declaration.schema.json +28 -0
  41. package/dist/workspace/init.js +2 -2
  42. package/dist/workspace/reason-codes.d.ts +9 -0
  43. package/dist/workspace/reason-codes.d.ts.map +1 -1
  44. package/dist/workspace/reason-codes.js +10 -0
  45. package/dist/workspace/release-workflow.d.ts +31 -0
  46. package/dist/workspace/release-workflow.d.ts.map +1 -0
  47. package/dist/workspace/release-workflow.js +150 -0
  48. package/package.json +1 -1
  49. package/src/cli/handlers/promote.ts +280 -83
  50. package/src/cli/main.ts +18 -1
  51. package/src/cli/parse-args.ts +5 -0
  52. package/src/cli/registry.ts +3 -1
  53. package/src/components/promote-rounds.test.ts +220 -0
  54. package/src/components/promote-rounds.ts +232 -0
  55. package/src/components/release-gate.test.ts +47 -0
  56. package/src/components/release-gate.ts +111 -46
  57. package/src/graph-declared-exports.test.ts +40 -0
  58. package/src/graph-declared.ts +9 -1
  59. package/src/workspace/__fixtures__/contract-repo.ts +11 -2
  60. package/src/workspace/check-contract.test.ts +3 -2
  61. package/src/workspace/check.schema.json +20 -2
  62. package/src/workspace/checks/ci-workflow.test.ts +150 -0
  63. package/src/workspace/checks/ci-workflow.ts +129 -0
  64. package/src/workspace/checks/security.ts +4 -4
  65. package/src/workspace/checks.ts +4 -1
  66. package/src/workspace/ci-init.test.ts +159 -0
  67. package/src/workspace/ci-init.ts +445 -0
  68. package/src/workspace/ci-report-url.test.ts +6 -1
  69. package/src/workspace/ci-workflow.ts +71 -0
  70. package/src/workspace/declaration.schema.json +28 -0
  71. package/src/workspace/declaration.ts +32 -3
  72. package/src/workspace/init.ts +2 -2
  73. package/src/workspace/reason-codes.test.ts +2 -0
  74. package/src/workspace/reason-codes.ts +10 -0
  75. package/src/workspace/release-workflow.ts +167 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A promote under a release that plans again once an upstream applied, and a
3
+ * gate per wave (#4168 step 4). Merging deploys nothing since step 1, so what
4
+ * `pr-apply` did on merge happens here, at promote:
5
+ *
6
+ * - **Rounds** (#3999's rule, `../pr-loop.ts`). The release gate approves a
7
+ * change set in which each dependent was planned against its upstream's
8
+ * planned outputs (`planReleaseSet` in ./release-gate.ts). The promote runs
9
+ * a wave at a time. Before a wave runs, a component whose plan read an
10
+ * upstream output that the run left as another value
11
+ * (`movedWiringReferences`) is planned again against what stands. When the
12
+ * approved change set covers the new plan (`waveNotCovered`, the test a
13
+ * pull request's resume uses), it applies under the approval it has.
14
+ * Otherwise its gates are decided as `<gate>-round-<n>` (`prRoundGate`),
15
+ * bound to the new plan's digest.
16
+ * - **A gate per wave** (`--wave-gate <gate>`, #4157's rule). Every wave
17
+ * after the first is planned again against what the waves before it left,
18
+ * and a wave the approved change set does not cover waits at
19
+ * `<gate>-wave-<k>` (`waveGateName`), one gate for the whole wave, bound to
20
+ * that wave's digest.
21
+ *
22
+ * A stage whose components declare no gate has nowhere to wait, so the
23
+ * promote stops there with nothing more deployed.
24
+ *
25
+ * A promote that stops at a round's or a wave's gate writes a resume record
26
+ * ({@link PromoteResume}): the components that applied and the outputs they
27
+ * left. The same promote run again with it skips what applied and carries on
28
+ * from the wave it stopped at, so an approval of the round or wave it waits on
29
+ * is decided against the same plan.
30
+ */
31
+ import { type DriverComponent, type DriverRunResult, type ReleaseGateBinding } from "./driver.js";
32
+ import type { ChangeSetDocument } from "../change-set.js";
33
+ import type { ReleasePlan } from "./release-gate.js";
34
+ export declare const PROMOTE_RESUME_SCHEMA = "chant-promote-resume/v1";
35
+ /** What a promote that stopped at a round's or a wave's gate leaves for the next attempt. */
36
+ export interface PromoteResume {
37
+ schema: typeof PROMOTE_RESUME_SCHEMA;
38
+ env: string;
39
+ releaseDigest: string;
40
+ /** The change set the release gate approved before anything applied. */
41
+ approvedDigest: string;
42
+ /** Components that applied, in run order. */
43
+ completed: string[];
44
+ /** The outputs standing after them. */
45
+ outputs: Record<string, Record<string, unknown>>;
46
+ /** ISO 8601. */
47
+ at: string;
48
+ }
49
+ /** One round or wave that needed its own approval. */
50
+ export interface PromoteStage {
51
+ kind: "round" | "wave";
52
+ /** The round number (2 and up) or the wave number (1-based). */
53
+ number: number;
54
+ components: string[];
55
+ /** The plan the stage's gates bind. */
56
+ digest: string;
57
+ /** Why the approved change set did not cover it, one line per member. */
58
+ notCovered: string[];
59
+ status: "applied" | "waiting" | "failed";
60
+ /** With `failed` before anything ran: why. */
61
+ message?: string;
62
+ }
63
+ export interface PromoteRoundsOptions {
64
+ /** The promoted compositions, in no particular order. */
65
+ components: DriverComponent[];
66
+ /** Outputs standing before the run: the seeds, and on a resume the record's. */
67
+ outputs: Record<string, Record<string, unknown>>;
68
+ /** The release gate the change set was approved under. */
69
+ binding: ReleaseGateBinding;
70
+ /** The approved change set, and what each component's plan read. */
71
+ approved: ChangeSetDocument;
72
+ reads: ReleasePlan["reads"];
73
+ /** Components an earlier attempt applied. Skipped. */
74
+ completed?: readonly string[];
75
+ /** `--wave-gate <gate>`: check every wave after the first, not only the components whose inputs moved, and name a wave's gate `<gate>-wave-<k>`. */
76
+ waveGate?: string;
77
+ /** Run one subset of the components with `outputs` standing (`runPromotion` with the binding). */
78
+ run: (components: DriverComponent[], outputs: Record<string, Record<string, unknown>>, binding: ReleaseGateBinding) => Promise<DriverRunResult>;
79
+ /** Plan `only` again with `outputs` standing (`planReleaseSet`). */
80
+ replan: (only: ReadonlySet<string>, outputs: Record<string, Record<string, unknown>>) => Promise<ChangeSetDocument>;
81
+ /** Store a stage's plan and bind its gates (empty resolutions included). */
82
+ bind: (doc: ChangeSetDocument, gates: string[]) => Promise<ReleaseGateBinding>;
83
+ }
84
+ export interface PromoteRoundsResult {
85
+ run: DriverRunResult;
86
+ /** The rounds and waves that needed their own approval. */
87
+ stages: PromoteStage[];
88
+ /** Every component that applied, earlier attempts included, in run order. */
89
+ completed: string[];
90
+ outputs: Record<string, Record<string, unknown>>;
91
+ }
92
+ /** `component` with each gate renamed by `rename`, nested phases included. */
93
+ export declare function renameGates(component: DriverComponent, rename: (gate: string) => string): DriverComponent;
94
+ /** The references `component`'s steps make that resolve to another value now than when its plan read them. */
95
+ export declare function movedInputs(component: DriverComponent, read: Record<string, Record<string, unknown>> | undefined, now: Record<string, Record<string, unknown>>): string[];
96
+ /**
97
+ * Run the promote a wave at a time, planning again where an upstream applied
98
+ * as another value, and with `waveGate`, checking each wave. Stops at the
99
+ * first wave that fails or waits on a gate.
100
+ */
101
+ export declare function runPromotionInRounds(options: PromoteRoundsOptions): Promise<PromoteRoundsResult>;
102
+ //# sourceMappingURL=promote-rounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"promote-rounds.d.ts","sourceRoot":"","sources":["../../src/components/promote-rounds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * A promote under a release that plans again once an upstream applied, and a
3
+ * gate per wave (#4168 step 4). Merging deploys nothing since step 1, so what
4
+ * `pr-apply` did on merge happens here, at promote:
5
+ *
6
+ * - **Rounds** (#3999's rule, `../pr-loop.ts`). The release gate approves a
7
+ * change set in which each dependent was planned against its upstream's
8
+ * planned outputs (`planReleaseSet` in ./release-gate.ts). The promote runs
9
+ * a wave at a time. Before a wave runs, a component whose plan read an
10
+ * upstream output that the run left as another value
11
+ * (`movedWiringReferences`) is planned again against what stands. When the
12
+ * approved change set covers the new plan (`waveNotCovered`, the test a
13
+ * pull request's resume uses), it applies under the approval it has.
14
+ * Otherwise its gates are decided as `<gate>-round-<n>` (`prRoundGate`),
15
+ * bound to the new plan's digest.
16
+ * - **A gate per wave** (`--wave-gate <gate>`, #4157's rule). Every wave
17
+ * after the first is planned again against what the waves before it left,
18
+ * and a wave the approved change set does not cover waits at
19
+ * `<gate>-wave-<k>` (`waveGateName`), one gate for the whole wave, bound to
20
+ * that wave's digest.
21
+ *
22
+ * A stage whose components declare no gate has nowhere to wait, so the
23
+ * promote stops there with nothing more deployed.
24
+ *
25
+ * A promote that stops at a round's or a wave's gate writes a resume record
26
+ * ({@link PromoteResume}): the components that applied and the outputs they
27
+ * left. The same promote run again with it skips what applied and carries on
28
+ * from the wave it stopped at, so an approval of the round or wave it waits on
29
+ * is decided against the same plan.
30
+ */
31
+ import { movedWiringReferences } from "./driver.js";
32
+ import { resolveComponentGraph } from "./driver.js";
33
+ import { waveNotCovered } from "./pr-run.js";
34
+ import { prRoundGate } from "../pr-loop.js";
35
+ import { waveGateName } from "../gated-waves.js";
36
+ import { capabilitySteps } from "./wave-plan.js";
37
+ export const PROMOTE_RESUME_SCHEMA = "chant-promote-resume/v1";
38
+ function isGate(step) {
39
+ return step.kind === "gate";
40
+ }
41
+ function isPhase(step) {
42
+ return typeof step.phase === "string" && Array.isArray(step.steps);
43
+ }
44
+ /** `component` with each gate renamed by `rename`, nested phases included. */
45
+ export function renameGates(component, rename) {
46
+ const walk = (phase) => ({
47
+ ...phase,
48
+ steps: phase.steps.map((step) => {
49
+ if (isPhase(step))
50
+ return walk(step);
51
+ if (!isGate(step))
52
+ return step;
53
+ const { signalName: _old, ...rest } = step;
54
+ return { ...rest, gate: rename(step.gate ?? step.signalName ?? "") };
55
+ }),
56
+ });
57
+ return { ...component, deploy: component.deploy.map(walk) };
58
+ }
59
+ /** The gate names `components` declare, sorted and distinct. */
60
+ function gatesOf(components) {
61
+ const names = new Set();
62
+ const walk = (steps) => {
63
+ for (const step of steps) {
64
+ if (isPhase(step))
65
+ walk(step.steps);
66
+ else if (isGate(step))
67
+ names.add(step.gate ?? step.signalName ?? "");
68
+ }
69
+ };
70
+ for (const c of components)
71
+ for (const phase of c.deploy)
72
+ walk(phase.steps);
73
+ return [...names].sort();
74
+ }
75
+ /** The references `component`'s steps make that resolve to another value now than when its plan read them. */
76
+ export function movedInputs(component, read, now) {
77
+ if (!read)
78
+ return [];
79
+ const moved = new Set();
80
+ for (const step of capabilitySteps(component.deploy)) {
81
+ const { kind: _kind, ...rest } = step;
82
+ for (const r of movedWiringReferences(rest, read, now))
83
+ if (!r.publish)
84
+ moved.add(r.ref);
85
+ }
86
+ return [...moved].sort();
87
+ }
88
+ function joinRuns(runs, outputs) {
89
+ const last = runs[runs.length - 1];
90
+ const base = {
91
+ order: runs.flatMap((r) => r.order),
92
+ waves: runs.flatMap((r) => r.waves),
93
+ results: runs.flatMap((r) => r.results),
94
+ ok: runs.every((r) => r.ok),
95
+ status: "ok",
96
+ componentOutputs: outputs,
97
+ };
98
+ if (!last || last.status === "ok")
99
+ return base;
100
+ const { order: _o, waves: _w, results: _r, ok: _ok, componentOutputs: _c, ...stop } = last;
101
+ return { ...base, ...stop, ok: false };
102
+ }
103
+ /**
104
+ * Run the promote a wave at a time, planning again where an upstream applied
105
+ * as another value, and with `waveGate`, checking each wave. Stops at the
106
+ * first wave that fails or waits on a gate.
107
+ */
108
+ export async function runPromotionInRounds(options) {
109
+ const byName = new Map(options.components.map((c) => [c.name, c]));
110
+ const { waves } = resolveComponentGraph(options.components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
111
+ const completed = [...(options.completed ?? [])];
112
+ const done = new Set(completed);
113
+ let outputs = { ...options.outputs };
114
+ const runs = [];
115
+ const stages = [];
116
+ let round = 1;
117
+ for (const [index, wave] of waves.entries()) {
118
+ const waveNumber = index + 1;
119
+ const pending = wave.filter((name) => !done.has(name));
120
+ if (pending.length === 0)
121
+ continue;
122
+ const check = options.waveGate !== undefined && waveNumber > 1
123
+ ? pending
124
+ : pending.filter((name) => movedInputs(byName.get(name), options.reads[name], outputs).length > 0);
125
+ const batches = [];
126
+ const rest = pending.filter((name) => !check.includes(name));
127
+ if (check.length > 0) {
128
+ const fresh = await options.replan(new Set(check), outputs);
129
+ const notCovered = waveNotCovered(options.approved, fresh);
130
+ if (notCovered.length === 0) {
131
+ rest.push(...check);
132
+ }
133
+ else {
134
+ const kind = options.waveGate !== undefined ? "wave" : "round";
135
+ if (kind === "round")
136
+ round += 1;
137
+ const number = kind === "wave" ? waveNumber : round;
138
+ const base = options.waveGate;
139
+ const rename = (g) => (base !== undefined ? waveGateName(base, number) : prRoundGate(g, number));
140
+ const renamed = check.map((name) => renameGates(byName.get(name), rename));
141
+ const gates = gatesOf(renamed);
142
+ const stage = { kind, number, components: check, digest: fresh.digest, notCovered, status: "waiting" };
143
+ stages.push(stage);
144
+ if (gates.length === 0) {
145
+ stage.status = "failed";
146
+ stage.message = `${check.join(", ")} planned again past the approved change set and declare${check.length === 1 ? "s" : ""} no gate to approve the new plan on`;
147
+ const stop = { order: [], waves: [], results: [], ok: false, status: "fail", failedComponent: check[0], componentOutputs: outputs };
148
+ return { run: joinRuns([...runs, stop], outputs), stages, completed, outputs };
149
+ }
150
+ const binding = await options.bind(fresh, gates);
151
+ batches.push({ names: check, binding, stage, rename });
152
+ }
153
+ }
154
+ if (rest.length > 0)
155
+ batches.unshift({ names: pending.filter((n) => rest.includes(n)), binding: options.binding });
156
+ for (const batch of batches) {
157
+ const components = batch.names.map((name) => (batch.rename ? renameGates(byName.get(name), batch.rename) : byName.get(name)));
158
+ const run = await options.run(components, outputs, batch.binding);
159
+ runs.push(run);
160
+ outputs = { ...outputs, ...run.componentOutputs };
161
+ for (const r of run.results) {
162
+ if (r.status === "ok" && !done.has(r.component)) {
163
+ done.add(r.component);
164
+ completed.push(r.component);
165
+ }
166
+ }
167
+ if (batch.stage)
168
+ batch.stage.status = run.status === "ok" ? "applied" : run.status === "gated" ? "waiting" : "failed";
169
+ if (run.status !== "ok")
170
+ return { run: joinRuns(runs, outputs), stages, completed, outputs };
171
+ }
172
+ }
173
+ return { run: joinRuns(runs, outputs), stages, completed, outputs };
174
+ }
@@ -39,6 +39,14 @@
39
39
  * planned by that capability, as a pull request's plan is. A plan that
40
40
  * returns no change-set part is a hole, since nothing says what it changes.
41
41
  *
42
+ * A dependency the promote also deploys is read from its plan, not from the
43
+ * estate (#4168 step 4, the rule #3999 gave a pull request): a dependent is
44
+ * planned against the outputs its upstream's plan says it will expose, and a
45
+ * reference the plan can't say is listed in `unknownInputs`. Any other
46
+ * dependency is read as it stands. What each component was planned against is
47
+ * kept in `reads`, so the promote can hold it back when an upstream applies
48
+ * as another value (`./promote-rounds.ts`).
49
+ *
42
50
  * Any other component is a `chant` member compared with what the
43
51
  * environment runs now: its latest deploy record and the release that record
44
52
  * names. The artifact digest, each template the release's target records for
@@ -91,6 +99,22 @@ export interface ReleaseChangeSetInput {
91
99
  seededOutputs?: Record<string, Record<string, unknown>>;
92
100
  /** The chant version to record in the document. */
93
101
  chant?: string;
102
+ /**
103
+ * Plan only these components (a later round or wave, ./promote-rounds.ts).
104
+ * The others are not planned; a dependency among them is read from
105
+ * `seededOutputs`, which then holds what the run left. Default: every one.
106
+ */
107
+ only?: ReadonlySet<string>;
108
+ }
109
+ /** A release's change set for an environment, and what each component was planned against. */
110
+ export interface ReleasePlan {
111
+ doc: ChangeSetDocument;
112
+ /** Per planned component, the outputs of its dependencies as its plan read them. */
113
+ reads: Record<string, Record<string, Record<string, unknown>>>;
114
+ /** Per dependency read from its plan, the names of the outputs the plan knows, sorted. Absent when none. */
115
+ plannedOutputs?: Record<string, string[]>;
116
+ /** Per component, the references to a promoted upstream that no value stands for until it applies, sorted. Absent when none. */
117
+ unknownInputs?: Record<string, string[]>;
94
118
  }
95
119
  /**
96
120
  * A component no capability plans, as a `chant` member: what the release
@@ -112,6 +136,12 @@ export declare function chantComponentPart(input: {
112
136
  * that fails to plan: it becomes a failed member.
113
137
  */
114
138
  export declare function planReleaseChangeSet(input: ReleaseChangeSetInput): Promise<ChangeSetDocument>;
139
+ /**
140
+ * {@link planReleaseChangeSet}, with what each component read. Components are
141
+ * planned a wave at a time, so a dependency the promote deploys has planned
142
+ * before its dependents read its outputs.
143
+ */
144
+ export declare function planReleaseSet(input: ReleaseChangeSetInput): Promise<ReleasePlan>;
115
145
  /** The members a release gate refuses: provisional ones, which no approval of the document covers (`change-set.mdx`). */
116
146
  export declare function provisionalMembers(doc: ChangeSetDocument): string[];
117
147
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"release-gate.d.ts","sourceRoot":"","sources":["../../src/components/release-gate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"release-gate.d.ts","sourceRoot":"","sources":["../../src/components/release-gate.ts"],"names":[],"mappings":"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"}
@@ -39,6 +39,14 @@
39
39
  * planned by that capability, as a pull request's plan is. A plan that
40
40
  * returns no change-set part is a hole, since nothing says what it changes.
41
41
  *
42
+ * A dependency the promote also deploys is read from its plan, not from the
43
+ * estate (#4168 step 4, the rule #3999 gave a pull request): a dependent is
44
+ * planned against the outputs its upstream's plan says it will expose, and a
45
+ * reference the plan can't say is listed in `unknownInputs`. Any other
46
+ * dependency is read as it stands. What each component was planned against is
47
+ * kept in `reads`, so the promote can hold it back when an upstream applies
48
+ * as another value (`./promote-rounds.ts`).
49
+ *
42
50
  * Any other component is a `chant` member compared with what the
43
51
  * environment runs now: its latest deploy record and the release that record
44
52
  * names. The artifact digest, each template the release's target records for
@@ -51,7 +59,7 @@
51
59
  * component a promote, rollback or redeploy left failed or gated with nothing
52
60
  * recorded since (#4035): its record names what ran before that run.
53
61
  */
54
- import { deployContext, resolveComponentGraph, resolveStepInput } from "./driver.js";
62
+ import { deployContext, resolveComponentGraph, resolveStepInput, unresolvedWiringReferences } from "./driver.js";
55
63
  import { componentPlanDigest } from "./gate-plan.js";
56
64
  import { gateName, RELEASE_GATE_OP } from "../op/gate-name.js";
57
65
  import { capabilitySteps, planContext, readComponentOutputs } from "./wave-plan.js";
@@ -187,66 +195,108 @@ export function chantComponentPart(input) {
187
195
  * that fails to plan: it becomes a failed member.
188
196
  */
189
197
  export async function planReleaseChangeSet(input) {
198
+ return (await planReleaseSet(input)).doc;
199
+ }
200
+ /**
201
+ * {@link planReleaseChangeSet}, with what each component read. Components are
202
+ * planned a wave at a time, so a dependency the promote deploys has planned
203
+ * before its dependents read its outputs.
204
+ */
205
+ export async function planReleaseSet(input) {
190
206
  const { release, env, registry } = input;
191
207
  const components = [...input.components];
192
208
  const byName = new Map(components.map((c) => [c.name, c]));
193
209
  const outputs = { ...(input.seededOutputs ?? {}) };
194
210
  const read = new Set(Object.keys(outputs));
195
211
  const parts = [];
196
- const { order } = resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
197
- for (const name of order) {
198
- const component = byName.get(name);
199
- const digest = input.digests.get(name) ?? "";
200
- const plannable = capabilitySteps(component.deploy).filter((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
201
- if (plannable.length === 0) {
202
- parts.push(chantComponentPart({ component, release, env, digest, current: input.current.get(name), unsettled: input.unsettled?.get(name) }));
203
- continue;
204
- }
205
- const ctx = deployContext({ env }, name);
206
- try {
207
- for (const dep of component.dependsOn ?? []) {
208
- if (read.has(dep) || !byName.has(dep))
209
- continue;
210
- read.add(dep);
211
- const found = await readComponentOutputs(byName.get(dep), registry, deployContext({ env }, dep), outputs);
212
- if (found)
213
- outputs[dep] = found;
212
+ const plannedBy = new Map();
213
+ const planned = {};
214
+ const unknownInputs = {};
215
+ const reads = {};
216
+ const inPlan = (name) => input.only === undefined || input.only.has(name);
217
+ const isPlannable = (c) => capabilitySteps(c.deploy).some((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
218
+ const { waves } = resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
219
+ for (const wave of waves) {
220
+ const planning = wave.filter(inPlan);
221
+ for (const name of planning) {
222
+ const component = byName.get(name);
223
+ const digest = input.digests.get(name) ?? "";
224
+ const plannable = capabilitySteps(component.deploy).filter((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
225
+ if (plannable.length === 0) {
226
+ parts.push(chantComponentPart({ component, release, env, digest, current: input.current.get(name), unsettled: input.unsettled?.get(name) }));
227
+ continue;
228
+ }
229
+ const ctx = deployContext({ env }, name);
230
+ try {
231
+ for (const dep of component.dependsOn ?? []) {
232
+ if (read.has(dep) || !byName.has(dep))
233
+ continue;
234
+ read.add(dep);
235
+ // A dependency this promote plans exposes what its plan says (#3999).
236
+ if (plannedBy.has(dep) && isPlannable(byName.get(dep))) {
237
+ const known = plannedBy.get(dep)?.known ?? {};
238
+ outputs[dep] = { ...known };
239
+ planned[dep] = Object.keys(known).sort();
240
+ continue;
241
+ }
242
+ const found = await readComponentOutputs(byName.get(dep), registry, deployContext({ env }, dep), outputs);
243
+ if (found)
244
+ outputs[dep] = found;
245
+ }
246
+ reads[name] = Object.fromEntries((component.dependsOn ?? []).filter((d) => d in outputs).map((d) => [d, structuredClone(outputs[d])]));
247
+ const unknown = new Set();
248
+ let exposes;
249
+ for (const step of plannable) {
250
+ const { kind, ...rest } = step;
251
+ for (const r of unresolvedWiringReferences(rest, outputs))
252
+ if (plannedBy.has(r.source) && !r.publish)
253
+ unknown.add(r.ref);
254
+ const result = await registry.resolve(kind).plan(planContext(ctx, rest, outputs), resolveStepInput(rest, {}, outputs));
255
+ if (result.outputs)
256
+ exposes = { known: { ...(exposes?.known ?? {}), ...result.outputs.known }, unknown: [...new Set([...(exposes?.unknown ?? []), ...result.outputs.unknown])].sort() };
257
+ parts.push(result.changeSet ?? {
258
+ member: {
259
+ member: result.member,
260
+ lexicon: "chant",
261
+ planner: "chant",
262
+ status: "planned",
263
+ planDigest: result.planDigest,
264
+ holes: [{ address: result.member, reason: `its ${kind} plan describes no changes, so what it changes is unknown` }],
265
+ },
266
+ entries: [],
267
+ });
268
+ }
269
+ plannedBy.set(name, exposes);
270
+ if (unknown.size > 0)
271
+ unknownInputs[name] = [...unknown].sort();
214
272
  }
215
- for (const step of plannable) {
216
- const { kind, ...rest } = step;
217
- const planned = await registry.resolve(kind).plan(planContext(ctx, rest, outputs), resolveStepInput(rest, {}, outputs));
218
- parts.push(planned.changeSet ?? {
273
+ catch (err) {
274
+ plannedBy.set(name, undefined);
275
+ parts.push({
219
276
  member: {
220
- member: planned.member,
277
+ member: name,
221
278
  lexicon: "chant",
222
279
  planner: "chant",
223
- status: "planned",
224
- planDigest: planned.planDigest,
225
- holes: [{ address: planned.member, reason: `its ${kind} plan describes no changes, so what it changes is unknown` }],
280
+ status: "failed",
281
+ error: err instanceof Error ? err.message : String(err),
282
+ planDigest: null,
283
+ holes: [],
226
284
  },
227
285
  entries: [],
228
286
  });
229
287
  }
230
288
  }
231
- catch (err) {
232
- parts.push({
233
- member: {
234
- member: name,
235
- lexicon: "chant",
236
- planner: "chant",
237
- status: "failed",
238
- error: err instanceof Error ? err.message : String(err),
239
- planDigest: null,
240
- holes: [],
241
- },
242
- entries: [],
243
- });
244
- }
245
289
  }
246
- return composeChangeSet(parts, {
290
+ const doc = composeChangeSet(parts, {
247
291
  ...(input.chant !== undefined ? { chant: input.chant } : {}),
248
292
  planned: { releaseDigest: release.releaseDigest, environment: env },
249
293
  });
294
+ return {
295
+ doc,
296
+ reads,
297
+ ...(Object.keys(planned).length > 0 ? { plannedOutputs: planned } : {}),
298
+ ...(Object.keys(unknownInputs).length > 0 ? { unknownInputs } : {}),
299
+ };
250
300
  }
251
301
  /** The members a release gate refuses: provisional ones, which no approval of the document covers (`change-set.mdx`). */
252
302
  export function provisionalMembers(doc) {
@@ -10,6 +10,10 @@ import { type GraphIR } from "./graph-ir.js";
10
10
  * that live side, build each stack's `src` in isolation and qualify its node ids
11
11
  * and edge endpoints the same way. Stacks are merged into one graph.
12
12
  *
13
+ * Each stack's exports and imports come along with their `node` qualified the
14
+ * same way (#4202), so a workspace viewer can still match an import in another
15
+ * member to an export here once an environment is picked.
16
+ *
13
17
  * A stack without `src` contributes nothing here — it has no declared source to
14
18
  * scope to (its live nodes still show as foreign in the overlay).
15
19
  */
@@ -1 +1 @@
1
- {"version":3,"file":"graph-declared.d.ts","sourceRoot":"","sources":["../src/graph-declared.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,OAAO,EAAe,MAAM,YAAY,CAAC;AAErE;;;;;;;;;;;;;GAaG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,CAAC,CAuBlB"}
1
+ {"version":3,"file":"graph-declared.d.ts","sourceRoot":"","sources":["../src/graph-declared.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,OAAO,EAAe,MAAM,YAAY,CAAC;AAErE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,CAAC,CA2BlB"}
@@ -12,6 +12,10 @@ import { buildGraphIr } from "./graph-ir.js";
12
12
  * that live side, build each stack's `src` in isolation and qualify its node ids
13
13
  * and edge endpoints the same way. Stacks are merged into one graph.
14
14
  *
15
+ * Each stack's exports and imports come along with their `node` qualified the
16
+ * same way (#4202), so a workspace viewer can still match an import in another
17
+ * member to an export here once an environment is picked.
18
+ *
15
19
  * A stack without `src` contributes nothing here — it has no declared source to
16
20
  * scope to (its live nodes still show as foreign in the overlay).
17
21
  */
@@ -19,6 +23,8 @@ export async function buildDeclaredPerStack(stacks, projectPath) {
19
23
  var _a;
20
24
  const nodes = [];
21
25
  const edges = [];
26
+ const exports = [];
27
+ const imports = [];
22
28
  // Which nodes belong to which stack (#1433). This is not derived or guessed:
23
29
  // the stack is declared in config and every node here is being renamed with
24
30
  // its name on the line below. The membership was always known and thrown
@@ -37,9 +43,13 @@ export async function buildDeclaredPerStack(stacks, projectPath) {
37
43
  }
38
44
  for (const e of g.edges)
39
45
  edges.push({ ...e, from: q(e.from), to: q(e.to) });
46
+ for (const e of g.exports ?? [])
47
+ exports.push({ ...e, ...(e.node ? { node: q(e.node) } : {}) });
48
+ for (const i of g.imports ?? [])
49
+ imports.push({ ...i, node: q(i.node) });
40
50
  }
41
51
  for (const ids of Object.values(byStack))
42
52
  ids.sort();
43
53
  const groups = Object.keys(byStack).length ? { byStack } : {};
44
- return { nodes, edges, groups };
54
+ return { nodes, edges, groups, ...(exports.length ? { exports } : {}), ...(imports.length ? { imports } : {}) };
45
55
  }