@intentius/chant-lexicon-pulumi 0.124.55 → 0.124.56

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.
@@ -0,0 +1,61 @@
1
+ import type { CheckStageMember, CheckStepOutcome, CheckStepsAdapter, CheckStepsContext } from "@intentius/chant/components/check-steps";
2
+ import { type PulumiMemberEngine } from "./member-engine.js";
3
+ import { type PulumiRunner } from "./run.js";
4
+ /** A runner for `bin` with exactly `env` (the check job's), never the process's whole environment. */
5
+ export declare function checkRunner(bin: PulumiMemberEngine, env: Record<string, string>): PulumiRunner;
6
+ /** One key the project declares in `Pulumi.yaml`'s `config:`. */
7
+ export interface PulumiConfigKey {
8
+ /** The full key, `<project>:<name>` for a project key. */
9
+ key: string;
10
+ type?: string;
11
+ items?: {
12
+ type?: string;
13
+ };
14
+ secret: boolean;
15
+ /** Whether it has a `default` or a project-level `value`. */
16
+ hasDefault: boolean;
17
+ }
18
+ /** The project's config schema: its name, the keys it declares, and where its stack files are. */
19
+ export interface PulumiProjectSchema {
20
+ project: string;
21
+ keys: PulumiConfigKey[];
22
+ /** The directory the stack files are in, relative to the project (`stackConfigDir`), or "." */
23
+ stackConfigDir: string;
24
+ }
25
+ /** Read `Pulumi.yaml`'s schema from its parsed document. A short-form entry (`key: value`) is a value with no type. */
26
+ export declare function pulumiProjectSchema(doc: unknown): PulumiProjectSchema;
27
+ /**
28
+ * What is wrong with one stack's config (`config:` of its
29
+ * `Pulumi.<stack>.yaml`, parsed) against the project's schema, as lines
30
+ * naming the stack and key. Empty when it passes. Keys of other namespaces
31
+ * (a provider's `aws:region`) are not checked: the project declares no type
32
+ * for them.
33
+ */
34
+ export declare function pulumiStackConfigProblems(schema: PulumiProjectSchema, stack: string, stackDoc: unknown): string[];
35
+ /** The stacks the project lists by their `Pulumi.<stack>.yaml` files in `dir`, sorted. */
36
+ export declare function listedStacks(dir: string): string[];
37
+ /** What `pulumi about --json` reported, for the check report. */
38
+ export interface PulumiAboutDetail {
39
+ engine: PulumiMemberEngine;
40
+ version?: string;
41
+ plugins: Array<{
42
+ name: string;
43
+ version?: string;
44
+ }>;
45
+ backend?: {
46
+ name?: string;
47
+ url?: string;
48
+ user?: string;
49
+ };
50
+ }
51
+ /** Read `pulumi about --json`'s document. Undefined when it is not JSON. */
52
+ export declare function pulumiAboutDetail(engine: PulumiMemberEngine, stdout: string | undefined): PulumiAboutDetail | undefined;
53
+ export interface PulumiCheckStepsOptions {
54
+ /** The runner for an engine, given the check job's environment. Default {@link checkRunner}. */
55
+ runner?: (engine: PulumiMemberEngine, env: Record<string, string>) => PulumiRunner;
56
+ }
57
+ /** The steps for one member, in order: install, each stack's config, about. */
58
+ export declare function pulumiMemberCheck(member: CheckStageMember, ctx: Pick<CheckStepsContext, "env" | "log">, opts?: PulumiCheckStepsOptions): Promise<CheckStepOutcome[]>;
59
+ /** The pulumi lexicon's check steps, for `LexiconPlugin.checkSteps`. */
60
+ export declare function pulumiCheckSteps(opts?: PulumiCheckStepsOptions): CheckStepsAdapter;
61
+ //# sourceMappingURL=check-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-step.d.ts","sourceRoot":"","sources":["../src/check-step.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,yCAAyC,CAAC;AAExI,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAA+C,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAOvF,sGAAsG;AACtG,wBAAgB,WAAW,CAAC,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAE9F;AAID,iEAAiE;AACjE,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,6DAA6D;IAC7D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,kGAAkG;AAClG,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,eAAe,EAAE,CAAC;IACxB,+FAA+F;IAC/F,cAAc,EAAE,MAAM,CAAC;CACxB;AASD,uHAAuH;AACvH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,CAsBrE;AAoBD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,EAAE,CA2BjH;AAOD,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAMlD;AASD,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D;AAED,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,GAAG,SAAS,CAkBvH;AAUD,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,YAAY,CAAC;CACpF;AAED,+EAA+E;AAC/E,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,IAAI,CAAC,iBAAiB,EAAE,KAAK,GAAG,KAAK,CAAC,EAAE,IAAI,GAAE,uBAA4B,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsF9K;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,uBAA4B,GAAG,iBAAiB,CAUtF"}
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The pulumi lexicon's steps in chant's check stage (#4633). `chant ci
3
+ * check` runs them after the `ci.hooks.check` hooks, for each workspace
4
+ * member of kind `pulumi` (`LexiconPlugin.checkSteps`,
5
+ * `@intentius/chant/components/check-steps`), so a product's check
6
+ * (`plywood check`) gets them through chant with nothing of its own:
7
+ *
8
+ * - `pulumi-install`: `pulumi install` (`plunge install` for a member on
9
+ * plunge, `./member-engine.ts`) in the member's directory, which fetches
10
+ * the program's dependencies and plugins the way `Pulumi.yaml` says;
11
+ * - `stack-config`: each stack's `Pulumi.<stack>.yaml` checked against the
12
+ * project's config schema in `Pulumi.yaml`, with no command run (a key
13
+ * with a type and no default must be set, a value must read as its type,
14
+ * and a `secret: true` key must be encrypted). The stacks are the ones the
15
+ * project lists by their files, plus the member's declared stack;
16
+ * - `pulumi-about`: `pulumi about --json`, which reports the engine, its
17
+ * plugins and the backend for the report.
18
+ *
19
+ * The argv go through `runCheckPulumi` (`./run.ts`), which refuses any
20
+ * other. The environment is the check job's ({@link CheckStepsContext.env}):
21
+ * no forge token, no OIDC request variables, no cloud credential, so neither
22
+ * command can write a stack. A failure names the member and stack, and the
23
+ * check exits with chant's `failed` code.
24
+ *
25
+ * The runner is injectable, so the tests run no binary.
26
+ */
27
+ import { execFile } from "node:child_process";
28
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { promisify } from "node:util";
31
+ import { parseYAML } from "@intentius/chant/yaml";
32
+ import { pulumiMemberEngine } from "./member-engine.js";
33
+ import { engineEnv, pulumiRunFailure, runCheckPulumi } from "./run.js";
34
+ const execFileAsync = promisify(execFile);
35
+ /** The lexicon name the report carries. */
36
+ const LEXICON = "pulumi";
37
+ /** A runner for `bin` with exactly `env` (the check job's), never the process's whole environment. */
38
+ export function checkRunner(bin, env) {
39
+ return (args, opts) => execFileAsync(bin, args, { cwd: opts.cwd, maxBuffer: 64 * 1024 * 1024, env: engineEnv({ ...env, ...opts.env }) });
40
+ }
41
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
42
+ /** The key in its full form: a bare name belongs to the project. */
43
+ function fullKey(project, key) {
44
+ return key.includes(":") ? key : `${project}:${key}`;
45
+ }
46
+ /** Read `Pulumi.yaml`'s schema from its parsed document. A short-form entry (`key: value`) is a value with no type. */
47
+ export function pulumiProjectSchema(doc) {
48
+ const d = isRecord(doc) ? doc : {};
49
+ const project = typeof d.name === "string" ? d.name : "";
50
+ const config = isRecord(d.config) ? d.config : {};
51
+ const keys = [];
52
+ for (const [name, entry] of Object.entries(config)) {
53
+ const key = fullKey(project, name);
54
+ const schema = isRecord(entry) && ("type" in entry || "default" in entry || "value" in entry || "secret" in entry || "description" in entry || "items" in entry);
55
+ if (!schema) {
56
+ keys.push({ key, secret: false, hasDefault: true });
57
+ continue;
58
+ }
59
+ const e = entry;
60
+ keys.push({
61
+ key,
62
+ ...(typeof e.type === "string" ? { type: e.type } : {}),
63
+ ...(isRecord(e.items) && typeof e.items.type === "string" ? { items: { type: e.items.type } } : {}),
64
+ secret: e.secret === true,
65
+ hasDefault: "default" in e || "value" in e,
66
+ });
67
+ }
68
+ return { project, keys, stackConfigDir: typeof d.stackConfigDir === "string" && d.stackConfigDir.length > 0 ? d.stackConfigDir : "." };
69
+ }
70
+ const isSecure = (v) => isRecord(v) && typeof v.secure === "string";
71
+ /** Whether a scalar stack value reads as `type`. Values are strings once Pulumi reads them, so `"3"` is an integer. */
72
+ function readsAs(type, value) {
73
+ switch (type) {
74
+ case "string":
75
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
76
+ case "integer":
77
+ return (typeof value === "number" && Number.isInteger(value)) || (typeof value === "string" && /^-?[0-9]+$/.test(value.trim()));
78
+ case "boolean":
79
+ return typeof value === "boolean" || value === "true" || value === "false";
80
+ case "array":
81
+ return Array.isArray(value);
82
+ default:
83
+ return true;
84
+ }
85
+ }
86
+ /**
87
+ * What is wrong with one stack's config (`config:` of its
88
+ * `Pulumi.<stack>.yaml`, parsed) against the project's schema, as lines
89
+ * naming the stack and key. Empty when it passes. Keys of other namespaces
90
+ * (a provider's `aws:region`) are not checked: the project declares no type
91
+ * for them.
92
+ */
93
+ export function pulumiStackConfigProblems(schema, stack, stackDoc) {
94
+ const raw = isRecord(stackDoc) && isRecord(stackDoc.config) ? stackDoc.config : {};
95
+ const values = new Map();
96
+ for (const [k, v] of Object.entries(raw))
97
+ values.set(fullKey(schema.project, k), v);
98
+ const problems = [];
99
+ for (const k of schema.keys) {
100
+ if (!k.type && !k.secret)
101
+ continue;
102
+ if (!values.has(k.key)) {
103
+ if (k.type && !k.hasDefault)
104
+ problems.push(`stack ${stack} is missing the configuration value ${k.key} (type ${k.type}, no default)`);
105
+ continue;
106
+ }
107
+ const value = values.get(k.key);
108
+ if (k.secret && !isSecure(value)) {
109
+ problems.push(`stack ${stack} sets ${k.key} in plain text, and the project declares it secret; set it with pulumi config set --secret`);
110
+ continue;
111
+ }
112
+ if (isSecure(value) || !k.type)
113
+ continue;
114
+ if (!readsAs(k.type, value)) {
115
+ problems.push(`stack ${stack} sets ${k.key} to ${JSON.stringify(value)}, which is not of type ${k.type}`);
116
+ continue;
117
+ }
118
+ if (k.type === "array" && k.items?.type && Array.isArray(value)) {
119
+ const bad = value.findIndex((item) => !isSecure(item) && !readsAs(k.items.type, item));
120
+ if (bad >= 0)
121
+ problems.push(`stack ${stack} sets ${k.key}[${bad}] to ${JSON.stringify(value[bad])}, which is not of type ${k.items.type}`);
122
+ }
123
+ }
124
+ return problems;
125
+ }
126
+ /** The project's file in `dir`, `Pulumi.yaml` or `Pulumi.yml`. */
127
+ function projectFile(dir) {
128
+ return ["Pulumi.yaml", "Pulumi.yml"].map((f) => join(dir, f)).find((f) => existsSync(f));
129
+ }
130
+ /** The stacks the project lists by their `Pulumi.<stack>.yaml` files in `dir`, sorted. */
131
+ export function listedStacks(dir) {
132
+ if (!existsSync(dir))
133
+ return [];
134
+ return readdirSync(dir)
135
+ .map((f) => /^Pulumi\.(.+)\.ya?ml$/.exec(f)?.[1])
136
+ .filter((s) => s !== undefined)
137
+ .sort();
138
+ }
139
+ /** The stack file for `stack` in `dir`, if there is one. */
140
+ function stackFile(dir, stack) {
141
+ return [`Pulumi.${stack}.yaml`, `Pulumi.${stack}.yml`].map((f) => join(dir, f)).find((f) => existsSync(f));
142
+ }
143
+ /** Read `pulumi about --json`'s document. Undefined when it is not JSON. */
144
+ export function pulumiAboutDetail(engine, stdout) {
145
+ if (!stdout)
146
+ return undefined;
147
+ let doc;
148
+ try {
149
+ doc = JSON.parse(stdout);
150
+ }
151
+ catch {
152
+ return undefined;
153
+ }
154
+ if (!isRecord(doc))
155
+ return undefined;
156
+ const cli = isRecord(doc.cli) ? doc.cli : {};
157
+ const plugins = Array.isArray(doc.plugins)
158
+ ? doc.plugins.filter(isRecord).map((p) => ({ name: String(p.name ?? ""), ...(typeof p.version === "string" ? { version: p.version } : {}) })).filter((p) => p.name)
159
+ : [];
160
+ const b = isRecord(doc.backend) ? doc.backend : undefined;
161
+ const backend = b
162
+ ? { ...(typeof b.name === "string" ? { name: b.name } : {}), ...(typeof b.url === "string" ? { url: b.url } : {}), ...(typeof b.user === "string" ? { user: b.user } : {}) }
163
+ : undefined;
164
+ return { engine, ...(typeof cli.version === "string" ? { version: cli.version } : {}), plugins, ...(backend ? { backend } : {}) };
165
+ }
166
+ // ── The steps ───────────────────────────────────────────────────────
167
+ /** The last lines of what a failed run printed, for its reason. */
168
+ function tail(text) {
169
+ const lines = (text ?? "").trim().split("\n").filter(Boolean);
170
+ return lines.slice(-3).join(" | ");
171
+ }
172
+ /** The steps for one member, in order: install, each stack's config, about. */
173
+ export async function pulumiMemberCheck(member, ctx, opts = {}) {
174
+ const fields = member.fields ?? {};
175
+ const declaredEngine = fields.engine === "pulumi" || fields.engine === "plunge" ? fields.engine : undefined;
176
+ const declaredStack = typeof fields.stack === "string" && fields.stack.length > 0 ? fields.stack : undefined;
177
+ const { engine } = pulumiMemberEngine(member.dir, declaredEngine, { env: ctx.env });
178
+ const run = (opts.runner ?? checkRunner)(engine, ctx.env);
179
+ const out = [];
180
+ const base = { lexicon: LEXICON, member: member.name };
181
+ const file = projectFile(member.dir);
182
+ if (!file) {
183
+ out.push({ ...base, step: "pulumi-install", exit: 1, outcome: "failed", reason: `member ${member.name} has no Pulumi.yaml in ${member.dir}` });
184
+ return out;
185
+ }
186
+ // pulumi install
187
+ let installed = true;
188
+ try {
189
+ await runCheckPulumi(run, ["install"], member.dir);
190
+ out.push({ ...base, step: "pulumi-install", exit: 0, outcome: "passed" });
191
+ }
192
+ catch (err) {
193
+ installed = false;
194
+ const f = pulumiRunFailure(err);
195
+ const why = f.missingBinary ? `${engine} is not installed in the check job` : tail(f.stderr) || tail(f.stdout) || f.message;
196
+ out.push({ ...base, step: "pulumi-install", exit: f.exitCode ?? 1, outcome: "failed", reason: `${engine} install failed for member ${member.name}: ${why}` });
197
+ }
198
+ // Stack config, run by nothing but this module.
199
+ let schema;
200
+ try {
201
+ schema = pulumiProjectSchema(parseYAML(readFileSync(file, "utf-8")));
202
+ }
203
+ catch (err) {
204
+ out.push({ ...base, step: "stack-config", exit: 1, outcome: "failed", reason: `member ${member.name}: ${file} does not read as YAML (${err instanceof Error ? err.message : String(err)})` });
205
+ }
206
+ if (schema) {
207
+ const stackDir = join(member.dir, schema.stackConfigDir);
208
+ const stacks = [...new Set([...listedStacks(stackDir), ...(declaredStack ? [declaredStack] : [])])].sort();
209
+ if (stacks.length === 0)
210
+ out.push({ ...base, step: "stack-config", exit: 0, outcome: "skipped", reason: `member ${member.name} lists no stack (no Pulumi.<stack>.yaml in ${stackDir})` });
211
+ for (const stack of stacks) {
212
+ const at = stackFile(stackDir, stack);
213
+ let doc = {};
214
+ if (at) {
215
+ try {
216
+ doc = parseYAML(readFileSync(at, "utf-8"));
217
+ }
218
+ catch (err) {
219
+ out.push({ ...base, step: "stack-config", stack, exit: 1, outcome: "failed", reason: `member ${member.name}, stack ${stack}: ${at} does not read as YAML (${err instanceof Error ? err.message : String(err)})` });
220
+ continue;
221
+ }
222
+ }
223
+ const problems = pulumiStackConfigProblems(schema, stack, doc);
224
+ out.push(problems.length === 0
225
+ ? { ...base, step: "stack-config", stack, exit: 0, outcome: "passed" }
226
+ : { ...base, step: "stack-config", stack, exit: 1, outcome: "failed", reason: `member ${member.name}: ${problems.join("; ")}`, detail: { problems } });
227
+ }
228
+ }
229
+ // pulumi about --json
230
+ if (!installed) {
231
+ out.push({ ...base, step: "pulumi-about", exit: 0, outcome: "skipped", reason: `member ${member.name}: ${engine} install failed, so about did not run` });
232
+ return out;
233
+ }
234
+ const aboutArgs = declaredStack ? ["about", "--json", "--stack", declaredStack] : ["about", "--json"];
235
+ const stackPart = declaredStack ? { stack: declaredStack } : {};
236
+ try {
237
+ const { stdout } = await runCheckPulumi(run, aboutArgs, member.dir);
238
+ const detail = pulumiAboutDetail(engine, stdout);
239
+ out.push(detail
240
+ ? { ...base, step: "pulumi-about", ...stackPart, exit: 0, outcome: "passed", detail }
241
+ : { ...base, step: "pulumi-about", ...stackPart, exit: 1, outcome: "failed", reason: `${engine} about --json for member ${member.name} printed no JSON` });
242
+ }
243
+ catch (err) {
244
+ const f = pulumiRunFailure(err);
245
+ // about exits non-zero when one of its reads fails (no backend login in a check job), and still prints what it read.
246
+ const detail = pulumiAboutDetail(engine, f.stdout);
247
+ if (detail) {
248
+ ctx.log(`pulumi check: ${engine} about for ${member.name} exited ${f.exitCode ?? 1} and reported: ${tail(f.stderr) || "no reason"}`);
249
+ out.push({ ...base, step: "pulumi-about", ...stackPart, exit: f.exitCode ?? 1, outcome: "passed", detail });
250
+ }
251
+ else {
252
+ const why = f.missingBinary ? `${engine} is not installed in the check job` : tail(f.stderr) || f.message;
253
+ out.push({ ...base, step: "pulumi-about", ...stackPart, exit: f.exitCode ?? 1, outcome: "failed", reason: `${engine} about failed for member ${member.name}${declaredStack ? `, stack ${declaredStack}` : ""}: ${why}` });
254
+ }
255
+ }
256
+ return out;
257
+ }
258
+ /** The pulumi lexicon's check steps, for `LexiconPlugin.checkSteps`. */
259
+ export function pulumiCheckSteps(opts = {}) {
260
+ return {
261
+ lexicon: LEXICON,
262
+ kinds: ["pulumi"],
263
+ async run(ctx) {
264
+ const out = [];
265
+ for (const member of ctx.members)
266
+ out.push(...(await pulumiMemberCheck(member, ctx, opts)));
267
+ return out;
268
+ },
269
+ };
270
+ }
package/dist/index.d.ts CHANGED
@@ -23,4 +23,5 @@ export { PulumiAdoptOp, type PulumiAdoptOpConfig, type PulumiAdoptOpResources, t
23
23
  export { parsePlungeAdoptView, pulumiAdoptDigest, adoptBucketTarget, type PlungeAdoptView, type PlungeAdoptStatus } from "./adopt.js";
24
24
  export { pulumiPreview as pulumiPreviewStep, pulumiDrift as pulumiDriftStep } from "./op/builders.js";
25
25
  export { pulumiExitOf, pulumiPlanMoved, PULUMI_EXIT, PLUNGE_PLAN_MOVED_EXIT } from "./exit-codes.js";
26
+ export { pulumiPlannedOutputChanges, type PulumiOutputChange, type PulumiPlannedOutputChanges } from "./planned-output-changes.js";
26
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -27,3 +27,5 @@ export { parsePlungeAdoptView, pulumiAdoptDigest, adoptBucketTarget } from "./ad
27
27
  export { pulumiPreview as pulumiPreviewStep, pulumiDrift as pulumiDriftStep } from "./op/builders.js";
28
28
  // Pulumi and plunge exit statuses on chant's exit-code table (#4553).
29
29
  export { pulumiExitOf, pulumiPlanMoved, PULUMI_EXIT, PLUNGE_PLAN_MOVED_EXIT } from "./exit-codes.js";
30
+ // Which stack outputs a preview changes, traced through the export (#4635).
31
+ export { pulumiPlannedOutputChanges } from "./planned-output-changes.js";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "d4de8cb6f01fdb50a4e9b53ae82877b221a4512e50e804f33eface5da48e9840",
4
+ "manifest.json": "27fc35795ca8c387b7ce55c46cd22c8ffe372f77a601cc1a33336aa2df5dabc1",
5
5
  "meta.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
6
6
  "types/index.d.ts": "883bcdf6a960e21360e79df010ca2087719fffec60fc39541c5ecd32d661e328",
7
7
  "rules/member-dir-absolute.ts": "4805d5e247ad544f942e1b2c36fc15a5087ca0aacaf804183a269021bd60e276",
@@ -25,5 +25,5 @@
25
25
  "skills/chant-pulumi-stack-export.md": "aadd67f5e067de61dfda43d57492bca08bd22429e4baf314d6856a810dc0e8dc",
26
26
  "skills/chant-pulumi-plunge.md": "8a7f2f9f1124744dee9645905f141057ca01f5a88e34e1bc3858d69406d7ff3a"
27
27
  },
28
- "composite": "362628fd365ccf7dfb116696cbe7f48f955530635fcba52c4366ad55a66ea747"
28
+ "composite": "6ac4e2b566ed424af816c52851540c62b0618ea4a8eea462bb03305465cd219c"
29
29
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulumi",
3
- "version": "0.124.55",
3
+ "version": "0.124.56",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Pulumi"
6
6
  }
@@ -1,5 +1,6 @@
1
1
  import type { EngineOutputSource, EnginePlannedOutputs } from "@intentius/chant/components/verbs/member-outputs";
2
2
  import type { ChangeSetPart } from "@intentius/chant/change-set";
3
+ import { type PulumiOutputChange } from "./planned-output-changes.js";
3
4
  import { type PulumiMemberEngine, type PulumiRunner } from "./run.js";
4
5
  /** Where a member's applied outputs are read from. */
5
6
  export type PulumiAppliedFrom = "cli" | "export";
@@ -26,14 +27,20 @@ export declare function pulumiPartHasChanges(part: ChangeSetPart): boolean;
26
27
  * the preview's error. A preview that plans no stack outputs (no stack step,
27
28
  * or one without outputs, which is what a real `pulumi preview --json`
28
29
  * prints, #4475) gives the export's outputs when it plans nothing to apply,
29
- * since no apply will move them, and names every output the export holds
30
- * unknown otherwise, so nothing reads a value the preview did not plan.
30
+ * since no apply will move them. With changes, each output is traced to the
31
+ * export's resources (#4635, {@link pulumiPlannedOutputChanges}): the ones
32
+ * the preview leaves alone keep their value, and the rest are unknown, so
33
+ * nothing reads a value the preview may move. `outputChanges` lists each
34
+ * output's verdict and why.
31
35
  */
32
36
  export declare function pulumiMemberPlannedOutputs(member: string, read: {
33
37
  part: ChangeSetPart;
34
38
  preview: unknown;
35
39
  exported?: unknown;
36
- }): EnginePlannedOutputs;
40
+ targeted?: boolean;
41
+ }): EnginePlannedOutputs & {
42
+ outputChanges?: PulumiOutputChange[];
43
+ };
37
44
  /** The applied outputs a `pulumi stack export` records, secrets left out. */
38
45
  export declare function pulumiExportedOutputValues(exported: unknown): Record<string, unknown>;
39
46
  /** A Pulumi member's outputs for `member-outputs`, `kinds: ["pulumi"]`. */
@@ -1 +1 @@
1
- {"version":3,"file":"member-outputs.d.ts","sourceRoot":"","sources":["../src/member-outputs.ts"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EAAmB,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,kDAAkD,CAAC;AAClI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAqE,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAItI,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjD,sGAAsG;AACtG,eAAO,MAAM,0BAA0B,iCAAiC,CAAC;AAEzE,MAAM,WAAW,yBAAyB;IACxC,qHAAqH;IACrH,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,oEAAoE;IACpE,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAcD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAGjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,oBAAoB,CAcpJ;AAED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErF;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,kBAAkB,CAoB9F;AAED,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,kBAAyC,CAAC"}
1
+ {"version":3,"file":"member-outputs.d.ts","sourceRoot":"","sources":["../src/member-outputs.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAmB,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,kDAAkD,CAAC;AAClI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAA8B,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE/F,OAAO,EAAqE,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAItI,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjD,sGAAsG;AACtG,eAAO,MAAM,0BAA0B,iCAAiC,CAAC;AAEzE,MAAM,WAAW,yBAAyB;IACxC,qHAAqH;IACrH,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,oEAAoE;IACpE,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAcD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GACtF,oBAAoB,GAAG;IAAE,aAAa,CAAC,EAAE,kBAAkB,EAAE,CAAA;CAAE,CAkBjE;AAED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAErF;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,kBAAkB,CAoB9F;AAED,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,kBAAyC,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { pulumiPlannedOutputChanges } from "./planned-output-changes.js";
1
2
  import { pulumiPlannedStackOutputs, pulumiExportedStackOutputs } from "./stack-outputs.js";
2
3
  import { memberExportOf, previewPulumiMemberRead, pulumiMemberStackOutputs } from "./run.js";
3
4
  import { isSecret } from "./values.js";
@@ -33,8 +34,11 @@ export function pulumiPartHasChanges(part) {
33
34
  * the preview's error. A preview that plans no stack outputs (no stack step,
34
35
  * or one without outputs, which is what a real `pulumi preview --json`
35
36
  * prints, #4475) gives the export's outputs when it plans nothing to apply,
36
- * since no apply will move them, and names every output the export holds
37
- * unknown otherwise, so nothing reads a value the preview did not plan.
37
+ * since no apply will move them. With changes, each output is traced to the
38
+ * export's resources (#4635, {@link pulumiPlannedOutputChanges}): the ones
39
+ * the preview leaves alone keep their value, and the rest are unknown, so
40
+ * nothing reads a value the preview may move. `outputChanges` lists each
41
+ * output's verdict and why.
38
42
  */
39
43
  export function pulumiMemberPlannedOutputs(member, read) {
40
44
  const { part } = read;
@@ -47,7 +51,11 @@ export function pulumiMemberPlannedOutputs(member, read) {
47
51
  const exported = read.exported === undefined ? [] : pulumiExportedStackOutputs(read.exported);
48
52
  if (!changes)
49
53
  return { known: knownValues(exported), unknown: [], planDigest: part.member.planDigest, changes };
50
- return { known: {}, unknown: exported.filter((o) => !o.secret).map((o) => o.name), planDigest: part.member.planDigest, changes };
54
+ if (part.member.holes.length > 0 || read.exported === undefined) {
55
+ return { known: {}, unknown: exported.filter((o) => !o.secret).map((o) => o.name), planDigest: part.member.planDigest, changes };
56
+ }
57
+ const traced = pulumiPlannedOutputChanges({ part, exported: read.exported, ...(read.targeted ? { targeted: true } : {}) });
58
+ return { known: traced.known, unknown: traced.unknown, planDigest: part.member.planDigest, changes, outputChanges: traced.outputs };
51
59
  }
52
60
  const unknown = planned.filter((o) => o.unknown).map((o) => o.name);
53
61
  return { known: knownValues(planned), unknown, planDigest: part.member.planDigest, changes: changes || unknown.length > 0 };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Which of a stack's outputs its preview changes (#4635). A real `pulumi
3
+ * preview --json` plans no stack outputs (recorded, #4475), so until this
4
+ * module every output of a stack with any change was unknown, and every
5
+ * stack reading any of them waited for the `up`. Here each output the last
6
+ * deployment holds is traced, by value, to the resource properties in the
7
+ * export that hold the same value, and the preview's entry for each such
8
+ * resource says whether the value can move:
9
+ *
10
+ * | the resource's entry | the property | verdict |
11
+ * |---|---|---|
12
+ * | replace, delete, or a `mayAppear` update | any | changes |
13
+ * | update | an input the diff names, or one that is unknown | changes |
14
+ * | update | an input the diff leaves alone; `id`, `arn` or `urn` | stable |
15
+ * | update | any other output (a provider-computed `version`) | may change |
16
+ * | no-op, read, or no entry in a targeted preview | any | stable |
17
+ *
18
+ * An output is changing when one of its matches changes, or when every
19
+ * match only may change; it is unchanged, with the export's value, when no
20
+ * match changes and one is stable. An output no property matches (one the
21
+ * program computed, such as `` `${bucket.arn}/*` ``), or an object, is
22
+ * untraced, and so unknown while the stack has changes. A list traces each
23
+ * string in it. Strings shorter than four characters match nothing, so a
24
+ * `"1"` or a `"true"` traces no output. Secret outputs are left out, as
25
+ * everywhere: chant reads outputs blind.
26
+ *
27
+ * The trace runs only when the preview plans no stack outputs and has
28
+ * changes; core's `member-outputs` and `pulumi-up`'s plan then take its
29
+ * unchanged outputs as known and the rest as unknown, so a stack reading
30
+ * through `StackReference` (#4547) an output the change moves is held until
31
+ * the producer applied and the reader is previewed again, and one reading
32
+ * only unchanged outputs plans now.
33
+ */
34
+ import type { ChangeSetPart } from "@intentius/chant/change-set";
35
+ /** One output's verdict, and the resource property that decided it. */
36
+ export interface PulumiOutputChange {
37
+ output: string;
38
+ verdict: "changing" | "unchanged" | "untraced";
39
+ /** The deciding match: `<urn tail>.<property>` and the resource's action. Absent when untraced. */
40
+ because?: string;
41
+ }
42
+ /** The trace of every output the export holds that is not secret, by name. */
43
+ export interface PulumiPlannedOutputChanges {
44
+ outputs: PulumiOutputChange[];
45
+ /** Unchanged outputs with the export's value. */
46
+ known: Record<string, unknown>;
47
+ /** Changing and untraced outputs. */
48
+ unknown: string[];
49
+ }
50
+ /**
51
+ * Trace each of the export's stack outputs through the member's part (the
52
+ * change set its preview became). `targeted` says the preview was narrowed
53
+ * with `--target`, so a resource it did not list is not touched by it.
54
+ */
55
+ export declare function pulumiPlannedOutputChanges(input: {
56
+ part: ChangeSetPart;
57
+ exported: unknown;
58
+ targeted?: boolean;
59
+ }): PulumiPlannedOutputChanges;
60
+ //# sourceMappingURL=planned-output-changes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planned-output-changes.d.ts","sourceRoot":"","sources":["../src/planned-output-changes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAkB,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAKjF,uEAAuE;AACvE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;IAC/C,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8EAA8E;AAC9E,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,qCAAqC;IACrC,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAiDD;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,0BAA0B,CAiC5I"}
@@ -0,0 +1,94 @@
1
+ import { isPulumiNodeless } from "@intentius/chant/pulumi-urn";
2
+ import { pulumiExportedStackOutputs } from "./stack-outputs.js";
3
+ import { isObject, isSecret } from "./values.js";
4
+ /** Properties that stay put through an in-place update. */
5
+ const STABLE_OUTPUTS = new Set(["id", "arn", "urn"]);
6
+ const tailOf = (urn) => urn.split("::").slice(2).join("::") || urn;
7
+ /** Every (resource, property) of the export holding a string worth matching, by value. */
8
+ function holdersByValue(exported) {
9
+ const deployment = isObject(exported) && isObject(exported.deployment) ? exported.deployment : {};
10
+ const resources = Array.isArray(deployment.resources) ? deployment.resources : [];
11
+ const by = new Map();
12
+ const add = (value, h) => {
13
+ if (typeof value !== "string" || value.length < 4)
14
+ return;
15
+ const list = by.get(value) ?? [];
16
+ list.push(h);
17
+ by.set(value, list);
18
+ };
19
+ for (const r of resources) {
20
+ if (!isObject(r) || typeof r.urn !== "string" || typeof r.type !== "string")
21
+ continue;
22
+ if (r.custom !== true || isPulumiNodeless(r.type))
23
+ continue;
24
+ const inputs = isObject(r.inputs) ? r.inputs : {};
25
+ add(r.id, { urn: r.urn, prop: "id", input: false });
26
+ for (const [k, v] of Object.entries(isObject(r.outputs) ? r.outputs : {})) {
27
+ if (isSecret(v))
28
+ continue;
29
+ add(v, { urn: r.urn, prop: k, input: k in inputs });
30
+ }
31
+ }
32
+ return by;
33
+ }
34
+ function verdictOf(h, entry, targeted) {
35
+ if (!entry)
36
+ return targeted ? "stable" : "changes";
37
+ if (entry.mayAppear === true || entry.action === "replace" || entry.action === "delete" || entry.action === "forget")
38
+ return "changes";
39
+ if (entry.action !== "update")
40
+ return "stable";
41
+ const attr = entry.attributes.find((a) => a.path === h.prop);
42
+ if (attr)
43
+ return "changes";
44
+ if (h.input || STABLE_OUTPUTS.has(h.prop))
45
+ return "stable";
46
+ return "maybe";
47
+ }
48
+ /**
49
+ * Trace each of the export's stack outputs through the member's part (the
50
+ * change set its preview became). `targeted` says the preview was narrowed
51
+ * with `--target`, so a resource it did not list is not touched by it.
52
+ */
53
+ export function pulumiPlannedOutputChanges(input) {
54
+ const byUrn = new Map();
55
+ for (const e of input.part.entries)
56
+ if (!byUrn.has(e.address))
57
+ byUrn.set(e.address, e);
58
+ const holders = holdersByValue(input.exported);
59
+ const targeted = input.targeted === true;
60
+ const traceString = (value) => {
61
+ const list = holders.get(value) ?? [];
62
+ if (list.length === 0)
63
+ return { verdict: "untraced" };
64
+ const judged = list.map((h) => ({ h, v: verdictOf(h, byUrn.get(h.urn), targeted), action: byUrn.get(h.urn)?.action ?? "unlisted" }));
65
+ const why = (j) => `${tailOf(j.h.urn)}.${j.h.prop} (${j.action})`;
66
+ const changes = judged.find((j) => j.v === "changes");
67
+ if (changes)
68
+ return { verdict: "changing", because: why(changes) };
69
+ const stable = judged.find((j) => j.v === "stable");
70
+ if (stable)
71
+ return { verdict: "unchanged", because: why(stable) };
72
+ return { verdict: "changing", because: why(judged[0]) };
73
+ };
74
+ const outputs = [];
75
+ const known = {};
76
+ for (const o of pulumiExportedStackOutputs(input.exported)) {
77
+ if (o.secret)
78
+ continue;
79
+ let traced;
80
+ if (typeof o.value === "string")
81
+ traced = traceString(o.value);
82
+ else if (Array.isArray(o.value) && o.value.length > 0 && o.value.every((v) => typeof v === "string")) {
83
+ const each = o.value.map(traceString);
84
+ traced = each.find((t) => t.verdict === "changing") ?? each.find((t) => t.verdict === "untraced") ?? each[0];
85
+ }
86
+ else
87
+ traced = { verdict: "untraced" };
88
+ outputs.push({ output: o.name, verdict: traced.verdict, ...(traced.because ? { because: traced.because } : {}) });
89
+ if (traced.verdict === "unchanged")
90
+ known[o.name] = o.value;
91
+ }
92
+ outputs.sort((a, b) => (a.output < b.output ? -1 : a.output > b.output ? 1 : 0));
93
+ return { outputs, known, unknown: outputs.filter((o) => o.verdict !== "unchanged").map((o) => o.output) };
94
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAoC/E,eAAO,MAAM,YAAY,EAAE,aAwK1B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAmB,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAoC/E,eAAO,MAAM,YAAY,EAAE,aAqL1B,CAAC"}