@intentius/chant-lexicon-pulumi 0.124.54 → 0.124.55

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 (146) hide show
  1. package/README.md +42 -3
  2. package/dist/adopt.d.ts +68 -0
  3. package/dist/adopt.d.ts.map +1 -0
  4. package/dist/adopt.js +162 -0
  5. package/dist/capability-plugin.d.ts +3 -0
  6. package/dist/capability-plugin.d.ts.map +1 -1
  7. package/dist/capability-plugin.js +8 -1
  8. package/dist/change-set.d.ts +27 -4
  9. package/dist/change-set.d.ts.map +1 -1
  10. package/dist/change-set.js +15 -5
  11. package/dist/ci-setup.d.ts +53 -0
  12. package/dist/ci-setup.d.ts.map +1 -0
  13. package/dist/ci-setup.js +146 -0
  14. package/dist/composites/pulumi-adopt-op.d.ts +106 -0
  15. package/dist/composites/pulumi-adopt-op.d.ts.map +1 -0
  16. package/dist/composites/pulumi-adopt-op.js +132 -0
  17. package/dist/engine-events.d.ts +222 -0
  18. package/dist/engine-events.d.ts.map +1 -0
  19. package/dist/engine-events.js +280 -0
  20. package/dist/exit-codes.d.ts +53 -0
  21. package/dist/exit-codes.d.ts.map +1 -0
  22. package/dist/exit-codes.js +67 -0
  23. package/dist/index.d.ts +12 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +15 -2
  26. package/dist/init-templates.d.ts.map +1 -1
  27. package/dist/init-templates.js +5 -3
  28. package/dist/integrity.json +3 -3
  29. package/dist/lint/post-synth/pul015.d.ts +2 -1
  30. package/dist/lint/post-synth/pul015.d.ts.map +1 -1
  31. package/dist/lock-rows.d.ts +32 -0
  32. package/dist/lock-rows.d.ts.map +1 -0
  33. package/dist/lock-rows.js +36 -0
  34. package/dist/manifest.json +1 -1
  35. package/dist/member-engine.d.ts +27 -2
  36. package/dist/member-engine.d.ts.map +1 -1
  37. package/dist/member-engine.js +58 -7
  38. package/dist/member-outputs.d.ts.map +1 -1
  39. package/dist/member-outputs.js +4 -4
  40. package/dist/op/activities/index.d.ts +9 -1
  41. package/dist/op/activities/index.d.ts.map +1 -1
  42. package/dist/op/activities/index.js +8 -1
  43. package/dist/op/activities/pulumi-adopt.d.ts +104 -0
  44. package/dist/op/activities/pulumi-adopt.d.ts.map +1 -0
  45. package/dist/op/activities/pulumi-adopt.js +216 -0
  46. package/dist/op/activities/pulumi.d.ts.map +1 -1
  47. package/dist/op/activities/pulumi.js +2 -1
  48. package/dist/op/activity-contracts.d.ts +73 -0
  49. package/dist/op/activity-contracts.d.ts.map +1 -1
  50. package/dist/op/activity-contracts.js +49 -0
  51. package/dist/op/builders.d.ts +9 -0
  52. package/dist/op/builders.d.ts.map +1 -1
  53. package/dist/op/builders.js +20 -0
  54. package/dist/plan-shape.d.ts +47 -0
  55. package/dist/plan-shape.d.ts.map +1 -0
  56. package/dist/plan-shape.js +94 -0
  57. package/dist/plugin.d.ts.map +1 -1
  58. package/dist/plugin.js +25 -4
  59. package/dist/plunge-timings.d.ts +43 -0
  60. package/dist/plunge-timings.d.ts.map +1 -0
  61. package/dist/plunge-timings.js +106 -0
  62. package/dist/pulumi-up.d.ts +376 -0
  63. package/dist/pulumi-up.d.ts.map +1 -0
  64. package/dist/pulumi-up.js +823 -0
  65. package/dist/release-copy.d.ts +47 -0
  66. package/dist/release-copy.d.ts.map +1 -0
  67. package/dist/release-copy.js +131 -0
  68. package/dist/review-stack.d.ts +92 -0
  69. package/dist/review-stack.d.ts.map +1 -0
  70. package/dist/review-stack.js +222 -0
  71. package/dist/rules/pul015.ts +2 -1
  72. package/dist/run.d.ts +250 -7
  73. package/dist/run.d.ts.map +1 -1
  74. package/dist/run.js +432 -26
  75. package/dist/stack-export-snapshot.d.ts +125 -0
  76. package/dist/stack-export-snapshot.d.ts.map +1 -0
  77. package/dist/stack-export-snapshot.js +233 -0
  78. package/dist/stack-ops.d.ts +175 -0
  79. package/dist/stack-ops.d.ts.map +1 -0
  80. package/dist/stack-ops.js +400 -0
  81. package/dist/stack-references.d.ts +46 -0
  82. package/dist/stack-references.d.ts.map +1 -0
  83. package/dist/stack-references.js +88 -0
  84. package/dist/trace-facts.d.ts +34 -0
  85. package/dist/trace-facts.d.ts.map +1 -0
  86. package/dist/trace-facts.js +81 -0
  87. package/dist/workspace-member.d.ts +13 -2
  88. package/dist/workspace-member.d.ts.map +1 -1
  89. package/dist/workspace-member.js +14 -9
  90. package/package.json +2 -2
  91. package/src/adopt.ts +202 -0
  92. package/src/capability-plugin.ts +15 -6
  93. package/src/change-set.ts +29 -5
  94. package/src/ci-setup.test.ts +123 -0
  95. package/src/ci-setup.ts +176 -0
  96. package/src/composites/pulumi-adopt-op.test.ts +223 -0
  97. package/src/composites/pulumi-adopt-op.ts +191 -0
  98. package/src/composites/pulumi-watch-op.test.ts +2 -2
  99. package/src/engine-events.test.ts +134 -0
  100. package/src/engine-events.ts +390 -0
  101. package/src/engine-graph.test.ts +1 -1
  102. package/src/exit-codes.test.ts +21 -0
  103. package/src/exit-codes.ts +71 -0
  104. package/src/index.ts +75 -1
  105. package/src/init-templates.ts +5 -3
  106. package/src/lint/post-synth/pul015.ts +2 -1
  107. package/src/lock-rows.test.ts +60 -0
  108. package/src/lock-rows.ts +56 -0
  109. package/src/member-engine.test.ts +112 -0
  110. package/src/member-engine.ts +73 -6
  111. package/src/member-outputs.test.ts +2 -2
  112. package/src/member-outputs.ts +9 -8
  113. package/src/op/activities/index.ts +19 -1
  114. package/src/op/activities/pulumi-adopt.ts +334 -0
  115. package/src/op/activities/pulumi.ts +2 -1
  116. package/src/op/activity-contracts.ts +75 -0
  117. package/src/op/builders.ts +37 -0
  118. package/src/plan-shape.test.ts +91 -0
  119. package/src/plan-shape.ts +105 -0
  120. package/src/plugin.test.ts +1 -1
  121. package/src/plugin.ts +28 -4
  122. package/src/plunge-timings.test.ts +241 -0
  123. package/src/plunge-timings.ts +115 -0
  124. package/src/plunge-up-failed.test.ts +54 -0
  125. package/src/pr-note.test.ts +87 -0
  126. package/src/pulumi-up.test.ts +470 -0
  127. package/src/pulumi-up.ts +1032 -0
  128. package/src/release-copy.test.ts +74 -0
  129. package/src/release-copy.ts +142 -0
  130. package/src/review-stack.test.ts +254 -0
  131. package/src/review-stack.ts +266 -0
  132. package/src/run.test.ts +176 -3
  133. package/src/run.ts +501 -30
  134. package/src/stack-export-snapshot.test.ts +190 -0
  135. package/src/stack-export-snapshot.ts +312 -0
  136. package/src/stack-ops.test.ts +286 -0
  137. package/src/stack-ops.ts +552 -0
  138. package/src/stack-references.test.ts +96 -0
  139. package/src/stack-references.ts +110 -0
  140. package/src/testdata/pulumi-stream/README.md +16 -0
  141. package/src/testdata/pulumi-stream/preview-stream.ndjson +42 -0
  142. package/src/testdata/pulumi-stream/up-failed-plunge.ndjson +5 -0
  143. package/src/trace-facts.test.ts +38 -0
  144. package/src/trace-facts.ts +83 -0
  145. package/src/workspace-member.test.ts +2 -1
  146. package/src/workspace-member.ts +30 -12
