@intentius/chant-lexicon-pulumi 0.124.58 → 0.124.59

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.
@@ -13,8 +13,12 @@
13
13
  * | plunge | unset | `plunge drift --resource <urn> --stream`, once per resource in the export |
14
14
  * | plunge | `<urn>` | the same, for that URN alone |
15
15
  *
16
- * Each read passes `--stack` (#4496): the member's declared
17
- * `pulumi.member.stack`, else the stack the export's URNs name.
16
+ * Each read passes `--stack` (#4496): the stack asked for (`chant workspace
17
+ * drift --stack`), else the member's declared `pulumi.member.stack`, else the
18
+ * stack the export's URNs name. A stack asked for is read from that stack's
19
+ * export in the index store alone; with none there the read is refused
20
+ * (`no-drift-read`), where without `--stack` the member reads the store's
21
+ * export, then the checked-in file.
18
22
  *
19
23
  * The one read is `refresh`. The member's engine is `pulumiMemberEngine`'s:
20
24
  * the member config's `engine`, then a plunge backend URL in `Pulumi.yaml`.
@@ -25,10 +29,13 @@
25
29
  */
26
30
  import type { ReadDriftOptions, ReadDriftResult } from "@intentius/chant/drift-record";
27
31
  import { type PulumiRunner } from "./run.js";
32
+ import type { SnapshotStore } from "@intentius/chant/lifecycle/snapshot-store";
28
33
  /** The runners, injectable so the tests run no binary. */
29
34
  export interface PulumiDriftRunners {
30
35
  run?: PulumiRunner;
31
36
  plunge?: PulumiRunner;
37
+ /** The index store the exports are read from. Default: the project's. */
38
+ exportStore?: SnapshotStore | null;
32
39
  }
33
40
  /** `LexiconPlugin.readDrift` for the pulumi lexicon. */
34
41
  export declare function readPulumiDrift(options: ReadDriftOptions, runners?: PulumiDriftRunners): Promise<ReadDriftResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"drift-read.d.ts","sourceRoot":"","sources":["../src/drift-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAA0B,gBAAgB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAG/G,OAAO,EAA2C,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAEnF,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CAmC3H"}
1
+ {"version":3,"file":"drift-read.d.ts","sourceRoot":"","sources":["../src/drift-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAA0B,gBAAgB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAG/G,OAAO,EAA2C,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAEnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAE/E,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,YAAY,CAAC;IACnB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,yEAAyE;IACzE,WAAW,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;CACpC;AAED,wDAAwD;AACxD,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,eAAe,CAAC,CAgD3H"}
@@ -1,5 +1,6 @@
1
1
  import { driftRecordLine, driftSettle, driftStart } from "@intentius/chant/drift-record";
2
2
  import { pulumiMemberEngine, refreshPulumiMember } from "./run.js";
3
+ import { readStoredPulumiExport } from "./stack-export-snapshot.js";
3
4
  /** `LexiconPlugin.readDrift` for the pulumi lexicon. */
4
5
  export async function readPulumiDrift(options, runners = {}) {
5
6
  const member = options.config.pulumi?.member;
@@ -9,6 +10,18 @@ export async function readPulumiDrift(options, runners = {}) {
9
10
  return { refused: { code: "read-unsupported", message: `a Pulumi member answers refresh, not ${options.read}` } };
10
11
  }
11
12
  const { engine } = pulumiMemberEngine(member.dir, member.engine);
13
+ const stack = options.stack ?? member.stack;
14
+ if (options.stack !== undefined) {
15
+ const stored = await readStoredPulumiExport({ dir: member.dir, stack: options.stack, ...(runners.exportStore !== undefined ? { store: runners.exportStore } : {}) });
16
+ if (!stored) {
17
+ return {
18
+ refused: {
19
+ code: "no-drift-read",
20
+ message: `the index store holds no export for stack ${options.stack} of ${options.member}; the pulumi-up capability writes one after each apply through chant, so apply that stack once, or read without --stack`,
21
+ },
22
+ };
23
+ }
24
+ }
12
25
  let records;
13
26
  try {
14
27
  records = await refreshPulumiMember({
@@ -16,7 +29,8 @@ export async function readPulumiDrift(options, runners = {}) {
16
29
  dir: member.dir,
17
30
  observedAt: options.observedAt,
18
31
  ...(member.engine ? { engine: member.engine } : {}),
19
- ...(member.stack ? { stack: member.stack } : {}),
32
+ ...(stack ? { stack } : {}),
33
+ ...(runners.exportStore !== undefined ? { exportStore: runners.exportStore } : {}),
20
34
  ...(options.resource !== undefined ? { urns: [options.resource] } : {}),
21
35
  ...(runners.run ? { run: runners.run } : {}),
22
36
  ...(runners.plunge ? { plunge: runners.plunge } : {}),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "d9975374a07d5764ef1b0b209c48fc2ec1b4ec4669808c6d6b1b6429d23a97d6",
4
+ "manifest.json": "36b210d8802b50b7293b719add906eb945a655ac56fb904660c7c6f210363318",
5
5
  "meta.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
6
6
  "types/index.d.ts": "883bcdf6a960e21360e79df010ca2087719fffec60fc39541c5ecd32d661e328",
7
7
  "rules/member-dir-absolute.ts": "4805d5e247ad544f942e1b2c36fc15a5087ca0aacaf804183a269021bd60e276",
@@ -20,10 +20,10 @@
20
20
  "rules/pul012.ts": "136f9764a532f56ce72461641550c9c4cee6af06c30968d8374f1a65a1b810d4",
21
21
  "rules/pul013.ts": "a19b397fecc2bda3e5f32c8f339b0befbd8f1148b32e70a3ba4f9370ee103379",
22
22
  "rules/pul014.ts": "25dd5ca4cb7f9906c608f4b7f78559efd188215c9a9a2f9f131498a847d3259b",
23
- "rules/pul015.ts": "f5eb45e941dfbe1ec53a31fd12258d7adb9877de33b5bcc566a09e3a0829d9da",
23
+ "rules/pul015.ts": "d6805c3b8bfacf0fc4a08fb466b066d949a8a5d164b7927cebc50111d1807ae9",
24
24
  "skills/chant-pulumi.md": "aef93407423daf6d0acc492a4d5bbf002fbae67b4f993ad36f86725686b98d61",
25
25
  "skills/chant-pulumi-stack-export.md": "aadd67f5e067de61dfda43d57492bca08bd22429e4baf314d6856a810dc0e8dc",
26
26
  "skills/chant-pulumi-plunge.md": "8a7f2f9f1124744dee9645905f141057ca01f5a88e34e1bc3858d69406d7ff3a"
27
27
  },
28
- "composite": "a4603282326e0ed1abf7f6c24fd702e35975fbbc9b1cdee72341269a8f9a9ac1"
28
+ "composite": "e0f89c0f3684dc4a84e5ca6d311e2901881c5bbd892bd5cbc8eabbe1e367bd76"
29
29
  }
@@ -9,8 +9,9 @@
9
9
  * `plunge+file://...` or `plunge://...` runs stock `pulumi` against a
10
10
  * backend scheme only plunge opens, and every read fails.
11
11
  *
12
- * The other direction, `engine: "plunge"` with a stock backend, is not
13
- * reported: plunge is a Pulumi fork and the config may mean it.
12
+ * One direction only, by maintainer ruling (#4559): `engine: "plunge"` with a
13
+ * stock backend is not reported, because plunge opens stock backends and the
14
+ * config may mean it.
14
15
  */
15
16
  import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
16
17
  export declare const pul015: PostSynthCheck;
@@ -1 +1 @@
1
- {"version":3,"file":"pul015.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/pul015.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAK9G,eAAO,MAAM,MAAM,EAAE,cAkBpB,CAAC"}
1
+ {"version":3,"file":"pul015.d.ts","sourceRoot":"","sources":["../../../src/lint/post-synth/pul015.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,cAAc,EAAyC,MAAM,kCAAkC,CAAC;AAK9G,eAAO,MAAM,MAAM,EAAE,cAkBpB,CAAC"}
@@ -3,7 +3,7 @@ import { lastMemberRead } from "../../workspace-member.js";
3
3
  import { finding } from "./member-read-helpers.js";
4
4
  export const pul015 = {
5
5
  id: "PUL015",
6
- description: "pulumi.member.engine says pulumi, but Pulumi.yaml names a plunge backend",
6
+ description: "pulumi.member.engine says pulumi, but the resolved engine is plunge",
7
7
  check(_ctx) {
8
8
  const read = lastMemberRead();
9
9
  if (read?.engine !== "pulumi")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulumi",
3
- "version": "0.124.58",
3
+ "version": "0.124.59",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Pulumi"
6
6
  }
package/dist/okf/index.md CHANGED
@@ -18,5 +18,5 @@ okf_version: '0.2'
18
18
  * [PUL012](/rules/PUL012.md) - The pulumi member's stack export holds resources the stack reads and does not manage (external: true)
19
19
  * [PUL013](/rules/PUL013.md) - The pulumi member's stack export has a deployment version other than 3, the one chant reads
20
20
  * [PUL014](/rules/PUL014.md) - The pulumi member's stack export reads but holds no resources besides the stack and its providers
21
- * [PUL015](/rules/PUL015.md) - pulumi.member.engine says pulumi, but Pulumi.yaml names a plunge backend
21
+ * [PUL015](/rules/PUL015.md) - pulumi.member.engine says pulumi, but the resolved engine is plunge
22
22
  * [PUL101](/rules/PUL101.md) - `pulumi.member.dir` is an absolute path written in source.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  type: post-synth-check
3
3
  title: PUL015
4
- description: pulumi.member.engine says pulumi, but Pulumi.yaml names a plunge backend
4
+ description: pulumi.member.engine says pulumi, but the resolved engine is plunge
5
5
  id: PUL015
6
6
  severity: error
7
7
  category: post-synth
8
8
  lexicon: pulumi
9
9
  docs: https://intentius.io/chant/lexicons/pulumi/rules/
10
10
  ---
11
- pulumi.member.engine says pulumi, but Pulumi.yaml names a plunge backend
11
+ pulumi.member.engine says pulumi, but the resolved engine is plunge
@@ -9,8 +9,9 @@
9
9
  * `plunge+file://...` or `plunge://...` runs stock `pulumi` against a
10
10
  * backend scheme only plunge opens, and every read fails.
11
11
  *
12
- * The other direction, `engine: "plunge"` with a stock backend, is not
13
- * reported: plunge is a Pulumi fork and the config may mean it.
12
+ * One direction only, by maintainer ruling (#4559): `engine: "plunge"` with a
13
+ * stock backend is not reported, because plunge opens stock backends and the
14
+ * config may mean it.
14
15
  */
15
16
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
16
17
  import { pulumiMemberEngine } from "../../run";
@@ -19,7 +20,7 @@ import { finding } from "./member-read-helpers";
19
20
 
20
21
  export const pul015: PostSynthCheck = {
21
22
  id: "PUL015",
22
- description: "pulumi.member.engine says pulumi, but Pulumi.yaml names a plunge backend",
23
+ description: "pulumi.member.engine says pulumi, but the resolved engine is plunge",
23
24
 
24
25
  check(_ctx: PostSynthContext): PostSynthDiagnostic[] {
25
26
  const read = lastMemberRead();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-pulumi",
3
- "version": "0.124.58",
3
+ "version": "0.124.59",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/",
@@ -54,7 +54,7 @@
54
54
  "access": "public"
55
55
  },
56
56
  "peerDependencies": {
57
- "@intentius/chant": "^0.124.58",
57
+ "@intentius/chant": "^0.124.59",
58
58
  "zod": "^4.3.6"
59
59
  },
60
60
  "description": "pulumi lexicon for chant: a Pulumi program as a workspace member, its preview in the change set and its refresh as drift records",
@@ -9,13 +9,16 @@
9
9
  * `plunge-member`'s export is a cut of the recorded plunge stack's. No binary
10
10
  * runs here.
11
11
  */
12
- import { readFileSync } from "node:fs";
12
+ import { cpSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
13
14
  import { join } from "node:path";
14
15
  import { describe, expect, test } from "vitest";
16
+ import { createSnapshotStore } from "@intentius/chant/lifecycle/snapshot-store";
15
17
  import { foldDriftLines, type ReadDriftOptions } from "@intentius/chant/drift-record";
16
18
  import { readPulumiDrift } from "./drift-read";
17
19
  import { pulumiPlugin } from "./plugin";
18
20
  import type { PulumiRunner } from "./run";
21
+ import { writePulumiExportSnapshot } from "./stack-export-snapshot";
19
22
 
20
23
  const testdata = join(import.meta.dirname, "testdata");
21
24
  const text = (...path: string[]) => readFileSync(join(testdata, ...path), "utf-8");
@@ -104,3 +107,72 @@ describe("readPulumiDrift", () => {
104
107
  expect(never.calls).toEqual([]);
105
108
  });
106
109
  });
110
+
111
+ describe("readPulumiDrift with --stack", () => {
112
+ const exportText = text("pulumi-preview", "stack-export.json");
113
+
114
+ /** A member directory with Pulumi.yaml and no checked-in export, and a store holding the stacks named. */
115
+ async function memberWithStore(stacks: string[]) {
116
+ const dir = mkdtempSync(join(tmpdir(), "pulumi-drift-stack-"));
117
+ cpSync(join(testdata, "pulumi-preview", "Pulumi.yaml"), join(dir, "Pulumi.yaml"));
118
+ mkdirSync(join(dir, "node_modules"));
119
+ const store = createSnapshotStore({ store: "local", dir: mkdtempSync(join(tmpdir(), "pulumi-drift-store-")) });
120
+ for (const stack of stacks) {
121
+ const wrote = await writePulumiExportSnapshot({
122
+ dir,
123
+ stack,
124
+ environment: "prod",
125
+ engine: "pulumi",
126
+ run: async () => ({ stdout: exportText }),
127
+ store,
128
+ });
129
+ expect(wrote.written, JSON.stringify(wrote)).toBe(true);
130
+ }
131
+ return { dir, store };
132
+ }
133
+
134
+ test("reads the named stack's stored export and passes that stack to every refresh", async () => {
135
+ const { dir, store } = await memberWithStore(["staging", "prod-eu"]);
136
+ const pulumi = fake(() => text("pulumi-refresh", "bucket-edited.json"));
137
+ const answer = await readPulumiDrift(options(dir, { stack: "prod-eu" }), { run: pulumi.run, plunge: never.run, exportStore: store });
138
+ if (!("lines" in answer)) throw new Error(JSON.stringify(answer));
139
+ expect(pulumi.calls.length).toBeGreaterThan(0);
140
+ for (const call of pulumi.calls) expect(call.slice(call.indexOf("--stack"), call.indexOf("--stack") + 2)).toEqual(["--stack", "prod-eu"]);
141
+ });
142
+
143
+ test("beats the member's declared stack", async () => {
144
+ const { dir, store } = await memberWithStore(["staging", "prod-eu"]);
145
+ const pulumi = fake(() => text("pulumi-refresh", "bucket-edited.json"));
146
+ const config = { pulumi: { member: { dir, label: "site", stack: "staging" } } };
147
+ const answer = await readPulumiDrift({ ...options(dir, { stack: "prod-eu", resource: bucket }), config }, { run: pulumi.run, plunge: never.run, exportStore: store });
148
+ expect("lines" in answer).toBe(true);
149
+ expect(pulumi.calls).toEqual([["refresh", "--preview-only", "--json", "--stack", "prod-eu", "--target", bucket]]);
150
+ });
151
+
152
+ test("a store with no export for that stack is an error naming the stack, and nothing runs", async () => {
153
+ const { dir, store } = await memberWithStore(["staging"]);
154
+ const spy = fake(() => text("pulumi-refresh", "bucket-edited.json"));
155
+ const answer = await readPulumiDrift(options(dir, { stack: "prod-eu" }), { run: spy.run, plunge: never.run, exportStore: store });
156
+ expect(answer).toMatchObject({ refused: { code: "no-drift-read" } });
157
+ expect("refused" in answer && answer.refused.message).toMatch(/holds no export for stack prod-eu of site/);
158
+ expect(spy.calls).toEqual([]);
159
+ });
160
+
161
+ test("an empty store is refused too, though a checked-in export sits beside the member", async () => {
162
+ const dir = join(testdata, "pulumi-preview");
163
+ const store = createSnapshotStore({ store: "local", dir: mkdtempSync(join(tmpdir(), "pulumi-drift-store-")) });
164
+ const spy = fake(() => text("pulumi-refresh", "bucket-edited.json"));
165
+ const answer = await readPulumiDrift(options(dir, { stack: "floci" }), { run: spy.run, plunge: never.run, exportStore: store });
166
+ expect(answer).toMatchObject({ refused: { code: "no-drift-read" } });
167
+ expect(spy.calls).toEqual([]);
168
+ });
169
+
170
+ test("without --stack the read is as before: the checked-in export when the store has none", async () => {
171
+ const dir = join(testdata, "pulumi-preview");
172
+ const store = createSnapshotStore({ store: "local", dir: mkdtempSync(join(tmpdir(), "pulumi-drift-store-")) });
173
+ const pulumi = fake(() => text("pulumi-refresh", "bucket-edited.json"));
174
+ const answer = await readPulumiDrift(options(dir, { resource: bucket }), { run: pulumi.run, plunge: never.run, exportStore: store });
175
+ expect("lines" in answer).toBe(true);
176
+ expect(pulumi.calls).toEqual([["refresh", "--preview-only", "--json", "--stack", "floci", "--target", bucket]]);
177
+ });
178
+ });
package/src/drift-read.ts CHANGED
@@ -13,8 +13,12 @@
13
13
  * | plunge | unset | `plunge drift --resource <urn> --stream`, once per resource in the export |
14
14
  * | plunge | `<urn>` | the same, for that URN alone |
15
15
  *
16
- * Each read passes `--stack` (#4496): the member's declared
17
- * `pulumi.member.stack`, else the stack the export's URNs name.
16
+ * Each read passes `--stack` (#4496): the stack asked for (`chant workspace
17
+ * drift --stack`), else the member's declared `pulumi.member.stack`, else the
18
+ * stack the export's URNs name. A stack asked for is read from that stack's
19
+ * export in the index store alone; with none there the read is refused
20
+ * (`no-drift-read`), where without `--stack` the member reads the store's
21
+ * export, then the checked-in file.
18
22
  *
19
23
  * The one read is `refresh`. The member's engine is `pulumiMemberEngine`'s:
20
24
  * the member config's `engine`, then a plunge backend URL in `Pulumi.yaml`.
@@ -27,11 +31,15 @@ import type { DriftLine, DriftRecord, ReadDriftOptions, ReadDriftResult } from "
27
31
  import { driftRecordLine, driftSettle, driftStart } from "@intentius/chant/drift-record";
28
32
  import type { PulumiConfig } from "./config";
29
33
  import { pulumiMemberEngine, refreshPulumiMember, type PulumiRunner } from "./run";
34
+ import { readStoredPulumiExport } from "./stack-export-snapshot";
35
+ import type { SnapshotStore } from "@intentius/chant/lifecycle/snapshot-store";
30
36
 
31
37
  /** The runners, injectable so the tests run no binary. */
32
38
  export interface PulumiDriftRunners {
33
39
  run?: PulumiRunner;
34
40
  plunge?: PulumiRunner;
41
+ /** The index store the exports are read from. Default: the project's. */
42
+ exportStore?: SnapshotStore | null;
35
43
  }
36
44
 
37
45
  /** `LexiconPlugin.readDrift` for the pulumi lexicon. */
@@ -42,6 +50,18 @@ export async function readPulumiDrift(options: ReadDriftOptions, runners: Pulumi
42
50
  return { refused: { code: "read-unsupported", message: `a Pulumi member answers refresh, not ${options.read}` } };
43
51
  }
44
52
  const { engine } = pulumiMemberEngine(member.dir, member.engine);
53
+ const stack = options.stack ?? member.stack;
54
+ if (options.stack !== undefined) {
55
+ const stored = await readStoredPulumiExport({ dir: member.dir, stack: options.stack, ...(runners.exportStore !== undefined ? { store: runners.exportStore } : {}) });
56
+ if (!stored) {
57
+ return {
58
+ refused: {
59
+ code: "no-drift-read",
60
+ message: `the index store holds no export for stack ${options.stack} of ${options.member}; the pulumi-up capability writes one after each apply through chant, so apply that stack once, or read without --stack`,
61
+ },
62
+ };
63
+ }
64
+ }
45
65
  let records: DriftRecord[];
46
66
  try {
47
67
  records = await refreshPulumiMember({
@@ -49,7 +69,8 @@ export async function readPulumiDrift(options: ReadDriftOptions, runners: Pulumi
49
69
  dir: member.dir,
50
70
  observedAt: options.observedAt,
51
71
  ...(member.engine ? { engine: member.engine } : {}),
52
- ...(member.stack ? { stack: member.stack } : {}),
72
+ ...(stack ? { stack } : {}),
73
+ ...(runners.exportStore !== undefined ? { exportStore: runners.exportStore } : {}),
53
74
  ...(options.resource !== undefined ? { urns: [options.resource] } : {}),
54
75
  ...(runners.run ? { run: runners.run } : {}),
55
76
  ...(runners.plunge ? { plunge: runners.plunge } : {}),
@@ -1,5 +1,7 @@
1
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
1
3
  import { join } from "node:path";
2
- import { afterEach, describe, expect, test } from "vitest";
4
+ import { afterEach, describe, expect, test, vi } from "vitest";
3
5
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
4
6
  import { PULUMI_SECRET_SIG, PULUMI_SIG_KEY } from "../../values";
5
7
  import { setLastMemberRead, setLastMemberReport } from "../../workspace-member";
@@ -23,6 +25,7 @@ import { pul015 } from "./pul015";
23
25
  const ctx = { outputs: new Map(), entities: new Map() } as unknown as PostSynthContext;
24
26
 
25
27
  afterEach(() => {
28
+ vi.unstubAllEnvs();
26
29
  setLastMemberReport(undefined);
27
30
  setLastMemberRead(undefined);
28
31
  });
@@ -196,4 +199,47 @@ describe("pulumi post-synth checks", () => {
196
199
  readWith([], { dir: stockDir, engine: "plunge" });
197
200
  expect(pul015.check(ctx)).toEqual([]);
198
201
  });
202
+
203
+ test("PUL015 names the source of the resolved plunge engine", () => {
204
+ // Pulumi.yaml
205
+ readWith([], { dir: plungeDir, engine: "pulumi" });
206
+ expect(pul015.check(ctx)[0].message).toContain("Pulumi.yaml names the plunge backend");
207
+
208
+ // PULUMI_BACKEND_URL, on a program whose Pulumi.yaml names no backend
209
+ vi.stubEnv("PULUMI_BACKEND_URL", "plunge+s3://bucket/state");
210
+ readWith([], { dir: stockDir, engine: "pulumi" });
211
+ const fromEnv = pul015.check(ctx);
212
+ expect(fromEnv).toHaveLength(1);
213
+ expect(fromEnv[0].message).toContain("PULUMI_BACKEND_URL names the plunge backend plunge+s3://bucket/state");
214
+ readWith([], { dir: stockDir, engine: "plunge" });
215
+ expect(pul015.check(ctx)).toEqual([]);
216
+ vi.stubEnv("PULUMI_BACKEND_URL", "s3://bucket/state");
217
+ readWith([], { dir: stockDir, engine: "pulumi" });
218
+ expect(pul015.check(ctx)).toEqual([]);
219
+ vi.unstubAllEnvs();
220
+
221
+ // plywood.yml, engine and backend
222
+ const root = mkdtempSync(join(tmpdir(), "pul015-"));
223
+ mkdirSync(join(root, ".git"));
224
+ const member = join(root, "infra");
225
+ mkdirSync(member);
226
+ writeFileSync(join(member, "Pulumi.yaml"), "name: app\nruntime: nodejs\n");
227
+ writeFileSync(join(root, "plywood.yml"), "engine: plunge\n");
228
+ readWith([], { dir: member, engine: "pulumi" });
229
+ const fromEngine = pul015.check(ctx);
230
+ expect(fromEngine).toHaveLength(1);
231
+ expect(fromEngine[0].message).toContain("plywood.yml says engine: plunge");
232
+ writeFileSync(join(root, "plywood.yml"), "backend: plunge+file:///state\n");
233
+ const fromBackend = pul015.check(ctx);
234
+ expect(fromBackend).toHaveLength(1);
235
+ expect(fromBackend[0].message).toContain("plywood.yml names the plunge backend plunge+file:///state");
236
+ writeFileSync(join(root, "plywood.yml"), "engine: pulumi\n");
237
+ expect(pul015.check(ctx)).toEqual([]);
238
+ });
239
+
240
+ test("PUL015 does not report the other direction: engine plunge on a stock backend", () => {
241
+ vi.stubEnv("PULUMI_BACKEND_URL", "s3://bucket/state");
242
+ readWith([], { dir: stockDir, engine: "plunge" });
243
+ expect(pul015.check(ctx)).toEqual([]);
244
+ });
199
245
  });
@@ -9,8 +9,9 @@
9
9
  * `plunge+file://...` or `plunge://...` runs stock `pulumi` against a
10
10
  * backend scheme only plunge opens, and every read fails.
11
11
  *
12
- * The other direction, `engine: "plunge"` with a stock backend, is not
13
- * reported: plunge is a Pulumi fork and the config may mean it.
12
+ * One direction only, by maintainer ruling (#4559): `engine: "plunge"` with a
13
+ * stock backend is not reported, because plunge opens stock backends and the
14
+ * config may mean it.
14
15
  */
15
16
  import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
16
17
  import { pulumiMemberEngine } from "../../run";
@@ -19,7 +20,7 @@ import { finding } from "./member-read-helpers";
19
20
 
20
21
  export const pul015: PostSynthCheck = {
21
22
  id: "PUL015",
22
- description: "pulumi.member.engine says pulumi, but Pulumi.yaml names a plunge backend",
23
+ description: "pulumi.member.engine says pulumi, but the resolved engine is plunge",
23
24
 
24
25
  check(_ctx: PostSynthContext): PostSynthDiagnostic[] {
25
26
  const read = lastMemberRead();