@intentius/plywood 0.0.0 → 0.2.0
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 +1696 -1442
- package/dist/plywood.mjs.map +4 -4
- package/package.json +6 -8
- package/plywood.schema.json +28 -0
package/dist/plywood.mjs
CHANGED
|
@@ -380,7 +380,7 @@ var init_chant_keys = __esm({
|
|
|
380
380
|
|
|
381
381
|
// src/words.ts
|
|
382
382
|
function wordsForJobs(jobs2) {
|
|
383
|
-
const commands = Object.fromEntries(Object.entries(PRODUCT_WORDS.commands).filter(([, c]) => jobs2.includes(c)));
|
|
383
|
+
const commands = Object.fromEntries(Object.entries({ ...PRODUCT_WORDS.commands, ...ACTION_COMMAND }).filter(([, c]) => jobs2.includes(c)));
|
|
384
384
|
return { ...PRODUCT_WORDS, commands };
|
|
385
385
|
}
|
|
386
386
|
function driftOpName(stack) {
|
|
@@ -417,7 +417,7 @@ function driftIssueBody(input) {
|
|
|
417
417
|
function driftClearedComment(stack, observedAt2) {
|
|
418
418
|
return `plywood's drift check at ${observedAt2} found ${stack} matching its state, so this issue is closed.`;
|
|
419
419
|
}
|
|
420
|
-
var PRODUCT, TAGLINE, OP_NAME, PR_GATE, VERBS, BIN, NOTIFY_WORDS, PRODUCT_WORDS, POLICY_RULE_FORMS, CLOUD_NAMES, NO_IDENTITY, STACK_OP_WORDS, DRIFT_OP_PREFIX, LOCK_GRAIN_WORDS, PLUNGE_TIMING_ATTRIBUTES;
|
|
420
|
+
var PRODUCT, TAGLINE, OP_NAME, PR_GATE, VERBS, BIN, NOTIFY_WORDS, PRODUCT_WORDS, ACTION_COMMAND, POLICY_RULE_FORMS, CLOUD_NAMES, NO_IDENTITY, TEMPLATE_REQUEST_WORDS, STACK_OP_WORDS, DRIFT_OP_PREFIX, LOCK_GRAIN_WORDS, PLUNGE_TIMING_ATTRIBUTES;
|
|
421
421
|
var init_words = __esm({
|
|
422
422
|
"src/words.ts"() {
|
|
423
423
|
"use strict";
|
|
@@ -450,18 +450,27 @@ var init_words = __esm({
|
|
|
450
450
|
bin: BIN,
|
|
451
451
|
notify: NOTIFY_WORDS
|
|
452
452
|
});
|
|
453
|
+
ACTION_COMMAND = Object.freeze({ action: "action" });
|
|
453
454
|
POLICY_RULE_FORMS = "a CrossGuard rule is crossguard.<pack>.<policy>, a Rego rule <package>.<rule>";
|
|
454
455
|
CLOUD_NAMES = { aws: "AWS", gcp: "GCP", azure: "Azure", pulumi: "Pulumi Cloud" };
|
|
455
456
|
NO_IDENTITY = "the job's own credentials (no OIDC trade)";
|
|
457
|
+
TEMPLATE_REQUEST_WORDS = {
|
|
458
|
+
tool: "request_from_template",
|
|
459
|
+
refusal: "Pulumi templates are not available yet: request_from_template opens a pull request for an app from a template once plywood has a template registry and catalog, and the plywood request command that opens the same pull request for a person. Until then it refuses every call and opens nothing. To change a stack now, open a pull request with the change."
|
|
460
|
+
};
|
|
456
461
|
STACK_OP_WORDS = {
|
|
457
462
|
usage: {
|
|
458
463
|
export: `${PRODUCT} export <project/stack> --out <file>`,
|
|
459
464
|
cancel: `${PRODUCT} cancel <project/stack> [--lock <id>]`,
|
|
460
465
|
move: `${PRODUCT} move <urn>... --from <project/stack> --to <project/stack>, or ${PRODUCT} move <project/stack> --onto plunge`,
|
|
461
|
-
rollback: `${PRODUCT} rollback <project/stack> [--digest <sha256:...>], or ${PRODUCT} rollback --release <name> --env <env
|
|
466
|
+
rollback: `${PRODUCT} rollback <project/stack> [--digest <sha256:...>], or ${PRODUCT} rollback --release <name> --env <env>`,
|
|
467
|
+
action: `${PRODUCT} action <project/stack> <name> [--anew], or ${PRODUCT} action --from-event`
|
|
462
468
|
},
|
|
463
469
|
waiting: (verb, approve2) => `The ${verb} waits for a second person's approval, and your own does not count. They run '${approve2}', then you run this command again to carry it out.`,
|
|
464
|
-
spent: (verb) => `This ${verb} ran once already under its approval, and a request runs once. Run the command again to ask for a new one.`,
|
|
470
|
+
spent: (verb) => verb === "action" ? "This action ran once already under its approval, and a request runs once. Run the command again with --anew to ask for a new run." : `This ${verb} ran once already under its approval, and a request runs once. Run the command again to ask for a new one.`,
|
|
471
|
+
acted: (stack, name, r) => `Ran action ${name} on ${stack}: ${r.engine} replaced ${r.urn}.${r.tail ? `
|
|
472
|
+
${r.tail}` : ""}`,
|
|
473
|
+
noAction: (stack, name, declared) => declared.length ? `${stack} has no action ${name} in plywood.yml; its actions are ${declared.join(", ")}` : `${stack} declares no actions in plywood.yml; add one under stacks.${stack}.actions with a name and the URN of a command resource`,
|
|
465
474
|
exported: (stack, r) => `Exported ${stack} to ${r.file} (${r.resources} resources, ${r.bytes} bytes, sha256 ${r.sha256}). The file is readable by you alone; the ledger keeps its digest and size, never its content.`,
|
|
466
475
|
cancelled: (stack, lock, checked) => `Released lock ${lock} on ${stack}.${checked ? "" : " The backend's lock could not be read, so it was not checked against the approved one first."}`,
|
|
467
476
|
moved: (from, to, r) => `Moved ${r.moved.length} resource${r.moved.length === 1 ? "" : "s"} from ${from} to ${to} on ${r.engine}. Both stacks preview no change.`,
|
|
@@ -578,10 +587,13 @@ var init_control = __esm({
|
|
|
578
587
|
var version, dependencies;
|
|
579
588
|
var init_package = __esm({
|
|
580
589
|
"package.json"() {
|
|
581
|
-
version = "0.
|
|
590
|
+
version = "0.2.0";
|
|
582
591
|
dependencies = {
|
|
583
|
-
"@intentius/chant": "0.124.
|
|
584
|
-
"@intentius/chant-lexicon-
|
|
592
|
+
"@intentius/chant": "0.124.58",
|
|
593
|
+
"@intentius/chant-lexicon-forgejo": "0.124.58",
|
|
594
|
+
"@intentius/chant-lexicon-github": "0.124.58",
|
|
595
|
+
"@intentius/chant-lexicon-gitlab": "0.124.58",
|
|
596
|
+
"@intentius/chant-lexicon-pulumi": "0.124.58"
|
|
585
597
|
};
|
|
586
598
|
}
|
|
587
599
|
});
|
|
@@ -590,7 +602,10 @@ var init_package = __esm({
|
|
|
590
602
|
var image_digests_default;
|
|
591
603
|
var init_image_digests = __esm({
|
|
592
604
|
"src/image-digests.json"() {
|
|
593
|
-
image_digests_default = {
|
|
605
|
+
image_digests_default = {
|
|
606
|
+
"ghcr.io/intentius/plywood-pulumi:0.2.0-pulumi3.272.0": "sha256:ea268f1dd2fa15f80ddcedd5c3498d24b79cc2b8a590177c5499e2db3f686f18",
|
|
607
|
+
"ghcr.io/intentius/plywood-plunge:0.2.0-plunge0.1.0": "sha256:6a862ad23e39081a32fe8b2331b095ed2de44e527f2719416201b0affa46a1eb"
|
|
608
|
+
};
|
|
594
609
|
}
|
|
595
610
|
});
|
|
596
611
|
|
|
@@ -917,12 +932,58 @@ var init_images2 = __esm({
|
|
|
917
932
|
});
|
|
918
933
|
|
|
919
934
|
// src/config/stacks.ts
|
|
935
|
+
function parseUrn(urn) {
|
|
936
|
+
if (!urn.startsWith("urn:pulumi:")) return void 0;
|
|
937
|
+
const parts = urn.slice("urn:pulumi:".length).split("::");
|
|
938
|
+
if (parts.length < 4 || parts.slice(0, 4).some((p) => p === "")) return void 0;
|
|
939
|
+
return { stack: parts[0], project: parts[1], type: parts[2].split("$").at(-1) };
|
|
940
|
+
}
|
|
941
|
+
function actionUrnProblem(stackKey, urn) {
|
|
942
|
+
const parsed = parseUrn(urn);
|
|
943
|
+
if (!parsed) return `is not a resource URN (urn:pulumi:<stack>::<project>::<type>::<name>)`;
|
|
944
|
+
const stack = stackKey.split("/").at(-1);
|
|
945
|
+
if (parsed.stack !== stack) return `names stack ${parsed.stack}, and the action is under ${stackKey}`;
|
|
946
|
+
if (!parsed.type.startsWith(ACTION_TYPE_PREFIX)) {
|
|
947
|
+
return `is a ${parsed.type}, and an action is a command resource (command:local:Command or command:remote:Command), whose replacement runs it`;
|
|
948
|
+
}
|
|
949
|
+
return void 0;
|
|
950
|
+
}
|
|
951
|
+
function actionsOf(doc, key) {
|
|
952
|
+
const a = stacksOf(doc)[key]?.actions;
|
|
953
|
+
return Array.isArray(a) ? a : [];
|
|
954
|
+
}
|
|
955
|
+
function hasActions(doc) {
|
|
956
|
+
return Object.keys(stacksOf(doc)).some((k) => actionsOf(doc, k).length > 0);
|
|
957
|
+
}
|
|
920
958
|
function wavesMappingOf(doc) {
|
|
921
959
|
return isObject(doc.waves) ? doc.waves : {};
|
|
922
960
|
}
|
|
923
961
|
function stacksOf(doc) {
|
|
924
962
|
return isObject(doc.stacks) ? doc.stacks : {};
|
|
925
963
|
}
|
|
964
|
+
function actionProblems(stackKey, actions, key) {
|
|
965
|
+
if (!Array.isArray(actions)) return [error(key, "must be a list of actions, each with a name and a urn")];
|
|
966
|
+
const out = [];
|
|
967
|
+
const seen = /* @__PURE__ */ new Map();
|
|
968
|
+
actions.forEach((a, i) => {
|
|
969
|
+
const at = `${key}[${i}]`;
|
|
970
|
+
if (!isObject(a)) {
|
|
971
|
+
out.push(error(at, "must be a mapping with a name and a urn"));
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
out.push(...onlyKeys(a, ACTION_KEYS, at));
|
|
975
|
+
if (typeof a.name !== "string" || !ACTION_NAME.test(a.name)) out.push(error(`${at}.name`, "must be a name of letters, digits, _, . and -"));
|
|
976
|
+
else if (seen.has(a.name)) out.push(error(`${at}.name`, `${a.name} is the name of actions[${seen.get(a.name)}] too; each action's name is unique in its stack`));
|
|
977
|
+
else seen.set(a.name, i);
|
|
978
|
+
if (typeof a.urn !== "string") out.push(error(`${at}.urn`, "must be the URN of a command resource in the stack"));
|
|
979
|
+
else {
|
|
980
|
+
const problem = actionUrnProblem(stackKey, a.urn);
|
|
981
|
+
if (problem) out.push(error(`${at}.urn`, `${a.urn} ${problem}`));
|
|
982
|
+
}
|
|
983
|
+
if (a.description !== void 0 && typeof a.description !== "string") out.push(error(`${at}.description`, "must be a string"));
|
|
984
|
+
});
|
|
985
|
+
return out;
|
|
986
|
+
}
|
|
926
987
|
function wavesMappingProblems(w, listed) {
|
|
927
988
|
const out = [...onlyKeys(w, WAVES_KEYS, "waves")];
|
|
928
989
|
const known = (s, key) => {
|
|
@@ -976,15 +1037,18 @@ function afterCycle(after) {
|
|
|
976
1037
|
}
|
|
977
1038
|
return void 0;
|
|
978
1039
|
}
|
|
979
|
-
var STACK_KEYS, WAVES_KEYS, STACK_NAME, stacks;
|
|
1040
|
+
var STACK_KEYS, ACTION_KEYS, WAVES_KEYS, STACK_NAME, ACTION_NAME, ACTION_TYPE_PREFIX, stacks;
|
|
980
1041
|
var init_stacks = __esm({
|
|
981
1042
|
"src/config/stacks.ts"() {
|
|
982
1043
|
"use strict";
|
|
983
1044
|
init_approval();
|
|
984
1045
|
init_types();
|
|
985
|
-
STACK_KEYS = ["dir", "paths", "gate", "backend"];
|
|
1046
|
+
STACK_KEYS = ["dir", "paths", "gate", "backend", "actions"];
|
|
1047
|
+
ACTION_KEYS = ["name", "urn", "description"];
|
|
986
1048
|
WAVES_KEYS = ["canary", "after"];
|
|
987
1049
|
STACK_NAME = /^[^/\s]+\/[^/\s]+$/;
|
|
1050
|
+
ACTION_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,99}$/;
|
|
1051
|
+
ACTION_TYPE_PREFIX = "command:";
|
|
988
1052
|
stacks = {
|
|
989
1053
|
name: "stacks",
|
|
990
1054
|
keys: ["stacks", "waves"],
|
|
@@ -1000,7 +1064,21 @@ var init_stacks = __esm({
|
|
|
1000
1064
|
dir: { type: "string", description: "The directory holding the stack's Pulumi.yaml." },
|
|
1001
1065
|
paths: { type: "array", items: { type: "string" }, description: "Paths whose change previews the stack, beside its directory." },
|
|
1002
1066
|
gate: { type: "string", enum: ["always", "on-destructive", "never"], description: "The stack's gate, in place of the top-level gate." },
|
|
1003
|
-
backend: BACKEND_SCHEMA
|
|
1067
|
+
backend: BACKEND_SCHEMA,
|
|
1068
|
+
actions: {
|
|
1069
|
+
type: "array",
|
|
1070
|
+
description: "The stack's day-2 actions, each run by plywood action <stack> <name> under a second person's approval.",
|
|
1071
|
+
items: {
|
|
1072
|
+
type: "object",
|
|
1073
|
+
additionalProperties: false,
|
|
1074
|
+
required: ["name", "urn"],
|
|
1075
|
+
properties: {
|
|
1076
|
+
name: { type: "string", pattern: ACTION_NAME.source, description: "The action's name, unique in the stack." },
|
|
1077
|
+
urn: { type: "string", pattern: "^urn:pulumi:", description: "The URN of a command:local:Command or command:remote:Command resource in the stack, replaced to run the action." },
|
|
1078
|
+
description: { type: "string", description: "What the action does, for people and agents." }
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1004
1082
|
}
|
|
1005
1083
|
}
|
|
1006
1084
|
},
|
|
@@ -1038,6 +1116,7 @@ var init_stacks = __esm({
|
|
|
1038
1116
|
if (s.dir !== void 0 && typeof s.dir !== "string") out.push(error(`${key}.dir`, "must be a string"));
|
|
1039
1117
|
if (s.backend !== void 0 && typeof s.backend !== "string") out.push(error(`${key}.backend`, "must be a URL string"));
|
|
1040
1118
|
out.push(...stringList(s.paths, `${key}.paths`), ...oneOf(s.gate, GATES, `${key}.gate`));
|
|
1119
|
+
if (s.actions !== void 0) out.push(...actionProblems(name, s.actions, `${key}.actions`));
|
|
1041
1120
|
}
|
|
1042
1121
|
if (isObject(doc.waves)) {
|
|
1043
1122
|
out.push(...wavesMappingProblems(doc.waves, isObject(doc.stacks) ? listed : void 0));
|
|
@@ -2348,227 +2427,6 @@ var init_config_check = __esm({
|
|
|
2348
2427
|
}
|
|
2349
2428
|
});
|
|
2350
2429
|
|
|
2351
|
-
// src/init/detect.ts
|
|
2352
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
2353
|
-
import { basename, dirname as dirname2, join as join2, relative, sep } from "node:path";
|
|
2354
|
-
import { parseYAML as parseYAML2 } from "@intentius/chant/yaml";
|
|
2355
|
-
function repoRoot(dir) {
|
|
2356
|
-
let cur = dir;
|
|
2357
|
-
for (let i = 0; i < 64; i++) {
|
|
2358
|
-
if (existsSync2(join2(cur, ".git"))) return cur;
|
|
2359
|
-
const parent = dirname2(cur);
|
|
2360
|
-
if (parent === cur) return dir;
|
|
2361
|
-
cur = parent;
|
|
2362
|
-
}
|
|
2363
|
-
return dir;
|
|
2364
|
-
}
|
|
2365
|
-
function posixRel(root, path) {
|
|
2366
|
-
const rel = relative(root, path);
|
|
2367
|
-
return rel === "" ? "." : rel.split(sep).join("/");
|
|
2368
|
-
}
|
|
2369
|
-
function readYamlFile(file) {
|
|
2370
|
-
try {
|
|
2371
|
-
const doc = parseYAML2(readFileSync2(file, "utf8"));
|
|
2372
|
-
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : void 0;
|
|
2373
|
-
} catch {
|
|
2374
|
-
return void 0;
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
function findPulumiProjects(root) {
|
|
2378
|
-
const out = [];
|
|
2379
|
-
const walk = (dir, depth) => {
|
|
2380
|
-
let entries;
|
|
2381
|
-
try {
|
|
2382
|
-
entries = readdirSync(dir).sort();
|
|
2383
|
-
} catch {
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
const file = ["Pulumi.yaml", "Pulumi.yml"].find((n) => entries.includes(n));
|
|
2387
|
-
if (file) {
|
|
2388
|
-
const project = readProject(root, dir, join2(dir, file));
|
|
2389
|
-
if (project) out.push(project);
|
|
2390
|
-
}
|
|
2391
|
-
if (depth >= MAX_DEPTH) return;
|
|
2392
|
-
for (const name of entries) {
|
|
2393
|
-
if (SKIP.has(name) || name.startsWith(".")) continue;
|
|
2394
|
-
const path = join2(dir, name);
|
|
2395
|
-
try {
|
|
2396
|
-
if (statSync(path).isDirectory()) walk(path, depth + 1);
|
|
2397
|
-
} catch {
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
};
|
|
2401
|
-
walk(root, 0);
|
|
2402
|
-
return out;
|
|
2403
|
-
}
|
|
2404
|
-
function readProject(root, dir, file) {
|
|
2405
|
-
const doc = readYamlFile(file);
|
|
2406
|
-
if (!doc || typeof doc.name !== "string" || doc.name === "") return void 0;
|
|
2407
|
-
const runtime = typeof doc.runtime === "string" ? doc.runtime : doc.runtime?.name;
|
|
2408
|
-
const backend = doc.backend;
|
|
2409
|
-
const configDir = typeof doc.stackConfigDir === "string" ? join2(dir, doc.stackConfigDir) : dir;
|
|
2410
|
-
return {
|
|
2411
|
-
name: doc.name,
|
|
2412
|
-
dir: posixRel(root, dir),
|
|
2413
|
-
...typeof runtime === "string" ? { runtime } : {},
|
|
2414
|
-
...backend && typeof backend.url === "string" ? { backendUrl: backend.url } : {},
|
|
2415
|
-
stacks: stackNames(configDir)
|
|
2416
|
-
};
|
|
2417
|
-
}
|
|
2418
|
-
function stackNames(configDir) {
|
|
2419
|
-
let entries;
|
|
2420
|
-
try {
|
|
2421
|
-
entries = readdirSync(configDir);
|
|
2422
|
-
} catch {
|
|
2423
|
-
return [];
|
|
2424
|
-
}
|
|
2425
|
-
const names = entries.map((n) => /^Pulumi\.(.+)\.ya?ml$/.exec(n)?.[1]).filter((s) => s !== void 0 && s !== "");
|
|
2426
|
-
return [...new Set(names)].sort(byEnvironment);
|
|
2427
|
-
}
|
|
2428
|
-
function environmentOf(stack) {
|
|
2429
|
-
const last = stack.split("/").pop().toLowerCase();
|
|
2430
|
-
const head = last.split(/[-_.]/)[0];
|
|
2431
|
-
return ENV_ORDER.includes(head) ? head : last;
|
|
2432
|
-
}
|
|
2433
|
-
function rank(stack) {
|
|
2434
|
-
const env = environmentOf(stack);
|
|
2435
|
-
const at = ENV_ORDER.indexOf(env);
|
|
2436
|
-
return at >= 0 ? at : PROD_RANK - 0.5;
|
|
2437
|
-
}
|
|
2438
|
-
function byEnvironment(a, b) {
|
|
2439
|
-
return rank(a) - rank(b) || a.localeCompare(b);
|
|
2440
|
-
}
|
|
2441
|
-
function stacksOf2(projects) {
|
|
2442
|
-
const names = /* @__PURE__ */ new Map();
|
|
2443
|
-
for (const p of projects) names.set(p.name, (names.get(p.name) ?? 0) + 1);
|
|
2444
|
-
const out = [];
|
|
2445
|
-
for (const p of projects) {
|
|
2446
|
-
const project = (names.get(p.name) ?? 0) > 1 ? `${p.name}-${slug(p.dir)}` : p.name;
|
|
2447
|
-
for (const stack of p.stacks) out.push({ key: `${project}/${stack}`, project: p.name, stack, dir: p.dir });
|
|
2448
|
-
}
|
|
2449
|
-
return out.sort((x, y) => byEnvironment(x.stack, y.stack) || x.key.localeCompare(y.key));
|
|
2450
|
-
}
|
|
2451
|
-
function slug(s) {
|
|
2452
|
-
const out = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2453
|
-
return out === "" ? "root" : out;
|
|
2454
|
-
}
|
|
2455
|
-
function originUrl(root) {
|
|
2456
|
-
const config2 = gitFile(root, "config");
|
|
2457
|
-
if (!config2) return void 0;
|
|
2458
|
-
let inOrigin = false;
|
|
2459
|
-
for (const raw of config2.split(/\r?\n/)) {
|
|
2460
|
-
const line = raw.trim();
|
|
2461
|
-
if (line.startsWith("[")) {
|
|
2462
|
-
inOrigin = /^\[remote\s+"origin"\]$/.test(line);
|
|
2463
|
-
continue;
|
|
2464
|
-
}
|
|
2465
|
-
const m = inOrigin ? /^url\s*=\s*(.+)$/.exec(line) : null;
|
|
2466
|
-
if (m) return m[1].trim();
|
|
2467
|
-
}
|
|
2468
|
-
return void 0;
|
|
2469
|
-
}
|
|
2470
|
-
function gitFile(root, name) {
|
|
2471
|
-
const dotGit = join2(root, ".git");
|
|
2472
|
-
let gitDir = dotGit;
|
|
2473
|
-
try {
|
|
2474
|
-
if (statSync(dotGit).isFile()) {
|
|
2475
|
-
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync2(dotGit, "utf8"));
|
|
2476
|
-
if (!m) return void 0;
|
|
2477
|
-
gitDir = join2(root, m[1].trim());
|
|
2478
|
-
const common = join2(gitDir, "commondir");
|
|
2479
|
-
if (existsSync2(common) && !existsSync2(join2(gitDir, name))) gitDir = join2(gitDir, readFileSync2(common, "utf8").trim());
|
|
2480
|
-
}
|
|
2481
|
-
const path = join2(gitDir, name);
|
|
2482
|
-
return existsSync2(path) ? readFileSync2(path, "utf8") : void 0;
|
|
2483
|
-
} catch {
|
|
2484
|
-
return void 0;
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
function remoteHost(url) {
|
|
2488
|
-
const scp = /^[^@/\s]+@([^:/\s]+):/.exec(url);
|
|
2489
|
-
if (scp) return scp[1].toLowerCase();
|
|
2490
|
-
try {
|
|
2491
|
-
return new URL(url).hostname.toLowerCase() || void 0;
|
|
2492
|
-
} catch {
|
|
2493
|
-
return void 0;
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
function forgeOfRemote(url) {
|
|
2497
|
-
const host = remoteHost(url);
|
|
2498
|
-
if (!host) return void 0;
|
|
2499
|
-
if (host === "github.com" || host.endsWith(".github.com") || host.startsWith("github.")) return { kind: "github", because: `origin is on ${host}` };
|
|
2500
|
-
if (host === "gitlab.com") return { kind: "gitlab", because: `origin is on ${host}` };
|
|
2501
|
-
if (host === "codeberg.org") return { kind: "forgejo", because: `origin is on ${host}` };
|
|
2502
|
-
if (/(^|[.-])gitlab([.-]|$)/.test(host)) return { kind: "gitlab", because: `origin is on ${host}`, url: `https://${host}` };
|
|
2503
|
-
if (/(^|[.-])(forgejo|gitea)([.-]|$)/.test(host)) return { kind: "forgejo", because: `origin is on ${host}`, url: `https://${host}` };
|
|
2504
|
-
return void 0;
|
|
2505
|
-
}
|
|
2506
|
-
function detectForge(root) {
|
|
2507
|
-
const url = originUrl(root);
|
|
2508
|
-
const byRemote = url ? forgeOfRemote(url) : void 0;
|
|
2509
|
-
if (byRemote) return byRemote;
|
|
2510
|
-
if (existsSync2(join2(root, ".forgejo"))) return { kind: "forgejo", because: "the repository has .forgejo/" };
|
|
2511
|
-
if (existsSync2(join2(root, ".gitlab-ci.yml"))) return { kind: "gitlab", because: "the repository has .gitlab-ci.yml" };
|
|
2512
|
-
if (existsSync2(join2(root, ".github", "workflows"))) return { kind: "github", because: "the repository has .github/workflows/" };
|
|
2513
|
-
return void 0;
|
|
2514
|
-
}
|
|
2515
|
-
function defaultBranch(root) {
|
|
2516
|
-
const head = gitFile(root, "refs/remotes/origin/HEAD");
|
|
2517
|
-
const remote = head && /ref:\s*refs\/remotes\/origin\/(.+)/.exec(head)?.[1];
|
|
2518
|
-
if (remote) return remote.trim();
|
|
2519
|
-
const local = gitFile(root, "HEAD");
|
|
2520
|
-
const branch = local && /ref:\s*refs\/heads\/(.+)/.exec(local)?.[1];
|
|
2521
|
-
return branch ? branch.trim() : "main";
|
|
2522
|
-
}
|
|
2523
|
-
function findPulumiActions(root) {
|
|
2524
|
-
const out = [];
|
|
2525
|
-
for (const d of WORKFLOW_DIRS) {
|
|
2526
|
-
let names;
|
|
2527
|
-
try {
|
|
2528
|
-
names = readdirSync(join2(root, d)).sort();
|
|
2529
|
-
} catch {
|
|
2530
|
-
continue;
|
|
2531
|
-
}
|
|
2532
|
-
for (const n of names) {
|
|
2533
|
-
if (!/\.ya?ml$/.test(n)) continue;
|
|
2534
|
-
try {
|
|
2535
|
-
if (/uses:\s*["']?pulumi\/actions@/.test(readFileSync2(join2(root, d, n), "utf8"))) out.push(`${d}/${n}`);
|
|
2536
|
-
} catch {
|
|
2537
|
-
}
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
return out;
|
|
2541
|
-
}
|
|
2542
|
-
function detect(cwd) {
|
|
2543
|
-
const root = repoRoot(cwd);
|
|
2544
|
-
const projects = findPulumiProjects(root);
|
|
2545
|
-
const forge = detectForge(root);
|
|
2546
|
-
const preCommit = [".pre-commit-config.yaml", ".pre-commit-config.yml"].find((n) => existsSync2(join2(root, n)));
|
|
2547
|
-
return {
|
|
2548
|
-
root,
|
|
2549
|
-
projects,
|
|
2550
|
-
stacks: stacksOf2(projects),
|
|
2551
|
-
...forge ? { forge } : {},
|
|
2552
|
-
defaultBranch: defaultBranch(root),
|
|
2553
|
-
pulumiActions: findPulumiActions(root),
|
|
2554
|
-
...preCommit ? { preCommit } : {}
|
|
2555
|
-
};
|
|
2556
|
-
}
|
|
2557
|
-
function repoName(root) {
|
|
2558
|
-
return slug(basename(root)).slice(0, 40).replace(/-+$/, "") || "plywood";
|
|
2559
|
-
}
|
|
2560
|
-
var SKIP, MAX_DEPTH, ENV_ORDER, PROD_RANK, WORKFLOW_DIRS;
|
|
2561
|
-
var init_detect = __esm({
|
|
2562
|
-
"src/init/detect.ts"() {
|
|
2563
|
-
"use strict";
|
|
2564
|
-
SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor", "venv", ".venv", "__pycache__", "target", "bin", "obj"]);
|
|
2565
|
-
MAX_DEPTH = 8;
|
|
2566
|
-
ENV_ORDER = ["local", "dev", "development", "sandbox", "test", "testing", "qa", "ci", "stage", "staging", "preprod", "uat", "prod", "production"];
|
|
2567
|
-
PROD_RANK = ENV_ORDER.indexOf("prod");
|
|
2568
|
-
WORKFLOW_DIRS = [".github/workflows", ".forgejo/workflows", ".gitea/workflows"];
|
|
2569
|
-
}
|
|
2570
|
-
});
|
|
2571
|
-
|
|
2572
2430
|
// src/chant-product.ts
|
|
2573
2431
|
function hasChant4620(chant) {
|
|
2574
2432
|
return typeof chant.prCommentPreviewJob === "function" && typeof chant.PR_PREVIEW_FLOOR === "string";
|
|
@@ -2600,44 +2458,11 @@ var init_chant_product = __esm({
|
|
|
2600
2458
|
"src/chant-product.ts"() {
|
|
2601
2459
|
"use strict";
|
|
2602
2460
|
REQUIRED2 = ["assertProductWords", "productPipelineJobs", "approveLine", "runChantInProcess"];
|
|
2603
|
-
BUILT_CHANT = true ? "0.124.
|
|
2461
|
+
BUILT_CHANT = true ? "0.124.58" : "unknown";
|
|
2604
2462
|
}
|
|
2605
2463
|
});
|
|
2606
2464
|
|
|
2607
2465
|
// src/chant-next.ts
|
|
2608
|
-
async function acceptsContentsWrite(load) {
|
|
2609
|
-
try {
|
|
2610
|
-
const m = await load();
|
|
2611
|
-
if (typeof m.assertScheduledOpCloud !== "function") return false;
|
|
2612
|
-
m.assertScheduledOpCloud(structuredClone(CONTENTS_WRITE_PROBE));
|
|
2613
|
-
return true;
|
|
2614
|
-
} catch {
|
|
2615
|
-
return false;
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
async function has(load, name, type = "function") {
|
|
2619
|
-
try {
|
|
2620
|
-
const m = await load();
|
|
2621
|
-
return typeof m[name] === type;
|
|
2622
|
-
} catch {
|
|
2623
|
-
return false;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
function chantNext() {
|
|
2627
|
-
detected ??= (async () => ({
|
|
2628
|
-
// @ts-ignore: the subpath is missing from chant 0.124.55 (chant#4624).
|
|
2629
|
-
checkJob: await has(() => import("@intentius/chant/ci-job-guard"), "checkJob"),
|
|
2630
|
-
planFlags: await has(() => import("@intentius/chant/components/capability"), "PLAN_FLAG_ARGS", "object"),
|
|
2631
|
-
staleNotes: await has(() => import("@intentius/chant/pr-post"), "markStaleNotes"),
|
|
2632
|
-
// @ts-ignore: the subpath is missing from chant 0.124.55 (chant#4631).
|
|
2633
|
-
scheduledOpCloud: await has(() => import("@intentius/chant/scheduled-op-cloud"), "assertScheduledOpCloud"),
|
|
2634
|
-
// @ts-ignore: the subpath is missing from chant 0.124.55 (chant#4631).
|
|
2635
|
-
updateContentsWrite: await acceptsContentsWrite(() => import("@intentius/chant/scheduled-op-cloud")),
|
|
2636
|
-
// @ts-ignore: the subpath is missing from chant 0.124.55 (chant#4636).
|
|
2637
|
-
applyMerge: await has(() => import("@intentius/chant/pr-merge"), "mergeAfterApply")
|
|
2638
|
-
}))();
|
|
2639
|
-
return detected;
|
|
2640
|
-
}
|
|
2641
2466
|
function hasChantRespond(chant) {
|
|
2642
2467
|
return typeof chant?.CI_RESPOND_FLOOR === "string";
|
|
2643
2468
|
}
|
|
@@ -2651,26 +2476,13 @@ function hasChantPullRequestApprovalStatus(chant) {
|
|
|
2651
2476
|
return typeof chant?.APPROVAL_STATUS_PULL_REQUEST_FLOOR === "string";
|
|
2652
2477
|
}
|
|
2653
2478
|
function chantAudit() {
|
|
2654
|
-
audit ??= (
|
|
2655
|
-
try {
|
|
2656
|
-
const m = await import("@intentius/chant/ci-audit");
|
|
2657
|
-
return typeof m.attributeDrift === "function" && typeof m.routeAttributions === "function" && typeof m.parseAuditLogSetting === "function" ? m : void 0;
|
|
2658
|
-
} catch {
|
|
2659
|
-
return void 0;
|
|
2660
|
-
}
|
|
2661
|
-
})();
|
|
2479
|
+
audit ??= import("@intentius/chant/ci-audit").then((m) => m);
|
|
2662
2480
|
return audit;
|
|
2663
2481
|
}
|
|
2664
|
-
var
|
|
2482
|
+
var audit;
|
|
2665
2483
|
var init_chant_next = __esm({
|
|
2666
2484
|
"src/chant-next.ts"() {
|
|
2667
2485
|
"use strict";
|
|
2668
|
-
CONTENTS_WRITE_PROBE = {
|
|
2669
|
-
name: "plywood-probe",
|
|
2670
|
-
trigger: { kind: "cron", schedule: "0 6 * * *" },
|
|
2671
|
-
findingMode: "issue",
|
|
2672
|
-
update: { command: "true", permissions: { contents: "write" } }
|
|
2673
|
-
};
|
|
2674
2486
|
}
|
|
2675
2487
|
});
|
|
2676
2488
|
|
|
@@ -2820,18 +2632,18 @@ var init_note_control = __esm({
|
|
|
2820
2632
|
});
|
|
2821
2633
|
|
|
2822
2634
|
// src/init/workflow.ts
|
|
2823
|
-
import { existsSync as
|
|
2824
|
-
import { dirname as
|
|
2635
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
2636
|
+
import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
|
|
2825
2637
|
async function writeWorkflow(chant, input) {
|
|
2826
|
-
const file = isAbsolute(input.output) ? input.output :
|
|
2827
|
-
const previous =
|
|
2828
|
-
mkdirSync(
|
|
2638
|
+
const file = isAbsolute(input.output) ? input.output : join2(input.root, input.output);
|
|
2639
|
+
const previous = existsSync2(file) ? readFileSync2(file, "utf8") : void 0;
|
|
2640
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
2829
2641
|
const code = await chant.runChantInProcess([...input.argv, "--output", file], input.words);
|
|
2830
|
-
if (code !== 0 || !
|
|
2642
|
+
if (code !== 0 || !existsSync2(file)) {
|
|
2831
2643
|
if (previous !== void 0) writeFileSync(file, previous);
|
|
2832
2644
|
return { code: code !== 0 ? code : 1, file, status: "failed", kept: [] };
|
|
2833
2645
|
}
|
|
2834
|
-
const generated = withNoteControl(
|
|
2646
|
+
const generated = withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl);
|
|
2835
2647
|
const { text, kept } = keepOwnJobs(previous, generated, input.forge);
|
|
2836
2648
|
writeFileSync(file, text);
|
|
2837
2649
|
return { code, file, status: text === previous ? "unchanged" : "written", kept };
|
|
@@ -2873,8 +2685,76 @@ var init_review_stacks2 = __esm({
|
|
|
2873
2685
|
});
|
|
2874
2686
|
|
|
2875
2687
|
// src/waves-workflow.ts
|
|
2876
|
-
import { existsSync as
|
|
2877
|
-
import { dirname as
|
|
2688
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2689
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
2690
|
+
function wavesEngine(pipeline) {
|
|
2691
|
+
const o = chantPipelineOptions({ ...pipeline.image ? { image: pipeline.image } : {}, ...pipeline.engineSetup ? { engineSetup: pipeline.engineSetup } : {} }, false);
|
|
2692
|
+
return { ...o.image ? { image: o.image } : {}, ...o.beforeScript?.length ? { beforeScript: o.beforeScript } : {} };
|
|
2693
|
+
}
|
|
2694
|
+
function wavesSpecText(spec, engine2 = {}) {
|
|
2695
|
+
const options = { ...engine2.image ? { image: engine2.image } : {}, ...engine2.beforeScript?.length ? { beforeScript: engine2.beforeScript } : {} };
|
|
2696
|
+
return JSON.stringify(Object.keys(options).length ? { waves: spec, options } : spec, null, 2) + "\n";
|
|
2697
|
+
}
|
|
2698
|
+
function unwrapWavesSpec(doc) {
|
|
2699
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc;
|
|
2700
|
+
const inner = doc.waves;
|
|
2701
|
+
if (Array.isArray(inner)) return doc;
|
|
2702
|
+
return inner && typeof inner === "object" && Array.isArray(inner.waves) ? inner : doc;
|
|
2703
|
+
}
|
|
2704
|
+
function stepId(wave, target) {
|
|
2705
|
+
return `${wave}-${target}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2706
|
+
}
|
|
2707
|
+
function upOpFile(spec) {
|
|
2708
|
+
const s = spec ?? {};
|
|
2709
|
+
const op = typeof s.op === "string" && s.op !== "" ? s.op : OP_NAME;
|
|
2710
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2711
|
+
const phases = [];
|
|
2712
|
+
for (const [i, w] of (s.waves ?? []).entries()) {
|
|
2713
|
+
const name = typeof w.name === "string" && w.name !== "" ? w.name : `wave-${i + 1}`;
|
|
2714
|
+
const steps = [];
|
|
2715
|
+
for (const run of w.runs ?? []) {
|
|
2716
|
+
const input = run.input ?? {};
|
|
2717
|
+
if (typeof input.member !== "string" || typeof input.dir !== "string") continue;
|
|
2718
|
+
let id = stepId(name, run.target);
|
|
2719
|
+
for (let n = 2; taken.has(id); n++) id = `${stepId(name, run.target)}-${n}`;
|
|
2720
|
+
taken.add(id);
|
|
2721
|
+
const opts = [
|
|
2722
|
+
`dir: ${JSON.stringify(input.dir)}`,
|
|
2723
|
+
...typeof input.stack === "string" ? [`stack: ${JSON.stringify(input.stack)}`] : [],
|
|
2724
|
+
...input.engine === "pulumi" || input.engine === "plunge" ? [`engine: ${JSON.stringify(input.engine)}`] : [],
|
|
2725
|
+
`id: ${JSON.stringify(id)}`
|
|
2726
|
+
];
|
|
2727
|
+
steps.push(`pulumiPreviewStep(${JSON.stringify(input.member)}, { ${opts.join(", ")} })`);
|
|
2728
|
+
}
|
|
2729
|
+
if (steps.length > 0) phases.push(` phase(${JSON.stringify(name)}, [${steps.join(", ")}]),`);
|
|
2730
|
+
}
|
|
2731
|
+
return [
|
|
2732
|
+
`// Written by '${PRODUCT} init' and '${PRODUCT} ci' from .chant/op-waves/${OP_NAME}.spec.json, and written again on each run: edit plywood.yml, not this file.`,
|
|
2733
|
+
`// The waves workflow runs under this Op's name: each wave job previews and updates its stacks through the spec's`,
|
|
2734
|
+
`// pulumi-up step and records the runs under ${op}. chant's generator needs the Op to exist. Run by hand`,
|
|
2735
|
+
`// (npx chant run ${op}), it previews each stack in wave order and changes nothing.`,
|
|
2736
|
+
`import { Op, phase } from "@intentius/chant/op";`,
|
|
2737
|
+
`import { pulumiPreviewStep } from "@intentius/chant-lexicon-pulumi";`,
|
|
2738
|
+
"",
|
|
2739
|
+
"export default Op({",
|
|
2740
|
+
` name: ${JSON.stringify(op)},`,
|
|
2741
|
+
` overview: "The stacks ${PRODUCT}'s waves update on merge, previewed in wave order",`,
|
|
2742
|
+
` labels: { Surface: "${PRODUCT}-up" },`,
|
|
2743
|
+
" phases: [",
|
|
2744
|
+
...phases,
|
|
2745
|
+
" ],",
|
|
2746
|
+
"});",
|
|
2747
|
+
""
|
|
2748
|
+
].join("\n");
|
|
2749
|
+
}
|
|
2750
|
+
function writeUpOp(cwd, spec) {
|
|
2751
|
+
const path = join3(cwd, UP_OP_FILE);
|
|
2752
|
+
const text = upOpFile(spec);
|
|
2753
|
+
if (existsSync3(path) && readFileSync3(path, "utf8") === text) return false;
|
|
2754
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
2755
|
+
writeFileSync2(path, text);
|
|
2756
|
+
return true;
|
|
2757
|
+
}
|
|
2878
2758
|
function binLines(waves, bin) {
|
|
2879
2759
|
const binArgs = bin.split(/\s+/).filter(Boolean);
|
|
2880
2760
|
return waves.jobs.map((job) => {
|
|
@@ -2891,22 +2771,23 @@ function withBinLines(text, lines2) {
|
|
|
2891
2771
|
});
|
|
2892
2772
|
}
|
|
2893
2773
|
function generatedFor(dir, specFile) {
|
|
2894
|
-
if (!
|
|
2895
|
-
return
|
|
2774
|
+
if (!existsSync3(dir)) return [];
|
|
2775
|
+
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}`));
|
|
2896
2776
|
}
|
|
2897
|
-
async function writeWaveJobs(chant, forge, waves, words, table, io) {
|
|
2898
|
-
const specPath =
|
|
2899
|
-
const specText =
|
|
2900
|
-
const before =
|
|
2777
|
+
async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}) {
|
|
2778
|
+
const specPath = join3(io.cwd, waves.specFile);
|
|
2779
|
+
const specText = wavesSpecText(waves.spec, engine2);
|
|
2780
|
+
const before = existsSync3(specPath) ? readFileSync3(specPath, "utf8") : void 0;
|
|
2901
2781
|
if (before !== specText) {
|
|
2902
|
-
mkdirSync2(
|
|
2782
|
+
mkdirSync2(dirname3(specPath), { recursive: true });
|
|
2903
2783
|
writeFileSync2(specPath, specText);
|
|
2904
2784
|
io.err(`${PRODUCT} ci: wrote ${waves.specFile} with plywood's op and gates; commit it with the workflow`);
|
|
2905
2785
|
}
|
|
2786
|
+
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`);
|
|
2906
2787
|
const outDir = WAVES_OUTPUT_DIR[forge];
|
|
2907
2788
|
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", waves.specFile, "--output", outDir], words);
|
|
2908
2789
|
if (code !== table.done) return code;
|
|
2909
|
-
const files = generatedFor(
|
|
2790
|
+
const files = generatedFor(join3(io.cwd, outDir), waves.specFile);
|
|
2910
2791
|
if (files.length === 0) {
|
|
2911
2792
|
io.err(`${PRODUCT} ci: chant wrote no waves workflow for ${waves.specFile} under ${outDir}`);
|
|
2912
2793
|
return table.failed;
|
|
@@ -2914,7 +2795,7 @@ async function writeWaveJobs(chant, forge, waves, words, table, io) {
|
|
|
2914
2795
|
const lines2 = hasChant4607(chant) ? void 0 : binLines(waves, words.bin);
|
|
2915
2796
|
for (const file of files) {
|
|
2916
2797
|
if (lines2) {
|
|
2917
|
-
const text =
|
|
2798
|
+
const text = readFileSync3(file, "utf8");
|
|
2918
2799
|
const next = withBinLines(text, lines2);
|
|
2919
2800
|
if (next !== text) writeFileSync2(file, next);
|
|
2920
2801
|
}
|
|
@@ -2925,23 +2806,25 @@ async function writeWaveJobs(chant, forge, waves, words, table, io) {
|
|
|
2925
2806
|
}
|
|
2926
2807
|
return table.done;
|
|
2927
2808
|
}
|
|
2928
|
-
var WAVES_OUTPUT_DIR;
|
|
2809
|
+
var WAVES_OUTPUT_DIR, UP_OP_FILE;
|
|
2929
2810
|
var init_waves_workflow = __esm({
|
|
2930
2811
|
"src/waves-workflow.ts"() {
|
|
2931
2812
|
"use strict";
|
|
2932
2813
|
init_chant_product();
|
|
2814
|
+
init_images();
|
|
2933
2815
|
init_words();
|
|
2934
2816
|
WAVES_OUTPUT_DIR = {
|
|
2935
2817
|
github: ".github/workflows",
|
|
2936
2818
|
forgejo: ".forgejo/workflows",
|
|
2937
2819
|
gitlab: "."
|
|
2938
2820
|
};
|
|
2821
|
+
UP_OP_FILE = "ops/pulumi-up.op.ts";
|
|
2939
2822
|
}
|
|
2940
2823
|
});
|
|
2941
2824
|
|
|
2942
2825
|
// src/agent-jobs.ts
|
|
2943
|
-
import { existsSync as
|
|
2944
|
-
import { dirname as
|
|
2826
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2827
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
2945
2828
|
function hasChantAgent(chant) {
|
|
2946
2829
|
return typeof chant?.CI_AGENT_FLOOR === "string";
|
|
2947
2830
|
}
|
|
@@ -2987,9 +2870,9 @@ function agentOpSpec(spec, doc) {
|
|
|
2987
2870
|
};
|
|
2988
2871
|
}
|
|
2989
2872
|
function writeIfChanged(cwd, file, text) {
|
|
2990
|
-
const path =
|
|
2991
|
-
if (
|
|
2992
|
-
mkdirSync3(
|
|
2873
|
+
const path = join4(cwd, file);
|
|
2874
|
+
if (existsSync4(path) && readFileSync4(path, "utf8") === text) return false;
|
|
2875
|
+
mkdirSync3(dirname4(path), { recursive: true });
|
|
2993
2876
|
writeFileSync3(path, text);
|
|
2994
2877
|
return true;
|
|
2995
2878
|
}
|
|
@@ -3016,9 +2899,9 @@ async function writeAgentJobs(chant, forge, env, doc, words, table, io) {
|
|
|
3016
2899
|
const outDir = OP_OUTPUT_DIR[forge];
|
|
3017
2900
|
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", AGENT_SPEC_FILE, "--output", outDir], agentWords);
|
|
3018
2901
|
if (code !== table.done) return code;
|
|
3019
|
-
const dir =
|
|
3020
|
-
const files =
|
|
3021
|
-
for (const f of files) io.err(`${PRODUCT} ci: agent jobs at ${
|
|
2902
|
+
const dir = join4(io.cwd, outDir);
|
|
2903
|
+
const files = existsSync4(dir) ? readdirSync2(dir).filter((f) => /\.ya?ml$/.test(f) && (readFileSync4(join4(dir, f), "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${AGENT_SPEC_FILE}`)) : [];
|
|
2904
|
+
for (const f of files) io.err(`${PRODUCT} ci: agent jobs at ${join4(outDir, f).replace(/^\.\//, "")}, answering /${PRODUCT} agent <ask>`);
|
|
3022
2905
|
io.err(`${PRODUCT} ci: the agent job reads the model's key from ${a.keyEnv}; set it from a secret${a.maxRuns === void 0 ? ", and set agent.max_runs to limit how often it runs" : ""}`);
|
|
3023
2906
|
if (forge === "github" && a.tokenEnv === void 0) io.err(`${PRODUCT} ci: the push job pushes with the job token, which starts no workflow on GitHub, so the agent's commit gets no preview; set agent.token_env to a secret holding a user's or app's token`);
|
|
3024
2907
|
else if (a.tokenEnv !== void 0) io.err(`${PRODUCT} ci: the push job pushes with ${a.tokenEnv}; set it from a secret`);
|
|
@@ -3116,7 +2999,7 @@ __export(jobs_exports, {
|
|
|
3116
2999
|
withJobSettings: () => withJobSettings
|
|
3117
3000
|
});
|
|
3118
3001
|
function hasChant4625() {
|
|
3119
|
-
|
|
3002
|
+
detected ??= (async () => {
|
|
3120
3003
|
try {
|
|
3121
3004
|
const m = await import("@intentius/chant/parallelism");
|
|
3122
3005
|
return typeof m.mapLimited === "function" && typeof m.parallelismProblem === "function";
|
|
@@ -3124,10 +3007,10 @@ function hasChant4625() {
|
|
|
3124
3007
|
return false;
|
|
3125
3008
|
}
|
|
3126
3009
|
})();
|
|
3127
|
-
return
|
|
3010
|
+
return detected;
|
|
3128
3011
|
}
|
|
3129
|
-
function setChant4625ForTests(
|
|
3130
|
-
|
|
3012
|
+
function setChant4625ForTests(has) {
|
|
3013
|
+
detected = has === "reset" ? void 0 : Promise.resolve(has);
|
|
3131
3014
|
}
|
|
3132
3015
|
function secretReference(forge, secret) {
|
|
3133
3016
|
return forge === "gitlab" ? `$${secret}` : `\${{ secrets.${secret} }}`;
|
|
@@ -3228,7 +3111,7 @@ async function prJobsFor(pipeline) {
|
|
|
3228
3111
|
const { flags, notes } = prLoopJobFlags(pipeline.jobs, pipeline.runner);
|
|
3229
3112
|
return { options: prLoopJobOptions(pipeline.jobs, pipeline.runner), flags, notes };
|
|
3230
3113
|
}
|
|
3231
|
-
var
|
|
3114
|
+
var detected, JOBS_NEED_CHANT, GROUP_NAME;
|
|
3232
3115
|
var init_jobs2 = __esm({
|
|
3233
3116
|
"src/jobs.ts"() {
|
|
3234
3117
|
"use strict";
|
|
@@ -3277,8 +3160,8 @@ var init_refusals = __esm({
|
|
|
3277
3160
|
});
|
|
3278
3161
|
|
|
3279
3162
|
// src/up.ts
|
|
3280
|
-
import { existsSync as
|
|
3281
|
-
import { join as
|
|
3163
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
3164
|
+
import { join as join5 } from "node:path";
|
|
3282
3165
|
function approveTarget(gate, opts) {
|
|
3283
3166
|
if (opts.op) return { op: opts.op, gate };
|
|
3284
3167
|
if (PR_OP.test(gate)) return { op: gate, gate: PR_GATE };
|
|
@@ -3359,10 +3242,10 @@ async function approve(args, json, table, io) {
|
|
|
3359
3242
|
});
|
|
3360
3243
|
}
|
|
3361
3244
|
function wavesSpecAt(cwd, file = WAVES_SPEC_FILE) {
|
|
3362
|
-
const path =
|
|
3363
|
-
if (!
|
|
3245
|
+
const path = join5(cwd, file);
|
|
3246
|
+
if (!existsSync5(path)) return void 0;
|
|
3364
3247
|
try {
|
|
3365
|
-
const spec = JSON.parse(
|
|
3248
|
+
const spec = unwrapWavesSpec(JSON.parse(readFileSync5(path, "utf8")));
|
|
3366
3249
|
return spec && typeof spec === "object" && !Array.isArray(spec) ? spec : void 0;
|
|
3367
3250
|
} catch {
|
|
3368
3251
|
return void 0;
|
|
@@ -3446,11 +3329,11 @@ async function override(args, json, table, io) {
|
|
|
3446
3329
|
return withChant(`${PRODUCT} override`, table, io, async (chant) => explainRefusal(await chant.runChantInProcess(argv, PRODUCT_WORDS), table, io.err));
|
|
3447
3330
|
}
|
|
3448
3331
|
function readUpSpec(cwd, file = WAVES_SPEC_FILE) {
|
|
3449
|
-
const path =
|
|
3450
|
-
if (!
|
|
3332
|
+
const path = join5(cwd, file);
|
|
3333
|
+
if (!existsSync5(path)) return { error: `no ${file}; '${PRODUCT} init' writes it from plywood.yml's waves` };
|
|
3451
3334
|
let spec;
|
|
3452
3335
|
try {
|
|
3453
|
-
spec = JSON.parse(
|
|
3336
|
+
spec = unwrapWavesSpec(JSON.parse(readFileSync5(path, "utf8")));
|
|
3454
3337
|
} catch (e) {
|
|
3455
3338
|
return { error: `${file}: ${e.message}` };
|
|
3456
3339
|
}
|
|
@@ -3549,20 +3432,15 @@ async function upBeforeMerge(number, args, json, table, io) {
|
|
|
3549
3432
|
"--forge",
|
|
3550
3433
|
forge,
|
|
3551
3434
|
...requiresArgv(chant, config2.doc),
|
|
3552
|
-
...
|
|
3435
|
+
...mergeArgv(config2.doc),
|
|
3553
3436
|
...json ? ["--json"] : []
|
|
3554
3437
|
];
|
|
3555
3438
|
return explainRefusal(await chant.runChantInProcess(argv, PRODUCT_WORDS), table, io.err, { pr: number });
|
|
3556
3439
|
});
|
|
3557
3440
|
}
|
|
3558
|
-
|
|
3441
|
+
function mergeArgv(doc) {
|
|
3559
3442
|
const merge = applyMergeOf(doc);
|
|
3560
|
-
|
|
3561
|
-
if (!(await chantNext()).applyMerge) {
|
|
3562
|
-
io.err(`${PRODUCT}: ${APPLY_MERGE_NEEDS_CHANT}`);
|
|
3563
|
-
return [];
|
|
3564
|
-
}
|
|
3565
|
-
return applyMergeArgv(merge);
|
|
3443
|
+
return merge ? applyMergeArgv(merge) : [];
|
|
3566
3444
|
}
|
|
3567
3445
|
async function resume(args, json, table, io) {
|
|
3568
3446
|
const op = takeFlag(args, "--op");
|
|
@@ -3597,6 +3475,7 @@ var init_up = __esm({
|
|
|
3597
3475
|
init_review_stacks2();
|
|
3598
3476
|
init_stages();
|
|
3599
3477
|
init_words();
|
|
3478
|
+
init_waves_workflow();
|
|
3600
3479
|
OVERRIDE_OP = "policy-override";
|
|
3601
3480
|
WAVES_SPEC_FILE = `.chant/op-waves/${OP_NAME}.spec.json`;
|
|
3602
3481
|
UP_STEP_KIND = "pulumi-up";
|
|
@@ -3609,9 +3488,9 @@ var init_up = __esm({
|
|
|
3609
3488
|
// src/drift.ts
|
|
3610
3489
|
import { createHash as createHash2 } from "node:crypto";
|
|
3611
3490
|
import { resolveTargetCloudIdentity } from "@intentius/chant/ci-identity";
|
|
3612
|
-
import { existsSync as
|
|
3491
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3613
3492
|
import { tmpdir } from "node:os";
|
|
3614
|
-
import { dirname as
|
|
3493
|
+
import { dirname as dirname5, join as join6 } from "node:path";
|
|
3615
3494
|
function driftRows(doc) {
|
|
3616
3495
|
return (doc.records ?? []).filter((r) => DRIFT_STATUSES.includes(r.status)).map((r) => ({ urn: r.address, status: r.status, attributes: (r.attributes ?? []).map((a) => a.path).sort() })).sort((a, b) => a.urn < b.urn ? -1 : a.urn > b.urn ? 1 : 0);
|
|
3617
3496
|
}
|
|
@@ -3670,7 +3549,7 @@ async function drift2(args, json, table, io) {
|
|
|
3670
3549
|
const respond = respondFlag.value ?? driftResponseOf(doc);
|
|
3671
3550
|
const forge = forgeFlag.value;
|
|
3672
3551
|
return withChant(`${PRODUCT} drift`, table, io, async (chant) => {
|
|
3673
|
-
const dir = mkdtempSync(
|
|
3552
|
+
const dir = mkdtempSync(join6(tmpdir(), "plywood-drift-"));
|
|
3674
3553
|
const results = [];
|
|
3675
3554
|
try {
|
|
3676
3555
|
const attribute = await attributionFor(doc, io);
|
|
@@ -3750,12 +3629,12 @@ async function driftNotice(chant, doc, results, environment, io) {
|
|
|
3750
3629
|
});
|
|
3751
3630
|
}
|
|
3752
3631
|
async function readStack(chant, stack, dir, opts) {
|
|
3753
|
-
const file =
|
|
3632
|
+
const file = join6(dir, `${driftOpName(stack)}.json`);
|
|
3754
3633
|
const argv = ["workspace", "drift", "--member", stack, ...opts.resource ? ["--resource", opts.resource] : [], ...opts.env ? ["--env", opts.env] : [], "-o", file];
|
|
3755
3634
|
const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
3756
3635
|
let doc;
|
|
3757
3636
|
try {
|
|
3758
|
-
if (
|
|
3637
|
+
if (existsSync6(file)) doc = JSON.parse(readFileSync6(file, "utf8"));
|
|
3759
3638
|
} catch (e) {
|
|
3760
3639
|
return { stack, ok: false, drifted: 0, rows: [], error: `chant wrote a drift document plywood cannot read: ${e.message}` };
|
|
3761
3640
|
}
|
|
@@ -3769,12 +3648,7 @@ async function readStack(chant, stack, dir, opts) {
|
|
|
3769
3648
|
async function attributionFor(doc, io) {
|
|
3770
3649
|
const logs = driftSettingsOf(doc).auditLogs;
|
|
3771
3650
|
if (!logs) return void 0;
|
|
3772
|
-
|
|
3773
|
-
if (!audit3) {
|
|
3774
|
-
io.err(`${PRODUCT} drift: ${ATTRIBUTION_NEEDS_CHANT}`);
|
|
3775
|
-
return void 0;
|
|
3776
|
-
}
|
|
3777
|
-
return { audit: audit3, logs };
|
|
3651
|
+
return { audit: await chantAudit(), logs };
|
|
3778
3652
|
}
|
|
3779
3653
|
function auditLogFor(logs, stack) {
|
|
3780
3654
|
return Object.entries(logs).find(([glob]) => glob === "*" || glob === "**" || globMatches(glob, stack))?.[1];
|
|
@@ -3793,7 +3667,7 @@ async function attributeStack(a, stack, doc) {
|
|
|
3793
3667
|
}
|
|
3794
3668
|
function specHasUpdate(cwd, op) {
|
|
3795
3669
|
try {
|
|
3796
|
-
const spec = JSON.parse(
|
|
3670
|
+
const spec = JSON.parse(readFileSync6(join6(cwd, DRIFT_SPEC_FILE), "utf8"));
|
|
3797
3671
|
return (spec.ops ?? []).some((o) => o.name === op && o.update !== void 0);
|
|
3798
3672
|
} catch {
|
|
3799
3673
|
return false;
|
|
@@ -3869,9 +3743,9 @@ function withNotifyVariables(fields, notify2) {
|
|
|
3869
3743
|
return { ...fields, variables: { ...notify2, ...fields.variables ?? {} } };
|
|
3870
3744
|
}
|
|
3871
3745
|
function writeIfChanged2(cwd, file, text) {
|
|
3872
|
-
const path =
|
|
3873
|
-
if (
|
|
3874
|
-
mkdirSync4(
|
|
3746
|
+
const path = join6(cwd, file);
|
|
3747
|
+
if (existsSync6(path) && readFileSync6(path, "utf8") === text) return false;
|
|
3748
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
3875
3749
|
writeFileSync4(path, text);
|
|
3876
3750
|
return true;
|
|
3877
3751
|
}
|
|
@@ -3882,19 +3756,18 @@ async function writeDriftJobs(chant, forge, doc, words, table, io, env = "produc
|
|
|
3882
3756
|
io.err(`${PRODUCT} ci: drift.schedule is set and plywood.yml lists no stacks, so no drift job was written`);
|
|
3883
3757
|
return table.done;
|
|
3884
3758
|
}
|
|
3885
|
-
const next = await chantNext();
|
|
3886
3759
|
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`);
|
|
3887
|
-
if (writeIfChanged2(io.cwd, DRIFT_SPEC_FILE, JSON.stringify(driftOpSpec(doc, forge, settings.stacks, settings.schedule, chant, { cloud:
|
|
3760
|
+
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}`);
|
|
3888
3761
|
const outDir = OP_OUTPUT_DIR2[forge];
|
|
3889
3762
|
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", DRIFT_SPEC_FILE, "--output", outDir], words);
|
|
3890
3763
|
if (code !== table.done) return code;
|
|
3891
|
-
const dir =
|
|
3892
|
-
const files =
|
|
3893
|
-
for (const f of files) io.err(`${PRODUCT} ci: drift job at ${
|
|
3764
|
+
const dir = join6(io.cwd, outDir);
|
|
3765
|
+
const files = existsSync6(dir) ? readdirSync3(dir).filter((f) => /\.ya?ml$/.test(f) && (readFileSync6(join6(dir, f), "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${DRIFT_SPEC_FILE}`)) : [];
|
|
3766
|
+
for (const f of files) io.err(`${PRODUCT} ci: drift job at ${join6(outDir, f).replace(/^\.\//, "")} on ${settings.schedule}`);
|
|
3894
3767
|
if (files.length && forge === "gitlab") io.err(`${PRODUCT} ci: GitLab runs the drift job from a pipeline schedule; include the file from .gitlab-ci.yml and add a schedule`);
|
|
3895
3768
|
return table.done;
|
|
3896
3769
|
}
|
|
3897
|
-
var DRIFT_OP_FILE, DRIFT_SPEC_FILE, SNAPSHOTS_REF, SNAPSHOTS_FETCH, DRIFT_STATUSES, forgeOverride, notifyFetch,
|
|
3770
|
+
var DRIFT_OP_FILE, DRIFT_SPEC_FILE, SNAPSHOTS_REF, SNAPSHOTS_FETCH, DRIFT_STATUSES, forgeOverride, notifyFetch, OP_OUTPUT_DIR2;
|
|
3898
3771
|
var init_drift2 = __esm({
|
|
3899
3772
|
"src/drift.ts"() {
|
|
3900
3773
|
"use strict";
|
|
@@ -3918,7 +3791,6 @@ var init_drift2 = __esm({
|
|
|
3918
3791
|
SNAPSHOTS_REF = "refs/chant/snapshots";
|
|
3919
3792
|
SNAPSHOTS_FETCH = `git fetch --no-tags origin '+${SNAPSHOTS_REF}:${SNAPSHOTS_REF}' || true`;
|
|
3920
3793
|
DRIFT_STATUSES = ["drifted", "missing"];
|
|
3921
|
-
ATTRIBUTION_NEEDS_CHANT = "drift.audit_log is set, and the installed chant has no audit-log reads (chant#4632), so no drifted attribute was attributed";
|
|
3922
3794
|
OP_OUTPUT_DIR2 = { github: ".github/workflows", forgejo: ".forgejo/workflows", gitlab: "." };
|
|
3923
3795
|
}
|
|
3924
3796
|
});
|
|
@@ -3965,8 +3837,8 @@ var init_tips = __esm({
|
|
|
3965
3837
|
});
|
|
3966
3838
|
|
|
3967
3839
|
// src/agents.ts
|
|
3968
|
-
import { existsSync as
|
|
3969
|
-
import { dirname as
|
|
3840
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3841
|
+
import { dirname as dirname6, join as join7 } from "node:path";
|
|
3970
3842
|
function plywoodMcpTools(ctx) {
|
|
3971
3843
|
const none = { type: "object", properties: {} };
|
|
3972
3844
|
const doc = () => {
|
|
@@ -3976,7 +3848,7 @@ function plywoodMcpTools(ctx) {
|
|
|
3976
3848
|
return [
|
|
3977
3849
|
{
|
|
3978
3850
|
name: "stacks",
|
|
3979
|
-
description: "The stacks plywood.yml lists, each with its directory, gate and
|
|
3851
|
+
description: "The stacks plywood.yml lists, each with its directory, gate, backend and actions, and the waves they update in.",
|
|
3980
3852
|
inputSchema: none,
|
|
3981
3853
|
readOnly: true,
|
|
3982
3854
|
handler: async () => {
|
|
@@ -4010,24 +3882,51 @@ function plywoodMcpTools(ctx) {
|
|
|
4010
3882
|
}
|
|
4011
3883
|
];
|
|
4012
3884
|
}
|
|
4013
|
-
function
|
|
3885
|
+
function plywoodProposeTools() {
|
|
3886
|
+
return [
|
|
3887
|
+
{
|
|
3888
|
+
name: TEMPLATE_REQUEST_WORDS.tool,
|
|
3889
|
+
description: `Open a pull request for an app from a template, reviewed and gated like a person's. ${TEMPLATE_REQUEST_WORDS.refusal}`,
|
|
3890
|
+
inputSchema: {
|
|
3891
|
+
type: "object",
|
|
3892
|
+
properties: {
|
|
3893
|
+
template: { type: "string", description: "The template's name in the catalog." },
|
|
3894
|
+
version: { type: "string", description: "The template's version; the newest when left out." },
|
|
3895
|
+
name: { type: "string", description: "The app's name, which names its directory and the request branch." },
|
|
3896
|
+
app: { type: "string", description: "The app an add-on template joins." },
|
|
3897
|
+
inputs: { type: "object", description: "The template's inputs, by name." }
|
|
3898
|
+
},
|
|
3899
|
+
required: ["template", "name", "inputs"]
|
|
3900
|
+
},
|
|
3901
|
+
forgeWrite: true,
|
|
3902
|
+
handler: async () => {
|
|
3903
|
+
throw new Error(TEMPLATE_REQUEST_WORDS.refusal);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
];
|
|
3907
|
+
}
|
|
3908
|
+
function plywoodMcpHook(reportUrl, propose = false) {
|
|
4014
3909
|
return {
|
|
4015
3910
|
product: PRODUCT,
|
|
4016
|
-
|
|
3911
|
+
// A read-only server without the tier refuses to start with a forgeWrite tool, so they go only where chant says it serves them.
|
|
3912
|
+
tools: (ctx) => [...plywoodMcpTools(ctx), ...propose && ctx.propose === true ? plywoodProposeTools() : []],
|
|
4017
3913
|
readOnly: true,
|
|
3914
|
+
...propose ? { propose: true } : {},
|
|
4018
3915
|
...reportUrl ? { reportUrl } : {},
|
|
4019
|
-
instructions: `${PRODUCT} (${TAGLINE}) serves read tools
|
|
3916
|
+
instructions: propose ? `${PRODUCT} (${TAGLINE}) serves read tools and ${TEMPLATE_REQUEST_WORDS.tool}, which may open a pull request and nothing else. Pulumi templates are not available yet, so ${TEMPLATE_REQUEST_WORDS.tool} refuses every call. ${READ_INSTRUCTIONS}` : `${PRODUCT} (${TAGLINE}) serves read tools only. ${READ_INSTRUCTIONS}`
|
|
4020
3917
|
};
|
|
4021
3918
|
}
|
|
4022
3919
|
async function mcp(args, _json, table, io) {
|
|
4023
|
-
const
|
|
3920
|
+
const propose = args.includes("--propose");
|
|
3921
|
+
const report = takeFlag(args.filter((a) => a !== "--propose"), "--report-url");
|
|
4024
3922
|
if (report.missing) return usage("--report-url takes a URL", table, io);
|
|
4025
3923
|
if (report.rest.length) return usage(`mcp does not take ${report.rest.join(" ")}`, table, io);
|
|
4026
3924
|
return withChant(`${PRODUCT} mcp`, table, io, (chant) => {
|
|
4027
3925
|
if (typeof chant.PRODUCT_MCP_HOOK_FLOOR !== "string") {
|
|
4028
3926
|
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`);
|
|
4029
3927
|
}
|
|
4030
|
-
|
|
3928
|
+
const argv = ["serve", "mcp", "--read-only", ...propose ? ["--propose"] : []];
|
|
3929
|
+
return chant.runChantInProcess(argv, PRODUCT_WORDS, { mcp: plywoodMcpHook(report.value, propose) });
|
|
4031
3930
|
});
|
|
4032
3931
|
}
|
|
4033
3932
|
function reviewOpFile(doc) {
|
|
@@ -4079,9 +3978,9 @@ function reviewOpSpec(doc) {
|
|
|
4079
3978
|
};
|
|
4080
3979
|
}
|
|
4081
3980
|
function writeIfChanged3(cwd, file, text) {
|
|
4082
|
-
const path =
|
|
4083
|
-
if (
|
|
4084
|
-
mkdirSync5(
|
|
3981
|
+
const path = join7(cwd, file);
|
|
3982
|
+
if (existsSync7(path) && readFileSync7(path, "utf8") === text) return false;
|
|
3983
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
4085
3984
|
writeFileSync5(path, text);
|
|
4086
3985
|
return true;
|
|
4087
3986
|
}
|
|
@@ -4092,15 +3991,15 @@ async function writeReviewJobs(chant, forge, doc, words, table, io) {
|
|
|
4092
3991
|
const outDir = OP_OUTPUT_DIR3[forge];
|
|
4093
3992
|
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", REVIEW_SPEC_FILE, "--output", outDir], words);
|
|
4094
3993
|
if (code !== table.done) return code;
|
|
4095
|
-
const dir =
|
|
4096
|
-
const files =
|
|
4097
|
-
for (const f of files) io.err(`${PRODUCT} ci: review jobs at ${
|
|
3994
|
+
const dir = join7(io.cwd, outDir);
|
|
3995
|
+
const files = existsSync7(dir) ? readdirSync4(dir).filter((f) => /\.ya?ml$/.test(f) && (readFileSync7(join7(dir, f), "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${REVIEW_SPEC_FILE}`)) : [];
|
|
3996
|
+
for (const f of files) io.err(`${PRODUCT} ci: review jobs at ${join7(outDir, f).replace(/^\.\//, "")}`);
|
|
4098
3997
|
const r = reviewOf(doc);
|
|
4099
3998
|
io.err(`${PRODUCT} ci: the review job reads the model's key from ${r.keyEnv}; set it from a secret`);
|
|
4100
3999
|
if (forge === "gitlab") io.err(`${PRODUCT} ci: on GitLab the note job runs on the default branch with CHANT_REVIEW_NOTES=1 from a pipeline schedule, and posts with a protected CHANT_FORGE_TOKEN`);
|
|
4101
4000
|
return table.done;
|
|
4102
4001
|
}
|
|
4103
|
-
var PERSON_VERBS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE, REVIEW_CHANGE_SET, REVIEW_PREVIEW_LINE, OP_OUTPUT_DIR3;
|
|
4002
|
+
var PERSON_VERBS, READ_INSTRUCTIONS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE, REVIEW_CHANGE_SET, REVIEW_PREVIEW_LINE, OP_OUTPUT_DIR3;
|
|
4104
4003
|
var init_agents2 = __esm({
|
|
4105
4004
|
"src/agents.ts"() {
|
|
4106
4005
|
"use strict";
|
|
@@ -4114,6 +4013,7 @@ var init_agents2 = __esm({
|
|
|
4114
4013
|
init_tips();
|
|
4115
4014
|
init_words();
|
|
4116
4015
|
PERSON_VERBS = ["up", "approve", "override"];
|
|
4016
|
+
READ_INSTRUCTIONS = `To change a stack, open a pull request: ${PRODUCT} previews it, and its update runs after a person approves the digest.`;
|
|
4117
4017
|
REVIEW_OP = "plywood-review";
|
|
4118
4018
|
REVIEW_OP_FILE = "ops/plywood-review.op.ts";
|
|
4119
4019
|
REVIEW_SPEC_FILE = ".chant/ops/plywood-review.spec.json";
|
|
@@ -4123,6 +4023,227 @@ var init_agents2 = __esm({
|
|
|
4123
4023
|
}
|
|
4124
4024
|
});
|
|
4125
4025
|
|
|
4026
|
+
// src/init/detect.ts
|
|
4027
|
+
import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync } from "node:fs";
|
|
4028
|
+
import { basename, dirname as dirname7, join as join8, relative, sep } from "node:path";
|
|
4029
|
+
import { parseYAML as parseYAML2 } from "@intentius/chant/yaml";
|
|
4030
|
+
function repoRoot(dir) {
|
|
4031
|
+
let cur = dir;
|
|
4032
|
+
for (let i = 0; i < 64; i++) {
|
|
4033
|
+
if (existsSync8(join8(cur, ".git"))) return cur;
|
|
4034
|
+
const parent = dirname7(cur);
|
|
4035
|
+
if (parent === cur) return dir;
|
|
4036
|
+
cur = parent;
|
|
4037
|
+
}
|
|
4038
|
+
return dir;
|
|
4039
|
+
}
|
|
4040
|
+
function posixRel(root, path) {
|
|
4041
|
+
const rel = relative(root, path);
|
|
4042
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
4043
|
+
}
|
|
4044
|
+
function readYamlFile(file) {
|
|
4045
|
+
try {
|
|
4046
|
+
const doc = parseYAML2(readFileSync8(file, "utf8"));
|
|
4047
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : void 0;
|
|
4048
|
+
} catch {
|
|
4049
|
+
return void 0;
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
function findPulumiProjects(root) {
|
|
4053
|
+
const out = [];
|
|
4054
|
+
const walk = (dir, depth) => {
|
|
4055
|
+
let entries;
|
|
4056
|
+
try {
|
|
4057
|
+
entries = readdirSync5(dir).sort();
|
|
4058
|
+
} catch {
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
const file = ["Pulumi.yaml", "Pulumi.yml"].find((n) => entries.includes(n));
|
|
4062
|
+
if (file) {
|
|
4063
|
+
const project = readProject(root, dir, join8(dir, file));
|
|
4064
|
+
if (project) out.push(project);
|
|
4065
|
+
}
|
|
4066
|
+
if (depth >= MAX_DEPTH) return;
|
|
4067
|
+
for (const name of entries) {
|
|
4068
|
+
if (SKIP.has(name) || name.startsWith(".")) continue;
|
|
4069
|
+
const path = join8(dir, name);
|
|
4070
|
+
try {
|
|
4071
|
+
if (statSync(path).isDirectory()) walk(path, depth + 1);
|
|
4072
|
+
} catch {
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
walk(root, 0);
|
|
4077
|
+
return out;
|
|
4078
|
+
}
|
|
4079
|
+
function readProject(root, dir, file) {
|
|
4080
|
+
const doc = readYamlFile(file);
|
|
4081
|
+
if (!doc || typeof doc.name !== "string" || doc.name === "") return void 0;
|
|
4082
|
+
const runtime = typeof doc.runtime === "string" ? doc.runtime : doc.runtime?.name;
|
|
4083
|
+
const backend = doc.backend;
|
|
4084
|
+
const configDir = typeof doc.stackConfigDir === "string" ? join8(dir, doc.stackConfigDir) : dir;
|
|
4085
|
+
return {
|
|
4086
|
+
name: doc.name,
|
|
4087
|
+
dir: posixRel(root, dir),
|
|
4088
|
+
...typeof runtime === "string" ? { runtime } : {},
|
|
4089
|
+
...backend && typeof backend.url === "string" ? { backendUrl: backend.url } : {},
|
|
4090
|
+
stacks: stackNames(configDir)
|
|
4091
|
+
};
|
|
4092
|
+
}
|
|
4093
|
+
function stackNames(configDir) {
|
|
4094
|
+
let entries;
|
|
4095
|
+
try {
|
|
4096
|
+
entries = readdirSync5(configDir);
|
|
4097
|
+
} catch {
|
|
4098
|
+
return [];
|
|
4099
|
+
}
|
|
4100
|
+
const names = entries.map((n) => /^Pulumi\.(.+)\.ya?ml$/.exec(n)?.[1]).filter((s) => s !== void 0 && s !== "");
|
|
4101
|
+
return [...new Set(names)].sort(byEnvironment);
|
|
4102
|
+
}
|
|
4103
|
+
function environmentOf(stack) {
|
|
4104
|
+
const last = stack.split("/").pop().toLowerCase();
|
|
4105
|
+
const head = last.split(/[-_.]/)[0];
|
|
4106
|
+
return ENV_ORDER.includes(head) ? head : last;
|
|
4107
|
+
}
|
|
4108
|
+
function rank(stack) {
|
|
4109
|
+
const env = environmentOf(stack);
|
|
4110
|
+
const at = ENV_ORDER.indexOf(env);
|
|
4111
|
+
return at >= 0 ? at : PROD_RANK - 0.5;
|
|
4112
|
+
}
|
|
4113
|
+
function byEnvironment(a, b) {
|
|
4114
|
+
return rank(a) - rank(b) || a.localeCompare(b);
|
|
4115
|
+
}
|
|
4116
|
+
function stacksOf2(projects) {
|
|
4117
|
+
const names = /* @__PURE__ */ new Map();
|
|
4118
|
+
for (const p of projects) names.set(p.name, (names.get(p.name) ?? 0) + 1);
|
|
4119
|
+
const out = [];
|
|
4120
|
+
for (const p of projects) {
|
|
4121
|
+
const project = (names.get(p.name) ?? 0) > 1 ? `${p.name}-${slug(p.dir)}` : p.name;
|
|
4122
|
+
for (const stack of p.stacks) out.push({ key: `${project}/${stack}`, project: p.name, stack, dir: p.dir });
|
|
4123
|
+
}
|
|
4124
|
+
return out.sort((x, y) => byEnvironment(x.stack, y.stack) || x.key.localeCompare(y.key));
|
|
4125
|
+
}
|
|
4126
|
+
function slug(s) {
|
|
4127
|
+
const out = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4128
|
+
return out === "" ? "root" : out;
|
|
4129
|
+
}
|
|
4130
|
+
function originUrl(root) {
|
|
4131
|
+
const config2 = gitFile(root, "config");
|
|
4132
|
+
if (!config2) return void 0;
|
|
4133
|
+
let inOrigin = false;
|
|
4134
|
+
for (const raw of config2.split(/\r?\n/)) {
|
|
4135
|
+
const line = raw.trim();
|
|
4136
|
+
if (line.startsWith("[")) {
|
|
4137
|
+
inOrigin = /^\[remote\s+"origin"\]$/.test(line);
|
|
4138
|
+
continue;
|
|
4139
|
+
}
|
|
4140
|
+
const m = inOrigin ? /^url\s*=\s*(.+)$/.exec(line) : null;
|
|
4141
|
+
if (m) return m[1].trim();
|
|
4142
|
+
}
|
|
4143
|
+
return void 0;
|
|
4144
|
+
}
|
|
4145
|
+
function gitFile(root, name) {
|
|
4146
|
+
const dotGit = join8(root, ".git");
|
|
4147
|
+
let gitDir = dotGit;
|
|
4148
|
+
try {
|
|
4149
|
+
if (statSync(dotGit).isFile()) {
|
|
4150
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync8(dotGit, "utf8"));
|
|
4151
|
+
if (!m) return void 0;
|
|
4152
|
+
gitDir = join8(root, m[1].trim());
|
|
4153
|
+
const common = join8(gitDir, "commondir");
|
|
4154
|
+
if (existsSync8(common) && !existsSync8(join8(gitDir, name))) gitDir = join8(gitDir, readFileSync8(common, "utf8").trim());
|
|
4155
|
+
}
|
|
4156
|
+
const path = join8(gitDir, name);
|
|
4157
|
+
return existsSync8(path) ? readFileSync8(path, "utf8") : void 0;
|
|
4158
|
+
} catch {
|
|
4159
|
+
return void 0;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
function remoteHost(url) {
|
|
4163
|
+
const scp = /^[^@/\s]+@([^:/\s]+):/.exec(url);
|
|
4164
|
+
if (scp) return scp[1].toLowerCase();
|
|
4165
|
+
try {
|
|
4166
|
+
return new URL(url).hostname.toLowerCase() || void 0;
|
|
4167
|
+
} catch {
|
|
4168
|
+
return void 0;
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
function forgeOfRemote(url) {
|
|
4172
|
+
const host = remoteHost(url);
|
|
4173
|
+
if (!host) return void 0;
|
|
4174
|
+
if (host === "github.com" || host.endsWith(".github.com") || host.startsWith("github.")) return { kind: "github", because: `origin is on ${host}` };
|
|
4175
|
+
if (host === "gitlab.com") return { kind: "gitlab", because: `origin is on ${host}` };
|
|
4176
|
+
if (host === "codeberg.org") return { kind: "forgejo", because: `origin is on ${host}` };
|
|
4177
|
+
if (/(^|[.-])gitlab([.-]|$)/.test(host)) return { kind: "gitlab", because: `origin is on ${host}`, url: `https://${host}` };
|
|
4178
|
+
if (/(^|[.-])(forgejo|gitea)([.-]|$)/.test(host)) return { kind: "forgejo", because: `origin is on ${host}`, url: `https://${host}` };
|
|
4179
|
+
return void 0;
|
|
4180
|
+
}
|
|
4181
|
+
function detectForge(root) {
|
|
4182
|
+
const url = originUrl(root);
|
|
4183
|
+
const byRemote = url ? forgeOfRemote(url) : void 0;
|
|
4184
|
+
if (byRemote) return byRemote;
|
|
4185
|
+
if (existsSync8(join8(root, ".forgejo"))) return { kind: "forgejo", because: "the repository has .forgejo/" };
|
|
4186
|
+
if (existsSync8(join8(root, ".gitlab-ci.yml"))) return { kind: "gitlab", because: "the repository has .gitlab-ci.yml" };
|
|
4187
|
+
if (existsSync8(join8(root, ".github", "workflows"))) return { kind: "github", because: "the repository has .github/workflows/" };
|
|
4188
|
+
return void 0;
|
|
4189
|
+
}
|
|
4190
|
+
function defaultBranch(root) {
|
|
4191
|
+
const head = gitFile(root, "refs/remotes/origin/HEAD");
|
|
4192
|
+
const remote = head && /ref:\s*refs\/remotes\/origin\/(.+)/.exec(head)?.[1];
|
|
4193
|
+
if (remote) return remote.trim();
|
|
4194
|
+
const local = gitFile(root, "HEAD");
|
|
4195
|
+
const branch = local && /ref:\s*refs\/heads\/(.+)/.exec(local)?.[1];
|
|
4196
|
+
return branch ? branch.trim() : "main";
|
|
4197
|
+
}
|
|
4198
|
+
function findPulumiActions(root) {
|
|
4199
|
+
const out = [];
|
|
4200
|
+
for (const d of WORKFLOW_DIRS) {
|
|
4201
|
+
let names;
|
|
4202
|
+
try {
|
|
4203
|
+
names = readdirSync5(join8(root, d)).sort();
|
|
4204
|
+
} catch {
|
|
4205
|
+
continue;
|
|
4206
|
+
}
|
|
4207
|
+
for (const n of names) {
|
|
4208
|
+
if (!/\.ya?ml$/.test(n)) continue;
|
|
4209
|
+
try {
|
|
4210
|
+
if (/uses:\s*["']?pulumi\/actions@/.test(readFileSync8(join8(root, d, n), "utf8"))) out.push(`${d}/${n}`);
|
|
4211
|
+
} catch {
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
return out;
|
|
4216
|
+
}
|
|
4217
|
+
function detect(cwd) {
|
|
4218
|
+
const root = repoRoot(cwd);
|
|
4219
|
+
const projects = findPulumiProjects(root);
|
|
4220
|
+
const forge = detectForge(root);
|
|
4221
|
+
const preCommit = [".pre-commit-config.yaml", ".pre-commit-config.yml"].find((n) => existsSync8(join8(root, n)));
|
|
4222
|
+
return {
|
|
4223
|
+
root,
|
|
4224
|
+
projects,
|
|
4225
|
+
stacks: stacksOf2(projects),
|
|
4226
|
+
...forge ? { forge } : {},
|
|
4227
|
+
defaultBranch: defaultBranch(root),
|
|
4228
|
+
pulumiActions: findPulumiActions(root),
|
|
4229
|
+
...preCommit ? { preCommit } : {}
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
function repoName(root) {
|
|
4233
|
+
return slug(basename(root)).slice(0, 40).replace(/-+$/, "") || "plywood";
|
|
4234
|
+
}
|
|
4235
|
+
var SKIP, MAX_DEPTH, ENV_ORDER, PROD_RANK, WORKFLOW_DIRS;
|
|
4236
|
+
var init_detect = __esm({
|
|
4237
|
+
"src/init/detect.ts"() {
|
|
4238
|
+
"use strict";
|
|
4239
|
+
SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor", "venv", ".venv", "__pycache__", "target", "bin", "obj"]);
|
|
4240
|
+
MAX_DEPTH = 8;
|
|
4241
|
+
ENV_ORDER = ["local", "dev", "development", "sandbox", "test", "testing", "qa", "ci", "stage", "staging", "preprod", "uat", "prod", "production"];
|
|
4242
|
+
PROD_RANK = ENV_ORDER.indexOf("prod");
|
|
4243
|
+
WORKFLOW_DIRS = [".github/workflows", ".forgejo/workflows", ".gitea/workflows"];
|
|
4244
|
+
}
|
|
4245
|
+
});
|
|
4246
|
+
|
|
4126
4247
|
// src/policy-presets.ts
|
|
4127
4248
|
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4128
4249
|
function providersRego(allowed, namespace = "main") {
|
|
@@ -4325,7 +4446,7 @@ function adoptLines(previews, chant) {
|
|
|
4325
4446
|
}
|
|
4326
4447
|
const count = `${v.resources.length} resource${v.resources.length === 1 ? "" : "s"}`;
|
|
4327
4448
|
if (v.status === "refused") {
|
|
4328
|
-
out.push(` ${p.key}: plunge refuses to adopt it:`);
|
|
4449
|
+
out.push(` ${p.key}: plunge refuses to adopt it, so the adopt Op leaves it out:`);
|
|
4329
4450
|
for (const r of v.refusals) out.push(` ${r.code}: ${r.reason}${r.urn ? ` (${r.urn})` : ""}`);
|
|
4330
4451
|
} else if (v.status === "already-adopted") {
|
|
4331
4452
|
out.push(` ${p.key}: already on plunge's records (${count})`);
|
|
@@ -4338,7 +4459,10 @@ function adoptLines(previews, chant) {
|
|
|
4338
4459
|
return out;
|
|
4339
4460
|
}
|
|
4340
4461
|
function stacksToAdopt(stacks2, previews) {
|
|
4341
|
-
return stacks2.filter((s) =>
|
|
4462
|
+
return stacks2.filter((s) => {
|
|
4463
|
+
const status = previews.find((p) => p.key === s.key)?.view?.status;
|
|
4464
|
+
return status !== "already-adopted" && status !== "refused";
|
|
4465
|
+
});
|
|
4342
4466
|
}
|
|
4343
4467
|
function adoptGate(member, stack, single) {
|
|
4344
4468
|
return single ? `approve-${ADOPT_OP}` : `approve-${ADOPT_OP}-${member}-${stack}`;
|
|
@@ -4401,9 +4525,9 @@ function plungeUrlOf(url) {
|
|
|
4401
4525
|
const scheme = url.split("://")[0];
|
|
4402
4526
|
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}` };
|
|
4403
4527
|
}
|
|
4404
|
-
function plungeStacks(
|
|
4405
|
-
return
|
|
4406
|
-
const project =
|
|
4528
|
+
function plungeStacks(detected2, env) {
|
|
4529
|
+
return detected2.stacks.map((s) => {
|
|
4530
|
+
const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
|
|
4407
4531
|
const b = backendOf(project, {}, env);
|
|
4408
4532
|
const to = plungeUrlOf(b.url);
|
|
4409
4533
|
return {
|
|
@@ -4534,6 +4658,7 @@ function stepInput(s) {
|
|
|
4534
4658
|
stack: s.stack,
|
|
4535
4659
|
member: s.project,
|
|
4536
4660
|
...s.engine ? { engine: s.engine } : {},
|
|
4661
|
+
...s.backend ? { backend: s.backend } : {},
|
|
4537
4662
|
...s.engineVersion ? { engineVersion: s.engineVersion, ...s.engineRoot ? { engineRoot: s.engineRoot } : {} } : {},
|
|
4538
4663
|
...s.lockGrain ? { lockGrain: s.lockGrain } : {},
|
|
4539
4664
|
...s.policyPacks?.length ? { policyPacks: s.policyPacks } : {}
|
|
@@ -4774,9 +4899,9 @@ function repoJoin(...parts) {
|
|
|
4774
4899
|
const out = posix2.normalize(parts.filter((p) => p !== "" && p !== ".").join("/") || ".").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
4775
4900
|
return out === "" ? "." : out;
|
|
4776
4901
|
}
|
|
4777
|
-
function projectAt(
|
|
4902
|
+
function projectAt(detected2, dir) {
|
|
4778
4903
|
const want = repoJoin(dir);
|
|
4779
|
-
return
|
|
4904
|
+
return detected2.projects.find((p) => repoJoin(p.dir) === want);
|
|
4780
4905
|
}
|
|
4781
4906
|
function stackEntry(keys, key) {
|
|
4782
4907
|
const stacks2 = isObject(keys.stacks) ? keys.stacks : keys.stacks = {};
|
|
@@ -4819,10 +4944,10 @@ var init_types2 = __esm({
|
|
|
4819
4944
|
// src/import/deployments.ts
|
|
4820
4945
|
import { readdirSync as readdirSync6 } from "node:fs";
|
|
4821
4946
|
import { basename as basename2, dirname as dirname8, join as join11 } from "node:path";
|
|
4822
|
-
function deploymentFiles(root,
|
|
4947
|
+
function deploymentFiles(root, detected2, named) {
|
|
4823
4948
|
if (named.length) return [...named];
|
|
4824
4949
|
const out = [];
|
|
4825
|
-
for (const p of
|
|
4950
|
+
for (const p of detected2.projects) {
|
|
4826
4951
|
let names;
|
|
4827
4952
|
try {
|
|
4828
4953
|
names = readdirSync6(join11(root, p.dir)).sort();
|
|
@@ -4852,7 +4977,7 @@ var init_deployments = __esm({
|
|
|
4852
4977
|
"gitHub.deployPullRequest": "set apply.when: pull-request and update with /plywood up",
|
|
4853
4978
|
"gitHub.repository": "plywood runs in the repository it is set up in"
|
|
4854
4979
|
};
|
|
4855
|
-
importDeployments = (root,
|
|
4980
|
+
importDeployments = (root, detected2, named) => {
|
|
4856
4981
|
let keys = {};
|
|
4857
4982
|
const unsupported = [];
|
|
4858
4983
|
const notes = [];
|
|
@@ -4860,7 +4985,7 @@ var init_deployments = __esm({
|
|
|
4860
4985
|
const not = (file, setting, why) => {
|
|
4861
4986
|
unsupported.push({ file, setting, why: why ?? NOT_CARRIED[setting] ?? "plywood has no setting for it" });
|
|
4862
4987
|
};
|
|
4863
|
-
for (const file of deploymentFiles(root,
|
|
4988
|
+
for (const file of deploymentFiles(root, detected2, named)) {
|
|
4864
4989
|
const m = SETTINGS_FILE.exec(basename2(file));
|
|
4865
4990
|
if (!m) {
|
|
4866
4991
|
not(file, "(file)", "names no stack: call it Pulumi.<stack>.deploy.yaml (or .json) beside the project's Pulumi.yaml");
|
|
@@ -4879,7 +5004,7 @@ var init_deployments = __esm({
|
|
|
4879
5004
|
const fileDir = repoJoin(posixRel(root, dirname8(join11(root, file))));
|
|
4880
5005
|
const source = isObject(settings.sourceContext) && isObject(settings.sourceContext.git) ? settings.sourceContext.git : {};
|
|
4881
5006
|
const dir = typeof source.repoDir === "string" ? repoJoin(source.repoDir) : fileDir;
|
|
4882
|
-
const project = projectAt(
|
|
5007
|
+
const project = projectAt(detected2, dir)?.name ?? projectAt(detected2, fileDir)?.name;
|
|
4883
5008
|
if (!project) {
|
|
4884
5009
|
not(file, "sourceContext.git.repoDir", `${dir} holds no Pulumi.yaml`);
|
|
4885
5010
|
continue;
|
|
@@ -4934,9 +5059,9 @@ var init_deployments = __esm({
|
|
|
4934
5059
|
});
|
|
4935
5060
|
|
|
4936
5061
|
// src/import/env0.ts
|
|
4937
|
-
function stackFor(
|
|
5062
|
+
function stackFor(detected2, names) {
|
|
4938
5063
|
for (const n of names) {
|
|
4939
|
-
const hits =
|
|
5064
|
+
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);
|
|
4940
5065
|
if (hits.length === 1) return hits[0].key;
|
|
4941
5066
|
}
|
|
4942
5067
|
return void 0;
|
|
@@ -4949,7 +5074,7 @@ var init_env0 = __esm({
|
|
|
4949
5074
|
init_types2();
|
|
4950
5075
|
ENV0_WORKFLOW_FILES = ["env0.workflow.yml", "env0.workflow.yaml"];
|
|
4951
5076
|
ENV0_FLOW_FILES = ["env0.yml", "env0.yaml"];
|
|
4952
|
-
importEnv0 = (root,
|
|
5077
|
+
importEnv0 = (root, detected2, named) => {
|
|
4953
5078
|
const keys = {};
|
|
4954
5079
|
const unsupported = [];
|
|
4955
5080
|
const notes = [];
|
|
@@ -4967,7 +5092,7 @@ var init_env0 = __esm({
|
|
|
4967
5092
|
for (const [id, raw] of Object.entries(envs)) {
|
|
4968
5093
|
const e = isObject(raw) ? raw : {};
|
|
4969
5094
|
const names = [id, ...typeof e.name === "string" ? [e.name] : [], ...typeof e.templateName === "string" ? [e.templateName] : []];
|
|
4970
|
-
const key = stackFor(
|
|
5095
|
+
const key = stackFor(detected2, names);
|
|
4971
5096
|
if (key) stackOf.set(id, key);
|
|
4972
5097
|
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` });
|
|
4973
5098
|
for (const k of Object.keys(e).filter((x) => !["name", "templateName", "needs"].includes(x))) {
|
|
@@ -5061,7 +5186,7 @@ var init_pulumi_actions = __esm({
|
|
|
5061
5186
|
plan: "plywood's update applies the preview its approval names, through chant"
|
|
5062
5187
|
};
|
|
5063
5188
|
PULUMI_CLOUD = /^https:\/\/(?:app|api)\.pulumi\.com\/?$/;
|
|
5064
|
-
importPulumiActions = (root,
|
|
5189
|
+
importPulumiActions = (root, detected2, named) => {
|
|
5065
5190
|
let keys = {};
|
|
5066
5191
|
const unsupported = [];
|
|
5067
5192
|
const notes = [];
|
|
@@ -5097,7 +5222,7 @@ var init_pulumi_actions = __esm({
|
|
|
5097
5222
|
const dir = repoJoin(typeof w["work-dir"] === "string" ? w["work-dir"] : ".");
|
|
5098
5223
|
const parts = stackName.split("/");
|
|
5099
5224
|
const stack = parts[parts.length - 1];
|
|
5100
|
-
const project = projectAt(
|
|
5225
|
+
const project = projectAt(detected2, dir)?.name ?? (parts.length === 3 ? parts[1] : void 0);
|
|
5101
5226
|
if (!project) {
|
|
5102
5227
|
unsupported.push({ file, setting: `${where}.with.stack-name`, why: `${dir} holds no Pulumi.yaml, so ${stackName} names no project` });
|
|
5103
5228
|
return;
|
|
@@ -5154,7 +5279,7 @@ var init_spacelift = __esm({
|
|
|
5154
5279
|
init_types2();
|
|
5155
5280
|
SPACELIFT_FILES = [".spacelift/config.yml", ".spacelift/config.yaml"];
|
|
5156
5281
|
CARRIED2 = ["project_root", "runner_image"];
|
|
5157
|
-
importSpacelift = (root,
|
|
5282
|
+
importSpacelift = (root, detected2, named) => {
|
|
5158
5283
|
const keys = {};
|
|
5159
5284
|
const unsupported = [];
|
|
5160
5285
|
const notes = [];
|
|
@@ -5181,7 +5306,7 @@ var init_spacelift = __esm({
|
|
|
5181
5306
|
const own = isObject(raw) ? raw : {};
|
|
5182
5307
|
const s = { ...defaults, ...own };
|
|
5183
5308
|
const dir = repoJoin(typeof s.project_root === "string" ? s.project_root : ".");
|
|
5184
|
-
const project = projectAt(
|
|
5309
|
+
const project = projectAt(detected2, dir);
|
|
5185
5310
|
if (!project) {
|
|
5186
5311
|
unsupported.push({ file, setting: `stacks.${id}.project_root`, why: `${dir} holds no Pulumi.yaml` });
|
|
5187
5312
|
continue;
|
|
@@ -5230,9 +5355,9 @@ function parseFrom(value) {
|
|
|
5230
5355
|
if (!source) return { error: `--from ${value} is not one of ${IMPORT_SOURCES.join(", ")}, and its path names none; give <source>=${value}` };
|
|
5231
5356
|
return { source, files: [file] };
|
|
5232
5357
|
}
|
|
5233
|
-
function runImport(root,
|
|
5358
|
+
function runImport(root, detected2, from) {
|
|
5234
5359
|
try {
|
|
5235
|
-
return IMPORTERS[from.source](root,
|
|
5360
|
+
return IMPORTERS[from.source](root, detected2, from.files);
|
|
5236
5361
|
} catch (e) {
|
|
5237
5362
|
const file = from.files[0] ?? "(its files)";
|
|
5238
5363
|
return { source: from.source, files: [], keys: {}, unsupported: [{ file, setting: "(file)", why: `could not be read: ${e.message}` }], notes: [] };
|
|
@@ -5327,7 +5452,7 @@ function put(root, rel, text, dryRun, note) {
|
|
|
5327
5452
|
function existingConfig(root) {
|
|
5328
5453
|
return CONFIG_NAMES.find((n) => existsSync10(join13(root, n)));
|
|
5329
5454
|
}
|
|
5330
|
-
async function newConfigKeys(
|
|
5455
|
+
async function newConfigKeys(detected2, opts, io) {
|
|
5331
5456
|
const notes = [];
|
|
5332
5457
|
let presetName = opts.preset;
|
|
5333
5458
|
if (!presetName && io.ask && !opts.yes) {
|
|
@@ -5337,31 +5462,31 @@ async function newConfigKeys(detected3, opts, io) {
|
|
|
5337
5462
|
const preset2 = presetByName(presetName ?? DEFAULT_PRESET);
|
|
5338
5463
|
const keys = structuredClone(preset2.keys);
|
|
5339
5464
|
if (opts.gate) keys.gate = opts.gate;
|
|
5340
|
-
const engine2 = engineStep({ engine: opts.engine, detected:
|
|
5465
|
+
const engine2 = engineStep({ engine: opts.engine, detected: detected2, env: io.env });
|
|
5341
5466
|
Object.assign(keys, engine2.keys);
|
|
5342
5467
|
notes.push(...engine2.notes);
|
|
5343
5468
|
const engineBackends = engine2.keys.backend !== void 0 || engine2.keys.stacks !== void 0;
|
|
5344
5469
|
let forge;
|
|
5345
5470
|
if (opts.forge) {
|
|
5346
5471
|
forge = { kind: opts.forge, because: "--forge" };
|
|
5347
|
-
if (
|
|
5348
|
-
else if (
|
|
5349
|
-
} else if (
|
|
5350
|
-
forge = { kind:
|
|
5351
|
-
if (
|
|
5472
|
+
if (detected2.forge?.kind !== opts.forge) keys.forge = { kind: opts.forge };
|
|
5473
|
+
else if (detected2.forge.url) keys.forge = { kind: opts.forge, url: detected2.forge.url };
|
|
5474
|
+
} else if (detected2.forge) {
|
|
5475
|
+
forge = { kind: detected2.forge.kind, because: detected2.forge.because };
|
|
5476
|
+
if (detected2.forge.url) keys.forge = { kind: detected2.forge.kind, url: detected2.forge.url };
|
|
5352
5477
|
} else {
|
|
5353
5478
|
const kind = io.ask && !opts.yes ? await io.ask("Forge", FORGES, "github") : "github";
|
|
5354
5479
|
forge = { kind, because: io.ask && !opts.yes ? "you chose it" : "origin names no forge init knows, so github; change forge.kind in plywood.yml" };
|
|
5355
5480
|
keys.forge = { kind };
|
|
5356
5481
|
}
|
|
5357
5482
|
const fromEnv = io.env.PULUMI_BACKEND_URL?.trim();
|
|
5358
|
-
if (fromEnv && !engineBackends &&
|
|
5483
|
+
if (fromEnv && !engineBackends && detected2.projects.some((p) => !p.backendUrl)) {
|
|
5359
5484
|
keys.backend = fromEnv;
|
|
5360
5485
|
notes.push(`backend ${fromEnv} came from PULUMI_BACKEND_URL; it is in plywood.yml so the jobs use it too`);
|
|
5361
5486
|
}
|
|
5362
5487
|
return { keys, notes, forge };
|
|
5363
5488
|
}
|
|
5364
|
-
function initStacks(
|
|
5489
|
+
function initStacks(detected2, doc, members = []) {
|
|
5365
5490
|
const listed = stacksOf(doc);
|
|
5366
5491
|
const pinned = new Map(members.filter((m) => m.engineBin && m.engineVersion).map((m) => [m.name, m]));
|
|
5367
5492
|
const out = [];
|
|
@@ -5370,6 +5495,7 @@ function initStacks(detected3, doc, members = []) {
|
|
|
5370
5495
|
const own = listed[key] ?? {};
|
|
5371
5496
|
const backend = own.backend ?? (typeof doc.backend === "string" ? doc.backend : void 0);
|
|
5372
5497
|
const engine2 = engineOf(doc, backend)?.engine;
|
|
5498
|
+
const grain = lockGrainFor(doc, engine2);
|
|
5373
5499
|
const stackDir = own.dir ?? dir;
|
|
5374
5500
|
const packs = packsForStack(stepPolicyPacksOf(doc), stackDir);
|
|
5375
5501
|
const pin = pinned.get(key);
|
|
@@ -5379,13 +5505,18 @@ function initStacks(detected3, doc, members = []) {
|
|
|
5379
5505
|
stack,
|
|
5380
5506
|
dir: stackDir,
|
|
5381
5507
|
...engine2 ? { engine: engine2 } : {},
|
|
5508
|
+
// plywood#22: the step's backend, so a preview and an update read this stack's store, not the job's PULUMI_BACKEND_URL.
|
|
5509
|
+
...backend ? { backend } : {},
|
|
5382
5510
|
...pin ? { engineVersion: pin.engineVersion, ...pin.engineRoot ? { engineRoot: pin.engineRoot } : {} } : {},
|
|
5383
5511
|
...own.gate ? { gate: own.gate } : {},
|
|
5384
|
-
...packs.length ? { policyPacks: packs } : {}
|
|
5512
|
+
...packs.length ? { policyPacks: packs } : {},
|
|
5513
|
+
// chant's ledger lock (ruling for ci-products 2026-10-28): each stack's grain on its step, so a plunge stack on the
|
|
5514
|
+
// resource grain leases its resource rows and never waits, and a stock stack (or locks.grain: stack) waits for its stack row.
|
|
5515
|
+
...grain !== "per-engine" ? { lockGrain: LOCK_GRAIN_OF[grain] } : {}
|
|
5385
5516
|
});
|
|
5386
5517
|
seen.add(key);
|
|
5387
5518
|
};
|
|
5388
|
-
for (const s of
|
|
5519
|
+
for (const s of detected2.stacks) add(s.key, s.project, s.stack, s.dir);
|
|
5389
5520
|
for (const [key, own] of Object.entries(listed)) {
|
|
5390
5521
|
if (seen.has(key)) continue;
|
|
5391
5522
|
const [project, stack] = key.split("/");
|
|
@@ -5393,11 +5524,11 @@ function initStacks(detected3, doc, members = []) {
|
|
|
5393
5524
|
}
|
|
5394
5525
|
return out;
|
|
5395
5526
|
}
|
|
5396
|
-
function forgeFor2(doc,
|
|
5527
|
+
function forgeFor2(doc, detected2, opts) {
|
|
5397
5528
|
if (opts.forge) return { kind: opts.forge, because: "--forge" };
|
|
5398
5529
|
const kind = isObject(doc.forge) ? doc.forge.kind : void 0;
|
|
5399
5530
|
if (kind) return { kind, because: "plywood.yml says forge.kind" };
|
|
5400
|
-
if (
|
|
5531
|
+
if (detected2.forge) return { kind: detected2.forge.kind, because: detected2.forge.because };
|
|
5401
5532
|
return { kind: "github", because: "origin names no forge init knows, so github" };
|
|
5402
5533
|
}
|
|
5403
5534
|
async function chantWriteCiHook() {
|
|
@@ -5409,14 +5540,14 @@ async function chantWriteCiHook() {
|
|
|
5409
5540
|
}
|
|
5410
5541
|
}
|
|
5411
5542
|
async function runInit(opts, table, io) {
|
|
5412
|
-
const
|
|
5413
|
-
const { root } =
|
|
5543
|
+
const detected2 = detect(io.cwd);
|
|
5544
|
+
const { root } = detected2;
|
|
5414
5545
|
const files = [];
|
|
5415
5546
|
const notes = [];
|
|
5416
5547
|
const todo = [];
|
|
5417
5548
|
const buckets = [];
|
|
5418
5549
|
const adopt = [];
|
|
5419
|
-
const imported = opts.from ? runImport(root,
|
|
5550
|
+
const imported = opts.from ? runImport(root, detected2, opts.from) : void 0;
|
|
5420
5551
|
if (imported && imported.files.length === 0) notes.push(`--from ${imported.source} found no file to read${imported.notes.length ? `: ${imported.notes.join("; ")}` : ""}`);
|
|
5421
5552
|
const fail = (message, forge2 = { kind: "github", because: "" }) => ({
|
|
5422
5553
|
ok: false,
|
|
@@ -5431,8 +5562,8 @@ async function runInit(opts, table, io) {
|
|
|
5431
5562
|
adopt,
|
|
5432
5563
|
...imported ? { imported } : {}
|
|
5433
5564
|
});
|
|
5434
|
-
if (
|
|
5435
|
-
if (
|
|
5565
|
+
if (detected2.projects.length === 0) return fail(`no Pulumi.yaml under ${root}; plywood init sets up a repository that has a Pulumi program`);
|
|
5566
|
+
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");
|
|
5436
5567
|
let config2;
|
|
5437
5568
|
let forge;
|
|
5438
5569
|
const kept = existingConfig(root);
|
|
@@ -5446,10 +5577,10 @@ async function runInit(opts, table, io) {
|
|
|
5446
5577
|
const onPlunge = opts.engine === "plunge" && engineOf(config2.doc)?.engine === "plunge";
|
|
5447
5578
|
const ignored = [opts.preset && "--preset", opts.gate && "--gate", opts.engine && !onPlunge && "--engine", imported && "--from"].filter(Boolean);
|
|
5448
5579
|
if (ignored.length) notes.push(`${kept} is kept as it is, so ${ignored.join(", ")} changed nothing; set the keys there`);
|
|
5449
|
-
if (opts.engine === "plunge" && !onPlunge) notes.push(...keptPlungeLines(kept,
|
|
5450
|
-
forge = forgeFor2(config2.doc,
|
|
5580
|
+
if (opts.engine === "plunge" && !onPlunge) notes.push(...keptPlungeLines(kept, detected2, io.env));
|
|
5581
|
+
forge = forgeFor2(config2.doc, detected2, opts);
|
|
5451
5582
|
} else {
|
|
5452
|
-
const made = await newConfigKeys(
|
|
5583
|
+
const made = await newConfigKeys(detected2, opts, io);
|
|
5453
5584
|
notes.push(...made.notes);
|
|
5454
5585
|
forge = made.forge;
|
|
5455
5586
|
const text = plywoodYml(imported ? mergeKeys(made.keys, imported.keys) : made.keys);
|
|
@@ -5465,12 +5596,12 @@ async function runInit(opts, table, io) {
|
|
|
5465
5596
|
const reviewStacks2 = config2.doc.review_stacks !== void 0 ? await chantReviewStacks() : false;
|
|
5466
5597
|
if (settings.ci.prEnvironment && !reviewStacks2) delete settings.ci.prEnvironment;
|
|
5467
5598
|
const reviewSuffix = reviewStacks2 ? reviewSuffixOf(settings.pipeline.reviewStackName) : void 0;
|
|
5468
|
-
const stacks2 = initStacks(
|
|
5599
|
+
const stacks2 = initStacks(detected2, doc, settings.members);
|
|
5469
5600
|
const order = await stackOrder(root, stacks2, settings.pipeline.after);
|
|
5470
5601
|
const ordered = orderWaves(wavesOf(stacks2, settings.pipeline.waves), order.edges, settings.pipeline.canary);
|
|
5471
5602
|
const waves = ordered.waves;
|
|
5472
5603
|
if (order.edges.length || order.outside.length || ordered.unmet.length || !order.reads && stacks2.length > 1) notes.push(...orderLines(order, ordered.unmet));
|
|
5473
|
-
const declaration = declarationStep(root,
|
|
5604
|
+
const declaration = declarationStep(root, detected2, settings, opts.dryRun);
|
|
5474
5605
|
files.push(declaration.file);
|
|
5475
5606
|
if (existsSync10(join13(root, CHANT_CONFIG_FILE))) {
|
|
5476
5607
|
const text = readFileSync12(join13(root, CHANT_CONFIG_FILE), "utf8");
|
|
@@ -5480,8 +5611,8 @@ async function runInit(opts, table, io) {
|
|
|
5480
5611
|
} else files.push(put(root, CHANT_CONFIG_FILE, chantConfigTs(), opts.dryRun));
|
|
5481
5612
|
files.push(put(root, COMPONENTS_FILE, componentsFile(stacks2, settings.pipeline.after, reviewSuffix ? { reviewSuffix } : {}), opts.dryRun));
|
|
5482
5613
|
files.push(...providersPresetStep(root, doc, opts.dryRun, notes));
|
|
5483
|
-
if (
|
|
5484
|
-
for (const wf of
|
|
5614
|
+
if (detected2.preCommit) files.push(await preCommitStep(root, detected2.preCommit, opts.dryRun));
|
|
5615
|
+
for (const wf of detected2.pulumiActions) {
|
|
5485
5616
|
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`);
|
|
5486
5617
|
}
|
|
5487
5618
|
const chant = await chantProduct();
|
|
@@ -5489,7 +5620,7 @@ async function runInit(opts, table, io) {
|
|
|
5489
5620
|
const message = needsChantMessage("Writing the waves spec and the workflow");
|
|
5490
5621
|
files.push({ file: WAVES_SPEC_FILE2, status: "skipped", note: message }, { file: WORKFLOW_FILE[forge.kind], status: "skipped", note: "needs the same chant" });
|
|
5491
5622
|
} else {
|
|
5492
|
-
let spec = wavesSpecStep(chant, stacks2, doc, settings,
|
|
5623
|
+
let spec = wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves);
|
|
5493
5624
|
if (settings.pipeline.jobs) {
|
|
5494
5625
|
try {
|
|
5495
5626
|
spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind, { parallelism: await hasChant4625() });
|
|
@@ -5498,12 +5629,15 @@ async function runInit(opts, table, io) {
|
|
|
5498
5629
|
spec = void 0;
|
|
5499
5630
|
}
|
|
5500
5631
|
}
|
|
5501
|
-
if (spec && stacks2.length > 0)
|
|
5632
|
+
if (spec && stacks2.length > 0) {
|
|
5633
|
+
files.push(put(root, WAVES_SPEC_FILE2, wavesSpecText(spec, wavesEngine(settings.pipeline)), opts.dryRun));
|
|
5634
|
+
files.push(put(root, UP_OP_FILE, upOpFile(spec), opts.dryRun));
|
|
5635
|
+
}
|
|
5502
5636
|
if (spec) files.push(...await workflowStep(chant, root, doc, settings, forge.kind, opts, stacks2.length > 0 ? spec : void 0, table, io));
|
|
5503
5637
|
}
|
|
5504
|
-
if (opts.bucketCheck) notes.push(...await bucketStep(
|
|
5505
|
-
adopt.push(...await adoptStep(root,
|
|
5506
|
-
todo.push(...todoLines(
|
|
5638
|
+
if (opts.bucketCheck) notes.push(...await bucketStep(detected2, doc, io.env, buckets));
|
|
5639
|
+
adopt.push(...await adoptStep(root, detected2, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
|
|
5640
|
+
todo.push(...todoLines(detected2, doc, forge.kind, io.env));
|
|
5507
5641
|
const failed = files.find((f) => f.status === "failed");
|
|
5508
5642
|
return {
|
|
5509
5643
|
ok: !failed,
|
|
@@ -5519,9 +5653,9 @@ async function runInit(opts, table, io) {
|
|
|
5519
5653
|
...imported ? { imported } : {}
|
|
5520
5654
|
};
|
|
5521
5655
|
}
|
|
5522
|
-
function declarationStep(root,
|
|
5656
|
+
function declarationStep(root, detected2, settings, dryRun) {
|
|
5523
5657
|
if (existsSync10(join13(root, "chant.workspace.jsonc"))) {
|
|
5524
|
-
return { file: { file: "chant.workspace.jsonc", status: "kept", note: `add a member of kind pulumi for each of ${
|
|
5658
|
+
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}` } };
|
|
5525
5659
|
}
|
|
5526
5660
|
let existing;
|
|
5527
5661
|
const file = join13(root, DECLARATION_FILE);
|
|
@@ -5534,7 +5668,7 @@ function declarationStep(root, detected3, settings, dryRun) {
|
|
|
5534
5668
|
return { file: { file: DECLARATION_FILE, status: "failed", note: `is not JSON init can merge into (${e.message})` } };
|
|
5535
5669
|
}
|
|
5536
5670
|
}
|
|
5537
|
-
const doc = workspaceDeclaration(existing, { name: repoName(root), projects:
|
|
5671
|
+
const doc = workspaceDeclaration(existing, { name: repoName(root), projects: detected2.projects, ci: settings.ci, lexiconVersion: CHANT_PIN });
|
|
5538
5672
|
if (existing && JSON.stringify(existing) === JSON.stringify(doc)) return { file: { file: DECLARATION_FILE, status: "unchanged" }, doc };
|
|
5539
5673
|
return { file: put(root, DECLARATION_FILE, jsonText(doc), dryRun), doc };
|
|
5540
5674
|
}
|
|
@@ -5589,7 +5723,7 @@ async function workflowStep(chant, root, doc, settings, forge, opts, spec, table
|
|
|
5589
5723
|
try {
|
|
5590
5724
|
const { noteControlTarget: noteControlTarget2 } = await Promise.resolve().then(() => (init_control2(), control_exports));
|
|
5591
5725
|
const control3 = await noteControlTarget2(doc, root);
|
|
5592
|
-
plan = workflowPlan(chant, doc, forge, opts.env, spec, chantBuildFlags(chantPipelineOptions(settings.pipeline, false)), { reviewStacks: await chantReviewStacks(), prJobs,
|
|
5726
|
+
plan = workflowPlan(chant, doc, forge, opts.env, spec, chantBuildFlags(chantPipelineOptions(settings.pipeline, false)), { reviewStacks: await chantReviewStacks(), prJobs, ...control3 ? { control: control3 } : {} });
|
|
5593
5727
|
} catch (e) {
|
|
5594
5728
|
return [{ file: output, status: "failed", note: e.message }];
|
|
5595
5729
|
}
|
|
@@ -5606,16 +5740,16 @@ async function workflowStep(chant, root, doc, settings, forge, opts, spec, table
|
|
|
5606
5740
|
];
|
|
5607
5741
|
const out = [{ file: output, status: written.status, ...notes.length ? { note: notes.join("; ") } : {} }];
|
|
5608
5742
|
if (plan.jobs.waves) {
|
|
5609
|
-
const code = await writeWaveJobs(chant, forge, plan.jobs.waves, plan.words, table, { ...io, cwd: root });
|
|
5743
|
+
const code = await writeWaveJobs(chant, forge, plan.jobs.waves, plan.words, table, { ...io, cwd: root }, wavesEngine(settings.pipeline));
|
|
5610
5744
|
if (code !== table.done) out.push({ file: WAVES_OUTPUT_DIR[forge], status: "failed", note: `the waves workflow: chant exited ${code}` });
|
|
5611
5745
|
}
|
|
5612
5746
|
return out;
|
|
5613
5747
|
}
|
|
5614
|
-
async function bucketStep(
|
|
5748
|
+
async function bucketStep(detected2, doc, env, reports) {
|
|
5615
5749
|
const listed = stacksOf(doc);
|
|
5616
5750
|
const urls = /* @__PURE__ */ new Set();
|
|
5617
|
-
for (const s of
|
|
5618
|
-
const project =
|
|
5751
|
+
for (const s of detected2.stacks) {
|
|
5752
|
+
const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
|
|
5619
5753
|
const b = backendOf(project, { ...listed[s.key]?.backend ? { stack: listed[s.key].backend } : {}, ...typeof doc.backend === "string" ? { all: doc.backend } : {} }, env);
|
|
5620
5754
|
if (b.url && !isPulumiCloud(b.url)) urls.add(b.url);
|
|
5621
5755
|
}
|
|
@@ -5639,12 +5773,12 @@ async function bucketStep(detected3, doc, env, reports) {
|
|
|
5639
5773
|
}
|
|
5640
5774
|
return lines2;
|
|
5641
5775
|
}
|
|
5642
|
-
async function adoptStep(root,
|
|
5776
|
+
async function adoptStep(root, detected2, doc, env, declaration, dryRun, files, notes, todo) {
|
|
5643
5777
|
const listed = stacksOf(doc);
|
|
5644
5778
|
const all = typeof doc.backend === "string" ? doc.backend : void 0;
|
|
5645
5779
|
const targets = [];
|
|
5646
|
-
for (const s of
|
|
5647
|
-
const project =
|
|
5780
|
+
for (const s of detected2.stacks) {
|
|
5781
|
+
const project = detected2.projects.find((p) => p.dir === s.dir && p.name === s.project) ?? { backendUrl: void 0 };
|
|
5648
5782
|
const own = listed[s.key];
|
|
5649
5783
|
const b = backendOf(project, { ...own?.backend ? { stack: own.backend } : {}, ...all ? { all } : {} }, env);
|
|
5650
5784
|
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 });
|
|
@@ -5667,17 +5801,17 @@ async function adoptStep(root, detected3, doc, env, declaration, dryRun, files,
|
|
|
5667
5801
|
todo.push(...adoptTodo(rows));
|
|
5668
5802
|
return previews;
|
|
5669
5803
|
}
|
|
5670
|
-
function keptPlungeLines(kept,
|
|
5671
|
-
const step = engineStep({ engine: "plunge", detected:
|
|
5804
|
+
function keptPlungeLines(kept, detected2, env) {
|
|
5805
|
+
const step = engineStep({ engine: "plunge", detected: detected2, env });
|
|
5672
5806
|
const entries = Object.entries(step.keys);
|
|
5673
5807
|
if (entries.length === 0) return step.notes;
|
|
5674
5808
|
const yaml = entries.map(([k, v]) => emitYAMLEntry2(k, v)).join("\n").split("\n").filter((l) => l !== "");
|
|
5675
5809
|
return [...step.notes, `To move to plunge, set these keys in ${kept}:`, ...yaml.map((l) => ` ${l}`)];
|
|
5676
5810
|
}
|
|
5677
|
-
function todoLines(
|
|
5811
|
+
function todoLines(detected2, doc, forge, env) {
|
|
5678
5812
|
const out = [];
|
|
5679
5813
|
const all = typeof doc.backend === "string" ? doc.backend : void 0;
|
|
5680
|
-
const cloud =
|
|
5814
|
+
const cloud = detected2.projects.some((p) => isPulumiCloud(backendOf(p, all ? { all } : {}, env).url));
|
|
5681
5815
|
const where = forge === "gitlab" ? "CI/CD variables" : "repository secrets";
|
|
5682
5816
|
const traded = identityClouds(doc);
|
|
5683
5817
|
if (cloud && traded.includes("pulumi")) {
|
|
@@ -5685,9 +5819,9 @@ function todoLines(detected3, doc, forge, env) {
|
|
|
5685
5819
|
} else if (cloud) {
|
|
5686
5820
|
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}`);
|
|
5687
5821
|
}
|
|
5688
|
-
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 ${
|
|
5822
|
+
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`);
|
|
5689
5823
|
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");
|
|
5690
|
-
out.push(`Make ${PRODUCT}/preview a required status on ${
|
|
5824
|
+
out.push(`Make ${PRODUCT}/preview a required status on ${detected2.defaultBranch}`);
|
|
5691
5825
|
return out;
|
|
5692
5826
|
}
|
|
5693
5827
|
function initLines(r) {
|
|
@@ -5732,6 +5866,7 @@ var init_init = __esm({
|
|
|
5732
5866
|
init_policy_presets();
|
|
5733
5867
|
init_approval();
|
|
5734
5868
|
init_engine();
|
|
5869
|
+
init_locks();
|
|
5735
5870
|
init_config();
|
|
5736
5871
|
init_routing();
|
|
5737
5872
|
init_stacks();
|
|
@@ -5748,7 +5883,6 @@ var init_init = __esm({
|
|
|
5748
5883
|
init_review_stacks2();
|
|
5749
5884
|
init_waves_workflow();
|
|
5750
5885
|
init_stages();
|
|
5751
|
-
init_chant_next();
|
|
5752
5886
|
init_stack_order();
|
|
5753
5887
|
init_import();
|
|
5754
5888
|
init_templates();
|
|
@@ -6132,7 +6266,6 @@ async function check(args, json, table, io) {
|
|
|
6132
6266
|
}
|
|
6133
6267
|
async function preview(args, json, table, io) {
|
|
6134
6268
|
const targeted = args.includes("--target");
|
|
6135
|
-
const planFlags = ["--refresh", "--target-dependents"].filter((f) => args.includes(f));
|
|
6136
6269
|
if (args.includes("--target-dependents") && !targeted) return usage("--target-dependents widens a --target preview to the resources that depend on the targets; give a --target", table, io);
|
|
6137
6270
|
for (let i = 0; i < args.length; i++) {
|
|
6138
6271
|
if (args[i] === "--target" && (args[i + 1] === void 0 || args[i + 1].startsWith("-"))) return usage("--target takes a resource's URN", table, io);
|
|
@@ -6155,16 +6288,12 @@ async function preview(args, json, table, io) {
|
|
|
6155
6288
|
const argv = ["components", "pr-plan", "--base", at, "--pr", number, ...gate, ...stack ? ["--member", stack] : [], ...rest, ...json ? ["--json"] : []];
|
|
6156
6289
|
return withChant(`${PRODUCT} preview`, table, io, async (chant) => {
|
|
6157
6290
|
if (targeted && !hasChant4620(chant)) return usage(PREVIEW_TARGET_NEEDS_CHANT, table, io);
|
|
6158
|
-
if (planFlags.length && !(await chantNext()).planFlags) return usage(previewFlagsNeedChant(planFlags), table, io);
|
|
6159
6291
|
return chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
6160
6292
|
});
|
|
6161
6293
|
}
|
|
6162
|
-
function previewFlagsNeedChant(flags) {
|
|
6163
|
-
return `${flags.join(" and ")} ${flags.length === 1 ? "needs" : "need"} a chant whose pr-plan passes ${flags.length === 1 ? "it" : "them"} to the pulumi-up step (chant#4627), which this chant lacks`;
|
|
6164
|
-
}
|
|
6165
6294
|
function readWavesSpec(cwd) {
|
|
6166
6295
|
const file = join15(cwd, ".chant/op-waves", `${OP_NAME}.spec.json`);
|
|
6167
|
-
return existsSync12(file) ? JSON.parse(readFileSync14(file, "utf8")) : void 0;
|
|
6296
|
+
return existsSync12(file) ? unwrapWavesSpec(JSON.parse(readFileSync14(file, "utf8"))) : void 0;
|
|
6168
6297
|
}
|
|
6169
6298
|
function pipelineShape(doc, forge, waves) {
|
|
6170
6299
|
const applyBeforeMerge = applyWhenOf(doc) === "pull-request";
|
|
@@ -6225,7 +6354,7 @@ async function ci(args, json, table, io) {
|
|
|
6225
6354
|
const noteControl = await noteControlTarget2(config2.doc, io.cwd);
|
|
6226
6355
|
let plan;
|
|
6227
6356
|
try {
|
|
6228
|
-
plan = workflowPlan(chant, config2.doc, forge, env, waves, [], { reviewStacks: reviewStacks2, prJobs,
|
|
6357
|
+
plan = workflowPlan(chant, config2.doc, forge, env, waves, [], { reviewStacks: reviewStacks2, prJobs, ...noteControl ? { control: noteControl } : {} });
|
|
6229
6358
|
} catch (e) {
|
|
6230
6359
|
io.err(`${PRODUCT}: ${e.message}`);
|
|
6231
6360
|
return table.failed;
|
|
@@ -6249,7 +6378,8 @@ async function ci(args, json, table, io) {
|
|
|
6249
6378
|
...jobs2.applyBeforeMerge ? { applyBeforeMerge: jobs2.applyBeforeMerge.command } : {},
|
|
6250
6379
|
comments: jobs2.comments,
|
|
6251
6380
|
...jobs2.waves ? { waves: { specFile: jobs2.waves.specFile, jobs: jobs2.waves.jobs.map((j) => j.command.join(" ")) } } : {},
|
|
6252
|
-
..."control" in control3 ? { control: control3.repo } : {}
|
|
6381
|
+
..."control" in control3 ? { control: control3.repo } : {},
|
|
6382
|
+
...jobs2.action ? { action: { command: jobs2.action.command, comment: jobs2.action.comment, inputs: jobs2.action.inputs } } : {}
|
|
6253
6383
|
};
|
|
6254
6384
|
if (json && dryRun) io.out(JSON.stringify({ command: "ci", ok: true, exitCode: table.done, dryRun, ...summary }));
|
|
6255
6385
|
else if (!json) {
|
|
@@ -6262,6 +6392,7 @@ async function ci(args, json, table, io) {
|
|
|
6262
6392
|
for (const [command, line] of Object.entries(jobs2.comments)) io.err(` comment ${line} (${command})`);
|
|
6263
6393
|
for (const job of summary.waves?.jobs ?? []) io.err(` wave ${job}`);
|
|
6264
6394
|
if (jobs2.stale) io.err(` stale ${jobs2.stale.command} (on each push to the target branch)`);
|
|
6395
|
+
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})`);
|
|
6265
6396
|
}
|
|
6266
6397
|
if (!json && "control" in control3) io.err(` control after the last wave, tells ${control3.repo} how the waves ended`);
|
|
6267
6398
|
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'`);
|
|
@@ -6274,7 +6405,7 @@ async function ci(args, json, table, io) {
|
|
|
6274
6405
|
if (!json) io.err(`${PRODUCT} ci: chant's generator exited ${written.code} and wrote no ${output}; any file there before is back as it was`);
|
|
6275
6406
|
return written.code;
|
|
6276
6407
|
}
|
|
6277
|
-
const wavesCode = waveJobs ? await writeWaveJobs(chant, forge, waveJobs, plan.words, table, io) : table.done;
|
|
6408
|
+
const wavesCode = waveJobs ? await writeWaveJobs(chant, forge, waveJobs, plan.words, table, io, wavesEngine(toChantSettings(config2).pipeline)) : table.done;
|
|
6278
6409
|
if (wavesCode !== table.done) return wavesCode;
|
|
6279
6410
|
const driftCode = await writeDriftJobs(chant, forge, config2.doc, plan.words, table, io, env);
|
|
6280
6411
|
if (driftCode !== table.done) return driftCode;
|
|
@@ -6289,14 +6420,14 @@ function writtenNotes(plan, file) {
|
|
|
6289
6420
|
}
|
|
6290
6421
|
function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
|
|
6291
6422
|
const shape = pipelineShape(doc, forge, waves);
|
|
6292
|
-
const next = opts.next;
|
|
6293
6423
|
const notes = [];
|
|
6294
6424
|
const pipeline = toChantSettings({ doc }).pipeline;
|
|
6295
|
-
const staleNotes = next?.staleNotes === true;
|
|
6296
6425
|
const respond = respondOptions(chant, pipeline, shape.applyBeforeMerge, notes);
|
|
6297
|
-
const applyMerge = shape.applyBeforeMerge
|
|
6426
|
+
const applyMerge = shape.applyBeforeMerge ? pipeline.applyMerge : void 0;
|
|
6298
6427
|
if (approvalOf(doc) === "pr-review" && shape.waves && hasChantApprovalStatus(chant) && !hasChantPullRequestApprovalStatus(chant)) notes.push(APPROVAL_STATUS_NOTE);
|
|
6299
|
-
const
|
|
6428
|
+
const actionJob = actionJobFor(doc, forge, notes);
|
|
6429
|
+
const jobsWanted = actionJob ? [...shape.jobs, "action"] : shape.jobs;
|
|
6430
|
+
const words = wordsForJobs(jobsWanted);
|
|
6300
6431
|
const requires = shape.applyBeforeMerge ? requiresArgv(chant, doc) : [];
|
|
6301
6432
|
const previewComment = hasChant4620(chant);
|
|
6302
6433
|
const jobs2 = chant.productPipelineJobs(forge, env, {
|
|
@@ -6310,11 +6441,13 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
|
|
|
6310
6441
|
...opts.control !== void 0 ? { control: { target: opts.control } } : {},
|
|
6311
6442
|
// plywood#29: runners, env, pass and parallelism on the pull request jobs, with chant#4625 (src/jobs.ts).
|
|
6312
6443
|
...opts.prJobs?.options ?? {},
|
|
6313
|
-
|
|
6444
|
+
// chant#4628: the note names the target commit it was previewed against, and a push marks older notes stale.
|
|
6445
|
+
staleNotes: true,
|
|
6314
6446
|
...respond.options,
|
|
6315
|
-
...applyMerge ? { applyMerge } : {}
|
|
6447
|
+
...applyMerge ? { applyMerge } : {},
|
|
6448
|
+
...actionJob ? { actionJob: true } : {}
|
|
6316
6449
|
});
|
|
6317
|
-
const prWords = shape.applyBeforeMerge ? words : wordsForJobs(
|
|
6450
|
+
const prWords = shape.applyBeforeMerge ? words : wordsForJobs(jobsWanted.filter((j) => j !== "apply"));
|
|
6318
6451
|
const argv = [
|
|
6319
6452
|
"build",
|
|
6320
6453
|
".",
|
|
@@ -6330,7 +6463,8 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
|
|
|
6330
6463
|
...applyMerge ? applyMergeArgv(applyMerge) : [],
|
|
6331
6464
|
...respond.argv,
|
|
6332
6465
|
...previewComment ? ["--preview-comment"] : [],
|
|
6333
|
-
|
|
6466
|
+
"--stale-notes",
|
|
6467
|
+
...actionJob ? ["--action-job"] : [],
|
|
6334
6468
|
// Review stacks (plywood#15): chant's pr-env jobs, only once the pulumi lexicon can remove what they create.
|
|
6335
6469
|
...doc.review_stacks !== void 0 && opts.reviewStacks ? ["--pr-env"] : [],
|
|
6336
6470
|
...opts.prJobs?.flags ?? [],
|
|
@@ -6338,6 +6472,11 @@ function workflowPlan(chant, doc, forge, env, waves, extra = [], opts = {}) {
|
|
|
6338
6472
|
];
|
|
6339
6473
|
return { jobs: jobs2, words, prWords, argv, notes, ...applyMerge ? { applyMerge } : {} };
|
|
6340
6474
|
}
|
|
6475
|
+
function actionJobFor(doc, forge, notes) {
|
|
6476
|
+
if (!hasActions(doc)) return false;
|
|
6477
|
+
if (forge === "gitlab") return notes.push(ACTIONS_NOT_ON_GITLAB), false;
|
|
6478
|
+
return true;
|
|
6479
|
+
}
|
|
6341
6480
|
function applyMergeArgv(m) {
|
|
6342
6481
|
return ["--merge", "auto", "--merge-token-env", m.tokenEnv, ...m.method ? ["--merge-method", m.method] : []];
|
|
6343
6482
|
}
|
|
@@ -6387,7 +6526,7 @@ function usage(message, table, io) {
|
|
|
6387
6526
|
io.err(`${PRODUCT}: ${message}`);
|
|
6388
6527
|
return table.failed;
|
|
6389
6528
|
}
|
|
6390
|
-
var CHANT_PASSTHROUGH, VALUED, PREVIEW_TARGET_NEEDS_CHANT, WORKFLOW_FILE,
|
|
6529
|
+
var CHANT_PASSTHROUGH, VALUED, PREVIEW_TARGET_NEEDS_CHANT, WORKFLOW_FILE, ACTIONS_NOT_ON_GITLAB, APPROVAL_STATUS_NOTE, CHANT_RESPOND_DEFAULTS, DECIDE_LEFT_OFF;
|
|
6391
6530
|
var init_stages = __esm({
|
|
6392
6531
|
"src/stages.ts"() {
|
|
6393
6532
|
"use strict";
|
|
@@ -6399,6 +6538,7 @@ var init_stages = __esm({
|
|
|
6399
6538
|
init_apply();
|
|
6400
6539
|
init_config();
|
|
6401
6540
|
init_locks();
|
|
6541
|
+
init_stacks();
|
|
6402
6542
|
init_routing();
|
|
6403
6543
|
init_workflow();
|
|
6404
6544
|
init_review_stacks2();
|
|
@@ -6415,13 +6555,38 @@ var init_stages = __esm({
|
|
|
6415
6555
|
forgejo: ".forgejo/workflows/plywood.yml",
|
|
6416
6556
|
gitlab: ".gitlab-ci.yml"
|
|
6417
6557
|
};
|
|
6418
|
-
|
|
6558
|
+
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";
|
|
6419
6559
|
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`;
|
|
6420
6560
|
CHANT_RESPOND_DEFAULTS = Object.freeze({ "wave-refused": "diff", "apply-failed": "triage", description: "off" });
|
|
6421
6561
|
DECIDE_LEFT_OFF = "decide is set and respond.description is not check, so plywood leaves the decide service off the pull request jobs";
|
|
6422
6562
|
}
|
|
6423
6563
|
});
|
|
6424
6564
|
|
|
6565
|
+
// src/chant-resolve.ts
|
|
6566
|
+
import * as nodeModule from "node:module";
|
|
6567
|
+
var CHANT_SPECIFIER = /^@intentius\/chant(?:-lexicon-[a-z0-9-]+)?(?:\/|$)/;
|
|
6568
|
+
function chantResolveHook(from) {
|
|
6569
|
+
return (specifier, context, next) => {
|
|
6570
|
+
try {
|
|
6571
|
+
return next(specifier, context);
|
|
6572
|
+
} catch (err) {
|
|
6573
|
+
const code = err.code;
|
|
6574
|
+
if (!CHANT_SPECIFIER.test(specifier) || code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") throw err;
|
|
6575
|
+
if (context.parentURL === from) throw err;
|
|
6576
|
+
return next(specifier, { ...context, parentURL: from });
|
|
6577
|
+
}
|
|
6578
|
+
};
|
|
6579
|
+
}
|
|
6580
|
+
var installed = false;
|
|
6581
|
+
function installChantResolution(from = import.meta.url) {
|
|
6582
|
+
if (installed) return true;
|
|
6583
|
+
const register = nodeModule.registerHooks;
|
|
6584
|
+
if (typeof register !== "function") return false;
|
|
6585
|
+
register({ resolve: chantResolveHook(from) });
|
|
6586
|
+
installed = true;
|
|
6587
|
+
return true;
|
|
6588
|
+
}
|
|
6589
|
+
|
|
6425
6590
|
// src/cli.ts
|
|
6426
6591
|
init_config_check();
|
|
6427
6592
|
|
|
@@ -6461,1100 +6626,1185 @@ function exitCodes() {
|
|
|
6461
6626
|
return cached;
|
|
6462
6627
|
}
|
|
6463
6628
|
|
|
6464
|
-
// src/
|
|
6629
|
+
// src/actions.ts
|
|
6630
|
+
init_chant_product();
|
|
6631
|
+
init_stacks();
|
|
6632
|
+
init_stages();
|
|
6633
|
+
|
|
6634
|
+
// src/stack-ops.ts
|
|
6635
|
+
init_chant_config();
|
|
6636
|
+
init_identity();
|
|
6637
|
+
init_config();
|
|
6638
|
+
init_stacks();
|
|
6639
|
+
init_adopt();
|
|
6465
6640
|
init_detect();
|
|
6641
|
+
init_refusals();
|
|
6466
6642
|
init_stages();
|
|
6643
|
+
init_chant_product();
|
|
6467
6644
|
init_words();
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6645
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
6646
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, join as join16, resolve as resolve2 } from "node:path";
|
|
6647
|
+
var STACK_OPS_OP = "pulumi-stack-ops";
|
|
6648
|
+
var REQUIRED4 = ["gatedStackExport", "gatedStackCancel", "gatedStackMove"];
|
|
6649
|
+
var cachedLocks;
|
|
6650
|
+
function chantStackLockReader() {
|
|
6651
|
+
cachedLocks ??= (async () => {
|
|
6652
|
+
try {
|
|
6653
|
+
const m = await import("@intentius/chant-lexicon-pulumi");
|
|
6654
|
+
return typeof m.pulumiStackLockIds === "function" ? m.pulumiStackLockIds : void 0;
|
|
6655
|
+
} catch {
|
|
6656
|
+
return void 0;
|
|
6657
|
+
}
|
|
6658
|
+
})();
|
|
6659
|
+
return cachedLocks;
|
|
6477
6660
|
}
|
|
6478
|
-
function
|
|
6479
|
-
|
|
6480
|
-
const
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
if (a === "--where") {
|
|
6488
|
-
const v = at.rest[i + 1];
|
|
6489
|
-
if (v === void 0 || v.startsWith("--")) return { error: "--where takes <field><op><value>, such as gate=wave-2" };
|
|
6490
|
-
wheres.push("--where", v);
|
|
6491
|
-
i++;
|
|
6492
|
-
} else rest.push(a);
|
|
6493
|
-
}
|
|
6494
|
-
const flags = rest.filter((a) => a.startsWith("-"));
|
|
6495
|
-
if (flags.length) return { error: `audit does not take ${flags.join(" ")}` };
|
|
6496
|
-
if (rest.length > 1) return { error: `audit reads one table: ${Object.keys(AUDIT_TABLES).join(", ")}, or a chant ledger table` };
|
|
6497
|
-
let member;
|
|
6498
|
-
if (stack.value !== void 0) {
|
|
6499
|
-
member = auditMember(stack.value);
|
|
6500
|
-
if (!member) return { error: `--stack takes project/stack, not '${stack.value}'` };
|
|
6661
|
+
async function lockToCancel(given, stack, env) {
|
|
6662
|
+
if (given) return { lockId: given, read: false };
|
|
6663
|
+
const reader = await chantStackLockReader();
|
|
6664
|
+
if (!reader) return { error: STACK_OP_WORDS.needsLock };
|
|
6665
|
+
let locks2;
|
|
6666
|
+
try {
|
|
6667
|
+
locks2 = await reader({ dir: stack.dir, stack: stack.stack, ...stack.project ? { project: stack.project } : {}, ...stack.engine ? { engine: stack.engine } : {}, env });
|
|
6668
|
+
} catch (e) {
|
|
6669
|
+
return { error: `${STACK_OP_WORDS.needsLock}. Reading the stack's lock failed: ${e.message}` };
|
|
6501
6670
|
}
|
|
6502
|
-
return
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
...since.value ? ["--since", since.value] : [],
|
|
6507
|
-
...wheres,
|
|
6508
|
-
...at.value ? ["--at", at.value] : [],
|
|
6509
|
-
...json ? ["--json"] : []
|
|
6510
|
-
];
|
|
6511
|
-
}
|
|
6512
|
-
async function audit2(args, json, table, io) {
|
|
6513
|
-
const argv = auditArgv(args, json);
|
|
6514
|
-
if ("error" in argv) return usage(argv.error, table, io);
|
|
6515
|
-
return withChant(`${PRODUCT} audit`, table, io, (chant) => chant.runChantInProcess(argv, PRODUCT_WORDS));
|
|
6671
|
+
if (!locks2.checked) return { error: STACK_OP_WORDS.lockNotListed(locks2.backend) };
|
|
6672
|
+
if (locks2.ids.length === 0) return { error: STACK_OP_WORDS.noLock(stack.stack) };
|
|
6673
|
+
if (locks2.ids.length > 1) return { error: STACK_OP_WORDS.manyLocks(stack.stack, locks2.ids) };
|
|
6674
|
+
return { lockId: locks2.ids[0], read: true };
|
|
6516
6675
|
}
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
// src/estate.ts
|
|
6523
|
-
init_chant_product();
|
|
6524
|
-
init_chant_config();
|
|
6525
|
-
init_config();
|
|
6526
|
-
init_stages();
|
|
6527
|
-
init_words();
|
|
6528
|
-
import { dirname as dirname11 } from "node:path";
|
|
6529
|
-
var estateModule;
|
|
6530
|
-
function chantEstate() {
|
|
6531
|
-
estateModule ??= (async () => {
|
|
6676
|
+
var cachedOps;
|
|
6677
|
+
var cachedRollback;
|
|
6678
|
+
function chantStackOps() {
|
|
6679
|
+
cachedOps ??= (async () => {
|
|
6532
6680
|
try {
|
|
6533
|
-
const m = await import("@intentius/chant
|
|
6534
|
-
|
|
6535
|
-
if (!["readEstateLedger", "buildLedgerEstate", "writeEstate"].every((f) => typeof m[f] === "function")) return void 0;
|
|
6536
|
-
if (typeof r.parseReportUrl !== "function" || typeof r.reportPut !== "function") return void 0;
|
|
6537
|
-
return { ...m, parseReportUrl: r.parseReportUrl, reportPut: r.reportPut };
|
|
6681
|
+
const m = await import("@intentius/chant-lexicon-pulumi");
|
|
6682
|
+
return REQUIRED4.every((name) => typeof m[name] === "function") ? m : void 0;
|
|
6538
6683
|
} catch {
|
|
6539
6684
|
return void 0;
|
|
6540
6685
|
}
|
|
6541
6686
|
})();
|
|
6542
|
-
return
|
|
6543
|
-
}
|
|
6544
|
-
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.`;
|
|
6545
|
-
function estateSchedules(config2) {
|
|
6546
|
-
const drift3 = toChantSettings(config2).pipeline.drift;
|
|
6547
|
-
const driftOps = (drift3?.stacks ?? []).map(driftOpName);
|
|
6548
|
-
const schedules = drift3?.schedule ? driftOps.map((op) => ({ op, cron: drift3.schedule })) : [];
|
|
6549
|
-
return { schedules, driftOps };
|
|
6687
|
+
return cachedOps;
|
|
6550
6688
|
}
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
let rest = args.filter((a) => a !== "--fail-overdue");
|
|
6554
|
-
const flags = {};
|
|
6555
|
-
for (const flag of ["--file", "--report-url", "--prefix"]) {
|
|
6556
|
-
const f = takeFlag(rest, flag);
|
|
6557
|
-
if (f.missing) return usage(`${flag} takes a value`, table, io);
|
|
6558
|
-
flags[flag] = f.value;
|
|
6559
|
-
rest = f.rest;
|
|
6560
|
-
}
|
|
6561
|
-
if (rest.length) return usage(`estate does not take ${rest.join(" ")}`, table, io);
|
|
6562
|
-
const chant = await chantEstate();
|
|
6563
|
-
if (!chant) {
|
|
6564
|
-
if (json) io.out(JSON.stringify({ command: "estate", ok: false, exitCode: table.failed, error: ESTATE_NEEDS_CHANT }));
|
|
6565
|
-
else io.err(`${PRODUCT}: ${ESTATE_NEEDS_CHANT}`);
|
|
6566
|
-
return table.failed;
|
|
6567
|
-
}
|
|
6568
|
-
const path = flags["--file"] ?? findConfig(io.cwd);
|
|
6569
|
-
let config2;
|
|
6570
|
-
if (path) {
|
|
6689
|
+
function chantStackRollback() {
|
|
6690
|
+
cachedRollback ??= (async () => {
|
|
6571
6691
|
try {
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
for (const p of errors) io.err(`${path}: ${p.key} ${p.message}`);
|
|
6579
|
-
return table.failed;
|
|
6692
|
+
const m = await import("@intentius/chant-lexicon-pulumi");
|
|
6693
|
+
if (typeof m.pulumiReleaseInputs !== "function") return false;
|
|
6694
|
+
const copy2 = await import("@intentius/chant-lexicon-pulumi/release-copy");
|
|
6695
|
+
return typeof copy2.prepareReleaseCopy === "function";
|
|
6696
|
+
} catch {
|
|
6697
|
+
return false;
|
|
6580
6698
|
}
|
|
6581
|
-
}
|
|
6582
|
-
|
|
6583
|
-
const product = await chantProduct();
|
|
6584
|
-
const words = product ? product.assertProductWords(PRODUCT_WORDS) : PRODUCT_WORDS;
|
|
6585
|
-
const root = path ? dirname11(path) : io.cwd;
|
|
6586
|
-
const load = await chant.readEstateLedger(root);
|
|
6587
|
-
const built = chant.buildLedgerEstate({ load, words, schedules, ...driftOps.length ? { driftOps } : {} });
|
|
6588
|
-
const url = flags["--report-url"] ?? (config2 ? toChantSettings(config2).ci.reportUrl : void 0);
|
|
6589
|
-
let written;
|
|
6590
|
-
if (url) {
|
|
6591
|
-
const target = chant.parseReportUrl(url, flags["--report-url"] ? "--report-url" : "reports.url");
|
|
6592
|
-
if (target && typeof target === "object" && "error" in target) return usage(String(target.error), table, io);
|
|
6593
|
-
written = await chant.writeEstate(built, { put: chant.reportPut(target), words, env: io.env, ...flags["--prefix"] ? { prefix: flags["--prefix"] } : {} });
|
|
6594
|
-
}
|
|
6595
|
-
const code = failOverdue && built.overdue.length > 0 ? table.failed : table.done;
|
|
6596
|
-
if (json) {
|
|
6597
|
-
io.out(JSON.stringify({ command: "estate", ok: code === table.done, exitCode: code, estate: built.estate, dora: built.dora, overdue: built.overdue, ...written ? { written } : {} }));
|
|
6598
|
-
return code;
|
|
6599
|
-
}
|
|
6600
|
-
const totals = built.estate.totals ?? {};
|
|
6601
|
-
io.out(`${PRODUCT} estate: ${Object.entries(totals).map(([k, v]) => `${v} ${k}`).join(", ") || "nothing on the ledger yet"}`);
|
|
6602
|
-
for (const o of built.overdue) io.out(` ${o.message}`);
|
|
6603
|
-
if (written) io.out(` wrote ${written.keys.join(", ")} under ${url}`);
|
|
6604
|
-
else io.out(` no report URL (reports.url or --report-url), so nothing was written`);
|
|
6605
|
-
return code;
|
|
6699
|
+
})();
|
|
6700
|
+
return cachedRollback;
|
|
6606
6701
|
}
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
init_stages();
|
|
6610
|
-
init_words();
|
|
6611
|
-
var RELAY_NEEDS_CHANT = `${PRODUCT} notify relay needs chant >= the release after 0.124.54, which carries chant notify relay (chant#4611).`;
|
|
6612
|
-
async function notify(args, json, table, io) {
|
|
6613
|
-
if (args[0] !== "relay") return usage(`unknown notify command '${args[0] ?? ""}'. Run '${PRODUCT} notify relay'.`, table, io);
|
|
6614
|
-
return withChant(`${PRODUCT} notify relay`, table, io, (chant) => {
|
|
6615
|
-
if (typeof chant.startRelayServer !== "function") {
|
|
6616
|
-
if (json) io.out(JSON.stringify({ command: "notify relay", ok: false, exitCode: table.failed, error: RELAY_NEEDS_CHANT }));
|
|
6617
|
-
else io.err(`${PRODUCT}: ${RELAY_NEEDS_CHANT}`);
|
|
6618
|
-
return Promise.resolve(table.failed);
|
|
6619
|
-
}
|
|
6620
|
-
return chant.runChantInProcess(["notify", ...args], PRODUCT_WORDS);
|
|
6621
|
-
});
|
|
6702
|
+
function needsStackOpsMessage(what, feature, after = "0.124.54") {
|
|
6703
|
+
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'.`;
|
|
6622
6704
|
}
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
import { chmodSync, copyFileSync, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
6631
|
-
import { join as join16 } from "node:path";
|
|
6632
|
-
var GUARDED = {
|
|
6633
|
-
pulumi: ["up", "update", "destroy"],
|
|
6634
|
-
plunge: ["up", "update", "destroy"]
|
|
6635
|
-
};
|
|
6636
|
-
var VALUED_GLOBALS = /* @__PURE__ */ new Set(["-C", "--cwd", "-s", "--stack", "--color", "--config-file", "--logflow", "-v", "--verbose", "--tracing", "--profiling", "--memprofilerate"]);
|
|
6637
|
-
var LAUNCHERS = /* @__PURE__ */ new Set(["env", "npx", "sudo", "command", "exec", "nohup", "time", "xargs"]);
|
|
6638
|
-
function segments(line) {
|
|
6639
|
-
return line.split(/\|\||&&|[;|\n`]|\$\(|\)/).map((seg) => (seg.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).map((t) => t.replace(/^['"]|['"]$/g, ""))).filter((t) => t.length > 0);
|
|
6705
|
+
function readConfig(cwd) {
|
|
6706
|
+
const path = findConfig(cwd);
|
|
6707
|
+
if (!path) return { error: `no plywood.yml in ${cwd} or above it; run '${PRODUCT} init' first` };
|
|
6708
|
+
const config2 = loadConfig(path);
|
|
6709
|
+
const errors = checkConfig(config2).filter((p) => p.severity === "error");
|
|
6710
|
+
if (errors.length) return { error: errors.map((p) => `${path}: ${p.key} ${p.message}`).join("\n") };
|
|
6711
|
+
return { config: config2, path };
|
|
6640
6712
|
}
|
|
6641
|
-
function
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
if (w.startsWith("-")) {
|
|
6651
|
-
if (VALUED_GLOBALS.has(w)) j++;
|
|
6652
|
-
continue;
|
|
6653
|
-
}
|
|
6654
|
-
if (refused.includes(w)) {
|
|
6655
|
-
return {
|
|
6656
|
-
engine: bin,
|
|
6657
|
-
subcommand: w,
|
|
6658
|
-
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.`
|
|
6659
|
-
};
|
|
6660
|
-
}
|
|
6661
|
-
break;
|
|
6662
|
-
}
|
|
6713
|
+
function resolveStack(config2, path, key, dir) {
|
|
6714
|
+
const [project, stack, extra] = key.split("/");
|
|
6715
|
+
if (!project || !stack || extra !== void 0) return { error: `a stack is project/stack, as plywood.yml lists it, not '${key}'` };
|
|
6716
|
+
const settings = toChantSettings(config2);
|
|
6717
|
+
const listed = stacksOf(config2.doc);
|
|
6718
|
+
const root = dirname11(path);
|
|
6719
|
+
const member = settings.members.find((m) => m.name === key) ?? settings.memberDefaults;
|
|
6720
|
+
if (dir === void 0 && Object.keys(listed).length > 0 && !(key in listed)) {
|
|
6721
|
+
return { error: `${key} is not under stacks in ${path}; the stacks are ${Object.keys(listed).join(", ")} (or name its directory with --dir)` };
|
|
6663
6722
|
}
|
|
6664
|
-
|
|
6723
|
+
const at = dir ?? listed[key]?.dir ?? ".";
|
|
6724
|
+
return {
|
|
6725
|
+
key,
|
|
6726
|
+
project,
|
|
6727
|
+
stack,
|
|
6728
|
+
dir: isAbsolute2(at) ? at : resolve2(root, at),
|
|
6729
|
+
...member.engine ? { engine: member.engine } : {},
|
|
6730
|
+
...member.backend ? { backend: member.backend } : {}
|
|
6731
|
+
};
|
|
6665
6732
|
}
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
const hooks = typeof settings.hooks === "object" && settings.hooks !== null ? settings.hooks : {};
|
|
6670
|
-
const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
6671
|
-
const has2 = JSON.stringify(pre).includes("plywood-guard.mjs");
|
|
6672
|
-
if (!has2) pre.push({ matcher: "Bash", hooks: [{ type: "command", command: HOOK_COMMAND }] });
|
|
6673
|
-
return { ...settings, hooks: { ...hooks, PreToolUse: pre } };
|
|
6733
|
+
function requestRules(config2) {
|
|
6734
|
+
const p = toChantSettings(config2).pipeline;
|
|
6735
|
+
return { mode: p.approval ?? "ledger", ...p.quorum?.length ? { quorum: p.quorum } : {} };
|
|
6674
6736
|
}
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
if (sub === "check") {
|
|
6678
|
-
const line = (rest[0] === "--" ? rest.slice(1) : rest).join(" ");
|
|
6679
|
-
if (!line.trim()) return usage("guard check takes the command line to check", table, io);
|
|
6680
|
-
const v = guardVerdict(line);
|
|
6681
|
-
if (json) io.out(JSON.stringify({ command: "guard check", ok: !v, exitCode: v ? table.denied : table.done, refused: v ?? null }));
|
|
6682
|
-
else io.out(v ? v.message : `${PRODUCT} guard: '${line}' runs`);
|
|
6683
|
-
return v ? table.denied : table.done;
|
|
6684
|
-
}
|
|
6685
|
-
if (sub === "install") {
|
|
6686
|
-
const dirFlag = takeFlag(rest, "--dir");
|
|
6687
|
-
if (dirFlag.missing) return usage("--dir takes a directory", table, io);
|
|
6688
|
-
if (dirFlag.rest.length) return usage(`guard install does not take ${dirFlag.rest.join(" ")}`, table, io);
|
|
6689
|
-
const root = dirFlag.value ? join16(io.cwd, dirFlag.value) : io.cwd;
|
|
6690
|
-
const written = [];
|
|
6691
|
-
const hookDir = join16(root, ".claude/hooks");
|
|
6692
|
-
mkdirSync7(hookDir, { recursive: true });
|
|
6693
|
-
copyFileSync(new URL("plywood-guard.mjs", GUARD_TEMPLATES), join16(hookDir, "plywood-guard.mjs"));
|
|
6694
|
-
written.push(".claude/hooks/plywood-guard.mjs");
|
|
6695
|
-
const settingsFile = join16(root, ".claude/settings.json");
|
|
6696
|
-
let settings = {};
|
|
6697
|
-
if (existsSync13(settingsFile)) {
|
|
6698
|
-
try {
|
|
6699
|
-
settings = JSON.parse(readFileSync15(settingsFile, "utf8"));
|
|
6700
|
-
} catch (e) {
|
|
6701
|
-
return usage(`.claude/settings.json is not JSON (${e.message}); fix it, then run this again`, table, io);
|
|
6702
|
-
}
|
|
6703
|
-
}
|
|
6704
|
-
writeFileSync8(settingsFile, JSON.stringify(withGuardHook(settings), null, 2) + "\n");
|
|
6705
|
-
written.push(".claude/settings.json");
|
|
6706
|
-
const binDir = join16(root, ".plywood/agent-bin");
|
|
6707
|
-
mkdirSync7(binDir, { recursive: true });
|
|
6708
|
-
for (const name of Object.keys(GUARDED)) {
|
|
6709
|
-
const to = join16(binDir, name);
|
|
6710
|
-
copyFileSync(new URL(`bin/${name}`, GUARD_TEMPLATES), to);
|
|
6711
|
-
chmodSync(to, 493);
|
|
6712
|
-
written.push(`.plywood/agent-bin/${name}`);
|
|
6713
|
-
}
|
|
6714
|
-
if (json) io.out(JSON.stringify({ command: "guard install", ok: true, exitCode: table.done, written }));
|
|
6715
|
-
else {
|
|
6716
|
-
for (const f of written) io.err(`${PRODUCT} guard: wrote ${f}`);
|
|
6717
|
-
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`);
|
|
6718
|
-
}
|
|
6719
|
-
return table.done;
|
|
6720
|
-
}
|
|
6721
|
-
return usage(`guard takes check <command line> or install [--dir <d>]`, table, io);
|
|
6722
|
-
}
|
|
6723
|
-
|
|
6724
|
-
// src/identities.ts
|
|
6725
|
-
init_jobs2();
|
|
6726
|
-
init_chant_config();
|
|
6727
|
-
init_identity();
|
|
6728
|
-
init_config();
|
|
6729
|
-
init_stacks();
|
|
6730
|
-
init_stages();
|
|
6731
|
-
init_words();
|
|
6732
|
-
import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
|
|
6733
|
-
import { dirname as dirname12, join as join17 } from "node:path";
|
|
6734
|
-
import { targetGlobMatch } from "@intentius/chant/ci-identity";
|
|
6735
|
-
function identityRows(config2, env) {
|
|
6736
|
-
const doc = config2.doc;
|
|
6737
|
-
const out = [];
|
|
6738
|
-
for (const stack of Object.keys(stacksOf(doc))) {
|
|
6739
|
-
const ids = stackIdentities(doc, stack, env, targetGlobMatch);
|
|
6740
|
-
for (const cloud of IDENTITY_CLOUDS) {
|
|
6741
|
-
const id = ids[cloud];
|
|
6742
|
-
if (id) out.push({ stack, cloud, preview: id.preview, up: id.up, rule: id.rule });
|
|
6743
|
-
}
|
|
6744
|
-
}
|
|
6745
|
-
return out;
|
|
6737
|
+
function requesterOf(by, env) {
|
|
6738
|
+
return by ?? env.GITHUB_ACTOR ?? env.GITLAB_USER_LOGIN ?? env.USER ?? "unknown";
|
|
6746
6739
|
}
|
|
6747
|
-
function
|
|
6748
|
-
|
|
6749
|
-
const
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
const clouds = Object.keys(block.cloud);
|
|
6753
|
-
lines2.push(` clouds ${clouds.map((c) => CLOUD_NAMES[c]).join(", ")}`);
|
|
6754
|
-
if (clouds.includes("pulumi")) {
|
|
6755
|
-
const p = pulumiCloudOf(doc);
|
|
6756
|
-
lines2.push(` Pulumi Cloud ${p.org}${p.apiUrl ? ` at ${p.apiUrl}` : ""}; ESC environments open with each job's own token`);
|
|
6757
|
-
}
|
|
6758
|
-
const stacks2 = Object.keys(stacksOf(doc));
|
|
6759
|
-
if (stacks2.length === 0) lines2.push(" plywood.yml lists no stacks; the rules apply to each stack plywood init finds");
|
|
6760
|
-
for (const stack of stacks2) {
|
|
6761
|
-
lines2.push(` ${stack}`);
|
|
6762
|
-
for (const cloud of clouds) {
|
|
6763
|
-
const r = rows.find((x) => x.stack === stack && x.cloud === cloud);
|
|
6764
|
-
lines2.push(r ? ` ${CLOUD_NAMES[cloud].padEnd(12)} preview ${r.preview}, up ${r.up} (${r.rule})` : ` ${CLOUD_NAMES[cloud].padEnd(12)} ${NO_IDENTITY}`);
|
|
6765
|
-
}
|
|
6766
|
-
}
|
|
6767
|
-
lines2.push(" The preview job trades its OIDC token before checkout, and no later step holds a forge token.");
|
|
6768
|
-
return lines2;
|
|
6740
|
+
function destStackName(source, dest, config2) {
|
|
6741
|
+
if (dest.project === source.project) return dest.stack;
|
|
6742
|
+
const cloud = !dest.backend || /^https?:\/\//.test(dest.backend);
|
|
6743
|
+
const org = cloud ? pulumiCloudOf(config2.doc).org : void 0;
|
|
6744
|
+
return `${org ?? "organization"}/${dest.project}/${dest.stack}`;
|
|
6769
6745
|
}
|
|
6770
|
-
function
|
|
6771
|
-
|
|
6772
|
-
const env = takeFlag(file.rest, "--env");
|
|
6773
|
-
if (file.missing || env.missing) return usage(`${file.missing ? "--file" : "--env"} takes a value`, table, io);
|
|
6774
|
-
if (env.rest.length) return usage(`config identities does not take ${env.rest.join(" ")}`, table, io);
|
|
6775
|
-
const path = file.value ?? findConfig(io.cwd);
|
|
6776
|
-
if (!path) return usage(`no plywood.yml in ${io.cwd} or above it; run '${PRODUCT} init', or pass --file`, table, io);
|
|
6777
|
-
let config2;
|
|
6778
|
-
try {
|
|
6779
|
-
config2 = loadConfig(path);
|
|
6780
|
-
} catch (e) {
|
|
6781
|
-
return usage(e instanceof ConfigError ? e.message : `${path}: ${e.message}`, table, io);
|
|
6782
|
-
}
|
|
6783
|
-
const errors = checkConfig(config2).filter((p) => p.severity === "error");
|
|
6784
|
-
if (errors.length) {
|
|
6785
|
-
for (const p of errors) io.err(`${path}: ${p.key} ${p.message}`);
|
|
6786
|
-
if (json) io.out(JSON.stringify({ command: "config identities", ok: false, exitCode: table.failed, file: path, problems: errors }));
|
|
6787
|
-
return table.failed;
|
|
6788
|
-
}
|
|
6789
|
-
const environment = env.value ?? "production";
|
|
6790
|
-
const rows = identityRows(config2, environment);
|
|
6791
|
-
if (json) io.out(JSON.stringify({ command: "config identities", ok: true, exitCode: table.done, file: path, env: environment, identity: toChantSettings(config2).ci.identity ?? null, stacks: rows }));
|
|
6792
|
-
else for (const line of identityLines(config2, environment, rows)) io.out(line);
|
|
6793
|
-
return table.done;
|
|
6746
|
+
function approveRequestLine(gate, digest, mode) {
|
|
6747
|
+
return `${PRODUCT} approve ${gate} --op ${STACK_OPS_OP} --plan ${digest}${mode === "sealed" ? " --sign" : ""}`;
|
|
6794
6748
|
}
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
forgeCheckModule ??= (async () => {
|
|
6801
|
-
try {
|
|
6802
|
-
const fc = await import("@intentius/chant/forge-check");
|
|
6803
|
-
const id = await import("@intentius/chant/ci-identity");
|
|
6804
|
-
if (!FORGE_CHECK_FUNCTIONS.every((f) => typeof fc[f] === "function") || typeof id.resolveCloudIdentity !== "function") return void 0;
|
|
6805
|
-
return { ...fc, resolveCloudIdentity: id.resolveCloudIdentity };
|
|
6806
|
-
} catch {
|
|
6807
|
-
return void 0;
|
|
6808
|
-
}
|
|
6809
|
-
})();
|
|
6810
|
-
return forgeCheckModule;
|
|
6749
|
+
function useBackend(stacks2, env) {
|
|
6750
|
+
const named = [...new Set(stacks2.map((s) => s.backend).filter((b) => b !== void 0))];
|
|
6751
|
+
if (named.length > 1) return `the stacks are on different backends (${named.join(", ")}); a move needs both on one backend`;
|
|
6752
|
+
if (named[0] && !env.PULUMI_BACKEND_URL) env.PULUMI_BACKEND_URL = named[0];
|
|
6753
|
+
return void 0;
|
|
6811
6754
|
}
|
|
6812
|
-
function
|
|
6813
|
-
const
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6755
|
+
function reportOutcome(verb, outcome, gate, rules, done, json, table, io) {
|
|
6756
|
+
const approveLine = outcome.status === "waiting" || outcome.status === "refused" ? approveRequestLine(gate, outcome.digest, rules.mode) : void 0;
|
|
6757
|
+
let code;
|
|
6758
|
+
let text;
|
|
6759
|
+
switch (outcome.status) {
|
|
6760
|
+
case "waiting":
|
|
6761
|
+
code = table.waiting;
|
|
6762
|
+
text = STACK_OP_WORDS.waiting(verb, approveLine);
|
|
6763
|
+
break;
|
|
6764
|
+
case "refused":
|
|
6765
|
+
code = table.waiting;
|
|
6766
|
+
text = `${outcome.reason}. ${STACK_OP_WORDS.waiting(verb, approveLine)}`;
|
|
6767
|
+
break;
|
|
6768
|
+
case "spent":
|
|
6769
|
+
code = table.failed;
|
|
6770
|
+
text = STACK_OP_WORDS.spent(verb);
|
|
6771
|
+
break;
|
|
6772
|
+
case "done":
|
|
6773
|
+
code = table.done;
|
|
6774
|
+
text = done(outcome.result);
|
|
6775
|
+
break;
|
|
6776
|
+
case "failed":
|
|
6777
|
+
code = table.failed;
|
|
6778
|
+
text = `${PRODUCT} ${verb}: ${outcome.error}`;
|
|
6779
|
+
break;
|
|
6820
6780
|
}
|
|
6781
|
+
if (json) {
|
|
6782
|
+
io.out(JSON.stringify({
|
|
6783
|
+
command: verb,
|
|
6784
|
+
ok: code === table.done,
|
|
6785
|
+
exitCode: code,
|
|
6786
|
+
status: outcome.status,
|
|
6787
|
+
op: STACK_OPS_OP,
|
|
6788
|
+
gate,
|
|
6789
|
+
digest: outcome.digest,
|
|
6790
|
+
...approveLine ? { approve: approveLine } : {},
|
|
6791
|
+
...outcome.status === "done" ? { result: outcome.result } : {},
|
|
6792
|
+
...outcome.status === "failed" ? { error: outcome.error } : {},
|
|
6793
|
+
...outcome.status === "refused" ? { refusedBy: outcome.by, reason: outcome.reason } : {}
|
|
6794
|
+
}));
|
|
6795
|
+
} else (code === table.done ? io.out : io.err)(text);
|
|
6796
|
+
return code;
|
|
6821
6797
|
}
|
|
6822
|
-
function
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
if (spec === void 0) return void 0;
|
|
6827
|
-
if (typeof spec === "string") return { labels: [spec] };
|
|
6828
|
-
if (Array.isArray(spec)) return { labels: spec.filter((l) => typeof l === "string") };
|
|
6829
|
-
const g = spec;
|
|
6830
|
-
return typeof g.group === "string" ? { labels: Array.isArray(g.labels) ? g.labels : [], group: g.group } : void 0;
|
|
6831
|
-
}
|
|
6832
|
-
function contractPipelines(fc, cloud, env, members, runner, jobs2) {
|
|
6833
|
-
const applyRunner = upRunner(runner);
|
|
6834
|
-
const list = members.length ? [...members] : [void 0];
|
|
6835
|
-
return list.flatMap((member) => {
|
|
6836
|
-
const identity2 = fc.resolveCloudIdentity(cloud, env, member);
|
|
6837
|
-
const name = member ? `${member}/${env}` : env;
|
|
6838
|
-
const forgeEnvironment = jobs2 ? environmentFor(name, jobs2)?.name : void 0;
|
|
6839
|
-
return identity2 ? [{ name, identity: identity2, ...member ? { member } : {}, ...applyRunner ? { applyRunner } : {}, ...forgeEnvironment ? { forgeEnvironment } : {} }] : [];
|
|
6840
|
-
});
|
|
6841
|
-
}
|
|
6842
|
-
async function forgeCheck(args, json, table, io) {
|
|
6843
|
-
if (args[0] !== "check") return usage(`unknown forge command '${args[0] ?? ""}'. Run '${PRODUCT} forge check'.`, table, io);
|
|
6844
|
-
const fc = await chantForgeCheck();
|
|
6845
|
-
if (!fc) {
|
|
6846
|
-
if (json) io.out(JSON.stringify({ command: "forge check", ok: false, exitCode: table.failed, error: FORGE_CHECK_MESSAGE }));
|
|
6847
|
-
else io.err(`${PRODUCT}: ${FORGE_CHECK_MESSAGE}`);
|
|
6798
|
+
async function runRequest(verb, json, table, io, call) {
|
|
6799
|
+
const ops = await chantStackOps();
|
|
6800
|
+
if (!ops) {
|
|
6801
|
+
io.err(`${PRODUCT}: ${needsStackOpsMessage(`${PRODUCT} ${verb}`, "the gated stack operations (chant#4578)")}`);
|
|
6848
6802
|
return table.failed;
|
|
6849
6803
|
}
|
|
6850
|
-
const requireClouds = args.includes("--require-clouds");
|
|
6851
|
-
let rest = args.slice(1).filter((a) => a !== "--require-clouds");
|
|
6852
|
-
const flags = {};
|
|
6853
|
-
for (const flag of ["--file", "--env", "--forge", "--repo", "--branch"]) {
|
|
6854
|
-
const f = takeFlag(rest, flag);
|
|
6855
|
-
if (f.missing) return usage(`${flag} takes a value`, table, io);
|
|
6856
|
-
flags[flag] = f.value;
|
|
6857
|
-
rest = f.rest;
|
|
6858
|
-
}
|
|
6859
|
-
if (rest.length) return usage(`forge check does not take ${rest.join(" ")}`, table, io);
|
|
6860
|
-
const forgeFlag = flags["--forge"];
|
|
6861
|
-
if (forgeFlag !== void 0 && !FORGES2.includes(forgeFlag)) return usage(`--forge is ${forgeFlag}; give github, gitlab or forgejo`, table, io);
|
|
6862
|
-
const path = flags["--file"] ?? findConfig(io.cwd);
|
|
6863
|
-
if (!path) return usage(`no plywood.yml in ${io.cwd} or above it; run '${PRODUCT} init', or pass --file`, table, io);
|
|
6864
|
-
let config2;
|
|
6865
6804
|
try {
|
|
6866
|
-
|
|
6805
|
+
return await call(ops);
|
|
6867
6806
|
} catch (e) {
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
if (errors.length) {
|
|
6872
|
-
for (const p of errors) io.err(`${path}: ${p.key} ${p.message}`);
|
|
6873
|
-
if (json) io.out(JSON.stringify({ command: "forge check", ok: false, exitCode: table.failed, file: path, problems: errors }));
|
|
6807
|
+
const message = e.message;
|
|
6808
|
+
if (json) io.out(JSON.stringify({ command: verb, ok: false, exitCode: table.failed, error: message }));
|
|
6809
|
+
else io.err(`${PRODUCT} ${verb}: ${message}`);
|
|
6874
6810
|
return table.failed;
|
|
6875
6811
|
}
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
const
|
|
6886
|
-
|
|
6887
|
-
const
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6812
|
+
}
|
|
6813
|
+
function commonFlags(args) {
|
|
6814
|
+
const by = takeFlag(args, "--by");
|
|
6815
|
+
const dir = takeFlag(by.rest, "--dir");
|
|
6816
|
+
if (by.missing) return { rest: args, missing: "--by" };
|
|
6817
|
+
if (dir.missing) return { rest: args, missing: "--dir" };
|
|
6818
|
+
return { by: by.value, dir: dir.value, rest: dir.rest };
|
|
6819
|
+
}
|
|
6820
|
+
async function exportStack(args, json, table, io) {
|
|
6821
|
+
const common = commonFlags(args);
|
|
6822
|
+
if (common.missing) return usage(`${common.missing} takes a value`, table, io);
|
|
6823
|
+
const out = takeFlag(common.rest, "--out");
|
|
6824
|
+
if (out.missing) return usage("--out takes a file path outside the repository", table, io);
|
|
6825
|
+
const extra = out.rest.filter((a) => a.startsWith("-"));
|
|
6826
|
+
if (extra.length) return usage(`export does not take ${extra.join(" ")}`, table, io);
|
|
6827
|
+
const named = positionals(out.rest);
|
|
6828
|
+
if (named.length !== 1) return usage(`export takes one stack: ${STACK_OP_WORDS.usage.export}`, table, io);
|
|
6829
|
+
if (!out.value) return usage(`export needs --out <file>, a path on your machine outside every git working tree: ${STACK_OP_WORDS.usage.export}`, table, io);
|
|
6830
|
+
const read = readConfig(io.cwd);
|
|
6831
|
+
if ("error" in read) return usage(read.error, table, io);
|
|
6832
|
+
const stack = resolveStack(read.config, read.path, named[0], common.dir);
|
|
6833
|
+
if ("error" in stack) return usage(stack.error, table, io);
|
|
6834
|
+
useBackend([stack], io.env);
|
|
6835
|
+
const rules = requestRules(read.config);
|
|
6836
|
+
const outFile = resolve2(io.cwd, out.value);
|
|
6837
|
+
return runRequest("export", json, table, io, async (ops) => {
|
|
6838
|
+
const outcome = await ops.gatedStackExport({
|
|
6839
|
+
by: requesterOf(common.by, io.env),
|
|
6840
|
+
rules,
|
|
6841
|
+
op: STACK_OPS_OP,
|
|
6842
|
+
ledgerCwd: io.cwd,
|
|
6843
|
+
env: io.env,
|
|
6844
|
+
dir: stack.dir,
|
|
6845
|
+
stack: stack.stack,
|
|
6846
|
+
out: outFile,
|
|
6847
|
+
...stack.engine ? { engine: stack.engine } : {}
|
|
6848
|
+
});
|
|
6849
|
+
return reportOutcome("export", outcome, stack.stack, rules, (r) => STACK_OP_WORDS.exported(stack.key, r), json, table, io);
|
|
6850
|
+
});
|
|
6851
|
+
}
|
|
6852
|
+
async function cancelStack(args, json, table, io) {
|
|
6853
|
+
const common = commonFlags(args);
|
|
6854
|
+
if (common.missing) return usage(`${common.missing} takes a value`, table, io);
|
|
6855
|
+
const lock = takeFlag(common.rest, "--lock");
|
|
6856
|
+
if (lock.missing) return usage("--lock takes the lock id", table, io);
|
|
6857
|
+
const extra = lock.rest.filter((a) => a.startsWith("-"));
|
|
6858
|
+
if (extra.length) return usage(`cancel does not take ${extra.join(" ")}`, table, io);
|
|
6859
|
+
const named = positionals(lock.rest);
|
|
6860
|
+
if (named.length !== 1) return usage(`cancel takes one stack: ${STACK_OP_WORDS.usage.cancel}`, table, io);
|
|
6861
|
+
const read = readConfig(io.cwd);
|
|
6862
|
+
if ("error" in read) return usage(read.error, table, io);
|
|
6863
|
+
const stack = resolveStack(read.config, read.path, named[0], common.dir);
|
|
6864
|
+
if ("error" in stack) return usage(stack.error, table, io);
|
|
6865
|
+
useBackend([stack], io.env);
|
|
6866
|
+
const lockId = await lockToCancel(lock.value, stack, io.env);
|
|
6867
|
+
if ("error" in lockId) return usage(lockId.error, table, io);
|
|
6868
|
+
if (lockId.read && !json) io.err(`${PRODUCT} cancel: ${stack.key} holds lock ${lockId.lockId}`);
|
|
6869
|
+
const rules = requestRules(read.config);
|
|
6870
|
+
return runRequest("cancel", json, table, io, async (ops) => {
|
|
6871
|
+
const outcome = await ops.gatedStackCancel({
|
|
6872
|
+
by: requesterOf(common.by, io.env),
|
|
6873
|
+
rules,
|
|
6874
|
+
op: STACK_OPS_OP,
|
|
6875
|
+
ledgerCwd: io.cwd,
|
|
6876
|
+
env: io.env,
|
|
6877
|
+
dir: stack.dir,
|
|
6878
|
+
stack: stack.stack,
|
|
6879
|
+
lockId: lockId.lockId,
|
|
6880
|
+
project: stack.project,
|
|
6881
|
+
...stack.engine ? { engine: stack.engine } : {}
|
|
6882
|
+
});
|
|
6883
|
+
return reportOutcome("cancel", outcome, stack.stack, rules, (r) => STACK_OP_WORDS.cancelled(stack.key, lockId.lockId, r.lockChecked), json, table, io);
|
|
6884
|
+
});
|
|
6885
|
+
}
|
|
6886
|
+
async function moveStack(args, json, table, io) {
|
|
6887
|
+
const onto = takeFlag(args, "--onto");
|
|
6888
|
+
if (onto.missing) return usage("--onto takes plunge", table, io);
|
|
6889
|
+
if (onto.value !== void 0) return moveOntoPlunge(onto.value, onto.rest, json, table, io);
|
|
6890
|
+
const common = commonFlags(onto.rest);
|
|
6891
|
+
if (common.missing) return usage(`${common.missing} takes a value`, table, io);
|
|
6892
|
+
const from = takeFlag(common.rest, "--from");
|
|
6893
|
+
const to = takeFlag(from.rest, "--to");
|
|
6894
|
+
if (from.missing || to.missing) return usage(`${from.missing ? "--from" : "--to"} takes a stack, as project/stack`, table, io);
|
|
6895
|
+
const extra = to.rest.filter((a) => a.startsWith("-"));
|
|
6896
|
+
if (extra.length) return usage(`move does not take ${extra.join(" ")}`, table, io);
|
|
6897
|
+
if (common.dir !== void 0) return usage("move reads each stack's directory from plywood.yml's stacks, so it does not take --dir", table, io);
|
|
6898
|
+
const urns = positionals(to.rest);
|
|
6899
|
+
if (!from.value || !to.value) return usage(`move needs --from and --to: ${STACK_OP_WORDS.usage.move}`, table, io);
|
|
6900
|
+
if (urns.length === 0) return usage(`move names each resource it moves by URN: ${STACK_OP_WORDS.usage.move}`, table, io);
|
|
6901
|
+
const bad = urns.filter((u) => !u.startsWith("urn:pulumi:"));
|
|
6902
|
+
if (bad.length) return usage(`move takes URNs (urn:pulumi:...), not ${bad.join(", ")}; 'pulumi stack --show-urns' lists them`, table, io);
|
|
6903
|
+
if (from.value === to.value) return usage(`--from and --to are both ${from.value}; a move is between two stacks`, table, io);
|
|
6904
|
+
const read = readConfig(io.cwd);
|
|
6905
|
+
if ("error" in read) return usage(read.error, table, io);
|
|
6906
|
+
const source = resolveStack(read.config, read.path, from.value, void 0);
|
|
6907
|
+
if ("error" in source) return usage(source.error, table, io);
|
|
6908
|
+
const dest = resolveStack(read.config, read.path, to.value, void 0);
|
|
6909
|
+
if ("error" in dest) return usage(dest.error, table, io);
|
|
6910
|
+
const backend = useBackend([source, dest], io.env);
|
|
6911
|
+
if (backend) return usage(backend, table, io);
|
|
6912
|
+
const rules = requestRules(read.config);
|
|
6913
|
+
const destName = destStackName(source, dest, read.config);
|
|
6914
|
+
const engine2 = source.engine ?? dest.engine;
|
|
6915
|
+
return runRequest("move", json, table, io, async (ops) => {
|
|
6916
|
+
const outcome = await ops.gatedStackMove({
|
|
6917
|
+
by: requesterOf(common.by, io.env),
|
|
6918
|
+
rules,
|
|
6919
|
+
op: STACK_OPS_OP,
|
|
6920
|
+
ledgerCwd: io.cwd,
|
|
6921
|
+
env: io.env,
|
|
6922
|
+
source: { member: source.key, dir: source.dir, stack: source.stack },
|
|
6923
|
+
dest: { member: dest.key, dir: dest.dir, stack: destName },
|
|
6924
|
+
urns,
|
|
6925
|
+
...engine2 ? { engine: engine2 } : {}
|
|
6926
|
+
});
|
|
6927
|
+
return reportOutcome("move", outcome, `move:${source.stack}:${destName}`, rules, (r) => STACK_OP_WORDS.moved(source.key, dest.key, r), json, table, io);
|
|
6928
|
+
});
|
|
6929
|
+
}
|
|
6930
|
+
async function moveOntoPlunge(engine2, args, json, table, io) {
|
|
6931
|
+
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);
|
|
6932
|
+
const extra = args.filter((a) => a.startsWith("-"));
|
|
6933
|
+
if (extra.length) return usage(`move --onto does not take ${extra.join(" ")}`, table, io);
|
|
6934
|
+
const named = positionals(args);
|
|
6935
|
+
if (named.length > 1) return usage(`move --onto plunge takes at most one stack`, table, io);
|
|
6936
|
+
const read = readConfig(io.cwd);
|
|
6937
|
+
if ("error" in read) return usage(read.error, table, io);
|
|
6938
|
+
const opFile = join16(dirname11(read.path), ADOPT_OP_FILE);
|
|
6939
|
+
if (!existsSync13(opFile)) return usage(STACK_OP_WORDS.noAdoptOp(opFile), table, io);
|
|
6940
|
+
if (named[0]) {
|
|
6941
|
+
const stack = resolveStack(read.config, read.path, named[0], void 0);
|
|
6942
|
+
if ("error" in stack) return usage(stack.error, table, io);
|
|
6943
|
+
if (!json) io.err(STACK_OP_WORDS.adoptAll(stack.key));
|
|
6944
|
+
}
|
|
6945
|
+
const argv = ["run", ADOPT_OP, ...json ? ["--json"] : []];
|
|
6946
|
+
return withChant(`${PRODUCT} move --onto plunge`, table, io, async (chant) => {
|
|
6947
|
+
const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
6948
|
+
if (code === table.waiting && !json) io.err(STACK_OP_WORDS.adoptWaiting);
|
|
6949
|
+
return code === table.waiting ? code : explainRefusal(code, table, io.err);
|
|
6950
|
+
});
|
|
6951
|
+
}
|
|
6952
|
+
async function rollbackStack(args, json, table, io) {
|
|
6953
|
+
const release2 = takeFlag(args, "--release");
|
|
6954
|
+
const env = takeFlag(release2.rest, "--env");
|
|
6955
|
+
const digest = takeFlag(env.rest, "--digest");
|
|
6956
|
+
const actor = takeFlag(digest.rest, "--actor");
|
|
6957
|
+
for (const [flag, f] of [["--release", release2], ["--env", env], ["--digest", digest], ["--actor", actor]]) {
|
|
6958
|
+
if (f.missing) return usage(`${flag} takes a value`, table, io);
|
|
6959
|
+
}
|
|
6960
|
+
const switches = actor.rest.filter((a) => a === "--dry-run" || a === "--events-json");
|
|
6961
|
+
const rest = actor.rest.filter((a) => !switches.includes(a));
|
|
6962
|
+
const extra = rest.filter((a) => a.startsWith("-"));
|
|
6963
|
+
if (extra.length) return usage(`rollback does not take ${extra.join(" ")}`, table, io);
|
|
6964
|
+
const named = positionals(rest);
|
|
6965
|
+
if (named.length > 1) return usage(`rollback takes one stack: ${STACK_OP_WORDS.usage.rollback}`, table, io);
|
|
6966
|
+
let argv;
|
|
6967
|
+
if (release2.value !== void 0) {
|
|
6968
|
+
if (named.length) return usage(STACK_OP_WORDS.releaseIsEnvWide(named[0], environmentOf(named[0])), table, io);
|
|
6969
|
+
if (digest.value !== void 0) return usage("--release takes no --digest: the release names what each stack goes back to", table, io);
|
|
6970
|
+
if (!env.value) return usage(`rollback --release needs --env <env>, the environment whose stacks go back: ${STACK_OP_WORDS.usage.rollback}`, table, io);
|
|
6971
|
+
argv = ["components", "rollback", env.value, "--release", release2.value];
|
|
6972
|
+
} else {
|
|
6973
|
+
if (named.length !== 1) return usage(`rollback takes a stack, or --release <name> --env <env>: ${STACK_OP_WORDS.usage.rollback}`, table, io);
|
|
6974
|
+
const read = readConfig(io.cwd);
|
|
6975
|
+
if ("error" in read) return usage(read.error, table, io);
|
|
6976
|
+
const stack = resolveStack(read.config, read.path, named[0], void 0);
|
|
6977
|
+
if ("error" in stack) return usage(stack.error, table, io);
|
|
6978
|
+
argv = ["components", "rollback", env.value ?? environmentOf(stack.stack), "--component", slug(stack.key), ...digest.value ? ["--digest", digest.value] : []];
|
|
6979
|
+
}
|
|
6980
|
+
argv.push(...actor.value ? ["--actor", actor.value] : [], ...switches, ...json ? ["--json"] : []);
|
|
6981
|
+
if (!await chantStackRollback()) {
|
|
6982
|
+
io.err(`${PRODUCT}: ${needsStackOpsMessage(`${PRODUCT} rollback`, "rollback by release for stacks (chant#4542)")}`);
|
|
6983
|
+
return table.failed;
|
|
6984
|
+
}
|
|
6985
|
+
return withChant(`${PRODUCT} rollback`, table, io, async (chant) => {
|
|
6986
|
+
const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
6987
|
+
if (code === table.waiting && !json) io.err(STACK_OP_WORDS.rollbackWaiting);
|
|
6988
|
+
return code === table.waiting ? code : explainRefusal(code, table, io.err);
|
|
6989
|
+
});
|
|
6913
6990
|
}
|
|
6914
6991
|
|
|
6915
|
-
// src/
|
|
6916
|
-
init_control2();
|
|
6917
|
-
|
|
6918
|
-
// src/telemetry.ts
|
|
6919
|
-
init_chant_product();
|
|
6920
|
-
init_observe();
|
|
6921
|
-
init_config();
|
|
6992
|
+
// src/actions.ts
|
|
6922
6993
|
init_words();
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
};
|
|
6929
|
-
if (!t.traces) {
|
|
6930
|
-
set("OTEL_TRACES_EXPORTER", "none");
|
|
6931
|
-
return out;
|
|
6932
|
-
}
|
|
6933
|
-
if (env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT === void 0) set("OTEL_EXPORTER_OTLP_ENDPOINT", t.endpoint);
|
|
6934
|
-
if (t.headersEnv) set("OTEL_EXPORTER_OTLP_HEADERS", env[t.headersEnv]);
|
|
6935
|
-
if (t.endpoint !== void 0 || env.OTEL_EXPORTER_OTLP_ENDPOINT !== void 0 || env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT !== void 0) set("OTEL_SERVICE_NAME", t.serviceName);
|
|
6936
|
-
return out;
|
|
6994
|
+
var actionSubject = (stack, name) => `action:${stack}:${name}`;
|
|
6995
|
+
var cachedAction;
|
|
6996
|
+
function chantStackAction() {
|
|
6997
|
+
cachedAction ??= import("@intentius/chant-lexicon-pulumi").then((m) => m.gatedStackAction);
|
|
6998
|
+
return cachedAction;
|
|
6937
6999
|
}
|
|
6938
|
-
async function
|
|
6939
|
-
|
|
7000
|
+
async function fromEvent(env) {
|
|
7001
|
+
const chant = await chantProduct();
|
|
7002
|
+
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` };
|
|
7003
|
+
const words = chant.assertProductWords({ ...PRODUCT_WORDS, commands: { ...PRODUCT_WORDS.commands, ...ACTION_COMMAND } });
|
|
7004
|
+
const asked = chant.productActionFromEnv(words, env);
|
|
7005
|
+
if (asked.kind === "refused") return { error: `the action job was asked for no action: ${asked.reason}` };
|
|
7006
|
+
return { stack: asked.member, name: asked.address };
|
|
7007
|
+
}
|
|
7008
|
+
async function action(args, json, table, io) {
|
|
7009
|
+
const event = args.includes("--from-event");
|
|
7010
|
+
const anew = event || args.includes("--anew");
|
|
7011
|
+
const common = commonFlags(args.filter((a) => a !== "--from-event" && a !== "--anew"));
|
|
7012
|
+
if (common.missing) return usage(`${common.missing} takes a value`, table, io);
|
|
7013
|
+
const extra = common.rest.filter((a) => a.startsWith("-"));
|
|
7014
|
+
if (extra.length) return usage(`action does not take ${extra.join(" ")}`, table, io);
|
|
7015
|
+
const named = positionals(common.rest);
|
|
7016
|
+
let key;
|
|
7017
|
+
let name;
|
|
7018
|
+
if (event) {
|
|
7019
|
+
if (named.length) return usage(`action --from-event reads the stack and the action from the job's event, so it takes neither: ${STACK_OP_WORDS.usage.action}`, table, io);
|
|
7020
|
+
const asked = await fromEvent(io.env);
|
|
7021
|
+
if ("error" in asked) return usage(asked.error, table, io);
|
|
7022
|
+
({ stack: key, name } = asked);
|
|
7023
|
+
} else {
|
|
7024
|
+
if (named.length !== 2) return usage(`action takes a stack and an action's name: ${STACK_OP_WORDS.usage.action}`, table, io);
|
|
7025
|
+
[key, name] = named;
|
|
7026
|
+
}
|
|
7027
|
+
const read = readConfig(io.cwd);
|
|
7028
|
+
if ("error" in read) return usage(read.error, table, io);
|
|
7029
|
+
const stack = resolveStack(read.config, read.path, key, common.dir);
|
|
7030
|
+
if ("error" in stack) return usage(stack.error, table, io);
|
|
7031
|
+
const declared = actionsOf(read.config.doc, stack.key);
|
|
7032
|
+
const chosen = declared.find((a) => a.name === name);
|
|
7033
|
+
if (!chosen) return usage(STACK_OP_WORDS.noAction(stack.key, name, declared.map((a) => a.name)), table, io);
|
|
7034
|
+
const gated = await chantStackAction();
|
|
7035
|
+
useBackend([stack], io.env);
|
|
7036
|
+
const rules = requestRules(read.config);
|
|
6940
7037
|
try {
|
|
6941
|
-
const
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
7038
|
+
const outcome = await gated({
|
|
7039
|
+
by: requesterOf(common.by, io.env),
|
|
7040
|
+
rules,
|
|
7041
|
+
op: STACK_OPS_OP,
|
|
7042
|
+
ledgerCwd: io.cwd,
|
|
7043
|
+
env: io.env,
|
|
7044
|
+
dir: stack.dir,
|
|
7045
|
+
stack: stack.stack,
|
|
7046
|
+
urn: chosen.urn,
|
|
7047
|
+
name: chosen.name,
|
|
7048
|
+
...stack.engine ? { engine: stack.engine } : {},
|
|
7049
|
+
...anew ? { anew: true } : {}
|
|
7050
|
+
});
|
|
7051
|
+
return reportOutcome("action", outcome, actionSubject(stack.stack, chosen.name), rules, (r) => STACK_OP_WORDS.acted(stack.key, chosen.name, r), json, table, io);
|
|
7052
|
+
} catch (e) {
|
|
7053
|
+
const err = e;
|
|
7054
|
+
const refusal = typeof err.code === "string" && err.code.startsWith("action-") ? err.code : void 0;
|
|
7055
|
+
const urns = Array.isArray(err.urns) ? err.urns : [];
|
|
7056
|
+
if (json) {
|
|
7057
|
+
io.out(JSON.stringify({
|
|
7058
|
+
command: "action",
|
|
7059
|
+
ok: false,
|
|
7060
|
+
exitCode: table.failed,
|
|
7061
|
+
op: STACK_OPS_OP,
|
|
7062
|
+
gate: actionSubject(stack.stack, chosen.name),
|
|
7063
|
+
error: err.message,
|
|
7064
|
+
...refusal ? { refusal } : {},
|
|
7065
|
+
...urns.length ? { urns } : {}
|
|
7066
|
+
}));
|
|
7067
|
+
} else io.err(`${PRODUCT} action: ${err.message}`);
|
|
7068
|
+
return table.failed;
|
|
6945
7069
|
}
|
|
6946
|
-
Object.assign(env, added);
|
|
6947
|
-
const chant = await chantProduct();
|
|
6948
|
-
if (chant && typeof chant.configureOtlp === "function") chant.configureOtlp({ prefix: PRODUCT, env });
|
|
6949
|
-
return added;
|
|
6950
7070
|
}
|
|
6951
7071
|
|
|
6952
|
-
// src/
|
|
6953
|
-
|
|
6954
|
-
init_stages();
|
|
6955
|
-
init_tips();
|
|
6956
|
-
init_images();
|
|
6957
|
-
init_up();
|
|
6958
|
-
|
|
6959
|
-
// src/upgrade.ts
|
|
6960
|
-
init_config();
|
|
6961
|
-
init_images();
|
|
7072
|
+
// src/audit.ts
|
|
7073
|
+
init_detect();
|
|
6962
7074
|
init_stages();
|
|
6963
7075
|
init_words();
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
var CHANT_FAMILY = /^@intentius\/chant(?:-lexicon-[a-z0-9-]+)?$/;
|
|
6970
|
-
var VERSION2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
6971
|
-
var DEP_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
6972
|
-
function parseUpgradeArgs(args) {
|
|
6973
|
-
const chant = takeFlag(args, "--chant");
|
|
6974
|
-
const forge = takeFlag(chant.rest, "--forge");
|
|
6975
|
-
const env = takeFlag(forge.rest, "--env");
|
|
6976
|
-
if (chant.missing || forge.missing || env.missing) return { error: "--chant, --forge and --env each take a value" };
|
|
6977
|
-
const flags = /* @__PURE__ */ new Set(["--no-install", "--dry-run"]);
|
|
6978
|
-
const unknown = env.rest.filter((a) => a.startsWith("-") && !flags.has(a));
|
|
6979
|
-
if (unknown.length) return { error: `upgrade does not take ${unknown.join(" ")}` };
|
|
6980
|
-
const words = env.rest.filter((a) => !a.startsWith("-"));
|
|
6981
|
-
if (words.length > 1) return { error: `upgrade takes one version, not ${words.join(" ")}` };
|
|
6982
|
-
const version2 = words[0]?.replace(/^v/, "");
|
|
6983
|
-
if (version2 !== void 0 && !VERSION2.test(version2)) return { error: `${words[0]} is not a version: give it as MAJOR.MINOR.PATCH` };
|
|
6984
|
-
if (chant.value !== void 0 && !VERSION2.test(chant.value)) return { error: `--chant ${chant.value} is not a version: give it as MAJOR.MINOR.PATCH` };
|
|
6985
|
-
return {
|
|
6986
|
-
...version2 ? { version: version2 } : {},
|
|
6987
|
-
...chant.value ? { chant: chant.value } : {},
|
|
6988
|
-
...forge.value ? { forge: forge.value } : {},
|
|
6989
|
-
...env.value ? { env: env.value } : {},
|
|
6990
|
-
install: !env.rest.includes("--no-install"),
|
|
6991
|
-
dryRun: env.rest.includes("--dry-run")
|
|
6992
|
-
};
|
|
7076
|
+
var AUDIT_TABLES = { approvals: "gates", updates: "applies", runs: "runs", "review-stacks": "pr_envs" };
|
|
7077
|
+
var DEFAULT_AUDIT_TABLE = "approvals";
|
|
7078
|
+
function auditTable(word) {
|
|
7079
|
+
const w = word ?? DEFAULT_AUDIT_TABLE;
|
|
7080
|
+
return AUDIT_TABLES[w] ?? w;
|
|
6993
7081
|
}
|
|
6994
|
-
function
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7082
|
+
function auditMember(stack) {
|
|
7083
|
+
const [project, name] = stack.split("/");
|
|
7084
|
+
return project && name ? slug(project).slice(0, 36).replace(/-+$/, "") : void 0;
|
|
7085
|
+
}
|
|
7086
|
+
function auditArgv(args, json) {
|
|
7087
|
+
const stack = takeFlag(args, "--stack");
|
|
7088
|
+
const since = takeFlag(stack.rest, "--since");
|
|
7089
|
+
const at = takeFlag(since.rest, "--at");
|
|
7090
|
+
for (const [flag, f] of [["--stack", stack], ["--since", since], ["--at", at]]) if (f.missing) return { error: `${flag} takes a value` };
|
|
7091
|
+
const wheres = [];
|
|
7092
|
+
const rest = [];
|
|
7093
|
+
for (let i = 0; i < at.rest.length; i++) {
|
|
7094
|
+
const a = at.rest[i];
|
|
7095
|
+
if (a === "--where") {
|
|
7096
|
+
const v = at.rest[i + 1];
|
|
7097
|
+
if (v === void 0 || v.startsWith("--")) return { error: "--where takes <field><op><value>, such as gate=wave-2" };
|
|
7098
|
+
wheres.push("--where", v);
|
|
7099
|
+
i++;
|
|
7100
|
+
} else rest.push(a);
|
|
7008
7101
|
}
|
|
7009
|
-
|
|
7102
|
+
const flags = rest.filter((a) => a.startsWith("-"));
|
|
7103
|
+
if (flags.length) return { error: `audit does not take ${flags.join(" ")}` };
|
|
7104
|
+
if (rest.length > 1) return { error: `audit reads one table: ${Object.keys(AUDIT_TABLES).join(", ")}, or a chant ledger table` };
|
|
7105
|
+
let member;
|
|
7106
|
+
if (stack.value !== void 0) {
|
|
7107
|
+
member = auditMember(stack.value);
|
|
7108
|
+
if (!member) return { error: `--stack takes project/stack, not '${stack.value}'` };
|
|
7109
|
+
}
|
|
7110
|
+
return [
|
|
7111
|
+
"ledger",
|
|
7112
|
+
auditTable(rest[0]),
|
|
7113
|
+
...member ? ["--member", member] : [],
|
|
7114
|
+
...since.value ? ["--since", since.value] : [],
|
|
7115
|
+
...wheres,
|
|
7116
|
+
...at.value ? ["--at", at.value] : [],
|
|
7117
|
+
...json ? ["--json"] : []
|
|
7118
|
+
];
|
|
7010
7119
|
}
|
|
7011
|
-
function
|
|
7012
|
-
const
|
|
7013
|
-
if (
|
|
7014
|
-
return `${
|
|
7120
|
+
async function audit2(args, json, table, io) {
|
|
7121
|
+
const argv = auditArgv(args, json);
|
|
7122
|
+
if ("error" in argv) return usage(argv.error, table, io);
|
|
7123
|
+
return withChant(`${PRODUCT} audit`, table, io, (chant) => chant.runChantInProcess(argv, PRODUCT_WORDS));
|
|
7015
7124
|
}
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7125
|
+
|
|
7126
|
+
// src/cli.ts
|
|
7127
|
+
init_agents2();
|
|
7128
|
+
init_drift2();
|
|
7129
|
+
|
|
7130
|
+
// src/estate.ts
|
|
7131
|
+
init_chant_product();
|
|
7132
|
+
init_chant_config();
|
|
7133
|
+
init_config();
|
|
7134
|
+
init_stages();
|
|
7135
|
+
init_words();
|
|
7136
|
+
import { dirname as dirname12 } from "node:path";
|
|
7137
|
+
var estateModule;
|
|
7138
|
+
function chantEstate() {
|
|
7139
|
+
estateModule ??= (async () => {
|
|
7140
|
+
try {
|
|
7141
|
+
const m = await import("@intentius/chant/ci-estate");
|
|
7142
|
+
const r = await import("@intentius/chant/pr-report-url");
|
|
7143
|
+
if (!["readEstateLedger", "buildLedgerEstate", "writeEstate"].every((f) => typeof m[f] === "function")) return void 0;
|
|
7144
|
+
if (typeof r.parseReportUrl !== "function" || typeof r.reportPut !== "function") return void 0;
|
|
7145
|
+
return { ...m, parseReportUrl: r.parseReportUrl, reportPut: r.reportPut };
|
|
7146
|
+
} catch {
|
|
7147
|
+
return void 0;
|
|
7035
7148
|
}
|
|
7036
|
-
}
|
|
7037
|
-
return
|
|
7038
|
-
}
|
|
7039
|
-
function moveDeclarationPins(text, file, chant) {
|
|
7040
|
-
const doc = JSON.parse(text);
|
|
7041
|
-
const moves = [];
|
|
7042
|
-
const pins = Array.isArray(doc.pins) ? doc.pins : [];
|
|
7043
|
-
for (const pin of pins) {
|
|
7044
|
-
if (typeof pin.package !== "string" || !CHANT_FAMILY.test(pin.package) || typeof pin.version !== "string") continue;
|
|
7045
|
-
if (pin.version === chant) continue;
|
|
7046
|
-
moves.push({ file, name: pin.package, from: pin.version, to: chant });
|
|
7047
|
-
pin.version = chant;
|
|
7048
|
-
}
|
|
7049
|
-
return { text: moves.length ? jsonText2(doc, text) : text, moves };
|
|
7149
|
+
})();
|
|
7150
|
+
return estateModule;
|
|
7050
7151
|
}
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
const
|
|
7054
|
-
|
|
7055
|
-
const
|
|
7056
|
-
|
|
7057
|
-
if (m) return `plywood.yml's image is plywood's ${m[1]} image at ${m[2]}; name the ${version2} image in it (its tag starts ${version2}-), or drop the key to take the release's own`;
|
|
7058
|
-
return `plywood.yml's image is your own (${name}); rebuild it FROM plywood's ${version2} image and name the new digest in it`;
|
|
7152
|
+
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.`;
|
|
7153
|
+
function estateSchedules(config2) {
|
|
7154
|
+
const drift3 = toChantSettings(config2).pipeline.drift;
|
|
7155
|
+
const driftOps = (drift3?.stacks ?? []).map(driftOpName);
|
|
7156
|
+
const schedules = drift3?.schedule ? driftOps.map((op) => ({ op, cron: drift3.schedule })) : [];
|
|
7157
|
+
return { schedules, driftOps };
|
|
7059
7158
|
}
|
|
7060
|
-
async function
|
|
7061
|
-
const
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
parsed = JSON.parse(r.stdout);
|
|
7070
|
-
} catch {
|
|
7071
|
-
return { error: `npm view ${spec} printed no JSON` };
|
|
7159
|
+
async function estate(args, json, table, io) {
|
|
7160
|
+
const failOverdue = args.includes("--fail-overdue");
|
|
7161
|
+
let rest = args.filter((a) => a !== "--fail-overdue");
|
|
7162
|
+
const flags = {};
|
|
7163
|
+
for (const flag of ["--file", "--report-url", "--prefix"]) {
|
|
7164
|
+
const f = takeFlag(rest, flag);
|
|
7165
|
+
if (f.missing) return usage(`${flag} takes a value`, table, io);
|
|
7166
|
+
flags[flag] = f.value;
|
|
7167
|
+
rest = f.rest;
|
|
7072
7168
|
}
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
return { version: doc.version, chant };
|
|
7079
|
-
}
|
|
7080
|
-
async function upgrade(args, json, table, io) {
|
|
7081
|
-
const opts = parseUpgradeArgs(args);
|
|
7082
|
-
const fail = (message) => {
|
|
7083
|
-
if (json) io.out(JSON.stringify({ command: "upgrade", ok: false, exitCode: table.failed, error: message }));
|
|
7084
|
-
else io.err(`${PRODUCT}: ${message}`);
|
|
7169
|
+
if (rest.length) return usage(`estate does not take ${rest.join(" ")}`, table, io);
|
|
7170
|
+
const chant = await chantEstate();
|
|
7171
|
+
if (!chant) {
|
|
7172
|
+
if (json) io.out(JSON.stringify({ command: "estate", ok: false, exitCode: table.failed, error: ESTATE_NEEDS_CHANT }));
|
|
7173
|
+
else io.err(`${PRODUCT}: ${ESTATE_NEEDS_CHANT}`);
|
|
7085
7174
|
return table.failed;
|
|
7086
|
-
}
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
if (!pkgFile) return fail(`no package.json in ${io.cwd} or above it depends on ${PLYWOOD_PACKAGE}; install it with 'npm i -D ${PLYWOOD_PACKAGE}', then run '${PRODUCT} init'`);
|
|
7091
|
-
const pkgDir = dirname13(pkgFile);
|
|
7092
|
-
const configFile = findConfig(io.cwd);
|
|
7093
|
-
const configDir = configFile ? dirname13(configFile) : pkgDir;
|
|
7094
|
-
const rel = (f) => relative2(io.cwd, f) || f;
|
|
7095
|
-
const release2 = await viewRelease(exec, pkgDir, io.env, opts.version);
|
|
7096
|
-
if ("error" in release2) return fail(release2.error);
|
|
7097
|
-
const chant = opts.chant ?? release2.chant;
|
|
7098
|
-
const pkgText = readFileSync17(pkgFile, "utf8");
|
|
7099
|
-
const pkgMove = movePackagePins(pkgText, rel(pkgFile), release2.version, chant);
|
|
7100
|
-
const declFile = join18(configDir, "chant.workspace.json");
|
|
7101
|
-
const declText = existsSync15(declFile) ? readFileSync17(declFile, "utf8") : void 0;
|
|
7102
|
-
let declMove;
|
|
7103
|
-
if (declText !== void 0) {
|
|
7175
|
+
}
|
|
7176
|
+
const path = flags["--file"] ?? findConfig(io.cwd);
|
|
7177
|
+
let config2;
|
|
7178
|
+
if (path) {
|
|
7104
7179
|
try {
|
|
7105
|
-
|
|
7180
|
+
config2 = loadConfig(path);
|
|
7106
7181
|
} catch (e) {
|
|
7107
|
-
return
|
|
7182
|
+
return usage(e instanceof ConfigError ? e.message : `${path}: ${e.message}`, table, io);
|
|
7108
7183
|
}
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
try {
|
|
7114
|
-
const note = imageNote(loadConfig(configFile).doc, release2.version);
|
|
7115
|
-
if (note) notes.push(note);
|
|
7116
|
-
} catch {
|
|
7184
|
+
const errors = checkConfig(config2).filter((p) => p.severity === "error");
|
|
7185
|
+
if (errors.length) {
|
|
7186
|
+
for (const p of errors) io.err(`${path}: ${p.key} ${p.message}`);
|
|
7187
|
+
return table.failed;
|
|
7117
7188
|
}
|
|
7118
|
-
} else {
|
|
7119
|
-
notes.push(`no plywood.yml, so there is no workflow to write again; run '${PRODUCT} init' after the install`);
|
|
7120
|
-
}
|
|
7121
|
-
const moves = [...pkgMove.moves, ...declMove?.moves ?? []];
|
|
7122
|
-
const ciArgs = ["--no-install", PRODUCT, "ci", ...opts.forge ? ["--forge", opts.forge] : [], ...opts.env ? ["--env", opts.env] : []];
|
|
7123
|
-
const steps = {
|
|
7124
|
-
install: { cwd: rel(pkgDir) || ".", command: "npm install --no-audit --no-fund" },
|
|
7125
|
-
ci: { cwd: rel(configDir) || ".", command: `npx ${ciArgs.join(" ")}` }
|
|
7126
|
-
};
|
|
7127
|
-
const envelope = { command: "upgrade", plywood: release2.version, chant, moves, notes, dryRun: opts.dryRun };
|
|
7128
|
-
const say = (line) => {
|
|
7129
|
-
if (!json) io.err(line);
|
|
7130
|
-
};
|
|
7131
|
-
say(`${PRODUCT} upgrade: ${PLYWOOD_PACKAGE} ${release2.version}, ${CHANT_PACKAGE} ${chant}`);
|
|
7132
|
-
for (const m of moves) say(` ${m.file} ${m.name} ${m.from} -> ${m.to}`);
|
|
7133
|
-
if (moves.length === 0) say(" every pin is already there");
|
|
7134
|
-
if (opts.dryRun) {
|
|
7135
|
-
say(` then: ${steps.install.command} (in ${steps.install.cwd}), then ${steps.ci.command} (in ${steps.ci.cwd})`);
|
|
7136
|
-
for (const n of notes) say(` note: ${n}`);
|
|
7137
|
-
if (json) io.out(JSON.stringify({ ...envelope, ok: true, exitCode: table.done, steps }));
|
|
7138
|
-
return table.done;
|
|
7139
|
-
}
|
|
7140
|
-
if (pkgMove.text !== pkgText) writeFileSync9(pkgFile, pkgMove.text);
|
|
7141
|
-
if (declMove && declText !== void 0 && declMove.text !== declText) writeFileSync9(declFile, declMove.text);
|
|
7142
|
-
const files = [...new Set(moves.map((m) => m.file))];
|
|
7143
|
-
if (!opts.install) {
|
|
7144
|
-
say(` left to run: ${steps.install.command} (in ${steps.install.cwd}), then ${steps.ci.command} (in ${steps.ci.cwd})`);
|
|
7145
|
-
for (const n of notes) say(` note: ${n}`);
|
|
7146
|
-
if (json) io.out(JSON.stringify({ ...envelope, ok: true, exitCode: table.done, files, steps }));
|
|
7147
|
-
return table.done;
|
|
7148
7189
|
}
|
|
7149
|
-
|
|
7150
|
-
const
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7190
|
+
const { schedules, driftOps } = config2 ? estateSchedules(config2) : { schedules: [], driftOps: [] };
|
|
7191
|
+
const product = await chantProduct();
|
|
7192
|
+
const words = product ? product.assertProductWords(PRODUCT_WORDS) : PRODUCT_WORDS;
|
|
7193
|
+
const root = path ? dirname12(path) : io.cwd;
|
|
7194
|
+
const load = await chant.readEstateLedger(root);
|
|
7195
|
+
const built = chant.buildLedgerEstate({ load, words, schedules, ...driftOps.length ? { driftOps } : {} });
|
|
7196
|
+
const url = flags["--report-url"] ?? (config2 ? toChantSettings(config2).ci.reportUrl : void 0);
|
|
7197
|
+
let written;
|
|
7198
|
+
if (url) {
|
|
7199
|
+
const target = chant.parseReportUrl(url, flags["--report-url"] ? "--report-url" : "reports.url");
|
|
7200
|
+
if (target && typeof target === "object" && "error" in target) return usage(String(target.error), table, io);
|
|
7201
|
+
written = await chant.writeEstate(built, { put: chant.reportPut(target), words, env: io.env, ...flags["--prefix"] ? { prefix: flags["--prefix"] } : {} });
|
|
7155
7202
|
}
|
|
7156
|
-
const
|
|
7157
|
-
if (
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
say(` $ ${steps.ci.command}`);
|
|
7161
|
-
ci2 = await exec("npx", ciArgs, { cwd: configDir, env: io.env });
|
|
7162
|
-
if (!json && ci2.stderr.trim()) io.err(ci2.stderr.trimEnd());
|
|
7203
|
+
const code = failOverdue && built.overdue.length > 0 ? table.failed : table.done;
|
|
7204
|
+
if (json) {
|
|
7205
|
+
io.out(JSON.stringify({ command: "estate", ok: code === table.done, exitCode: code, estate: built.estate, dora: built.dora, overdue: built.overdue, ...written ? { written } : {} }));
|
|
7206
|
+
return code;
|
|
7163
7207
|
}
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
const
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
else say(` ${PRODUCT} ci failed (exit ${ci2?.code}); the pins are moved and installed, so fix plywood.yml and run '${steps.ci.command}'`);
|
|
7170
|
-
const code = ok ? table.done : ci2?.code ?? table.failed;
|
|
7171
|
-
if (json) io.out(JSON.stringify({ ...envelope, ok, exitCode: code, files: changed, ...ci2 ? { ci: { exitCode: ci2.code } } : {} }));
|
|
7208
|
+
const totals = built.estate.totals ?? {};
|
|
7209
|
+
io.out(`${PRODUCT} estate: ${Object.entries(totals).map(([k, v]) => `${v} ${k}`).join(", ") || "nothing on the ledger yet"}`);
|
|
7210
|
+
for (const o of built.overdue) io.out(` ${o.message}`);
|
|
7211
|
+
if (written) io.out(` wrote ${written.keys.join(", ")} under ${url}`);
|
|
7212
|
+
else io.out(` no report URL (reports.url or --report-url), so nothing was written`);
|
|
7172
7213
|
return code;
|
|
7173
7214
|
}
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7215
|
+
|
|
7216
|
+
// src/notify.ts
|
|
7217
|
+
init_stages();
|
|
7218
|
+
init_words();
|
|
7219
|
+
var RELAY_NEEDS_CHANT = `${PRODUCT} notify relay needs chant >= the release after 0.124.54, which carries chant notify relay (chant#4611).`;
|
|
7220
|
+
async function notify(args, json, table, io) {
|
|
7221
|
+
if (args[0] !== "relay") return usage(`unknown notify command '${args[0] ?? ""}'. Run '${PRODUCT} notify relay'.`, table, io);
|
|
7222
|
+
return withChant(`${PRODUCT} notify relay`, table, io, (chant) => {
|
|
7223
|
+
if (typeof chant.startRelayServer !== "function") {
|
|
7224
|
+
if (json) io.out(JSON.stringify({ command: "notify relay", ok: false, exitCode: table.failed, error: RELAY_NEEDS_CHANT }));
|
|
7225
|
+
else io.err(`${PRODUCT}: ${RELAY_NEEDS_CHANT}`);
|
|
7226
|
+
return Promise.resolve(table.failed);
|
|
7227
|
+
}
|
|
7228
|
+
return chant.runChantInProcess(["notify", ...args], PRODUCT_WORDS);
|
|
7229
|
+
});
|
|
7230
|
+
}
|
|
7231
|
+
|
|
7232
|
+
// src/cli.ts
|
|
7233
|
+
init_policy();
|
|
7234
|
+
|
|
7235
|
+
// src/guard.ts
|
|
7236
|
+
init_stages();
|
|
7237
|
+
init_words();
|
|
7238
|
+
import { chmodSync, copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
7239
|
+
import { join as join17 } from "node:path";
|
|
7240
|
+
var GUARDED = {
|
|
7241
|
+
pulumi: ["up", "update", "destroy"],
|
|
7242
|
+
plunge: ["up", "update", "destroy"]
|
|
7243
|
+
};
|
|
7244
|
+
var VALUED_GLOBALS = /* @__PURE__ */ new Set(["-C", "--cwd", "-s", "--stack", "--color", "--config-file", "--logflow", "-v", "--verbose", "--tracing", "--profiling", "--memprofilerate"]);
|
|
7245
|
+
var LAUNCHERS = /* @__PURE__ */ new Set(["env", "npx", "sudo", "command", "exec", "nohup", "time", "xargs"]);
|
|
7246
|
+
function segments(line) {
|
|
7247
|
+
return line.split(/\|\||&&|[;|\n`]|\$\(|\)/).map((seg) => (seg.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).map((t) => t.replace(/^['"]|['"]$/g, ""))).filter((t) => t.length > 0);
|
|
7248
|
+
}
|
|
7249
|
+
function guardVerdict(line) {
|
|
7250
|
+
for (const words of segments(line)) {
|
|
7251
|
+
let i = 0;
|
|
7252
|
+
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++;
|
|
7253
|
+
const bin = (words[i] ?? "").split("/").pop();
|
|
7254
|
+
const refused = GUARDED[bin];
|
|
7255
|
+
if (!refused) continue;
|
|
7256
|
+
for (let j = i + 1; j < words.length; j++) {
|
|
7257
|
+
const w = words[j];
|
|
7258
|
+
if (w.startsWith("-")) {
|
|
7259
|
+
if (VALUED_GLOBALS.has(w)) j++;
|
|
7260
|
+
continue;
|
|
7261
|
+
}
|
|
7262
|
+
if (refused.includes(w)) {
|
|
7263
|
+
return {
|
|
7264
|
+
engine: bin,
|
|
7265
|
+
subcommand: w,
|
|
7266
|
+
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.`
|
|
7267
|
+
};
|
|
7268
|
+
}
|
|
7269
|
+
break;
|
|
7270
|
+
}
|
|
7180
7271
|
}
|
|
7272
|
+
return void 0;
|
|
7181
7273
|
}
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7274
|
+
var GUARD_TEMPLATES = new URL("../templates/agent-guard/", import.meta.url);
|
|
7275
|
+
var HOOK_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/plywood-guard.mjs"';
|
|
7276
|
+
function withGuardHook(settings) {
|
|
7277
|
+
const hooks = typeof settings.hooks === "object" && settings.hooks !== null ? settings.hooks : {};
|
|
7278
|
+
const pre = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
7279
|
+
const has = JSON.stringify(pre).includes("plywood-guard.mjs");
|
|
7280
|
+
if (!has) pre.push({ matcher: "Bash", hooks: [{ type: "command", command: HOOK_COMMAND }] });
|
|
7281
|
+
return { ...settings, hooks: { ...hooks, PreToolUse: pre } };
|
|
7186
7282
|
}
|
|
7187
|
-
function
|
|
7188
|
-
|
|
7283
|
+
async function guard(args, json, table, io) {
|
|
7284
|
+
const [sub, ...rest] = args;
|
|
7285
|
+
if (sub === "check") {
|
|
7286
|
+
const line = (rest[0] === "--" ? rest.slice(1) : rest).join(" ");
|
|
7287
|
+
if (!line.trim()) return usage("guard check takes the command line to check", table, io);
|
|
7288
|
+
const v = guardVerdict(line);
|
|
7289
|
+
if (json) io.out(JSON.stringify({ command: "guard check", ok: !v, exitCode: v ? table.denied : table.done, refused: v ?? null }));
|
|
7290
|
+
else io.out(v ? v.message : `${PRODUCT} guard: '${line}' runs`);
|
|
7291
|
+
return v ? table.denied : table.done;
|
|
7292
|
+
}
|
|
7293
|
+
if (sub === "install") {
|
|
7294
|
+
const dirFlag = takeFlag(rest, "--dir");
|
|
7295
|
+
if (dirFlag.missing) return usage("--dir takes a directory", table, io);
|
|
7296
|
+
if (dirFlag.rest.length) return usage(`guard install does not take ${dirFlag.rest.join(" ")}`, table, io);
|
|
7297
|
+
const root = dirFlag.value ? join17(io.cwd, dirFlag.value) : io.cwd;
|
|
7298
|
+
const written = [];
|
|
7299
|
+
const hookDir = join17(root, ".claude/hooks");
|
|
7300
|
+
mkdirSync7(hookDir, { recursive: true });
|
|
7301
|
+
copyFileSync(new URL("plywood-guard.mjs", GUARD_TEMPLATES), join17(hookDir, "plywood-guard.mjs"));
|
|
7302
|
+
written.push(".claude/hooks/plywood-guard.mjs");
|
|
7303
|
+
const settingsFile = join17(root, ".claude/settings.json");
|
|
7304
|
+
let settings = {};
|
|
7305
|
+
if (existsSync14(settingsFile)) {
|
|
7306
|
+
try {
|
|
7307
|
+
settings = JSON.parse(readFileSync15(settingsFile, "utf8"));
|
|
7308
|
+
} catch (e) {
|
|
7309
|
+
return usage(`.claude/settings.json is not JSON (${e.message}); fix it, then run this again`, table, io);
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
writeFileSync8(settingsFile, JSON.stringify(withGuardHook(settings), null, 2) + "\n");
|
|
7313
|
+
written.push(".claude/settings.json");
|
|
7314
|
+
const binDir = join17(root, ".plywood/agent-bin");
|
|
7315
|
+
mkdirSync7(binDir, { recursive: true });
|
|
7316
|
+
for (const name of Object.keys(GUARDED)) {
|
|
7317
|
+
const to = join17(binDir, name);
|
|
7318
|
+
copyFileSync(new URL(`bin/${name}`, GUARD_TEMPLATES), to);
|
|
7319
|
+
chmodSync(to, 493);
|
|
7320
|
+
written.push(`.plywood/agent-bin/${name}`);
|
|
7321
|
+
}
|
|
7322
|
+
if (json) io.out(JSON.stringify({ command: "guard install", ok: true, exitCode: table.done, written }));
|
|
7323
|
+
else {
|
|
7324
|
+
for (const f of written) io.err(`${PRODUCT} guard: wrote ${f}`);
|
|
7325
|
+
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`);
|
|
7326
|
+
}
|
|
7327
|
+
return table.done;
|
|
7328
|
+
}
|
|
7329
|
+
return usage(`guard takes check <command line> or install [--dir <d>]`, table, io);
|
|
7189
7330
|
}
|
|
7190
|
-
var execChild = (bin, args, opts) => new Promise((done) => {
|
|
7191
|
-
execFile(bin, args, { cwd: opts.cwd, env: opts.env, maxBuffer: 64 * 1024 * 1024, shell: process.platform === "win32" }, (error2, stdout, stderr) => {
|
|
7192
|
-
const code = error2 ? typeof error2.code === "number" ? error2.code : 1 : 0;
|
|
7193
|
-
done({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
7194
|
-
});
|
|
7195
|
-
});
|
|
7196
7331
|
|
|
7197
|
-
// src/
|
|
7332
|
+
// src/identities.ts
|
|
7333
|
+
init_jobs2();
|
|
7198
7334
|
init_chant_config();
|
|
7199
7335
|
init_identity();
|
|
7200
7336
|
init_config();
|
|
7201
7337
|
init_stacks();
|
|
7202
|
-
init_adopt();
|
|
7203
|
-
init_detect();
|
|
7204
|
-
init_refusals();
|
|
7205
7338
|
init_stages();
|
|
7206
|
-
init_chant_product();
|
|
7207
7339
|
init_words();
|
|
7208
|
-
import { existsSync as
|
|
7209
|
-
import { dirname as
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
return void 0;
|
|
7340
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16 } from "node:fs";
|
|
7341
|
+
import { dirname as dirname13, join as join18 } from "node:path";
|
|
7342
|
+
import { targetGlobMatch } from "@intentius/chant/ci-identity";
|
|
7343
|
+
function identityRows(config2, env) {
|
|
7344
|
+
const doc = config2.doc;
|
|
7345
|
+
const out = [];
|
|
7346
|
+
for (const stack of Object.keys(stacksOf(doc))) {
|
|
7347
|
+
const ids = stackIdentities(doc, stack, env, targetGlobMatch);
|
|
7348
|
+
for (const cloud of IDENTITY_CLOUDS) {
|
|
7349
|
+
const id = ids[cloud];
|
|
7350
|
+
if (id) out.push({ stack, cloud, preview: id.preview, up: id.up, rule: id.rule });
|
|
7220
7351
|
}
|
|
7221
|
-
}
|
|
7222
|
-
return
|
|
7352
|
+
}
|
|
7353
|
+
return out;
|
|
7223
7354
|
}
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
const
|
|
7227
|
-
if (!
|
|
7228
|
-
|
|
7355
|
+
function identityLines(config2, env, rows) {
|
|
7356
|
+
const doc = config2.doc;
|
|
7357
|
+
const block = chantIdentityOf(doc);
|
|
7358
|
+
if (!block) return [`${config2.file ?? "plywood.yml"}: no identity; every job runs as ${NO_IDENTITY}`];
|
|
7359
|
+
const lines2 = [`${config2.file ?? "plywood.yml"}: identities in environment ${env}`];
|
|
7360
|
+
const clouds = Object.keys(block.cloud);
|
|
7361
|
+
lines2.push(` clouds ${clouds.map((c) => CLOUD_NAMES[c]).join(", ")}`);
|
|
7362
|
+
if (clouds.includes("pulumi")) {
|
|
7363
|
+
const p = pulumiCloudOf(doc);
|
|
7364
|
+
lines2.push(` Pulumi Cloud ${p.org}${p.apiUrl ? ` at ${p.apiUrl}` : ""}; ESC environments open with each job's own token`);
|
|
7365
|
+
}
|
|
7366
|
+
const stacks2 = Object.keys(stacksOf(doc));
|
|
7367
|
+
if (stacks2.length === 0) lines2.push(" plywood.yml lists no stacks; the rules apply to each stack plywood init finds");
|
|
7368
|
+
for (const stack of stacks2) {
|
|
7369
|
+
lines2.push(` ${stack}`);
|
|
7370
|
+
for (const cloud of clouds) {
|
|
7371
|
+
const r = rows.find((x) => x.stack === stack && x.cloud === cloud);
|
|
7372
|
+
lines2.push(r ? ` ${CLOUD_NAMES[cloud].padEnd(12)} preview ${r.preview}, up ${r.up} (${r.rule})` : ` ${CLOUD_NAMES[cloud].padEnd(12)} ${NO_IDENTITY}`);
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
lines2.push(" The preview job trades its OIDC token before checkout, and no later step holds a forge token.");
|
|
7376
|
+
return lines2;
|
|
7377
|
+
}
|
|
7378
|
+
function configIdentities(args, json, table, io) {
|
|
7379
|
+
const file = takeFlag(args, "--file");
|
|
7380
|
+
const env = takeFlag(file.rest, "--env");
|
|
7381
|
+
if (file.missing || env.missing) return usage(`${file.missing ? "--file" : "--env"} takes a value`, table, io);
|
|
7382
|
+
if (env.rest.length) return usage(`config identities does not take ${env.rest.join(" ")}`, table, io);
|
|
7383
|
+
const path = file.value ?? findConfig(io.cwd);
|
|
7384
|
+
if (!path) return usage(`no plywood.yml in ${io.cwd} or above it; run '${PRODUCT} init', or pass --file`, table, io);
|
|
7385
|
+
let config2;
|
|
7229
7386
|
try {
|
|
7230
|
-
|
|
7387
|
+
config2 = loadConfig(path);
|
|
7231
7388
|
} catch (e) {
|
|
7232
|
-
return
|
|
7389
|
+
return usage(e instanceof ConfigError ? e.message : `${path}: ${e.message}`, table, io);
|
|
7233
7390
|
}
|
|
7234
|
-
|
|
7235
|
-
if (
|
|
7236
|
-
|
|
7237
|
-
|
|
7391
|
+
const errors = checkConfig(config2).filter((p) => p.severity === "error");
|
|
7392
|
+
if (errors.length) {
|
|
7393
|
+
for (const p of errors) io.err(`${path}: ${p.key} ${p.message}`);
|
|
7394
|
+
if (json) io.out(JSON.stringify({ command: "config identities", ok: false, exitCode: table.failed, file: path, problems: errors }));
|
|
7395
|
+
return table.failed;
|
|
7396
|
+
}
|
|
7397
|
+
const environment = env.value ?? "production";
|
|
7398
|
+
const rows = identityRows(config2, environment);
|
|
7399
|
+
if (json) io.out(JSON.stringify({ command: "config identities", ok: true, exitCode: table.done, file: path, env: environment, identity: toChantSettings(config2).ci.identity ?? null, stacks: rows }));
|
|
7400
|
+
else for (const line of identityLines(config2, environment, rows)) io.out(line);
|
|
7401
|
+
return table.done;
|
|
7238
7402
|
}
|
|
7239
|
-
var
|
|
7240
|
-
var
|
|
7241
|
-
|
|
7242
|
-
|
|
7403
|
+
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.`;
|
|
7404
|
+
var FORGES2 = ["github", "gitlab", "forgejo"];
|
|
7405
|
+
var FORGE_CHECK_FUNCTIONS = ["forgeCheck", "forgeCheckExit", "describeForgeCheck", "forgeCheckTarget", "pipelineContract"];
|
|
7406
|
+
var forgeCheckModule;
|
|
7407
|
+
function chantForgeCheck() {
|
|
7408
|
+
forgeCheckModule ??= (async () => {
|
|
7243
7409
|
try {
|
|
7244
|
-
const
|
|
7245
|
-
|
|
7410
|
+
const fc = await import("@intentius/chant/forge-check");
|
|
7411
|
+
const id = await import("@intentius/chant/ci-identity");
|
|
7412
|
+
if (!FORGE_CHECK_FUNCTIONS.every((f) => typeof fc[f] === "function") || typeof id.resolveCloudIdentity !== "function") return void 0;
|
|
7413
|
+
return { ...fc, resolveCloudIdentity: id.resolveCloudIdentity };
|
|
7246
7414
|
} catch {
|
|
7247
7415
|
return void 0;
|
|
7248
7416
|
}
|
|
7249
7417
|
})();
|
|
7250
|
-
return
|
|
7418
|
+
return forgeCheckModule;
|
|
7251
7419
|
}
|
|
7252
|
-
function
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
return false;
|
|
7261
|
-
}
|
|
7262
|
-
})();
|
|
7263
|
-
return cachedRollback;
|
|
7264
|
-
}
|
|
7265
|
-
function needsStackOpsMessage(what, feature) {
|
|
7266
|
-
return `${what} needs @intentius/chant-lexicon-pulumi >= the release after 0.124.54, 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'.`;
|
|
7267
|
-
}
|
|
7268
|
-
function readConfig(cwd) {
|
|
7269
|
-
const path = findConfig(cwd);
|
|
7270
|
-
if (!path) return { error: `no plywood.yml in ${cwd} or above it; run '${PRODUCT} init' first` };
|
|
7271
|
-
const config2 = loadConfig(path);
|
|
7272
|
-
const errors = checkConfig(config2).filter((p) => p.severity === "error");
|
|
7273
|
-
if (errors.length) return { error: errors.map((p) => `${path}: ${p.key} ${p.message}`).join("\n") };
|
|
7274
|
-
return { config: config2, path };
|
|
7275
|
-
}
|
|
7276
|
-
function resolveStack(config2, path, key, dir) {
|
|
7277
|
-
const [project, stack, extra] = key.split("/");
|
|
7278
|
-
if (!project || !stack || extra !== void 0) return { error: `a stack is project/stack, as plywood.yml lists it, not '${key}'` };
|
|
7279
|
-
const settings = toChantSettings(config2);
|
|
7280
|
-
const listed = stacksOf(config2.doc);
|
|
7281
|
-
const root = dirname14(path);
|
|
7282
|
-
const member = settings.members.find((m) => m.name === key) ?? settings.memberDefaults;
|
|
7283
|
-
if (dir === void 0 && Object.keys(listed).length > 0 && !(key in listed)) {
|
|
7284
|
-
return { error: `${key} is not under stacks in ${path}; the stacks are ${Object.keys(listed).join(", ")} (or name its directory with --dir)` };
|
|
7420
|
+
function workspaceMembers(root) {
|
|
7421
|
+
const file = join18(root, "chant.workspace.json");
|
|
7422
|
+
if (!existsSync15(file)) return [];
|
|
7423
|
+
try {
|
|
7424
|
+
const doc = JSON.parse(readFileSync16(file, "utf8"));
|
|
7425
|
+
return (doc.members ?? []).map((m) => m.name).filter((n) => typeof n === "string");
|
|
7426
|
+
} catch {
|
|
7427
|
+
return [];
|
|
7285
7428
|
}
|
|
7286
|
-
const at = dir ?? listed[key]?.dir ?? ".";
|
|
7287
|
-
return {
|
|
7288
|
-
key,
|
|
7289
|
-
project,
|
|
7290
|
-
stack,
|
|
7291
|
-
dir: isAbsolute2(at) ? at : resolve3(root, at),
|
|
7292
|
-
...member.engine ? { engine: member.engine } : {},
|
|
7293
|
-
...member.backend ? { backend: member.backend } : {}
|
|
7294
|
-
};
|
|
7295
|
-
}
|
|
7296
|
-
function requestRules(config2) {
|
|
7297
|
-
const p = toChantSettings(config2).pipeline;
|
|
7298
|
-
return { mode: p.approval ?? "ledger", ...p.quorum?.length ? { quorum: p.quorum } : {} };
|
|
7299
7429
|
}
|
|
7300
|
-
function
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
if (
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
}
|
|
7309
|
-
function approveRequestLine(gate, digest, mode) {
|
|
7310
|
-
return `${PRODUCT} approve ${gate} --op ${STACK_OPS_OP} --plan ${digest}${mode === "sealed" ? " --sign" : ""}`;
|
|
7311
|
-
}
|
|
7312
|
-
function useBackend(stacks2, env) {
|
|
7313
|
-
const named = [...new Set(stacks2.map((s) => s.backend).filter((b) => b !== void 0))];
|
|
7314
|
-
if (named.length > 1) return `the stacks are on different backends (${named.join(", ")}); a move needs both on one backend`;
|
|
7315
|
-
if (named[0] && !env.PULUMI_BACKEND_URL) env.PULUMI_BACKEND_URL = named[0];
|
|
7316
|
-
return void 0;
|
|
7430
|
+
function upRunner(runner) {
|
|
7431
|
+
if (runner === void 0) return void 0;
|
|
7432
|
+
const staged = typeof runner === "object" && !Array.isArray(runner) && !("group" in runner);
|
|
7433
|
+
const spec = staged ? runner.up ?? runner.default : runner;
|
|
7434
|
+
if (spec === void 0) return void 0;
|
|
7435
|
+
if (typeof spec === "string") return { labels: [spec] };
|
|
7436
|
+
if (Array.isArray(spec)) return { labels: spec.filter((l) => typeof l === "string") };
|
|
7437
|
+
const g = spec;
|
|
7438
|
+
return typeof g.group === "string" ? { labels: Array.isArray(g.labels) ? g.labels : [], group: g.group } : void 0;
|
|
7317
7439
|
}
|
|
7318
|
-
function
|
|
7319
|
-
const
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
case "refused":
|
|
7328
|
-
code = table.waiting;
|
|
7329
|
-
text = `${outcome.reason}. ${STACK_OP_WORDS.waiting(verb, approveLine)}`;
|
|
7330
|
-
break;
|
|
7331
|
-
case "spent":
|
|
7332
|
-
code = table.failed;
|
|
7333
|
-
text = STACK_OP_WORDS.spent(verb);
|
|
7334
|
-
break;
|
|
7335
|
-
case "done":
|
|
7336
|
-
code = table.done;
|
|
7337
|
-
text = done(outcome.result);
|
|
7338
|
-
break;
|
|
7339
|
-
case "failed":
|
|
7340
|
-
code = table.failed;
|
|
7341
|
-
text = `${PRODUCT} ${verb}: ${outcome.error}`;
|
|
7342
|
-
break;
|
|
7343
|
-
}
|
|
7344
|
-
if (json) {
|
|
7345
|
-
io.out(JSON.stringify({
|
|
7346
|
-
command: verb,
|
|
7347
|
-
ok: code === table.done,
|
|
7348
|
-
exitCode: code,
|
|
7349
|
-
status: outcome.status,
|
|
7350
|
-
op: STACK_OPS_OP,
|
|
7351
|
-
gate,
|
|
7352
|
-
digest: outcome.digest,
|
|
7353
|
-
...approveLine ? { approve: approveLine } : {},
|
|
7354
|
-
...outcome.status === "done" ? { result: outcome.result } : {},
|
|
7355
|
-
...outcome.status === "failed" ? { error: outcome.error } : {},
|
|
7356
|
-
...outcome.status === "refused" ? { refusedBy: outcome.by, reason: outcome.reason } : {}
|
|
7357
|
-
}));
|
|
7358
|
-
} else (code === table.done ? io.out : io.err)(text);
|
|
7359
|
-
return code;
|
|
7440
|
+
function contractPipelines(fc, cloud, env, members, runner, jobs2) {
|
|
7441
|
+
const applyRunner = upRunner(runner);
|
|
7442
|
+
const list = members.length ? [...members] : [void 0];
|
|
7443
|
+
return list.flatMap((member) => {
|
|
7444
|
+
const identity2 = fc.resolveCloudIdentity(cloud, env, member);
|
|
7445
|
+
const name = member ? `${member}/${env}` : env;
|
|
7446
|
+
const forgeEnvironment = jobs2 ? environmentFor(name, jobs2)?.name : void 0;
|
|
7447
|
+
return identity2 ? [{ name, identity: identity2, ...member ? { member } : {}, ...applyRunner ? { applyRunner } : {}, ...forgeEnvironment ? { forgeEnvironment } : {} }] : [];
|
|
7448
|
+
});
|
|
7360
7449
|
}
|
|
7361
|
-
async function
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7450
|
+
async function forgeCheck(args, json, table, io) {
|
|
7451
|
+
if (args[0] !== "check") return usage(`unknown forge command '${args[0] ?? ""}'. Run '${PRODUCT} forge check'.`, table, io);
|
|
7452
|
+
const fc = await chantForgeCheck();
|
|
7453
|
+
if (!fc) {
|
|
7454
|
+
if (json) io.out(JSON.stringify({ command: "forge check", ok: false, exitCode: table.failed, error: FORGE_CHECK_MESSAGE }));
|
|
7455
|
+
else io.err(`${PRODUCT}: ${FORGE_CHECK_MESSAGE}`);
|
|
7365
7456
|
return table.failed;
|
|
7366
7457
|
}
|
|
7458
|
+
const requireClouds = args.includes("--require-clouds");
|
|
7459
|
+
let rest = args.slice(1).filter((a) => a !== "--require-clouds");
|
|
7460
|
+
const flags = {};
|
|
7461
|
+
for (const flag of ["--file", "--env", "--forge", "--repo", "--branch"]) {
|
|
7462
|
+
const f = takeFlag(rest, flag);
|
|
7463
|
+
if (f.missing) return usage(`${flag} takes a value`, table, io);
|
|
7464
|
+
flags[flag] = f.value;
|
|
7465
|
+
rest = f.rest;
|
|
7466
|
+
}
|
|
7467
|
+
if (rest.length) return usage(`forge check does not take ${rest.join(" ")}`, table, io);
|
|
7468
|
+
const forgeFlag = flags["--forge"];
|
|
7469
|
+
if (forgeFlag !== void 0 && !FORGES2.includes(forgeFlag)) return usage(`--forge is ${forgeFlag}; give github, gitlab or forgejo`, table, io);
|
|
7470
|
+
const path = flags["--file"] ?? findConfig(io.cwd);
|
|
7471
|
+
if (!path) return usage(`no plywood.yml in ${io.cwd} or above it; run '${PRODUCT} init', or pass --file`, table, io);
|
|
7472
|
+
let config2;
|
|
7367
7473
|
try {
|
|
7368
|
-
|
|
7474
|
+
config2 = loadConfig(path);
|
|
7369
7475
|
} catch (e) {
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7476
|
+
return usage(e instanceof ConfigError ? e.message : `${path}: ${e.message}`, table, io);
|
|
7477
|
+
}
|
|
7478
|
+
const errors = checkConfig(config2).filter((p) => p.severity === "error");
|
|
7479
|
+
if (errors.length) {
|
|
7480
|
+
for (const p of errors) io.err(`${path}: ${p.key} ${p.message}`);
|
|
7481
|
+
if (json) io.out(JSON.stringify({ command: "forge check", ok: false, exitCode: table.failed, file: path, problems: errors }));
|
|
7373
7482
|
return table.failed;
|
|
7374
7483
|
}
|
|
7484
|
+
const settings = toChantSettings(config2);
|
|
7485
|
+
const kind = forgeFlag ?? settings.pipeline.forge?.kind;
|
|
7486
|
+
let target;
|
|
7487
|
+
try {
|
|
7488
|
+
target = fc.forgeCheckTarget(io.env, { ...kind && FORGES2.includes(kind) ? { kind } : {}, ...flags["--repo"] ? { repo: flags["--repo"] } : {} });
|
|
7489
|
+
} catch (e) {
|
|
7490
|
+
return usage(e.message, table, io);
|
|
7491
|
+
}
|
|
7492
|
+
const env = flags["--env"] ?? "production";
|
|
7493
|
+
const cloud = settings.ci.identity?.cloud;
|
|
7494
|
+
const pipelines = contractPipelines(fc, cloud, env, workspaceMembers(dirname13(path)), settings.pipeline.runner, settings.pipeline.jobs);
|
|
7495
|
+
const result = await fc.forgeCheck(
|
|
7496
|
+
{
|
|
7497
|
+
target,
|
|
7498
|
+
words: PRODUCT_WORDS,
|
|
7499
|
+
file: path,
|
|
7500
|
+
...requireClouds ? { requireClouds: true } : {},
|
|
7501
|
+
contract: (live) => fc.pipelineContract({
|
|
7502
|
+
forge: target.kind,
|
|
7503
|
+
repository: {
|
|
7504
|
+
host: target.host,
|
|
7505
|
+
path: live.path ?? target.repo,
|
|
7506
|
+
defaultBranch: flags["--branch"] ?? live.defaultBranch ?? "main",
|
|
7507
|
+
...live.ownerId ? { ownerId: live.ownerId } : {},
|
|
7508
|
+
...live.repoId ? { repoId: live.repoId } : {}
|
|
7509
|
+
},
|
|
7510
|
+
pipelines,
|
|
7511
|
+
words: PRODUCT_WORDS
|
|
7512
|
+
})
|
|
7513
|
+
},
|
|
7514
|
+
{ fetch: globalThis.fetch, env: io.env }
|
|
7515
|
+
);
|
|
7516
|
+
if (json) io.out(JSON.stringify(result));
|
|
7517
|
+
else io.out(fc.describeForgeCheck(result));
|
|
7518
|
+
if (!cloud) io.err(`${PRODUCT} forge check: ${path} sets no identity, so no trust was checked`);
|
|
7519
|
+
else if (pipelines.length === 0) io.err(`${PRODUCT} forge check: no identity rule covers environment ${env}, so no trust was checked`);
|
|
7520
|
+
return fc.forgeCheckExit(result);
|
|
7375
7521
|
}
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7522
|
+
|
|
7523
|
+
// src/cli.ts
|
|
7524
|
+
init_control2();
|
|
7525
|
+
|
|
7526
|
+
// src/telemetry.ts
|
|
7527
|
+
init_chant_product();
|
|
7528
|
+
init_observe();
|
|
7529
|
+
init_config();
|
|
7530
|
+
init_words();
|
|
7531
|
+
function telemetryEnv(doc, env) {
|
|
7532
|
+
const t = telemetryOf(doc);
|
|
7533
|
+
const out = {};
|
|
7534
|
+
const set = (k, v) => {
|
|
7535
|
+
if (v !== void 0 && v !== "" && env[k] === void 0) out[k] = v;
|
|
7536
|
+
};
|
|
7537
|
+
if (!t.traces) {
|
|
7538
|
+
set("OTEL_TRACES_EXPORTER", "none");
|
|
7539
|
+
return out;
|
|
7540
|
+
}
|
|
7541
|
+
if (env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT === void 0) set("OTEL_EXPORTER_OTLP_ENDPOINT", t.endpoint);
|
|
7542
|
+
if (t.headersEnv) set("OTEL_EXPORTER_OTLP_HEADERS", env[t.headersEnv]);
|
|
7543
|
+
if (t.endpoint !== void 0 || env.OTEL_EXPORTER_OTLP_ENDPOINT !== void 0 || env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT !== void 0) set("OTEL_SERVICE_NAME", t.serviceName);
|
|
7544
|
+
return out;
|
|
7382
7545
|
}
|
|
7383
|
-
async function
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
const stack = resolveStack(read.config, read.path, named[0], common.dir);
|
|
7396
|
-
if ("error" in stack) return usage(stack.error, table, io);
|
|
7397
|
-
useBackend([stack], io.env);
|
|
7398
|
-
const rules = requestRules(read.config);
|
|
7399
|
-
const outFile = resolve3(io.cwd, out.value);
|
|
7400
|
-
return runRequest("export", json, table, io, async (ops) => {
|
|
7401
|
-
const outcome = await ops.gatedStackExport({
|
|
7402
|
-
by: requesterOf(common.by, io.env),
|
|
7403
|
-
rules,
|
|
7404
|
-
op: STACK_OPS_OP,
|
|
7405
|
-
ledgerCwd: io.cwd,
|
|
7406
|
-
env: io.env,
|
|
7407
|
-
dir: stack.dir,
|
|
7408
|
-
stack: stack.stack,
|
|
7409
|
-
out: outFile,
|
|
7410
|
-
...stack.engine ? { engine: stack.engine } : {}
|
|
7411
|
-
});
|
|
7412
|
-
return reportOutcome("export", outcome, stack.stack, rules, (r) => STACK_OP_WORDS.exported(stack.key, r), json, table, io);
|
|
7413
|
-
});
|
|
7546
|
+
async function applyTelemetry(cwd, env = process.env) {
|
|
7547
|
+
let added = {};
|
|
7548
|
+
try {
|
|
7549
|
+
const file = findConfig(cwd);
|
|
7550
|
+
if (file) added = telemetryEnv(loadConfig(file).doc, env);
|
|
7551
|
+
} catch {
|
|
7552
|
+
return {};
|
|
7553
|
+
}
|
|
7554
|
+
Object.assign(env, added);
|
|
7555
|
+
const chant = await chantProduct();
|
|
7556
|
+
if (chant && typeof chant.configureOtlp === "function") chant.configureOtlp({ prefix: PRODUCT, env });
|
|
7557
|
+
return added;
|
|
7414
7558
|
}
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
}
|
|
7559
|
+
|
|
7560
|
+
// src/cli.ts
|
|
7561
|
+
init_init();
|
|
7562
|
+
init_stages();
|
|
7563
|
+
init_tips();
|
|
7564
|
+
init_images();
|
|
7565
|
+
init_up();
|
|
7566
|
+
|
|
7567
|
+
// src/upgrade.ts
|
|
7568
|
+
init_config();
|
|
7569
|
+
init_images();
|
|
7570
|
+
init_stages();
|
|
7571
|
+
init_words();
|
|
7572
|
+
import { execFile } from "node:child_process";
|
|
7573
|
+
import { existsSync as existsSync16, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "node:fs";
|
|
7574
|
+
import { dirname as dirname14, join as join19, relative as relative2, resolve as resolve3 } from "node:path";
|
|
7575
|
+
var PLYWOOD_PACKAGE = "@intentius/plywood";
|
|
7576
|
+
var CHANT_PACKAGE = "@intentius/chant";
|
|
7577
|
+
var CHANT_FAMILY = /^@intentius\/chant(?:-lexicon-[a-z0-9-]+)?$/;
|
|
7578
|
+
var VERSION2 = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
7579
|
+
var DEP_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
7580
|
+
function parseUpgradeArgs(args) {
|
|
7581
|
+
const chant = takeFlag(args, "--chant");
|
|
7582
|
+
const forge = takeFlag(chant.rest, "--forge");
|
|
7583
|
+
const env = takeFlag(forge.rest, "--env");
|
|
7584
|
+
if (chant.missing || forge.missing || env.missing) return { error: "--chant, --forge and --env each take a value" };
|
|
7585
|
+
const flags = /* @__PURE__ */ new Set(["--no-install", "--dry-run"]);
|
|
7586
|
+
const unknown = env.rest.filter((a) => a.startsWith("-") && !flags.has(a));
|
|
7587
|
+
if (unknown.length) return { error: `upgrade does not take ${unknown.join(" ")}` };
|
|
7588
|
+
const words = env.rest.filter((a) => !a.startsWith("-"));
|
|
7589
|
+
if (words.length > 1) return { error: `upgrade takes one version, not ${words.join(" ")}` };
|
|
7590
|
+
const version2 = words[0]?.replace(/^v/, "");
|
|
7591
|
+
if (version2 !== void 0 && !VERSION2.test(version2)) return { error: `${words[0]} is not a version: give it as MAJOR.MINOR.PATCH` };
|
|
7592
|
+
if (chant.value !== void 0 && !VERSION2.test(chant.value)) return { error: `--chant ${chant.value} is not a version: give it as MAJOR.MINOR.PATCH` };
|
|
7593
|
+
return {
|
|
7594
|
+
...version2 ? { version: version2 } : {},
|
|
7595
|
+
...chant.value ? { chant: chant.value } : {},
|
|
7596
|
+
...forge.value ? { forge: forge.value } : {},
|
|
7597
|
+
...env.value ? { env: env.value } : {},
|
|
7598
|
+
install: !env.rest.includes("--no-install"),
|
|
7599
|
+
dryRun: env.rest.includes("--dry-run")
|
|
7600
|
+
};
|
|
7448
7601
|
}
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
if (bad.length) return usage(`move takes URNs (urn:pulumi:...), not ${bad.join(", ")}; 'pulumi stack --show-urns' lists them`, table, io);
|
|
7466
|
-
if (from.value === to.value) return usage(`--from and --to are both ${from.value}; a move is between two stacks`, table, io);
|
|
7467
|
-
const read = readConfig(io.cwd);
|
|
7468
|
-
if ("error" in read) return usage(read.error, table, io);
|
|
7469
|
-
const source = resolveStack(read.config, read.path, from.value, void 0);
|
|
7470
|
-
if ("error" in source) return usage(source.error, table, io);
|
|
7471
|
-
const dest = resolveStack(read.config, read.path, to.value, void 0);
|
|
7472
|
-
if ("error" in dest) return usage(dest.error, table, io);
|
|
7473
|
-
const backend = useBackend([source, dest], io.env);
|
|
7474
|
-
if (backend) return usage(backend, table, io);
|
|
7475
|
-
const rules = requestRules(read.config);
|
|
7476
|
-
const destName = destStackName(source, dest, read.config);
|
|
7477
|
-
const engine2 = source.engine ?? dest.engine;
|
|
7478
|
-
return runRequest("move", json, table, io, async (ops) => {
|
|
7479
|
-
const outcome = await ops.gatedStackMove({
|
|
7480
|
-
by: requesterOf(common.by, io.env),
|
|
7481
|
-
rules,
|
|
7482
|
-
op: STACK_OPS_OP,
|
|
7483
|
-
ledgerCwd: io.cwd,
|
|
7484
|
-
env: io.env,
|
|
7485
|
-
source: { member: source.key, dir: source.dir, stack: source.stack },
|
|
7486
|
-
dest: { member: dest.key, dir: dest.dir, stack: destName },
|
|
7487
|
-
urns,
|
|
7488
|
-
...engine2 ? { engine: engine2 } : {}
|
|
7489
|
-
});
|
|
7490
|
-
return reportOutcome("move", outcome, `move:${source.stack}:${destName}`, rules, (r) => STACK_OP_WORDS.moved(source.key, dest.key, r), json, table, io);
|
|
7491
|
-
});
|
|
7602
|
+
function findPlywoodPackage(dir) {
|
|
7603
|
+
let cur = resolve3(dir);
|
|
7604
|
+
for (let depth = 0; depth < 32; depth++) {
|
|
7605
|
+
const file = join19(cur, "package.json");
|
|
7606
|
+
if (existsSync16(file)) {
|
|
7607
|
+
try {
|
|
7608
|
+
const pkg = JSON.parse(readFileSync17(file, "utf8"));
|
|
7609
|
+
if (DEP_FIELDS.some((f) => isObject2(pkg[f]) && typeof pkg[f][PLYWOOD_PACKAGE] === "string")) return file;
|
|
7610
|
+
} catch {
|
|
7611
|
+
}
|
|
7612
|
+
}
|
|
7613
|
+
const up2 = dirname14(cur);
|
|
7614
|
+
if (up2 === cur) return void 0;
|
|
7615
|
+
cur = up2;
|
|
7616
|
+
}
|
|
7617
|
+
return void 0;
|
|
7492
7618
|
}
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
const
|
|
7500
|
-
|
|
7501
|
-
const
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7619
|
+
function movedRange(range, version2) {
|
|
7620
|
+
const m = /^([~^]?)(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.exec(range.trim());
|
|
7621
|
+
if (!m) return void 0;
|
|
7622
|
+
return `${m[1]}${version2}`;
|
|
7623
|
+
}
|
|
7624
|
+
function movePackagePins(text, file, plywood, chant) {
|
|
7625
|
+
const pkg = JSON.parse(text);
|
|
7626
|
+
const moves = [];
|
|
7627
|
+
const left = [];
|
|
7628
|
+
for (const field of DEP_FIELDS) {
|
|
7629
|
+
const deps = pkg[field];
|
|
7630
|
+
if (!isObject2(deps)) continue;
|
|
7631
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
7632
|
+
if (typeof range !== "string") continue;
|
|
7633
|
+
const target = name === PLYWOOD_PACKAGE ? plywood : CHANT_FAMILY.test(name) ? chant : void 0;
|
|
7634
|
+
if (target === void 0) continue;
|
|
7635
|
+
const next = movedRange(range, target);
|
|
7636
|
+
if (next === void 0) {
|
|
7637
|
+
left.push(`${field}.${name} is '${range}'; move it to ${target} by hand`);
|
|
7638
|
+
continue;
|
|
7639
|
+
}
|
|
7640
|
+
if (next === range) continue;
|
|
7641
|
+
deps[name] = next;
|
|
7642
|
+
moves.push({ file, name, from: range, to: next });
|
|
7643
|
+
}
|
|
7507
7644
|
}
|
|
7508
|
-
|
|
7509
|
-
return withChant(`${PRODUCT} move --onto plunge`, table, io, async (chant) => {
|
|
7510
|
-
const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
7511
|
-
if (code === table.waiting && !json) io.err(STACK_OP_WORDS.adoptWaiting);
|
|
7512
|
-
return code === table.waiting ? code : explainRefusal(code, table, io.err);
|
|
7513
|
-
});
|
|
7645
|
+
return { text: moves.length ? jsonText2(pkg, text) : text, moves, left };
|
|
7514
7646
|
}
|
|
7515
|
-
|
|
7516
|
-
const
|
|
7517
|
-
const
|
|
7518
|
-
const
|
|
7519
|
-
const
|
|
7520
|
-
|
|
7521
|
-
if (
|
|
7647
|
+
function moveDeclarationPins(text, file, chant) {
|
|
7648
|
+
const doc = JSON.parse(text);
|
|
7649
|
+
const moves = [];
|
|
7650
|
+
const pins = Array.isArray(doc.pins) ? doc.pins : [];
|
|
7651
|
+
for (const pin of pins) {
|
|
7652
|
+
if (typeof pin.package !== "string" || !CHANT_FAMILY.test(pin.package) || typeof pin.version !== "string") continue;
|
|
7653
|
+
if (pin.version === chant) continue;
|
|
7654
|
+
moves.push({ file, name: pin.package, from: pin.version, to: chant });
|
|
7655
|
+
pin.version = chant;
|
|
7522
7656
|
}
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
const
|
|
7528
|
-
if (
|
|
7529
|
-
|
|
7530
|
-
if (
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
}
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
if ("error" in stack) return usage(stack.error, table, io);
|
|
7541
|
-
argv = ["components", "rollback", env.value ?? environmentOf(stack.stack), "--component", slug(stack.key), ...digest.value ? ["--digest", digest.value] : []];
|
|
7657
|
+
return { text: moves.length ? jsonText2(doc, text) : text, moves };
|
|
7658
|
+
}
|
|
7659
|
+
function imageNote(doc, version2) {
|
|
7660
|
+
const image = doc.image;
|
|
7661
|
+
const name = typeof image === "string" ? image : isObject2(image) && typeof image.name === "string" ? image.name : void 0;
|
|
7662
|
+
if (name === void 0) return void 0;
|
|
7663
|
+
const m = new RegExp(`^${REGISTRY.replace(/[.]/g, "\\.")}/plywood-(pulumi|plunge):([^@]+)`).exec(name);
|
|
7664
|
+
if (m && m[2].startsWith(`${version2}-`)) return void 0;
|
|
7665
|
+
if (m) return `plywood.yml's image is plywood's ${m[1]} image at ${m[2]}; name the ${version2} image in it (its tag starts ${version2}-), or drop the key to take the release's own`;
|
|
7666
|
+
return `plywood.yml's image is your own (${name}); rebuild it FROM plywood's ${version2} image and name the new digest in it`;
|
|
7667
|
+
}
|
|
7668
|
+
async function viewRelease(exec, cwd, env, version2) {
|
|
7669
|
+
const spec = `${PLYWOOD_PACKAGE}@${version2 ?? "latest"}`;
|
|
7670
|
+
const r = await exec("npm", ["view", spec, "version", "dependencies", "--json"], { cwd, env });
|
|
7671
|
+
if (r.code !== 0) {
|
|
7672
|
+
const notFound = /E404/.test(r.stderr + r.stdout);
|
|
7673
|
+
return { error: notFound ? `npm has no ${spec}` : `npm view ${spec} failed: ${(r.stderr || r.stdout).trim().split("\n")[0] ?? ""}` };
|
|
7542
7674
|
}
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7675
|
+
let parsed;
|
|
7676
|
+
try {
|
|
7677
|
+
parsed = JSON.parse(r.stdout);
|
|
7678
|
+
} catch {
|
|
7679
|
+
return { error: `npm view ${spec} printed no JSON` };
|
|
7680
|
+
}
|
|
7681
|
+
const doc = Array.isArray(parsed) ? parsed[parsed.length - 1] : parsed;
|
|
7682
|
+
if (!isObject2(doc) || typeof doc.version !== "string") return { error: `npm view ${spec} named no version` };
|
|
7683
|
+
const deps = isObject2(doc.dependencies) ? doc.dependencies : {};
|
|
7684
|
+
const chant = deps[CHANT_PACKAGE];
|
|
7685
|
+
if (typeof chant !== "string" || !VERSION2.test(chant)) return { error: `${PLYWOOD_PACKAGE}@${doc.version} pins no exact ${CHANT_PACKAGE}` };
|
|
7686
|
+
return { version: doc.version, chant };
|
|
7687
|
+
}
|
|
7688
|
+
async function upgrade(args, json, table, io) {
|
|
7689
|
+
const opts = parseUpgradeArgs(args);
|
|
7690
|
+
const fail = (message) => {
|
|
7691
|
+
if (json) io.out(JSON.stringify({ command: "upgrade", ok: false, exitCode: table.failed, error: message }));
|
|
7692
|
+
else io.err(`${PRODUCT}: ${message}`);
|
|
7546
7693
|
return table.failed;
|
|
7694
|
+
};
|
|
7695
|
+
if ("error" in opts) return fail(opts.error);
|
|
7696
|
+
const exec = io.exec ?? execChild;
|
|
7697
|
+
const pkgFile = findPlywoodPackage(io.cwd);
|
|
7698
|
+
if (!pkgFile) return fail(`no package.json in ${io.cwd} or above it depends on ${PLYWOOD_PACKAGE}; install it with 'npm i -D ${PLYWOOD_PACKAGE}', then run '${PRODUCT} init'`);
|
|
7699
|
+
const pkgDir = dirname14(pkgFile);
|
|
7700
|
+
const configFile = findConfig(io.cwd);
|
|
7701
|
+
const configDir = configFile ? dirname14(configFile) : pkgDir;
|
|
7702
|
+
const rel = (f) => relative2(io.cwd, f) || f;
|
|
7703
|
+
const release2 = await viewRelease(exec, pkgDir, io.env, opts.version);
|
|
7704
|
+
if ("error" in release2) return fail(release2.error);
|
|
7705
|
+
const chant = opts.chant ?? release2.chant;
|
|
7706
|
+
const pkgText = readFileSync17(pkgFile, "utf8");
|
|
7707
|
+
const pkgMove = movePackagePins(pkgText, rel(pkgFile), release2.version, chant);
|
|
7708
|
+
const declFile = join19(configDir, "chant.workspace.json");
|
|
7709
|
+
const declText = existsSync16(declFile) ? readFileSync17(declFile, "utf8") : void 0;
|
|
7710
|
+
let declMove;
|
|
7711
|
+
if (declText !== void 0) {
|
|
7712
|
+
try {
|
|
7713
|
+
declMove = moveDeclarationPins(declText, rel(declFile), chant);
|
|
7714
|
+
} catch (e) {
|
|
7715
|
+
return fail(`${rel(declFile)} is not JSON upgrade can move pins in (${e.message})`);
|
|
7716
|
+
}
|
|
7547
7717
|
}
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7718
|
+
const notes = [...pkgMove.left];
|
|
7719
|
+
if (existsSync16(join19(configDir, "chant.workspace.jsonc"))) notes.push(`chant.workspace.jsonc is yours: move its @intentius/chant* pins to ${chant} by hand`);
|
|
7720
|
+
if (configFile) {
|
|
7721
|
+
try {
|
|
7722
|
+
const note = imageNote(loadConfig(configFile).doc, release2.version);
|
|
7723
|
+
if (note) notes.push(note);
|
|
7724
|
+
} catch {
|
|
7725
|
+
}
|
|
7726
|
+
} else {
|
|
7727
|
+
notes.push(`no plywood.yml, so there is no workflow to write again; run '${PRODUCT} init' after the install`);
|
|
7728
|
+
}
|
|
7729
|
+
const moves = [...pkgMove.moves, ...declMove?.moves ?? []];
|
|
7730
|
+
const ciArgs = ["--no-install", PRODUCT, "ci", ...opts.forge ? ["--forge", opts.forge] : [], ...opts.env ? ["--env", opts.env] : []];
|
|
7731
|
+
const steps = {
|
|
7732
|
+
install: { cwd: rel(pkgDir) || ".", command: "npm install --no-audit --no-fund" },
|
|
7733
|
+
ci: { cwd: rel(configDir) || ".", command: `npx ${ciArgs.join(" ")}` }
|
|
7734
|
+
};
|
|
7735
|
+
const envelope = { command: "upgrade", plywood: release2.version, chant, moves, notes, dryRun: opts.dryRun };
|
|
7736
|
+
const say = (line) => {
|
|
7737
|
+
if (!json) io.err(line);
|
|
7738
|
+
};
|
|
7739
|
+
say(`${PRODUCT} upgrade: ${PLYWOOD_PACKAGE} ${release2.version}, ${CHANT_PACKAGE} ${chant}`);
|
|
7740
|
+
for (const m of moves) say(` ${m.file} ${m.name} ${m.from} -> ${m.to}`);
|
|
7741
|
+
if (moves.length === 0) say(" every pin is already there");
|
|
7742
|
+
if (opts.dryRun) {
|
|
7743
|
+
say(` then: ${steps.install.command} (in ${steps.install.cwd}), then ${steps.ci.command} (in ${steps.ci.cwd})`);
|
|
7744
|
+
for (const n of notes) say(` note: ${n}`);
|
|
7745
|
+
if (json) io.out(JSON.stringify({ ...envelope, ok: true, exitCode: table.done, steps }));
|
|
7746
|
+
return table.done;
|
|
7747
|
+
}
|
|
7748
|
+
if (pkgMove.text !== pkgText) writeFileSync9(pkgFile, pkgMove.text);
|
|
7749
|
+
if (declMove && declText !== void 0 && declMove.text !== declText) writeFileSync9(declFile, declMove.text);
|
|
7750
|
+
const files = [...new Set(moves.map((m) => m.file))];
|
|
7751
|
+
if (!opts.install) {
|
|
7752
|
+
say(` left to run: ${steps.install.command} (in ${steps.install.cwd}), then ${steps.ci.command} (in ${steps.ci.cwd})`);
|
|
7753
|
+
for (const n of notes) say(` note: ${n}`);
|
|
7754
|
+
if (json) io.out(JSON.stringify({ ...envelope, ok: true, exitCode: table.done, files, steps }));
|
|
7755
|
+
return table.done;
|
|
7756
|
+
}
|
|
7757
|
+
say(` $ ${steps.install.command}`);
|
|
7758
|
+
const installed2 = await exec("npm", ["install", "--no-audit", "--no-fund"], { cwd: pkgDir, env: io.env });
|
|
7759
|
+
if (installed2.code !== 0) {
|
|
7760
|
+
for (const n of notes) say(` note: ${n}`);
|
|
7761
|
+
return fail(`npm install failed (exit ${installed2.code}); package.json is moved, so fix the install and run '${steps.ci.command}'
|
|
7762
|
+
${installed2.stderr.trim()}`);
|
|
7763
|
+
}
|
|
7764
|
+
const got = installedVersion(pkgDir);
|
|
7765
|
+
if (got !== void 0 && got !== release2.version) notes.push(`node_modules holds ${PLYWOOD_PACKAGE} ${got}, not ${release2.version}; check the install`);
|
|
7766
|
+
let ci2;
|
|
7767
|
+
if (configFile) {
|
|
7768
|
+
say(` $ ${steps.ci.command}`);
|
|
7769
|
+
ci2 = await exec("npx", ciArgs, { cwd: configDir, env: io.env });
|
|
7770
|
+
if (!json && ci2.stderr.trim()) io.err(ci2.stderr.trimEnd());
|
|
7771
|
+
}
|
|
7772
|
+
for (const n of notes) say(` note: ${n}`);
|
|
7773
|
+
const ok = ci2 === void 0 || ci2.code === 0;
|
|
7774
|
+
const lockFile = join19(pkgDir, "package-lock.json");
|
|
7775
|
+
const changed = [...files, ...existsSync16(lockFile) ? [rel(lockFile)] : []];
|
|
7776
|
+
if (ok) say(` Open one pull request with ${changed.join(", ")} and the files ${PRODUCT} ci wrote.`);
|
|
7777
|
+
else say(` ${PRODUCT} ci failed (exit ${ci2?.code}); the pins are moved and installed, so fix plywood.yml and run '${steps.ci.command}'`);
|
|
7778
|
+
const code = ok ? table.done : ci2?.code ?? table.failed;
|
|
7779
|
+
if (json) io.out(JSON.stringify({ ...envelope, ok, exitCode: code, files: changed, ...ci2 ? { ci: { exitCode: ci2.code } } : {} }));
|
|
7780
|
+
return code;
|
|
7781
|
+
}
|
|
7782
|
+
function installedVersion(dir) {
|
|
7783
|
+
try {
|
|
7784
|
+
const pkg = JSON.parse(readFileSync17(join19(dir, "node_modules", PLYWOOD_PACKAGE, "package.json"), "utf8"));
|
|
7785
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
7786
|
+
} catch {
|
|
7787
|
+
return void 0;
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
function jsonText2(value, original) {
|
|
7791
|
+
const indent = /^[{[]\r?\n([ \t]+)/.exec(original)?.[1] ?? " ";
|
|
7792
|
+
return `${JSON.stringify(value, null, indent)}
|
|
7793
|
+
`;
|
|
7794
|
+
}
|
|
7795
|
+
function isObject2(v) {
|
|
7796
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7553
7797
|
}
|
|
7798
|
+
var execChild = (bin, args, opts) => new Promise((done) => {
|
|
7799
|
+
execFile(bin, args, { cwd: opts.cwd, env: opts.env, maxBuffer: 64 * 1024 * 1024, shell: process.platform === "win32" }, (error2, stdout, stderr) => {
|
|
7800
|
+
const code = error2 ? typeof error2.code === "number" ? error2.code : 1 : 0;
|
|
7801
|
+
done({ code, stdout: String(stdout), stderr: String(stderr) });
|
|
7802
|
+
});
|
|
7803
|
+
});
|
|
7554
7804
|
|
|
7555
7805
|
// src/cli.ts
|
|
7556
7806
|
init_words();
|
|
7557
|
-
var VERSION3 = true ? "0.
|
|
7807
|
+
var VERSION3 = true ? "0.2.0" : "0.0.0-dev";
|
|
7558
7808
|
var stdio = {
|
|
7559
7809
|
out: (s) => process.stdout.write(s + "\n"),
|
|
7560
7810
|
err: (s) => process.stderr.write(s + "\n")
|
|
@@ -7605,6 +7855,7 @@ function helpText() {
|
|
|
7605
7855
|
" move <stack> --onto plunge adopt the stacks onto plunge (plywood.adopt.op.ts)",
|
|
7606
7856
|
" rollback <stack> [--digest <d>] | --release <name> --env <env>",
|
|
7607
7857
|
" preview a release's program, then update behind its gate",
|
|
7858
|
+
" action <stack> <name> [--anew] run an action plywood.yml declares: replace its command resource",
|
|
7608
7859
|
"",
|
|
7609
7860
|
"Ledger:",
|
|
7610
7861
|
" audit [approvals|updates|runs|review-stacks] [--stack <s>] [--since <t>] [--where <f>=<v>]...",
|
|
@@ -7625,7 +7876,8 @@ function helpText() {
|
|
|
7625
7876
|
" control dispatch [--sha <sha>] tell plywood.yml's control repo this commit applied",
|
|
7626
7877
|
"",
|
|
7627
7878
|
"Agents:",
|
|
7628
|
-
" mcp [--report-url <url>]
|
|
7879
|
+
" mcp [--report-url <url>] [--propose] chant's read-only MCP server over stdio, with plywood's read tools;",
|
|
7880
|
+
" --propose adds request_from_template, which refuses until Pulumi templates exist",
|
|
7629
7881
|
" guard check <command line> whether the agent guard refuses a command",
|
|
7630
7882
|
" guard install [--dir <d>] the Claude Code hook and the pulumi and plunge wrappers",
|
|
7631
7883
|
"",
|
|
@@ -7686,6 +7938,7 @@ async function main(argv, io = stdio) {
|
|
|
7686
7938
|
if (verb === "cancel") return cancelStack(args.slice(1), json, table, stageIo);
|
|
7687
7939
|
if (verb === "move") return moveStack(args.slice(1), json, table, stageIo);
|
|
7688
7940
|
if (verb === "rollback") return rollbackStack(args.slice(1), json, table, stageIo);
|
|
7941
|
+
if (verb === "action") return action(args.slice(1), json, table, stageIo);
|
|
7689
7942
|
if (verb === "drift") return drift2(args.slice(1), json, table, stageIo);
|
|
7690
7943
|
if (verb === "mcp") return mcp(args.slice(1), json, table, stageIo);
|
|
7691
7944
|
if (verb === "guard") return guard(args.slice(1), json, table, stageIo);
|
|
@@ -7750,5 +8003,6 @@ function config(args, table, emit) {
|
|
|
7750
8003
|
}
|
|
7751
8004
|
|
|
7752
8005
|
// src/bin.ts
|
|
8006
|
+
installChantResolution();
|
|
7753
8007
|
process.exitCode = await main(process.argv.slice(2));
|
|
7754
8008
|
//# sourceMappingURL=plywood.mjs.map
|