@intentius/plywood 0.2.0 → 0.3.1
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 +776 -652
- package/dist/plywood.mjs.map +4 -4
- package/package.json +7 -7
- package/plywood.schema.json +6 -1
package/dist/plywood.mjs
CHANGED
|
@@ -260,13 +260,16 @@ function approvalOf(doc) {
|
|
|
260
260
|
function gateOf(doc) {
|
|
261
261
|
return typeof doc.gate === "string" && GATES.includes(doc.gate) ? doc.gate : DEFAULT_GATE;
|
|
262
262
|
}
|
|
263
|
+
function previewOf(doc) {
|
|
264
|
+
return doc.preview === true;
|
|
265
|
+
}
|
|
263
266
|
function quorumOf(rules) {
|
|
264
267
|
return Object.entries(rules).map(([glob, r]) => {
|
|
265
268
|
const roles = [...r.role !== void 0 ? [r.role] : [], ...r.roles ?? []];
|
|
266
269
|
return { targets: [glob], count: r.quorum ?? 1, ...roles.length ? { roles } : {}, notAuthor: true };
|
|
267
270
|
});
|
|
268
271
|
}
|
|
269
|
-
var GATES, DEFAULT_GATE, APPROVALS, DEFAULT_APPROVAL, APPROVAL_RULE_KEYS, approval;
|
|
272
|
+
var GATES, DEFAULT_GATE, APPROVALS, DEFAULT_APPROVAL, DEFAULT_PREVIEW, APPROVAL_RULE_KEYS, approval;
|
|
270
273
|
var init_approval = __esm({
|
|
271
274
|
"src/config/approval.ts"() {
|
|
272
275
|
"use strict";
|
|
@@ -275,13 +278,19 @@ var init_approval = __esm({
|
|
|
275
278
|
DEFAULT_GATE = "on-destructive";
|
|
276
279
|
APPROVALS = ["ledger", "sealed", "pr-review"];
|
|
277
280
|
DEFAULT_APPROVAL = "ledger";
|
|
281
|
+
DEFAULT_PREVIEW = false;
|
|
278
282
|
APPROVAL_RULE_KEYS = ["role", "roles", "quorum"];
|
|
279
283
|
approval = {
|
|
280
284
|
name: "approval",
|
|
281
|
-
keys: ["gate", "approval", "approval_rules"],
|
|
285
|
+
keys: ["gate", "approval", "approval_rules", "preview"],
|
|
282
286
|
schema: {
|
|
283
287
|
gate: { type: "string", enum: [...GATES], default: DEFAULT_GATE, description: "always, on-destructive or never." },
|
|
284
288
|
approval: { type: "string", enum: [...APPROVALS], default: DEFAULT_APPROVAL, description: "ledger, sealed or pr-review." },
|
|
289
|
+
preview: {
|
|
290
|
+
type: "boolean",
|
|
291
|
+
default: DEFAULT_PREVIEW,
|
|
292
|
+
description: "true: plywood init sets approvals.preview in the waves spec, so a merged wave applies only the plans its pull request previewed, or exits 4. Known limits: plywood#55 (the second of two disjoint merges is refused) and plywood#56 (a pull request that creates an autonamed resource is refused)."
|
|
293
|
+
},
|
|
285
294
|
approval_rules: {
|
|
286
295
|
type: "object",
|
|
287
296
|
description: "Role and quorum per stack glob. Read at base.",
|
|
@@ -298,6 +307,7 @@ var init_approval = __esm({
|
|
|
298
307
|
},
|
|
299
308
|
check(doc) {
|
|
300
309
|
const out = [...oneOf(doc.gate, GATES, "gate"), ...oneOf(doc.approval, APPROVALS, "approval")];
|
|
310
|
+
if (doc.preview !== void 0 && typeof doc.preview !== "boolean") out.push(error("preview", "must be true or false"));
|
|
301
311
|
const rules = doc.approval_rules;
|
|
302
312
|
if (rules === void 0) return out;
|
|
303
313
|
if (!isObject(rules)) return [...out, error("approval_rules", "must map a stack glob to { role, quorum }")];
|
|
@@ -422,7 +432,7 @@ var init_words = __esm({
|
|
|
422
432
|
"src/words.ts"() {
|
|
423
433
|
"use strict";
|
|
424
434
|
PRODUCT = "plywood";
|
|
425
|
-
TAGLINE = "
|
|
435
|
+
TAGLINE = "Sophisticated automation for Pulumi.";
|
|
426
436
|
OP_NAME = "pulumi-up";
|
|
427
437
|
PR_GATE = "up";
|
|
428
438
|
VERBS = [
|
|
@@ -587,13 +597,13 @@ var init_control = __esm({
|
|
|
587
597
|
var version, dependencies;
|
|
588
598
|
var init_package = __esm({
|
|
589
599
|
"package.json"() {
|
|
590
|
-
version = "0.
|
|
600
|
+
version = "0.3.1";
|
|
591
601
|
dependencies = {
|
|
592
|
-
"@intentius/chant": "0.124.
|
|
593
|
-
"@intentius/chant-lexicon-forgejo": "0.124.
|
|
594
|
-
"@intentius/chant-lexicon-github": "0.124.
|
|
595
|
-
"@intentius/chant-lexicon-gitlab": "0.124.
|
|
596
|
-
"@intentius/chant-lexicon-pulumi": "0.124.
|
|
602
|
+
"@intentius/chant": "0.124.59",
|
|
603
|
+
"@intentius/chant-lexicon-forgejo": "0.124.59",
|
|
604
|
+
"@intentius/chant-lexicon-github": "0.124.59",
|
|
605
|
+
"@intentius/chant-lexicon-gitlab": "0.124.59",
|
|
606
|
+
"@intentius/chant-lexicon-pulumi": "0.124.59"
|
|
597
607
|
};
|
|
598
608
|
}
|
|
599
609
|
});
|
|
@@ -603,8 +613,8 @@ var image_digests_default;
|
|
|
603
613
|
var init_image_digests = __esm({
|
|
604
614
|
"src/image-digests.json"() {
|
|
605
615
|
image_digests_default = {
|
|
606
|
-
"ghcr.io/intentius/plywood-pulumi:0.
|
|
607
|
-
"ghcr.io/intentius/plywood-plunge:0.
|
|
616
|
+
"ghcr.io/intentius/plywood-pulumi:0.3.1-pulumi3.272.0": "sha256:acdadb5000ade3afea20ab2f555946312281066e1df288b1007bff096514ebfd",
|
|
617
|
+
"ghcr.io/intentius/plywood-plunge:0.3.1-plunge0.2.0": "sha256:adc42bc97564ecb2074b3ee4d97f84ddb986e0f30f6a26f35b8721106c177456"
|
|
608
618
|
};
|
|
609
619
|
}
|
|
610
620
|
});
|
|
@@ -741,14 +751,14 @@ var init_images = __esm({
|
|
|
741
751
|
TOOL_VERSIONS = {
|
|
742
752
|
pulumi: "3.272.0",
|
|
743
753
|
/** A plunge release (INTENTIUS/plunge, tag plunge-v<ver>), not the Pulumi version it is built from. */
|
|
744
|
-
plunge: "0.
|
|
754
|
+
plunge: "0.2.0"
|
|
745
755
|
};
|
|
746
756
|
ARCHES = ["x64", "arm64"];
|
|
747
757
|
PINNED_SHA256 = {
|
|
748
758
|
"pulumi-v3.272.0-linux-x64.tar.gz": "58d545f2b98761051922008ebd381ecef95484d7f7a5ff7faa15d7a780ea84f6",
|
|
749
759
|
"pulumi-v3.272.0-linux-arm64.tar.gz": "4f0741901b8982c4326b702dabf483c63d2cf77c88ac42dd752b72541284a534",
|
|
750
|
-
"plunge-v0.
|
|
751
|
-
"plunge-v0.
|
|
760
|
+
"plunge-v0.2.0-linux-x64.tar.gz": "c9c73d05cca9ad214b7f44230a6162cdcb6ad64bbfc725bc94688b997b191495",
|
|
761
|
+
"plunge-v0.2.0-linux-arm64.tar.gz": "c176a82f038164085aa19c8ad6530d6022e560e124050e535dbe364ae0473357"
|
|
752
762
|
};
|
|
753
763
|
ENGINE_ROOT = "/opt/plywood";
|
|
754
764
|
JOB_ARCH = `"$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')"`;
|
|
@@ -2458,7 +2468,7 @@ var init_chant_product = __esm({
|
|
|
2458
2468
|
"src/chant-product.ts"() {
|
|
2459
2469
|
"use strict";
|
|
2460
2470
|
REQUIRED2 = ["assertProductWords", "productPipelineJobs", "approveLine", "runChantInProcess"];
|
|
2461
|
-
BUILT_CHANT = true ? "0.124.
|
|
2471
|
+
BUILT_CHANT = true ? "0.124.59" : "unknown";
|
|
2462
2472
|
}
|
|
2463
2473
|
});
|
|
2464
2474
|
|
|
@@ -2475,14 +2485,37 @@ function hasChantApprovalStatus(chant) {
|
|
|
2475
2485
|
function hasChantPullRequestApprovalStatus(chant) {
|
|
2476
2486
|
return typeof chant?.APPROVAL_STATUS_PULL_REQUEST_FLOOR === "string";
|
|
2477
2487
|
}
|
|
2488
|
+
function hasChantWavePreview() {
|
|
2489
|
+
wavePreview ??= (async () => {
|
|
2490
|
+
try {
|
|
2491
|
+
const m = await import("@intentius/chant/op");
|
|
2492
|
+
return typeof m.OP_WAVES_PREVIEW_FLOOR === "string";
|
|
2493
|
+
} catch {
|
|
2494
|
+
return false;
|
|
2495
|
+
}
|
|
2496
|
+
})();
|
|
2497
|
+
return wavePreview;
|
|
2498
|
+
}
|
|
2499
|
+
function withWavePreview(spec, forge) {
|
|
2500
|
+
const approvals = spec.approvals && typeof spec.approvals === "object" && !Array.isArray(spec.approvals) ? spec.approvals : {};
|
|
2501
|
+
const next = approvals.preview !== void 0 ? spec : { ...spec, approvals: { ...approvals, preview: true } };
|
|
2502
|
+
if (forge === "gitlab" || next.approvals.preview !== true || !Array.isArray(next.waves)) return next;
|
|
2503
|
+
const waves = next.waves.map((w) => ({
|
|
2504
|
+
...w,
|
|
2505
|
+
variables: { [WAVE_TOKEN_VARIABLE]: "${{ github.token }}", ...w.variables ?? {} }
|
|
2506
|
+
}));
|
|
2507
|
+
return { ...next, waves };
|
|
2508
|
+
}
|
|
2478
2509
|
function chantAudit() {
|
|
2479
2510
|
audit ??= import("@intentius/chant/ci-audit").then((m) => m);
|
|
2480
2511
|
return audit;
|
|
2481
2512
|
}
|
|
2482
|
-
var audit;
|
|
2513
|
+
var wavePreview, WAVE_TOKEN_VARIABLE, GITLAB_READ_TOKEN, audit;
|
|
2483
2514
|
var init_chant_next = __esm({
|
|
2484
2515
|
"src/chant-next.ts"() {
|
|
2485
2516
|
"use strict";
|
|
2517
|
+
WAVE_TOKEN_VARIABLE = "GITHUB_TOKEN";
|
|
2518
|
+
GITLAB_READ_TOKEN = "CHANT_FORGE_TOKEN";
|
|
2486
2519
|
}
|
|
2487
2520
|
});
|
|
2488
2521
|
|
|
@@ -2631,6 +2664,38 @@ var init_note_control = __esm({
|
|
|
2631
2664
|
}
|
|
2632
2665
|
});
|
|
2633
2666
|
|
|
2667
|
+
// src/init/run-group.ts
|
|
2668
|
+
function withRunGroup(text, forge, key) {
|
|
2669
|
+
if (forge !== "forgejo" || TOP_CONCURRENCY.test(text)) return text;
|
|
2670
|
+
const jobs2 = TOP_JOBS.exec(text);
|
|
2671
|
+
if (!jobs2) return text;
|
|
2672
|
+
const name = (TOP_NAME.exec(text)?.[1] ?? "plywood").replace(/^["']|["']$/g, "");
|
|
2673
|
+
const group = key === "commit" ? `${name}-\${{ github.event_name }}-\${{ github.sha }}` : `${name}-\${{ github.event_name == 'pull_request' && github.ref || github.run_id }}`;
|
|
2674
|
+
const cancel = key === "commit" ? "false" : "${{ github.event_name == 'pull_request' }}";
|
|
2675
|
+
const block = [
|
|
2676
|
+
"# Without a group Forgejo cancels the run in progress when a push starts another run of this workflow.",
|
|
2677
|
+
key === "commit" ? "# One group for each commit keeps a merge's wave jobs running to completion when another merge lands (plywood#51)." : "# A push to main and a comment get a group of their own, so the record job runs to completion (plywood#51).",
|
|
2678
|
+
"concurrency:",
|
|
2679
|
+
` group: ${yamlScalar(group)}`,
|
|
2680
|
+
` cancel-in-progress: ${yamlScalar(cancel)}`,
|
|
2681
|
+
"",
|
|
2682
|
+
""
|
|
2683
|
+
].join("\n");
|
|
2684
|
+
return text.slice(0, jobs2.index) + block + text.slice(jobs2.index);
|
|
2685
|
+
}
|
|
2686
|
+
function yamlScalar(value) {
|
|
2687
|
+
return value === "false" ? value : JSON.stringify(value);
|
|
2688
|
+
}
|
|
2689
|
+
var TOP_CONCURRENCY, TOP_JOBS, TOP_NAME;
|
|
2690
|
+
var init_run_group = __esm({
|
|
2691
|
+
"src/init/run-group.ts"() {
|
|
2692
|
+
"use strict";
|
|
2693
|
+
TOP_CONCURRENCY = /^concurrency:/m;
|
|
2694
|
+
TOP_JOBS = /^jobs:/m;
|
|
2695
|
+
TOP_NAME = /^name:\s*(.+?)\s*$/m;
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
|
|
2634
2699
|
// src/init/workflow.ts
|
|
2635
2700
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
2636
2701
|
import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
|
|
@@ -2643,7 +2708,7 @@ async function writeWorkflow(chant, input) {
|
|
|
2643
2708
|
if (previous !== void 0) writeFileSync(file, previous);
|
|
2644
2709
|
return { code: code !== 0 ? code : 1, file, status: "failed", kept: [] };
|
|
2645
2710
|
}
|
|
2646
|
-
const generated = withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl);
|
|
2711
|
+
const generated = withRunGroup(withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl), input.forge, "pull-request");
|
|
2647
2712
|
const { text, kept } = keepOwnJobs(previous, generated, input.forge);
|
|
2648
2713
|
writeFileSync(file, text);
|
|
2649
2714
|
return { code, file, status: text === previous ? "unchanged" : "written", kept };
|
|
@@ -2653,6 +2718,7 @@ var init_workflow = __esm({
|
|
|
2653
2718
|
"use strict";
|
|
2654
2719
|
init_own_jobs();
|
|
2655
2720
|
init_note_control();
|
|
2721
|
+
init_run_group();
|
|
2656
2722
|
}
|
|
2657
2723
|
});
|
|
2658
2724
|
|
|
@@ -2794,11 +2860,9 @@ async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}
|
|
|
2794
2860
|
}
|
|
2795
2861
|
const lines2 = hasChant4607(chant) ? void 0 : binLines(waves, words.bin);
|
|
2796
2862
|
for (const file of files) {
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
if (next !== text) writeFileSync2(file, next);
|
|
2801
|
-
}
|
|
2863
|
+
const text = readFileSync3(file, "utf8");
|
|
2864
|
+
const next = withRunGroup(lines2 ? withBinLines(text, lines2) : text, forge, "commit");
|
|
2865
|
+
if (next !== text) writeFileSync2(file, next);
|
|
2802
2866
|
io.err(`${PRODUCT} ci: waves workflow at ${file.slice(io.cwd.length + 1)}`);
|
|
2803
2867
|
}
|
|
2804
2868
|
if (forge === "gitlab") {
|
|
@@ -2812,6 +2876,7 @@ var init_waves_workflow = __esm({
|
|
|
2812
2876
|
"use strict";
|
|
2813
2877
|
init_chant_product();
|
|
2814
2878
|
init_images();
|
|
2879
|
+
init_run_group();
|
|
2815
2880
|
init_words();
|
|
2816
2881
|
WAVES_OUTPUT_DIR = {
|
|
2817
2882
|
github: ".github/workflows",
|
|
@@ -3121,193 +3186,573 @@ var init_jobs2 = __esm({
|
|
|
3121
3186
|
}
|
|
3122
3187
|
});
|
|
3123
3188
|
|
|
3124
|
-
// src/
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
}
|
|
3128
|
-
function
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
return `The preview moved after it was approved (on plunge, a record the saved plan read was written by another update), so nothing was updated. Preview again ('${PRODUCT} preview' or /${PRODUCT} preview), read the new change, and approve its digest with '${PRODUCT} approve ${PR_GATE} --plan <digest>'.`;
|
|
3136
|
-
case "held":
|
|
3137
|
-
return `Another update or pull request holds a lock on a resource this update touches, and chant named it above. A running update lets go when it ends; a pull request's lock goes with /${PRODUCT} unlock on that pull request. Then run ${again} again.`;
|
|
3138
|
-
case "superseded":
|
|
3139
|
-
return `A newer push stands in for this one, which stood down. Nothing needs doing: the newer run updates the stacks.`;
|
|
3140
|
-
case "denied":
|
|
3141
|
-
return `A policy denied the change. Fix it and preview again, or override it with '${PRODUCT} override <stack> --rule <id> --reason <text>' naming exactly the rules chant listed (${POLICY_RULE_FORMS}). An override is spent once its update runs.`;
|
|
3142
|
-
case "declined":
|
|
3143
|
-
return `An approver declined the change, so nothing was updated. Push a new change and preview it again.`;
|
|
3144
|
-
case "misplaced":
|
|
3145
|
-
return `The job ran outside the forge environment, branch or runner plywood.yml gives this stack, and chant's job guard named the difference above, so nothing ran. Run it from that branch on that runner, or change environments and runners in plywood.yml and run '${PRODUCT} ci' again.`;
|
|
3189
|
+
// src/init/detect.ts
|
|
3190
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
3191
|
+
import { basename, dirname as dirname5, join as join5, relative, sep } from "node:path";
|
|
3192
|
+
import { parseYAML as parseYAML2 } from "@intentius/chant/yaml";
|
|
3193
|
+
function repoRoot(dir) {
|
|
3194
|
+
let cur = dir;
|
|
3195
|
+
for (let i = 0; i < 64; i++) {
|
|
3196
|
+
if (existsSync5(join5(cur, ".git"))) return cur;
|
|
3197
|
+
const parent = dirname5(cur);
|
|
3198
|
+
if (parent === cur) return dir;
|
|
3199
|
+
cur = parent;
|
|
3146
3200
|
}
|
|
3201
|
+
return dir;
|
|
3147
3202
|
}
|
|
3148
|
-
function
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
return code;
|
|
3152
|
-
}
|
|
3153
|
-
var REFUSALS;
|
|
3154
|
-
var init_refusals = __esm({
|
|
3155
|
-
"src/refusals.ts"() {
|
|
3156
|
-
"use strict";
|
|
3157
|
-
init_words();
|
|
3158
|
-
REFUSALS = ["waiting", "moved", "held", "superseded", "denied", "declined", "misplaced"];
|
|
3159
|
-
}
|
|
3160
|
-
});
|
|
3161
|
-
|
|
3162
|
-
// src/up.ts
|
|
3163
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
3164
|
-
import { join as join5 } from "node:path";
|
|
3165
|
-
function approveTarget(gate, opts) {
|
|
3166
|
-
if (opts.op) return { op: opts.op, gate };
|
|
3167
|
-
if (PR_OP.test(gate)) return { op: gate, gate: PR_GATE };
|
|
3168
|
-
if (gate === REVIEW_GATE_WORD || gate === REVIEW_GATE) {
|
|
3169
|
-
if (!opts.pr) return { error: `a review stack is a pull request's: name it with --pr <n> (or PR_NUMBER)` };
|
|
3170
|
-
if (!/^\d+$/.test(opts.pr)) return { error: `--pr takes a pull request number, not '${opts.pr}'` };
|
|
3171
|
-
return { op: reviewOp(opts.pr), gate: REVIEW_GATE };
|
|
3172
|
-
}
|
|
3173
|
-
if (gate === PR_GATE) {
|
|
3174
|
-
if (!opts.pr) return { error: `the ${PR_GATE} gate is a pull request's: name it with --pr <n> (or PR_NUMBER), or approve pr-<n>` };
|
|
3175
|
-
if (!/^\d+$/.test(opts.pr)) return { error: `--pr takes a pull request number, not '${opts.pr}'` };
|
|
3176
|
-
return { op: opts.stack ? `pr-${opts.pr}-${opts.stack}` : `pr-${opts.pr}`, gate };
|
|
3177
|
-
}
|
|
3178
|
-
return { op: OP_NAME, gate };
|
|
3203
|
+
function posixRel(root, path) {
|
|
3204
|
+
const rel = relative(root, path);
|
|
3205
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
3179
3206
|
}
|
|
3180
|
-
function
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
const v = args[i + 1];
|
|
3187
|
-
if (v === void 0 || v.startsWith("--")) return { argv, rest, missing: a };
|
|
3188
|
-
argv.push(a, v);
|
|
3189
|
-
i++;
|
|
3190
|
-
} else if (switches.includes(a)) argv.push(a);
|
|
3191
|
-
else rest.push(a);
|
|
3207
|
+
function readYamlFile(file) {
|
|
3208
|
+
try {
|
|
3209
|
+
const doc = parseYAML2(readFileSync5(file, "utf8"));
|
|
3210
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : void 0;
|
|
3211
|
+
} catch {
|
|
3212
|
+
return void 0;
|
|
3192
3213
|
}
|
|
3193
|
-
return { argv, rest };
|
|
3194
3214
|
}
|
|
3195
|
-
|
|
3196
|
-
const
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
}
|
|
3204
|
-
if (forgeFlag.value !== void 0 && !FORGES.includes(forgeFlag.value)) return usage(`--forge ${forgeFlag.value} is not one of ${FORGES.join(", ")}`, table, io);
|
|
3205
|
-
const passed = passFlags(branchFlag.rest, APPROVE_PASS, APPROVE_SWITCHES);
|
|
3206
|
-
if (passed.missing) return usage(`${passed.missing} takes a value`, table, io);
|
|
3207
|
-
const named = positionals(passed.rest);
|
|
3208
|
-
const unknown = passed.rest.filter((a) => a.startsWith("-"));
|
|
3209
|
-
if (unknown.length) return usage(`approve does not take ${unknown.join(" ")}`, table, io);
|
|
3210
|
-
if (named.length !== 1) return usage(`approve takes one gate: '${PRODUCT} approve ${PR_GATE} --pr <n> --plan <digest>' or '${PRODUCT} approve wave-<k> --plan <digest>'`, table, io);
|
|
3211
|
-
const prNumber = pr.value ?? io.env.PR_NUMBER;
|
|
3212
|
-
const gate = named[0];
|
|
3213
|
-
const plan = passed.argv[passed.argv.indexOf("--plan") + 1];
|
|
3214
|
-
const byPlan = gate === PR_GATE && !prNumber && !op.value && passed.argv.includes("--plan");
|
|
3215
|
-
const target = byPlan ? void 0 : approveTarget(gate, { pr: prNumber, stack: stack.value, op: op.value });
|
|
3216
|
-
if (target && "error" in target) return usage(target.error, table, io);
|
|
3217
|
-
if (!passed.argv.includes("--plan") && !passed.argv.includes("--expire")) {
|
|
3218
|
-
return usage(`approve needs --plan <digest>, the digest the preview printed, so a change that moved since is never approved in its place`, table, io);
|
|
3219
|
-
}
|
|
3220
|
-
return withChant(`${PRODUCT} approve`, table, io, async (chant) => {
|
|
3221
|
-
let to = target;
|
|
3222
|
-
if (!to) {
|
|
3223
|
-
const found = await upTargetByPlan(chant, plan, { stack: stack.value, cwd: io.cwd });
|
|
3224
|
-
if ("error" in found) return usage(found.error, table, io);
|
|
3225
|
-
if (!json) io.err(`${PRODUCT} approve: ${plan} waits at pull request ${found.pr}'s ${PR_GATE} gate (op ${found.op})`);
|
|
3226
|
-
to = { op: found.op, gate: PR_GATE };
|
|
3215
|
+
function findPulumiProjects(root) {
|
|
3216
|
+
const out = [];
|
|
3217
|
+
const walk = (dir, depth) => {
|
|
3218
|
+
let entries;
|
|
3219
|
+
try {
|
|
3220
|
+
entries = readdirSync3(dir).sort();
|
|
3221
|
+
} catch {
|
|
3222
|
+
return;
|
|
3227
3223
|
}
|
|
3228
|
-
const
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3224
|
+
const file = ["Pulumi.yaml", "Pulumi.yml"].find((n) => entries.includes(n));
|
|
3225
|
+
if (file) {
|
|
3226
|
+
const project = readProject(root, dir, join5(dir, file));
|
|
3227
|
+
if (project) out.push(project);
|
|
3228
|
+
}
|
|
3229
|
+
if (depth >= MAX_DEPTH) return;
|
|
3230
|
+
for (const name of entries) {
|
|
3231
|
+
if (SKIP.has(name) || name.startsWith(".")) continue;
|
|
3232
|
+
const path = join5(dir, name);
|
|
3233
|
+
try {
|
|
3234
|
+
if (statSync(path).isDirectory()) walk(path, depth + 1);
|
|
3235
|
+
} catch {
|
|
3237
3236
|
}
|
|
3238
|
-
if (!json) io.err(`${PRODUCT} approve: recorded the same approval under ${OP_NAME}, where the waves after the merge read the pull request's ${PR_GATE} gate`);
|
|
3239
3237
|
}
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3238
|
+
};
|
|
3239
|
+
walk(root, 0);
|
|
3240
|
+
return out;
|
|
3243
3241
|
}
|
|
3244
|
-
function
|
|
3245
|
-
const
|
|
3246
|
-
if (!
|
|
3242
|
+
function readProject(root, dir, file) {
|
|
3243
|
+
const doc = readYamlFile(file);
|
|
3244
|
+
if (!doc || typeof doc.name !== "string" || doc.name === "") return void 0;
|
|
3245
|
+
const runtime = typeof doc.runtime === "string" ? doc.runtime : doc.runtime?.name;
|
|
3246
|
+
const backend = doc.backend;
|
|
3247
|
+
const configDir = typeof doc.stackConfigDir === "string" ? join5(dir, doc.stackConfigDir) : dir;
|
|
3248
|
+
return {
|
|
3249
|
+
name: doc.name,
|
|
3250
|
+
dir: posixRel(root, dir),
|
|
3251
|
+
...typeof runtime === "string" ? { runtime } : {},
|
|
3252
|
+
...backend && typeof backend.url === "string" ? { backendUrl: backend.url } : {},
|
|
3253
|
+
stacks: stackNames(configDir)
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
function stackNames(configDir) {
|
|
3257
|
+
let entries;
|
|
3247
3258
|
try {
|
|
3248
|
-
|
|
3249
|
-
return spec && typeof spec === "object" && !Array.isArray(spec) ? spec : void 0;
|
|
3259
|
+
entries = readdirSync3(configDir);
|
|
3250
3260
|
} catch {
|
|
3251
|
-
return
|
|
3261
|
+
return [];
|
|
3252
3262
|
}
|
|
3263
|
+
const names = entries.map((n) => /^Pulumi\.(.+)\.ya?ml$/.exec(n)?.[1]).filter((s) => s !== void 0 && s !== "");
|
|
3264
|
+
return [...new Set(names)].sort(byEnvironment);
|
|
3253
3265
|
}
|
|
3254
|
-
function
|
|
3255
|
-
const
|
|
3256
|
-
const
|
|
3257
|
-
return
|
|
3266
|
+
function environmentOf(stack) {
|
|
3267
|
+
const last = stack.split("/").pop().toLowerCase();
|
|
3268
|
+
const head = last.split(/[-_.]/)[0];
|
|
3269
|
+
return ENV_ORDER.includes(head) ? head : last;
|
|
3258
3270
|
}
|
|
3259
|
-
function
|
|
3260
|
-
const
|
|
3261
|
-
|
|
3271
|
+
function rank(stack) {
|
|
3272
|
+
const env = environmentOf(stack);
|
|
3273
|
+
const at = ENV_ORDER.indexOf(env);
|
|
3274
|
+
return at >= 0 ? at : PROD_RANK - 0.5;
|
|
3262
3275
|
}
|
|
3263
|
-
function
|
|
3264
|
-
|
|
3265
|
-
const ci2 = env.GITLAB_CI === "true" ? "gitlab" : env.FORGEJO_ACTIONS === "true" || env.GITEA_ACTIONS === "true" ? "forgejo" : env.GITHUB_ACTIONS === "true" ? "github" : void 0;
|
|
3266
|
-
const forge = opts.forge ?? configured ?? ci2;
|
|
3267
|
-
const branch = opts.branch ?? (env.GITHUB_BASE_REF || env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || env.CI_DEFAULT_BRANCH || (env.GITHUB_ACTIONS === "true" ? env.GITHUB_REF_NAME : void 0) || void 0);
|
|
3268
|
-
return { ...forge ? { forge } : {}, ...branch ? { branch } : {} };
|
|
3276
|
+
function byEnvironment(a, b) {
|
|
3277
|
+
return rank(a) - rank(b) || a.localeCompare(b);
|
|
3269
3278
|
}
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
const
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
const line = (branch, forge) => `${PRODUCT} run wave --spec ${WAVES_SPEC_FILE} --approval-status --open --branch ${branch} --forge ${forge}`;
|
|
3278
|
-
if (!at.forge || !at.branch) {
|
|
3279
|
-
io.err(`${PRODUCT} approve: ${PRODUCT}/approval moves at the next push or review; to post it now, run '${line(at.branch ?? "<target branch>", at.forge ?? "<forge>")}' with a forge token`);
|
|
3280
|
-
return;
|
|
3279
|
+
function stacksOf2(projects) {
|
|
3280
|
+
const names = /* @__PURE__ */ new Map();
|
|
3281
|
+
for (const p of projects) names.set(p.name, (names.get(p.name) ?? 0) + 1);
|
|
3282
|
+
const out = [];
|
|
3283
|
+
for (const p of projects) {
|
|
3284
|
+
const project = (names.get(p.name) ?? 0) > 1 ? `${p.name}-${slug(p.dir)}` : p.name;
|
|
3285
|
+
for (const stack of p.stacks) out.push({ key: `${project}/${stack}`, project: p.name, stack, dir: p.dir });
|
|
3281
3286
|
}
|
|
3282
|
-
|
|
3283
|
-
if (code !== 0) io.err(`${PRODUCT} approve: the approval is recorded, and ${PRODUCT}/approval was not posted again (exit ${code}); it moves at the next push or review, or run '${line(at.branch, at.forge)}'`);
|
|
3284
|
-
else if (!opts.json) io.err(`${PRODUCT} approve: posted ${PRODUCT}/approval again on the open pull requests into ${at.branch}`);
|
|
3287
|
+
return out.sort((x, y) => byEnvironment(x.stack, y.stack) || x.key.localeCompare(y.key));
|
|
3285
3288
|
}
|
|
3286
|
-
|
|
3287
|
-
const
|
|
3288
|
-
|
|
3289
|
-
let gates;
|
|
3290
|
-
try {
|
|
3291
|
-
gates = await chant.pendingGatesByPlan(digest, { pullRequestsOnly: true, ...opts.cwd ? { cwd: opts.cwd } : {} });
|
|
3292
|
-
} catch (e) {
|
|
3293
|
-
return { error: `${needsPr}. Reading the pending gates for ${digest} failed: ${e.message}` };
|
|
3294
|
-
}
|
|
3295
|
-
let up2 = gates.filter((g) => g.gate === PR_GATE && g.pr !== void 0);
|
|
3296
|
-
if (opts.stack) up2 = up2.filter((g) => g.member === opts.stack);
|
|
3297
|
-
const ops = [...new Set(up2.map((g) => g.op))];
|
|
3298
|
-
if (ops.length === 0) return { error: `no pull request's ${PR_GATE} gate waits on ${digest}${opts.stack ? ` for ${opts.stack}` : ""}. Check the digest the preview printed and fetch chant/lifecycle, or name the pull request with --pr <n>` };
|
|
3299
|
-
const prs = [...new Set(up2.map((g) => g.pr))];
|
|
3300
|
-
if (prs.length > 1) return { error: `${digest} waits at the ${PR_GATE} gate of pull requests ${prs.sort((a, b) => a - b).join(", ")}; name one with --pr <n>` };
|
|
3301
|
-
const whole = up2.find((g) => g.member === void 0);
|
|
3302
|
-
if (whole) return { op: whole.op, pr: whole.pr };
|
|
3303
|
-
if (ops.length > 1) return { error: `${digest} waits at the ${PR_GATE} gate of ${ops.join(", ")}; name the stack with --stack <stack>` };
|
|
3304
|
-
return { op: ops[0], pr: prs[0] };
|
|
3289
|
+
function slug(s) {
|
|
3290
|
+
const out = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3291
|
+
return out === "" ? "root" : out;
|
|
3305
3292
|
}
|
|
3306
|
-
|
|
3307
|
-
const
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3293
|
+
function originUrl(root) {
|
|
3294
|
+
const config2 = gitFile(root, "config");
|
|
3295
|
+
if (!config2) return void 0;
|
|
3296
|
+
let inOrigin = false;
|
|
3297
|
+
for (const raw of config2.split(/\r?\n/)) {
|
|
3298
|
+
const line = raw.trim();
|
|
3299
|
+
if (line.startsWith("[")) {
|
|
3300
|
+
inOrigin = /^\[remote\s+"origin"\]$/.test(line);
|
|
3301
|
+
continue;
|
|
3302
|
+
}
|
|
3303
|
+
const m = inOrigin ? /^url\s*=\s*(.+)$/.exec(line) : null;
|
|
3304
|
+
if (m) return m[1].trim();
|
|
3305
|
+
}
|
|
3306
|
+
return void 0;
|
|
3307
|
+
}
|
|
3308
|
+
function gitFile(root, name) {
|
|
3309
|
+
const dotGit = join5(root, ".git");
|
|
3310
|
+
let gitDir = dotGit;
|
|
3311
|
+
try {
|
|
3312
|
+
if (statSync(dotGit).isFile()) {
|
|
3313
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync5(dotGit, "utf8"));
|
|
3314
|
+
if (!m) return void 0;
|
|
3315
|
+
gitDir = join5(root, m[1].trim());
|
|
3316
|
+
const common = join5(gitDir, "commondir");
|
|
3317
|
+
if (existsSync5(common) && !existsSync5(join5(gitDir, name))) gitDir = join5(gitDir, readFileSync5(common, "utf8").trim());
|
|
3318
|
+
}
|
|
3319
|
+
const path = join5(gitDir, name);
|
|
3320
|
+
return existsSync5(path) ? readFileSync5(path, "utf8") : void 0;
|
|
3321
|
+
} catch {
|
|
3322
|
+
return void 0;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
function remoteHost(url) {
|
|
3326
|
+
const scp = /^[^@/\s]+@([^:/\s]+):/.exec(url);
|
|
3327
|
+
if (scp) return scp[1].toLowerCase();
|
|
3328
|
+
try {
|
|
3329
|
+
return new URL(url).hostname.toLowerCase() || void 0;
|
|
3330
|
+
} catch {
|
|
3331
|
+
return void 0;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
function forgeOfRemote(url) {
|
|
3335
|
+
const host = remoteHost(url);
|
|
3336
|
+
if (!host) return void 0;
|
|
3337
|
+
if (host === "github.com" || host.endsWith(".github.com") || host.startsWith("github.")) return { kind: "github", because: `origin is on ${host}` };
|
|
3338
|
+
if (host === "gitlab.com") return { kind: "gitlab", because: `origin is on ${host}` };
|
|
3339
|
+
if (host === "codeberg.org") return { kind: "forgejo", because: `origin is on ${host}` };
|
|
3340
|
+
if (/(^|[.-])gitlab([.-]|$)/.test(host)) return { kind: "gitlab", because: `origin is on ${host}`, url: `https://${host}` };
|
|
3341
|
+
if (/(^|[.-])(forgejo|gitea)([.-]|$)/.test(host)) return { kind: "forgejo", because: `origin is on ${host}`, url: `https://${host}` };
|
|
3342
|
+
return void 0;
|
|
3343
|
+
}
|
|
3344
|
+
function detectForge(root) {
|
|
3345
|
+
const url = originUrl(root);
|
|
3346
|
+
const byRemote = url ? forgeOfRemote(url) : void 0;
|
|
3347
|
+
if (byRemote) return byRemote;
|
|
3348
|
+
if (existsSync5(join5(root, ".forgejo"))) return { kind: "forgejo", because: "the repository has .forgejo/" };
|
|
3349
|
+
if (existsSync5(join5(root, ".gitlab-ci.yml"))) return { kind: "gitlab", because: "the repository has .gitlab-ci.yml" };
|
|
3350
|
+
if (existsSync5(join5(root, ".github", "workflows"))) return { kind: "github", because: "the repository has .github/workflows/" };
|
|
3351
|
+
return void 0;
|
|
3352
|
+
}
|
|
3353
|
+
function defaultBranch(root) {
|
|
3354
|
+
const head = gitFile(root, "refs/remotes/origin/HEAD");
|
|
3355
|
+
const remote = head && /ref:\s*refs\/remotes\/origin\/(.+)/.exec(head)?.[1];
|
|
3356
|
+
if (remote) return remote.trim();
|
|
3357
|
+
const local = gitFile(root, "HEAD");
|
|
3358
|
+
const branch = local && /ref:\s*refs\/heads\/(.+)/.exec(local)?.[1];
|
|
3359
|
+
return branch ? branch.trim() : "main";
|
|
3360
|
+
}
|
|
3361
|
+
function findPulumiActions(root) {
|
|
3362
|
+
const out = [];
|
|
3363
|
+
for (const d of WORKFLOW_DIRS) {
|
|
3364
|
+
let names;
|
|
3365
|
+
try {
|
|
3366
|
+
names = readdirSync3(join5(root, d)).sort();
|
|
3367
|
+
} catch {
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
for (const n of names) {
|
|
3371
|
+
if (!/\.ya?ml$/.test(n)) continue;
|
|
3372
|
+
try {
|
|
3373
|
+
if (/uses:\s*["']?pulumi\/actions@/.test(readFileSync5(join5(root, d, n), "utf8"))) out.push(`${d}/${n}`);
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
return out;
|
|
3379
|
+
}
|
|
3380
|
+
function detect(cwd) {
|
|
3381
|
+
const root = repoRoot(cwd);
|
|
3382
|
+
const projects = findPulumiProjects(root);
|
|
3383
|
+
const forge = detectForge(root);
|
|
3384
|
+
const preCommit = [".pre-commit-config.yaml", ".pre-commit-config.yml"].find((n) => existsSync5(join5(root, n)));
|
|
3385
|
+
return {
|
|
3386
|
+
root,
|
|
3387
|
+
projects,
|
|
3388
|
+
stacks: stacksOf2(projects),
|
|
3389
|
+
...forge ? { forge } : {},
|
|
3390
|
+
defaultBranch: defaultBranch(root),
|
|
3391
|
+
pulumiActions: findPulumiActions(root),
|
|
3392
|
+
...preCommit ? { preCommit } : {}
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function repoName(root) {
|
|
3396
|
+
return slug(basename(root)).slice(0, 40).replace(/-+$/, "") || "plywood";
|
|
3397
|
+
}
|
|
3398
|
+
var SKIP, MAX_DEPTH, ENV_ORDER, PROD_RANK, WORKFLOW_DIRS;
|
|
3399
|
+
var init_detect = __esm({
|
|
3400
|
+
"src/init/detect.ts"() {
|
|
3401
|
+
"use strict";
|
|
3402
|
+
SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor", "venv", ".venv", "__pycache__", "target", "bin", "obj"]);
|
|
3403
|
+
MAX_DEPTH = 8;
|
|
3404
|
+
ENV_ORDER = ["local", "dev", "development", "sandbox", "test", "testing", "qa", "ci", "stage", "staging", "preprod", "uat", "prod", "production"];
|
|
3405
|
+
PROD_RANK = ENV_ORDER.indexOf("prod");
|
|
3406
|
+
WORKFLOW_DIRS = [".github/workflows", ".forgejo/workflows", ".gitea/workflows"];
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
// src/templates.ts
|
|
3411
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3412
|
+
function template(name) {
|
|
3413
|
+
return readFileSync6(new URL(`../templates/${name}`, import.meta.url), "utf8");
|
|
3414
|
+
}
|
|
3415
|
+
function fill(text, values) {
|
|
3416
|
+
return text.replace(/\{\{(\w+)\}\}/g, (all, key) => key in values ? values[key] : all);
|
|
3417
|
+
}
|
|
3418
|
+
var init_templates = __esm({
|
|
3419
|
+
"src/templates.ts"() {
|
|
3420
|
+
"use strict";
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
// src/init/render.ts
|
|
3425
|
+
import { emitYAMLEntry } from "@intentius/chant/yaml";
|
|
3426
|
+
function plywoodYml(keys) {
|
|
3427
|
+
const body = Object.entries(keys).filter(([, v]) => v !== void 0).map(([k, v]) => emitYAMLEntry(k, v)).join("\n");
|
|
3428
|
+
const text = fill(template("plywood.yml"), { keys: body });
|
|
3429
|
+
return text.endsWith("\n") ? text : text + "\n";
|
|
3430
|
+
}
|
|
3431
|
+
function chantConfigTs() {
|
|
3432
|
+
return template("chant.config.ts.tmpl");
|
|
3433
|
+
}
|
|
3434
|
+
function componentName(stack) {
|
|
3435
|
+
return slug(stack.key);
|
|
3436
|
+
}
|
|
3437
|
+
function exportName(stack, taken) {
|
|
3438
|
+
let base = stack.key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3439
|
+
if (base === "" || /^\d/.test(base)) base = `stack_${base}`;
|
|
3440
|
+
let name = base;
|
|
3441
|
+
for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
|
|
3442
|
+
taken.add(name);
|
|
3443
|
+
return name;
|
|
3444
|
+
}
|
|
3445
|
+
function stepInput(s) {
|
|
3446
|
+
return {
|
|
3447
|
+
dir: s.dir,
|
|
3448
|
+
stack: s.stack,
|
|
3449
|
+
member: s.project,
|
|
3450
|
+
...s.engine ? { engine: s.engine } : {},
|
|
3451
|
+
...s.backend ? { backend: s.backend } : {},
|
|
3452
|
+
...s.engineVersion ? { engineVersion: s.engineVersion, ...s.engineRoot ? { engineRoot: s.engineRoot } : {} } : {},
|
|
3453
|
+
...s.lockGrain ? { lockGrain: s.lockGrain } : {},
|
|
3454
|
+
...s.policyPacks?.length ? { policyPacks: s.policyPacks } : {}
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
function componentsFile(stacks2, after = {}, opts = {}) {
|
|
3458
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3459
|
+
const lines2 = [
|
|
3460
|
+
"// Written by plywood init from the stacks it found; running it again rewrites",
|
|
3461
|
+
"// this file. Each stack is a chant component whose pulumi-up step previews it",
|
|
3462
|
+
"// on a pull request and updates it in its wave.",
|
|
3463
|
+
""
|
|
3464
|
+
];
|
|
3465
|
+
for (const s of stacks2) {
|
|
3466
|
+
const dependsOn = (after[s.key] ?? []).filter((u) => stacks2.some((x) => x.key === u)).map((u) => componentName({ key: u }));
|
|
3467
|
+
const step = { kind: "pulumi-up", ...stepInput(s), ...opts.reviewSuffix ? { reviewSuffix: opts.reviewSuffix } : {} };
|
|
3468
|
+
const component = { name: componentName(s), dependsOn, deploy: [{ phase: "up", steps: [step] }] };
|
|
3469
|
+
lines2.push(`export const ${exportName(s, taken)} = ${JSON.stringify(component)};`);
|
|
3470
|
+
}
|
|
3471
|
+
return lines2.join("\n") + "\n";
|
|
3472
|
+
}
|
|
3473
|
+
function wavesOf(stacks2, configured) {
|
|
3474
|
+
const byKey = new Map(stacks2.map((s) => [s.key, s]));
|
|
3475
|
+
if (configured && configured.length > 0) {
|
|
3476
|
+
return configured.map((keys, i) => ({ name: `wave-${i + 1}`, stacks: keys.map((k) => byKey.get(k)).filter((s) => s !== void 0) }));
|
|
3477
|
+
}
|
|
3478
|
+
const out = [];
|
|
3479
|
+
for (const s of stacks2) {
|
|
3480
|
+
const env = environmentOf(s.stack);
|
|
3481
|
+
let name = slug(env);
|
|
3482
|
+
if (!WAVE_NAME.test(name)) name = `wave-${out.length + 1}`;
|
|
3483
|
+
const wave = out.find((w) => w.name === name);
|
|
3484
|
+
if (wave) wave.stacks.push(s);
|
|
3485
|
+
else out.push({ name, stacks: [s] });
|
|
3486
|
+
}
|
|
3487
|
+
return out;
|
|
3488
|
+
}
|
|
3489
|
+
function waveRunner(runner) {
|
|
3490
|
+
if (runner === void 0) return void 0;
|
|
3491
|
+
if (typeof runner === "string" || Array.isArray(runner) || "group" in runner) return runner;
|
|
3492
|
+
const staged = runner;
|
|
3493
|
+
return staged.up ?? staged.default;
|
|
3494
|
+
}
|
|
3495
|
+
function wavesSpec(input) {
|
|
3496
|
+
const waves = (input.ordered ?? wavesOf(input.stacks, input.waves)).filter((w) => w.stacks.length > 0);
|
|
3497
|
+
return {
|
|
3498
|
+
name: OP_NAME,
|
|
3499
|
+
op: OP_NAME,
|
|
3500
|
+
gate: PR_GATE,
|
|
3501
|
+
branches: [input.branch],
|
|
3502
|
+
...input.runner !== void 0 ? { runner: input.runner } : {},
|
|
3503
|
+
step: { kind: "pulumi-up" },
|
|
3504
|
+
...input.quorum && input.quorum.length > 0 ? { approvals: { quorum: input.quorum } } : {},
|
|
3505
|
+
...input.notify ? { notify: input.notify } : {},
|
|
3506
|
+
waves: waves.map((w) => {
|
|
3507
|
+
const gate = w.stacks.map((s) => s.gate ?? input.gate).reduce((a, b) => STRICTNESS[b] > STRICTNESS[a] ? b : a);
|
|
3508
|
+
return { name: w.name, gate, runs: w.stacks.map((s) => ({ target: s.key, input: stepInput(s) })) };
|
|
3509
|
+
})
|
|
3510
|
+
};
|
|
3511
|
+
}
|
|
3512
|
+
function jsonText(v) {
|
|
3513
|
+
return JSON.stringify(v, null, 2) + "\n";
|
|
3514
|
+
}
|
|
3515
|
+
function memberName(project, taken) {
|
|
3516
|
+
let base = slug(project.name).slice(0, 36).replace(/-+$/, "");
|
|
3517
|
+
if (!MEMBER_NAME.test(base)) base = "stack";
|
|
3518
|
+
let name = base;
|
|
3519
|
+
for (let i = 2; taken.has(name); i++) name = `${base}-${i}`;
|
|
3520
|
+
taken.add(name);
|
|
3521
|
+
return name;
|
|
3522
|
+
}
|
|
3523
|
+
function workspaceDeclaration(existing, input) {
|
|
3524
|
+
const doc = existing ? structuredClone(existing) : { name: input.name, schema: 1 };
|
|
3525
|
+
const members = Array.isArray(doc.members) ? doc.members : [];
|
|
3526
|
+
const taken = new Set(members.map((m) => String(m.name)));
|
|
3527
|
+
for (const p of input.projects) {
|
|
3528
|
+
if (members.some((m) => m.dir === p.dir || p.dir === "." && m.dir === void 0)) continue;
|
|
3529
|
+
members.push({ name: memberName(p, taken), dir: p.dir, kind: "pulumi" });
|
|
3530
|
+
}
|
|
3531
|
+
doc.members = members;
|
|
3532
|
+
const pins = Array.isArray(doc.pins) ? doc.pins : [];
|
|
3533
|
+
if (!pins.some((p) => p.package === PULUMI_LEXICON)) pins.push({ package: PULUMI_LEXICON, version: input.lexiconVersion });
|
|
3534
|
+
doc.pins = pins;
|
|
3535
|
+
const ci2 = doc.ci && typeof doc.ci === "object" && !Array.isArray(doc.ci) ? doc.ci : {};
|
|
3536
|
+
for (const [k, v] of Object.entries(input.ci)) if (v !== void 0) ci2[k] = v;
|
|
3537
|
+
if (Object.keys(ci2).length > 0) doc.ci = ci2;
|
|
3538
|
+
return doc;
|
|
3539
|
+
}
|
|
3540
|
+
function memberOfDir(doc, dir) {
|
|
3541
|
+
const members = doc && Array.isArray(doc.members) ? doc.members : [];
|
|
3542
|
+
const m = members.find((x) => x.dir === dir || dir === "." && x.dir === void 0);
|
|
3543
|
+
return typeof m?.name === "string" ? m.name : void 0;
|
|
3544
|
+
}
|
|
3545
|
+
var WAVE_NAME, STRICTNESS, MEMBER_NAME, PULUMI_LEXICON;
|
|
3546
|
+
var init_render = __esm({
|
|
3547
|
+
"src/init/render.ts"() {
|
|
3548
|
+
"use strict";
|
|
3549
|
+
init_templates();
|
|
3550
|
+
init_words();
|
|
3551
|
+
init_detect();
|
|
3552
|
+
WAVE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
3553
|
+
STRICTNESS = { never: 0, "on-destructive": 1, always: 2 };
|
|
3554
|
+
MEMBER_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
3555
|
+
PULUMI_LEXICON = "@intentius/chant-lexicon-pulumi";
|
|
3556
|
+
}
|
|
3557
|
+
});
|
|
3558
|
+
|
|
3559
|
+
// src/refusals.ts
|
|
3560
|
+
function refusalOf(code, table) {
|
|
3561
|
+
return REFUSALS.find((name) => table[name] === code);
|
|
3562
|
+
}
|
|
3563
|
+
function refusalLine(refusal, ctx = {}) {
|
|
3564
|
+
const gate = ctx.wave !== void 0 ? `wave-${ctx.wave}` : PR_GATE;
|
|
3565
|
+
const again = ctx.pr !== void 0 ? `/${PRODUCT} up on pull request ${ctx.pr}` : `${PRODUCT} up${ctx.wave !== void 0 ? ` --wave ${ctx.wave}` : ""}`;
|
|
3566
|
+
switch (refusal) {
|
|
3567
|
+
case "waiting":
|
|
3568
|
+
return `${gate} waits for an approval. Approve the digest chant printed with '${PRODUCT} approve ${gate} --plan <digest>', then run ${again} again.`;
|
|
3569
|
+
case "moved":
|
|
3570
|
+
return `The preview moved after it was approved (on plunge, a record the saved plan read was written by another update), so nothing was updated. Preview again ('${PRODUCT} preview' or /${PRODUCT} preview), read the new change, and approve its digest with '${PRODUCT} approve ${PR_GATE} --plan <digest>'.`;
|
|
3571
|
+
case "held":
|
|
3572
|
+
return `Another update or pull request holds a lock on a resource this update touches, and chant named it above. A running update lets go when it ends; a pull request's lock goes with /${PRODUCT} unlock on that pull request. Then run ${again} again.`;
|
|
3573
|
+
case "superseded":
|
|
3574
|
+
return `A newer push stands in for this one, which stood down. Nothing needs doing: the newer run updates the stacks.`;
|
|
3575
|
+
case "denied":
|
|
3576
|
+
return `A policy denied the change. Fix it and preview again, or override it with '${PRODUCT} override <stack> --rule <id> --reason <text>' naming exactly the rules chant listed (${POLICY_RULE_FORMS}). An override is spent once its update runs.`;
|
|
3577
|
+
case "declined":
|
|
3578
|
+
return `An approver declined the change, so nothing was updated. Push a new change and preview it again.`;
|
|
3579
|
+
case "misplaced":
|
|
3580
|
+
return `The job ran outside the forge environment, branch or runner plywood.yml gives this stack, and chant's job guard named the difference above, so nothing ran. Run it from that branch on that runner, or change environments and runners in plywood.yml and run '${PRODUCT} ci' again.`;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
function explainRefusal(code, table, err, ctx = {}) {
|
|
3584
|
+
const refusal = refusalOf(code, table);
|
|
3585
|
+
if (refusal) err(`${PRODUCT}: ${refusalLine(refusal, ctx)}`);
|
|
3586
|
+
return code;
|
|
3587
|
+
}
|
|
3588
|
+
function approveLineIn(output) {
|
|
3589
|
+
const m = /Approve these plans to apply them:[ \t]*([^\r\n]+)/.exec(output);
|
|
3590
|
+
const line = m?.[1]?.trim();
|
|
3591
|
+
return line ? line : void 0;
|
|
3592
|
+
}
|
|
3593
|
+
function waveMovedLine(output) {
|
|
3594
|
+
const approve2 = approveLineIn(output);
|
|
3595
|
+
const how = approve2 ? `If a person has read the plans chant listed above and wants them applied, approve exactly them with: ${approve2}, then run the job again.` : `chant named no approval for these plans. Read the plans above, then approve them with '${PRODUCT} approve <wave gate> --plan <digest>' and run the job again.`;
|
|
3596
|
+
return `A merged wave stopped with exit 4 and nothing in it was applied: its plans are not the ones its pull request previewed (or no preview was recorded for it). ${how} To apply only what a pull request previews, open a new pull request, let '${PRODUCT} run wave --record-plans' record its plans, and merge that.`;
|
|
3597
|
+
}
|
|
3598
|
+
var REFUSALS;
|
|
3599
|
+
var init_refusals = __esm({
|
|
3600
|
+
"src/refusals.ts"() {
|
|
3601
|
+
"use strict";
|
|
3602
|
+
init_words();
|
|
3603
|
+
REFUSALS = ["waiting", "moved", "held", "superseded", "denied", "declined", "misplaced"];
|
|
3604
|
+
}
|
|
3605
|
+
});
|
|
3606
|
+
|
|
3607
|
+
// src/up.ts
|
|
3608
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
3609
|
+
import { join as join6 } from "node:path";
|
|
3610
|
+
function approveTarget(gate, opts) {
|
|
3611
|
+
if (opts.op) return { op: opts.op, gate };
|
|
3612
|
+
if (PR_OP.test(gate)) return { op: gate, gate: PR_GATE };
|
|
3613
|
+
if (gate === REVIEW_GATE_WORD || gate === REVIEW_GATE) {
|
|
3614
|
+
if (!opts.pr) return { error: `a review stack is a pull request's: name it with --pr <n> (or PR_NUMBER)` };
|
|
3615
|
+
if (!/^\d+$/.test(opts.pr)) return { error: `--pr takes a pull request number, not '${opts.pr}'` };
|
|
3616
|
+
return { op: reviewOp(opts.pr), gate: REVIEW_GATE };
|
|
3617
|
+
}
|
|
3618
|
+
if (gate === PR_GATE) {
|
|
3619
|
+
if (!opts.pr) return { error: `the ${PR_GATE} gate is a pull request's: name it with --pr <n> (or PR_NUMBER), or approve pr-<n>` };
|
|
3620
|
+
if (!/^\d+$/.test(opts.pr)) return { error: `--pr takes a pull request number, not '${opts.pr}'` };
|
|
3621
|
+
return { op: opts.stack ? `pr-${opts.pr}-${opts.stack}` : `pr-${opts.pr}`, gate };
|
|
3622
|
+
}
|
|
3623
|
+
return { op: OP_NAME, gate };
|
|
3624
|
+
}
|
|
3625
|
+
function passFlags(args, valued, switches) {
|
|
3626
|
+
const argv = [];
|
|
3627
|
+
const rest = [];
|
|
3628
|
+
for (let i = 0; i < args.length; i++) {
|
|
3629
|
+
const a = args[i];
|
|
3630
|
+
if (valued.includes(a)) {
|
|
3631
|
+
const v = args[i + 1];
|
|
3632
|
+
if (v === void 0 || v.startsWith("--")) return { argv, rest, missing: a };
|
|
3633
|
+
argv.push(a, v);
|
|
3634
|
+
i++;
|
|
3635
|
+
} else if (switches.includes(a)) argv.push(a);
|
|
3636
|
+
else rest.push(a);
|
|
3637
|
+
}
|
|
3638
|
+
return { argv, rest };
|
|
3639
|
+
}
|
|
3640
|
+
async function approve(args, json, table, io) {
|
|
3641
|
+
const pr = takeFlag(args, "--pr");
|
|
3642
|
+
const stack = takeFlag(pr.rest, "--stack");
|
|
3643
|
+
const op = takeFlag(stack.rest, "--op");
|
|
3644
|
+
const forgeFlag = takeFlag(op.rest, "--forge");
|
|
3645
|
+
const branchFlag = takeFlag(forgeFlag.rest, "--branch");
|
|
3646
|
+
for (const [flag, f] of [["--pr", pr], ["--stack", stack], ["--op", op], ["--forge", forgeFlag], ["--branch", branchFlag]]) {
|
|
3647
|
+
if (f.missing) return usage(`${flag} takes a value`, table, io);
|
|
3648
|
+
}
|
|
3649
|
+
if (forgeFlag.value !== void 0 && !FORGES.includes(forgeFlag.value)) return usage(`--forge ${forgeFlag.value} is not one of ${FORGES.join(", ")}`, table, io);
|
|
3650
|
+
const passed = passFlags(branchFlag.rest, APPROVE_PASS, APPROVE_SWITCHES);
|
|
3651
|
+
if (passed.missing) return usage(`${passed.missing} takes a value`, table, io);
|
|
3652
|
+
const named = positionals(passed.rest);
|
|
3653
|
+
const unknown = passed.rest.filter((a) => a.startsWith("-"));
|
|
3654
|
+
if (unknown.length) return usage(`approve does not take ${unknown.join(" ")}`, table, io);
|
|
3655
|
+
if (named.length !== 1) return usage(`approve takes one gate: '${PRODUCT} approve ${PR_GATE} --pr <n> --plan <digest>' or '${PRODUCT} approve wave-<k> --plan <digest>'`, table, io);
|
|
3656
|
+
const prNumber = pr.value ?? io.env.PR_NUMBER;
|
|
3657
|
+
const gate = named[0];
|
|
3658
|
+
const plan = passed.argv[passed.argv.indexOf("--plan") + 1];
|
|
3659
|
+
const byPlan = gate === PR_GATE && !prNumber && !op.value && passed.argv.includes("--plan");
|
|
3660
|
+
const target = byPlan ? void 0 : approveTarget(gate, { pr: prNumber, stack: stack.value, op: op.value });
|
|
3661
|
+
if (target && "error" in target) return usage(target.error, table, io);
|
|
3662
|
+
if (!passed.argv.includes("--plan") && !passed.argv.includes("--expire")) {
|
|
3663
|
+
return usage(`approve needs --plan <digest>, the digest the preview printed, so a change that moved since is never approved in its place`, table, io);
|
|
3664
|
+
}
|
|
3665
|
+
return withChant(`${PRODUCT} approve`, table, io, async (chant) => {
|
|
3666
|
+
let to = target;
|
|
3667
|
+
if (!to) {
|
|
3668
|
+
const found = await upTargetByPlan(chant, plan, { stack: stack.value, cwd: io.cwd });
|
|
3669
|
+
if ("error" in found) return usage(found.error, table, io);
|
|
3670
|
+
if (!json) io.err(`${PRODUCT} approve: ${plan} waits at pull request ${found.pr}'s ${PR_GATE} gate (op ${found.op})`);
|
|
3671
|
+
to = { op: found.op, gate: PR_GATE };
|
|
3672
|
+
}
|
|
3673
|
+
const argv = ["approve", to.op, to.gate, ...passed.argv, ...json ? ["--json"] : []];
|
|
3674
|
+
const code = explainRefusal(await chant.runChantInProcess(argv, PRODUCT_WORDS), table, io.err);
|
|
3675
|
+
if (code !== table.done || to.gate !== PR_GATE || !PR_OP.test(to.op) || !passed.argv.includes("--plan") || passed.argv.includes("--expire")) return code;
|
|
3676
|
+
const spec = wavesSpecAt(io.cwd);
|
|
3677
|
+
if (spec && wavesTakePullRequestApproval(spec)) {
|
|
3678
|
+
const carried = await chant.runChantInProcess(carryArgv(passed.argv), PRODUCT_WORDS);
|
|
3679
|
+
if (carried !== table.done) {
|
|
3680
|
+
io.err(`${PRODUCT} approve: ${to.op} is approved, and recording the same approval under ${OP_NAME} failed (exit ${carried}), so the waves after the merge still wait at ${PR_GATE}. Run '${PRODUCT} approve ${PR_GATE} --op ${OP_NAME} --plan ${plan} --yes' to record it there.`);
|
|
3681
|
+
return explainRefusal(carried, table, io.err);
|
|
3682
|
+
}
|
|
3683
|
+
if (!json) io.err(`${PRODUCT} approve: recorded the same approval under ${OP_NAME}, where the waves after the merge read the pull request's ${PR_GATE} gate`);
|
|
3684
|
+
}
|
|
3685
|
+
if (spec) await refreshApprovalStatus(chant, spec, { forge: forgeFlag.value, branch: branchFlag.value, json }, io);
|
|
3686
|
+
return code;
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
function wavesSpecAt(cwd, file = WAVES_SPEC_FILE) {
|
|
3690
|
+
const path = join6(cwd, file);
|
|
3691
|
+
if (!existsSync6(path)) return void 0;
|
|
3692
|
+
try {
|
|
3693
|
+
const spec = unwrapWavesSpec(JSON.parse(readFileSync7(path, "utf8")));
|
|
3694
|
+
return spec && typeof spec === "object" && !Array.isArray(spec) ? spec : void 0;
|
|
3695
|
+
} catch {
|
|
3696
|
+
return void 0;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
function wavesTakePullRequestApproval(spec) {
|
|
3700
|
+
const gate = spec.approvals?.pullRequest?.gate;
|
|
3701
|
+
const waves = Array.isArray(spec.waves) ? spec.waves : [];
|
|
3702
|
+
return gate === PR_GATE && waves.some((w) => w?.approval === "pull-request");
|
|
3703
|
+
}
|
|
3704
|
+
function carryArgv(passed) {
|
|
3705
|
+
const kept = passed.filter((a) => a !== "--resume");
|
|
3706
|
+
return ["approve", OP_NAME, PR_GATE, ...kept, ...kept.includes("--yes") ? [] : ["--yes"]];
|
|
3707
|
+
}
|
|
3708
|
+
function approvalStatusTarget(opts, doc, env) {
|
|
3709
|
+
const configured = doc?.forge?.kind;
|
|
3710
|
+
const ci2 = env.GITLAB_CI === "true" ? "gitlab" : env.FORGEJO_ACTIONS === "true" || env.GITEA_ACTIONS === "true" ? "forgejo" : env.GITHUB_ACTIONS === "true" ? "github" : void 0;
|
|
3711
|
+
const forge = opts.forge ?? configured ?? ci2;
|
|
3712
|
+
const branch = opts.branch ?? (env.GITHUB_BASE_REF || env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || env.CI_DEFAULT_BRANCH || (env.GITHUB_ACTIONS === "true" ? env.GITHUB_REF_NAME : void 0) || void 0);
|
|
3713
|
+
return { ...forge ? { forge } : {}, ...branch ? { branch } : {} };
|
|
3714
|
+
}
|
|
3715
|
+
async function refreshApprovalStatus(chant, spec, opts, io) {
|
|
3716
|
+
if (!hasChantPullRequestApprovalStatus(chant)) return;
|
|
3717
|
+
const posts = chant.opWavesPostsApprovalStatus ? chant.opWavesPostsApprovalStatus(spec) : wavesTakePullRequestApproval(spec);
|
|
3718
|
+
if (!posts) return;
|
|
3719
|
+
const path = findConfig(io.cwd);
|
|
3720
|
+
const doc = path ? loadConfig(path).doc : void 0;
|
|
3721
|
+
const at = approvalStatusTarget(opts, doc, io.env);
|
|
3722
|
+
const line = (branch, forge) => `${PRODUCT} run wave --spec ${WAVES_SPEC_FILE} --approval-status --open --branch ${branch} --forge ${forge}`;
|
|
3723
|
+
if (!at.forge || !at.branch) {
|
|
3724
|
+
io.err(`${PRODUCT} approve: ${PRODUCT}/approval moves at the next push or review; to post it now, run '${line(at.branch ?? "<target branch>", at.forge ?? "<forge>")}' with a forge token`);
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const code = await chant.runChantInProcess(["run", "wave", "--spec", WAVES_SPEC_FILE, "--approval-status", "--open", "--branch", at.branch, "--forge", at.forge], PRODUCT_WORDS);
|
|
3728
|
+
if (code !== 0) io.err(`${PRODUCT} approve: the approval is recorded, and ${PRODUCT}/approval was not posted again (exit ${code}); it moves at the next push or review, or run '${line(at.branch, at.forge)}'`);
|
|
3729
|
+
else if (!opts.json) io.err(`${PRODUCT} approve: posted ${PRODUCT}/approval again on the open pull requests into ${at.branch}`);
|
|
3730
|
+
}
|
|
3731
|
+
async function upTargetByPlan(chant, digest, opts = {}) {
|
|
3732
|
+
const needsPr = `the ${PR_GATE} gate is a pull request's: name it with --pr <n> (or PR_NUMBER), or approve pr-<n>`;
|
|
3733
|
+
if (!hasChant4607(chant)) return { error: `${needsPr}. The installed chant cannot find a pull request from a plan digest (chant#4607)` };
|
|
3734
|
+
let gates;
|
|
3735
|
+
try {
|
|
3736
|
+
gates = await chant.pendingGatesByPlan(digest, { pullRequestsOnly: true, ...opts.cwd ? { cwd: opts.cwd } : {} });
|
|
3737
|
+
} catch (e) {
|
|
3738
|
+
return { error: `${needsPr}. Reading the pending gates for ${digest} failed: ${e.message}` };
|
|
3739
|
+
}
|
|
3740
|
+
let up2 = gates.filter((g) => g.gate === PR_GATE && g.pr !== void 0);
|
|
3741
|
+
if (opts.stack) up2 = up2.filter((g) => g.member === opts.stack);
|
|
3742
|
+
const ops = [...new Set(up2.map((g) => g.op))];
|
|
3743
|
+
if (ops.length === 0) return { error: `no pull request's ${PR_GATE} gate waits on ${digest}${opts.stack ? ` for ${opts.stack}` : ""}. Check the digest the preview printed and fetch chant/lifecycle, or name the pull request with --pr <n>` };
|
|
3744
|
+
const prs = [...new Set(up2.map((g) => g.pr))];
|
|
3745
|
+
if (prs.length > 1) return { error: `${digest} waits at the ${PR_GATE} gate of pull requests ${prs.sort((a, b) => a - b).join(", ")}; name one with --pr <n>` };
|
|
3746
|
+
const whole = up2.find((g) => g.member === void 0);
|
|
3747
|
+
if (whole) return { op: whole.op, pr: whole.pr };
|
|
3748
|
+
if (ops.length > 1) return { error: `${digest} waits at the ${PR_GATE} gate of ${ops.join(", ")}; name the stack with --stack <stack>` };
|
|
3749
|
+
return { op: ops[0], pr: prs[0] };
|
|
3750
|
+
}
|
|
3751
|
+
async function override(args, json, table, io) {
|
|
3752
|
+
const rules = [];
|
|
3753
|
+
const rest = [];
|
|
3754
|
+
for (let i = 0; i < args.length; i++) {
|
|
3755
|
+
if (args[i] === "--rule") {
|
|
3311
3756
|
const v = args[i + 1];
|
|
3312
3757
|
if (v === void 0 || v.startsWith("--")) return usage("--rule takes a rule id", table, io);
|
|
3313
3758
|
rules.push(v);
|
|
@@ -3329,11 +3774,11 @@ async function override(args, json, table, io) {
|
|
|
3329
3774
|
return withChant(`${PRODUCT} override`, table, io, async (chant) => explainRefusal(await chant.runChantInProcess(argv, PRODUCT_WORDS), table, io.err));
|
|
3330
3775
|
}
|
|
3331
3776
|
function readUpSpec(cwd, file = WAVES_SPEC_FILE) {
|
|
3332
|
-
const path =
|
|
3333
|
-
if (!
|
|
3777
|
+
const path = join6(cwd, file);
|
|
3778
|
+
if (!existsSync6(path)) return { error: `no ${file}; '${PRODUCT} init' writes it from plywood.yml's waves` };
|
|
3334
3779
|
let spec;
|
|
3335
3780
|
try {
|
|
3336
|
-
spec = unwrapWavesSpec(JSON.parse(
|
|
3781
|
+
spec = unwrapWavesSpec(JSON.parse(readFileSync7(path, "utf8")));
|
|
3337
3782
|
} catch (e) {
|
|
3338
3783
|
return { error: `${file}: ${e.message}` };
|
|
3339
3784
|
}
|
|
@@ -3488,9 +3933,9 @@ var init_up = __esm({
|
|
|
3488
3933
|
// src/drift.ts
|
|
3489
3934
|
import { createHash as createHash2 } from "node:crypto";
|
|
3490
3935
|
import { resolveTargetCloudIdentity } from "@intentius/chant/ci-identity";
|
|
3491
|
-
import { existsSync as
|
|
3936
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, mkdtempSync, readdirSync as readdirSync4, readFileSync as readFileSync8, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3492
3937
|
import { tmpdir } from "node:os";
|
|
3493
|
-
import { dirname as
|
|
3938
|
+
import { dirname as dirname6, join as join7 } from "node:path";
|
|
3494
3939
|
function driftRows(doc) {
|
|
3495
3940
|
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);
|
|
3496
3941
|
}
|
|
@@ -3549,11 +3994,12 @@ async function drift2(args, json, table, io) {
|
|
|
3549
3994
|
const respond = respondFlag.value ?? driftResponseOf(doc);
|
|
3550
3995
|
const forge = forgeFlag.value;
|
|
3551
3996
|
return withChant(`${PRODUCT} drift`, table, io, async (chant) => {
|
|
3552
|
-
const dir = mkdtempSync(
|
|
3997
|
+
const dir = mkdtempSync(join7(tmpdir(), "plywood-drift-"));
|
|
3553
3998
|
const results = [];
|
|
3554
3999
|
try {
|
|
3555
4000
|
const attribute = await attributionFor(doc, io);
|
|
3556
|
-
|
|
4001
|
+
const root = dirname6(path);
|
|
4002
|
+
for (const stack of stacks2) results.push(await readStack(chant, stack, driftMember(root, doc, stack), dir, { resource: resource.value, env: envFlag.value, ...attribute ? { attribute } : {} }));
|
|
3557
4003
|
} finally {
|
|
3558
4004
|
rmSync(dir, { recursive: true, force: true });
|
|
3559
4005
|
}
|
|
@@ -3628,13 +4074,31 @@ async function driftNotice(chant, doc, results, environment, io) {
|
|
|
3628
4074
|
...notifyFetch ? { fetch: notifyFetch } : {}
|
|
3629
4075
|
});
|
|
3630
4076
|
}
|
|
3631
|
-
|
|
3632
|
-
const
|
|
3633
|
-
|
|
4077
|
+
function driftMember(root, doc, stack) {
|
|
4078
|
+
const project = stack.includes("/") ? stack.slice(0, stack.indexOf("/")) : stack;
|
|
4079
|
+
let declaration;
|
|
4080
|
+
try {
|
|
4081
|
+
declaration = JSON.parse(readFileSync8(join7(root, "chant.workspace.json"), "utf8"));
|
|
4082
|
+
} catch {
|
|
4083
|
+
return project;
|
|
4084
|
+
}
|
|
4085
|
+
const dir = stacksOf(doc)[stack]?.dir;
|
|
4086
|
+
const byDir = dir !== void 0 ? memberOfDir(declaration, dir) : void 0;
|
|
4087
|
+
if (byDir) return byDir;
|
|
4088
|
+
const names = (Array.isArray(declaration.members) ? declaration.members : []).flatMap((m) => typeof m.name === "string" ? [m.name] : []);
|
|
4089
|
+
if (names.includes(project)) return project;
|
|
4090
|
+
const slugged = names.find((n) => n === slug(project));
|
|
4091
|
+
if (slugged) return slugged;
|
|
4092
|
+
return names.length === 1 ? names[0] : project;
|
|
4093
|
+
}
|
|
4094
|
+
async function readStack(chant, stack, member, dir, opts) {
|
|
4095
|
+
const file = join7(dir, `${driftOpName(stack)}.json`);
|
|
4096
|
+
const pulumiStack = stack.slice(stack.indexOf("/") + 1);
|
|
4097
|
+
const argv = ["workspace", "drift", "--member", member, "--stack", pulumiStack, ...opts.resource ? ["--resource", opts.resource] : [], ...opts.env ? ["--env", opts.env] : [], "-o", file];
|
|
3634
4098
|
const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
3635
4099
|
let doc;
|
|
3636
4100
|
try {
|
|
3637
|
-
if (
|
|
4101
|
+
if (existsSync7(file)) doc = JSON.parse(readFileSync8(file, "utf8"));
|
|
3638
4102
|
} catch (e) {
|
|
3639
4103
|
return { stack, ok: false, drifted: 0, rows: [], error: `chant wrote a drift document plywood cannot read: ${e.message}` };
|
|
3640
4104
|
}
|
|
@@ -3667,7 +4131,7 @@ async function attributeStack(a, stack, doc) {
|
|
|
3667
4131
|
}
|
|
3668
4132
|
function specHasUpdate(cwd, op) {
|
|
3669
4133
|
try {
|
|
3670
|
-
const spec = JSON.parse(
|
|
4134
|
+
const spec = JSON.parse(readFileSync8(join7(cwd, DRIFT_SPEC_FILE), "utf8"));
|
|
3671
4135
|
return (spec.ops ?? []).some((o) => o.name === op && o.update !== void 0);
|
|
3672
4136
|
} catch {
|
|
3673
4137
|
return false;
|
|
@@ -3743,9 +4207,9 @@ function withNotifyVariables(fields, notify2) {
|
|
|
3743
4207
|
return { ...fields, variables: { ...notify2, ...fields.variables ?? {} } };
|
|
3744
4208
|
}
|
|
3745
4209
|
function writeIfChanged2(cwd, file, text) {
|
|
3746
|
-
const path =
|
|
3747
|
-
if (
|
|
3748
|
-
mkdirSync4(
|
|
4210
|
+
const path = join7(cwd, file);
|
|
4211
|
+
if (existsSync7(path) && readFileSync8(path, "utf8") === text) return false;
|
|
4212
|
+
mkdirSync4(dirname6(path), { recursive: true });
|
|
3749
4213
|
writeFileSync4(path, text);
|
|
3750
4214
|
return true;
|
|
3751
4215
|
}
|
|
@@ -3761,9 +4225,9 @@ async function writeDriftJobs(chant, forge, doc, words, table, io, env = "produc
|
|
|
3761
4225
|
const outDir = OP_OUTPUT_DIR2[forge];
|
|
3762
4226
|
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", DRIFT_SPEC_FILE, "--output", outDir], words);
|
|
3763
4227
|
if (code !== table.done) return code;
|
|
3764
|
-
const dir =
|
|
3765
|
-
const files =
|
|
3766
|
-
for (const f of files) io.err(`${PRODUCT} ci: drift job at ${
|
|
4228
|
+
const dir = join7(io.cwd, outDir);
|
|
4229
|
+
const files = existsSync7(dir) ? readdirSync4(dir).filter((f) => /\.ya?ml$/.test(f) && (readFileSync8(join7(dir, f), "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${DRIFT_SPEC_FILE}`)) : [];
|
|
4230
|
+
for (const f of files) io.err(`${PRODUCT} ci: drift job at ${join7(outDir, f).replace(/^\.\//, "")} on ${settings.schedule}`);
|
|
3767
4231
|
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`);
|
|
3768
4232
|
return table.done;
|
|
3769
4233
|
}
|
|
@@ -3781,6 +4245,8 @@ var init_drift2 = __esm({
|
|
|
3781
4245
|
init_drift();
|
|
3782
4246
|
init_config();
|
|
3783
4247
|
init_stacks();
|
|
4248
|
+
init_detect();
|
|
4249
|
+
init_render();
|
|
3784
4250
|
init_routing();
|
|
3785
4251
|
init_images();
|
|
3786
4252
|
init_stages();
|
|
@@ -3837,8 +4303,8 @@ var init_tips = __esm({
|
|
|
3837
4303
|
});
|
|
3838
4304
|
|
|
3839
4305
|
// src/agents.ts
|
|
3840
|
-
import { existsSync as
|
|
3841
|
-
import { dirname as
|
|
4306
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4307
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
3842
4308
|
function plywoodMcpTools(ctx) {
|
|
3843
4309
|
const none = { type: "object", properties: {} };
|
|
3844
4310
|
const doc = () => {
|
|
@@ -3948,304 +4414,83 @@ function reviewOpFile(doc) {
|
|
|
3948
4414
|
"",
|
|
3949
4415
|
"export default Op({",
|
|
3950
4416
|
` name: ${JSON.stringify(REVIEW_OP)},`,
|
|
3951
|
-
` overview: "A model reviews the pull request against its stacks' previews",`,
|
|
3952
|
-
" phases: [",
|
|
3953
|
-
` phase("Preview", [shell(${JSON.stringify(REVIEW_PREVIEW_LINE)})]),`,
|
|
3954
|
-
` phase("Review", [reviewAgent({ ${fields} })]),`,
|
|
3955
|
-
" ],",
|
|
3956
|
-
"});",
|
|
3957
|
-
""
|
|
3958
|
-
].join("\n");
|
|
3959
|
-
}
|
|
3960
|
-
function reviewOpSpec(doc) {
|
|
3961
|
-
const settings = toChantSettings({ doc });
|
|
3962
|
-
const p = settings.pipeline;
|
|
3963
|
-
const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
|
|
3964
|
-
return {
|
|
3965
|
-
ops: [
|
|
3966
|
-
{
|
|
3967
|
-
name: REVIEW_OP,
|
|
3968
|
-
trigger: { kind: "pull_request" },
|
|
3969
|
-
reviewAgent: true,
|
|
3970
|
-
// The preview in the review job takes the preview identity; id-token is the one write the review job may hold.
|
|
3971
|
-
...settings.ci.identity ? { permissions: { "id-token": "write" } } : {}
|
|
3972
|
-
}
|
|
3973
|
-
],
|
|
3974
|
-
options: {
|
|
3975
|
-
...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
|
|
3976
|
-
...engine2.image ? { image: engine2.image } : {}
|
|
3977
|
-
}
|
|
3978
|
-
};
|
|
3979
|
-
}
|
|
3980
|
-
function writeIfChanged3(cwd, file, text) {
|
|
3981
|
-
const path = join7(cwd, file);
|
|
3982
|
-
if (existsSync7(path) && readFileSync7(path, "utf8") === text) return false;
|
|
3983
|
-
mkdirSync5(dirname6(path), { recursive: true });
|
|
3984
|
-
writeFileSync5(path, text);
|
|
3985
|
-
return true;
|
|
3986
|
-
}
|
|
3987
|
-
async function writeReviewJobs(chant, forge, doc, words, table, io) {
|
|
3988
|
-
if (!reviewOf(doc)) return table.done;
|
|
3989
|
-
if (writeIfChanged3(io.cwd, REVIEW_OP_FILE, reviewOpFile(doc))) io.err(`${PRODUCT} ci: wrote ${REVIEW_OP_FILE}; commit it with the workflow`);
|
|
3990
|
-
if (writeIfChanged3(io.cwd, REVIEW_SPEC_FILE, JSON.stringify(reviewOpSpec(doc), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${REVIEW_SPEC_FILE}`);
|
|
3991
|
-
const outDir = OP_OUTPUT_DIR3[forge];
|
|
3992
|
-
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", REVIEW_SPEC_FILE, "--output", outDir], words);
|
|
3993
|
-
if (code !== table.done) return code;
|
|
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(/^\.\//, "")}`);
|
|
3997
|
-
const r = reviewOf(doc);
|
|
3998
|
-
io.err(`${PRODUCT} ci: the review job reads the model's key from ${r.keyEnv}; set it from a secret`);
|
|
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`);
|
|
4000
|
-
return table.done;
|
|
4001
|
-
}
|
|
4002
|
-
var PERSON_VERBS, READ_INSTRUCTIONS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE, REVIEW_CHANGE_SET, REVIEW_PREVIEW_LINE, OP_OUTPUT_DIR3;
|
|
4003
|
-
var init_agents2 = __esm({
|
|
4004
|
-
"src/agents.ts"() {
|
|
4005
|
-
"use strict";
|
|
4006
|
-
init_chant_config();
|
|
4007
|
-
init_config_check();
|
|
4008
|
-
init_agents();
|
|
4009
|
-
init_config();
|
|
4010
|
-
init_stacks();
|
|
4011
|
-
init_images();
|
|
4012
|
-
init_stages();
|
|
4013
|
-
init_tips();
|
|
4014
|
-
init_words();
|
|
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.`;
|
|
4017
|
-
REVIEW_OP = "plywood-review";
|
|
4018
|
-
REVIEW_OP_FILE = "ops/plywood-review.op.ts";
|
|
4019
|
-
REVIEW_SPEC_FILE = ".chant/ops/plywood-review.spec.json";
|
|
4020
|
-
REVIEW_CHANGE_SET = ".chant/pr/change-set.json";
|
|
4021
|
-
REVIEW_PREVIEW_LINE = `${BIN} components pr-plan --base "origin/\${GITHUB_BASE_REF:-$CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" --pr "\${CI_MERGE_REQUEST_IID:-$(node -p 'require(process.env.GITHUB_EVENT_PATH).pull_request.number')}" --gate ${PR_GATE} --no-status`;
|
|
4022
|
-
OP_OUTPUT_DIR3 = { github: ".github/workflows", forgejo: ".forgejo/workflows", gitlab: "." };
|
|
4023
|
-
}
|
|
4024
|
-
});
|
|
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";
|
|
4417
|
+
` overview: "A model reviews the pull request against its stacks' previews",`,
|
|
4418
|
+
" phases: [",
|
|
4419
|
+
` phase("Preview", [shell(${JSON.stringify(REVIEW_PREVIEW_LINE)})]),`,
|
|
4420
|
+
` phase("Review", [reviewAgent({ ${fields} })]),`,
|
|
4421
|
+
" ],",
|
|
4422
|
+
"});",
|
|
4423
|
+
""
|
|
4424
|
+
].join("\n");
|
|
4197
4425
|
}
|
|
4198
|
-
function
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
if (/uses:\s*["']?pulumi\/actions@/.test(readFileSync8(join8(root, d, n), "utf8"))) out.push(`${d}/${n}`);
|
|
4211
|
-
} catch {
|
|
4426
|
+
function reviewOpSpec(doc) {
|
|
4427
|
+
const settings = toChantSettings({ doc });
|
|
4428
|
+
const p = settings.pipeline;
|
|
4429
|
+
const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
|
|
4430
|
+
return {
|
|
4431
|
+
ops: [
|
|
4432
|
+
{
|
|
4433
|
+
name: REVIEW_OP,
|
|
4434
|
+
trigger: { kind: "pull_request" },
|
|
4435
|
+
reviewAgent: true,
|
|
4436
|
+
// The preview in the review job takes the preview identity; id-token is the one write the review job may hold.
|
|
4437
|
+
...settings.ci.identity ? { permissions: { "id-token": "write" } } : {}
|
|
4212
4438
|
}
|
|
4439
|
+
],
|
|
4440
|
+
options: {
|
|
4441
|
+
...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
|
|
4442
|
+
...engine2.image ? { image: engine2.image } : {}
|
|
4213
4443
|
}
|
|
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
4444
|
};
|
|
4231
4445
|
}
|
|
4232
|
-
function
|
|
4233
|
-
|
|
4446
|
+
function writeIfChanged3(cwd, file, text) {
|
|
4447
|
+
const path = join8(cwd, file);
|
|
4448
|
+
if (existsSync8(path) && readFileSync9(path, "utf8") === text) return false;
|
|
4449
|
+
mkdirSync5(dirname7(path), { recursive: true });
|
|
4450
|
+
writeFileSync5(path, text);
|
|
4451
|
+
return true;
|
|
4234
4452
|
}
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4453
|
+
async function writeReviewJobs(chant, forge, doc, words, table, io) {
|
|
4454
|
+
if (!reviewOf(doc)) return table.done;
|
|
4455
|
+
if (writeIfChanged3(io.cwd, REVIEW_OP_FILE, reviewOpFile(doc))) io.err(`${PRODUCT} ci: wrote ${REVIEW_OP_FILE}; commit it with the workflow`);
|
|
4456
|
+
if (writeIfChanged3(io.cwd, REVIEW_SPEC_FILE, JSON.stringify(reviewOpSpec(doc), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${REVIEW_SPEC_FILE}`);
|
|
4457
|
+
const outDir = OP_OUTPUT_DIR3[forge];
|
|
4458
|
+
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", REVIEW_SPEC_FILE, "--output", outDir], words);
|
|
4459
|
+
if (code !== table.done) return code;
|
|
4460
|
+
const dir = join8(io.cwd, outDir);
|
|
4461
|
+
const files = existsSync8(dir) ? readdirSync5(dir).filter((f) => /\.ya?ml$/.test(f) && (readFileSync9(join8(dir, f), "utf8").split("\n", 1)[0] ?? "").includes(`--spec ${REVIEW_SPEC_FILE}`)) : [];
|
|
4462
|
+
for (const f of files) io.err(`${PRODUCT} ci: review jobs at ${join8(outDir, f).replace(/^\.\//, "")}`);
|
|
4463
|
+
const r = reviewOf(doc);
|
|
4464
|
+
io.err(`${PRODUCT} ci: the review job reads the model's key from ${r.keyEnv}; set it from a secret`);
|
|
4465
|
+
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`);
|
|
4466
|
+
return table.done;
|
|
4467
|
+
}
|
|
4468
|
+
var PERSON_VERBS, READ_INSTRUCTIONS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE, REVIEW_CHANGE_SET, REVIEW_PREVIEW_LINE, OP_OUTPUT_DIR3;
|
|
4469
|
+
var init_agents2 = __esm({
|
|
4470
|
+
"src/agents.ts"() {
|
|
4238
4471
|
"use strict";
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4472
|
+
init_chant_config();
|
|
4473
|
+
init_config_check();
|
|
4474
|
+
init_agents();
|
|
4475
|
+
init_config();
|
|
4476
|
+
init_stacks();
|
|
4477
|
+
init_images();
|
|
4478
|
+
init_stages();
|
|
4479
|
+
init_tips();
|
|
4480
|
+
init_words();
|
|
4481
|
+
PERSON_VERBS = ["up", "approve", "override"];
|
|
4482
|
+
READ_INSTRUCTIONS = `To change a stack, open a pull request: ${PRODUCT} previews it, and its update runs after a person approves the digest.`;
|
|
4483
|
+
REVIEW_OP = "plywood-review";
|
|
4484
|
+
REVIEW_OP_FILE = "ops/plywood-review.op.ts";
|
|
4485
|
+
REVIEW_SPEC_FILE = ".chant/ops/plywood-review.spec.json";
|
|
4486
|
+
REVIEW_CHANGE_SET = ".chant/pr/change-set.json";
|
|
4487
|
+
REVIEW_PREVIEW_LINE = `${BIN} components pr-plan --base "origin/\${GITHUB_BASE_REF:-$CI_MERGE_REQUEST_TARGET_BRANCH_NAME}" --pr "\${CI_MERGE_REQUEST_IID:-$(node -p 'require(process.env.GITHUB_EVENT_PATH).pull_request.number')}" --gate ${PR_GATE} --no-status`;
|
|
4488
|
+
OP_OUTPUT_DIR3 = { github: ".github/workflows", forgejo: ".forgejo/workflows", gitlab: "." };
|
|
4244
4489
|
}
|
|
4245
4490
|
});
|
|
4246
4491
|
|
|
4247
4492
|
// src/policy-presets.ts
|
|
4248
|
-
import { readFileSync as
|
|
4493
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4249
4494
|
function providersRego(allowed, namespace = "main") {
|
|
4250
4495
|
const set = allowed.length ? `{${[...new Set(allowed)].sort().map((p) => JSON.stringify(p)).join(", ")}}` : "set()";
|
|
4251
4496
|
return preset("plywood_providers.rego.tmpl").replace("{{namespace}}", namespace).replace("{{allowed}}", set);
|
|
@@ -4264,7 +4509,7 @@ var init_policy_presets = __esm({
|
|
|
4264
4509
|
PROVIDERS_PACK_CONFIG = "policy/plywood-providers.json";
|
|
4265
4510
|
PROVIDERS_REGO_FILE = "plywood_providers.rego";
|
|
4266
4511
|
PROVIDERS_POLICY = "provider-allowlist";
|
|
4267
|
-
preset = (rel) =>
|
|
4512
|
+
preset = (rel) => readFileSync10(new URL(`../presets/policy/${rel}`, import.meta.url), "utf8");
|
|
4268
4513
|
}
|
|
4269
4514
|
});
|
|
4270
4515
|
|
|
@@ -4617,155 +4862,6 @@ var init_presets = __esm({
|
|
|
4617
4862
|
}
|
|
4618
4863
|
});
|
|
4619
4864
|
|
|
4620
|
-
// src/templates.ts
|
|
4621
|
-
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4622
|
-
function template(name) {
|
|
4623
|
-
return readFileSync10(new URL(`../templates/${name}`, import.meta.url), "utf8");
|
|
4624
|
-
}
|
|
4625
|
-
function fill(text, values) {
|
|
4626
|
-
return text.replace(/\{\{(\w+)\}\}/g, (all, key) => key in values ? values[key] : all);
|
|
4627
|
-
}
|
|
4628
|
-
var init_templates = __esm({
|
|
4629
|
-
"src/templates.ts"() {
|
|
4630
|
-
"use strict";
|
|
4631
|
-
}
|
|
4632
|
-
});
|
|
4633
|
-
|
|
4634
|
-
// src/init/render.ts
|
|
4635
|
-
import { emitYAMLEntry } from "@intentius/chant/yaml";
|
|
4636
|
-
function plywoodYml(keys) {
|
|
4637
|
-
const body = Object.entries(keys).filter(([, v]) => v !== void 0).map(([k, v]) => emitYAMLEntry(k, v)).join("\n");
|
|
4638
|
-
const text = fill(template("plywood.yml"), { keys: body });
|
|
4639
|
-
return text.endsWith("\n") ? text : text + "\n";
|
|
4640
|
-
}
|
|
4641
|
-
function chantConfigTs() {
|
|
4642
|
-
return template("chant.config.ts.tmpl");
|
|
4643
|
-
}
|
|
4644
|
-
function componentName(stack) {
|
|
4645
|
-
return slug(stack.key);
|
|
4646
|
-
}
|
|
4647
|
-
function exportName(stack, taken) {
|
|
4648
|
-
let base = stack.key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4649
|
-
if (base === "" || /^\d/.test(base)) base = `stack_${base}`;
|
|
4650
|
-
let name = base;
|
|
4651
|
-
for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
|
|
4652
|
-
taken.add(name);
|
|
4653
|
-
return name;
|
|
4654
|
-
}
|
|
4655
|
-
function stepInput(s) {
|
|
4656
|
-
return {
|
|
4657
|
-
dir: s.dir,
|
|
4658
|
-
stack: s.stack,
|
|
4659
|
-
member: s.project,
|
|
4660
|
-
...s.engine ? { engine: s.engine } : {},
|
|
4661
|
-
...s.backend ? { backend: s.backend } : {},
|
|
4662
|
-
...s.engineVersion ? { engineVersion: s.engineVersion, ...s.engineRoot ? { engineRoot: s.engineRoot } : {} } : {},
|
|
4663
|
-
...s.lockGrain ? { lockGrain: s.lockGrain } : {},
|
|
4664
|
-
...s.policyPacks?.length ? { policyPacks: s.policyPacks } : {}
|
|
4665
|
-
};
|
|
4666
|
-
}
|
|
4667
|
-
function componentsFile(stacks2, after = {}, opts = {}) {
|
|
4668
|
-
const taken = /* @__PURE__ */ new Set();
|
|
4669
|
-
const lines2 = [
|
|
4670
|
-
"// Written by plywood init from the stacks it found; running it again rewrites",
|
|
4671
|
-
"// this file. Each stack is a chant component whose pulumi-up step previews it",
|
|
4672
|
-
"// on a pull request and updates it in its wave.",
|
|
4673
|
-
""
|
|
4674
|
-
];
|
|
4675
|
-
for (const s of stacks2) {
|
|
4676
|
-
const dependsOn = (after[s.key] ?? []).filter((u) => stacks2.some((x) => x.key === u)).map((u) => componentName({ key: u }));
|
|
4677
|
-
const step = { kind: "pulumi-up", ...stepInput(s), ...opts.reviewSuffix ? { reviewSuffix: opts.reviewSuffix } : {} };
|
|
4678
|
-
const component = { name: componentName(s), dependsOn, deploy: [{ phase: "up", steps: [step] }] };
|
|
4679
|
-
lines2.push(`export const ${exportName(s, taken)} = ${JSON.stringify(component)};`);
|
|
4680
|
-
}
|
|
4681
|
-
return lines2.join("\n") + "\n";
|
|
4682
|
-
}
|
|
4683
|
-
function wavesOf(stacks2, configured) {
|
|
4684
|
-
const byKey = new Map(stacks2.map((s) => [s.key, s]));
|
|
4685
|
-
if (configured && configured.length > 0) {
|
|
4686
|
-
return configured.map((keys, i) => ({ name: `wave-${i + 1}`, stacks: keys.map((k) => byKey.get(k)).filter((s) => s !== void 0) }));
|
|
4687
|
-
}
|
|
4688
|
-
const out = [];
|
|
4689
|
-
for (const s of stacks2) {
|
|
4690
|
-
const env = environmentOf(s.stack);
|
|
4691
|
-
let name = slug(env);
|
|
4692
|
-
if (!WAVE_NAME.test(name)) name = `wave-${out.length + 1}`;
|
|
4693
|
-
const wave = out.find((w) => w.name === name);
|
|
4694
|
-
if (wave) wave.stacks.push(s);
|
|
4695
|
-
else out.push({ name, stacks: [s] });
|
|
4696
|
-
}
|
|
4697
|
-
return out;
|
|
4698
|
-
}
|
|
4699
|
-
function waveRunner(runner) {
|
|
4700
|
-
if (runner === void 0) return void 0;
|
|
4701
|
-
if (typeof runner === "string" || Array.isArray(runner) || "group" in runner) return runner;
|
|
4702
|
-
const staged = runner;
|
|
4703
|
-
return staged.up ?? staged.default;
|
|
4704
|
-
}
|
|
4705
|
-
function wavesSpec(input) {
|
|
4706
|
-
const waves = (input.ordered ?? wavesOf(input.stacks, input.waves)).filter((w) => w.stacks.length > 0);
|
|
4707
|
-
return {
|
|
4708
|
-
name: OP_NAME,
|
|
4709
|
-
op: OP_NAME,
|
|
4710
|
-
gate: PR_GATE,
|
|
4711
|
-
branches: [input.branch],
|
|
4712
|
-
...input.runner !== void 0 ? { runner: input.runner } : {},
|
|
4713
|
-
step: { kind: "pulumi-up" },
|
|
4714
|
-
...input.quorum && input.quorum.length > 0 ? { approvals: { quorum: input.quorum } } : {},
|
|
4715
|
-
...input.notify ? { notify: input.notify } : {},
|
|
4716
|
-
waves: waves.map((w) => {
|
|
4717
|
-
const gate = w.stacks.map((s) => s.gate ?? input.gate).reduce((a, b) => STRICTNESS[b] > STRICTNESS[a] ? b : a);
|
|
4718
|
-
return { name: w.name, gate, runs: w.stacks.map((s) => ({ target: s.key, input: stepInput(s) })) };
|
|
4719
|
-
})
|
|
4720
|
-
};
|
|
4721
|
-
}
|
|
4722
|
-
function jsonText(v) {
|
|
4723
|
-
return JSON.stringify(v, null, 2) + "\n";
|
|
4724
|
-
}
|
|
4725
|
-
function memberName(project, taken) {
|
|
4726
|
-
let base = slug(project.name).slice(0, 36).replace(/-+$/, "");
|
|
4727
|
-
if (!MEMBER_NAME.test(base)) base = "stack";
|
|
4728
|
-
let name = base;
|
|
4729
|
-
for (let i = 2; taken.has(name); i++) name = `${base}-${i}`;
|
|
4730
|
-
taken.add(name);
|
|
4731
|
-
return name;
|
|
4732
|
-
}
|
|
4733
|
-
function workspaceDeclaration(existing, input) {
|
|
4734
|
-
const doc = existing ? structuredClone(existing) : { name: input.name, schema: 1 };
|
|
4735
|
-
const members = Array.isArray(doc.members) ? doc.members : [];
|
|
4736
|
-
const taken = new Set(members.map((m) => String(m.name)));
|
|
4737
|
-
for (const p of input.projects) {
|
|
4738
|
-
if (members.some((m) => m.dir === p.dir || p.dir === "." && m.dir === void 0)) continue;
|
|
4739
|
-
members.push({ name: memberName(p, taken), dir: p.dir, kind: "pulumi" });
|
|
4740
|
-
}
|
|
4741
|
-
doc.members = members;
|
|
4742
|
-
const pins = Array.isArray(doc.pins) ? doc.pins : [];
|
|
4743
|
-
if (!pins.some((p) => p.package === PULUMI_LEXICON)) pins.push({ package: PULUMI_LEXICON, version: input.lexiconVersion });
|
|
4744
|
-
doc.pins = pins;
|
|
4745
|
-
const ci2 = doc.ci && typeof doc.ci === "object" && !Array.isArray(doc.ci) ? doc.ci : {};
|
|
4746
|
-
for (const [k, v] of Object.entries(input.ci)) if (v !== void 0) ci2[k] = v;
|
|
4747
|
-
if (Object.keys(ci2).length > 0) doc.ci = ci2;
|
|
4748
|
-
return doc;
|
|
4749
|
-
}
|
|
4750
|
-
function memberOfDir(doc, dir) {
|
|
4751
|
-
const members = doc && Array.isArray(doc.members) ? doc.members : [];
|
|
4752
|
-
const m = members.find((x) => x.dir === dir || dir === "." && x.dir === void 0);
|
|
4753
|
-
return typeof m?.name === "string" ? m.name : void 0;
|
|
4754
|
-
}
|
|
4755
|
-
var WAVE_NAME, STRICTNESS, MEMBER_NAME, PULUMI_LEXICON;
|
|
4756
|
-
var init_render = __esm({
|
|
4757
|
-
"src/init/render.ts"() {
|
|
4758
|
-
"use strict";
|
|
4759
|
-
init_templates();
|
|
4760
|
-
init_words();
|
|
4761
|
-
init_detect();
|
|
4762
|
-
WAVE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
4763
|
-
STRICTNESS = { never: 0, "on-destructive": 1, always: 2 };
|
|
4764
|
-
MEMBER_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
4765
|
-
PULUMI_LEXICON = "@intentius/chant-lexicon-pulumi";
|
|
4766
|
-
}
|
|
4767
|
-
});
|
|
4768
|
-
|
|
4769
4865
|
// src/stack-order.ts
|
|
4770
4866
|
import { join as join9 } from "node:path";
|
|
4771
4867
|
function chantReferenceReader() {
|
|
@@ -5400,6 +5496,11 @@ __export(init_exports, {
|
|
|
5400
5496
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5401
5497
|
import { emitYAMLEntry as emitYAMLEntry2 } from "@intentius/chant/yaml";
|
|
5402
5498
|
import { dirname as dirname9, join as join13, posix as posix3 } from "node:path";
|
|
5499
|
+
function chantPin() {
|
|
5500
|
+
const pin = dependencies["@intentius/chant"];
|
|
5501
|
+
if (!pin) throw new Error("packages/plywood/package.json has no @intentius/chant dependency");
|
|
5502
|
+
return pin;
|
|
5503
|
+
}
|
|
5403
5504
|
function parseInitArgs(args) {
|
|
5404
5505
|
if (args.includes("--print-prompt")) return { printPrompt: true };
|
|
5405
5506
|
let rest = args;
|
|
@@ -5620,7 +5721,7 @@ async function runInit(opts, table, io) {
|
|
|
5620
5721
|
const message = needsChantMessage("Writing the waves spec and the workflow");
|
|
5621
5722
|
files.push({ file: WAVES_SPEC_FILE2, status: "skipped", note: message }, { file: WORKFLOW_FILE[forge.kind], status: "skipped", note: "needs the same chant" });
|
|
5622
5723
|
} else {
|
|
5623
|
-
let spec = wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves);
|
|
5724
|
+
let spec = await wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves, forge.kind);
|
|
5624
5725
|
if (settings.pipeline.jobs) {
|
|
5625
5726
|
try {
|
|
5626
5727
|
spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind, { parallelism: await hasChant4625() });
|
|
@@ -5638,6 +5739,9 @@ async function runInit(opts, table, io) {
|
|
|
5638
5739
|
if (opts.bucketCheck) notes.push(...await bucketStep(detected2, doc, io.env, buckets));
|
|
5639
5740
|
adopt.push(...await adoptStep(root, detected2, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
|
|
5640
5741
|
todo.push(...todoLines(detected2, doc, forge.kind, io.env));
|
|
5742
|
+
if (forge.kind === "gitlab" && chant && stacks2.length > 0 && previewOf(doc) && await hasChantWavePreview()) {
|
|
5743
|
+
todo.push(`Add a ${GITLAB_READ_TOKEN} CI/CD variable (a project access token with the read_api scope, masked and protected) so a wave can read the merge request it applies; without it the wave refuses`);
|
|
5744
|
+
}
|
|
5641
5745
|
const failed = files.find((f) => f.status === "failed");
|
|
5642
5746
|
return {
|
|
5643
5747
|
ok: !failed,
|
|
@@ -5702,7 +5806,7 @@ async function preCommitStep(root, config2, dryRun) {
|
|
|
5702
5806
|
const after = readFileSync12(join13(root, DECLARATION_FILE), "utf8");
|
|
5703
5807
|
return { file: DECLARATION_FILE, status: after === before ? "unchanged" : "written", note: `${config2} runs as the check hook pre-commit; the check job's image needs pre-commit` };
|
|
5704
5808
|
}
|
|
5705
|
-
function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered) {
|
|
5809
|
+
async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, forge) {
|
|
5706
5810
|
const runner = waveRunner(chantPipelineOptions(settings.pipeline, false).runner);
|
|
5707
5811
|
const base = wavesSpec({
|
|
5708
5812
|
stacks: stacks2,
|
|
@@ -5714,7 +5818,8 @@ function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered) {
|
|
|
5714
5818
|
// notify.relay only on a chant with the relay (chant#4611); an older chant refuses the setting.
|
|
5715
5819
|
...settings.pipeline.notify ? { notify: { ...notifyForChant(settings.pipeline.notify, chant) } } : {}
|
|
5716
5820
|
});
|
|
5717
|
-
|
|
5821
|
+
const named = chant.productWavesSpec ? chant.productWavesSpec(base, PRODUCT_WORDS) : base;
|
|
5822
|
+
return previewOf(doc) && await hasChantWavePreview() ? withWavePreview(named, forge) : named;
|
|
5718
5823
|
}
|
|
5719
5824
|
async function workflowStep(chant, root, doc, settings, forge, opts, spec, table, io) {
|
|
5720
5825
|
const output = WORKFLOW_FILE[forge];
|
|
@@ -5872,6 +5977,7 @@ var init_init = __esm({
|
|
|
5872
5977
|
init_stacks();
|
|
5873
5978
|
init_types();
|
|
5874
5979
|
init_images();
|
|
5980
|
+
init_chant_next();
|
|
5875
5981
|
init_jobs2();
|
|
5876
5982
|
init_backend();
|
|
5877
5983
|
init_adopt();
|
|
@@ -5891,7 +5997,7 @@ var init_init = __esm({
|
|
|
5891
5997
|
CHANT_CONFIG_FILE = "chant.config.ts";
|
|
5892
5998
|
COMPONENTS_FILE = "plywood.component.ts";
|
|
5893
5999
|
WAVES_SPEC_FILE2 = `.chant/op-waves/${OP_NAME}.spec.json`;
|
|
5894
|
-
CHANT_PIN =
|
|
6000
|
+
CHANT_PIN = chantPin();
|
|
5895
6001
|
}
|
|
5896
6002
|
});
|
|
5897
6003
|
|
|
@@ -6244,8 +6350,25 @@ async function withChant(what, table, io, run) {
|
|
|
6244
6350
|
return run(chant);
|
|
6245
6351
|
}
|
|
6246
6352
|
function passthrough(argv, table, io) {
|
|
6353
|
+
if (argv[0] === "run" && argv[1] === "wave") return runWave(argv, table, io);
|
|
6247
6354
|
return withChant(`${PRODUCT} ${argv[0]}`, table, io, (chant) => chant.runChantInProcess(argv, PRODUCT_WORDS));
|
|
6248
6355
|
}
|
|
6356
|
+
async function runWave(argv, table, io) {
|
|
6357
|
+
const seen = [];
|
|
6358
|
+
const write = process.stderr.write;
|
|
6359
|
+
process.stderr.write = ((chunk, ...rest) => {
|
|
6360
|
+
seen.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
6361
|
+
return write.call(process.stderr, chunk, ...rest);
|
|
6362
|
+
});
|
|
6363
|
+
let code;
|
|
6364
|
+
try {
|
|
6365
|
+
code = await withChant(`${PRODUCT} run wave`, table, io, (chant) => chant.runChantInProcess(argv, PRODUCT_WORDS));
|
|
6366
|
+
} finally {
|
|
6367
|
+
process.stderr.write = write;
|
|
6368
|
+
}
|
|
6369
|
+
if (code === table.moved) io.err(`${PRODUCT}: ${waveMovedLine(seen.join(""))}`);
|
|
6370
|
+
return code;
|
|
6371
|
+
}
|
|
6249
6372
|
async function check(args, json, table, io) {
|
|
6250
6373
|
const file = takeFlag(args, "--file");
|
|
6251
6374
|
if (file.missing) return usage("--file takes a path", table, io);
|
|
@@ -6544,6 +6667,7 @@ var init_stages = __esm({
|
|
|
6544
6667
|
init_review_stacks2();
|
|
6545
6668
|
init_waves_workflow();
|
|
6546
6669
|
init_drift2();
|
|
6670
|
+
init_refusals();
|
|
6547
6671
|
init_agents2();
|
|
6548
6672
|
init_agent_jobs();
|
|
6549
6673
|
init_words();
|
|
@@ -7804,7 +7928,7 @@ var execChild = (bin, args, opts) => new Promise((done) => {
|
|
|
7804
7928
|
|
|
7805
7929
|
// src/cli.ts
|
|
7806
7930
|
init_words();
|
|
7807
|
-
var VERSION3 = true ? "0.
|
|
7931
|
+
var VERSION3 = true ? "0.3.1" : "0.0.0-dev";
|
|
7808
7932
|
var stdio = {
|
|
7809
7933
|
out: (s) => process.stdout.write(s + "\n"),
|
|
7810
7934
|
err: (s) => process.stderr.write(s + "\n")
|