@intentius/chant 0.124.29 → 0.124.31

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 (145) hide show
  1. package/dist/cli/handlers/build.d.ts.map +1 -1
  2. package/dist/cli/handlers/build.js +4 -1
  3. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  4. package/dist/cli/handlers/fan-out.js +27 -5
  5. package/dist/cli/handlers/pr.d.ts.map +1 -1
  6. package/dist/cli/main.d.ts.map +1 -1
  7. package/dist/cli/main.js +64 -0
  8. package/dist/cli/parse-args.d.ts.map +1 -1
  9. package/dist/cli/parse-args.js +99 -0
  10. package/dist/cli/registry.d.ts +35 -1
  11. package/dist/cli/registry.d.ts.map +1 -1
  12. package/dist/components/pr-pipeline.d.ts +24 -0
  13. package/dist/components/pr-pipeline.d.ts.map +1 -1
  14. package/dist/components/pr-pipeline.js +16 -0
  15. package/dist/components/pr-run.d.ts +20 -2
  16. package/dist/components/pr-run.d.ts.map +1 -1
  17. package/dist/components/pr-run.js +26 -13
  18. package/dist/components/wave-plan.d.ts +11 -0
  19. package/dist/components/wave-plan.d.ts.map +1 -1
  20. package/dist/components/wave-plan.js +26 -0
  21. package/dist/lifecycle/release-ledger.d.ts +8 -3
  22. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  23. package/dist/lifecycle/release-ledger.js +2 -0
  24. package/dist/lifecycle/run-ledger.d.ts +1 -1
  25. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  26. package/dist/lifecycle/run-ledger.js +9 -0
  27. package/dist/op/activities/activity-contracts.d.ts +1 -0
  28. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  29. package/dist/op/activities/activity-contracts.js +1 -0
  30. package/dist/op/activities/shell.d.ts +6 -0
  31. package/dist/op/activities/shell.d.ts.map +1 -1
  32. package/dist/op/activities/shell.js +1 -1
  33. package/dist/op/local-executor.d.ts +35 -1
  34. package/dist/op/local-executor.d.ts.map +1 -1
  35. package/dist/op/local-executor.js +31 -6
  36. package/dist/op/redact.d.ts +34 -0
  37. package/dist/op/redact.d.ts.map +1 -0
  38. package/dist/op/redact.js +94 -0
  39. package/dist/op/runtime.d.ts +27 -0
  40. package/dist/op/runtime.d.ts.map +1 -1
  41. package/dist/op/runtime.js +20 -0
  42. package/dist/op/step-console.d.ts +2 -0
  43. package/dist/op/step-console.d.ts.map +1 -1
  44. package/dist/op/step-console.js +5 -2
  45. package/dist/pr-forge.d.ts +40 -0
  46. package/dist/pr-forge.d.ts.map +1 -1
  47. package/dist/pr-forge.js +56 -6
  48. package/dist/pr-loop.d.ts +10 -0
  49. package/dist/pr-loop.d.ts.map +1 -1
  50. package/dist/pr-loop.js +7 -0
  51. package/dist/pr-report-url.d.ts +8 -0
  52. package/dist/pr-report-url.d.ts.map +1 -1
  53. package/dist/pr-report-url.js +47 -1
  54. package/dist/run-event-stream.d.ts +2 -2
  55. package/dist/run-event-stream.d.ts.map +1 -1
  56. package/dist/run-event-stream.js +2 -2
  57. package/dist/run-event.d.ts +15 -0
  58. package/dist/run-event.d.ts.map +1 -1
  59. package/dist/run-event.js +2 -0
  60. package/dist/wave-runtime.mjs +171 -171
  61. package/dist/workspace/ci-green-forge.d.ts +2 -0
  62. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  63. package/dist/workspace/ci-green.d.ts +2 -0
  64. package/dist/workspace/ci-green.d.ts.map +1 -1
  65. package/dist/workspace/ci-green.js +4 -2
  66. package/dist/workspace/ci-run-index.d.ts +96 -0
  67. package/dist/workspace/ci-run-index.d.ts.map +1 -0
  68. package/dist/workspace/ci-run-index.js +162 -0
  69. package/dist/workspace/ci-run-index.schema.json +50 -0
  70. package/dist/workspace/ci-run.d.ts +299 -0
  71. package/dist/workspace/ci-run.d.ts.map +1 -0
  72. package/dist/workspace/ci-run.js +691 -0
  73. package/dist/workspace/ci-runner-key.d.ts +120 -0
  74. package/dist/workspace/ci-runner-key.d.ts.map +1 -0
  75. package/dist/workspace/ci-runner-key.js +217 -0
  76. package/dist/workspace/ci-secrets.d.ts +65 -0
  77. package/dist/workspace/ci-secrets.d.ts.map +1 -0
  78. package/dist/workspace/ci-secrets.js +211 -0
  79. package/dist/workspace/ci-serve.d.ts +227 -0
  80. package/dist/workspace/ci-serve.d.ts.map +1 -0
  81. package/dist/workspace/ci-serve.js +468 -0
  82. package/dist/workspace/pr-report.schema.json +5 -0
  83. package/dist/workspace/reason-codes.d.ts +6 -2
  84. package/dist/workspace/reason-codes.d.ts.map +1 -1
  85. package/dist/workspace/reason-codes.js +7 -2
  86. package/dist/workspace/run-event.schema.json +14 -3
  87. package/dist/workspace/status-stewards.d.ts +2 -0
  88. package/dist/workspace/status-stewards.d.ts.map +1 -1
  89. package/dist/workspace/status-stewards.js +1 -0
  90. package/dist/workspace/status.schema.json +12 -1
  91. package/dist/workspace/trust/trust-write.d.ts +109 -0
  92. package/dist/workspace/trust/trust-write.d.ts.map +1 -0
  93. package/dist/workspace/trust/trust-write.js +320 -0
  94. package/dist/workspace/trust-write.schema.json +265 -0
  95. package/package.json +1 -1
  96. package/src/cli/handlers/build.test.ts +15 -0
  97. package/src/cli/handlers/build.ts +4 -1
  98. package/src/cli/handlers/fan-out.test.ts +82 -0
  99. package/src/cli/handlers/fan-out.ts +34 -5
  100. package/src/cli/handlers/pr.ts +5 -0
  101. package/src/cli/main.test.ts +1 -0
  102. package/src/cli/main.ts +64 -0
  103. package/src/cli/parse-args.ts +71 -0
  104. package/src/cli/registry.ts +35 -1
  105. package/src/components/pr-pipeline.ts +32 -0
  106. package/src/components/pr-run.test.ts +127 -0
  107. package/src/components/pr-run.ts +32 -12
  108. package/src/components/wave-plan.ts +30 -0
  109. package/src/lifecycle/release-ledger.ts +9 -3
  110. package/src/lifecycle/run-ledger.ts +7 -1
  111. package/src/op/activities/activity-contracts.ts +1 -0
  112. package/src/op/activities/shell.ts +7 -1
  113. package/src/op/local-executor.ts +58 -7
  114. package/src/op/redact.ts +93 -0
  115. package/src/op/runtime.ts +42 -0
  116. package/src/op/step-console.ts +7 -2
  117. package/src/pr-forge.test.ts +54 -0
  118. package/src/pr-forge.ts +92 -6
  119. package/src/pr-loop.test.ts +13 -1
  120. package/src/pr-loop.ts +16 -0
  121. package/src/pr-report-url.ts +53 -1
  122. package/src/run-event-stream.ts +4 -3
  123. package/src/run-event.ts +17 -0
  124. package/src/workspace/ci-green-forge.ts +2 -0
  125. package/src/workspace/ci-green.ts +6 -2
  126. package/src/workspace/ci-run-index.schema.json +50 -0
  127. package/src/workspace/ci-run-index.test.ts +353 -0
  128. package/src/workspace/ci-run-index.ts +192 -0
  129. package/src/workspace/ci-run.test.ts +716 -0
  130. package/src/workspace/ci-run.ts +833 -0
  131. package/src/workspace/ci-runner-key.test.ts +166 -0
  132. package/src/workspace/ci-runner-key.ts +250 -0
  133. package/src/workspace/ci-secrets.test.ts +106 -0
  134. package/src/workspace/ci-secrets.ts +213 -0
  135. package/src/workspace/ci-serve.test.ts +367 -0
  136. package/src/workspace/ci-serve.ts +574 -0
  137. package/src/workspace/pr-report.schema.json +5 -0
  138. package/src/workspace/reason-codes.test.ts +2 -0
  139. package/src/workspace/reason-codes.ts +7 -2
  140. package/src/workspace/run-event.schema.json +14 -3
  141. package/src/workspace/status-stewards.ts +3 -0
  142. package/src/workspace/status.schema.json +12 -1
  143. package/src/workspace/trust/trust-write.test.ts +179 -0
  144. package/src/workspace/trust/trust-write.ts +377 -0
  145. package/src/workspace/trust-write.schema.json +265 -0
