@intentius/plywood 0.3.1 → 0.3.5

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
@@ -16,6 +16,14 @@ var __export = (target, all) => {
16
16
  __defProp(target, name, { get: all[name], enumerable: true });
17
17
  };
18
18
 
19
+ // <define:__PLYWOOD_IMAGE_DIGESTS__>
20
+ var define_PLYWOOD_IMAGE_DIGESTS_default;
21
+ var init_define_PLYWOOD_IMAGE_DIGESTS = __esm({
22
+ "<define:__PLYWOOD_IMAGE_DIGESTS__>"() {
23
+ define_PLYWOOD_IMAGE_DIGESTS_default = { "ghcr.io/intentius/plywood-pulumi:0.3.5-pulumi3.272.0": "sha256:a1288d42eaba8ec556ed3ea40a24cfda94a8413576b7f0129b30ab7620639f07", "ghcr.io/intentius/plywood-plunge:0.3.5-plunge0.2.0": "sha256:4384ee4b92c8b0cb00c4833bc0118a820a0c3620ad78dc50d9e21fb0d9fff370" };
24
+ }
25
+ });
26
+
19
27
  // src/config/types.ts
20
28
  function isObject(v) {
21
29
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -45,6 +53,7 @@ var BACKEND_SCHEMA;
45
53
  var init_types = __esm({
46
54
  "src/config/types.ts"() {
47
55
  "use strict";
56
+ init_define_PLYWOOD_IMAGE_DIGESTS();
48
57
  BACKEND_SCHEMA = {
49
58
  type: "string",
50
59
  description: "The backend URL. A plunge+ URL (plunge+file://, plunge+s3://) picks the plunge engine."
@@ -89,6 +98,7 @@ var DEFAULT_KEY_ENV, VARIABLE, REVIEW_KEYS, AGENT_KEYS, modelSchema, agents;
89
98
  var init_agents = __esm({
90
99
  "src/config/agents.ts"() {
91
100
  "use strict";
101
+ init_define_PLYWOOD_IMAGE_DIGESTS();
92
102
  init_types();
93
103
  DEFAULT_KEY_ENV = "ANTHROPIC_API_KEY";
94
104
  VARIABLE = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -182,6 +192,7 @@ var APPLY_WHEN, DEFAULT_APPLY_WHEN, APPLY_REQUIRES, APPLY_MERGE, MERGE_METHODS,
182
192
  var init_apply = __esm({
183
193
  "src/config/apply.ts"() {
184
194
  "use strict";
195
+ init_define_PLYWOOD_IMAGE_DIGESTS();
185
196
  init_types();
186
197
  APPLY_WHEN = ["merge", "pull-request"];
187
198
  DEFAULT_APPLY_WHEN = "merge";
@@ -261,7 +272,7 @@ function gateOf(doc) {
261
272
  return typeof doc.gate === "string" && GATES.includes(doc.gate) ? doc.gate : DEFAULT_GATE;
262
273
  }
263
274
  function previewOf(doc) {
264
- return doc.preview === true;
275
+ return doc.preview !== false;
265
276
  }
266
277
  function quorumOf(rules) {
267
278
  return Object.entries(rules).map(([glob, r]) => {
@@ -273,12 +284,13 @@ var GATES, DEFAULT_GATE, APPROVALS, DEFAULT_APPROVAL, DEFAULT_PREVIEW, APPROVAL_
273
284
  var init_approval = __esm({
274
285
  "src/config/approval.ts"() {
275
286
  "use strict";
287
+ init_define_PLYWOOD_IMAGE_DIGESTS();
276
288
  init_types();
277
289
  GATES = ["always", "on-destructive", "never"];
278
290
  DEFAULT_GATE = "on-destructive";
279
291
  APPROVALS = ["ledger", "sealed", "pr-review"];
280
292
  DEFAULT_APPROVAL = "ledger";
281
- DEFAULT_PREVIEW = false;
293
+ DEFAULT_PREVIEW = true;
282
294
  APPROVAL_RULE_KEYS = ["role", "roles", "quorum"];
283
295
  approval = {
284
296
  name: "approval",
@@ -289,7 +301,7 @@ var init_approval = __esm({
289
301
  preview: {
290
302
  type: "boolean",
291
303
  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)."
304
+ description: "true (the default): plywood init sets approvals.preview in the waves spec, so a merged wave applies only the plans its pull request previewed, or exits 4. false leaves it out."
293
305
  },
294
306
  approval_rules: {
295
307
  type: "object",
@@ -351,6 +363,7 @@ var PLYWOOD_KEY_FOR, chantKeys;
351
363
  var init_chant_keys = __esm({
352
364
  "src/config/chant-keys.ts"() {
353
365
  "use strict";
366
+ init_define_PLYWOOD_IMAGE_DIGESTS();
354
367
  init_types();
355
368
  PLYWOOD_KEY_FOR = {
356
369
  workflow: "apply.when",
@@ -431,8 +444,9 @@ var PRODUCT, TAGLINE, OP_NAME, PR_GATE, VERBS, BIN, NOTIFY_WORDS, PRODUCT_WORDS,
431
444
  var init_words = __esm({
432
445
  "src/words.ts"() {
433
446
  "use strict";
447
+ init_define_PLYWOOD_IMAGE_DIGESTS();
434
448
  PRODUCT = "plywood";
435
- TAGLINE = "Sophisticated automation for Pulumi.";
449
+ TAGLINE = "Pulumi ops and governance.";
436
450
  OP_NAME = "pulumi-up";
437
451
  PR_GATE = "up";
438
452
  VERBS = [
@@ -496,7 +510,7 @@ ${r.tail}` : ""}`,
496
510
  };
497
511
  DRIFT_OP_PREFIX = "plywood-drift-";
498
512
  LOCK_GRAIN_WORDS = {
499
- perEngine: (engine2) => engine2 === "plunge" ? "resource, plunge's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : engine2 === "pulumi" ? "stack, stock Pulumi's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : "per engine: resource on plunge, stack on stock Pulumi (no ci.lockGrain; chant's pulumi lexicon sets it)"
513
+ perEngine: (engine3) => engine3 === "plunge" ? "resource, plunge's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : engine3 === "pulumi" ? "stack, stock Pulumi's default (no ci.lockGrain; chant's pulumi lexicon sets it)" : "per engine: resource on plunge, stack on stock Pulumi (no ci.lockGrain; chant's pulumi lexicon sets it)"
500
514
  };
501
515
  PLUNGE_TIMING_ATTRIBUTES = {
502
516
  resources: `${PRODUCT}.plunge.resources`,
@@ -525,10 +539,17 @@ function controlOf(doc) {
525
539
  }
526
540
  return out;
527
541
  }
542
+ function controlBuildArgv(doc, member) {
543
+ const settings = controlOf(doc);
544
+ if (!settings) return [];
545
+ const { repo, ...rest } = { member, ...settings };
546
+ return ["--control", repo, ...Object.entries(rest).flatMap(([k, v]) => ["--control-setting", `${k}=${v}`])];
547
+ }
528
548
  var CONTROL_KEYS, CONTROL_FORGES, control;
529
549
  var init_control = __esm({
530
550
  "src/config/control.ts"() {
531
551
  "use strict";
552
+ init_define_PLYWOOD_IMAGE_DIGESTS();
532
553
  init_words();
533
554
  init_types();
534
555
  CONTROL_KEYS = {
@@ -597,37 +618,27 @@ var init_control = __esm({
597
618
  var version, dependencies;
598
619
  var init_package = __esm({
599
620
  "package.json"() {
600
- version = "0.3.1";
621
+ version = "0.3.5";
601
622
  dependencies = {
602
- "@intentius/chant": "0.124.59",
603
- "@intentius/chant-lexicon-forgejo": "0.124.59",
604
- "@intentius/chant-lexicon-github": "0.124.59",
605
- "@intentius/chant-lexicon-gitlab": "0.124.59",
606
- "@intentius/chant-lexicon-pulumi": "0.124.59"
607
- };
608
- }
609
- });
610
-
611
- // src/image-digests.json
612
- var image_digests_default;
613
- var init_image_digests = __esm({
614
- "src/image-digests.json"() {
615
- image_digests_default = {
616
- "ghcr.io/intentius/plywood-pulumi:0.3.1-pulumi3.272.0": "sha256:acdadb5000ade3afea20ab2f555946312281066e1df288b1007bff096514ebfd",
617
- "ghcr.io/intentius/plywood-plunge:0.3.1-plunge0.2.0": "sha256:adc42bc97564ecb2074b3ee4d97f84ddb986e0f30f6a26f35b8721106c177456"
623
+ "@intentius/chant": "0.124.79",
624
+ "@intentius/chant-lexicon-forgejo": "0.124.79",
625
+ "@intentius/chant-lexicon-github": "0.124.79",
626
+ "@intentius/chant-lexicon-gitlab": "0.124.79",
627
+ "@intentius/chant-lexicon-pulumi": "0.124.79"
618
628
  };
619
629
  }
620
630
  });
621
631
 
622
632
  // src/images.ts
623
- function engineDir(engine2, version2) {
624
- return `${ENGINE_ROOT}/${engine2}-${version2}`;
633
+ import { CI_DOCKER_ARCH, ciFetchScript, plungeInstallLine, pulumiInstallLine } from "@intentius/chant-lexicon-pulumi/ci-setup";
634
+ function engineDir(engine3, version2) {
635
+ return `${ENGINE_ROOT}/${engine3}-${version2}`;
625
636
  }
626
- function engineBin(engine2, version2) {
627
- return `${engineDir(engine2, version2)}/${engine2}`;
637
+ function engineBin(engine3, version2) {
638
+ return `${engineDir(engine3, version2)}/${engine3}`;
628
639
  }
629
- function release(engine2, version2) {
630
- if (engine2 === "pulumi") {
640
+ function release(engine3, version2) {
641
+ if (engine3 === "pulumi") {
631
642
  return {
632
643
  base: `https://github.com/pulumi/pulumi/releases/download/v${version2}`,
633
644
  archive: (arch) => `pulumi-v${version2}-linux-${arch}.tar.gz`,
@@ -640,8 +651,8 @@ function release(engine2, version2) {
640
651
  checksums: `plunge-v${version2}-checksums.txt`
641
652
  };
642
653
  }
643
- function pinnedSums(engine2, version2) {
644
- const r = release(engine2, version2);
654
+ function pinnedSums(engine3, version2) {
655
+ const r = release(engine3, version2);
645
656
  const out = {};
646
657
  for (const arch of ARCHES) {
647
658
  const sum = PINNED_SHA256[r.archive(arch)];
@@ -649,54 +660,10 @@ function pinnedSums(engine2, version2) {
649
660
  }
650
661
  return out;
651
662
  }
652
- function fetchScript(engine2, version2, dir, arch, checksumsUrl) {
653
- const r = release(engine2, version2);
654
- const sums = pinnedSums(engine2, version2);
655
- const pinCase = Object.keys(sums).length ? `case "$a" in ${ARCHES.filter((x) => sums[x]).map((x) => `${x}) want=${sums[x]} ;;`).join(" ")} *) want= ;; esac` : "want=";
656
- const file = `${r.archive("x64").replace("-x64.", "-$a.")}`;
657
- return [
658
- `a=${arch}`,
659
- `f="${file}"`,
660
- `t="$(mktemp -d)"`,
661
- `curl -fsSLo "$t/$f" "${r.base}/$f"`,
662
- `curl -fsSLo "$t/sums" ${checksumsUrl ?? `"${r.base}/${r.checksums}"`}`,
663
- // The archive's line, with any ./ or * before the name taken off.
664
- `line="$(sed -n "s#^\\([0-9a-f]\\{64\\}\\) *[*.]*/*$f\\$#\\1#p" "$t/sums")"`,
665
- `{ [ -n "$line" ] || { echo "${engine2} ${version2}: $f is not in ${r.checksums}" >&2; exit 1; }; }`,
666
- pinCase,
667
- `{ [ -z "$want" ] || [ "$line" = "$want" ] || { echo "${engine2} ${version2}: ${r.checksums} lists $line for $f; plywood pins $want" >&2; exit 1; }; }`,
668
- `echo "$line $t/$f" | sha256sum -c - >/dev/null`,
669
- `mkdir -p "${dir}"`,
670
- `tar -xzf "$t/$f" -C "${dir}" --strip-components=1`
671
- ].join(" && ");
672
- }
673
- function installLine(engine2, version2, link = true) {
674
- const chant = chantLines;
675
- if (link && chant) {
676
- const sums = pinnedSums(engine2, version2);
677
- const options = { root: ENGINE_ROOT, ...Object.keys(sums).length ? { sha256: sums } : {} };
678
- return engine2 === "pulumi" ? chant.pulumiInstallLine(version2, options) : chant.plungeInstallLine(version2, options);
679
- }
680
- return plywoodInstallLine(engine2, version2, link);
681
- }
682
- function plywoodInstallLine(engine2, version2, link = true) {
683
- const dir = engineDir(engine2, version2);
684
- const linkCmd = `ln -sf "${dir}/${engine2}" /usr/local/bin/${engine2}`;
685
- const install = `(set -e; ${fetchScript(engine2, version2, dir, JOB_ARCH)})`;
686
- return link ? `{ [ -x "${dir}/${engine2}" ] || ${install}; } && ${linkCmd}` : `[ -x "${dir}/${engine2}" ] || ${install}`;
687
- }
688
- function useChantInstallLines() {
689
- loading ??= (async () => {
690
- try {
691
- const m = await import("@intentius/chant-lexicon-pulumi/ci-setup");
692
- const ok = typeof m.ciRelease === "function" && typeof m.PINNED_SHA256 === "object" && typeof m.pulumiInstallLine === "function" && typeof m.plungeInstallLine === "function";
693
- chantLines = ok ? m : void 0;
694
- } catch {
695
- chantLines = void 0;
696
- }
697
- return chantLines !== void 0;
698
- })();
699
- return loading;
663
+ function installLine(engine3, version2, link = true) {
664
+ const sums = pinnedSums(engine3, version2);
665
+ const options = { root: ENGINE_ROOT, ...Object.keys(sums).length ? { sha256: sums } : {}, link };
666
+ return engine3 === "pulumi" ? pulumiInstallLine(version2, options) : plungeInstallLine(version2, options);
700
667
  }
701
668
  function installLines(pins) {
702
669
  const seen = /* @__PURE__ */ new Set();
@@ -710,15 +677,15 @@ function installLines(pins) {
710
677
  }
711
678
  return out;
712
679
  }
713
- function imageName(engine2) {
714
- return `plywood-${engine2}`;
680
+ function imageName(engine3) {
681
+ return `plywood-${engine3}`;
715
682
  }
716
- function imageTag(engine2, version2 = version) {
717
- return `${version2}-${engine2}${TOOL_VERSIONS[engine2]}`;
683
+ function imageTag(engine3, version2 = version) {
684
+ return `${version2}-${engine3}${TOOL_VERSIONS[engine3]}`;
718
685
  }
719
- function imageFor(engine2, table = image_digests_default) {
720
- const repository = `${REGISTRY}/${imageName(engine2)}`;
721
- const tag = imageTag(engine2);
686
+ function imageFor(engine3, table = digests) {
687
+ const repository = `${REGISTRY}/${imageName(engine3)}`;
688
+ const tag = imageTag(engine3);
722
689
  const digest = table[`${repository}:${tag}`];
723
690
  return { repository, tag, ...digest ? { digest } : {} };
724
691
  }
@@ -730,23 +697,20 @@ function parsePlugin(s) {
730
697
  return m ? { kind: m[1] ?? "resource", name: m[2], version: m[3] } : void 0;
731
698
  }
732
699
  function chantBuildFlags(o) {
733
- const lines2 = [...o.beforeScript ?? [], ...o.prLoop?.engineSetup ?? []];
700
+ const lines2 = o.beforeScript ?? [];
734
701
  return [...o.image ? ["--image", o.image] : [], ...lines2.flatMap((l) => ["--before-script", l])];
735
702
  }
736
- function chantPipelineOptions(p, chantReadsEngineSetup) {
703
+ function chantPipelineOptions(p) {
737
704
  const lines2 = p.engineSetup ?? [];
738
- const out = { ...p.image ? { image: p.image } : {}, ...p.runner !== void 0 ? { runner: p.runner } : {} };
739
- if (lines2.length === 0) return out;
740
- if (!p.image && chantReadsEngineSetup) out.prLoop = { engineSetup: [...lines2] };
741
- else out.beforeScript = [...lines2];
742
- return out;
705
+ return { ...p.image ? { image: p.image } : {}, ...p.runner !== void 0 ? { runner: p.runner } : {}, ...lines2.length ? { beforeScript: [...lines2] } : {} };
743
706
  }
744
- var REGISTRY, TOOL_VERSIONS, ARCHES, PINNED_SHA256, ENGINE_ROOT, JOB_ARCH, chantLines, loading, DIGEST, PLUGIN;
707
+ var digests, REGISTRY, TOOL_VERSIONS, ARCHES, PINNED_SHA256, ENGINE_ROOT, DIGEST, RELEASE_LABEL_ARG, LABEL_REF, PLUGIN;
745
708
  var init_images = __esm({
746
709
  "src/images.ts"() {
747
710
  "use strict";
711
+ init_define_PLYWOOD_IMAGE_DIGESTS();
748
712
  init_package();
749
- init_image_digests();
713
+ digests = typeof define_PLYWOOD_IMAGE_DIGESTS_default !== "undefined" ? define_PLYWOOD_IMAGE_DIGESTS_default : {};
750
714
  REGISTRY = "ghcr.io/intentius";
751
715
  TOOL_VERSIONS = {
752
716
  pulumi: "3.272.0",
@@ -761,8 +725,9 @@ var init_images = __esm({
761
725
  "plunge-v0.2.0-linux-arm64.tar.gz": "c176a82f038164085aa19c8ad6530d6022e560e124050e535dbe364ae0473357"
762
726
  };
763
727
  ENGINE_ROOT = "/opt/plywood";
764
- JOB_ARCH = `"$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')"`;
765
728
  DIGEST = /@sha256:[0-9a-f]{64}$/;
729
+ RELEASE_LABEL_ARG = "CHANT_RELEASE_LABEL";
730
+ LABEL_REF = `\${${RELEASE_LABEL_ARG}}`;
766
731
  PLUGIN = /^(?:(resource|converter|language|analyzer|tool):)?([a-z0-9][a-z0-9-]*)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
767
732
  }
768
733
  });
@@ -831,8 +796,8 @@ function userImage(doc) {
831
796
  if (typeof doc.image === "string") return doc.image;
832
797
  return isObject(doc.image) && typeof doc.image.name === "string" ? doc.image.name : void 0;
833
798
  }
834
- function linkedVersion(engine2, doc, versions) {
835
- const top = pinFor(doc[engine2], void 0) ?? TOOL_VERSIONS[engine2];
799
+ function linkedVersion(engine3, doc, versions) {
800
+ const top = pinFor(doc[engine3], void 0) ?? TOOL_VERSIONS[engine3];
836
801
  if (versions.length === 0) return top;
837
802
  const count = /* @__PURE__ */ new Map();
838
803
  for (const v of versions) count.set(v, (count.get(v) ?? 0) + 1);
@@ -843,14 +808,15 @@ var VERSION, LABEL, RUNNER_STAGES, IMAGE_KEYS, versionSchema, runnerSpecSchema,
843
808
  var init_images2 = __esm({
844
809
  "src/config/images.ts"() {
845
810
  "use strict";
811
+ init_define_PLYWOOD_IMAGE_DIGESTS();
846
812
  init_images();
847
813
  init_types();
848
814
  VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
849
815
  LABEL = /^[^\s,]+$/;
850
816
  RUNNER_STAGES = ["default", "preview", "up", "drift"];
851
817
  IMAGE_KEYS = ["name", "plugins"];
852
- versionSchema = (engine2, example) => ({
853
- description: `The ${engine2} release the jobs run, such as ${example}: one for every stack, or a map of stack glob to release. plywood's image's release when absent.`,
818
+ versionSchema = (engine3, example) => ({
819
+ description: `The ${engine3} release the jobs run, such as ${example}: one for every stack, or a map of stack glob to release. plywood's image's release when absent.`,
854
820
  anyOf: [
855
821
  { type: "string", pattern: VERSION.source },
856
822
  { type: "object", minProperties: 1, additionalProperties: { type: "string", pattern: VERSION.source } }
@@ -910,22 +876,22 @@ var init_images2 = __esm({
910
876
  const engineOfMember = (e) => e ?? out.memberDefaults.engine ?? "pulumi";
911
877
  const per = { pulumi: [], plunge: [] };
912
878
  const chosen = out.members.map((m) => {
913
- const engine2 = engineOfMember(m.engine);
914
- const version2 = pinFor(doc[engine2], m.name) ?? pinFor(doc[engine2], void 0) ?? TOOL_VERSIONS[engine2];
915
- per[engine2].push(version2);
916
- return { m, engine: engine2, version: version2 };
879
+ const engine3 = engineOfMember(m.engine);
880
+ const version2 = pinFor(doc[engine3], m.name) ?? pinFor(doc[engine3], void 0) ?? TOOL_VERSIONS[engine3];
881
+ per[engine3].push(version2);
882
+ return { m, engine: engine3, version: version2 };
917
883
  });
918
884
  const engines = new Set(chosen.map((c) => c.engine));
919
885
  engines.add(engineOfMember(void 0));
920
886
  if (doc.plunge !== void 0) engines.add("plunge");
921
887
  const linked = Object.fromEntries([...engines].map((e) => [e, linkedVersion(e, doc, per[e])]));
922
- const pins = [...engines].map((engine2) => ({ engine: engine2, version: linked[engine2], linked: true }));
923
- for (const { m, engine: engine2, version: version2 } of chosen) {
924
- if (doc[engine2] !== void 0) m.engineVersion = version2;
925
- if (version2 !== linked[engine2]) {
926
- m.engineBin = engineBin(engine2, version2);
888
+ const pins = [...engines].map((engine3) => ({ engine: engine3, version: linked[engine3], linked: true }));
889
+ for (const { m, engine: engine3, version: version2 } of chosen) {
890
+ if (doc[engine3] !== void 0) m.engineVersion = version2;
891
+ if (version2 !== linked[engine3]) {
892
+ m.engineBin = engineBin(engine3, version2);
927
893
  m.engineRoot = ENGINE_ROOT;
928
- pins.push({ engine: engine2, version: version2, linked: false });
894
+ pins.push({ engine: engine3, version: version2, linked: false });
929
895
  }
930
896
  }
931
897
  out.pipeline.engineSetup = installLines(pins);
@@ -1051,6 +1017,7 @@ var STACK_KEYS, ACTION_KEYS, WAVES_KEYS, STACK_NAME, ACTION_NAME, ACTION_TYPE_PR
1051
1017
  var init_stacks = __esm({
1052
1018
  "src/config/stacks.ts"() {
1053
1019
  "use strict";
1020
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1054
1021
  init_approval();
1055
1022
  init_types();
1056
1023
  STACK_KEYS = ["dir", "paths", "gate", "backend", "actions"];
@@ -1215,6 +1182,7 @@ var DRIFT_RESPONSES, DEFAULT_DRIFT_RESPONSE, EVENT_RESPONSES, EVENT_KEYS, RESPON
1215
1182
  var init_drift = __esm({
1216
1183
  "src/config/drift.ts"() {
1217
1184
  "use strict";
1185
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1218
1186
  init_images2();
1219
1187
  init_stacks();
1220
1188
  init_types();
@@ -1339,6 +1307,7 @@ var ENGINES, PLUNGE_URL, engine;
1339
1307
  var init_engine = __esm({
1340
1308
  "src/config/engine.ts"() {
1341
1309
  "use strict";
1310
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1342
1311
  init_stacks();
1343
1312
  init_types();
1344
1313
  ENGINES = ["pulumi", "plunge"];
@@ -1472,6 +1441,7 @@ var IDENTITY_CLOUDS, CLOUD_SETTINGS, SETTINGS_CLOUDS, REQUIRED, IDENTITY_KEYS, S
1472
1441
  var init_identity = __esm({
1473
1442
  "src/config/identity.ts"() {
1474
1443
  "use strict";
1444
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1475
1445
  init_types();
1476
1446
  IDENTITY_CLOUDS = ["aws", "gcp", "azure", "pulumi"];
1477
1447
  CLOUD_SETTINGS = {
@@ -1630,6 +1600,7 @@ var VARIABLE3, RESERVED_VARIABLES, environmentSchema, jobs;
1630
1600
  var init_jobs = __esm({
1631
1601
  "src/config/jobs.ts"() {
1632
1602
  "use strict";
1603
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1633
1604
  init_types();
1634
1605
  init_images2();
1635
1606
  VARIABLE3 = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -1736,6 +1707,7 @@ var LATER_KEYS, later;
1736
1707
  var init_later = __esm({
1737
1708
  "src/config/later.ts"() {
1738
1709
  "use strict";
1710
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1739
1711
  LATER_KEYS = {};
1740
1712
  later = {
1741
1713
  name: "later",
@@ -1749,10 +1721,10 @@ var init_later = __esm({
1749
1721
  });
1750
1722
 
1751
1723
  // src/config/locks.ts
1752
- function lockGrainFor(doc, engine2) {
1724
+ function lockGrainFor(doc, engine3) {
1753
1725
  const set = locksOf(doc).grain;
1754
1726
  if (set) return set;
1755
- return engine2 ? DEFAULT_LOCK_GRAIN[engine2] : "per-engine";
1727
+ return engine3 ? DEFAULT_LOCK_GRAIN[engine3] : "per-engine";
1756
1728
  }
1757
1729
  function locksOf(doc) {
1758
1730
  const l = isObject(doc.locks) ? doc.locks : {};
@@ -1770,6 +1742,7 @@ var LOCKS_WHEN, DEFAULT_LOCKS_WHEN, LOCK_GRAINS, DEFAULT_LOCK_GRAIN, CI_LOCKS_OF
1770
1742
  var init_locks = __esm({
1771
1743
  "src/config/locks.ts"() {
1772
1744
  "use strict";
1745
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1773
1746
  init_engine();
1774
1747
  init_stacks();
1775
1748
  init_types();
@@ -1814,11 +1787,6 @@ var init_locks = __esm({
1814
1787
  });
1815
1788
 
1816
1789
  // src/config/observe.ts
1817
- function notifyForChant(notify2, chant) {
1818
- if (notify2.relay === void 0 || typeof chant?.NOTIFY_RELAY_FLOOR === "string") return { ...notify2 };
1819
- const { relay: _relay, ...rest } = notify2;
1820
- return rest;
1821
- }
1822
1790
  function variable(v, key) {
1823
1791
  if (v === void 0) return [];
1824
1792
  return typeof v === "string" && VARIABLE4.test(v) ? [] : [error(key, "names an environment variable, such as SLACK_WEBHOOK_URL; the URL or key itself never goes in plywood.yml")];
@@ -1849,16 +1817,16 @@ function telemetryOf(doc) {
1849
1817
  traces: t.traces !== false
1850
1818
  };
1851
1819
  }
1852
- var NOTIFY_EVENTS, VARIABLE4, DEFAULT_SERVICE_NAME, REPORT_SCHEMES, NEWER_SCHEMES, WEBHOOK_VAR, observe;
1820
+ var NOTIFY_EVENTS, VARIABLE4, DEFAULT_SERVICE_NAME, REPORT_SCHEMES, WEBHOOK_VAR, observe;
1853
1821
  var init_observe = __esm({
1854
1822
  "src/config/observe.ts"() {
1855
1823
  "use strict";
1824
+ init_define_PLYWOOD_IMAGE_DIGESTS();
1856
1825
  init_types();
1857
1826
  NOTIFY_EVENTS = { "wave-waiting": "waiting", "up-refused": "refused", "up-failed": "failed", drift: "drift" };
1858
1827
  VARIABLE4 = /^[A-Za-z_][A-Za-z0-9_]*$/;
1859
1828
  DEFAULT_SERVICE_NAME = "plywood";
1860
1829
  REPORT_SCHEMES = ["s3", "gs", "az", "file"];
1861
- NEWER_SCHEMES = /* @__PURE__ */ new Set(["gs", "az"]);
1862
1830
  WEBHOOK_VAR = { type: "object", additionalProperties: false, required: ["webhook"], properties: { webhook: { type: "string", pattern: VARIABLE4.source, description: "The variable holding the incoming webhook's URL." } } };
1863
1831
  observe = {
1864
1832
  name: "notify, telemetry and reports",
@@ -1914,7 +1882,6 @@ var init_observe = __esm({
1914
1882
  if (n.relay !== void 0) {
1915
1883
  out.push(...variable(n.relay, "notify.relay"));
1916
1884
  if (n.slack === void 0 && n.teams === void 0) out.push(error("notify.relay", "needs notify.slack or notify.teams: approvals come back from a chat message"));
1917
- else out.push(warning("notify.relay", "needs chant >= the release after 0.124.54 (chant#4611); plywood init leaves it out of the waves spec on a chant without the relay"));
1918
1885
  }
1919
1886
  for (const chat of ["slack", "teams"]) {
1920
1887
  const c = n[chat];
@@ -1954,7 +1921,6 @@ var init_observe = __esm({
1954
1921
  out.push(...onlyKeys(r, ["url"], "reports"));
1955
1922
  const scheme = typeof r.url === "string" ? /^([a-z0-9]+):\/\/\S+$/.exec(r.url)?.[1] : void 0;
1956
1923
  if (!scheme || !REPORT_SCHEMES.includes(scheme)) out.push(error("reports.url", `is a ${REPORT_SCHEMES.map((s) => `${s}://`).join(", ")} URL`));
1957
- else if (NEWER_SCHEMES.has(scheme)) out.push(warning("reports.url", `${scheme}:// reports need chant >= the release after 0.124.54 (chant#4554); the pinned chant takes s3:// and file://`));
1958
1924
  }
1959
1925
  }
1960
1926
  return out;
@@ -1970,21 +1936,7 @@ var init_observe = __esm({
1970
1936
 
1971
1937
  // src/config/policy.ts
1972
1938
  import { posix } from "node:path";
1973
- function useChantPolicySource() {
1974
- policySourceLoading ??= (async () => {
1975
- try {
1976
- const m = await import("@intentius/chant/policy-source");
1977
- policySource = typeof m.CI_POLICY_SOURCE_FLOOR === "string" && typeof m.assertGatePolicySource === "function";
1978
- } catch {
1979
- policySource = false;
1980
- }
1981
- return policySource;
1982
- })();
1983
- return policySourceLoading;
1984
- }
1985
- function chantHasPolicySource() {
1986
- return policySource;
1987
- }
1939
+ import { assertGatePolicySource } from "@intentius/chant/policy-source";
1988
1940
  function repoPath(v, key) {
1989
1941
  if (typeof v !== "string" || v.trim() === "") return [error(key, "is a path inside the repository")];
1990
1942
  if (v.startsWith("/") || v.startsWith("-") || v.split("/").includes("..")) return [error(key, `is ${JSON.stringify(v)}; give a path relative to the repository root, inside it`)];
@@ -2019,13 +1971,13 @@ function policySourceOf(doc) {
2019
1971
  if (!isObject(src) || typeof src.repo !== "string" || typeof src.ref !== "string") return void 0;
2020
1972
  return { repo: src.repo, ref: src.ref, ...typeof src.path === "string" ? { path: cleanPath(src.path) } : {} };
2021
1973
  }
2022
- function chantPolicyOf(doc, chantReadsSource = chantHasPolicySource()) {
1974
+ function chantPolicyOf(doc) {
2023
1975
  const p = doc.policy;
2024
1976
  if (!isObject(p)) return void 0;
2025
1977
  const rego = typeof p.rego === "string" ? cleanPath(p.rego) : void 0;
2026
1978
  const command = Array.isArray(p.command) ? p.command : void 0;
2027
- const source = chantReadsSource ? policySourceOf(doc) : void 0;
2028
- const packs = chantReadsSource ? policyPacksOf(doc) : [];
1979
+ const source = policySourceOf(doc);
1980
+ const packs = policyPacksOf(doc);
2029
1981
  if (rego === void 0 && command === void 0 && p.engine === void 0 && p.namespace === void 0 && !source && packs.length === 0) return void 0;
2030
1982
  return {
2031
1983
  ...typeof p.engine === "string" ? { engine: p.engine } : {},
@@ -2036,26 +1988,22 @@ function chantPolicyOf(doc, chantReadsSource = chantHasPolicySource()) {
2036
1988
  ...packs.length ? { packs } : {}
2037
1989
  };
2038
1990
  }
2039
- function stepPolicyPacksOf(doc, chantReadsSource = chantHasPolicySource()) {
2040
- return chantReadsSource && policySourceOf(doc) ? [] : policyPacksOf(doc);
1991
+ function stepPolicyPacksOf(doc) {
1992
+ return policySourceOf(doc) ? [] : policyPacksOf(doc);
2041
1993
  }
2042
- var POLICY_ENGINES, POLICY_KEYS, PACK_KEYS, SOURCE_KEYS, SOURCE_REPO, SOURCE_REF, policySource, policySourceLoading, NO_SUCH_KEY, SOURCE_NEEDS_CHANT, policy;
1994
+ var POLICY_ENGINES, POLICY_KEYS, PACK_KEYS, NO_SUCH_KEY, policy;
2043
1995
  var init_policy = __esm({
2044
1996
  "src/config/policy.ts"() {
2045
1997
  "use strict";
1998
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2046
1999
  init_types();
2047
2000
  POLICY_ENGINES = ["conftest", "opa"];
2048
2001
  POLICY_KEYS = ["rego", "engine", "namespace", "command", "packs", "source", "providers"];
2049
2002
  PACK_KEYS = ["dir", "config"];
2050
- SOURCE_KEYS = ["repo", "ref", "path"];
2051
- SOURCE_REPO = /^(https?:\/\/[^\s/]+\/\S+|ssh:\/\/[^\s/]+\/\S+|file:\/\/\/?\S+|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:\S+)$/;
2052
- SOURCE_REF = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
2053
- policySource = false;
2054
2003
  NO_SUCH_KEY = {
2055
2004
  allow_unattended: "chant's gate denies and holds only; a non-empty change always waits for a person, so there is no unattended outcome",
2056
2005
  outcomes: "a Rego rule's name sets its outcome: deny_* denies, hold_* holds, warn_* is a note line"
2057
2006
  };
2058
- SOURCE_NEEDS_CHANT = "is a shared policy source from a git ref, which needs chant >= the release after 0.124.54 (chant#4613's ci.policy.source); on this chant, vendor the rules into the repository, where they are read at base";
2059
2007
  policy = {
2060
2008
  name: "policy",
2061
2009
  keys: ["policy"],
@@ -2073,7 +2021,7 @@ var init_policy = __esm({
2073
2021
  type: "object",
2074
2022
  additionalProperties: false,
2075
2023
  required: ["repo", "ref"],
2076
- description: "A shared rule set from a git ref, fetched by the job at the ref the base names (chant >= the release after 0.124.54). packs are then paths inside it.",
2024
+ description: "A shared rule set from a git ref, fetched by the job at the ref the base names. packs are then paths inside it.",
2077
2025
  properties: {
2078
2026
  repo: { type: "string", description: "The repository: https://, ssh:// or file:// URL, or git@<host>:<path>." },
2079
2027
  ref: { type: "string", description: "A tag, a branch or a full commit." },
@@ -2114,16 +2062,13 @@ var init_policy = __esm({
2114
2062
  const out = [];
2115
2063
  for (const k of Object.keys(p)) if (k in NO_SUCH_KEY) out.push(error(`policy.${k}`, `is not a plywood key: ${NO_SUCH_KEY[k]}`));
2116
2064
  if (p.source !== void 0) {
2117
- if (!chantHasPolicySource()) out.push(error("policy.source", SOURCE_NEEDS_CHANT));
2118
- else if (!isObject(p.source)) out.push(error("policy.source", "is { repo, ref, path }"));
2119
- else {
2120
- const src = p.source;
2121
- out.push(...onlyKeys(src, SOURCE_KEYS, "policy.source"));
2122
- if (typeof src.repo !== "string" || !SOURCE_REPO.test(src.repo) || src.repo.startsWith("-")) out.push(error("policy.source.repo", "is a git URL: https://, ssh://, file:// or git@<host>:<path>"));
2123
- if (typeof src.ref !== "string" || !SOURCE_REF.test(src.ref) || src.ref.includes("..") || src.ref.endsWith("/")) out.push(error("policy.source.ref", "is a tag, a branch or a commit"));
2124
- if (src.path !== void 0) out.push(...repoPath(src.path, "policy.source.path"));
2125
- if (p.rego !== void 0) out.push(error("policy.rego", "goes with no policy.source: with a source, policy.source.path names the Rego directory inside it"));
2065
+ try {
2066
+ assertGatePolicySource("policy.source", p.source);
2067
+ } catch (e) {
2068
+ const m = /^(\S+) (.*)$/.exec(e.message);
2069
+ out.push(error(m?.[1] ?? "policy.source", m?.[2] ?? e.message));
2126
2070
  }
2071
+ if (p.rego !== void 0) out.push(error("policy.rego", "goes with no policy.source: with a source, policy.source.path names the Rego directory inside it"));
2127
2072
  }
2128
2073
  out.push(...onlyKeys(Object.fromEntries(Object.entries(p).filter(([k]) => !(k in NO_SUCH_KEY))), POLICY_KEYS, "policy"));
2129
2074
  if (p.rego !== void 0) out.push(...repoPath(p.rego, "policy.rego"));
@@ -2173,6 +2118,7 @@ var DEFAULT_REVIEW_STACK_NAME, TTL, REVIEW_STACK_NAME, STACK_KEY, reviewStacks;
2173
2118
  var init_review_stacks = __esm({
2174
2119
  "src/config/review-stacks.ts"() {
2175
2120
  "use strict";
2121
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2176
2122
  init_types();
2177
2123
  DEFAULT_REVIEW_STACK_NAME = "pr-{number}";
2178
2124
  TTL = /^\d+[mhd]$/;
@@ -2227,6 +2173,7 @@ var FORGES, GLOB_MAP, copy, routing;
2227
2173
  var init_routing = __esm({
2228
2174
  "src/config/routing.ts"() {
2229
2175
  "use strict";
2176
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2230
2177
  init_types();
2231
2178
  FORGES = ["github", "gitlab", "forgejo"];
2232
2179
  GLOB_MAP = {
@@ -2322,6 +2269,7 @@ var GROUPS, CONFIG_NAMES, ConfigError;
2322
2269
  var init_config = __esm({
2323
2270
  "src/config/index.ts"() {
2324
2271
  "use strict";
2272
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2325
2273
  init_agents();
2326
2274
  init_apply();
2327
2275
  init_approval();
@@ -2362,6 +2310,7 @@ function toChantSettings(config2) {
2362
2310
  var init_chant_config = __esm({
2363
2311
  "src/chant-config.ts"() {
2364
2312
  "use strict";
2313
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2365
2314
  init_config();
2366
2315
  }
2367
2316
  });
@@ -2385,25 +2334,25 @@ function configCheck(cwd, file) {
2385
2334
  const lines2 = problems.map((p) => `${path}: ${p.key} ${p.message}${p.severity === "warning" ? " (warning)" : ""}`);
2386
2335
  if (errors.length) return { ok: false, lines: lines2, envelope: { file: path, problems } };
2387
2336
  const doc = config2.doc;
2388
- const engine2 = engineOf(doc);
2337
+ const engine3 = engineOf(doc);
2389
2338
  const locks2 = locksOf(doc);
2390
2339
  const later2 = Object.keys(doc).filter((k) => k in LATER_KEYS);
2391
2340
  const settings = {
2392
- engine: engine2?.engine ?? null,
2341
+ engine: engine3?.engine ?? null,
2393
2342
  applyWhen: applyWhenOf(doc),
2394
2343
  gate: gateOf(doc),
2395
2344
  approval: approvalOf(doc),
2396
2345
  locksWhen: locks2.when,
2397
- lockGrain: lockGrainFor(doc, engine2?.engine)
2346
+ lockGrain: lockGrainFor(doc, engine3?.engine)
2398
2347
  };
2399
2348
  lines2.push(
2400
2349
  `${path}: ok`,
2401
- ` engine ${engine2 ? `${engine2.engine} (${engine2.because})` : "not set here; chant reads PULUMI_BACKEND_URL, then Pulumi.yaml"}`,
2350
+ ` engine ${engine3 ? `${engine3.engine} (${engine3.because})` : "not set here; chant reads PULUMI_BACKEND_URL, then Pulumi.yaml"}`,
2402
2351
  ` apply.when ${settings.applyWhen}`,
2403
2352
  ` gate ${settings.gate}`,
2404
2353
  ` approval ${settings.approval}`,
2405
2354
  ` locks.when ${locks2.when} (ci.locks ${CI_LOCKS_OF[locks2.when]})`,
2406
- ` locks.grain ${locks2.grain ? `${locks2.grain} (ci.lockGrain ${LOCK_GRAIN_OF[locks2.grain]})` : LOCK_GRAIN_WORDS.perEngine(engine2?.engine)}`,
2355
+ ` locks.grain ${locks2.grain ? `${locks2.grain} (ci.lockGrain ${LOCK_GRAIN_OF[locks2.grain]})` : LOCK_GRAIN_WORDS.perEngine(engine3?.engine)}`,
2407
2356
  ` policy ${policyLine(doc)}`,
2408
2357
  ...later2.length ? [` read as written, not yet passed to chant: ${later2.join(", ")}`] : []
2409
2358
  );
@@ -2424,6 +2373,7 @@ function policyLine(doc) {
2424
2373
  var init_config_check = __esm({
2425
2374
  "src/config-check.ts"() {
2426
2375
  "use strict";
2376
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2427
2377
  init_chant_config();
2428
2378
  init_apply();
2429
2379
  init_approval();
@@ -2438,315 +2388,43 @@ var init_config_check = __esm({
2438
2388
  });
2439
2389
 
2440
2390
  // src/chant-product.ts
2441
- function hasChant4620(chant) {
2442
- return typeof chant.prCommentPreviewJob === "function" && typeof chant.PR_PREVIEW_FLOOR === "string";
2443
- }
2444
- function hasChant4607(chant) {
2445
- return typeof chant.pendingGatesByPlan === "function" && typeof chant.parseRequiresList === "function";
2446
- }
2447
- function needsChantMessage(what) {
2448
- return `${what} needs chant >= the release after 0.124.54, which carries @intentius/chant/ci-product (chant#4550). This plywood was built with chant ${BUILT_CHANT}, which lacks it. Upgrade plywood once that chant is out, or build against a local chant with 'just chant-local'.`;
2449
- }
2450
- function isChantProduct(m) {
2451
- if (!m || typeof m !== "object") return false;
2452
- const r = m;
2453
- return REQUIRED2.every((name) => typeof r[name] === "function");
2454
- }
2455
2391
  function chantProduct() {
2456
- cached2 ??= (async () => {
2457
- try {
2458
- const m = await import("@intentius/chant/ci-product");
2459
- return isChantProduct(m) ? m : void 0;
2460
- } catch {
2461
- return void 0;
2462
- }
2463
- })();
2464
- return cached2;
2392
+ cached ??= import("@intentius/chant/ci-product").then((m) => m);
2393
+ return cached;
2465
2394
  }
2466
- var REQUIRED2, BUILT_CHANT, cached2;
2395
+ var cached;
2467
2396
  var init_chant_product = __esm({
2468
2397
  "src/chant-product.ts"() {
2469
2398
  "use strict";
2470
- REQUIRED2 = ["assertProductWords", "productPipelineJobs", "approveLine", "runChantInProcess"];
2471
- BUILT_CHANT = true ? "0.124.59" : "unknown";
2472
- }
2473
- });
2474
-
2475
- // src/chant-next.ts
2476
- function hasChantRespond(chant) {
2477
- return typeof chant?.CI_RESPOND_FLOOR === "string";
2478
- }
2479
- function hasChantPrLoopFlags(chant) {
2480
- return typeof chant?.PR_LOOP_BUILD_FLAGS_FLOOR === "string";
2481
- }
2482
- function hasChantApprovalStatus(chant) {
2483
- return typeof chant?.APPROVAL_STATUS_FLOOR === "string";
2484
- }
2485
- function hasChantPullRequestApprovalStatus(chant) {
2486
- return typeof chant?.APPROVAL_STATUS_PULL_REQUEST_FLOOR === "string";
2487
- }
2488
- function hasChantWavePreview() {
2489
- wavePreview ??= (async () => {
2490
- try {
2491
- const m = await import("@intentius/chant/op");
2492
- return typeof m.OP_WAVES_PREVIEW_FLOOR === "string";
2493
- } catch {
2494
- return false;
2495
- }
2496
- })();
2497
- return wavePreview;
2498
- }
2499
- function withWavePreview(spec, forge) {
2500
- const approvals = spec.approvals && typeof spec.approvals === "object" && !Array.isArray(spec.approvals) ? spec.approvals : {};
2501
- const next = approvals.preview !== void 0 ? spec : { ...spec, approvals: { ...approvals, preview: true } };
2502
- if (forge === "gitlab" || next.approvals.preview !== true || !Array.isArray(next.waves)) return next;
2503
- const waves = next.waves.map((w) => ({
2504
- ...w,
2505
- variables: { [WAVE_TOKEN_VARIABLE]: "${{ github.token }}", ...w.variables ?? {} }
2506
- }));
2507
- return { ...next, waves };
2508
- }
2509
- function chantAudit() {
2510
- audit ??= import("@intentius/chant/ci-audit").then((m) => m);
2511
- return audit;
2512
- }
2513
- var wavePreview, WAVE_TOKEN_VARIABLE, GITLAB_READ_TOKEN, audit;
2514
- var init_chant_next = __esm({
2515
- "src/chant-next.ts"() {
2516
- "use strict";
2517
- WAVE_TOKEN_VARIABLE = "GITHUB_TOKEN";
2518
- GITLAB_READ_TOKEN = "CHANT_FORGE_TOKEN";
2519
- }
2520
- });
2521
-
2522
- // src/init/own-jobs.ts
2523
- function indentOf(line) {
2524
- return line.length - line.trimStart().length;
2525
- }
2526
- function isContent(line) {
2527
- const t = line.trim();
2528
- return t !== "" && !t.startsWith("#");
2529
- }
2530
- function jobBlocks(text, forge) {
2531
- const lines2 = text.split("\n");
2532
- const out = /* @__PURE__ */ new Map();
2533
- let start = 0;
2534
- let end = lines2.length;
2535
- let depth = 0;
2536
- if (forge !== "gitlab") {
2537
- const at = lines2.findIndex((l) => /^jobs:\s*(#.*)?$/.test(l));
2538
- if (at < 0) return out;
2539
- start = at + 1;
2540
- const next = lines2.findIndex((l, i) => i > at && indentOf(l) === 0 && isContent(l));
2541
- end = next < 0 ? lines2.length : next;
2542
- depth = 2;
2543
- }
2544
- let name;
2545
- let body = [];
2546
- const flush = () => {
2547
- if (name !== void 0) {
2548
- while (body.length > 0 && body[body.length - 1].trim() === "") body.pop();
2549
- out.set(name, body.join("\n"));
2550
- }
2551
- name = void 0;
2552
- body = [];
2553
- };
2554
- for (let i = start; i < end; i++) {
2555
- const line = lines2[i];
2556
- if (isContent(line) && indentOf(line) === depth) {
2557
- const m = KEY.exec(line.slice(depth));
2558
- flush();
2559
- if (m) {
2560
- name = m[1].replace(/^["']|["']$/g, "");
2561
- body = [line];
2562
- }
2563
- continue;
2564
- }
2565
- if (isContent(line) && indentOf(line) < depth) {
2566
- flush();
2567
- continue;
2568
- }
2569
- if (name !== void 0) body.push(line);
2570
- }
2571
- flush();
2572
- if (forge === "gitlab") {
2573
- for (const k of [...out.keys()]) if (GITLAB_KEYWORDS.has(k)) out.delete(k);
2574
- }
2575
- return out;
2576
- }
2577
- function ownJobs(previous, generated, forge) {
2578
- const ours = jobBlocks(generated, forge);
2579
- const out = /* @__PURE__ */ new Map();
2580
- for (const [name, block] of jobBlocks(previous, forge)) {
2581
- if (ours.has(name) || PLYWOOD_LINE.test(block)) continue;
2582
- out.set(name, block);
2583
- }
2584
- return out;
2585
- }
2586
- function withOwnJobs(generated, own, forge) {
2587
- if (own.size === 0) return generated;
2588
- const indent = forge === "gitlab" ? "" : " ";
2589
- const block = [`${indent}${OWN_JOBS_LINE}`, ...own.values()].join("\n");
2590
- const lines2 = generated.replace(/\n+$/, "").split("\n");
2591
- if (forge !== "gitlab") {
2592
- const at = lines2.findIndex((l) => /^jobs:\s*(#.*)?$/.test(l));
2593
- if (at >= 0) {
2594
- const next = lines2.findIndex((l, i) => i > at && indentOf(l) === 0 && isContent(l));
2595
- if (next >= 0) {
2596
- let cut = next;
2597
- while (cut > at + 1 && lines2[cut - 1].trim() === "") cut--;
2598
- return [...lines2.slice(0, cut), block, ...lines2.slice(cut)].join("\n") + "\n";
2599
- }
2600
- }
2601
- }
2602
- return [...lines2, block].join("\n") + "\n";
2603
- }
2604
- function keepOwnJobs(previous, generated, forge) {
2605
- if (previous === void 0) return { text: generated, kept: [] };
2606
- const own = ownJobs(previous, generated, forge);
2607
- return { text: withOwnJobs(generated, own, forge), kept: [...own.keys()] };
2608
- }
2609
- var OWN_JOBS_LINE, GITLAB_KEYWORDS, PLYWOOD_LINE, KEY;
2610
- var init_own_jobs = __esm({
2611
- "src/init/own-jobs.ts"() {
2612
- "use strict";
2613
- OWN_JOBS_LINE = "# Your own jobs, kept as they are by plywood init and plywood ci.";
2614
- GITLAB_KEYWORDS = /* @__PURE__ */ new Set(["default", "include", "stages", "variables", "workflow", "image", "services", "cache", "before_script", "after_script", "spec"]);
2615
- PLYWOOD_LINE = /\bplywood\b\s+(components|ci|run|lock|unlock|check|preview|up|drift)\b/;
2616
- KEY = /^([A-Za-z0-9_.-]+|"[^"]+"|'[^']+'):(\s|$)/;
2617
- }
2618
- });
2619
-
2620
- // src/init/note-control.ts
2621
- import { emitYAML } from "@intentius/chant/yaml";
2622
- function indentOf2(line) {
2623
- return line.length - line.trimStart().length;
2624
- }
2625
- function jobRange(lines2, name, depth) {
2626
- const head = new RegExp(`^${" ".repeat(depth)}${name}:\\s*(#.*)?$`);
2627
- const start = lines2.findIndex((l) => head.test(l));
2628
- if (start < 0) return void 0;
2629
- let end = lines2.length;
2630
- for (let i = start + 1; i < lines2.length; i++) {
2631
- const l = lines2[i];
2632
- if (l.trim() === "" || l.trimStart().startsWith("#")) continue;
2633
- if (indentOf2(l) <= depth) {
2634
- end = i;
2635
- break;
2636
- }
2637
- }
2638
- return [start, end];
2639
- }
2640
- function withNoteControl(text, forge, step) {
2641
- if (!step) return text;
2642
- const lines2 = text.split("\n");
2643
- const range = jobRange(lines2, NOTE_JOB, forge === "gitlab" ? 0 : 2);
2644
- if (!range) return text;
2645
- const [start, end] = range;
2646
- const job = lines2.slice(start, end);
2647
- if (job.some((l) => l.includes(step.stepName)) || job.some((l) => l.includes("CHANT_CONTROL_DIR="))) return text;
2648
- if (forge === "gitlab") {
2649
- const at2 = job.findIndex((l) => /^ {4}- /.test(l) && l.includes("components pr-post"));
2650
- if (at2 < 0) return text;
2651
- const item2 = emitYAML([step.script], 2).replace(/^\n/, "");
2652
- return [...lines2.slice(0, start + at2), ...item2.split("\n"), ...lines2.slice(start + at2)].join("\n");
2653
- }
2654
- const at = job.findIndex((l) => /^ {6}- name: Post the plan note\b/.test(l));
2655
- if (at < 0) return text;
2656
- const item = emitYAML([{ name: step.stepName, if: "always()", ...Object.keys(step.env).length ? { env: step.env } : {}, run: step.script }], 3).replace(/^\n/, "");
2657
- return [...lines2.slice(0, start + at), ...item.split("\n"), ...lines2.slice(start + at)].join("\n");
2658
- }
2659
- var NOTE_JOB;
2660
- var init_note_control = __esm({
2661
- "src/init/note-control.ts"() {
2662
- "use strict";
2663
- NOTE_JOB = "plan-note";
2664
- }
2665
- });
2666
-
2667
- // src/init/run-group.ts
2668
- function withRunGroup(text, forge, key) {
2669
- if (forge !== "forgejo" || TOP_CONCURRENCY.test(text)) return text;
2670
- const jobs2 = TOP_JOBS.exec(text);
2671
- if (!jobs2) return text;
2672
- const name = (TOP_NAME.exec(text)?.[1] ?? "plywood").replace(/^["']|["']$/g, "");
2673
- const group = key === "commit" ? `${name}-\${{ github.event_name }}-\${{ github.sha }}` : `${name}-\${{ github.event_name == 'pull_request' && github.ref || github.run_id }}`;
2674
- const cancel = key === "commit" ? "false" : "${{ github.event_name == 'pull_request' }}";
2675
- const block = [
2676
- "# Without a group Forgejo cancels the run in progress when a push starts another run of this workflow.",
2677
- key === "commit" ? "# One group for each commit keeps a merge's wave jobs running to completion when another merge lands (plywood#51)." : "# A push to main and a comment get a group of their own, so the record job runs to completion (plywood#51).",
2678
- "concurrency:",
2679
- ` group: ${yamlScalar(group)}`,
2680
- ` cancel-in-progress: ${yamlScalar(cancel)}`,
2681
- "",
2682
- ""
2683
- ].join("\n");
2684
- return text.slice(0, jobs2.index) + block + text.slice(jobs2.index);
2685
- }
2686
- function yamlScalar(value) {
2687
- return value === "false" ? value : JSON.stringify(value);
2688
- }
2689
- var TOP_CONCURRENCY, TOP_JOBS, TOP_NAME;
2690
- var init_run_group = __esm({
2691
- "src/init/run-group.ts"() {
2692
- "use strict";
2693
- TOP_CONCURRENCY = /^concurrency:/m;
2694
- TOP_JOBS = /^jobs:/m;
2695
- TOP_NAME = /^name:\s*(.+?)\s*$/m;
2399
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2696
2400
  }
2697
2401
  });
2698
2402
 
2699
2403
  // src/init/workflow.ts
2700
2404
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2701
2405
  import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
2406
+ import { jobBlocks, ownJobsOf } from "@intentius/chant/ci-own-jobs";
2702
2407
  async function writeWorkflow(chant, input) {
2703
2408
  const file = isAbsolute(input.output) ? input.output : join2(input.root, input.output);
2704
2409
  const previous = existsSync2(file) ? readFileSync2(file, "utf8") : void 0;
2705
2410
  mkdirSync(dirname2(file), { recursive: true });
2706
- const code = await chant.runChantInProcess([...input.argv, "--output", file], input.words);
2411
+ const code = await chant.runChantInProcess([...input.argv, "--keep-own-jobs", "--run-group", "pull-request", "--output", file], input.words);
2707
2412
  if (code !== 0 || !existsSync2(file)) {
2708
2413
  if (previous !== void 0) writeFileSync(file, previous);
2709
2414
  return { code: code !== 0 ? code : 1, file, status: "failed", kept: [] };
2710
2415
  }
2711
- const generated = withRunGroup(withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl), input.forge, "pull-request");
2712
- const { text, kept } = keepOwnJobs(previous, generated, input.forge);
2713
- writeFileSync(file, text);
2416
+ const text = readFileSync2(file, "utf8");
2417
+ const kept = previous === void 0 ? [] : keptJobs(previous, text, input.forge, input.words.bin);
2714
2418
  return { code, file, status: text === previous ? "unchanged" : "written", kept };
2715
2419
  }
2420
+ function keptJobs(previous, written, forge, bin) {
2421
+ const now = jobBlocks(written, forge);
2422
+ return [...ownJobsOf(previous, "", forge, bin ? [bin] : [])].filter(([name, block]) => now.get(name) === block).map(([name]) => name);
2423
+ }
2716
2424
  var init_workflow = __esm({
2717
2425
  "src/init/workflow.ts"() {
2718
2426
  "use strict";
2719
- init_own_jobs();
2720
- init_note_control();
2721
- init_run_group();
2722
- }
2723
- });
2724
-
2725
- // src/review-stacks.ts
2726
- function chantReviewStacks() {
2727
- cached3 ??= (async () => {
2728
- try {
2729
- const m = await import("@intentius/chant-lexicon-pulumi");
2730
- const plugin = m.pulumiPlugin;
2731
- return typeof m.initReviewStack === "function" && typeof m.removeReviewStack === "function" && typeof plugin?.prEnvTemplate === "function" && typeof plugin?.prEnvTeardown === "function";
2732
- } catch {
2733
- return false;
2734
- }
2735
- })();
2736
- return cached3;
2737
- }
2738
- function reviewSuffixOf(pattern) {
2739
- const m = pattern ? /^pr-\{number\}-([a-z0-9][a-z0-9-]*)$/.exec(pattern) : null;
2740
- return m ? m[1] : void 0;
2741
- }
2742
- var REVIEW_GATE, REVIEW_GATE_WORD, reviewOp, cached3, REVIEW_STACKS_NEED_CHANT;
2743
- var init_review_stacks2 = __esm({
2744
- "src/review-stacks.ts"() {
2745
- "use strict";
2746
- REVIEW_GATE = "pr-env";
2747
- REVIEW_GATE_WORD = "review";
2748
- reviewOp = (pr) => `pr-${pr}`;
2749
- REVIEW_STACKS_NEED_CHANT = "review_stacks is set, and the installed @intentius/chant-lexicon-pulumi cannot run a review stack end to end (initReviewStack, removeReviewStack, and the plugin's prEnvTemplate and prEnvTeardown from chant#4609), so plywood writes no ci.prEnvironment and the workflow has no review stack jobs yet";
2427
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2750
2428
  }
2751
2429
  });
2752
2430
 
@@ -2754,11 +2432,11 @@ var init_review_stacks2 = __esm({
2754
2432
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
2755
2433
  import { dirname as dirname3, join as join3 } from "node:path";
2756
2434
  function wavesEngine(pipeline) {
2757
- const o = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} }, false);
2435
+ const o = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} });
2758
2436
  return { ...o.image ? { image: o.image } : {}, ...o.beforeScript?.length ? { beforeScript: o.beforeScript } : {} };
2759
2437
  }
2760
- function wavesSpecText(spec, engine2 = {}) {
2761
- const options = { ...engine2.image ? { image: engine2.image } : {}, ...engine2.beforeScript?.length ? { beforeScript: engine2.beforeScript } : {} };
2438
+ function wavesSpecText(spec, engine3 = {}) {
2439
+ const options = { ...engine3.image ? { image: engine3.image } : {}, ...engine3.beforeScript?.length ? { beforeScript: engine3.beforeScript } : {} };
2762
2440
  return JSON.stringify(Object.keys(options).length ? { waves: spec, options } : spec, null, 2) + "\n";
2763
2441
  }
2764
2442
  function unwrapWavesSpec(doc) {
@@ -2821,28 +2499,13 @@ function writeUpOp(cwd, spec) {
2821
2499
  writeFileSync2(path, text);
2822
2500
  return true;
2823
2501
  }
2824
- function binLines(waves, bin) {
2825
- const binArgs = bin.split(/\s+/).filter(Boolean);
2826
- return waves.jobs.map((job) => {
2827
- const own = job.command.slice(0, binArgs.length).join(" ") === binArgs.join(" ");
2828
- const rest = own ? job.command.slice(binArgs.length) : job.command.slice(1);
2829
- return { chant: ["chant", ...rest].join(" "), product: [...binArgs, ...rest].join(" ") };
2830
- });
2831
- }
2832
- function withBinLines(text, lines2) {
2833
- const sorted = [...lines2].sort((a, b) => b.chant.length - a.chant.length);
2834
- return text.replace(/chant run wave [^\n"']*/g, (found) => {
2835
- const hit = sorted.find((l) => found.trimEnd() === l.chant);
2836
- return hit ? hit.product : found;
2837
- });
2838
- }
2839
2502
  function generatedFor(dir, specFile) {
2840
2503
  if (!existsSync3(dir)) return [];
2841
2504
  return readdirSync(dir).filter((f) => /\.ya?ml$/.test(f)).map((f) => join3(dir, f)).filter((p) => (readFileSync3(p, "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${specFile}`));
2842
2505
  }
2843
- async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}) {
2506
+ async function writeWaveJobs(chant, forge, waves, words, table, io, engine3 = {}) {
2844
2507
  const specPath = join3(io.cwd, waves.specFile);
2845
- const specText = wavesSpecText(waves.spec, engine2);
2508
+ const specText = wavesSpecText(waves.spec, engine3);
2846
2509
  const before = existsSync3(specPath) ? readFileSync3(specPath, "utf8") : void 0;
2847
2510
  if (before !== specText) {
2848
2511
  mkdirSync2(dirname3(specPath), { recursive: true });
@@ -2851,18 +2514,14 @@ async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}
2851
2514
  }
2852
2515
  if (writeUpOp(io.cwd, waves.spec)) io.err(`${PRODUCT} ci: wrote ${UP_OP_FILE}, the Op the waves spec names; commit it with the workflow`);
2853
2516
  const outDir = WAVES_OUTPUT_DIR[forge];
2854
- const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", waves.specFile, "--output", outDir], words);
2517
+ const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", waves.specFile, "--run-group", "commit", "--output", outDir], words);
2855
2518
  if (code !== table.done) return code;
2856
2519
  const files = generatedFor(join3(io.cwd, outDir), waves.specFile);
2857
2520
  if (files.length === 0) {
2858
2521
  io.err(`${PRODUCT} ci: chant wrote no waves workflow for ${waves.specFile} under ${outDir}`);
2859
2522
  return table.failed;
2860
2523
  }
2861
- const lines2 = hasChant4607(chant) ? void 0 : binLines(waves, words.bin);
2862
2524
  for (const file of files) {
2863
- const text = readFileSync3(file, "utf8");
2864
- const next = withRunGroup(lines2 ? withBinLines(text, lines2) : text, forge, "commit");
2865
- if (next !== text) writeFileSync2(file, next);
2866
2525
  io.err(`${PRODUCT} ci: waves workflow at ${file.slice(io.cwd.length + 1)}`);
2867
2526
  }
2868
2527
  if (forge === "gitlab") {
@@ -2874,9 +2533,8 @@ var WAVES_OUTPUT_DIR, UP_OP_FILE;
2874
2533
  var init_waves_workflow = __esm({
2875
2534
  "src/waves-workflow.ts"() {
2876
2535
  "use strict";
2877
- init_chant_product();
2536
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2878
2537
  init_images();
2879
- init_run_group();
2880
2538
  init_words();
2881
2539
  WAVES_OUTPUT_DIR = {
2882
2540
  github: ".github/workflows",
@@ -2887,12 +2545,34 @@ var init_waves_workflow = __esm({
2887
2545
  }
2888
2546
  });
2889
2547
 
2548
+ // src/chant-next.ts
2549
+ function withWavePreview(spec, forge) {
2550
+ const approvals = spec.approvals && typeof spec.approvals === "object" && !Array.isArray(spec.approvals) ? spec.approvals : {};
2551
+ const next = approvals.preview !== void 0 ? spec : { ...spec, approvals: { ...approvals, preview: true } };
2552
+ if (forge === "gitlab" || next.approvals.preview !== true || !Array.isArray(next.waves)) return next;
2553
+ const waves = next.waves.map((w) => ({
2554
+ ...w,
2555
+ variables: { [WAVE_TOKEN_VARIABLE]: "${{ github.token }}", ...w.variables ?? {} }
2556
+ }));
2557
+ return { ...next, waves };
2558
+ }
2559
+ function chantAudit() {
2560
+ audit ??= import("@intentius/chant/ci-audit").then((m) => m);
2561
+ return audit;
2562
+ }
2563
+ var WAVE_TOKEN_VARIABLE, GITLAB_READ_TOKEN, audit;
2564
+ var init_chant_next = __esm({
2565
+ "src/chant-next.ts"() {
2566
+ "use strict";
2567
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2568
+ WAVE_TOKEN_VARIABLE = "GITHUB_TOKEN";
2569
+ GITLAB_READ_TOKEN = "CHANT_FORGE_TOKEN";
2570
+ }
2571
+ });
2572
+
2890
2573
  // src/agent-jobs.ts
2891
2574
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
2892
2575
  import { dirname as dirname4, join as join4 } from "node:path";
2893
- function hasChantAgent(chant) {
2894
- return typeof chant?.CI_AGENT_FLOOR === "string";
2895
- }
2896
2576
  function stageRunner(runner, stage) {
2897
2577
  if (runner === void 0) return void 0;
2898
2578
  if (typeof runner === "string" || Array.isArray(runner) || "group" in runner) return runner;
@@ -2915,22 +2595,21 @@ function chantAgentOptions(a, doc) {
2915
2595
  };
2916
2596
  }
2917
2597
  function driftOpFields(chant, doc, op, respond) {
2918
- if (!hasChantAgent(chant)) return {};
2919
2598
  const runner = stageRunner(toChantSettings({ doc }).pipeline.runner, "drift");
2920
2599
  const a = agentOf(doc);
2921
2600
  const wantsAgent = respond === "pull-request" && a?.drift === true;
2922
- const agent = wantsAgent ? chant.productDriftAgent ? chant.productDriftAgent(PRODUCT_WORDS, op, chantAgentOptions(a, doc)) : { on: "drift", ...chantAgentOptions(a, doc) } : void 0;
2601
+ const agent = wantsAgent ? chant.productDriftAgent(PRODUCT_WORDS, op, chantAgentOptions(a, doc)) : void 0;
2923
2602
  return { ...runner !== void 0 ? { runner } : {}, ...agent ? { agent } : {} };
2924
2603
  }
2925
2604
  function agentOpSpec(spec, doc) {
2926
2605
  const p = toChantSettings({ doc }).pipeline;
2927
- const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
2606
+ const engine3 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} });
2928
2607
  const runner = stageRunner(p.runner, "preview");
2929
2608
  return {
2930
2609
  ops: [{ ...spec, ...runner !== void 0 && spec.runner === void 0 ? { runner } : {} }],
2931
2610
  options: {
2932
- ...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
2933
- ...engine2.image ? { image: engine2.image } : {}
2611
+ ...engine3.beforeScript ? { beforeScript: engine3.beforeScript } : {},
2612
+ ...engine3.image ? { image: engine3.image } : {}
2934
2613
  }
2935
2614
  };
2936
2615
  }
@@ -2944,10 +2623,6 @@ function writeIfChanged(cwd, file, text) {
2944
2623
  async function writeAgentJobs(chant, forge, env, doc, words, table, io) {
2945
2624
  const a = agentOf(doc);
2946
2625
  if (!a?.comment) return table.done;
2947
- if (!hasChantAgent(chant)) {
2948
- io.err(`${PRODUCT} ci: ${AGENT_NEEDS_CHANT}`);
2949
- return table.done;
2950
- }
2951
2626
  const agentWords = { ...words, commands: { ...words.commands, agent: "agent" } };
2952
2627
  let spec;
2953
2628
  try {
@@ -2973,10 +2648,11 @@ async function writeAgentJobs(chant, forge, env, doc, words, table, io) {
2973
2648
  if (forge === "gitlab") io.err(`${PRODUCT} ci: on GitLab a pipeline of the default branch started with CHANT_AGENT_PR and CHANT_AGENT_ASK runs the agent; scope CHANT_FORGE_TOKEN to environment ${AGENT_OP}-push`);
2974
2649
  return table.done;
2975
2650
  }
2976
- var AGENT_OP, AGENT_SPEC_FILE, AGENT_NEEDS_CHANT, OP_OUTPUT_DIR;
2651
+ var AGENT_OP, AGENT_SPEC_FILE, OP_OUTPUT_DIR;
2977
2652
  var init_agent_jobs = __esm({
2978
2653
  "src/agent-jobs.ts"() {
2979
2654
  "use strict";
2655
+ init_define_PLYWOOD_IMAGE_DIGESTS();
2980
2656
  init_chant_config();
2981
2657
  init_agents();
2982
2658
  init_config();
@@ -2984,7 +2660,6 @@ var init_agent_jobs = __esm({
2984
2660
  init_words();
2985
2661
  AGENT_OP = "plywood-agent";
2986
2662
  AGENT_SPEC_FILE = ".chant/ops/plywood-agent.spec.json";
2987
- AGENT_NEEDS_CHANT = "agent needs a chant with the agent job (chant#4616), and the installed one has none, so no agent job was written";
2988
2663
  OP_OUTPUT_DIR = { github: ".github/workflows", forgejo: ".forgejo/workflows", gitlab: "." };
2989
2664
  }
2990
2665
  });
@@ -3003,7 +2678,6 @@ async function sendDriftNotice(input) {
3003
2678
  const drifted = input.results.filter((r) => r.ok && r.drifted > 0);
3004
2679
  if (drifted.length === 0) return void 0;
3005
2680
  const fingerprint = runFingerprint(input.results, input.stackFingerprint);
3006
- if (!input.chant) return { sent: false, reason: DRIFT_NOTIFY_NEEDS_CHANT, fingerprint };
3007
2681
  if (drifted.every((r) => r.issue?.action === "unchanged")) {
3008
2682
  return { sent: false, reason: "no drift event was sent: every drifted stack's finding is the one its issue already holds", fingerprint };
3009
2683
  }
@@ -3021,62 +2695,39 @@ async function sendDriftNotice(input) {
3021
2695
  }
3022
2696
  function chantDriftNotify() {
3023
2697
  loaded ??= (async () => {
3024
- try {
3025
- const product = await import("@intentius/chant/ci-product");
3026
- const notify2 = await import("@intentius/chant/notify");
3027
- const fns = [product.driftNotifyFields, product.productSendOptions, notify2.makeNotifyEvent, notify2.sendNotify];
3028
- if (!fns.every((f) => typeof f === "function")) return void 0;
3029
- return {
3030
- driftNotifyFields: product.driftNotifyFields,
3031
- productSendOptions: product.productSendOptions,
3032
- makeNotifyEvent: notify2.makeNotifyEvent,
3033
- sendNotify: notify2.sendNotify
3034
- };
3035
- } catch {
3036
- return void 0;
3037
- }
2698
+ const product = await import("@intentius/chant/ci-product");
2699
+ const notify2 = await import("@intentius/chant/notify");
2700
+ return {
2701
+ driftNotifyFields: product.driftNotifyFields,
2702
+ productSendOptions: product.productSendOptions,
2703
+ makeNotifyEvent: notify2.makeNotifyEvent,
2704
+ sendNotify: notify2.sendNotify
2705
+ };
3038
2706
  })();
3039
2707
  return loaded;
3040
2708
  }
3041
- var DRIFT_NOTIFY_NEEDS_CHANT, loaded;
2709
+ var loaded;
3042
2710
  var init_drift_notify = __esm({
3043
2711
  "src/drift-notify.ts"() {
3044
2712
  "use strict";
3045
- DRIFT_NOTIFY_NEEDS_CHANT = "no drift event was sent: the installed chant has no driftNotifyFields (chant#4610)";
2713
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3046
2714
  }
3047
2715
  });
3048
2716
 
3049
2717
  // src/jobs.ts
3050
2718
  var jobs_exports = {};
3051
2719
  __export(jobs_exports, {
3052
- JOBS_NEED_CHANT: () => JOBS_NEED_CHANT,
3053
2720
  environmentFor: () => environmentFor,
3054
2721
  groupName: () => groupName,
3055
- hasChant4625: () => hasChant4625,
3056
2722
  jobVariables: () => jobVariables,
3057
2723
  prJobsFor: () => prJobsFor,
3058
2724
  prLoopJobFlags: () => prLoopJobFlags,
3059
2725
  prLoopJobOptions: () => prLoopJobOptions,
3060
2726
  prLoopRunners: () => prLoopRunners,
3061
2727
  secretReference: () => secretReference,
3062
- setChant4625ForTests: () => setChant4625ForTests,
3063
2728
  wantsChant4625: () => wantsChant4625,
3064
2729
  withJobSettings: () => withJobSettings
3065
2730
  });
3066
- function hasChant4625() {
3067
- detected ??= (async () => {
3068
- try {
3069
- const m = await import("@intentius/chant/parallelism");
3070
- return typeof m.mapLimited === "function" && typeof m.parallelismProblem === "function";
3071
- } catch {
3072
- return false;
3073
- }
3074
- })();
3075
- return detected;
3076
- }
3077
- function setChant4625ForTests(has) {
3078
- detected = has === "reset" ? void 0 : Promise.resolve(has);
3079
- }
3080
2731
  function secretReference(forge, secret) {
3081
2732
  return forge === "gitlab" ? `$${secret}` : `\${{ secrets.${secret} }}`;
3082
2733
  }
@@ -3099,7 +2750,7 @@ function groupName(environment, taken) {
3099
2750
  taken.add(name);
3100
2751
  return name;
3101
2752
  }
3102
- function withJobSettings(spec, s, forge, opts) {
2753
+ function withJobSettings(spec, s, forge) {
3103
2754
  const variables = jobVariables(forge, s);
3104
2755
  const waves = (Array.isArray(spec.waves) ? spec.waves : []).map((wave) => {
3105
2756
  const out = { ...wave };
@@ -3128,7 +2779,7 @@ function withJobSettings(spec, s, forge, opts) {
3128
2779
  out.groups = [...byEnv.values()].map((g) => ({ name: groupName(g.environment, taken), runs: g.runs, ...g.environment ? { environment: { ...g.environment } } : {} }));
3129
2780
  return out;
3130
2781
  });
3131
- return { ...spec, waves, ...opts.parallelism && s.parallelism !== void 0 && spec.step !== void 0 ? { parallelism: s.parallelism } : {} };
2782
+ return { ...spec, waves, ...s.parallelism !== void 0 && spec.step !== void 0 ? { parallelism: s.parallelism } : {} };
3132
2783
  }
3133
2784
  function prLoopRunners(runner) {
3134
2785
  if (runner === void 0) return void 0;
@@ -3164,24 +2815,15 @@ function wantsChant4625(s, runner) {
3164
2815
  }
3165
2816
  async function prJobsFor(pipeline) {
3166
2817
  if (!wantsChant4625(pipeline.jobs, pipeline.runner)) return { options: {}, flags: [], notes: [] };
3167
- if (!await hasChant4625()) {
3168
- const asked = [
3169
- ...pipeline.runner !== void 0 ? ["runners"] : [],
3170
- ...pipeline.jobs && Object.keys(pipeline.jobs.variables).length ? ["env"] : [],
3171
- ...pipeline.jobs && Object.keys(pipeline.jobs.secrets).length ? ["pass"] : [],
3172
- ...pipeline.jobs?.parallelism !== void 0 ? ["parallelism"] : []
3173
- ];
3174
- return { options: {}, flags: [], notes: [`${asked.join(", ")} on the pull request jobs ${JOBS_NEED_CHANT}; the waves and drift jobs have ${asked.filter((k) => k !== "parallelism").join(", ") || "none of them"}`] };
3175
- }
3176
2818
  const { flags, notes } = prLoopJobFlags(pipeline.jobs, pipeline.runner);
3177
2819
  return { options: prLoopJobOptions(pipeline.jobs, pipeline.runner), flags, notes };
3178
2820
  }
3179
- var detected, JOBS_NEED_CHANT, GROUP_NAME;
2821
+ var GROUP_NAME;
3180
2822
  var init_jobs2 = __esm({
3181
2823
  "src/jobs.ts"() {
3182
2824
  "use strict";
2825
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3183
2826
  init_images2();
3184
- JOBS_NEED_CHANT = "needs chant's runners, variables, passed secrets and parallelism on the pull request jobs (chant#4625), which this chant lacks";
3185
2827
  GROUP_NAME = /[^a-z0-9_-]+/g;
3186
2828
  }
3187
2829
  });
@@ -3399,6 +3041,7 @@ var SKIP, MAX_DEPTH, ENV_ORDER, PROD_RANK, WORKFLOW_DIRS;
3399
3041
  var init_detect = __esm({
3400
3042
  "src/init/detect.ts"() {
3401
3043
  "use strict";
3044
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3402
3045
  SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor", "venv", ".venv", "__pycache__", "target", "bin", "obj"]);
3403
3046
  MAX_DEPTH = 8;
3404
3047
  ENV_ORDER = ["local", "dev", "development", "sandbox", "test", "testing", "qa", "ci", "stage", "staging", "preprod", "uat", "prod", "production"];
@@ -3418,6 +3061,7 @@ function fill(text, values) {
3418
3061
  var init_templates = __esm({
3419
3062
  "src/templates.ts"() {
3420
3063
  "use strict";
3064
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3421
3065
  }
3422
3066
  });
3423
3067
 
@@ -3503,6 +3147,7 @@ function wavesSpec(input) {
3503
3147
  step: { kind: "pulumi-up" },
3504
3148
  ...input.quorum && input.quorum.length > 0 ? { approvals: { quorum: input.quorum } } : {},
3505
3149
  ...input.notify ? { notify: input.notify } : {},
3150
+ ...input.policy ? { holds: { policy: true } } : {},
3506
3151
  waves: waves.map((w) => {
3507
3152
  const gate = w.stacks.map((s) => s.gate ?? input.gate).reduce((a, b) => STRICTNESS[b] > STRICTNESS[a] ? b : a);
3508
3153
  return { name: w.name, gate, runs: w.stacks.map((s) => ({ target: s.key, input: stepInput(s) })) };
@@ -3546,6 +3191,7 @@ var WAVE_NAME, STRICTNESS, MEMBER_NAME, PULUMI_LEXICON;
3546
3191
  var init_render = __esm({
3547
3192
  "src/init/render.ts"() {
3548
3193
  "use strict";
3194
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3549
3195
  init_templates();
3550
3196
  init_words();
3551
3197
  init_detect();
@@ -3599,11 +3245,28 @@ var REFUSALS;
3599
3245
  var init_refusals = __esm({
3600
3246
  "src/refusals.ts"() {
3601
3247
  "use strict";
3248
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3602
3249
  init_words();
3603
3250
  REFUSALS = ["waiting", "moved", "held", "superseded", "denied", "declined", "misplaced"];
3604
3251
  }
3605
3252
  });
3606
3253
 
3254
+ // src/review-stacks.ts
3255
+ function reviewSuffixOf(pattern) {
3256
+ const m = pattern ? /^pr-\{number\}-([a-z0-9][a-z0-9-]*)$/.exec(pattern) : null;
3257
+ return m ? m[1] : void 0;
3258
+ }
3259
+ var REVIEW_GATE, REVIEW_GATE_WORD, reviewOp;
3260
+ var init_review_stacks2 = __esm({
3261
+ "src/review-stacks.ts"() {
3262
+ "use strict";
3263
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3264
+ REVIEW_GATE = "pr-env";
3265
+ REVIEW_GATE_WORD = "review";
3266
+ reviewOp = (pr) => `pr-${pr}`;
3267
+ }
3268
+ });
3269
+
3607
3270
  // src/up.ts
3608
3271
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
3609
3272
  import { join as join6 } from "node:path";
@@ -3713,8 +3376,7 @@ function approvalStatusTarget(opts, doc, env) {
3713
3376
  return { ...forge ? { forge } : {}, ...branch ? { branch } : {} };
3714
3377
  }
3715
3378
  async function refreshApprovalStatus(chant, spec, opts, io) {
3716
- if (!hasChantPullRequestApprovalStatus(chant)) return;
3717
- const posts = chant.opWavesPostsApprovalStatus ? chant.opWavesPostsApprovalStatus(spec) : wavesTakePullRequestApproval(spec);
3379
+ const posts = chant.opWavesPostsApprovalStatus(spec);
3718
3380
  if (!posts) return;
3719
3381
  const path = findConfig(io.cwd);
3720
3382
  const doc = path ? loadConfig(path).doc : void 0;
@@ -3730,7 +3392,6 @@ async function refreshApprovalStatus(chant, spec, opts, io) {
3730
3392
  }
3731
3393
  async function upTargetByPlan(chant, digest, opts = {}) {
3732
3394
  const needsPr = `the ${PR_GATE} gate is a pull request's: name it with --pr <n> (or PR_NUMBER), or approve pr-<n>`;
3733
- if (!hasChant4607(chant)) return { error: `${needsPr}. The installed chant cannot find a pull request from a plan digest (chant#4607)` };
3734
3395
  let gates;
3735
3396
  try {
3736
3397
  gates = await chant.pendingGatesByPlan(digest, { pullRequestsOnly: true, ...opts.cwd ? { cwd: opts.cwd } : {} });
@@ -3860,8 +3521,6 @@ async function upBeforeMerge(number, args, json, table, io) {
3860
3521
  const forge = forgeFlag.value ?? configured ?? "github";
3861
3522
  if (!FORGES.includes(forge)) return usage(`--forge ${forge} is not one of ${FORGES.join(", ")}`, table, io);
3862
3523
  return withChant(`/${PRODUCT} up`, table, io, async (chant) => {
3863
- const note = requiresNotHonoured(chant, config2.doc);
3864
- if (note) io.err(`${PRODUCT}: ${note}`);
3865
3524
  const argv = [
3866
3525
  "components",
3867
3526
  "pr-apply",
@@ -3911,12 +3570,11 @@ var OVERRIDE_OP, WAVES_SPEC_FILE, UP_STEP_KIND, PR_OP, APPROVE_PASS, APPROVE_SWI
3911
3570
  var init_up = __esm({
3912
3571
  "src/up.ts"() {
3913
3572
  "use strict";
3573
+ init_define_PLYWOOD_IMAGE_DIGESTS();
3914
3574
  init_apply();
3915
- init_chant_next();
3916
3575
  init_config();
3917
3576
  init_routing();
3918
3577
  init_refusals();
3919
- init_chant_product();
3920
3578
  init_review_stacks2();
3921
3579
  init_stages();
3922
3580
  init_words();
@@ -3999,7 +3657,10 @@ async function drift2(args, json, table, io) {
3999
3657
  try {
4000
3658
  const attribute = await attributionFor(doc, io);
4001
3659
  const root = dirname6(path);
4002
- for (const stack of stacks2) results.push(await readStack(chant, stack, driftMember(root, doc, stack), dir, { resource: resource.value, env: envFlag.value, ...attribute ? { attribute } : {} }));
3660
+ for (const stack of stacks2) {
3661
+ const backend = stacksOf(doc)[stack]?.backend ?? (typeof doc.backend === "string" ? doc.backend : void 0);
3662
+ results.push(await withBackend(backend, () => readStack(chant, stack, driftMember(root, doc, stack), dir, { resource: resource.value, env: envFlag.value, ...attribute ? { attribute } : {} })));
3663
+ }
4003
3664
  } finally {
4004
3665
  rmSync(dir, { recursive: true, force: true });
4005
3666
  }
@@ -4022,9 +3683,7 @@ async function drift2(args, json, table, io) {
4022
3683
  }
4023
3684
  }
4024
3685
  if (respond === "pull-request" && results.some((r) => r.drifted > 0)) {
4025
- io.err(
4026
- hasChantAgent(chant) ? `${PRODUCT} drift: respond.drift is pull-request; in CI the agent job after this one opens or updates the pull request (agent.drift)` : `${PRODUCT} drift: respond.drift is pull-request; this chant has no agent job (chant#4616), so this run kept the issue only`
4027
- );
3686
+ io.err(`${PRODUCT} drift: respond.drift is pull-request; in CI the agent job after this one opens or updates the pull request (agent.drift)`);
4028
3687
  }
4029
3688
  const notice = await driftNotice(chant, doc, results, envFlag.value, io);
4030
3689
  const drifted = results.filter((r) => r.drifted > 0).map((r) => r.stack);
@@ -4062,12 +3721,12 @@ async function drift2(args, json, table, io) {
4062
3721
  }
4063
3722
  async function driftNotice(chant, doc, results, environment, io) {
4064
3723
  const notify2 = chantNotifyOf(doc);
4065
- const loaded2 = notifiesDrift(notify2) ? await chantDriftNotify() : void 0;
3724
+ if (!notifiesDrift(notify2)) return void 0;
4066
3725
  return sendDriftNotice({
4067
3726
  notify: notify2,
4068
3727
  results,
4069
3728
  stackFingerprint: driftFingerprint,
4070
- chant: loaded2 ? { ...loaded2, words: chant.assertProductWords(PRODUCT_WORDS) } : void 0,
3729
+ chant: { ...await chantDriftNotify(), words: chant.assertProductWords(PRODUCT_WORDS) },
4071
3730
  env: io.env,
4072
3731
  ...environment ? { environment } : {},
4073
3732
  log: io.err,
@@ -4106,9 +3765,26 @@ async function readStack(chant, stack, member, dir, opts) {
4106
3765
  return { stack, ok: false, drifted: 0, rows: [], error: doc?.error ? `${doc.error.code}: ${doc.error.message}` : `chant workspace drift exited ${code}` };
4107
3766
  }
4108
3767
  const rows = driftRows(doc);
3768
+ const unread = (doc.records ?? []).filter((r) => r.status === "not-observed");
3769
+ if (rows.length === 0 && unread.length > 0) {
3770
+ const first = unread[0];
3771
+ const why = first.unobserved?.detail ?? first.unobserved?.reason ?? "no reason given";
3772
+ 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}` };
3773
+ }
4109
3774
  const attribution = rows.length && opts.attribute ? await attributeStack(opts.attribute, stack, doc) : void 0;
4110
3775
  return { stack, ok: true, drifted: rows.length, rows, observedAt: observedAt(doc), ...attribution ? { attribution } : {} };
4111
3776
  }
3777
+ async function withBackend(backend, read) {
3778
+ if (backend === void 0) return read();
3779
+ const before = process.env.PULUMI_BACKEND_URL;
3780
+ process.env.PULUMI_BACKEND_URL = backend;
3781
+ try {
3782
+ return await read();
3783
+ } finally {
3784
+ if (before === void 0) delete process.env.PULUMI_BACKEND_URL;
3785
+ else process.env.PULUMI_BACKEND_URL = before;
3786
+ }
3787
+ }
4112
3788
  async function attributionFor(doc, io) {
4113
3789
  const logs = driftSettingsOf(doc).auditLogs;
4114
3790
  if (!logs) return void 0;
@@ -4165,20 +3841,19 @@ function driftOpFile(doc, stacks2, schedule) {
4165
3841
  }
4166
3842
  function driftOpSpec(doc, forge, stacks2, schedule, chant, opts = {}) {
4167
3843
  const respond = driftResponseOf(doc);
4168
- const agentJob = hasChantAgent(chant) && respond === "pull-request";
4169
- const findingMode = respond === "none" ? "report" : respond === "pull-request" && !agentJob ? forge === "gitlab" ? "merge-request" : "pull-request" : "issue";
3844
+ const findingMode = respond === "none" ? "report" : "issue";
4170
3845
  const pipeline = toChantSettings({ doc }).pipeline;
4171
- const engine2 = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} }, false);
3846
+ const engine3 = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} });
4172
3847
  return {
4173
- ops: stacks2.map((s) => ({ name: driftOpName(s), trigger: { kind: "cron", schedule }, findingMode, ...driftOpFields(chant, doc, driftOpName(s), respond), ...withNotifyVariables(driftJobFields(forge, pipeline.jobs, s), driftNotifyVariables(forge, chantNotifyOf(doc))), ...opts.cloud ? driftCloudFields(doc, forge, s, respond, opts.env ?? "production", { contentsWrite: opts.contentsWrite === true }) : {} })),
3848
+ ops: stacks2.map((s) => ({ name: driftOpName(s), trigger: { kind: "cron", schedule }, findingMode, ...driftOpFields(chant, doc, driftOpName(s), respond), ...withNotifyVariables(driftJobFields(forge, pipeline.jobs, s), driftNotifyVariables(forge, chantNotifyOf(doc))), ...opts.cloud ? driftCloudFields(doc, forge, s, respond, opts.env ?? "production") : {} })),
4174
3849
  options: {
4175
3850
  runCommand: [...BIN.split(/\s+/), "drift", "--op", "{name}", "--forge", forge],
4176
- beforeScript: [SNAPSHOTS_FETCH, ...engine2.beforeScript ?? []],
4177
- ...engine2.image ? { image: engine2.image } : {}
3851
+ beforeScript: [SNAPSHOTS_FETCH, ...engine3.beforeScript ?? []],
3852
+ ...engine3.image ? { image: engine3.image } : {}
4178
3853
  }
4179
3854
  };
4180
3855
  }
4181
- function driftCloudFields(doc, forge, stack, respond, env, opts = {}) {
3856
+ function driftCloudFields(doc, forge, stack, respond, env) {
4182
3857
  const settings = toChantSettings({ doc });
4183
3858
  const decl = settings.ci.identity?.cloud;
4184
3859
  const ids = decl ? resolveTargetCloudIdentity(decl, env, stack, stack) : void 0;
@@ -4187,7 +3862,7 @@ function driftCloudFields(doc, forge, stack, respond, env, opts = {}) {
4187
3862
  const waves = wavesHolding(doc, [stack]);
4188
3863
  const command = waves === "all" ? `${BIN} up` : waves.map((k) => `${BIN} up --wave ${k}`).join(" && ");
4189
3864
  const fields = driftJobFields(forge, settings.pipeline.jobs, stack);
4190
- const permissions = opts.contentsWrite && forge !== "gitlab" ? { permissions: { contents: "write" } } : {};
3865
+ const permissions = forge !== "gitlab" ? { permissions: { contents: "write" } } : {};
4191
3866
  out.update = { command, ...ids?.apply ? { cloud: ids.apply } : {}, ...fields, ...permissions };
4192
3867
  return out;
4193
3868
  }
@@ -4221,7 +3896,7 @@ async function writeDriftJobs(chant, forge, doc, words, table, io, env = "produc
4221
3896
  return table.done;
4222
3897
  }
4223
3898
  if (writeIfChanged2(io.cwd, DRIFT_OP_FILE, driftOpFile(doc, settings.stacks, settings.schedule))) io.err(`${PRODUCT} ci: wrote ${DRIFT_OP_FILE}; commit it with the workflow`);
4224
- if (writeIfChanged2(io.cwd, DRIFT_SPEC_FILE, JSON.stringify(driftOpSpec(doc, forge, settings.stacks, settings.schedule, chant, { cloud: true, contentsWrite: true, env }), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${DRIFT_SPEC_FILE}`);
3899
+ if (writeIfChanged2(io.cwd, DRIFT_SPEC_FILE, JSON.stringify(driftOpSpec(doc, forge, settings.stacks, settings.schedule, chant, { cloud: true, env }), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${DRIFT_SPEC_FILE}`);
4225
3900
  const outDir = OP_OUTPUT_DIR2[forge];
4226
3901
  const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", DRIFT_SPEC_FILE, "--output", outDir], words);
4227
3902
  if (code !== table.done) return code;
@@ -4235,6 +3910,7 @@ var DRIFT_OP_FILE, DRIFT_SPEC_FILE, SNAPSHOTS_REF, SNAPSHOTS_FETCH, DRIFT_STATUS
4235
3910
  var init_drift2 = __esm({
4236
3911
  "src/drift.ts"() {
4237
3912
  "use strict";
3913
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4238
3914
  init_chant_next();
4239
3915
  init_images2();
4240
3916
  init_chant_config();
@@ -4272,6 +3948,7 @@ var TIPS;
4272
3948
  var init_tips = __esm({
4273
3949
  "src/tips.ts"() {
4274
3950
  "use strict";
3951
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4275
3952
  TIPS = [
4276
3953
  {
4277
3954
  id: "rename-without-aliases",
@@ -4388,9 +4065,6 @@ async function mcp(args, _json, table, io) {
4388
4065
  if (report.missing) return usage("--report-url takes a URL", table, io);
4389
4066
  if (report.rest.length) return usage(`mcp does not take ${report.rest.join(" ")}`, table, io);
4390
4067
  return withChant(`${PRODUCT} mcp`, table, io, (chant) => {
4391
- if (typeof chant.PRODUCT_MCP_HOOK_FLOOR !== "string") {
4392
- io.err(`${PRODUCT} mcp: this chant has no product tool hook (chant#4555), so the server serves chant's read tools and none of ${PRODUCT}'s`);
4393
- }
4394
4068
  const argv = ["serve", "mcp", "--read-only", ...propose ? ["--propose"] : []];
4395
4069
  return chant.runChantInProcess(argv, PRODUCT_WORDS, { mcp: plywoodMcpHook(report.value, propose) });
4396
4070
  });
@@ -4426,7 +4100,7 @@ function reviewOpFile(doc) {
4426
4100
  function reviewOpSpec(doc) {
4427
4101
  const settings = toChantSettings({ doc });
4428
4102
  const p = settings.pipeline;
4429
- const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
4103
+ const engine3 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} });
4430
4104
  return {
4431
4105
  ops: [
4432
4106
  {
@@ -4438,8 +4112,8 @@ function reviewOpSpec(doc) {
4438
4112
  }
4439
4113
  ],
4440
4114
  options: {
4441
- ...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
4442
- ...engine2.image ? { image: engine2.image } : {}
4115
+ ...engine3.beforeScript ? { beforeScript: engine3.beforeScript } : {},
4116
+ ...engine3.image ? { image: engine3.image } : {}
4443
4117
  }
4444
4118
  };
4445
4119
  }
@@ -4469,6 +4143,7 @@ var PERSON_VERBS, READ_INSTRUCTIONS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE
4469
4143
  var init_agents2 = __esm({
4470
4144
  "src/agents.ts"() {
4471
4145
  "use strict";
4146
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4472
4147
  init_chant_config();
4473
4148
  init_config_check();
4474
4149
  init_agents();
@@ -4505,6 +4180,7 @@ var PROVIDERS_PACK_DIR, PROVIDERS_PACK_CONFIG, PROVIDERS_REGO_FILE, PROVIDERS_PO
4505
4180
  var init_policy_presets = __esm({
4506
4181
  "src/policy-presets.ts"() {
4507
4182
  "use strict";
4183
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4508
4184
  PROVIDERS_PACK_DIR = "policy/plywood-providers";
4509
4185
  PROVIDERS_PACK_CONFIG = "policy/plywood-providers.json";
4510
4186
  PROVIDERS_REGO_FILE = "plywood_providers.rego";
@@ -4586,19 +4262,9 @@ function bucketTarget(url, env) {
4586
4262
  }
4587
4263
  return void 0;
4588
4264
  }
4589
- function needsBucketCheckMessage(builtChant) {
4590
- return `the bucket check needs chant >= the release after 0.124.54, which carries @intentius/chant/bucket-check (chant#4558). This plywood was built with chant ${builtChant}, which lacks it, so the bucket was not checked.`;
4591
- }
4592
4265
  function chantBucketCheck() {
4593
- cached4 ??= (async () => {
4594
- try {
4595
- const m = await import("@intentius/chant/bucket-check");
4596
- return typeof m.checkBucket === "function" ? m : void 0;
4597
- } catch {
4598
- return void 0;
4599
- }
4600
- })();
4601
- return cached4;
4266
+ cached2 ??= import("@intentius/chant/bucket-check").then((m) => m);
4267
+ return cached2;
4602
4268
  }
4603
4269
  function bucketLines(report) {
4604
4270
  const where = `${report.store === "azblob" ? "container" : "bucket"} ${report.bucket}`;
@@ -4611,47 +4277,38 @@ function bucketLines(report) {
4611
4277
  }
4612
4278
  return out;
4613
4279
  }
4614
- var cached4;
4280
+ var cached2;
4615
4281
  var init_backend = __esm({
4616
4282
  "src/init/backend.ts"() {
4617
4283
  "use strict";
4284
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4618
4285
  }
4619
4286
  });
4620
4287
 
4621
4288
  // src/init/adopt.ts
4622
4289
  function chantAdopt() {
4623
- cached5 ??= (async () => {
4624
- try {
4625
- const run = await import("@intentius/chant-lexicon-pulumi/run");
4626
- const lexicon = await import("@intentius/chant-lexicon-pulumi");
4627
- const args = run.plungeAdoptArgs;
4628
- const allowed = run.runAllowedPlunge;
4629
- const runner = run.defaultPlungeRunner;
4630
- const parse = lexicon.parsePlungeAdoptView;
4631
- const failure = run.pulumiRunFailure;
4632
- if (typeof args !== "function" || typeof allowed !== "function" || typeof runner !== "function" || typeof parse !== "function") return void 0;
4633
- return {
4634
- hasOp: typeof lexicon.PulumiAdoptOp === "function",
4635
- async preview({ dir, stack, backend }) {
4636
- try {
4637
- const { stdout } = await allowed(runner, args({ stack }), dir, { PULUMI_BACKEND_URL: backend });
4638
- const view = parse(stdout);
4639
- if (!view) throw new Error("plunge printed no adopt document");
4640
- return view;
4641
- } catch (err) {
4642
- const f = failure ? failure(err) : { message: err.message };
4643
- const view = parse(f.stdout);
4644
- if (view) return view;
4645
- if (f.missingBinary) throw new Error("the plunge binary was not found on PATH");
4646
- throw new Error((f.stderr?.trim() || f.message).split("\n").slice(-3).join("\n"));
4647
- }
4290
+ cached3 ??= (async () => {
4291
+ const { plungeAdoptArgs, runAllowedPlunge, defaultPlungeRunner, pulumiRunFailure } = await import("@intentius/chant-lexicon-pulumi/run");
4292
+ const { parsePlungeAdoptView } = await import("@intentius/chant-lexicon-pulumi");
4293
+ const parse = (t) => parsePlungeAdoptView(t);
4294
+ return {
4295
+ async preview({ dir, stack, backend }) {
4296
+ try {
4297
+ const { stdout } = await runAllowedPlunge(defaultPlungeRunner, plungeAdoptArgs({ stack }), dir, { PULUMI_BACKEND_URL: backend });
4298
+ const view = parse(stdout);
4299
+ if (!view) throw new Error("plunge printed no adopt document");
4300
+ return view;
4301
+ } catch (err) {
4302
+ const f = pulumiRunFailure(err);
4303
+ const view = parse(f.stdout);
4304
+ if (view) return view;
4305
+ if (f.missingBinary) throw new Error("the plunge binary was not found on PATH");
4306
+ throw new Error((f.stderr?.trim() || f.message).split("\n").slice(-3).join("\n"));
4648
4307
  }
4649
- };
4650
- } catch {
4651
- return void 0;
4652
- }
4308
+ }
4309
+ };
4653
4310
  })();
4654
- return cached5;
4311
+ return cached3;
4655
4312
  }
4656
4313
  function adoptCommand(s, backend) {
4657
4314
  const cd = s.dir === "." ? "" : `cd ${s.dir} && `;
@@ -4661,10 +4318,6 @@ async function adoptPreviews(stacks2, chant, root) {
4661
4318
  const out = [];
4662
4319
  for (const s of stacks2) {
4663
4320
  const command = adoptCommand(s, s.url);
4664
- if (!chant) {
4665
- out.push({ key: s.key, backend: s.url, command, error: "not run" });
4666
- continue;
4667
- }
4668
4321
  try {
4669
4322
  const view = await chant.preview({ dir: s.dir === "." ? root : `${root}/${s.dir}`, stack: s.stack, backend: s.url });
4670
4323
  out.push({ key: s.key, backend: s.url, command, view });
@@ -4679,9 +4332,8 @@ function fates(view) {
4679
4332
  for (const r of view.resources) n[r.fate] = (n[r.fate] ?? 0) + 1;
4680
4333
  return Object.entries(n).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([f, c]) => `${f} ${c}`).join(", ");
4681
4334
  }
4682
- function adoptLines(previews, chant) {
4335
+ function adoptLines(previews) {
4683
4336
  if (previews.length === 0) return [];
4684
- if (!chant) return [NEEDS_ADOPT_MESSAGE, ...previews.map((p) => ` ${p.key}: ${p.command}`)];
4685
4337
  const out = ["Adopt previews (plunge stack adopt --json, which writes nothing):"];
4686
4338
  for (const p of previews) {
4687
4339
  const v = p.view;
@@ -4747,14 +4399,14 @@ function adoptTodo(stacks2) {
4747
4399
  `Adopt the stacks onto plunge before their first plunge update: npx chant run ${ADOPT_OP}, then npx chant approve ${ADOPT_OP} <gate> for each of ${gates.join(", ")} after reading its preview, then npx chant run ${ADOPT_OP} again`
4748
4400
  ];
4749
4401
  }
4750
- var ADOPT_OP, ADOPT_OP_FILE, cached5, NEEDS_ADOPT_MESSAGE;
4402
+ var ADOPT_OP, ADOPT_OP_FILE, cached3;
4751
4403
  var init_adopt = __esm({
4752
4404
  "src/init/adopt.ts"() {
4753
4405
  "use strict";
4406
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4754
4407
  init_detect();
4755
4408
  ADOPT_OP = "plywood-adopt";
4756
4409
  ADOPT_OP_FILE = "plywood.adopt.op.ts";
4757
- NEEDS_ADOPT_MESSAGE = "the adopt previews and the adopt Op need @intentius/chant-lexicon-pulumi >= the release after 0.124.54 (chant#4562); run each preview by hand:";
4758
4410
  }
4759
4411
  });
4760
4412
 
@@ -4770,9 +4422,9 @@ function plungeUrlOf(url) {
4770
4422
  const scheme = url.split("://")[0];
4771
4423
  return { pointer: `is on ${url}, and plunge has no plunge+ form for ${scheme}. Move it to a file, s3, azblob or gs backend first: ${MOVE_A_STACK_GUIDE}` };
4772
4424
  }
4773
- function plungeStacks(detected2, env) {
4774
- return detected2.stacks.map((s) => {
4775
- const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
4425
+ function plungeStacks(detected, env) {
4426
+ return detected.stacks.map((s) => {
4427
+ const project = detected.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
4776
4428
  const b = backendOf(project, {}, env);
4777
4429
  const to = plungeUrlOf(b.url);
4778
4430
  return {
@@ -4817,6 +4469,7 @@ var INIT_ENGINES, MOVE_A_STACK_GUIDE, PLUNGE_FORMS;
4817
4469
  var init_engine2 = __esm({
4818
4470
  "src/init/engine.ts"() {
4819
4471
  "use strict";
4472
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4820
4473
  init_engine();
4821
4474
  init_backend();
4822
4475
  INIT_ENGINES = ["pulumi", "plunge"];
@@ -4840,6 +4493,7 @@ var PRESET_NAMES, DEFAULT_PRESET, PRESETS;
4840
4493
  var init_presets = __esm({
4841
4494
  "src/init/presets.ts"() {
4842
4495
  "use strict";
4496
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4843
4497
  PRESET_NAMES = ["merge", "before-merge", "approve-all"];
4844
4498
  DEFAULT_PRESET = "merge";
4845
4499
  PRESETS = [
@@ -4865,26 +4519,17 @@ var init_presets = __esm({
4865
4519
  // src/stack-order.ts
4866
4520
  import { join as join9 } from "node:path";
4867
4521
  function chantReferenceReader() {
4868
- cached6 ??= (async () => {
4869
- try {
4870
- const lexicon = await import("@intentius/chant-lexicon-pulumi");
4871
- if (typeof lexicon.pulumiStackReferences !== "function" || typeof lexicon.stackReferenceOwner !== "function") return void 0;
4872
- let run = {};
4873
- try {
4874
- run = await import("@intentius/chant-lexicon-pulumi/run");
4875
- } catch {
4876
- }
4877
- return {
4878
- pulumiStackReferences: lexicon.pulumiStackReferences,
4879
- stackReferenceOwner: lexicon.stackReferenceOwner,
4880
- ...typeof run.memberExportOf === "function" ? { memberExportOf: run.memberExportOf } : {},
4881
- ...typeof lexicon.readMemberExport === "function" ? { readMemberExport: lexicon.readMemberExport } : {}
4882
- };
4883
- } catch {
4884
- return void 0;
4885
- }
4522
+ cached4 ??= (async () => {
4523
+ const lexicon = await import("@intentius/chant-lexicon-pulumi");
4524
+ const run = await import("@intentius/chant-lexicon-pulumi/run");
4525
+ return {
4526
+ pulumiStackReferences: lexicon.pulumiStackReferences,
4527
+ stackReferenceOwner: lexicon.stackReferenceOwner,
4528
+ memberExportOf: run.memberExportOf,
4529
+ readMemberExport: lexicon.readMemberExport
4530
+ };
4886
4531
  })();
4887
- return cached6;
4532
+ return cached4;
4888
4533
  }
4889
4534
  async function exportOf(reader, root, s) {
4890
4535
  const dir = join9(root, s.dir);
@@ -4908,7 +4553,6 @@ async function stackOrder(root, stacks2, after = {}) {
4908
4553
  for (const upstream of ups) if (keys.has(reader2) && keys.has(upstream)) add({ reader: reader2, upstream, because: "waves.after" });
4909
4554
  }
4910
4555
  const reader = await chantReferenceReader();
4911
- if (!reader) return { edges, outside, reads: false };
4912
4556
  const owners = stacks2.map((s) => ({ component: s.key, project: s.project, stack: s.stack }));
4913
4557
  for (const s of stacks2) {
4914
4558
  const exported = await exportOf(reader, root, s);
@@ -4919,7 +4563,7 @@ async function stackOrder(root, stacks2, after = {}) {
4919
4563
  else outside.push({ stack: s.key, reference: ref.name, why: found.outside });
4920
4564
  }
4921
4565
  }
4922
- return { edges, outside, reads: true };
4566
+ return { edges, outside };
4923
4567
  }
4924
4568
  function orderWaves(waves, edges, canary = []) {
4925
4569
  const unmet = [];
@@ -4968,16 +4612,16 @@ function orderWaves(waves, edges, canary = []) {
4968
4612
  }
4969
4613
  function orderLines(order, unmet) {
4970
4614
  const out = [];
4971
- if (!order.reads) out.push("the installed @intentius/chant-lexicon-pulumi reads no StackReferences (chant#4547), so the waves follow waves.after alone");
4972
4615
  for (const e of order.edges) out.push(`${e.reader} updates after ${e.upstream} (${e.because === "stack-reference" ? "its StackReference" : "waves.after"})`);
4973
4616
  for (const o of order.outside) out.push(`${o.stack}'s StackReference ${o.reference} orders nothing: ${o.why}`);
4974
4617
  for (const u of unmet) out.push(`${u.reader} should update after ${u.upstream}, and cannot: ${u.why}; list them in one wave, or reorder waves`);
4975
4618
  return out;
4976
4619
  }
4977
- var cached6;
4620
+ var cached4;
4978
4621
  var init_stack_order = __esm({
4979
4622
  "src/stack-order.ts"() {
4980
4623
  "use strict";
4624
+ init_define_PLYWOOD_IMAGE_DIGESTS();
4981
4625
  }
4982
4626
  });
4983
4627
 
@@ -4995,9 +4639,9 @@ function repoJoin(...parts) {
4995
4639
  const out = posix2.normalize(parts.filter((p) => p !== "" && p !== ".").join("/") || ".").replace(/^\.\//, "").replace(/\/+$/, "");
4996
4640
  return out === "" ? "." : out;
4997
4641
  }
4998
- function projectAt(detected2, dir) {
4642
+ function projectAt(detected, dir) {
4999
4643
  const want = repoJoin(dir);
5000
- return detected2.projects.find((p) => repoJoin(p.dir) === want);
4644
+ return detected.projects.find((p) => repoJoin(p.dir) === want);
5001
4645
  }
5002
4646
  function stackEntry(keys, key) {
5003
4647
  const stacks2 = isObject(keys.stacks) ? keys.stacks : keys.stacks = {};
@@ -5032,6 +4676,7 @@ var IMPORT_SOURCES;
5032
4676
  var init_types2 = __esm({
5033
4677
  "src/import/types.ts"() {
5034
4678
  "use strict";
4679
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5035
4680
  init_types();
5036
4681
  IMPORT_SOURCES = ["pulumi-actions", "deployments", "spacelift", "env0"];
5037
4682
  }
@@ -5040,10 +4685,10 @@ var init_types2 = __esm({
5040
4685
  // src/import/deployments.ts
5041
4686
  import { readdirSync as readdirSync6 } from "node:fs";
5042
4687
  import { basename as basename2, dirname as dirname8, join as join11 } from "node:path";
5043
- function deploymentFiles(root, detected2, named) {
4688
+ function deploymentFiles(root, detected, named) {
5044
4689
  if (named.length) return [...named];
5045
4690
  const out = [];
5046
- for (const p of detected2.projects) {
4691
+ for (const p of detected.projects) {
5047
4692
  let names;
5048
4693
  try {
5049
4694
  names = readdirSync6(join11(root, p.dir)).sort();
@@ -5058,6 +4703,7 @@ var SETTINGS_FILE, NOT_CARRIED, importDeployments;
5058
4703
  var init_deployments = __esm({
5059
4704
  "src/import/deployments.ts"() {
5060
4705
  "use strict";
4706
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5061
4707
  init_types();
5062
4708
  init_detect();
5063
4709
  init_types2();
@@ -5073,7 +4719,7 @@ var init_deployments = __esm({
5073
4719
  "gitHub.deployPullRequest": "set apply.when: pull-request and update with /plywood up",
5074
4720
  "gitHub.repository": "plywood runs in the repository it is set up in"
5075
4721
  };
5076
- importDeployments = (root, detected2, named) => {
4722
+ importDeployments = (root, detected, named) => {
5077
4723
  let keys = {};
5078
4724
  const unsupported = [];
5079
4725
  const notes = [];
@@ -5081,7 +4727,7 @@ var init_deployments = __esm({
5081
4727
  const not = (file, setting, why) => {
5082
4728
  unsupported.push({ file, setting, why: why ?? NOT_CARRIED[setting] ?? "plywood has no setting for it" });
5083
4729
  };
5084
- for (const file of deploymentFiles(root, detected2, named)) {
4730
+ for (const file of deploymentFiles(root, detected, named)) {
5085
4731
  const m = SETTINGS_FILE.exec(basename2(file));
5086
4732
  if (!m) {
5087
4733
  not(file, "(file)", "names no stack: call it Pulumi.<stack>.deploy.yaml (or .json) beside the project's Pulumi.yaml");
@@ -5100,7 +4746,7 @@ var init_deployments = __esm({
5100
4746
  const fileDir = repoJoin(posixRel(root, dirname8(join11(root, file))));
5101
4747
  const source = isObject(settings.sourceContext) && isObject(settings.sourceContext.git) ? settings.sourceContext.git : {};
5102
4748
  const dir = typeof source.repoDir === "string" ? repoJoin(source.repoDir) : fileDir;
5103
- const project = projectAt(detected2, dir)?.name ?? projectAt(detected2, fileDir)?.name;
4749
+ const project = projectAt(detected, dir)?.name ?? projectAt(detected, fileDir)?.name;
5104
4750
  if (!project) {
5105
4751
  not(file, "sourceContext.git.repoDir", `${dir} holds no Pulumi.yaml`);
5106
4752
  continue;
@@ -5155,9 +4801,9 @@ var init_deployments = __esm({
5155
4801
  });
5156
4802
 
5157
4803
  // src/import/env0.ts
5158
- function stackFor(detected2, names) {
4804
+ function stackFor(detected, names) {
5159
4805
  for (const n of names) {
5160
- const hits = detected2.stacks.filter((s) => s.key === n || s.stack === n || detected2.stacks.filter((x) => x.project === s.project).length === 1 && s.project === n);
4806
+ const hits = detected.stacks.filter((s) => s.key === n || s.stack === n || detected.stacks.filter((x) => x.project === s.project).length === 1 && s.project === n);
5161
4807
  if (hits.length === 1) return hits[0].key;
5162
4808
  }
5163
4809
  return void 0;
@@ -5166,11 +4812,12 @@ var ENV0_WORKFLOW_FILES, ENV0_FLOW_FILES, importEnv0;
5166
4812
  var init_env0 = __esm({
5167
4813
  "src/import/env0.ts"() {
5168
4814
  "use strict";
4815
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5169
4816
  init_types();
5170
4817
  init_types2();
5171
4818
  ENV0_WORKFLOW_FILES = ["env0.workflow.yml", "env0.workflow.yaml"];
5172
4819
  ENV0_FLOW_FILES = ["env0.yml", "env0.yaml"];
5173
- importEnv0 = (root, detected2, named) => {
4820
+ importEnv0 = (root, detected, named) => {
5174
4821
  const keys = {};
5175
4822
  const unsupported = [];
5176
4823
  const notes = [];
@@ -5188,7 +4835,7 @@ var init_env0 = __esm({
5188
4835
  for (const [id, raw] of Object.entries(envs)) {
5189
4836
  const e = isObject(raw) ? raw : {};
5190
4837
  const names = [id, ...typeof e.name === "string" ? [e.name] : [], ...typeof e.templateName === "string" ? [e.templateName] : []];
5191
- const key = stackFor(detected2, names);
4838
+ const key = stackFor(detected, names);
5192
4839
  if (key) stackOf.set(id, key);
5193
4840
  else unsupported.push({ file: workflow, setting: `environments.${id}`, why: `names no stack of this repository (${names.join(", ")}); list it under stacks and waves.after by hand` });
5194
4841
  for (const k of Object.keys(e).filter((x) => !["name", "templateName", "needs"].includes(x))) {
@@ -5266,6 +4913,7 @@ var WORKFLOW_DIRS2, CARRIED, WHY, PULUMI_CLOUD, importPulumiActions;
5266
4913
  var init_pulumi_actions = __esm({
5267
4914
  "src/import/pulumi-actions.ts"() {
5268
4915
  "use strict";
4916
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5269
4917
  init_types();
5270
4918
  init_types2();
5271
4919
  WORKFLOW_DIRS2 = [".github/workflows", ".forgejo/workflows", ".gitea/workflows"];
@@ -5282,7 +4930,7 @@ var init_pulumi_actions = __esm({
5282
4930
  plan: "plywood's update applies the preview its approval names, through chant"
5283
4931
  };
5284
4932
  PULUMI_CLOUD = /^https:\/\/(?:app|api)\.pulumi\.com\/?$/;
5285
- importPulumiActions = (root, detected2, named) => {
4933
+ importPulumiActions = (root, detected, named) => {
5286
4934
  let keys = {};
5287
4935
  const unsupported = [];
5288
4936
  const notes = [];
@@ -5318,7 +4966,7 @@ var init_pulumi_actions = __esm({
5318
4966
  const dir = repoJoin(typeof w["work-dir"] === "string" ? w["work-dir"] : ".");
5319
4967
  const parts = stackName.split("/");
5320
4968
  const stack = parts[parts.length - 1];
5321
- const project = projectAt(detected2, dir)?.name ?? (parts.length === 3 ? parts[1] : void 0);
4969
+ const project = projectAt(detected, dir)?.name ?? (parts.length === 3 ? parts[1] : void 0);
5322
4970
  if (!project) {
5323
4971
  unsupported.push({ file, setting: `${where}.with.stack-name`, why: `${dir} holds no Pulumi.yaml, so ${stackName} names no project` });
5324
4972
  return;
@@ -5371,11 +5019,12 @@ var SPACELIFT_FILES, CARRIED2, importSpacelift;
5371
5019
  var init_spacelift = __esm({
5372
5020
  "src/import/spacelift.ts"() {
5373
5021
  "use strict";
5022
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5374
5023
  init_types();
5375
5024
  init_types2();
5376
5025
  SPACELIFT_FILES = [".spacelift/config.yml", ".spacelift/config.yaml"];
5377
5026
  CARRIED2 = ["project_root", "runner_image"];
5378
- importSpacelift = (root, detected2, named) => {
5027
+ importSpacelift = (root, detected, named) => {
5379
5028
  const keys = {};
5380
5029
  const unsupported = [];
5381
5030
  const notes = [];
@@ -5402,7 +5051,7 @@ var init_spacelift = __esm({
5402
5051
  const own = isObject(raw) ? raw : {};
5403
5052
  const s = { ...defaults, ...own };
5404
5053
  const dir = repoJoin(typeof s.project_root === "string" ? s.project_root : ".");
5405
- const project = projectAt(detected2, dir);
5054
+ const project = projectAt(detected, dir);
5406
5055
  if (!project) {
5407
5056
  unsupported.push({ file, setting: `stacks.${id}.project_root`, why: `${dir} holds no Pulumi.yaml` });
5408
5057
  continue;
@@ -5451,9 +5100,9 @@ function parseFrom(value) {
5451
5100
  if (!source) return { error: `--from ${value} is not one of ${IMPORT_SOURCES.join(", ")}, and its path names none; give <source>=${value}` };
5452
5101
  return { source, files: [file] };
5453
5102
  }
5454
- function runImport(root, detected2, from) {
5103
+ function runImport(root, detected, from) {
5455
5104
  try {
5456
- return IMPORTERS[from.source](root, detected2, from.files);
5105
+ return IMPORTERS[from.source](root, detected, from.files);
5457
5106
  } catch (e) {
5458
5107
  const file = from.files[0] ?? "(its files)";
5459
5108
  return { source: from.source, files: [], keys: {}, unsupported: [{ file, setting: "(file)", why: `could not be read: ${e.message}` }], notes: [] };
@@ -5463,6 +5112,7 @@ var IMPORTERS;
5463
5112
  var init_import = __esm({
5464
5113
  "src/import/index.ts"() {
5465
5114
  "use strict";
5115
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5466
5116
  init_deployments();
5467
5117
  init_env0();
5468
5118
  init_pulumi_actions();
@@ -5512,11 +5162,11 @@ function parseInitArgs(args) {
5512
5162
  };
5513
5163
  const preset2 = take("--preset");
5514
5164
  const gate = take("--gate");
5515
- const engine2 = take("--engine");
5165
+ const engine3 = take("--engine");
5516
5166
  const forge = take("--forge");
5517
5167
  const env = take("--env");
5518
5168
  const fromFlag = take("--from");
5519
- const bad = [preset2, gate, engine2, forge, env, fromFlag].find((t) => t.error);
5169
+ const bad = [preset2, gate, engine3, forge, env, fromFlag].find((t) => t.error);
5520
5170
  if (bad) return { error: bad.error };
5521
5171
  const flags = /* @__PURE__ */ new Set(["--no-bucket-check", "--dry-run", "--yes", "-y"]);
5522
5172
  const extra = rest.filter((a) => !flags.has(a));
@@ -5524,7 +5174,7 @@ function parseInitArgs(args) {
5524
5174
  if (preset2.value !== void 0 && !presetByName(preset2.value)) return { error: `--preset ${preset2.value} is not one of ${PRESET_NAMES.join(", ")}` };
5525
5175
  const g = gate.value !== void 0 ? gateFlag(gate.value) : void 0;
5526
5176
  if (gate.value !== void 0 && !g) return { error: `--gate ${gate.value} is not one of always, on-destructive (or destroy), never` };
5527
- if (engine2.value !== void 0 && !INIT_ENGINES.includes(engine2.value)) return { error: `--engine ${engine2.value} is not one of ${INIT_ENGINES.join(", ")}` };
5177
+ if (engine3.value !== void 0 && !INIT_ENGINES.includes(engine3.value)) return { error: `--engine ${engine3.value} is not one of ${INIT_ENGINES.join(", ")}` };
5528
5178
  if (forge.value !== void 0 && !FORGES.includes(forge.value)) return { error: `--forge ${forge.value} is not one of ${FORGES.join(", ")}` };
5529
5179
  const from = fromFlag.value !== void 0 ? parseFrom(fromFlag.value) : void 0;
5530
5180
  if (from && "error" in from) return { error: from.error };
@@ -5532,7 +5182,7 @@ function parseInitArgs(args) {
5532
5182
  ...from ? { from } : {},
5533
5183
  ...preset2.value ? { preset: preset2.value } : {},
5534
5184
  ...g ? { gate: g } : {},
5535
- ...engine2.value ? { engine: engine2.value } : {},
5185
+ ...engine3.value ? { engine: engine3.value } : {},
5536
5186
  ...forge.value ? { forge: forge.value } : {},
5537
5187
  env: env.value ?? "production",
5538
5188
  bucketCheck: !rest.includes("--no-bucket-check"),
@@ -5553,7 +5203,7 @@ function put(root, rel, text, dryRun, note) {
5553
5203
  function existingConfig(root) {
5554
5204
  return CONFIG_NAMES.find((n) => existsSync10(join13(root, n)));
5555
5205
  }
5556
- async function newConfigKeys(detected2, opts, io) {
5206
+ async function newConfigKeys(detected, opts, io) {
5557
5207
  const notes = [];
5558
5208
  let presetName = opts.preset;
5559
5209
  if (!presetName && io.ask && !opts.yes) {
@@ -5563,31 +5213,31 @@ async function newConfigKeys(detected2, opts, io) {
5563
5213
  const preset2 = presetByName(presetName ?? DEFAULT_PRESET);
5564
5214
  const keys = structuredClone(preset2.keys);
5565
5215
  if (opts.gate) keys.gate = opts.gate;
5566
- const engine2 = engineStep({ engine: opts.engine, detected: detected2, env: io.env });
5567
- Object.assign(keys, engine2.keys);
5568
- notes.push(...engine2.notes);
5569
- const engineBackends = engine2.keys.backend !== void 0 || engine2.keys.stacks !== void 0;
5216
+ const engine3 = engineStep({ engine: opts.engine, detected, env: io.env });
5217
+ Object.assign(keys, engine3.keys);
5218
+ notes.push(...engine3.notes);
5219
+ const engineBackends = engine3.keys.backend !== void 0 || engine3.keys.stacks !== void 0;
5570
5220
  let forge;
5571
5221
  if (opts.forge) {
5572
5222
  forge = { kind: opts.forge, because: "--forge" };
5573
- if (detected2.forge?.kind !== opts.forge) keys.forge = { kind: opts.forge };
5574
- else if (detected2.forge.url) keys.forge = { kind: opts.forge, url: detected2.forge.url };
5575
- } else if (detected2.forge) {
5576
- forge = { kind: detected2.forge.kind, because: detected2.forge.because };
5577
- if (detected2.forge.url) keys.forge = { kind: detected2.forge.kind, url: detected2.forge.url };
5223
+ if (detected.forge?.kind !== opts.forge) keys.forge = { kind: opts.forge };
5224
+ else if (detected.forge.url) keys.forge = { kind: opts.forge, url: detected.forge.url };
5225
+ } else if (detected.forge) {
5226
+ forge = { kind: detected.forge.kind, because: detected.forge.because };
5227
+ if (detected.forge.url) keys.forge = { kind: detected.forge.kind, url: detected.forge.url };
5578
5228
  } else {
5579
5229
  const kind = io.ask && !opts.yes ? await io.ask("Forge", FORGES, "github") : "github";
5580
5230
  forge = { kind, because: io.ask && !opts.yes ? "you chose it" : "origin names no forge init knows, so github; change forge.kind in plywood.yml" };
5581
5231
  keys.forge = { kind };
5582
5232
  }
5583
5233
  const fromEnv = io.env.PULUMI_BACKEND_URL?.trim();
5584
- if (fromEnv && !engineBackends && detected2.projects.some((p) => !p.backendUrl)) {
5234
+ if (fromEnv && !engineBackends && detected.projects.some((p) => !p.backendUrl)) {
5585
5235
  keys.backend = fromEnv;
5586
5236
  notes.push(`backend ${fromEnv} came from PULUMI_BACKEND_URL; it is in plywood.yml so the jobs use it too`);
5587
5237
  }
5588
5238
  return { keys, notes, forge };
5589
5239
  }
5590
- function initStacks(detected2, doc, members = []) {
5240
+ function initStacks(detected, doc, members = []) {
5591
5241
  const listed = stacksOf(doc);
5592
5242
  const pinned = new Map(members.filter((m) => m.engineBin && m.engineVersion).map((m) => [m.name, m]));
5593
5243
  const out = [];
@@ -5595,8 +5245,8 @@ function initStacks(detected2, doc, members = []) {
5595
5245
  const add = (key, project, stack, dir) => {
5596
5246
  const own = listed[key] ?? {};
5597
5247
  const backend = own.backend ?? (typeof doc.backend === "string" ? doc.backend : void 0);
5598
- const engine2 = engineOf(doc, backend)?.engine;
5599
- const grain = lockGrainFor(doc, engine2);
5248
+ const engine3 = engineOf(doc, backend)?.engine;
5249
+ const grain = lockGrainFor(doc, engine3);
5600
5250
  const stackDir = own.dir ?? dir;
5601
5251
  const packs = packsForStack(stepPolicyPacksOf(doc), stackDir);
5602
5252
  const pin = pinned.get(key);
@@ -5605,7 +5255,7 @@ function initStacks(detected2, doc, members = []) {
5605
5255
  project,
5606
5256
  stack,
5607
5257
  dir: stackDir,
5608
- ...engine2 ? { engine: engine2 } : {},
5258
+ ...engine3 ? { engine: engine3 } : {},
5609
5259
  // plywood#22: the step's backend, so a preview and an update read this stack's store, not the job's PULUMI_BACKEND_URL.
5610
5260
  ...backend ? { backend } : {},
5611
5261
  ...pin ? { engineVersion: pin.engineVersion, ...pin.engineRoot ? { engineRoot: pin.engineRoot } : {} } : {},
@@ -5617,7 +5267,7 @@ function initStacks(detected2, doc, members = []) {
5617
5267
  });
5618
5268
  seen.add(key);
5619
5269
  };
5620
- for (const s of detected2.stacks) add(s.key, s.project, s.stack, s.dir);
5270
+ for (const s of detected.stacks) add(s.key, s.project, s.stack, s.dir);
5621
5271
  for (const [key, own] of Object.entries(listed)) {
5622
5272
  if (seen.has(key)) continue;
5623
5273
  const [project, stack] = key.split("/");
@@ -5625,30 +5275,22 @@ function initStacks(detected2, doc, members = []) {
5625
5275
  }
5626
5276
  return out;
5627
5277
  }
5628
- function forgeFor2(doc, detected2, opts) {
5278
+ function forgeFor2(doc, detected, opts) {
5629
5279
  if (opts.forge) return { kind: opts.forge, because: "--forge" };
5630
5280
  const kind = isObject(doc.forge) ? doc.forge.kind : void 0;
5631
5281
  if (kind) return { kind, because: "plywood.yml says forge.kind" };
5632
- if (detected2.forge) return { kind: detected2.forge.kind, because: detected2.forge.because };
5282
+ if (detected.forge) return { kind: detected.forge.kind, because: detected.forge.because };
5633
5283
  return { kind: "github", because: "origin names no forge init knows, so github" };
5634
5284
  }
5635
- async function chantWriteCiHook() {
5636
- try {
5637
- const m = await import("@intentius/chant/components/check-hooks");
5638
- return typeof m.writeCiHook === "function" ? m.writeCiHook : void 0;
5639
- } catch {
5640
- return void 0;
5641
- }
5642
- }
5643
5285
  async function runInit(opts, table, io) {
5644
- const detected2 = detect(io.cwd);
5645
- const { root } = detected2;
5286
+ const detected = detect(io.cwd);
5287
+ const { root } = detected;
5646
5288
  const files = [];
5647
5289
  const notes = [];
5648
5290
  const todo = [];
5649
5291
  const buckets = [];
5650
5292
  const adopt = [];
5651
- const imported = opts.from ? runImport(root, detected2, opts.from) : void 0;
5293
+ const imported = opts.from ? runImport(root, detected, opts.from) : void 0;
5652
5294
  if (imported && imported.files.length === 0) notes.push(`--from ${imported.source} found no file to read${imported.notes.length ? `: ${imported.notes.join("; ")}` : ""}`);
5653
5295
  const fail = (message, forge2 = { kind: "github", because: "" }) => ({
5654
5296
  ok: false,
@@ -5663,8 +5305,8 @@ async function runInit(opts, table, io) {
5663
5305
  adopt,
5664
5306
  ...imported ? { imported } : {}
5665
5307
  });
5666
- if (detected2.projects.length === 0) return fail(`no Pulumi.yaml under ${root}; plywood init sets up a repository that has a Pulumi program`);
5667
- if (detected2.stacks.length === 0) notes.push("no Pulumi.<stack>.yaml beside any Pulumi.yaml, so no stack is set up; run pulumi stack init, then plywood init again");
5308
+ if (detected.projects.length === 0) return fail(`no Pulumi.yaml under ${root}; plywood init sets up a repository that has a Pulumi program`);
5309
+ if (detected.stacks.length === 0) notes.push("no Pulumi.<stack>.yaml beside any Pulumi.yaml, so no stack is set up; run pulumi stack init, then plywood init again");
5668
5310
  let config2;
5669
5311
  let forge;
5670
5312
  const kept = existingConfig(root);
@@ -5678,10 +5320,10 @@ async function runInit(opts, table, io) {
5678
5320
  const onPlunge = opts.engine === "plunge" && engineOf(config2.doc)?.engine === "plunge";
5679
5321
  const ignored = [opts.preset && "--preset", opts.gate && "--gate", opts.engine && !onPlunge && "--engine", imported && "--from"].filter(Boolean);
5680
5322
  if (ignored.length) notes.push(`${kept} is kept as it is, so ${ignored.join(", ")} changed nothing; set the keys there`);
5681
- if (opts.engine === "plunge" && !onPlunge) notes.push(...keptPlungeLines(kept, detected2, io.env));
5682
- forge = forgeFor2(config2.doc, detected2, opts);
5323
+ if (opts.engine === "plunge" && !onPlunge) notes.push(...keptPlungeLines(kept, detected, io.env));
5324
+ forge = forgeFor2(config2.doc, detected, opts);
5683
5325
  } else {
5684
- const made = await newConfigKeys(detected2, opts, io);
5326
+ const made = await newConfigKeys(detected, opts, io);
5685
5327
  notes.push(...made.notes);
5686
5328
  forge = made.forge;
5687
5329
  const text = plywoodYml(imported ? mergeKeys(made.keys, imported.keys) : made.keys);
@@ -5694,15 +5336,13 @@ async function runInit(opts, table, io) {
5694
5336
  if (problems.length) return fail(problems.map((p) => `${kept}: ${p.key} ${p.message}`).join("\n"), forge);
5695
5337
  const doc = config2.doc;
5696
5338
  const settings = toChantSettings(config2);
5697
- const reviewStacks2 = config2.doc.review_stacks !== void 0 ? await chantReviewStacks() : false;
5698
- if (settings.ci.prEnvironment && !reviewStacks2) delete settings.ci.prEnvironment;
5699
- const reviewSuffix = reviewStacks2 ? reviewSuffixOf(settings.pipeline.reviewStackName) : void 0;
5700
- const stacks2 = initStacks(detected2, doc, settings.members);
5339
+ const reviewSuffix = config2.doc.review_stacks !== void 0 ? reviewSuffixOf(settings.pipeline.reviewStackName) : void 0;
5340
+ const stacks2 = initStacks(detected, doc, settings.members);
5701
5341
  const order = await stackOrder(root, stacks2, settings.pipeline.after);
5702
5342
  const ordered = orderWaves(wavesOf(stacks2, settings.pipeline.waves), order.edges, settings.pipeline.canary);
5703
5343
  const waves = ordered.waves;
5704
- if (order.edges.length || order.outside.length || ordered.unmet.length || !order.reads && stacks2.length > 1) notes.push(...orderLines(order, ordered.unmet));
5705
- const declaration = declarationStep(root, detected2, settings, opts.dryRun);
5344
+ if (order.edges.length || order.outside.length || ordered.unmet.length) notes.push(...orderLines(order, ordered.unmet));
5345
+ const declaration = declarationStep(root, detected, settings, opts.dryRun);
5706
5346
  files.push(declaration.file);
5707
5347
  if (existsSync10(join13(root, CHANT_CONFIG_FILE))) {
5708
5348
  const text = readFileSync12(join13(root, CHANT_CONFIG_FILE), "utf8");
@@ -5712,19 +5352,16 @@ async function runInit(opts, table, io) {
5712
5352
  } else files.push(put(root, CHANT_CONFIG_FILE, chantConfigTs(), opts.dryRun));
5713
5353
  files.push(put(root, COMPONENTS_FILE, componentsFile(stacks2, settings.pipeline.after, reviewSuffix ? { reviewSuffix } : {}), opts.dryRun));
5714
5354
  files.push(...providersPresetStep(root, doc, opts.dryRun, notes));
5715
- if (detected2.preCommit) files.push(await preCommitStep(root, detected2.preCommit, opts.dryRun));
5716
- for (const wf of detected2.pulumiActions) {
5355
+ if (detected.preCommit) files.push(await preCommitStep(root, detected.preCommit, opts.dryRun));
5356
+ for (const wf of detected.pulumiActions) {
5717
5357
  notes.push(`${wf} runs pulumi/actions. init leaves it as it is; once plywood's previews and updates replace it, remove it, or keep it for the stacks plywood does not run`);
5718
5358
  }
5719
5359
  const chant = await chantProduct();
5720
- if (!chant) {
5721
- const message = needsChantMessage("Writing the waves spec and the workflow");
5722
- files.push({ file: WAVES_SPEC_FILE2, status: "skipped", note: message }, { file: WORKFLOW_FILE[forge.kind], status: "skipped", note: "needs the same chant" });
5723
- } else {
5724
- let spec = await wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves, forge.kind);
5360
+ {
5361
+ let spec = await wavesSpecStep(chant, stacks2, doc, settings, detected.defaultBranch, waves, forge.kind);
5725
5362
  if (settings.pipeline.jobs) {
5726
5363
  try {
5727
- spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind, { parallelism: await hasChant4625() });
5364
+ spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind);
5728
5365
  } catch (e) {
5729
5366
  files.push({ file: WAVES_SPEC_FILE2, status: "failed", note: e.message });
5730
5367
  spec = void 0;
@@ -5736,10 +5373,10 @@ async function runInit(opts, table, io) {
5736
5373
  }
5737
5374
  if (spec) files.push(...await workflowStep(chant, root, doc, settings, forge.kind, opts, stacks2.length > 0 ? spec : void 0, table, io));
5738
5375
  }
5739
- if (opts.bucketCheck) notes.push(...await bucketStep(detected2, doc, io.env, buckets));
5740
- adopt.push(...await adoptStep(root, detected2, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
5741
- todo.push(...todoLines(detected2, doc, forge.kind, io.env));
5742
- if (forge.kind === "gitlab" && chant && stacks2.length > 0 && previewOf(doc) && await hasChantWavePreview()) {
5376
+ if (opts.bucketCheck) notes.push(...await bucketStep(detected, doc, io.env, buckets));
5377
+ adopt.push(...await adoptStep(root, detected, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
5378
+ todo.push(...todoLines(detected, doc, forge.kind, io.env));
5379
+ if (forge.kind === "gitlab" && stacks2.length > 0 && previewOf(doc)) {
5743
5380
  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`);
5744
5381
  }
5745
5382
  const failed = files.find((f) => f.status === "failed");
@@ -5757,9 +5394,9 @@ async function runInit(opts, table, io) {
5757
5394
  ...imported ? { imported } : {}
5758
5395
  };
5759
5396
  }
5760
- function declarationStep(root, detected2, settings, dryRun) {
5397
+ function declarationStep(root, detected, settings, dryRun) {
5761
5398
  if (existsSync10(join13(root, "chant.workspace.jsonc"))) {
5762
- return { file: { file: "chant.workspace.jsonc", status: "kept", note: `add a member of kind pulumi for each of ${detected2.projects.map((p) => p.dir).join(", ")}, and pin ${PULUMI_LEXICON}@${CHANT_PIN}` } };
5399
+ return { file: { file: "chant.workspace.jsonc", status: "kept", note: `add a member of kind pulumi for each of ${detected.projects.map((p) => p.dir).join(", ")}, and pin ${PULUMI_LEXICON}@${CHANT_PIN}` } };
5763
5400
  }
5764
5401
  let existing;
5765
5402
  const file = join13(root, DECLARATION_FILE);
@@ -5772,7 +5409,7 @@ function declarationStep(root, detected2, settings, dryRun) {
5772
5409
  return { file: { file: DECLARATION_FILE, status: "failed", note: `is not JSON init can merge into (${e.message})` } };
5773
5410
  }
5774
5411
  }
5775
- const doc = workspaceDeclaration(existing, { name: repoName(root), projects: detected2.projects, ci: settings.ci, lexiconVersion: CHANT_PIN });
5412
+ const doc = workspaceDeclaration(existing, { name: repoName(root), projects: detected.projects, ci: settings.ci, lexiconVersion: CHANT_PIN });
5776
5413
  if (existing && JSON.stringify(existing) === JSON.stringify(doc)) return { file: { file: DECLARATION_FILE, status: "unchanged" }, doc };
5777
5414
  return { file: put(root, DECLARATION_FILE, jsonText(doc), dryRun), doc };
5778
5415
  }
@@ -5792,10 +5429,7 @@ function providersPresetStep(root, doc, dryRun, notes) {
5792
5429
  return out;
5793
5430
  }
5794
5431
  async function preCommitStep(root, config2, dryRun) {
5795
- const write = await chantWriteCiHook();
5796
- if (!write) {
5797
- return { file: DECLARATION_FILE, status: "skipped", note: `${config2} found; running it as a check hook needs chant >= the release after 0.124.54 (chant#4556's writeCiHook). This plywood was built with chant ${BUILT_CHANT}` };
5798
- }
5432
+ const { writeCiHook: write } = await import("@intentius/chant/components/check-hooks");
5799
5433
  if (dryRun || !existsSync10(join13(root, DECLARATION_FILE))) return { file: DECLARATION_FILE, status: "skipped", note: `${config2} would run as the check hook pre-commit` };
5800
5434
  const before = readFileSync12(join13(root, DECLARATION_FILE), "utf8");
5801
5435
  try {
@@ -5807,19 +5441,20 @@ async function preCommitStep(root, config2, dryRun) {
5807
5441
  return { file: DECLARATION_FILE, status: after === before ? "unchanged" : "written", note: `${config2} runs as the check hook pre-commit; the check job's image needs pre-commit` };
5808
5442
  }
5809
5443
  async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, forge) {
5810
- const runner = waveRunner(chantPipelineOptions(settings.pipeline, false).runner);
5444
+ const runner = waveRunner(chantPipelineOptions(settings.pipeline).runner);
5445
+ const rego = isObject(doc.policy) && (typeof doc.policy.rego === "string" || doc.policy.source !== void 0 || doc.policy.command !== void 0);
5811
5446
  const base = wavesSpec({
5447
+ ...rego ? { policy: true } : {},
5812
5448
  stacks: stacks2,
5813
5449
  ordered,
5814
5450
  gate: gateOf(doc),
5815
5451
  branch,
5816
5452
  ...runner !== void 0 ? { runner } : {},
5817
5453
  ...settings.pipeline.quorum?.length ? { quorum: settings.pipeline.quorum } : {},
5818
- // notify.relay only on a chant with the relay (chant#4611); an older chant refuses the setting.
5819
- ...settings.pipeline.notify ? { notify: { ...notifyForChant(settings.pipeline.notify, chant) } } : {}
5454
+ ...settings.pipeline.notify ? { notify: { ...settings.pipeline.notify } } : {}
5820
5455
  });
5821
- const named = chant.productWavesSpec ? chant.productWavesSpec(base, PRODUCT_WORDS) : base;
5822
- return previewOf(doc) && await hasChantWavePreview() ? withWavePreview(named, forge) : named;
5456
+ const named = chant.productWavesSpec(base, PRODUCT_WORDS);
5457
+ return previewOf(doc) ? withWavePreview(named, forge) : named;
5823
5458
  }
5824
5459
  async function workflowStep(chant, root, doc, settings, forge, opts, spec, table, io) {
5825
5460
  const output = WORKFLOW_FILE[forge];
@@ -5828,17 +5463,16 @@ async function workflowStep(chant, root, doc, settings, forge, opts, spec, table
5828
5463
  try {
5829
5464
  const { noteControlTarget: noteControlTarget2 } = await Promise.resolve().then(() => (init_control2(), control_exports));
5830
5465
  const control3 = await noteControlTarget2(doc, root);
5831
- plan = workflowPlan(chant, doc, forge, opts.env, spec, chantBuildFlags(chantPipelineOptions(settings.pipeline, false)), { reviewStacks: await chantReviewStacks(), prJobs, ...control3 ? { control: control3 } : {} });
5466
+ plan = workflowPlan(chant, doc, forge, opts.env, spec, chantBuildFlags(chantPipelineOptions(settings.pipeline)), { prJobs, ...control3 ? { control: control3 } : {} });
5832
5467
  } catch (e) {
5833
5468
  return [{ file: output, status: "failed", note: e.message }];
5834
5469
  }
5835
5470
  if (opts.dryRun) return [{ file: output, status: "skipped", note: "--dry-run: chant's generator was not run" }];
5836
5471
  const argv = plan.argv.map((a, i) => i === 1 && a === "." ? root : a);
5837
- const written = await writeWorkflow(chant, { root, forge, output, argv, words: plan.prWords, noteControl: plan.jobs.post.control });
5472
+ const written = await writeWorkflow(chant, { root, forge, output, argv, words: plan.prWords });
5838
5473
  if (written.status === "failed") return [{ file: output, status: "failed", note: `chant's generator exited ${written.code}; the old file is back as it was` }];
5839
5474
  const notes = [
5840
5475
  ...written.kept.length ? [`kept your own jobs: ${written.kept.join(", ")}`] : [],
5841
- ...doc.review_stacks !== void 0 && !plan.argv.includes("--pr-env") ? [REVIEW_STACKS_NEED_CHANT] : [],
5842
5476
  ...prJobs.notes,
5843
5477
  ...plan.notes,
5844
5478
  ...writtenNotes(plan, written.file)
@@ -5850,18 +5484,17 @@ async function workflowStep(chant, root, doc, settings, forge, opts, spec, table
5850
5484
  }
5851
5485
  return out;
5852
5486
  }
5853
- async function bucketStep(detected2, doc, env, reports) {
5487
+ async function bucketStep(detected, doc, env, reports) {
5854
5488
  const listed = stacksOf(doc);
5855
5489
  const urls = /* @__PURE__ */ new Set();
5856
- for (const s of detected2.stacks) {
5857
- const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5490
+ for (const s of detected.stacks) {
5491
+ const project = detected.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5858
5492
  const b = backendOf(project, { ...listed[s.key]?.backend ? { stack: listed[s.key].backend } : {}, ...typeof doc.backend === "string" ? { all: doc.backend } : {} }, env);
5859
5493
  if (b.url && !isPulumiCloud(b.url)) urls.add(b.url);
5860
5494
  }
5861
5495
  const targets = [...urls].map((u) => bucketTarget(u, env)).filter((t) => t !== void 0);
5862
5496
  if (targets.length === 0) return [];
5863
5497
  const check2 = await chantBucketCheck();
5864
- if (!check2) return [needsBucketCheckMessage(BUILT_CHANT)];
5865
5498
  const lines2 = ["State buckets:"];
5866
5499
  for (const t of targets) {
5867
5500
  if (!t.ok) {
@@ -5878,12 +5511,12 @@ async function bucketStep(detected2, doc, env, reports) {
5878
5511
  }
5879
5512
  return lines2;
5880
5513
  }
5881
- async function adoptStep(root, detected2, doc, env, declaration, dryRun, files, notes, todo) {
5514
+ async function adoptStep(root, detected, doc, env, declaration, dryRun, files, notes, todo) {
5882
5515
  const listed = stacksOf(doc);
5883
5516
  const all = typeof doc.backend === "string" ? doc.backend : void 0;
5884
5517
  const targets = [];
5885
- for (const s of detected2.stacks) {
5886
- const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5518
+ for (const s of detected.stacks) {
5519
+ const project = detected.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
5887
5520
  const own = listed[s.key];
5888
5521
  const b = backendOf(project, { ...own?.backend ? { stack: own.backend } : {}, ...all ? { all } : {} }, env);
5889
5522
  if (b.url && /^plunge\+/.test(b.url)) targets.push({ key: s.key, project: s.project, projectDir: s.dir, dir: own?.dir ?? s.dir, stack: s.stack, url: b.url });
@@ -5891,11 +5524,7 @@ async function adoptStep(root, detected2, doc, env, declaration, dryRun, files,
5891
5524
  if (targets.length === 0) return [];
5892
5525
  const chant = await chantAdopt();
5893
5526
  const previews = await adoptPreviews(targets, chant, root);
5894
- notes.push(...adoptLines(previews, chant));
5895
- if (!chant?.hasOp) {
5896
- files.push({ file: ADOPT_OP_FILE, status: "skipped", note: `the adopt Op needs @intentius/chant-lexicon-pulumi >= the release after 0.124.54 (chant#4562's PulumiAdoptOp). This plywood was built with chant ${BUILT_CHANT}` });
5897
- return previews;
5898
- }
5527
+ notes.push(...adoptLines(previews));
5899
5528
  const left = stacksToAdopt(targets, previews);
5900
5529
  if (left.length === 0) {
5901
5530
  if (existsSync10(join13(root, ADOPT_OP_FILE))) files.push({ file: ADOPT_OP_FILE, status: "kept", note: "every stack is adopted; delete it" });
@@ -5906,17 +5535,17 @@ async function adoptStep(root, detected2, doc, env, declaration, dryRun, files,
5906
5535
  todo.push(...adoptTodo(rows));
5907
5536
  return previews;
5908
5537
  }
5909
- function keptPlungeLines(kept, detected2, env) {
5910
- const step = engineStep({ engine: "plunge", detected: detected2, env });
5538
+ function keptPlungeLines(kept, detected, env) {
5539
+ const step = engineStep({ engine: "plunge", detected, env });
5911
5540
  const entries = Object.entries(step.keys);
5912
5541
  if (entries.length === 0) return step.notes;
5913
5542
  const yaml = entries.map(([k, v]) => emitYAMLEntry2(k, v)).join("\n").split("\n").filter((l) => l !== "");
5914
5543
  return [...step.notes, `To move to plunge, set these keys in ${kept}:`, ...yaml.map((l) => ` ${l}`)];
5915
5544
  }
5916
- function todoLines(detected2, doc, forge, env) {
5545
+ function todoLines(detected, doc, forge, env) {
5917
5546
  const out = [];
5918
5547
  const all = typeof doc.backend === "string" ? doc.backend : void 0;
5919
- const cloud = detected2.projects.some((p) => isPulumiCloud(backendOf(p, all ? { all } : {}, env).url));
5548
+ const cloud = detected.projects.some((p) => isPulumiCloud(backendOf(p, all ? { all } : {}, env).url));
5920
5549
  const where = forge === "gitlab" ? "CI/CD variables" : "repository secrets";
5921
5550
  const traded = identityClouds(doc);
5922
5551
  if (cloud && traded.includes("pulumi")) {
@@ -5924,9 +5553,9 @@ function todoLines(detected2, doc, forge, env) {
5924
5553
  } else if (cloud) {
5925
5554
  out.push(`Set pulumi_cloud.org and identity.preview.pulumi and identity.up.pulumi in plywood.yml, so each job trades its OIDC token for its own Pulumi Cloud token, or add PULUMI_ACCESS_TOKEN_PREVIEW and PULUMI_ACCESS_TOKEN_UP to the ${where}`);
5926
5555
  }
5927
- if (traded.some((c) => c !== "pulumi")) out.push(`Let each role identity names trust this repository's OIDC tokens: the preview role for pull requests, the up role for ${detected2.defaultBranch} only`);
5556
+ if (traded.some((c) => c !== "pulumi")) out.push(`Let each role identity names trust this repository's OIDC tokens: the preview role for pull requests, the up role for ${detected.defaultBranch} only`);
5928
5557
  else if (!cloud) out.push("Give the preview job and the update job their cloud access, each its own role through OIDC (identity in plywood.yml); the backend bucket's read and write go with them");
5929
- out.push(`Make ${PRODUCT}/preview a required status on ${detected2.defaultBranch}`);
5558
+ out.push(`Make ${PRODUCT}/preview a required status on ${detected.defaultBranch}`);
5930
5559
  return out;
5931
5560
  }
5932
5561
  function initLines(r) {
@@ -5962,11 +5591,11 @@ var DECLARATION_FILE, CHANT_CONFIG_FILE, COMPONENTS_FILE, WAVES_SPEC_FILE2, CHAN
5962
5591
  var init_init = __esm({
5963
5592
  "src/init.ts"() {
5964
5593
  "use strict";
5594
+ init_define_PLYWOOD_IMAGE_DIGESTS();
5965
5595
  init_package();
5966
5596
  init_chant_product();
5967
5597
  init_chant_config();
5968
5598
  init_identity();
5969
- init_observe();
5970
5599
  init_policy();
5971
5600
  init_policy_presets();
5972
5601
  init_approval();
@@ -6005,13 +5634,11 @@ var init_init = __esm({
6005
5634
  var control_exports = {};
6006
5635
  __export(control_exports, {
6007
5636
  CONTROLLED_KEYS: () => CONTROLLED_KEYS,
6008
- CONTROL_NEEDS_CHANT: () => CONTROL_NEEDS_CHANT,
6009
5637
  chantControl: () => chantControl,
6010
5638
  control: () => control2,
6011
5639
  controlGenerator: () => controlGenerator,
6012
5640
  controlTarget: () => controlTarget,
6013
5641
  controlledConfigText: () => controlledConfigText,
6014
- hasWavesControl: () => hasWavesControl,
6015
5642
  noteControlTarget: () => noteControlTarget,
6016
5643
  projectSettings: () => projectSettings,
6017
5644
  setChantControlForTests: () => setChantControlForTests,
@@ -6024,15 +5651,10 @@ import { createRequire } from "node:module";
6024
5651
  import { dirname as dirname10, join as join14 } from "node:path";
6025
5652
  import { pathToFileURL } from "node:url";
6026
5653
  import { emitYAMLEntry as emitYAMLEntry3, parseYAML as parseYAML4 } from "@intentius/chant/yaml";
6027
- function hasWavesControl(chant) {
6028
- return typeof chant.recordControlAppliedFromEnv === "function";
6029
- }
6030
5654
  async function wavesControl(cwd) {
6031
5655
  const file = findConfig(cwd);
6032
5656
  if (!file || controlOf(loadConfig(file).doc) === void 0) return { none: true };
6033
5657
  const chant = await chantControl();
6034
- if (!chant) return { missing: CONTROL_NEEDS_CHANT };
6035
- if (!hasWavesControl(chant)) return { missing: "the waves workflow tells the control repo with a chant that has chant#4617" };
6036
5658
  const target = await controlTarget(chant, cwd);
6037
5659
  if ("error" in target) return target;
6038
5660
  const settings = { ...controlOf(loadConfig(file).doc), member: target.member };
@@ -6044,7 +5666,7 @@ async function noteControlTarget(doc, root) {
6044
5666
  const settings = controlOf(doc);
6045
5667
  if (!settings) return void 0;
6046
5668
  const chant = await chantControl();
6047
- if (!chant || chant.controlSettingsProblems(settings).length > 0) return void 0;
5669
+ if (chant.controlSettingsProblems(settings).length > 0) return void 0;
6048
5670
  try {
6049
5671
  return chant.resolveControlTarget(settings, repoName(root));
6050
5672
  } catch {
@@ -6052,18 +5674,11 @@ async function noteControlTarget(doc, root) {
6052
5674
  }
6053
5675
  }
6054
5676
  function chantControl() {
6055
- cached7 ??= (async () => {
6056
- try {
6057
- const m = await import("@intentius/chant/ci-control");
6058
- return REQUIRED3.every((n) => typeof m[n] === "function") ? m : void 0;
6059
- } catch {
6060
- return void 0;
6061
- }
6062
- })();
6063
- return cached7;
5677
+ cached5 ??= import("@intentius/chant/ci-control").then((m) => m);
5678
+ return cached5;
6064
5679
  }
6065
5680
  function setChantControlForTests(fake) {
6066
- cached7 = fake === "reset" ? void 0 : Promise.resolve(fake);
5681
+ cached5 = fake === "reset" ? void 0 : Promise.resolve(fake);
6067
5682
  }
6068
5683
  function projectSettings(defaults, own) {
6069
5684
  return { ...defaults ?? {}, ...own };
@@ -6109,7 +5724,6 @@ async function control2(args, json, table, io) {
6109
5724
  const subs = ["init", "put", "import", "list", "retire", "reconcile", "dispatch"];
6110
5725
  if (!sub || !subs.includes(sub)) return usage(`control takes one of ${subs.join(", ")}`, table, io);
6111
5726
  const chant = await chantControl();
6112
- if (!chant) return usage(`${CONTROL_NEEDS_CHANT}. Upgrade plywood once that chant is out, or build against a local chant with 'just chant-local'.`, table, io);
6113
5727
  const emit = (envelope, lines2) => {
6114
5728
  if (json) io.out(JSON.stringify({ command: `control ${sub}`, ...envelope }));
6115
5729
  else for (const l of lines2) io.out(l);
@@ -6157,8 +5771,8 @@ async function controlInit(chant, args, emit, table, io) {
6157
5771
  const before = existsSync11(path) ? readFileSync13(path, "utf8") : void 0;
6158
5772
  const status = before === file.content ? "unchanged" : "written";
6159
5773
  if (!dryRun && status === "written") {
6160
- const { mkdirSync: mkdirSync8 } = await import("node:fs");
6161
- mkdirSync8(dirname10(path), { recursive: true });
5774
+ const { mkdirSync: mkdirSync7 } = await import("node:fs");
5775
+ mkdirSync7(dirname10(path), { recursive: true });
6162
5776
  writeFileSync7(path, file.content);
6163
5777
  }
6164
5778
  emit({ ok: true, exitCode: table.done, forge, file: file.path, status, dryRun, notes: file.notes }, [
@@ -6293,10 +5907,11 @@ async function controlReconcile(chant, args, emit, table, io) {
6293
5907
  emit({ ok: !failed, exitCode: code, mode, outcomes }, chant.describeReconcile(outcomes, mode).split("\n"));
6294
5908
  return code;
6295
5909
  }
6296
- var CONTROLLED_KEYS, CONTROL_NEEDS_CHANT, WAVES_CONTROL_KEYS, REQUIRED3, cached7, validateControlSettings;
5910
+ var CONTROLLED_KEYS, WAVES_CONTROL_KEYS, cached5, validateControlSettings;
6297
5911
  var init_control2 = __esm({
6298
5912
  "src/control.ts"() {
6299
5913
  "use strict";
5914
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6300
5915
  init_config();
6301
5916
  init_control();
6302
5917
  init_routing();
@@ -6305,9 +5920,7 @@ var init_control2 = __esm({
6305
5920
  init_stages();
6306
5921
  init_words();
6307
5922
  CONTROLLED_KEYS = ["gate", "approval", "approval_rules", "pulumi", "plunge", "image"];
6308
- CONTROL_NEEDS_CHANT = "the control repo needs chant >= the release after 0.124.54, which carries @intentius/chant/ci-control (chant#4568)";
6309
5923
  WAVES_CONTROL_KEYS = ["repo", "op", "forge", "member", "tokenSecret", "readTokenSecret", "ref", "apiBase"];
6310
- REQUIRED3 = ["controlDispatchRequest", "putControlEntry", "reconcileProjects"];
6311
5924
  validateControlSettings = (_entry, _project, settings) => {
6312
5925
  const out = [];
6313
5926
  for (const key of Object.keys(settings)) {
@@ -6341,13 +5954,8 @@ function positionals(args) {
6341
5954
  }
6342
5955
  return out;
6343
5956
  }
6344
- async function withChant(what, table, io, run) {
6345
- const chant = await chantProduct();
6346
- if (!chant) {
6347
- io.err(`${PRODUCT}: ${needsChantMessage(what)}`);
6348
- return table.failed;
6349
- }
6350
- return run(chant);
5957
+ async function withChant(_what, _table, _io, run) {
5958
+ return run(await chantProduct());
6351
5959
  }
6352
5960
  function passthrough(argv, table, io) {
6353
5961
  if (argv[0] === "run" && argv[1] === "wave") return runWave(argv, table, io);
@@ -6410,7 +6018,6 @@ async function preview(args, json, table, io) {
6410
6018
  const gate = rest.includes("--gate") ? [] : ["--gate", PR_GATE];
6411
6019
  const argv = ["components", "pr-plan", "--base", at, "--pr", number, ...gate, ...stack ? ["--member", stack] : [], ...rest, ...json ? ["--json"] : []];
6412
6020
  return withChant(`${PRODUCT} preview`, table, io, async (chant) => {
6413
- if (targeted && !hasChant4620(chant)) return usage(PREVIEW_TARGET_NEEDS_CHANT, table, io);
6414
6021
  return chant.runChantInProcess(argv, PRODUCT_WORDS);
6415
6022
  });
6416
6023
  }
@@ -6449,12 +6056,6 @@ async function ci(args, json, table, io) {
6449
6056
  const env = envFlag.value ?? "production";
6450
6057
  const output = outFlag.value ?? WORKFLOW_FILE[forge];
6451
6058
  return withChant(`${PRODUCT} ci`, table, io, async (chant) => {
6452
- const reviewStacks2 = await chantReviewStacks();
6453
- if (config2.doc.review_stacks !== void 0 && !reviewStacks2) io.err(`${PRODUCT}: ${REVIEW_STACKS_NEED_CHANT}`);
6454
- if (applyWhenOf(config2.doc) === "pull-request") {
6455
- const note = requiresNotHonoured(chant, config2.doc);
6456
- if (note) io.err(`${PRODUCT}: ${note}`);
6457
- }
6458
6059
  let waves;
6459
6060
  try {
6460
6061
  waves = readWavesSpec(io.cwd);
@@ -6462,12 +6063,12 @@ async function ci(args, json, table, io) {
6462
6063
  return usage(`.chant/op-waves/${OP_NAME}.spec.json: ${e.message}`, table, io);
6463
6064
  }
6464
6065
  const pipeline = toChantSettings(config2).pipeline;
6465
- const { prJobsFor: prJobsFor2, hasChant4625: hasChant46252, withJobSettings: withJobSettings2 } = await Promise.resolve().then(() => (init_jobs2(), jobs_exports));
6066
+ const { prJobsFor: prJobsFor2, withJobSettings: withJobSettings2 } = await Promise.resolve().then(() => (init_jobs2(), jobs_exports));
6466
6067
  const prJobs = await prJobsFor2(pipeline);
6467
6068
  for (const note of prJobs.notes) io.err(`${PRODUCT}: ${note}`);
6468
6069
  if (waves !== void 0 && pipeline.jobs) {
6469
6070
  try {
6470
- waves = withJobSettings2(waves, pipeline.jobs, forge, { parallelism: await hasChant46252() });
6071
+ waves = withJobSettings2(waves, pipeline.jobs, forge);
6471
6072
  } catch (e) {
6472
6073
  io.err(`${PRODUCT}: ${e.message}`);
6473
6074
  return table.failed;
@@ -6477,7 +6078,8 @@ async function ci(args, json, table, io) {
6477
6078
  const noteControl = await noteControlTarget2(config2.doc, io.cwd);
6478
6079
  let plan;
6479
6080
  try {
6480
- plan = workflowPlan(chant, config2.doc, forge, env, waves, [], { reviewStacks: reviewStacks2, prJobs, ...noteControl ? { control: noteControl } : {} });
6081
+ const engine3 = chantBuildFlags(chantPipelineOptions(pipeline));
6082
+ plan = workflowPlan(chant, config2.doc, forge, env, waves, engine3, { prJobs, ...noteControl ? { control: noteControl } : {} });
6481
6083
  } catch (e) {
6482
6084
  io.err(`${PRODUCT}: ${e.message}`);
6483
6085
  return table.failed;
@@ -6518,9 +6120,8 @@ async function ci(args, json, table, io) {
6518
6120
  if (jobs2.action) io.err(` action ${jobs2.action.command} (${jobs2.action.comment} <stack> <name>, or a dispatch with inputs ${jobs2.action.inputs.member} and ${jobs2.action.inputs.action})`);
6519
6121
  }
6520
6122
  if (!json && "control" in control3) io.err(` control after the last wave, tells ${control3.repo} how the waves ended`);
6521
- else if (!json && "missing" in control3) io.err(` control the waves workflow sends no dispatch: ${control3.missing}; a job of your own after the last wave runs '${PRODUCT} control dispatch'`);
6522
6123
  if (dryRun) return table.done;
6523
- const written = await writeWorkflow(chant, { root: io.cwd, forge, output, argv: plan.argv, words: plan.prWords, noteControl: plan.jobs.post.control });
6124
+ const written = await writeWorkflow(chant, { root: io.cwd, forge, output, argv: plan.argv, words: plan.prWords });
6524
6125
  if (!json && written.kept.length) io.err(` kept your own jobs: ${written.kept.join(", ")}`);
6525
6126
  if (written.code === table.done) for (const note of writtenNotes(plan, written.file)) io.err(`${PRODUCT}: ${note}`);
6526
6127
  if (json) io.out(JSON.stringify({ command: "ci", ok: written.code === 0, exitCode: written.code, dryRun, ...summary, status: written.status, kept: written.kept }));
@@ -6545,21 +6146,20 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
6545
6146
  const shape = pipelineShape(doc, forge, waves);
6546
6147
  const notes = [];
6547
6148
  const pipeline = toChantSettings({ doc }).pipeline;
6548
- const respond = respondOptions(chant, pipeline, shape.applyBeforeMerge, notes);
6149
+ const respond = respondOptions(pipeline, notes);
6549
6150
  const applyMerge = shape.applyBeforeMerge ? pipeline.applyMerge : void 0;
6550
- if (approvalOf(doc) === "pr-review" && shape.waves && hasChantApprovalStatus(chant) && !hasChantPullRequestApprovalStatus(chant)) notes.push(APPROVAL_STATUS_NOTE);
6551
6151
  const actionJob = actionJobFor(doc, forge, notes);
6552
6152
  const jobsWanted = actionJob ? [...shape.jobs, "action"] : shape.jobs;
6553
6153
  const words = wordsForJobs(jobsWanted);
6554
6154
  const requires = shape.applyBeforeMerge ? requiresArgv(chant, doc) : [];
6555
- const previewComment = hasChant4620(chant);
6556
6155
  const jobs2 = chant.productPipelineJobs(forge, env, {
6557
6156
  words,
6558
6157
  ...shape.applyBeforeMerge ? { applyBeforeMerge: true } : {},
6559
6158
  ...shape.locks ? { locks: shape.locks } : {},
6560
6159
  ...shape.waves ? { waves } : {},
6561
6160
  ...requires.length ? { applyRequires: requires[1].split(",") } : {},
6562
- ...previewComment ? { previewComment: true } : {},
6161
+ // chant#4620: `/plywood preview` previews the pull request again.
6162
+ previewComment: true,
6563
6163
  // chant#4619: the note job's read of the control repo comes back as post.control.
6564
6164
  ...opts.control !== void 0 ? { control: { target: opts.control } } : {},
6565
6165
  // plywood#29: runners, env, pass and parallelism on the pull request jobs, with chant#4625 (src/jobs.ts).
@@ -6585,12 +6185,14 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
6585
6185
  ...shape.applyBeforeMerge ? ["--apply-before-merge", ...requires] : [],
6586
6186
  ...applyMerge ? applyMergeArgv(applyMerge) : [],
6587
6187
  ...respond.argv,
6588
- ...previewComment ? ["--preview-comment"] : [],
6188
+ "--preview-comment",
6589
6189
  "--stale-notes",
6590
6190
  ...actionJob ? ["--action-job"] : [],
6591
- // Review stacks (plywood#15): chant's pr-env jobs, only once the pulumi lexicon can remove what they create.
6592
- ...doc.review_stacks !== void 0 && opts.reviewStacks ? ["--pr-env"] : [],
6191
+ // Review stacks (plywood#15): chant's pr-env jobs.
6192
+ ...doc.review_stacks !== void 0 ? ["--pr-env"] : [],
6593
6193
  ...opts.prJobs?.flags ?? [],
6194
+ // chant#4772: the control repo, so the note job the build writes reads its records (plywood#28).
6195
+ ...opts.control !== void 0 ? controlBuildArgv(doc, opts.control.member) : [],
6594
6196
  ...extra
6595
6197
  ];
6596
6198
  return { jobs: jobs2, words, prWords, argv, notes, ...applyMerge ? { applyMerge } : {} };
@@ -6603,15 +6205,11 @@ function actionJobFor(doc, forge, notes) {
6603
6205
  function applyMergeArgv(m) {
6604
6206
  return ["--merge", "auto", "--merge-token-env", m.tokenEnv, ...m.method ? ["--merge-method", m.method] : []];
6605
6207
  }
6606
- function respondOptions(chant, pipeline, applyBeforeMerge, notes) {
6208
+ function respondOptions(pipeline, notes) {
6607
6209
  const r = pipeline.respond;
6608
6210
  if (pipeline.decide && r?.description !== "check") notes.push(DECIDE_LEFT_OFF);
6609
6211
  if (!r || Object.keys(r).length === 0) return { options: {}, argv: [] };
6610
6212
  const keys = Object.keys(r).map((e) => e.replace("-", "_"));
6611
- if (!hasChantRespond(chant)) {
6612
- notes.push(`respond.${keys.join(", respond.")} ${keys.length === 1 ? "needs" : "need"} chant's responses to pipeline events (chant#4629), which this chant lacks; chant's defaults hold`);
6613
- return { options: {}, argv: [] };
6614
- }
6615
6213
  const responses = { ...r };
6616
6214
  const check2 = responses.description === "check";
6617
6215
  const decide = check2 ? pipeline.decide : void 0;
@@ -6620,14 +6218,7 @@ function respondOptions(chant, pipeline, applyBeforeMerge, notes) {
6620
6218
  notes.push("respond.description is check and decide names no backend and model, so the description check is left off");
6621
6219
  }
6622
6220
  const options = { respond: responses, ...decide ? { decide: { ...decide } } : {} };
6623
- if (hasChantPrLoopFlags(chant)) return { options, argv: respondArgv(responses, decide) };
6624
- const prSide = [
6625
- ...responses.description === "check" ? ["respond.description"] : [],
6626
- ...applyBeforeMerge && responses["apply-failed"] === "off" ? ["respond.apply_failed"] : [],
6627
- ...applyBeforeMerge && responses["wave-refused"] === "off" ? ["respond.wave_refused"] : []
6628
- ];
6629
- if (prSide.length) notes.push(`${prSide.join(", ")} ${prSide.length === 1 ? "reaches" : "reach"} no pull request job yet: chant's build --pr-loop writes no responses into them, so the note and the update before merge keep chant's defaults; the waves workflow takes wave_refused and apply_failed`);
6630
- return { options, argv: [] };
6221
+ return { options, argv: respondArgv(responses, decide) };
6631
6222
  }
6632
6223
  function respondArgv(responses, decide) {
6633
6224
  const list = Object.keys(CHANT_RESPOND_DEFAULTS).filter((e) => responses[e] !== void 0 && responses[e] !== CHANT_RESPOND_DEFAULTS[e]).map((e) => `${e}=${responses[e]}`).join(",");
@@ -6636,35 +6227,30 @@ function respondArgv(responses, decide) {
6636
6227
  }
6637
6228
  function requiresArgv(chant, doc) {
6638
6229
  const listed = applyRequiresOf(doc);
6639
- if (listed.length === APPLY_REQUIRES.length || !hasChant4607(chant)) return [];
6230
+ if (listed.length === APPLY_REQUIRES.length) return [];
6640
6231
  const parsed = chant.parseRequiresList(listed);
6641
6232
  return "checks" in parsed ? ["--requires", parsed.checks.join(",")] : [];
6642
6233
  }
6643
- function requiresNotHonoured(chant, doc) {
6644
- const listed = applyRequiresOf(doc);
6645
- if (listed.length === APPLY_REQUIRES.length || hasChant4607(chant)) return void 0;
6646
- return `apply.requires lists ${listed.join(", ")}, and the installed chant has no pr-apply --requires (chant#4607), so the update before merge asks all four checks: ${APPLY_REQUIRES.join(", ")}`;
6647
- }
6648
6234
  function usage(message, table, io) {
6649
6235
  io.err(`${PRODUCT}: ${message}`);
6650
6236
  return table.failed;
6651
6237
  }
6652
- var CHANT_PASSTHROUGH, VALUED, PREVIEW_TARGET_NEEDS_CHANT, WORKFLOW_FILE, ACTIONS_NOT_ON_GITLAB, APPROVAL_STATUS_NOTE, CHANT_RESPOND_DEFAULTS, DECIDE_LEFT_OFF;
6238
+ var CHANT_PASSTHROUGH, VALUED, WORKFLOW_FILE, ACTIONS_NOT_ON_GITLAB, CHANT_RESPOND_DEFAULTS, DECIDE_LEFT_OFF;
6653
6239
  var init_stages = __esm({
6654
6240
  "src/stages.ts"() {
6655
6241
  "use strict";
6242
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6656
6243
  init_chant_product();
6657
6244
  init_chant_config();
6658
- init_chant_next();
6659
- init_approval();
6660
6245
  init_config_check();
6661
6246
  init_apply();
6662
6247
  init_config();
6248
+ init_control();
6663
6249
  init_locks();
6664
6250
  init_stacks();
6665
6251
  init_routing();
6252
+ init_images();
6666
6253
  init_workflow();
6667
- init_review_stacks2();
6668
6254
  init_waves_workflow();
6669
6255
  init_drift2();
6670
6256
  init_refusals();
@@ -6673,20 +6259,22 @@ var init_stages = __esm({
6673
6259
  init_words();
6674
6260
  CHANT_PASSTHROUGH = ["components", "run", "lock", "unlock", "query", "ledger", "audit-log"];
6675
6261
  VALUED = /* @__PURE__ */ new Set(["--base", "--pr", "--forge", "--env", "--output", "-o", "--stack", "--file", "--head", "--gate", "--branch", "--report-url", "--target"]);
6676
- PREVIEW_TARGET_NEEDS_CHANT = "--target needs a chant whose pr-plan passes it to the pulumi-up step (chant#4620), which this chant lacks";
6677
6262
  WORKFLOW_FILE = {
6678
6263
  github: ".github/workflows/plywood.yml",
6679
6264
  forgejo: ".forgejo/workflows/plywood.yml",
6680
6265
  gitlab: ".gitlab-ci.yml"
6681
6266
  };
6682
6267
  ACTIONS_NOT_ON_GITLAB = "plywood.yml declares actions, and a GitLab pipeline starts on no comment or dispatch with inputs, so it has no action job; run 'plywood action <stack> <name>' from a pipeline of your own";
6683
- APPROVAL_STATUS_NOTE = `approval is pr-review, and chant posts its approval status only for waves that wait on pr-review; plywood's waves wait on the pull request's own approval, so no ${PRODUCT}/approval status is posted before merge`;
6684
6268
  CHANT_RESPOND_DEFAULTS = Object.freeze({ "wave-refused": "diff", "apply-failed": "triage", description: "off" });
6685
6269
  DECIDE_LEFT_OFF = "decide is set and respond.description is not check, so plywood leaves the decide service off the pull request jobs";
6686
6270
  }
6687
6271
  });
6688
6272
 
6273
+ // src/bin.ts
6274
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6275
+
6689
6276
  // src/chant-resolve.ts
6277
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6690
6278
  import * as nodeModule from "node:module";
6691
6279
  var CHANT_SPECIFIER = /^@intentius\/chant(?:-lexicon-[a-z0-9-]+)?(?:\/|$)/;
6692
6280
  function chantResolveHook(from) {
@@ -6712,50 +6300,22 @@ function installChantResolution(from = import.meta.url) {
6712
6300
  }
6713
6301
 
6714
6302
  // src/cli.ts
6303
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6715
6304
  init_config_check();
6716
6305
 
6717
6306
  // src/exit-codes.ts
6718
- var FALLBACK_EXIT = Object.freeze({
6719
- done: 0,
6720
- failed: 1,
6721
- drift: 2,
6722
- waiting: 3,
6723
- moved: 4,
6724
- held: 5,
6725
- superseded: 6,
6726
- denied: 7,
6727
- declined: 8,
6728
- misplaced: 9
6729
- });
6730
- var NAMES = Object.keys(FALLBACK_EXIT).filter((n) => n !== "misplaced");
6731
- function isExitTable(t) {
6732
- if (!t || typeof t !== "object") return false;
6733
- const r = t;
6734
- return NAMES.every((n) => Number.isInteger(r[n])) && (r.misplaced === void 0 || Number.isInteger(r.misplaced));
6735
- }
6736
- function withMisplaced(t) {
6737
- return Number.isInteger(t.misplaced) ? t : Object.freeze({ ...t, misplaced: FALLBACK_EXIT.misplaced });
6738
- }
6739
- var cached;
6740
- function exitCodes() {
6741
- cached ??= (async () => {
6742
- try {
6743
- const m = await import("@intentius/chant/exit-codes");
6744
- const table = m.CHANT_EXIT;
6745
- if (isExitTable(table)) return { table: withMisplaced(table), source: "chant" };
6746
- } catch {
6747
- }
6748
- return { table: FALLBACK_EXIT, source: "fallback" };
6749
- })();
6750
- return cached;
6751
- }
6307
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6308
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
6309
+ var EXIT = CHANT_EXIT;
6752
6310
 
6753
6311
  // src/actions.ts
6312
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6754
6313
  init_chant_product();
6755
6314
  init_stacks();
6756
6315
  init_stages();
6757
6316
 
6758
6317
  // src/stack-ops.ts
6318
+ init_define_PLYWOOD_IMAGE_DIGESTS();
6759
6319
  init_chant_config();
6760
6320
  init_identity();
6761
6321
  init_config();
@@ -6764,28 +6324,18 @@ init_adopt();
6764
6324
  init_detect();
6765
6325
  init_refusals();
6766
6326
  init_stages();
6767
- init_chant_product();
6768
6327
  init_words();
6769
6328
  import { existsSync as existsSync13 } from "node:fs";
6770
6329
  import { dirname as dirname11, isAbsolute as isAbsolute2, join as join16, resolve as resolve2 } from "node:path";
6771
6330
  var STACK_OPS_OP = "pulumi-stack-ops";
6772
- var REQUIRED4 = ["gatedStackExport", "gatedStackCancel", "gatedStackMove"];
6773
6331
  var cachedLocks;
6774
6332
  function chantStackLockReader() {
6775
- cachedLocks ??= (async () => {
6776
- try {
6777
- const m = await import("@intentius/chant-lexicon-pulumi");
6778
- return typeof m.pulumiStackLockIds === "function" ? m.pulumiStackLockIds : void 0;
6779
- } catch {
6780
- return void 0;
6781
- }
6782
- })();
6333
+ cachedLocks ??= import("@intentius/chant-lexicon-pulumi").then((m) => m.pulumiStackLockIds);
6783
6334
  return cachedLocks;
6784
6335
  }
6785
6336
  async function lockToCancel(given, stack, env) {
6786
6337
  if (given) return { lockId: given, read: false };
6787
6338
  const reader = await chantStackLockReader();
6788
- if (!reader) return { error: STACK_OP_WORDS.needsLock };
6789
6339
  let locks2;
6790
6340
  try {
6791
6341
  locks2 = await reader({ dir: stack.dir, stack: stack.stack, ...stack.project ? { project: stack.project } : {}, ...stack.engine ? { engine: stack.engine } : {}, env });
@@ -6798,34 +6348,10 @@ async function lockToCancel(given, stack, env) {
6798
6348
  return { lockId: locks2.ids[0], read: true };
6799
6349
  }
6800
6350
  var cachedOps;
6801
- var cachedRollback;
6802
6351
  function chantStackOps() {
6803
- cachedOps ??= (async () => {
6804
- try {
6805
- const m = await import("@intentius/chant-lexicon-pulumi");
6806
- return REQUIRED4.every((name) => typeof m[name] === "function") ? m : void 0;
6807
- } catch {
6808
- return void 0;
6809
- }
6810
- })();
6352
+ cachedOps ??= import("@intentius/chant-lexicon-pulumi").then((m) => m);
6811
6353
  return cachedOps;
6812
6354
  }
6813
- function chantStackRollback() {
6814
- cachedRollback ??= (async () => {
6815
- try {
6816
- const m = await import("@intentius/chant-lexicon-pulumi");
6817
- if (typeof m.pulumiReleaseInputs !== "function") return false;
6818
- const copy2 = await import("@intentius/chant-lexicon-pulumi/release-copy");
6819
- return typeof copy2.prepareReleaseCopy === "function";
6820
- } catch {
6821
- return false;
6822
- }
6823
- })();
6824
- return cachedRollback;
6825
- }
6826
- function needsStackOpsMessage(what, feature, after = "0.124.54") {
6827
- return `${what} needs @intentius/chant-lexicon-pulumi >= the release after ${after}, which carries ${feature}. This plywood was built with chant ${BUILT_CHANT}, which lacks it. Upgrade plywood once that chant is out, or build against a local chant with 'just chant-local'.`;
6828
- }
6829
6355
  function readConfig(cwd) {
6830
6356
  const path = findConfig(cwd);
6831
6357
  if (!path) return { error: `no plywood.yml in ${cwd} or above it; run '${PRODUCT} init' first` };
@@ -6921,10 +6447,6 @@ function reportOutcome(verb, outcome, gate, rules, done, json, table, io) {
6921
6447
  }
6922
6448
  async function runRequest(verb, json, table, io, call) {
6923
6449
  const ops = await chantStackOps();
6924
- if (!ops) {
6925
- io.err(`${PRODUCT}: ${needsStackOpsMessage(`${PRODUCT} ${verb}`, "the gated stack operations (chant#4578)")}`);
6926
- return table.failed;
6927
- }
6928
6450
  try {
6929
6451
  return await call(ops);
6930
6452
  } catch (e) {
@@ -7035,7 +6557,7 @@ async function moveStack(args, json, table, io) {
7035
6557
  if (backend) return usage(backend, table, io);
7036
6558
  const rules = requestRules(read.config);
7037
6559
  const destName = destStackName(source, dest, read.config);
7038
- const engine2 = source.engine ?? dest.engine;
6560
+ const engine3 = source.engine ?? dest.engine;
7039
6561
  return runRequest("move", json, table, io, async (ops) => {
7040
6562
  const outcome = await ops.gatedStackMove({
7041
6563
  by: requesterOf(common.by, io.env),
@@ -7046,13 +6568,13 @@ async function moveStack(args, json, table, io) {
7046
6568
  source: { member: source.key, dir: source.dir, stack: source.stack },
7047
6569
  dest: { member: dest.key, dir: dest.dir, stack: destName },
7048
6570
  urns,
7049
- ...engine2 ? { engine: engine2 } : {}
6571
+ ...engine3 ? { engine: engine3 } : {}
7050
6572
  });
7051
6573
  return reportOutcome("move", outcome, `move:${source.stack}:${destName}`, rules, (r) => STACK_OP_WORDS.moved(source.key, dest.key, r), json, table, io);
7052
6574
  });
7053
6575
  }
7054
- async function moveOntoPlunge(engine2, args, json, table, io) {
7055
- if (engine2 !== "plunge") return usage(`--onto takes plunge, not '${engine2}'; a move between backends is an export and an import (see '${PRODUCT} export')`, table, io);
6576
+ async function moveOntoPlunge(engine3, args, json, table, io) {
6577
+ if (engine3 !== "plunge") return usage(`--onto takes plunge, not '${engine3}'; a move between backends is an export and an import (see '${PRODUCT} export')`, table, io);
7056
6578
  const extra = args.filter((a) => a.startsWith("-"));
7057
6579
  if (extra.length) return usage(`move --onto does not take ${extra.join(" ")}`, table, io);
7058
6580
  const named = positionals(args);
@@ -7102,10 +6624,6 @@ async function rollbackStack(args, json, table, io) {
7102
6624
  argv = ["components", "rollback", env.value ?? environmentOf(stack.stack), "--component", slug(stack.key), ...digest.value ? ["--digest", digest.value] : []];
7103
6625
  }
7104
6626
  argv.push(...actor.value ? ["--actor", actor.value] : [], ...switches, ...json ? ["--json"] : []);
7105
- if (!await chantStackRollback()) {
7106
- io.err(`${PRODUCT}: ${needsStackOpsMessage(`${PRODUCT} rollback`, "rollback by release for stacks (chant#4542)")}`);
7107
- return table.failed;
7108
- }
7109
6627
  return withChant(`${PRODUCT} rollback`, table, io, async (chant) => {
7110
6628
  const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
7111
6629
  if (code === table.waiting && !json) io.err(STACK_OP_WORDS.rollbackWaiting);
@@ -7123,7 +6641,6 @@ function chantStackAction() {
7123
6641
  }
7124
6642
  async function fromEvent(env) {
7125
6643
  const chant = await chantProduct();
7126
- if (!chant) return { error: `${PRODUCT} action --from-event reads the action job's comment or inputs through chant's productActionFromEnv, and chant's product entry did not load` };
7127
6644
  const words = chant.assertProductWords({ ...PRODUCT_WORDS, commands: { ...PRODUCT_WORDS.commands, ...ACTION_COMMAND } });
7128
6645
  const asked = chant.productActionFromEnv(words, env);
7129
6646
  if (asked.kind === "refused") return { error: `the action job was asked for no action: ${asked.reason}` };
@@ -7194,6 +6711,7 @@ async function action(args, json, table, io) {
7194
6711
  }
7195
6712
 
7196
6713
  // src/audit.ts
6714
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7197
6715
  init_detect();
7198
6716
  init_stages();
7199
6717
  init_words();
@@ -7252,6 +6770,7 @@ init_agents2();
7252
6770
  init_drift2();
7253
6771
 
7254
6772
  // src/estate.ts
6773
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7255
6774
  init_chant_product();
7256
6775
  init_chant_config();
7257
6776
  init_config();
@@ -7261,19 +6780,12 @@ import { dirname as dirname12 } from "node:path";
7261
6780
  var estateModule;
7262
6781
  function chantEstate() {
7263
6782
  estateModule ??= (async () => {
7264
- try {
7265
- const m = await import("@intentius/chant/ci-estate");
7266
- const r = await import("@intentius/chant/pr-report-url");
7267
- if (!["readEstateLedger", "buildLedgerEstate", "writeEstate"].every((f) => typeof m[f] === "function")) return void 0;
7268
- if (typeof r.parseReportUrl !== "function" || typeof r.reportPut !== "function") return void 0;
7269
- return { ...m, parseReportUrl: r.parseReportUrl, reportPut: r.reportPut };
7270
- } catch {
7271
- return void 0;
7272
- }
6783
+ const m = await import("@intentius/chant/ci-estate");
6784
+ const r = await import("@intentius/chant/pr-report-url");
6785
+ return { readEstateLedger: m.readEstateLedger, buildLedgerEstate: m.buildLedgerEstate, writeEstate: m.writeEstate, parseReportUrl: r.parseReportUrl, reportPut: r.reportPut };
7273
6786
  })();
7274
6787
  return estateModule;
7275
6788
  }
7276
- var ESTATE_NEEDS_CHANT = `${PRODUCT} estate needs chant >= the release after 0.124.54, which carries @intentius/chant/ci-estate (chant#4612). Run '${PRODUCT} audit' for the ledger's rows on this chant.`;
7277
6789
  function estateSchedules(config2) {
7278
6790
  const drift3 = toChantSettings(config2).pipeline.drift;
7279
6791
  const driftOps = (drift3?.stacks ?? []).map(driftOpName);
@@ -7292,11 +6804,6 @@ async function estate(args, json, table, io) {
7292
6804
  }
7293
6805
  if (rest.length) return usage(`estate does not take ${rest.join(" ")}`, table, io);
7294
6806
  const chant = await chantEstate();
7295
- if (!chant) {
7296
- if (json) io.out(JSON.stringify({ command: "estate", ok: false, exitCode: table.failed, error: ESTATE_NEEDS_CHANT }));
7297
- else io.err(`${PRODUCT}: ${ESTATE_NEEDS_CHANT}`);
7298
- return table.failed;
7299
- }
7300
6807
  const path = flags["--file"] ?? findConfig(io.cwd);
7301
6808
  let config2;
7302
6809
  if (path) {
@@ -7312,8 +6819,7 @@ async function estate(args, json, table, io) {
7312
6819
  }
7313
6820
  }
7314
6821
  const { schedules, driftOps } = config2 ? estateSchedules(config2) : { schedules: [], driftOps: [] };
7315
- const product = await chantProduct();
7316
- const words = product ? product.assertProductWords(PRODUCT_WORDS) : PRODUCT_WORDS;
6822
+ const words = (await chantProduct()).assertProductWords(PRODUCT_WORDS);
7317
6823
  const root = path ? dirname12(path) : io.cwd;
7318
6824
  const load = await chant.readEstateLedger(root);
7319
6825
  const built = chant.buildLedgerEstate({ load, words, schedules, ...driftOps.length ? { driftOps } : {} });
@@ -7338,79 +6844,37 @@ async function estate(args, json, table, io) {
7338
6844
  }
7339
6845
 
7340
6846
  // src/notify.ts
6847
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7341
6848
  init_stages();
7342
6849
  init_words();
7343
- var RELAY_NEEDS_CHANT = `${PRODUCT} notify relay needs chant >= the release after 0.124.54, which carries chant notify relay (chant#4611).`;
7344
6850
  async function notify(args, json, table, io) {
7345
6851
  if (args[0] !== "relay") return usage(`unknown notify command '${args[0] ?? ""}'. Run '${PRODUCT} notify relay'.`, table, io);
7346
- return withChant(`${PRODUCT} notify relay`, table, io, (chant) => {
7347
- if (typeof chant.startRelayServer !== "function") {
7348
- if (json) io.out(JSON.stringify({ command: "notify relay", ok: false, exitCode: table.failed, error: RELAY_NEEDS_CHANT }));
7349
- else io.err(`${PRODUCT}: ${RELAY_NEEDS_CHANT}`);
7350
- return Promise.resolve(table.failed);
7351
- }
7352
- return chant.runChantInProcess(["notify", ...args], PRODUCT_WORDS);
7353
- });
6852
+ return withChant(`${PRODUCT} notify relay`, table, io, (chant) => chant.runChantInProcess(["notify", ...args], PRODUCT_WORDS));
7354
6853
  }
7355
6854
 
7356
- // src/cli.ts
7357
- init_policy();
7358
-
7359
6855
  // src/guard.ts
6856
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7360
6857
  init_stages();
7361
6858
  init_words();
7362
- import { chmodSync, copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
7363
6859
  import { join as join17 } from "node:path";
7364
- var GUARDED = {
7365
- pulumi: ["up", "update", "destroy"],
7366
- plunge: ["up", "update", "destroy"]
7367
- };
7368
- var VALUED_GLOBALS = /* @__PURE__ */ new Set(["-C", "--cwd", "-s", "--stack", "--color", "--config-file", "--logflow", "-v", "--verbose", "--tracing", "--profiling", "--memprofilerate"]);
7369
- var LAUNCHERS = /* @__PURE__ */ new Set(["env", "npx", "sudo", "command", "exec", "nohup", "time", "xargs"]);
7370
- function segments(line) {
7371
- return line.split(/\|\||&&|[;|\n`]|\$\(|\)/).map((seg) => (seg.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).map((t) => t.replace(/^['"]|['"]$/g, ""))).filter((t) => t.length > 0);
7372
- }
7373
- function guardVerdict(line) {
7374
- for (const words of segments(line)) {
7375
- let i = 0;
7376
- while (i < words.length && (LAUNCHERS.has(words[i]) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]) || words[i - 1] === "env" && words[i].startsWith("-"))) i++;
7377
- const bin = (words[i] ?? "").split("/").pop();
7378
- const refused = GUARDED[bin];
7379
- if (!refused) continue;
7380
- for (let j = i + 1; j < words.length; j++) {
7381
- const w = words[j];
7382
- if (w.startsWith("-")) {
7383
- if (VALUED_GLOBALS.has(w)) j++;
7384
- continue;
7385
- }
7386
- if (refused.includes(w)) {
7387
- return {
7388
- engine: bin,
7389
- subcommand: w,
7390
- message: `${PRODUCT} guard: an agent session does not run '${bin} ${w}'. Open a pull request; ${PRODUCT} previews it, and the update runs after a person approves the digest.`
7391
- };
7392
- }
7393
- break;
7394
- }
6860
+ import { agentGuard, installAgentGuard } from "@intentius/chant/ci-product";
6861
+ var engine2 = { refuse: ["up", "update", "destroy"], valuedFlags: ["-C", "--cwd", "-s", "--stack", "--color", "--config-file", "--logflow", "-v", "--verbose", "--tracing", "--profiling", "--memprofilerate"] };
6862
+ var GUARD = {
6863
+ words: { product: PRODUCT, refusal: `an agent session does not run '{what}'. Open a pull request; ${PRODUCT} previews it, and the update runs after a person approves the digest.` },
6864
+ rules: {
6865
+ engines: { pulumi: engine2, plunge: engine2 },
6866
+ product: { commands: [["up"], ["resume"], ["approve"], ["override"], ["cancel"], ["move"], ["rollback"], ["action"]], flagValues: [["--respond", "up"]] },
6867
+ wrapperExit: 1
7395
6868
  }
7396
- return void 0;
7397
- }
7398
- var GUARD_TEMPLATES = new URL("../templates/agent-guard/", import.meta.url);
7399
- var HOOK_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/plywood-guard.mjs"';
7400
- function withGuardHook(settings) {
7401
- const hooks = typeof settings.hooks === "object" && settings.hooks !== null ? settings.hooks : {};
7402
- const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
7403
- const has = JSON.stringify(pre).includes("plywood-guard.mjs");
7404
- if (!has) pre.push({ matcher: "Bash", hooks: [{ type: "command", command: HOOK_COMMAND }] });
7405
- return { ...settings, hooks: { ...hooks, PreToolUse: pre } };
7406
- }
6869
+ };
7407
6870
  async function guard(args, json, table, io) {
7408
6871
  const [sub, ...rest] = args;
7409
6872
  if (sub === "check") {
7410
6873
  const line = (rest[0] === "--" ? rest.slice(1) : rest).join(" ");
7411
6874
  if (!line.trim()) return usage("guard check takes the command line to check", table, io);
7412
- const v = guardVerdict(line);
7413
- if (json) io.out(JSON.stringify({ command: "guard check", ok: !v, exitCode: v ? table.denied : table.done, refused: v ?? null }));
6875
+ const v = agentGuard(GUARD, { repo: io.cwd }).checkCommand(line, io.cwd);
6876
+ const refused = v ? { engine: v.bin, subcommand: v.command, message: v.message } : null;
6877
+ if (json) io.out(JSON.stringify({ command: "guard check", ok: !v, exitCode: v ? table.denied : table.done, refused }));
7414
6878
  else io.out(v ? v.message : `${PRODUCT} guard: '${line}' runs`);
7415
6879
  return v ? table.denied : table.done;
7416
6880
  }
@@ -7419,34 +6883,16 @@ async function guard(args, json, table, io) {
7419
6883
  if (dirFlag.missing) return usage("--dir takes a directory", table, io);
7420
6884
  if (dirFlag.rest.length) return usage(`guard install does not take ${dirFlag.rest.join(" ")}`, table, io);
7421
6885
  const root = dirFlag.value ? join17(io.cwd, dirFlag.value) : io.cwd;
7422
- const written = [];
7423
- const hookDir = join17(root, ".claude/hooks");
7424
- mkdirSync7(hookDir, { recursive: true });
7425
- copyFileSync(new URL("plywood-guard.mjs", GUARD_TEMPLATES), join17(hookDir, "plywood-guard.mjs"));
7426
- written.push(".claude/hooks/plywood-guard.mjs");
7427
- const settingsFile = join17(root, ".claude/settings.json");
7428
- let settings = {};
7429
- if (existsSync14(settingsFile)) {
7430
- try {
7431
- settings = JSON.parse(readFileSync15(settingsFile, "utf8"));
7432
- } catch (e) {
7433
- return usage(`.claude/settings.json is not JSON (${e.message}); fix it, then run this again`, table, io);
7434
- }
7435
- }
7436
- writeFileSync8(settingsFile, JSON.stringify(withGuardHook(settings), null, 2) + "\n");
7437
- written.push(".claude/settings.json");
7438
- const binDir = join17(root, ".plywood/agent-bin");
7439
- mkdirSync7(binDir, { recursive: true });
7440
- for (const name of Object.keys(GUARDED)) {
7441
- const to = join17(binDir, name);
7442
- copyFileSync(new URL(`bin/${name}`, GUARD_TEMPLATES), to);
7443
- chmodSync(to, 493);
7444
- written.push(`.plywood/agent-bin/${name}`);
6886
+ let written;
6887
+ try {
6888
+ written = installAgentGuard({ ...GUARD, repo: root, install: "project" }).written;
6889
+ } catch (e) {
6890
+ return usage(e.message, table, io);
7445
6891
  }
7446
6892
  if (json) io.out(JSON.stringify({ command: "guard install", ok: true, exitCode: table.done, written }));
7447
6893
  else {
7448
6894
  for (const f of written) io.err(`${PRODUCT} guard: wrote ${f}`);
7449
- io.err(`${PRODUCT} guard: an agent session that runs outside Claude Code puts .plywood/agent-bin first on PATH and sets PLYWOOD_AGENT_GUARD=1`);
6895
+ io.err(`${PRODUCT} guard: an agent session that runs outside Claude Code puts .plywood/agent-bin first on PATH`);
7450
6896
  }
7451
6897
  return table.done;
7452
6898
  }
@@ -7454,6 +6900,7 @@ async function guard(args, json, table, io) {
7454
6900
  }
7455
6901
 
7456
6902
  // src/identities.ts
6903
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7457
6904
  init_jobs2();
7458
6905
  init_chant_config();
7459
6906
  init_identity();
@@ -7461,7 +6908,7 @@ init_config();
7461
6908
  init_stacks();
7462
6909
  init_stages();
7463
6910
  init_words();
7464
- import { existsSync as existsSync15, readFileSync as readFileSync16 } from "node:fs";
6911
+ import { existsSync as existsSync14, readFileSync as readFileSync15 } from "node:fs";
7465
6912
  import { dirname as dirname13, join as join18 } from "node:path";
7466
6913
  import { targetGlobMatch } from "@intentius/chant/ci-identity";
7467
6914
  function identityRows(config2, env) {
@@ -7524,28 +6971,21 @@ function configIdentities(args, json, table, io) {
7524
6971
  else for (const line of identityLines(config2, environment, rows)) io.out(line);
7525
6972
  return table.done;
7526
6973
  }
7527
- var FORGE_CHECK_MESSAGE = `${PRODUCT} forge check holds the identities next to the forge's environments, protected branches and each cloud's trust policy. The read is chant's (@intentius/chant/forge-check, chant#4608), and the installed chant lacks it; it comes with the chant release after 0.124.54. Run '${PRODUCT} config identities' for what the jobs will hold.`;
7528
6974
  var FORGES2 = ["github", "gitlab", "forgejo"];
7529
- var FORGE_CHECK_FUNCTIONS = ["forgeCheck", "forgeCheckExit", "describeForgeCheck", "forgeCheckTarget", "pipelineContract"];
7530
6975
  var forgeCheckModule;
7531
6976
  function chantForgeCheck() {
7532
6977
  forgeCheckModule ??= (async () => {
7533
- try {
7534
- const fc = await import("@intentius/chant/forge-check");
7535
- const id = await import("@intentius/chant/ci-identity");
7536
- if (!FORGE_CHECK_FUNCTIONS.every((f) => typeof fc[f] === "function") || typeof id.resolveCloudIdentity !== "function") return void 0;
7537
- return { ...fc, resolveCloudIdentity: id.resolveCloudIdentity };
7538
- } catch {
7539
- return void 0;
7540
- }
6978
+ const fc = await import("@intentius/chant/forge-check");
6979
+ const { resolveCloudIdentity } = await import("@intentius/chant/ci-identity");
6980
+ return { ...fc, resolveCloudIdentity };
7541
6981
  })();
7542
6982
  return forgeCheckModule;
7543
6983
  }
7544
6984
  function workspaceMembers(root) {
7545
6985
  const file = join18(root, "chant.workspace.json");
7546
- if (!existsSync15(file)) return [];
6986
+ if (!existsSync14(file)) return [];
7547
6987
  try {
7548
- const doc = JSON.parse(readFileSync16(file, "utf8"));
6988
+ const doc = JSON.parse(readFileSync15(file, "utf8"));
7549
6989
  return (doc.members ?? []).map((m) => m.name).filter((n) => typeof n === "string");
7550
6990
  } catch {
7551
6991
  return [];
@@ -7574,11 +7014,6 @@ function contractPipelines(fc, cloud, env, members, runner, jobs2) {
7574
7014
  async function forgeCheck(args, json, table, io) {
7575
7015
  if (args[0] !== "check") return usage(`unknown forge command '${args[0] ?? ""}'. Run '${PRODUCT} forge check'.`, table, io);
7576
7016
  const fc = await chantForgeCheck();
7577
- if (!fc) {
7578
- if (json) io.out(JSON.stringify({ command: "forge check", ok: false, exitCode: table.failed, error: FORGE_CHECK_MESSAGE }));
7579
- else io.err(`${PRODUCT}: ${FORGE_CHECK_MESSAGE}`);
7580
- return table.failed;
7581
- }
7582
7017
  const requireClouds = args.includes("--require-clouds");
7583
7018
  let rest = args.slice(1).filter((a) => a !== "--require-clouds");
7584
7019
  const flags = {};
@@ -7648,6 +7083,7 @@ async function forgeCheck(args, json, table, io) {
7648
7083
  init_control2();
7649
7084
 
7650
7085
  // src/telemetry.ts
7086
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7651
7087
  init_chant_product();
7652
7088
  init_observe();
7653
7089
  init_config();
@@ -7676,8 +7112,7 @@ async function applyTelemetry(cwd, env = process.env) {
7676
7112
  return {};
7677
7113
  }
7678
7114
  Object.assign(env, added);
7679
- const chant = await chantProduct();
7680
- if (chant && typeof chant.configureOtlp === "function") chant.configureOtlp({ prefix: PRODUCT, env });
7115
+ (await chantProduct()).configureOtlp({ prefix: PRODUCT, env });
7681
7116
  return added;
7682
7117
  }
7683
7118
 
@@ -7685,16 +7120,16 @@ async function applyTelemetry(cwd, env = process.env) {
7685
7120
  init_init();
7686
7121
  init_stages();
7687
7122
  init_tips();
7688
- init_images();
7689
7123
  init_up();
7690
7124
 
7691
7125
  // src/upgrade.ts
7126
+ init_define_PLYWOOD_IMAGE_DIGESTS();
7692
7127
  init_config();
7693
7128
  init_images();
7694
7129
  init_stages();
7695
7130
  init_words();
7696
7131
  import { execFile } from "node:child_process";
7697
- import { existsSync as existsSync16, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "node:fs";
7132
+ import { existsSync as existsSync15, readFileSync as readFileSync16, writeFileSync as writeFileSync8 } from "node:fs";
7698
7133
  import { dirname as dirname14, join as join19, relative as relative2, resolve as resolve3 } from "node:path";
7699
7134
  var PLYWOOD_PACKAGE = "@intentius/plywood";
7700
7135
  var CHANT_PACKAGE = "@intentius/chant";
@@ -7727,9 +7162,9 @@ function findPlywoodPackage(dir) {
7727
7162
  let cur = resolve3(dir);
7728
7163
  for (let depth = 0; depth < 32; depth++) {
7729
7164
  const file = join19(cur, "package.json");
7730
- if (existsSync16(file)) {
7165
+ if (existsSync15(file)) {
7731
7166
  try {
7732
- const pkg = JSON.parse(readFileSync17(file, "utf8"));
7167
+ const pkg = JSON.parse(readFileSync16(file, "utf8"));
7733
7168
  if (DEP_FIELDS.some((f) => isObject2(pkg[f]) && typeof pkg[f][PLYWOOD_PACKAGE] === "string")) return file;
7734
7169
  } catch {
7735
7170
  }
@@ -7827,10 +7262,10 @@ async function upgrade(args, json, table, io) {
7827
7262
  const release2 = await viewRelease(exec, pkgDir, io.env, opts.version);
7828
7263
  if ("error" in release2) return fail(release2.error);
7829
7264
  const chant = opts.chant ?? release2.chant;
7830
- const pkgText = readFileSync17(pkgFile, "utf8");
7265
+ const pkgText = readFileSync16(pkgFile, "utf8");
7831
7266
  const pkgMove = movePackagePins(pkgText, rel(pkgFile), release2.version, chant);
7832
7267
  const declFile = join19(configDir, "chant.workspace.json");
7833
- const declText = existsSync16(declFile) ? readFileSync17(declFile, "utf8") : void 0;
7268
+ const declText = existsSync15(declFile) ? readFileSync16(declFile, "utf8") : void 0;
7834
7269
  let declMove;
7835
7270
  if (declText !== void 0) {
7836
7271
  try {
@@ -7840,7 +7275,7 @@ async function upgrade(args, json, table, io) {
7840
7275
  }
7841
7276
  }
7842
7277
  const notes = [...pkgMove.left];
7843
- if (existsSync16(join19(configDir, "chant.workspace.jsonc"))) notes.push(`chant.workspace.jsonc is yours: move its @intentius/chant* pins to ${chant} by hand`);
7278
+ if (existsSync15(join19(configDir, "chant.workspace.jsonc"))) notes.push(`chant.workspace.jsonc is yours: move its @intentius/chant* pins to ${chant} by hand`);
7844
7279
  if (configFile) {
7845
7280
  try {
7846
7281
  const note = imageNote(loadConfig(configFile).doc, release2.version);
@@ -7869,8 +7304,8 @@ async function upgrade(args, json, table, io) {
7869
7304
  if (json) io.out(JSON.stringify({ ...envelope, ok: true, exitCode: table.done, steps }));
7870
7305
  return table.done;
7871
7306
  }
7872
- if (pkgMove.text !== pkgText) writeFileSync9(pkgFile, pkgMove.text);
7873
- if (declMove && declText !== void 0 && declMove.text !== declText) writeFileSync9(declFile, declMove.text);
7307
+ if (pkgMove.text !== pkgText) writeFileSync8(pkgFile, pkgMove.text);
7308
+ if (declMove && declText !== void 0 && declMove.text !== declText) writeFileSync8(declFile, declMove.text);
7874
7309
  const files = [...new Set(moves.map((m) => m.file))];
7875
7310
  if (!opts.install) {
7876
7311
  say(` left to run: ${steps.install.command} (in ${steps.install.cwd}), then ${steps.ci.command} (in ${steps.ci.cwd})`);
@@ -7896,7 +7331,7 @@ ${installed2.stderr.trim()}`);
7896
7331
  for (const n of notes) say(` note: ${n}`);
7897
7332
  const ok = ci2 === void 0 || ci2.code === 0;
7898
7333
  const lockFile = join19(pkgDir, "package-lock.json");
7899
- const changed = [...files, ...existsSync16(lockFile) ? [rel(lockFile)] : []];
7334
+ const changed = [...files, ...existsSync15(lockFile) ? [rel(lockFile)] : []];
7900
7335
  if (ok) say(` Open one pull request with ${changed.join(", ")} and the files ${PRODUCT} ci wrote.`);
7901
7336
  else say(` ${PRODUCT} ci failed (exit ${ci2?.code}); the pins are moved and installed, so fix plywood.yml and run '${steps.ci.command}'`);
7902
7337
  const code = ok ? table.done : ci2?.code ?? table.failed;
@@ -7905,7 +7340,7 @@ ${installed2.stderr.trim()}`);
7905
7340
  }
7906
7341
  function installedVersion(dir) {
7907
7342
  try {
7908
- const pkg = JSON.parse(readFileSync17(join19(dir, "node_modules", PLYWOOD_PACKAGE, "package.json"), "utf8"));
7343
+ const pkg = JSON.parse(readFileSync16(join19(dir, "node_modules", PLYWOOD_PACKAGE, "package.json"), "utf8"));
7909
7344
  return typeof pkg.version === "string" ? pkg.version : void 0;
7910
7345
  } catch {
7911
7346
  return void 0;
@@ -7928,7 +7363,7 @@ var execChild = (bin, args, opts) => new Promise((done) => {
7928
7363
 
7929
7364
  // src/cli.ts
7930
7365
  init_words();
7931
- var VERSION3 = true ? "0.3.1" : "0.0.0-dev";
7366
+ var VERSION3 = true ? "0.3.5" : "0.0.0-dev";
7932
7367
  var stdio = {
7933
7368
  out: (s) => process.stdout.write(s + "\n"),
7934
7369
  err: (s) => process.stderr.write(s + "\n")
@@ -8019,7 +7454,7 @@ function helpText() {
8019
7454
  ].join("\n");
8020
7455
  }
8021
7456
  async function main(argv, io = stdio) {
8022
- const { table, source } = await exitCodes();
7457
+ const table = EXIT;
8023
7458
  const json = argv.includes("--json");
8024
7459
  const args = argv.filter((a) => a !== "--json");
8025
7460
  const verb = args[0];
@@ -8032,11 +7467,9 @@ async function main(argv, io = stdio) {
8032
7467
  return table.done;
8033
7468
  }
8034
7469
  if (verb === "--version" || verb === "-v") {
8035
- emit({ command: "version", ok: true, exitCode: table.done, version: VERSION3, exitCodes: source }, VERSION3);
7470
+ emit({ command: "version", ok: true, exitCode: table.done, version: VERSION3, exitCodes: "chant" }, VERSION3);
8036
7471
  return table.done;
8037
7472
  }
8038
- await useChantInstallLines();
8039
- await useChantPolicySource();
8040
7473
  const stageIo = { out: io.out, err: io.err, cwd: process.cwd(), env: process.env };
8041
7474
  if (verb !== "config" && verb !== "init" && verb !== "tips" && verb !== "upgrade") await applyTelemetry(stageIo.cwd, stageIo.env);
8042
7475
  if (verb === "audit") return audit2(args.slice(1), json, table, stageIo);