@@ -0,0 +1,823 @@
1
+ /**
2
+ * `pulumi-up`, the pulumi lexicon's apply capability (#4541): a Pulumi or
3
+ * plunge stack as a component step that plans and applies, the way
4
+ * `terraform-apply` does a root. Ruling 1 on INTENTIUS/plywood#1 widened
5
+ * #4450 and #4476 so chant runs `up`, from an apply job holding the apply
6
+ * identity only; the argv go through `runApplyPulumi` and `runApplyPlunge`
7
+ * (`./run.ts`), each one fixed form.
8
+ *
9
+ * The step is `{ kind: "pulumi-up", dir, stack }`, with optional `member`,
10
+ * `config`, `engine`, `cwd`, `lockGrain` and `policyPacks`. One kind covers
11
+ * both engines; `pulumiMemberEngine` (`./member-engine.ts`, #4559) picks
12
+ * the binary from the step's `engine`, `plywood.yml`, `PULUMI_BACKEND_URL`
13
+ * or `Pulumi.yaml`, and the artifact records which rule answered.
14
+ *
15
+ * ## plan
16
+ *
17
+ * The preview runs with `PULUMI_ENABLE_STREAMING_JSON_PREVIEW=true` (ruling
18
+ * C), so one engine run gives the change set and CrossGuard's verdict
19
+ * (`./engine-events.ts`). On plunge it also writes `--save-plan` into a
20
+ * scratch directory under the OS temp dir, never the working tree, since the
21
+ * file holds secret ciphertext. The plan answers:
22
+ *
23
+ * - `member`: `<member>/<stack>`, the name in the wave's set digest;
24
+ * - `planDigest` and `changeSet` from `pulumiPreviewPart`; for a member on
25
+ * the resource lock grain (the step's `lockGrain`, else `ci.lockGrain`
26
+ * through `DeployContext.lockGrain`, else `resource` on plunge and
27
+ * `target` on stock Pulumi, #4552) the digest leaves `same` and `read`
28
+ * steps out (ruling B);
29
+ * - `outputs` from the stack step's planned outputs;
30
+ * - `shape` from `pulumiPlanShape` (`./plan-shape.ts`);
31
+ * - `artifact`: the engine, the shape, the digest, CrossGuard's findings,
32
+ * and on plunge the plan file.
33
+ *
34
+ * A mandatory CrossGuard violation, or a preview Pulumi fails with its
35
+ * policy code, throws {@link PulumiPolicyDeniedError}: no gate opens. Any
36
+ * other failed preview throws, naming the engine's error.
37
+ *
38
+ * With `DeployContext.unresolvedInputs` set the member is a hole naming the
39
+ * upstreams it waits on, as `terraform-apply`'s `waitingRoot` is. A stand-in
40
+ * preview is later work.
41
+ *
42
+ * ## run
43
+ *
44
+ * - plunge, with the plan's artifact: `plunge up --plan <file> --yes --json
45
+ * --stack <s>`. plunge checks the plan's stamp and refuses a plan whose
46
+ * records moved, or one with no stamp, before anything changes (exit 40),
47
+ * which throws {@link PulumiPlanMovedError}: the table's moved code, 4.
48
+ * - stock Pulumi, with the artifact: preview again and throw core's
49
+ * `PlanShapeMovedError` when the fresh shape goes beyond the approved one;
50
+ * then `pulumi up --yes --json --stack <s>`, and compare the steps in its
51
+ * event stream with the approved shape (ruling A). A difference fails the
52
+ * step with {@link PulumiAppliedShapeError}, whose message the apply
53
+ * record keeps. It is failed, never moved: resources changed.
54
+ * - no approved plan (`chant run --components`, a release promote, a fan-out
55
+ * with no wave gate): preview and `up` in one go (ruling D), as
56
+ * `terraform-apply` does with no plan.
57
+ *
58
+ * Every engine exit goes through `pulumiExitOf` (`./exit-codes.ts`, #4553):
59
+ * plunge's 40 is moved, Pulumi's policy code is denied, anything else is
60
+ * failed with the stream's error and the URNs that landed.
61
+ *
62
+ * After `up`, the stack's outputs come from `stack output --json
63
+ * --show-secrets=false`, secrets as `[secret]`. Then `stack export --stack
64
+ * <s>` runs, its secrets are stripped, and it goes to the index store as
65
+ * the member's snapshot (#4546, `./stack-export-snapshot.ts`), at
66
+ * `<env>/<stack>__pulumi-export` on `refs/chant/snapshots` by default. The
67
+ * graph and the member's reads take it from there, so the member needs no
68
+ * checked-in `stack-export.json`. A failed export or write leaves the apply
69
+ * standing and says why in `exportSnapshot`.
70
+ *
71
+ * ## policy packs at base (#4613)
72
+ *
73
+ * Under a pull request or an op wave the caller sets
74
+ * `DeployContext.policyBase`, and the plan copies its CrossGuard packs out
75
+ * of that commit into the step's scratch directory before the preview
76
+ * (`@intentius/chant/policy-packs`): every `ci.policy.packs` entry of the
77
+ * declaration at base (or of `ci.policy.source`), then each step pack that
78
+ * is in the repository. A step pack the base lacks, or one that differs on
79
+ * the head, is a note on stderr and in the artifact; the base's copy is the
80
+ * one that runs. `run` hands `up` the same copies. A base or source that
81
+ * cannot be read throws, so no gate opens on a plan no policy judged.
82
+ *
83
+ * ## config
84
+ *
85
+ * `config` (a dependent's `stackOutput()` wiring) is written into a scratch
86
+ * copy of `Pulumi.<stack>.yaml` and passed with `--config-file`; `--config`
87
+ * is never used, since it saves into the working tree's file. A key with no
88
+ * `:` is the project's own (`<project>:<key>`).
89
+ *
90
+ * ## StackReference (#4547)
91
+ *
92
+ * `declaredReads` reads each step's export for its `StackReference`s
93
+ * (`./stack-references.ts`) and reports each as a read of the other
94
+ * `pulumi-up` step that applies the referenced stack, so core orders the
95
+ * reader after it, holds the reader when it fails, and leaves the reader a
96
+ * hole while the upstream's plan cannot say an output it reads. A reference
97
+ * to a stack no step applies adds no edge and is named on the plan's
98
+ * artifact under `stackReferences.outside`.
99
+ *
100
+ * ## releases (#4542)
101
+ *
102
+ * `releaseInputs` answers the stack's directory, so a release cut records it
103
+ * and a promote or rollback under the release previews and applies the
104
+ * recorded copy, under the release gate with that copy's plan. A stack whose
105
+ * backend is a file path relative to its directory is unbound: a copy
106
+ * elsewhere would read no state.
107
+ *
108
+ * The cut records tracked files, so a Node program's copy has no
109
+ * `node_modules`. Before the copy's first preview, `plan` and `run` call
110
+ * `prepareReleaseCopy` (`./release-copy.ts`), which runs `pulumi install`
111
+ * (`plunge install`) in the copy when its runtime needs one, and refuses a
112
+ * Node copy with no lockfile by name, since an install would take today's
113
+ * versions rather than the release's.
114
+ *
115
+ * ## review stacks (#4609)
116
+ *
117
+ * When `DeployContext.env` is a pull request's environment `pr-<n>` (`chant
118
+ * components pr-env up`), the step applies its review stack instead of its
119
+ * own: the step's stack with the last segment replaced by `pr-<n>` (or
120
+ * `pr-<n>-<reviewSuffix>`), so `acme/shop/dev` becomes `acme/shop/pr-12`.
121
+ * Before the first preview, `plan` runs `stack init <pr-stack> --no-select
122
+ * --copy-config-from <template>` (`initReviewStack`, `./review-stack.ts`),
123
+ * where the template is `CHANT_ENV_TEMPLATE`'s stack when it names a stack of
124
+ * this project (`<project>/<stack>`, plywood's `review_stacks.template`),
125
+ * else the step's own stack. A stack that exists already is updated. The
126
+ * plan gives core the stack's `location`, `<engine>:<dir>#<stack>`, which
127
+ * `pr-env up` keeps on the environment's record and `pr-env down` and the
128
+ * TTL sweep read back to destroy and remove it (`reviewStackTeardown`).
129
+ */
130
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
131
+ import { tmpdir } from "node:os";
132
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
133
+ import { PlanShapeMovedError } from "@intentius/chant/components/covered-round";
134
+ import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
135
+ import { parseYAML } from "@intentius/chant/yaml";
136
+ import { pulumiPreviewPart } from "./change-set.js";
137
+ import { PULUMI_STREAMING_PREVIEW_ENV, pulumiAppliedSteps, pulumiFailedUrns, pulumiLandedUrns, pulumiPolicyEvents, pulumiPolicyFindings, pulumiStreamError, pulumiStreamPreview, pulumiStreamSummary, pulumiStreamTimings, } from "./engine-events.js";
138
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
139
+ import { pulumiExitOf } from "./exit-codes.js";
140
+ import { pulumiMemberEngine } from "./member-engine.js";
141
+ import { ciEngineDir, ciRelease, CI_ENGINE_ROOT } from "./ci-setup.js";
142
+ import { pulumiMemberPlannedOutputs } from "./member-outputs.js";
143
+ import { pulumiPlanShape, pulumiShapeDifferences } from "./plan-shape.js";
144
+ import { pulumiLockRows } from "./lock-rows.js";
145
+ import { pulumiTraceFacts } from "./trace-facts.js";
146
+ import { defaultPlungeRunner, defaultPulumiRunner, engineRunnerAt, plungeUpArgs, pulumiMemberStackOutputs, pulumiRunFailure, pulumiUpArgs, pulumiUpPreviewArgs, memberExportOf, runAllowedPlunge, runAllowedPulumi, runApplyPlunge, runApplyPulumi, } from "./run.js";
147
+ import { writePulumiExportSnapshot } from "./stack-export-snapshot.js";
148
+ import { pointerSegment, pulumiStackReferences, stackReferenceOwner } from "./stack-references.js";
149
+ import { isObject } from "./values.js";
150
+ import { prepareReleaseCopy } from "./release-copy.js";
151
+ import { initReviewStack, reviewConfigSource, reviewStackFor, reviewStackLocation } from "./review-stack.js";
152
+ /**
153
+ * The packs a preview runs (#4613). With `ctx.policyBase` each step pack in
154
+ * the repository and every `ci.policy.packs` entry is copied from base into
155
+ * `scratch`, and the notes say which step packs did not run as the head has
156
+ * them. Without it, the step's packs as they stand. Throws when the base or
157
+ * the policy source cannot be read: the preview then does not run.
158
+ */
159
+ async function policyPacksFor(ctx, r, input, scratch) {
160
+ const own = input.policyPacks ?? [];
161
+ if (!ctx.policyBase)
162
+ return { packs: own };
163
+ let mod;
164
+ try {
165
+ mod = await import("@intentius/chant/policy-packs");
166
+ }
167
+ catch {
168
+ mod = undefined;
169
+ }
170
+ if (!mod)
171
+ throw new Error(`pulumi-up: "${r.member}": the policy packs are read at base ${ctx.policyBase.slice(0, 12)}, and this chant has no reader for them; update @intentius/chant`);
172
+ const got = await mod.policyPacksAtBase({ cwd: r.dir, base: ctx.policyBase, stepPacks: own, tmp: scratch });
173
+ if ("error" in got)
174
+ throw new Error(`pulumi-up: "${r.member}": ${got.error}, so the preview did not run`);
175
+ for (const note of got.notes)
176
+ console.error(`pulumi-up: "${r.member}": ${note}`);
177
+ if (got.source)
178
+ console.error(`pulumi-up: "${r.member}": ci.policy.packs read from ${got.source.repo}@${got.source.ref} at commit ${got.source.commit}`);
179
+ const read = {
180
+ base: got.base,
181
+ packs: got.packs.map((p) => ({ dir: p.dir, ...(p.config ? { config: p.config } : {}), from: p.from, name: p.name })),
182
+ notes: got.notes,
183
+ ...(got.source ? { source: { ...got.source } } : {}),
184
+ };
185
+ return { packs: got.packs.map((p) => ({ dir: p.dir, ...(p.config ? { config: p.config } : {}) })), read };
186
+ }
187
+ /** plunge refused a saved plan whose records moved, or one with no stamp (exit 40). `planMoved` is core's moved mark. */
188
+ export class PulumiPlanMovedError extends Error {
189
+ constructor(member, message) {
190
+ super(message);
191
+ this.member = member;
192
+ this.planMoved = true;
193
+ this.name = "PulumiPlanMovedError";
194
+ }
195
+ }
196
+ /** CrossGuard denied the plan: a mandatory violation, or the engine's policy exit. No gate opens. */
197
+ export class PulumiPolicyDeniedError extends Error {
198
+ constructor(member, findings, detail) {
199
+ const denied = findings.denials.map((d) => `${d.rule}: ${d.message}`).join("; ");
200
+ super(`"${member}": CrossGuard denied the plan${denied ? `: ${denied}` : ""}${detail ? ` (${detail})` : ""}`);
201
+ this.member = member;
202
+ this.findings = findings;
203
+ this.policyDenied = true;
204
+ /** The table's code for it (#4553, ruling B: 7 everywhere). */
205
+ this.exitCode = CHANT_EXIT.denied;
206
+ this.name = "PulumiPolicyDeniedError";
207
+ }
208
+ }
209
+ /**
210
+ * A stock `pulumi up` ran steps the approved shape does not hold (ruling A).
211
+ * The update has run, so this is a failure, not a moved plan: the step fails
212
+ * and the differences land on the apply record through the message.
213
+ */
214
+ export class PulumiAppliedShapeError extends Error {
215
+ constructor(member, differences, summary) {
216
+ super(`"${member}": pulumi up ran steps the approved plan did not hold: ${differences.join("; ")}. The update ran; check the stack and plan it again.`);
217
+ this.member = member;
218
+ this.differences = differences;
219
+ this.summary = summary;
220
+ this.name = "PulumiAppliedShapeError";
221
+ }
222
+ }
223
+ /**
224
+ * An `up` that failed (#4553's failed outcome). It carries the stream's
225
+ * summary, plunge's per-resource timings included (#4579), so the run record
226
+ * and the trace (`./trace-facts.ts` reads `member` and `summary` off the
227
+ * error) keep what ran before it stopped.
228
+ */
229
+ export class PulumiUpFailedError extends Error {
230
+ constructor(member, message, summary) {
231
+ super(message);
232
+ this.member = member;
233
+ this.summary = summary;
234
+ this.name = "PulumiUpFailedError";
235
+ }
236
+ }
237
+ /** A stock Pulumi member set to the resource grain (#4552): the backend's stack lock already orders its updates. */
238
+ export class PulumiResourceGrainRefusedError extends Error {
239
+ constructor(member) {
240
+ super(`"${member}": the resource lock grain needs plunge; a stock Pulumi stack keeps the stack grain, since its backend's stack lock orders updates (ci.lockGrain: target)`);
241
+ this.member = member;
242
+ this.name = "PulumiResourceGrainRefusedError";
243
+ }
244
+ }
245
+ /** The default lock grain for an engine (#4552's design): resource on plunge, the stack (target) on stock Pulumi. */
246
+ export function pulumiDefaultLockGrain(engine) {
247
+ return engine === "plunge" ? "resource" : "target";
248
+ }
249
+ const STREAMING = { [PULUMI_STREAMING_PREVIEW_ENV]: "true" };
250
+ function stackDir(input) {
251
+ return isAbsolute(input.dir) ? input.dir : resolve(input.cwd ?? process.cwd(), input.dir);
252
+ }
253
+ function readProject(dir) {
254
+ for (const name of ["Pulumi.yaml", "Pulumi.yml"]) {
255
+ const file = join(dir, name);
256
+ if (!existsSync(file))
257
+ continue;
258
+ try {
259
+ return parseYAML(readFileSync(file, "utf-8"));
260
+ }
261
+ catch {
262
+ return undefined;
263
+ }
264
+ }
265
+ return undefined;
266
+ }
267
+ function requireInput(input) {
268
+ if (typeof input?.dir !== "string" || input.dir === "")
269
+ throw new Error('pulumi-up: the step needs "dir", the directory holding Pulumi.yaml');
270
+ if (typeof input.stack !== "string" || input.stack === "" || input.stack.startsWith("-"))
271
+ throw new Error('pulumi-up: the step needs "stack", the stack to apply');
272
+ }
273
+ /** `<member>/<stack>`: the member's name in the wave's set digest. */
274
+ export function pulumiUpMember(input) {
275
+ const dir = stackDir(input);
276
+ const named = input.member ?? readProject(dir)?.name ?? basename(dir);
277
+ return `${named}/${input.stack}`;
278
+ }
279
+ /**
280
+ * The stack's config with `config` written over it, as the text of a
281
+ * `Pulumi.<stack>.yaml` (JSON, which YAML reads). A key with no `:` is the
282
+ * project's own. A value left `undefined` or `null` is not written.
283
+ */
284
+ export function pulumiScratchConfig(dir, stack, config) {
285
+ const project = readProject(dir)?.name;
286
+ const file = join(dir, `Pulumi.${stack}.yaml`);
287
+ let doc = {};
288
+ if (existsSync(file)) {
289
+ try {
290
+ doc = parseYAML(readFileSync(file, "utf-8")) ?? {};
291
+ }
292
+ catch (err) {
293
+ throw new Error(`pulumi-up: ${file} does not read as YAML, so its config cannot be copied: ${err instanceof Error ? err.message : String(err)}`);
294
+ }
295
+ }
296
+ const merged = { ...(doc.config ?? {}) };
297
+ for (const [key, value] of Object.entries(config)) {
298
+ if (value === undefined || value === null)
299
+ continue;
300
+ const full = key.includes(":") || typeof project !== "string" ? key : `${project}:${key}`;
301
+ merged[full] = value;
302
+ }
303
+ return `${JSON.stringify({ ...doc, config: merged }, null, 2)}\n`;
304
+ }
305
+ /** The variable `pr-env up` sets to `ci.prEnvironment.template` (core's `PR_ENV_TEMPLATE_VAR`). */
306
+ export const PR_ENV_TEMPLATE_VARIABLE = "CHANT_ENV_TEMPLATE";
307
+ /**
308
+ * The step as it applies in `env` (#4609): with its review stack in place of
309
+ * its own stack in a pull request's environment, and the stack it copies its
310
+ * config from. The step unchanged, with no review, anywhere else.
311
+ */
312
+ export function pulumiReviewStep(input, env, template) {
313
+ requireInput(input);
314
+ const stack = reviewStackFor(input.stack, env, input.reviewSuffix);
315
+ if (!stack)
316
+ return { step: input };
317
+ const project = readProject(stackDir(input))?.name;
318
+ const copyConfigFrom = reviewConfigSource({ stack: input.stack, ...(typeof project === "string" ? { project } : {}) }, template, stack);
319
+ return { step: { ...input, stack }, review: { stack, ...(copyConfigFrom ? { copyConfigFrom } : {}) } };
320
+ }
321
+ function templateOf(opts) {
322
+ const v = (opts.env ?? process.env)[PR_ENV_TEMPLATE_VARIABLE];
323
+ return typeof v === "string" && v !== "" ? v : undefined;
324
+ }
325
+ /** The review stack's location on the environment's record: `<engine>:<dir>#<stack>`, `dir` relative to the working directory core runs in. */
326
+ function locationOf(r, stack) {
327
+ return reviewStackLocation(r.engine, relative(process.cwd(), r.dir) || ".", stack);
328
+ }
329
+ /** A plan file name with nothing a path could read into. */
330
+ const fileSafe = (s) => s.replace(/[^A-Za-z0-9._-]+/g, "_");
331
+ function resolveStep(input, opts, declared) {
332
+ requireInput(input);
333
+ const dir = stackDir(input);
334
+ const member = pulumiUpMember(input);
335
+ const picked = pulumiMemberEngine(dir, input.engine, opts.env ? { env: opts.env } : {});
336
+ // #4552: the step's own grain, else ci.lockGrain as the caller read it at base, else the engine's default.
337
+ const grain = input.lockGrain ?? declared ?? pulumiDefaultLockGrain(picked.engine);
338
+ if (grain === "resource" && picked.engine === "pulumi")
339
+ throw new PulumiResourceGrainRefusedError(member);
340
+ const engineBin = pulumiUpEngineBin(input, picked.engine);
341
+ return { dir, member, engine: picked.engine, ...(engineBin ? { engineBin } : {}), engineSource: picked.source, engineBecause: picked.because, grain };
342
+ }
343
+ /**
344
+ * The binary a step pinned with `engineVersion` runs (#4592): the versioned
345
+ * path the install lines write. Undefined without `engineVersion`, so the
346
+ * binary on PATH runs. A version or root a shell or path would read as
347
+ * more is refused, as the install lines refuse it.
348
+ */
349
+ export function pulumiUpEngineBin(input, engine) {
350
+ if (input.engineVersion === undefined) {
351
+ if (input.engineRoot !== undefined)
352
+ throw new Error('pulumi-up: "engineRoot" needs "engineVersion", the release installed under it');
353
+ return undefined;
354
+ }
355
+ try {
356
+ ciRelease(engine, input.engineVersion);
357
+ return `${ciEngineDir(engine, input.engineVersion, input.engineRoot ?? CI_ENGINE_ROOT)}/${engine}`;
358
+ }
359
+ catch (err) {
360
+ throw new Error(`pulumi-up: ${err instanceof Error ? err.message : String(err)}`);
361
+ }
362
+ }
363
+ /** The `run` and `plunge` options for the stack output and export reads: the pinned binary's for both when the step names one (#4592), else the caller's. */
364
+ function runnerOptions(r, opts) {
365
+ if (r.engineBin) {
366
+ const { pulumi, plunge } = runnersFor(r, opts);
367
+ return { run: pulumi, plunge };
368
+ }
369
+ return { ...(opts.run ? { run: opts.run } : {}), ...(opts.plunge ? { plunge: opts.plunge } : {}) };
370
+ }
371
+ /** The runners a resolved step uses: the pinned binary's when it names one, else the options' or the PATH default. */
372
+ function runnersFor(r, opts) {
373
+ if (r.engineBin) {
374
+ const pinned = opts.runAt ? opts.runAt(r.engineBin) : engineRunnerAt(r.engineBin);
375
+ return { pulumi: pinned, plunge: pinned };
376
+ }
377
+ return { pulumi: opts.run ?? defaultPulumiRunner, plunge: opts.plunge ?? defaultPlungeRunner };
378
+ }
379
+ /** The scratch directory, the config copy in it and plunge's plan file path. */
380
+ function scratchFor(r, input, opts) {
381
+ const scratch = mkdtempSync(join(opts.tmp ?? tmpdir(), "chant-pulumi-up-"));
382
+ let configFile;
383
+ if (input.config && Object.keys(input.config).length > 0) {
384
+ configFile = join(scratch, `Pulumi.${input.stack}.yaml`);
385
+ writeFileSync(configFile, pulumiScratchConfig(r.dir, input.stack, input.config), { mode: 0o600 });
386
+ }
387
+ const planFile = r.engine === "plunge" ? join(scratch, `${fileSafe(r.member)}.plan.json`) : undefined;
388
+ return { scratch, ...(configFile ? { configFile } : {}), ...(planFile ? { planFile } : {}) };
389
+ }
390
+ function dropScratch(scratch) {
391
+ if (!scratch)
392
+ return;
393
+ try {
394
+ rmSync(scratch, { recursive: true, force: true });
395
+ }
396
+ catch {
397
+ // A scratch dir that stays behind holds nothing a later run reads.
398
+ }
399
+ }
400
+ /** The streamed preview, as part, shape and CrossGuard findings. Throws for a failed preview or a denial. */
401
+ async function previewStack(ctx, r, input, opts, files) {
402
+ const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
403
+ // #4613: the packs read at base when the caller gave them, else the step's as they stand.
404
+ const packs = files.policyPacks ?? input.policyPacks ?? [];
405
+ const args = pulumiUpPreviewArgs({
406
+ stack: input.stack,
407
+ ...(files.planFile ? { savePlan: files.planFile } : {}),
408
+ ...(files.configFile ? { configFile: files.configFile } : {}),
409
+ ...(targets ? { targets } : {}),
410
+ ...(packs.length ? { policyPacks: packs } : {}),
411
+ });
412
+ let stdout;
413
+ try {
414
+ ({ stdout } =
415
+ r.engine === "plunge"
416
+ ? await runAllowedPlunge(runnersFor(r, opts).plunge, args, r.dir, STREAMING)
417
+ : await runAllowedPulumi(runnersFor(r, opts).pulumi, args, r.dir, STREAMING));
418
+ }
419
+ catch (err) {
420
+ const f = pulumiRunFailure(err);
421
+ if (f.missingBinary)
422
+ throw new Error(`pulumi-up: "${r.member}": the ${r.engine} binary was not found on PATH; install ${r.engine} to plan this stack`);
423
+ const stream = f.stdout ?? "";
424
+ const policy = pulumiPolicyFindings(pulumiPolicyEvents(stream));
425
+ if (policy.denials.length > 0 || pulumiExitOf(f.exitCode) === CHANT_EXIT.denied)
426
+ throw new PulumiPolicyDeniedError(r.member, policy, pulumiStreamError(stream));
427
+ const why = pulumiStreamError(stream) ?? (f.stderr?.trim() || f.message);
428
+ throw new Error(`pulumi-up: "${r.member}": ${r.engine} preview failed: ${why}`);
429
+ }
430
+ const preview = pulumiStreamPreview(stdout);
431
+ const policy = pulumiPolicyFindings(pulumiPolicyEvents(stdout));
432
+ // A mandatory violation that left the exit at 0 still denies: the gate reads the events, never the code alone.
433
+ if (policy.denials.length > 0)
434
+ throw new PulumiPolicyDeniedError(r.member, policy);
435
+ const prior = await memberExportOf(r.dir, { stack: input.stack, ...(opts.snapshots !== undefined ? { store: opts.snapshots } : {}) });
436
+ const part = pulumiPreviewPart({
437
+ member: r.member,
438
+ preview,
439
+ stack: input.stack,
440
+ ...(prior !== undefined ? { prior } : {}),
441
+ ...(targets ? { targeted: true } : {}),
442
+ ...(r.grain === "resource" ? { digest: { changedOnly: true } } : {}),
443
+ });
444
+ const shape = pulumiPlanShape(part, r.member);
445
+ return { part, preview, shape, policy, prior };
446
+ }
447
+ /** The member of a stack whose `config` or `StackReference`s read outputs not known yet: a hole naming them, no plan. */
448
+ function waitingStack(ctx, r, input, unresolved) {
449
+ const config = Object.fromEntries(Object.entries(input.config ?? {}).map(([k, v]) => [k, v === undefined ? null : v]));
450
+ const planDigest = computePlanDigest("pulumi-up-unresolved", { env: ctx.env, member: r.member, stack: input.stack, config, unresolved });
451
+ const changeSet = {
452
+ member: {
453
+ member: r.member,
454
+ lexicon: "pulumi",
455
+ planner: "pulumi",
456
+ scope: input.stack,
457
+ status: "planned",
458
+ planDigest,
459
+ holes: [
460
+ {
461
+ address: r.member,
462
+ type: "pulumi:pulumi:Stack",
463
+ reason: `the stack reads ${unresolved.join(", ")}, known only once ${unresolved.length === 1 ? "that applies" : "those apply"}; it is planned again then`,
464
+ },
465
+ ],
466
+ },
467
+ entries: [],
468
+ };
469
+ return { member: r.member, planDigest, changeSet, ...(ctx.targets?.length ? { targets: ctx.targets } : {}) };
470
+ }
471
+ function isArtifact(value) {
472
+ const v = value;
473
+ return v?.kind === "pulumi-up" && typeof v.planDigest === "string" && typeof v.shape === "object" && v.shape !== null;
474
+ }
475
+ /** A release's copy of the program, made runnable before its first engine run (#4542). */
476
+ async function readyCopy(r, opts) {
477
+ const runners = runnersFor(r, opts);
478
+ await prepareReleaseCopy({ dir: r.dir, member: r.member, engine: r.engine, run: r.engine === "plunge" ? runners.plunge : runners.pulumi });
479
+ }
480
+ async function planStack(ctx, original, opts) {
481
+ // #4609: in pr-<n>, the review stack, made from the template before its first preview.
482
+ const { step: input, review } = pulumiReviewStep(original, ctx.env, templateOf(opts));
483
+ const r = resolveStep(input, opts, ctx.lockGrain);
484
+ const location = review ? { location: locationOf(r, review.stack) } : {};
485
+ const unresolved = [...(ctx.unresolvedInputs ?? [])].sort();
486
+ if (unresolved.length > 0)
487
+ return { ...waitingStack(ctx, r, input, unresolved), ...location };
488
+ await readyCopy(r, opts);
489
+ if (review) {
490
+ const runners = runnersFor(r, opts);
491
+ await initReviewStack({
492
+ dir: r.dir,
493
+ stack: review.stack,
494
+ engine: r.engine,
495
+ run: r.engine === "plunge" ? runners.plunge : runners.pulumi,
496
+ ...(review.copyConfigFrom ? { template: review.copyConfigFrom } : {}),
497
+ });
498
+ }
499
+ const files = scratchFor(r, input, opts);
500
+ let read;
501
+ let packs;
502
+ try {
503
+ packs = await policyPacksFor(ctx, r, input, files.scratch);
504
+ read = await previewStack(ctx, r, input, opts, { ...files, policyPacks: packs.packs });
505
+ }
506
+ catch (err) {
507
+ dropScratch(files.scratch);
508
+ throw err;
509
+ }
510
+ const planDigest = read.part.member.planDigest;
511
+ const references = stackReferencesOf(read.prior, input);
512
+ const planned = pulumiMemberPlannedOutputs(r.member, { part: read.part, preview: read.preview, ...(read.prior !== undefined ? { exported: read.prior } : {}) });
513
+ const artifact = {
514
+ kind: "pulumi-up",
515
+ member: r.member,
516
+ stack: input.stack,
517
+ engine: r.engine,
518
+ engineSource: r.engineSource,
519
+ engineBecause: r.engineBecause,
520
+ planDigest,
521
+ shape: read.shape,
522
+ scratch: files.scratch,
523
+ ...(files.planFile ? { planFile: files.planFile } : {}),
524
+ ...(files.configFile ? { configFile: files.configFile } : {}),
525
+ policy: read.policy,
526
+ ...(packs.read ? { policyPacks: packs.read } : {}),
527
+ ...(references ? { stackReferences: references } : {}),
528
+ };
529
+ return {
530
+ member: r.member,
531
+ planDigest,
532
+ artifact,
533
+ changeSet: read.part,
534
+ outputs: { known: planned.known, unknown: planned.unknown },
535
+ shape: read.shape,
536
+ ...(ctx.targets?.length ? { targets: ctx.targets } : {}),
537
+ ...location,
538
+ };
539
+ }
540
+ /** The export's `StackReference`s, split into the ones core wired into `stackReferences` and the rest; undefined when it holds none. */
541
+ function stackReferencesOf(exported, input) {
542
+ const refs = pulumiStackReferences(exported);
543
+ if (refs.length === 0)
544
+ return undefined;
545
+ const wired = new Set(Object.keys(input.stackReferences ?? {}));
546
+ const read = refs.filter((r) => wired.has(r.name)).map((r) => r.name);
547
+ const outside = refs
548
+ .filter((r) => !wired.has(r.name))
549
+ .map((r) => ({
550
+ name: r.name,
551
+ why: r.outputs.length === 0 && r.secretOutputs.length > 0
552
+ ? "every output the export recorded for it is secret, and chant reads outputs blind"
553
+ : r.outputs.length === 0
554
+ ? "the export recorded no output for it"
555
+ : "no other pulumi-up step in this project applies that stack",
556
+ }));
557
+ const lastUpdate = exportUpdatedAt(exported);
558
+ return { read: [...new Set(read)].sort(), outside, ...(lastUpdate ? { lastUpdate } : {}) };
559
+ }
560
+ /** When the exported stack last updated (#4617): `deployment.manifest.time`, when it is a time. */
561
+ export function exportUpdatedAt(exported) {
562
+ const deployment = isObject(exported) && isObject(exported.deployment) ? exported.deployment : undefined;
563
+ const time = deployment && isObject(deployment.manifest) ? deployment.manifest.time : undefined;
564
+ return typeof time === "string" && !Number.isNaN(Date.parse(time)) ? time : undefined;
565
+ }
566
+ /**
567
+ * `declaredReads` (#4547): each step's `StackReference`s, read from its
568
+ * export (the index store's, else the checked-in one), as reads of the one
569
+ * other `pulumi-up` step whose project and stack the reference names, one
570
+ * per output the export recorded that is not secret, placed at
571
+ * `/stackReferences/<reference name>/<output>`.
572
+ */
573
+ export async function pulumiDeclaredReads(ctx, members, opts = {}) {
574
+ const steps = members
575
+ .map((m) => {
576
+ const step = m.step;
577
+ if (typeof step?.dir !== "string" || typeof step.stack !== "string")
578
+ return undefined;
579
+ const dir = isAbsolute(step.dir) ? step.dir : resolve(ctx.root, step.cwd ?? ".", step.dir);
580
+ const project = readProject(dir)?.name;
581
+ return { m, step, dir, project: typeof project === "string" ? project : basename(dir) };
582
+ })
583
+ .filter((x) => x !== undefined);
584
+ const owners = steps.map((s) => ({ component: s.m.component, project: s.project, stack: s.step.stack }));
585
+ const out = [];
586
+ for (const s of steps) {
587
+ const exported = await memberExportOf(s.dir, { stack: s.step.stack, ...(opts.exportStore !== undefined ? { store: opts.exportStore } : {}) });
588
+ for (const ref of pulumiStackReferences(exported)) {
589
+ const found = stackReferenceOwner(ref, { component: s.m.component, project: s.project }, owners);
590
+ if (!("owner" in found))
591
+ continue;
592
+ for (const output of ref.outputs) {
593
+ out.push({ reader: s.m.component, index: s.m.index, upstream: found.owner.component, output, at: `/stackReferences/${pointerSegment(ref.name)}/${pointerSegment(output)}` });
594
+ }
595
+ }
596
+ }
597
+ return out;
598
+ }
599
+ /** An `up` that exited non-zero, read through the exit-code table (#4553). */
600
+ function upFailure(r, err, policy) {
601
+ const f = pulumiRunFailure(err);
602
+ if (f.missingBinary)
603
+ return new Error(`pulumi-up: "${r.member}": the ${r.engine} binary was not found on PATH; nothing was applied`);
604
+ const stream = f.stdout ?? "";
605
+ const why = pulumiStreamError(stream) ?? (f.stderr?.trim() || f.message).split("\n").slice(-3).join("\n");
606
+ const outcome = pulumiExitOf(f.exitCode);
607
+ if (outcome === CHANT_EXIT.moved) {
608
+ return new PulumiPlanMovedError(r.member, `"${r.member}": refusing to apply. plunge refused the saved plan, since a record it planned against moved or the plan has no stamp: ${why}. Plan it again.`);
609
+ }
610
+ const events = pulumiPolicyFindings(pulumiPolicyEvents(stream));
611
+ if (outcome === CHANT_EXIT.denied || events.denials.length > 0) {
612
+ return new PulumiPolicyDeniedError(r.member, events.denials.length > 0 ? events : policy, why);
613
+ }
614
+ const landed = pulumiLandedUrns(stream);
615
+ const failed = pulumiFailedUrns(stream);
616
+ const detail = [
617
+ ...(failed.length > 0 ? [`failed: ${failed.join(", ")}`] : []),
618
+ ...(landed.length > 0 ? [`landed before it stopped: ${landed.join(", ")}`] : ["nothing landed"]),
619
+ ].join("; ");
620
+ return new PulumiUpFailedError(r.member, `pulumi-up: "${r.member}": ${r.engine} up failed (exit ${f.exitCode ?? "unknown"}): ${why}. ${detail}`, failedUpSummary(stream));
621
+ }
622
+ /**
623
+ * What a failed `up`'s stream still says (#4579): its `summaryEvent`, or,
624
+ * when the engine stopped before printing one, the per-resource timings
625
+ * plunge put on the steps that ran (`resOutputsEvent`, `resOpFailedEvent`),
626
+ * under `result: "failed"`. Undefined when the stream holds neither.
627
+ */
628
+ export function failedUpSummary(stream) {
629
+ const summary = pulumiStreamSummary(stream);
630
+ if (summary)
631
+ return summary;
632
+ const timings = pulumiStreamTimings(stream);
633
+ return Object.keys(timings).length > 0 ? { resourceChanges: {}, result: "failed", timings } : undefined;
634
+ }
635
+ async function runStack(ctx, original, opts) {
636
+ // #4609: in pr-<n>, the review stack the plan made and previewed.
637
+ const { step: input } = pulumiReviewStep(original, ctx.env, templateOf(opts));
638
+ const r = resolveStep(input, opts, ctx.lockGrain);
639
+ const handed = ctx.plans?.[r.member];
640
+ const fromWave = isArtifact(handed);
641
+ const targets = ctx.targets && ctx.targets.length > 0 ? ctx.targets : undefined;
642
+ await readyCopy(r, opts);
643
+ // Ruling D: no approved plan handed in, so plan here and apply that.
644
+ let approved;
645
+ if (fromWave) {
646
+ approved = handed;
647
+ }
648
+ else {
649
+ const planned = await planStack(ctx, original, opts);
650
+ if (!isArtifact(planned.artifact)) {
651
+ throw new Error(`pulumi-up: "${r.member}": the stack's config reads outputs not known yet (${(ctx.unresolvedInputs ?? []).join(", ")}), so it cannot apply`);
652
+ }
653
+ approved = planned.artifact;
654
+ }
655
+ // The config copy: the plan's when it is still there, else written again (its text is the same).
656
+ const own = existsSync(approved.scratch) ? undefined : scratchFor(r, input, opts);
657
+ const configFile = approved.configFile && existsSync(approved.configFile) ? approved.configFile : own?.configFile;
658
+ // #4613: the packs the plan read at base, the same copies, else read again at that base; a plan that read none runs the step's own.
659
+ let policyPacks = input.policyPacks;
660
+ if (approved.policyPacks) {
661
+ const copies = approved.policyPacks.packs;
662
+ if (copies.every((p) => existsSync(p.dir) && (!p.config || existsSync(p.config)))) {
663
+ policyPacks = copies.map((p) => ({ dir: p.dir, ...(p.config ? { config: p.config } : {}) }));
664
+ }
665
+ else {
666
+ try {
667
+ policyPacks = (await policyPacksFor({ ...ctx, policyBase: approved.policyPacks.base }, r, input, own?.scratch ?? approved.scratch)).packs;
668
+ }
669
+ catch (err) {
670
+ dropScratch(approved.scratch);
671
+ dropScratch(own?.scratch);
672
+ throw err;
673
+ }
674
+ }
675
+ }
676
+ const tail = {
677
+ ...(configFile ? { configFile } : {}),
678
+ ...(targets ? { targets } : {}),
679
+ ...(policyPacks?.length ? { policyPacks } : {}),
680
+ };
681
+ try {
682
+ let stdout;
683
+ if (r.engine === "plunge") {
684
+ if (!approved.planFile || !existsSync(approved.planFile)) {
685
+ throw new Error(`pulumi-up: "${r.member}": the saved plan ${approved.planFile ?? "(none)"} is not on this machine; plan the stack in the apply job, then apply`);
686
+ }
687
+ try {
688
+ ({ stdout } = await runApplyPlunge(runnersFor(r, opts).plunge, plungeUpArgs({ plan: approved.planFile, stack: input.stack, ...tail }), r.dir));
689
+ }
690
+ catch (err) {
691
+ throw upFailure(r, err, approved.policy);
692
+ }
693
+ }
694
+ else {
695
+ if (fromWave) {
696
+ // Preview again, and hold it to the approved shape before anything changes.
697
+ const fresh = await previewStack(ctx, r, input, opts, { ...(configFile ? { configFile } : {}), ...(policyPacks ? { policyPacks } : {}) });
698
+ const differences = pulumiShapeDifferences(approved.shape, fresh.shape, fresh.preview.steps);
699
+ if (differences.length > 0)
700
+ throw new PlanShapeMovedError(r.member, differences);
701
+ }
702
+ try {
703
+ ({ stdout } = await runApplyPulumi(runnersFor(r, opts).pulumi, pulumiUpArgs({ stack: input.stack, ...tail }), r.dir));
704
+ }
705
+ catch (err) {
706
+ throw upFailure(r, err, approved.policy);
707
+ }
708
+ // Ruling A: the steps the update ran, held to the approved shape.
709
+ const steps = pulumiAppliedSteps(stdout);
710
+ const ran = pulumiPreviewPart({ member: r.member, preview: { steps }, stack: input.stack });
711
+ const differences = pulumiShapeDifferences(approved.shape, pulumiPlanShape(ran, r.member), steps);
712
+ if (differences.length > 0)
713
+ throw new PulumiAppliedShapeError(r.member, differences, pulumiStreamSummary(stdout));
714
+ }
715
+ const outputs = await pulumiMemberStackOutputs({
716
+ dir: r.dir,
717
+ stack: input.stack,
718
+ engine: r.engine,
719
+ ...runnerOptions(r, opts),
720
+ });
721
+ const summary = pulumiStreamSummary(stdout);
722
+ const exportSnapshot = await writePulumiExportSnapshot({
723
+ dir: r.dir,
724
+ stack: input.stack,
725
+ environment: ctx.env,
726
+ engine: r.engine,
727
+ ...runnerOptions(r, opts),
728
+ ...(opts.snapshots !== undefined ? { store: opts.snapshots } : {}),
729
+ });
730
+ const hasPolicy = approved.policy.warnings.length > 0 || approved.policy.denials.length > 0;
731
+ return {
732
+ member: r.member,
733
+ stack: input.stack,
734
+ engine: r.engine,
735
+ planDigest: approved.planDigest,
736
+ fromWavePlan: fromWave,
737
+ outputs,
738
+ ...(summary ? { summary } : {}),
739
+ ...(hasPolicy ? { policy: approved.policy } : {}),
740
+ exportSnapshot,
741
+ };
742
+ }
743
+ finally {
744
+ dropScratch(approved.scratch);
745
+ dropScratch(own?.scratch);
746
+ }
747
+ }
748
+ /** `releaseInputs` (#4542): the stack's directory, or unbound when its state is a file relative to it. */
749
+ export function pulumiReleaseInputs(input, ctx) {
750
+ if (typeof input?.dir !== "string" || input.dir === "" || typeof input.stack !== "string")
751
+ return null;
752
+ const base = resolve(ctx.memberDir, input.cwd ?? ".");
753
+ const dir = isAbsolute(input.dir) ? input.dir : resolve(base, input.dir);
754
+ const project = readProject(dir);
755
+ if (!project)
756
+ return null;
757
+ const toPosix = (p) => p.split("\\").join("/") || ".";
758
+ const fromMember = (abs) => toPosix(relative(ctx.memberDir, abs));
759
+ const backend = project.backend?.url;
760
+ if (typeof backend === "string") {
761
+ const m = /^(?:plunge\+)?file:\/\/(.*)$/.exec(backend);
762
+ if (m && !m[1].startsWith("/") && !m[1].startsWith("~")) {
763
+ return {
764
+ unbound: `stack "${input.stack}" keeps its state at ${backend}, a path relative to ${fromMember(dir)}, so a preview of a recorded copy would read no state; roll it back with chant lifecycle rollback --to <ref>`,
765
+ };
766
+ }
767
+ }
768
+ const paths = [fromMember(dir)];
769
+ const main = typeof project.main === "string" ? resolve(dir, project.main) : undefined;
770
+ if (main && relative(dir, main).startsWith(".."))
771
+ paths.push(fromMember(main));
772
+ return { field: "dir", dir: fromMember(dir), paths: [...new Set(paths)].sort() };
773
+ }
774
+ /** The `pulumi-up` capability, with injectable runners for tests. */
775
+ export function createPulumiUpCapability(opts = {}) {
776
+ return {
777
+ kind: "pulumi-up",
778
+ // A stack's update has no undo short of applying the previous program, which is a change of its own.
779
+ rollbackPolicy: "needs-opt-out",
780
+ // Each target passes as `--target <urn>` to the preview and to `up`.
781
+ targets: "accept",
782
+ plan: (ctx, input) => planStack(ctx, input, opts),
783
+ run: (ctx, input) => runStack(ctx, input, opts),
784
+ // The outputs standing now, for a dependent planned before this stack applies.
785
+ async outputs(ctx, original) {
786
+ const { step: input } = pulumiReviewStep(original, ctx.env, templateOf(opts));
787
+ const r = resolveStep(input, opts);
788
+ return pulumiMemberStackOutputs({
789
+ dir: r.dir,
790
+ stack: input.stack,
791
+ engine: r.engine,
792
+ ...runnerOptions(r, opts),
793
+ });
794
+ },
795
+ releaseInputs: (input, ctx) => pulumiReleaseInputs(input, ctx),
796
+ // #4552: resource on plunge, the stack (target) on stock Pulumi.
797
+ defaultLockGrain(input) {
798
+ requireInput(input);
799
+ return pulumiDefaultLockGrain(pulumiMemberEngine(stackDir(input), input.engine, opts.env ? { env: opts.env } : {}).engine);
800
+ },
801
+ // Stock Pulumi keeps the stack grain: its backend's stack lock already orders updates, and nothing in it is per resource.
802
+ lockGrainRefusal(input, grain) {
803
+ if (grain !== "resource")
804
+ return undefined;
805
+ requireInput(input);
806
+ const engine = pulumiMemberEngine(stackDir(input), input.engine, opts.env ? { env: opts.env } : {}).engine;
807
+ return engine === "pulumi" ? new PulumiResourceGrainRefusedError(pulumiUpMember(input)).message : undefined;
808
+ },
809
+ // URN rows, object rows, and the stack row when something may appear only at apply (./lock-rows.ts).
810
+ lockRows(input, plan) {
811
+ if (!plan.changeSet)
812
+ return [];
813
+ const project = readProject(stackDir(input))?.name;
814
+ // #4609: the stack the plan previewed, the review stack in pr-<n>.
815
+ const stack = isArtifact(plan.artifact) ? plan.artifact.stack : input.stack;
816
+ return pulumiLockRows(plan.changeSet, plan.member, { ...(typeof project === "string" ? { project } : {}), stack });
817
+ },
818
+ // #4555: the stack, the engine and the summary's counts, for the step's span and run record.
819
+ traceFacts: (input, outcome) => pulumiTraceFacts(input, outcome),
820
+ declaredReads: (ctx, members) => pulumiDeclaredReads(ctx, members, opts.snapshots !== undefined ? { exportStore: opts.snapshots } : {}),
821
+ };
822
+ }
823
+ export const pulumiUpCapability = createPulumiUpCapability();