@@ -0,0 +1,265 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/trust-write/v1/trust-write.schema.json",
4
+ "title": "chant workspace trust runner add|remove output",
5
+ "description": "What `chant workspace trust runner add <principal> --key <key>` and `trust runner remove <principal>` print (#3510): the runner entry before and after the write, the digest of .chant/trust.json after it, or the reason it wrote nothing. Each write edits .chant/trust.json in the working tree in place and never commits. Readers ignore fields they do not know; a field is only ever added within a version. The error codes are a closed list, each in the one closed list of `reason-codes.ts`. Contract version 1 of this document is written by chant 0.124.31 and newer.",
6
+ "oneOf": [
7
+ {
8
+ "$ref": "#/$defs/result"
9
+ },
10
+ {
11
+ "$ref": "#/$defs/failure"
12
+ }
13
+ ],
14
+ "$defs": {
15
+ "runner": {
16
+ "type": "object",
17
+ "required": [
18
+ "principal",
19
+ "class",
20
+ "key"
21
+ ],
22
+ "additionalProperties": false,
23
+ "properties": {
24
+ "principal": {
25
+ "type": "string",
26
+ "minLength": 1
27
+ },
28
+ "class": {
29
+ "enum": [
30
+ "runner",
31
+ "service"
32
+ ]
33
+ },
34
+ "key": {
35
+ "type": "string",
36
+ "pattern": "^ssh-ed25519 [A-Za-z0-9+/]+={0,2}$",
37
+ "description": "The runner's public key, as an ssh-ed25519 line with no comment."
38
+ }
39
+ }
40
+ },
41
+ "digest": {
42
+ "type": "string",
43
+ "pattern": "^[0-9a-f]{64}$",
44
+ "description": "A SHA-256 of the file's bytes, in lowercase hex. A file that is not there has the digest of empty text."
45
+ },
46
+ "result": {
47
+ "description": "The entry was written, or would be with --dry-run, or the file already held it (changed false). Exit code 0.",
48
+ "type": "object",
49
+ "required": [
50
+ "$schema",
51
+ "contract",
52
+ "chant",
53
+ "action",
54
+ "name",
55
+ "path",
56
+ "digest",
57
+ "previousDigest",
58
+ "paths",
59
+ "changed",
60
+ "dryRun",
61
+ "previous",
62
+ "entry",
63
+ "runnerRole"
64
+ ],
65
+ "properties": {
66
+ "$schema": {
67
+ "const": "https://intentius.io/chant/schemas/workspace/trust-write/v1/trust-write.schema.json"
68
+ },
69
+ "contract": {
70
+ "const": 1
71
+ },
72
+ "chant": {
73
+ "type": "string",
74
+ "description": "The chant that wrote it."
75
+ },
76
+ "action": {
77
+ "enum": [
78
+ "runner add",
79
+ "runner remove"
80
+ ]
81
+ },
82
+ "name": {
83
+ "type": "string",
84
+ "description": "The runner's principal."
85
+ },
86
+ "path": {
87
+ "const": ".chant/trust.json",
88
+ "description": "The trust config, relative to the repository root."
89
+ },
90
+ "digest": {
91
+ "$ref": "#/$defs/digest",
92
+ "description": "The file's digest after the write, or as it would be with --dry-run: what the caller passes as --expect to its next trust write."
93
+ },
94
+ "previousDigest": {
95
+ "$ref": "#/$defs/digest",
96
+ "description": "The file's digest before the write."
97
+ },
98
+ "paths": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "string"
102
+ },
103
+ "description": "The trust config when the write changed it or would with --dry-run; empty when it already held what was given."
104
+ },
105
+ "changed": {
106
+ "type": "boolean",
107
+ "description": "Whether the file changed: paths is not empty."
108
+ },
109
+ "dryRun": {
110
+ "type": "boolean",
111
+ "description": "True when --dry-run was given: nothing was written, and the document says what would have been."
112
+ },
113
+ "previous": {
114
+ "oneOf": [
115
+ {
116
+ "type": "null"
117
+ },
118
+ {
119
+ "$ref": "#/$defs/runner"
120
+ }
121
+ ],
122
+ "description": "The principal's runner entry before the write, or null when there was none."
123
+ },
124
+ "entry": {
125
+ "oneOf": [
126
+ {
127
+ "type": "null"
128
+ },
129
+ {
130
+ "$ref": "#/$defs/runner"
131
+ }
132
+ ],
133
+ "description": "The principal's runner entry after the write, or null after runner remove."
134
+ },
135
+ "runnerRole": {
136
+ "type": "boolean",
137
+ "description": "Whether the principal holds the runner role after the write. runner add grants it for class runner; runner remove takes it away."
138
+ }
139
+ }
140
+ },
141
+ "failure": {
142
+ "type": "object",
143
+ "description": "Nothing was written. Exit code 1.",
144
+ "required": [
145
+ "$schema",
146
+ "contract",
147
+ "chant",
148
+ "action",
149
+ "name",
150
+ "error"
151
+ ],
152
+ "properties": {
153
+ "$schema": {
154
+ "const": "https://intentius.io/chant/schemas/workspace/trust-write/v1/trust-write.schema.json"
155
+ },
156
+ "contract": {
157
+ "const": 1
158
+ },
159
+ "chant": {
160
+ "type": "string"
161
+ },
162
+ "action": {
163
+ "enum": [
164
+ "runner add",
165
+ "runner remove"
166
+ ]
167
+ },
168
+ "name": {
169
+ "type": [
170
+ "string",
171
+ "null"
172
+ ]
173
+ },
174
+ "error": {
175
+ "type": "object",
176
+ "required": [
177
+ "code",
178
+ "message"
179
+ ],
180
+ "properties": {
181
+ "code": {
182
+ "enum": [
183
+ "not-a-git-repository",
184
+ "write-usage-invalid",
185
+ "trust-config-invalid",
186
+ "trust-conflict",
187
+ "runner-key-invalid",
188
+ "runner-key-is-signer",
189
+ "runner-exists",
190
+ "runner-unknown",
191
+ "write-scope-member",
192
+ "write-scope-protected",
193
+ "write-scope-class-unknown",
194
+ "agent-unknown",
195
+ "principal-unidentified",
196
+ "write-lock-timeout",
197
+ "write-lock-not-held"
198
+ ]
199
+ },
200
+ "message": {
201
+ "type": "string"
202
+ }
203
+ }
204
+ },
205
+ "conflict": {
206
+ "type": "object",
207
+ "description": "With trust-conflict: the digest --expect named, the one the file has now, and its last chant write when the write journal knows it.",
208
+ "required": [
209
+ "path",
210
+ "expected",
211
+ "digest",
212
+ "lastWrite"
213
+ ],
214
+ "properties": {
215
+ "path": {
216
+ "type": "string"
217
+ },
218
+ "expected": {
219
+ "$ref": "#/$defs/digest"
220
+ },
221
+ "digest": {
222
+ "$ref": "#/$defs/digest"
223
+ },
224
+ "lastWrite": {
225
+ "oneOf": [
226
+ {
227
+ "type": "null"
228
+ },
229
+ {
230
+ "type": "object",
231
+ "required": [
232
+ "verb",
233
+ "by",
234
+ "agent",
235
+ "at"
236
+ ],
237
+ "properties": {
238
+ "verb": {
239
+ "type": "string"
240
+ },
241
+ "by": {
242
+ "type": [
243
+ "string",
244
+ "null"
245
+ ]
246
+ },
247
+ "agent": {
248
+ "type": [
249
+ "string",
250
+ "null"
251
+ ]
252
+ },
253
+ "at": {
254
+ "type": "string"
255
+ }
256
+ }
257
+ }
258
+ ]
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.124.29",
3
+ "version": "0.124.31",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -148,6 +148,21 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
148
148
  vi.restoreAllMocks();
149
149
  });
150
150
 
151
+ test("--pr-build and --pr-build-command become the prLoop build options (#4088)", async () => {
152
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "jobs: {}", stages: [], jobs: [] });
153
+ vi.spyOn(console, "log").mockImplementation(() => {});
154
+
155
+ const exit = await runBuild({
156
+ args: makeArgs({ generate: "github", prLoop: true, prBuild: true, prBuildCommand: "npm run build" }),
157
+ plugins: [],
158
+ serializers: [],
159
+ });
160
+
161
+ expect(exit).toBe(0);
162
+ expect(generateComponentsPipelineMock).toHaveBeenCalledWith(".", "github", { env: undefined, prLoop: { build: true, buildCommand: "npm run build" } }, undefined, []);
163
+ vi.restoreAllMocks();
164
+ });
165
+
151
166
  test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
152
167
  loadChantConfigUpwardMock.mockResolvedValue({
153
168
  config: { buildParams: { tier: { type: "string", default: "light" } } },
@@ -13,7 +13,8 @@ import type { PrLoopPipelineOptions } from "../../components/pr-pipeline";
13
13
  /**
14
14
  * The `prLoop` generator option from `--pr-loop`'s flags (#3183): the gate
15
15
  * and the branch, and with `--events-json` and `--report-url` (#4007) the
16
- * flags the generated jobs pass to pr-plan and pr-apply.
16
+ * flags the generated jobs pass to pr-plan and pr-apply. `--pr-build` and
17
+ * `--pr-build-command` (#4088) add the build and lint steps before both.
17
18
  */
18
19
  function prLoopOption(args: CommandContext["args"]): PrLoopPipelineOptions {
19
20
  return {
@@ -21,6 +22,8 @@ function prLoopOption(args: CommandContext["args"]): PrLoopPipelineOptions {
21
22
  ...(args.branch ? { branch: args.branch } : {}),
22
23
  ...(args.eventsJson ? { events: true } : {}),
23
24
  ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
25
+ ...(args.prBuild ? { build: true } : {}),
26
+ ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
24
27
  };
25
28
  }
26
29
 
@@ -84,6 +84,13 @@ let inputs: Record<string, Record<string, unknown>> = {};
84
84
  let stepOutputs: Record<string, Record<string, unknown>> = {};
85
85
  /** Called as a component's step starts, before it applies. */
86
86
  let onStart: (component: string) => void = () => {};
87
+ /**
88
+ * What each stack exposes in the deployed estate, read through the step's
89
+ * `outputs` as cfn-deploy's `describeStack` does (#4104). `undefined` leaves
90
+ * the capability with no `outputs`; an `Error` makes the read fail.
91
+ */
92
+ let liveOutputs: Record<string, Record<string, unknown> | Error> | undefined;
93
+ let outputsRead: string[] = [];
87
94
 
88
95
  function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
89
96
  return {
@@ -114,6 +121,8 @@ beforeEach(() => {
114
121
  inputs = {};
115
122
  stepOutputs = {};
116
123
  onStart = () => {};
124
+ liveOutputs = undefined;
125
+ outputsRead = [];
117
126
  gateLedger = memoryGateLedgerPort();
118
127
  stderrLines = [];
119
128
  stdoutLines = [];
@@ -149,6 +158,16 @@ beforeEach(() => {
149
158
  if (failing.includes(runCtx.component)) throw new Error(`${runCtx.component} failed`);
150
159
  return { ok: true, ...(stepOutputs[runCtx.component] ? { outputs: stepOutputs[runCtx.component] } : {}) };
151
160
  },
161
+ ...(liveOutputs
162
+ ? {
163
+ async outputs(readCtx: { component: string }) {
164
+ outputsRead.push(readCtx.component);
165
+ const live = liveOutputs![readCtx.component];
166
+ if (live instanceof Error) throw live;
167
+ return { ...(live ?? {}) };
168
+ },
169
+ }
170
+ : {}),
152
171
  } as never);
153
172
  return Promise.resolve(registry);
154
173
  });
@@ -849,3 +868,66 @@ describe("progress carries only for an unchanged input (#4081)", () => {
849
868
  expect(stderr()).toContain("does not record what each component applied, so none of its progress carries");
850
869
  });
851
870
  });
871
+
872
+ describe("a seed's outputs, read from the deployed estate (#4104)", () => {
873
+ /** cluster-a reads net's TopicArn, and the change touches cluster-a alone. */
874
+ function wireClusterA(): void {
875
+ const wired: Component = {
876
+ name: "cluster-a",
877
+ dependsOn: ["net"],
878
+ deploy: [
879
+ {
880
+ phase: "Apply",
881
+ steps: [{ kind: "cfn-deploy", stack: "cluster-a-stack", inputs: { topic: { stackOutput: { stack: "net", name: "TopicArn" } } } }],
882
+ },
883
+ ],
884
+ };
885
+ const estate = ESTATE.map((c) => (c.name === "cluster-a" ? wired : c));
886
+ discoverComponentsMock.mockResolvedValue({
887
+ components: new Map(estate.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
888
+ sourceFiles: [],
889
+ errors: [],
890
+ });
891
+ affectedStacksMock.mockResolvedValue({ changed: ["cluster-a-stack"], dependents: [], indeterminate: [] });
892
+ }
893
+
894
+ test("a cluster-a-only change runs cluster-a with net's deployed outputs, and says it read them", async () => {
895
+ wireClusterA();
896
+ liveOutputs = { net: { TopicArn: "arn:aws:sns:us-east-1:000000000000:net", QueueUrl: "http://q" } };
897
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(0);
898
+ expect(ran).not.toContain("net");
899
+ expect(outputsRead).toEqual(["net"]);
900
+ expect(inputs["cluster-a"]).toEqual(expect.objectContaining({ inputs: { topic: "arn:aws:sns:us-east-1:000000000000:net" } }));
901
+ expect(stderr()).toContain("net: outputs read from the deployed estate (QueueUrl, TopicArn)");
902
+ });
903
+
904
+ test("--seed-outputs wins: a seed it gives is not read", async () => {
905
+ wireClusterA();
906
+ liveOutputs = { net: { TopicArn: "arn:deployed" } };
907
+ const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
908
+ try {
909
+ const seed = join(dir, "seed.json");
910
+ await writeFile(seed, JSON.stringify({ net: { TopicArn: "arn:seeded" } }));
911
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", seedOutputs: [seed] }))).toBe(0);
912
+ expect(outputsRead).toEqual([]);
913
+ expect(inputs["cluster-a"]).toEqual(expect.objectContaining({ inputs: { topic: "arn:seeded" } }));
914
+ } finally {
915
+ await rm(dir, { recursive: true, force: true });
916
+ }
917
+ });
918
+
919
+ test("a read that fails stops the fan-out before anything runs, naming the seed", async () => {
920
+ wireClusterA();
921
+ liveOutputs = { net: new Error("ExpiredToken") };
922
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(1);
923
+ expect(ran).toEqual([]);
924
+ expect(stderr()).toContain("could not read the deployed outputs of net: ExpiredToken");
925
+ });
926
+
927
+ test("--dry-run reads nothing", async () => {
928
+ wireClusterA();
929
+ liveOutputs = { net: { TopicArn: "arn:deployed" } };
930
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test", dryRun: true }))).toBe(0);
931
+ expect(outputsRead).toEqual([]);
932
+ });
933
+ });
@@ -67,6 +67,8 @@ import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary
67
67
  import { approveCommand, gateIsSealed } from "../../op/gate";
68
68
  import { getHeadCommit } from "../../lifecycle/git";
69
69
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
70
+ import { readDeployedOutputs } from "../../components/wave-plan";
71
+ import { deployContext } from "../../components/driver";
70
72
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
71
73
  import { formatError, formatInfo, formatWarning } from "../format";
72
74
  import { FAN_OUT_GATE_OP } from "../../op/gate-name";
@@ -343,6 +345,36 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
343
345
  }
344
346
 
345
347
  const registry = await fanOutRegistry(projectPath, config);
348
+ const env = args.env ?? "local";
349
+
350
+ // A completed component's recorded outputs win over a `--seed-outputs`
351
+ // entry for the same name, as a fresh apply's outputs merge over a seed in
352
+ // the driver: the record holds what this fan-out itself applied. A seed
353
+ // neither gives is read from the deployed estate (#4104), through its
354
+ // capability's `outputs`, as pr-plan reads a dependency outside the change.
355
+ // One that cannot be read leaves its dependents' references unresolved.
356
+ const componentOutputs: Record<string, Record<string, unknown>> = { ...seededOutputs, ...priorOutputs };
357
+ const seeds = (progress ? remainingFanOut(derived.plan, derived.components, progress) : derived.plan).seeds;
358
+ let deployedSeeds: Record<string, string[]>;
359
+ try {
360
+ deployedSeeds = await readDeployedOutputs(
361
+ seeds,
362
+ new Map(derived.components.map((c) => [c.name, c])),
363
+ registry,
364
+ (name) => deployContext({ env }, name),
365
+ componentOutputs,
366
+ );
367
+ } catch (err) {
368
+ console.error(formatError({
369
+ message: `could not read the deployed outputs of ${seeds.filter((s) => !(s in componentOutputs)).join(", ")}: ${err instanceof Error ? err.message : String(err)}`,
370
+ hint: "Nothing ran. --seed-outputs <file> gives a component's outputs instead of reading them.",
371
+ }));
372
+ return 1;
373
+ }
374
+ for (const [name, keys] of Object.entries(deployedSeeds)) {
375
+ console.error(formatInfo(`${name}: outputs read from the deployed estate (${keys.length > 0 ? keys.join(", ") : "none yet"})`));
376
+ }
377
+
346
378
  // #3061: the commit this run deploys, for the workloads' vcs.ref.head.revision.
347
379
  const revision = await getHeadCommit().catch(() => undefined);
348
380
 
@@ -378,12 +410,9 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
378
410
  let result;
379
411
  try {
380
412
  result = await runFanOut(derived.plan, derived.components, registry, {
381
- env: args.env ?? "local",
413
+ env,
382
414
  releaseIdentity: () => (revision ? { revision } : {}),
383
- // A completed component's recorded outputs win over a `--seed-outputs`
384
- // entry for the same name, as a fresh apply's outputs merge over a seed in
385
- // the driver: the record holds what this fan-out itself applied.
386
- componentOutputs: { ...seededOutputs, ...priorOutputs },
415
+ componentOutputs,
387
416
  ...(gate ? { gate } : {}),
388
417
  ...(waveGate ? { waveGate } : {}),
389
418
  ...(waveGate
@@ -112,6 +112,11 @@ interface Prepared {
112
112
  head: string;
113
113
  gate: string;
114
114
  forge?: PrForge;
115
+ /**
116
+ * Every component in the project, not the selection (`deriveFanOut`), so
117
+ * the plan reads the deployed outputs of a dependency the change did not
118
+ * select (#4104).
119
+ */
115
120
  components: DriverComponent[];
116
121
  plan: FanOutPlan;
117
122
  signal: ComponentChangeSignal;
@@ -836,6 +836,7 @@ describe("workspace init and ls (#2534)", () => {
836
836
  "workspace runs",
837
837
  "workspace signers",
838
838
  "workspace status",
839
+ "workspace trust",
839
840
  "workspace upgrade",
840
841
  "workspace verify",
841
842
  "workspace versions",
package/src/cli/main.ts CHANGED
@@ -249,6 +249,56 @@ CI:
249
249
  chant/lifecycle that a runner key signed. Pushes the
250
250
  tags. --key, or the PEM in CHANT_RUNNER_KEY, signs
251
251
  each tag with a runner key.
252
+ ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>]
253
+ [--forge github|gitlab|forgejo] [--gate <name>] [--branch <name>]
254
+ [--events-json] [--report-url <url>] [--image <image>]
255
+ [--before-script <line>]... [--pr-build]
256
+ [--pr-build-command <line>] [--chant <command>]
257
+ [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>]
258
+ [-o <dir>]
259
+ [--no-status] [--status-url <url template>]
260
+ Run the pull-request pipeline's plan or apply job as
261
+ a pipeline Op in a fresh worktree of --commit (default
262
+ HEAD) with a clean environment. --base and --pr stand
263
+ in for the forge's event values. --pr-build runs
264
+ the job's build and lint lines first. Records the run as
265
+ pr-plan or pr-apply on chant/lifecycle, signed with
266
+ --key or CHANT_RUNNER_KEY, so ci tick from runs counts
267
+ it, and copies the report to -o (default .chant/pr). With a forge
268
+ token it sets a chant/pr-plan or chant/pr-apply commit
269
+ status (pending, then success or failure); --no-status
270
+ turns that off, and --status-url links it.
271
+ --secrets (or CHANT_CI_SECRETS) names a dotenv or
272
+ JSON file only its owner can read whose values the
273
+ job gets, and which are shown as *** in the events,
274
+ the record, the report and the output.
275
+ With --report-url it also adds the run to
276
+ <url>/runs/index.json, newest first, cut to the
277
+ newest --run-index-limit entries (default 200).
278
+ ci runs --report-url <url> [--json]
279
+ List the runs in the index at the report URL, newest
280
+ first.
281
+ ci serve --forge forgejo|github --repo <owner/repo> --env <env>
282
+ [--interval <s>] [--max-jobs <n>] [--state <dir>]
283
+ [--author <login>]... [--once] [--rerun-failed] [--unsigned]
284
+ [ci run's job flags]
285
+ Poll the forge for open pull requests and the target
286
+ branch's head, and run ci run plan for each new pull
287
+ request head and ci run apply for each new commit on
288
+ the branch. Dedupes on job and commit in a state file
289
+ under --state, takes a lease per apply group, keeps
290
+ each apply's resume record there, and runs at most
291
+ --max-jobs (default 1) jobs at once. --once runs one
292
+ poll and waits for its jobs. --rerun-failed runs a
293
+ failed apply or plan again. Refuses to start when its
294
+ runner key is not listed in .chant/trust.json at the
295
+ target's head, unless --unsigned
296
+ ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]
297
+ Make the host's runner key (Ed25519, mode 0600) in the
298
+ state directory when there is none and print its public
299
+ key. --trust commits its runner entry on a branch of its
300
+ own and pushes it for review; the target branch is never
301
+ written
252
302
  ci workflow [--workflow <name>]... [--output <file>] Write
253
303
  [--chant <command>] [--install <command>]
254
304
  [--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>]
@@ -674,6 +724,13 @@ Workspace (level 1, #2524):
674
724
  threshold of the old one. A commit an agent run made
675
725
  is attested by the run's signed statement; with
676
726
  attested-runs, only those commits must be
727
+ workspace trust runner add <principal> --key <ssh-ed25519 line or file> [--class runner|service] [--by <principal>] [--expect <digest>] [--dry-run]
728
+ workspace trust runner remove <principal> [--by <principal>] [--expect <digest>] [--dry-run]
729
+ List a runner's key under runners in .chant/trust.json,
730
+ granting a runner the runner role, or take both away,
731
+ editing the file in place. --expect refuses the write
732
+ when the file's digest has changed. Prints the
733
+ trust-write document and the new digest; never commits
677
734
  workspace signers [--json] [rotate [--threshold <n>] | sign --key <file>]
678
735
  Show the signer history at base, or propose the next
679
736
  signer set and sign it with a current signer's key
@@ -1306,6 +1363,11 @@ export const commandRegistry: CommandDef[] = [
1306
1363
  { name: "ci list", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiList(ctx) },
1307
1364
  { name: "ci tick", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiTick(ctx) },
1308
1365
  { name: "ci workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiWorkflow(ctx) },
1366
+ // #4094: one pull-request job as a pipeline Op in a fresh checkout, the chant runner's first slice.
1367
+ { name: "ci run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run")).runCiRun(ctx) },
1368
+ // #4098: the run index at the report URL.
1369
+ { name: "ci runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run-index")).runCiRuns(ctx) },
1370
+ { name: "ci serve", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-serve")).runCiServe(ctx) },
1309
1371
  // #4026: the release cut from a green commit (ws-104). Imported on first use.
1310
1372
  { name: "release cut", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCut(ctx) },
1311
1373
  // #4027: publish a cut release, to the targets #4047 declares.
@@ -1409,6 +1471,8 @@ export const commandRegistry: CommandDef[] = [
1409
1471
  // #2548 — an agent session's member, write scope and spec, for a session that resumes.
1410
1472
  { name: "workspace agent", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/agent-cli")).runWorkspaceAgent(ctx) },
1411
1473
  { name: "workspace verify", handler: async (ctx) => (await import("../workspace/trust/verify-cli")).runWorkspaceVerify(ctx) },
1474
+ // #3510 — a runner entry of .chant/trust.json, written through chant so a box repo never edits it by hand (ws-074).
1475
+ { name: "workspace trust", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/trust/trust-write")).runWorkspaceTrust(ctx) },
1412
1476
  { name: "workspace signers", handler: async (ctx) => (await import("../workspace/trust/signers-cli")).runWorkspaceSigners(ctx) },
1413
1477
  { name: "workspace evidence", handler: async (ctx) => (await import("../workspace/trust/evidence-cli")).runWorkspaceEvidence(ctx) },
1414
1478