@intentius/plywood 0.3.0 → 0.3.2

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.
package/dist/plywood.mjs CHANGED
@@ -260,13 +260,16 @@ function approvalOf(doc) {
260
260
  function gateOf(doc) {
261
261
  return typeof doc.gate === "string" && GATES.includes(doc.gate) ? doc.gate : DEFAULT_GATE;
262
262
  }
263
+ function previewOf(doc) {
264
+ return doc.preview === true;
265
+ }
263
266
  function quorumOf(rules) {
264
267
  return Object.entries(rules).map(([glob, r]) => {
265
268
  const roles = [...r.role !== void 0 ? [r.role] : [], ...r.roles ?? []];
266
269
  return { targets: [glob], count: r.quorum ?? 1, ...roles.length ? { roles } : {}, notAuthor: true };
267
270
  });
268
271
  }
269
- var GATES, DEFAULT_GATE, APPROVALS, DEFAULT_APPROVAL, APPROVAL_RULE_KEYS, approval;
272
+ var GATES, DEFAULT_GATE, APPROVALS, DEFAULT_APPROVAL, DEFAULT_PREVIEW, APPROVAL_RULE_KEYS, approval;
270
273
  var init_approval = __esm({
271
274
  "src/config/approval.ts"() {
272
275
  "use strict";
@@ -275,13 +278,19 @@ var init_approval = __esm({
275
278
  DEFAULT_GATE = "on-destructive";
276
279
  APPROVALS = ["ledger", "sealed", "pr-review"];
277
280
  DEFAULT_APPROVAL = "ledger";
281
+ DEFAULT_PREVIEW = false;
278
282
  APPROVAL_RULE_KEYS = ["role", "roles", "quorum"];
279
283
  approval = {
280
284
  name: "approval",
281
- keys: ["gate", "approval", "approval_rules"],
285
+ keys: ["gate", "approval", "approval_rules", "preview"],
282
286
  schema: {
283
287
  gate: { type: "string", enum: [...GATES], default: DEFAULT_GATE, description: "always, on-destructive or never." },
284
288
  approval: { type: "string", enum: [...APPROVALS], default: DEFAULT_APPROVAL, description: "ledger, sealed or pr-review." },
289
+ preview: {
290
+ type: "boolean",
291
+ default: DEFAULT_PREVIEW,
292
+ description: "true: plywood init sets approvals.preview in the waves spec, so a merged wave applies only the plans its pull request previewed, or exits 4. Known limits: plywood#55 (the second of two disjoint merges is refused) and plywood#56 (a pull request that creates an autonamed resource is refused)."
293
+ },
285
294
  approval_rules: {
286
295
  type: "object",
287
296
  description: "Role and quorum per stack glob. Read at base.",
@@ -298,6 +307,7 @@ var init_approval = __esm({
298
307
  },
299
308
  check(doc) {
300
309
  const out = [...oneOf(doc.gate, GATES, "gate"), ...oneOf(doc.approval, APPROVALS, "approval")];
310
+ if (doc.preview !== void 0 && typeof doc.preview !== "boolean") out.push(error("preview", "must be true or false"));
301
311
  const rules = doc.approval_rules;
302
312
  if (rules === void 0) return out;
303
313
  if (!isObject(rules)) return [...out, error("approval_rules", "must map a stack glob to { role, quorum }")];
@@ -422,7 +432,7 @@ var init_words = __esm({
422
432
  "src/words.ts"() {
423
433
  "use strict";
424
434
  PRODUCT = "plywood";
425
- TAGLINE = "Sophisticated automation for Pulumi.";
435
+ TAGLINE = "Pulumi ops and governance.";
426
436
  OP_NAME = "pulumi-up";
427
437
  PR_GATE = "up";
428
438
  VERBS = [
@@ -587,13 +597,13 @@ var init_control = __esm({
587
597
  var version, dependencies;
588
598
  var init_package = __esm({
589
599
  "package.json"() {
590
- version = "0.3.0";
600
+ version = "0.3.2";
591
601
  dependencies = {
592
- "@intentius/chant": "0.124.59",
593
- "@intentius/chant-lexicon-forgejo": "0.124.59",
594
- "@intentius/chant-lexicon-github": "0.124.59",
595
- "@intentius/chant-lexicon-gitlab": "0.124.59",
596
- "@intentius/chant-lexicon-pulumi": "0.124.59"
602
+ "@intentius/chant": "0.124.67",
603
+ "@intentius/chant-lexicon-forgejo": "0.124.67",
604
+ "@intentius/chant-lexicon-github": "0.124.67",
605
+ "@intentius/chant-lexicon-gitlab": "0.124.67",
606
+ "@intentius/chant-lexicon-pulumi": "0.124.67"
597
607
  };
598
608
  }
599
609
  });
@@ -603,8 +613,8 @@ var image_digests_default;
603
613
  var init_image_digests = __esm({
604
614
  "src/image-digests.json"() {
605
615
  image_digests_default = {
606
- "ghcr.io/intentius/plywood-pulumi:0.3.0-pulumi3.272.0": "sha256:39fc85f67be6a281aa5f01b02239aa1f02c492f40f1fb0d145c848cc608aaecd",
607
- "ghcr.io/intentius/plywood-plunge:0.3.0-plunge0.2.0": "sha256:6647f67defb1719395b29d1a6c9d977b6900e228547c4569563962e4234847e3"
616
+ "ghcr.io/intentius/plywood-pulumi:0.3.2-pulumi3.272.0": "sha256:eeb44ac18dc594acd8a56dfc1a6ed00751154f6b6bfba8bbdff97a513f4b9f84",
617
+ "ghcr.io/intentius/plywood-plunge:0.3.2-plunge0.2.0": "sha256:5239ffabd5f5b7a4ffc5cbbe1628a34990bc95549cd4a1bef27953d3dfb7d8a9"
608
618
  };
609
619
  }
610
620
  });
@@ -2458,7 +2468,7 @@ var init_chant_product = __esm({
2458
2468
  "src/chant-product.ts"() {
2459
2469
  "use strict";
2460
2470
  REQUIRED2 = ["assertProductWords", "productPipelineJobs", "approveLine", "runChantInProcess"];
2461
- BUILT_CHANT = true ? "0.124.59" : "unknown";
2471
+ BUILT_CHANT = true ? "0.124.67" : "unknown";
2462
2472
  }
2463
2473
  });
2464
2474
 
@@ -3493,6 +3503,7 @@ function wavesSpec(input) {
3493
3503
  step: { kind: "pulumi-up" },
3494
3504
  ...input.quorum && input.quorum.length > 0 ? { approvals: { quorum: input.quorum } } : {},
3495
3505
  ...input.notify ? { notify: input.notify } : {},
3506
+ ...input.policy ? { holds: { policy: true } } : {},
3496
3507
  waves: waves.map((w) => {
3497
3508
  const gate = w.stacks.map((s) => s.gate ?? input.gate).reduce((a, b) => STRICTNESS[b] > STRICTNESS[a] ? b : a);
3498
3509
  return { name: w.name, gate, runs: w.stacks.map((s) => ({ target: s.key, input: stepInput(s) })) };
@@ -3989,7 +4000,10 @@ async function drift2(args, json, table, io) {
3989
4000
  try {
3990
4001
  const attribute = await attributionFor(doc, io);
3991
4002
  const root = dirname6(path);
3992
- for (const stack of stacks2) results.push(await readStack(chant, stack, driftMember(root, doc, stack), dir, { resource: resource.value, env: envFlag.value, ...attribute ? { attribute } : {} }));
4003
+ for (const stack of stacks2) {
4004
+ const backend = stacksOf(doc)[stack]?.backend ?? (typeof doc.backend === "string" ? doc.backend : void 0);
4005
+ results.push(await withBackend(backend, () => readStack(chant, stack, driftMember(root, doc, stack), dir, { resource: resource.value, env: envFlag.value, ...attribute ? { attribute } : {} })));
4006
+ }
3993
4007
  } finally {
3994
4008
  rmSync(dir, { recursive: true, force: true });
3995
4009
  }
@@ -4096,9 +4110,26 @@ async function readStack(chant, stack, member, dir, opts) {
4096
4110
  return { stack, ok: false, drifted: 0, rows: [], error: doc?.error ? `${doc.error.code}: ${doc.error.message}` : `chant workspace drift exited ${code}` };
4097
4111
  }
4098
4112
  const rows = driftRows(doc);
4113
+ const unread = (doc.records ?? []).filter((r) => r.status === "not-observed");
4114
+ if (rows.length === 0 && unread.length > 0) {
4115
+ const first = unread[0];
4116
+ const why = first.unobserved?.detail ?? first.unobserved?.reason ?? "no reason given";
4117
+ return { stack, ok: false, drifted: 0, rows: [], error: `${unread.length} resource${unread.length === 1 ? "" : "s"} could not be read, so the stack's drift is unknown: ${first.address}: ${why}` };
4118
+ }
4099
4119
  const attribution = rows.length && opts.attribute ? await attributeStack(opts.attribute, stack, doc) : void 0;
4100
4120
  return { stack, ok: true, drifted: rows.length, rows, observedAt: observedAt(doc), ...attribution ? { attribution } : {} };
4101
4121
  }
4122
+ async function withBackend(backend, read) {
4123
+ if (backend === void 0) return read();
4124
+ const before = process.env.PULUMI_BACKEND_URL;
4125
+ process.env.PULUMI_BACKEND_URL = backend;
4126
+ try {
4127
+ return await read();
4128
+ } finally {
4129
+ if (before === void 0) delete process.env.PULUMI_BACKEND_URL;
4130
+ else process.env.PULUMI_BACKEND_URL = before;
4131
+ }
4132
+ }
4102
4133
  async function attributionFor(doc, io) {
4103
4134
  const logs = driftSettingsOf(doc).auditLogs;
4104
4135
  if (!logs) return void 0;
@@ -5729,7 +5760,7 @@ async function runInit(opts, table, io) {
5729
5760
  if (opts.bucketCheck) notes.push(...await bucketStep(detected2, doc, io.env, buckets));
5730
5761
  adopt.push(...await adoptStep(root, detected2, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
5731
5762
  todo.push(...todoLines(detected2, doc, forge.kind, io.env));
5732
- if (forge.kind === "gitlab" && chant && stacks2.length > 0 && await hasChantWavePreview()) {
5763
+ if (forge.kind === "gitlab" && chant && stacks2.length > 0 && previewOf(doc) && await hasChantWavePreview()) {
5733
5764
  todo.push(`Add a ${GITLAB_READ_TOKEN} CI/CD variable (a project access token with the read_api scope, masked and protected) so a wave can read the merge request it applies; without it the wave refuses`);
5734
5765
  }
5735
5766
  const failed = files.find((f) => f.status === "failed");
@@ -5798,7 +5829,9 @@ async function preCommitStep(root, config2, dryRun) {
5798
5829
  }
5799
5830
  async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, forge) {
5800
5831
  const runner = waveRunner(chantPipelineOptions(settings.pipeline, false).runner);
5832
+ const rego = isObject(doc.policy) && (typeof doc.policy.rego === "string" || doc.policy.source !== void 0 || doc.policy.command !== void 0);
5801
5833
  const base = wavesSpec({
5834
+ ...rego ? { policy: true } : {},
5802
5835
  stacks: stacks2,
5803
5836
  ordered,
5804
5837
  gate: gateOf(doc),
@@ -5809,7 +5842,7 @@ async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, for
5809
5842
  ...settings.pipeline.notify ? { notify: { ...notifyForChant(settings.pipeline.notify, chant) } } : {}
5810
5843
  });
5811
5844
  const named = chant.productWavesSpec ? chant.productWavesSpec(base, PRODUCT_WORDS) : base;
5812
- return await hasChantWavePreview() ? withWavePreview(named, forge) : named;
5845
+ return previewOf(doc) && await hasChantWavePreview() ? withWavePreview(named, forge) : named;
5813
5846
  }
5814
5847
  async function workflowStep(chant, root, doc, settings, forge, opts, spec, table, io) {
5815
5848
  const output = WORKFLOW_FILE[forge];
@@ -7918,7 +7951,7 @@ var execChild = (bin, args, opts) => new Promise((done) => {
7918
7951
 
7919
7952
  // src/cli.ts
7920
7953
  init_words();
7921
- var VERSION3 = true ? "0.3.0" : "0.0.0-dev";
7954
+ var VERSION3 = true ? "0.3.2" : "0.0.0-dev";
7922
7955
  var stdio = {
7923
7956
  out: (s) => process.stdout.write(s + "\n"),
7924
7957
  err: (s) => process.stderr.write(s + "\n")