@intentius/plywood 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plywood.mjs +607 -493
- package/dist/plywood.mjs.map +4 -4
- package/package.json +7 -7
- package/plywood.schema.json +1 -1
package/dist/plywood.mjs
CHANGED
|
@@ -422,7 +422,7 @@ var init_words = __esm({
|
|
|
422
422
|
"src/words.ts"() {
|
|
423
423
|
"use strict";
|
|
424
424
|
PRODUCT = "plywood";
|
|
425
|
-
TAGLINE = "
|
|
425
|
+
TAGLINE = "Sophisticated automation for Pulumi.";
|
|
426
426
|
OP_NAME = "pulumi-up";
|
|
427
427
|
PR_GATE = "up";
|
|
428
428
|
VERBS = [
|
|
@@ -587,13 +587,13 @@ var init_control = __esm({
|
|
|
587
587
|
var version, dependencies;
|
|
588
588
|
var init_package = __esm({
|
|
589
589
|
"package.json"() {
|
|
590
|
-
version = "0.
|
|
590
|
+
version = "0.3.0";
|
|
591
591
|
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.
|
|
592
|
+
"@intentius/chant": "0.124.59",
|
|
593
|
+
"@intentius/chant-lexicon-forgejo": "0.124.59",
|
|
594
|
+
"@intentius/chant-lexicon-github": "0.124.59",
|
|
595
|
+
"@intentius/chant-lexicon-gitlab": "0.124.59",
|
|
596
|
+
"@intentius/chant-lexicon-pulumi": "0.124.59"
|
|
597
597
|
};
|
|
598
598
|
}
|
|
599
599
|
});
|
|
@@ -603,8 +603,8 @@ var image_digests_default;
|
|
|
603
603
|
var init_image_digests = __esm({
|
|
604
604
|
"src/image-digests.json"() {
|
|
605
605
|
image_digests_default = {
|
|
606
|
-
"ghcr.io/intentius/plywood-pulumi:0.
|
|
607
|
-
"ghcr.io/intentius/plywood-plunge:0.
|
|
606
|
+
"ghcr.io/intentius/plywood-pulumi:0.3.0-pulumi3.272.0": "sha256:39fc85f67be6a281aa5f01b02239aa1f02c492f40f1fb0d145c848cc608aaecd",
|
|
607
|
+
"ghcr.io/intentius/plywood-plunge:0.3.0-plunge0.2.0": "sha256:6647f67defb1719395b29d1a6c9d977b6900e228547c4569563962e4234847e3"
|
|
608
608
|
};
|
|
609
609
|
}
|
|
610
610
|
});
|
|
@@ -741,14 +741,14 @@ var init_images = __esm({
|
|
|
741
741
|
TOOL_VERSIONS = {
|
|
742
742
|
pulumi: "3.272.0",
|
|
743
743
|
/** A plunge release (INTENTIUS/plunge, tag plunge-v<ver>), not the Pulumi version it is built from. */
|
|
744
|
-
plunge: "0.
|
|
744
|
+
plunge: "0.2.0"
|
|
745
745
|
};
|
|
746
746
|
ARCHES = ["x64", "arm64"];
|
|
747
747
|
PINNED_SHA256 = {
|
|
748
748
|
"pulumi-v3.272.0-linux-x64.tar.gz": "58d545f2b98761051922008ebd381ecef95484d7f7a5ff7faa15d7a780ea84f6",
|
|
749
749
|
"pulumi-v3.272.0-linux-arm64.tar.gz": "4f0741901b8982c4326b702dabf483c63d2cf77c88ac42dd752b72541284a534",
|
|
750
|
-
"plunge-v0.
|
|
751
|
-
"plunge-v0.
|
|
750
|
+
"plunge-v0.2.0-linux-x64.tar.gz": "c9c73d05cca9ad214b7f44230a6162cdcb6ad64bbfc725bc94688b997b191495",
|
|
751
|
+
"plunge-v0.2.0-linux-arm64.tar.gz": "c176a82f038164085aa19c8ad6530d6022e560e124050e535dbe364ae0473357"
|
|
752
752
|
};
|
|
753
753
|
ENGINE_ROOT = "/opt/plywood";
|
|
754
754
|
JOB_ARCH = `"$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')"`;
|
|
@@ -2458,7 +2458,7 @@ var init_chant_product = __esm({
|
|
|
2458
2458
|
"src/chant-product.ts"() {
|
|
2459
2459
|
"use strict";
|
|
2460
2460
|
REQUIRED2 = ["assertProductWords", "productPipelineJobs", "approveLine", "runChantInProcess"];
|
|
2461
|
-
BUILT_CHANT = true ? "0.124.
|
|
2461
|
+
BUILT_CHANT = true ? "0.124.59" : "unknown";
|
|
2462
2462
|
}
|
|
2463
2463
|
});
|
|
2464
2464
|
|
|
@@ -2475,14 +2475,37 @@ function hasChantApprovalStatus(chant) {
|
|
|
2475
2475
|
function hasChantPullRequestApprovalStatus(chant) {
|
|
2476
2476
|
return typeof chant?.APPROVAL_STATUS_PULL_REQUEST_FLOOR === "string";
|
|
2477
2477
|
}
|
|
2478
|
+
function hasChantWavePreview() {
|
|
2479
|
+
wavePreview ??= (async () => {
|
|
2480
|
+
try {
|
|
2481
|
+
const m = await import("@intentius/chant/op");
|
|
2482
|
+
return typeof m.OP_WAVES_PREVIEW_FLOOR === "string";
|
|
2483
|
+
} catch {
|
|
2484
|
+
return false;
|
|
2485
|
+
}
|
|
2486
|
+
})();
|
|
2487
|
+
return wavePreview;
|
|
2488
|
+
}
|
|
2489
|
+
function withWavePreview(spec, forge) {
|
|
2490
|
+
const approvals = spec.approvals && typeof spec.approvals === "object" && !Array.isArray(spec.approvals) ? spec.approvals : {};
|
|
2491
|
+
const next = approvals.preview !== void 0 ? spec : { ...spec, approvals: { ...approvals, preview: true } };
|
|
2492
|
+
if (forge === "gitlab" || next.approvals.preview !== true || !Array.isArray(next.waves)) return next;
|
|
2493
|
+
const waves = next.waves.map((w) => ({
|
|
2494
|
+
...w,
|
|
2495
|
+
variables: { [WAVE_TOKEN_VARIABLE]: "${{ github.token }}", ...w.variables ?? {} }
|
|
2496
|
+
}));
|
|
2497
|
+
return { ...next, waves };
|
|
2498
|
+
}
|
|
2478
2499
|
function chantAudit() {
|
|
2479
2500
|
audit ??= import("@intentius/chant/ci-audit").then((m) => m);
|
|
2480
2501
|
return audit;
|
|
2481
2502
|
}
|
|
2482
|
-
var audit;
|
|
2503
|
+
var wavePreview, WAVE_TOKEN_VARIABLE, GITLAB_READ_TOKEN, audit;
|
|
2483
2504
|
var init_chant_next = __esm({
|
|
2484
2505
|
"src/chant-next.ts"() {
|
|
2485
2506
|
"use strict";
|
|
2507
|
+
WAVE_TOKEN_VARIABLE = "GITHUB_TOKEN";
|
|
2508
|
+
GITLAB_READ_TOKEN = "CHANT_FORGE_TOKEN";
|
|
2486
2509
|
}
|
|
2487
2510
|
});
|
|
2488
2511
|
|
|
@@ -2631,6 +2654,38 @@ var init_note_control = __esm({
|
|
|
2631
2654
|
}
|
|
2632
2655
|
});
|
|
2633
2656
|
|
|
2657
|
+
// src/init/run-group.ts
|
|
2658
|
+
function withRunGroup(text, forge, key) {
|
|
2659
|
+
if (forge !== "forgejo" || TOP_CONCURRENCY.test(text)) return text;
|
|
2660
|
+
const jobs2 = TOP_JOBS.exec(text);
|
|
2661
|
+
if (!jobs2) return text;
|
|
2662
|
+
const name = (TOP_NAME.exec(text)?.[1] ?? "plywood").replace(/^["']|["']$/g, "");
|
|
2663
|
+
const group = key === "commit" ? `${name}-\${{ github.event_name }}-\${{ github.sha }}` : `${name}-\${{ github.event_name == 'pull_request' && github.ref || github.run_id }}`;
|
|
2664
|
+
const cancel = key === "commit" ? "false" : "${{ github.event_name == 'pull_request' }}";
|
|
2665
|
+
const block = [
|
|
2666
|
+
"# Without a group Forgejo cancels the run in progress when a push starts another run of this workflow.",
|
|
2667
|
+
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).",
|
|
2668
|
+
"concurrency:",
|
|
2669
|
+
` group: ${yamlScalar(group)}`,
|
|
2670
|
+
` cancel-in-progress: ${yamlScalar(cancel)}`,
|
|
2671
|
+
"",
|
|
2672
|
+
""
|
|
2673
|
+
].join("\n");
|
|
2674
|
+
return text.slice(0, jobs2.index) + block + text.slice(jobs2.index);
|
|
2675
|
+
}
|
|
2676
|
+
function yamlScalar(value) {
|
|
2677
|
+
return value === "false" ? value : JSON.stringify(value);
|
|
2678
|
+
}
|
|
2679
|
+
var TOP_CONCURRENCY, TOP_JOBS, TOP_NAME;
|
|
2680
|
+
var init_run_group = __esm({
|
|
2681
|
+
"src/init/run-group.ts"() {
|
|
2682
|
+
"use strict";
|
|
2683
|
+
TOP_CONCURRENCY = /^concurrency:/m;
|
|
2684
|
+
TOP_JOBS = /^jobs:/m;
|
|
2685
|
+
TOP_NAME = /^name:\s*(.+?)\s*$/m;
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2634
2689
|
// src/init/workflow.ts
|
|
2635
2690
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
2636
2691
|
import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
|
|
@@ -2643,7 +2698,7 @@ async function writeWorkflow(chant, input) {
|
|
|
2643
2698
|
if (previous !== void 0) writeFileSync(file, previous);
|
|
2644
2699
|
return { code: code !== 0 ? code : 1, file, status: "failed", kept: [] };
|
|
2645
2700
|
}
|
|
2646
|
-
const generated = withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl);
|
|
2701
|
+
const generated = withRunGroup(withNoteControl(readFileSync2(file, "utf8"), input.forge, input.noteControl), input.forge, "pull-request");
|
|
2647
2702
|
const { text, kept } = keepOwnJobs(previous, generated, input.forge);
|
|
2648
2703
|
writeFileSync(file, text);
|
|
2649
2704
|
return { code, file, status: text === previous ? "unchanged" : "written", kept };
|
|
@@ -2653,6 +2708,7 @@ var init_workflow = __esm({
|
|
|
2653
2708
|
"use strict";
|
|
2654
2709
|
init_own_jobs();
|
|
2655
2710
|
init_note_control();
|
|
2711
|
+
init_run_group();
|
|
2656
2712
|
}
|
|
2657
2713
|
});
|
|
2658
2714
|
|
|
@@ -2794,11 +2850,9 @@ async function writeWaveJobs(chant, forge, waves, words, table, io, engine2 = {}
|
|
|
2794
2850
|
}
|
|
2795
2851
|
const lines2 = hasChant4607(chant) ? void 0 : binLines(waves, words.bin);
|
|
2796
2852
|
for (const file of files) {
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
if (next !== text) writeFileSync2(file, next);
|
|
2801
|
-
}
|
|
2853
|
+
const text = readFileSync3(file, "utf8");
|
|
2854
|
+
const next = withRunGroup(lines2 ? withBinLines(text, lines2) : text, forge, "commit");
|
|
2855
|
+
if (next !== text) writeFileSync2(file, next);
|
|
2802
2856
|
io.err(`${PRODUCT} ci: waves workflow at ${file.slice(io.cwd.length + 1)}`);
|
|
2803
2857
|
}
|
|
2804
2858
|
if (forge === "gitlab") {
|
|
@@ -2812,6 +2866,7 @@ var init_waves_workflow = __esm({
|
|
|
2812
2866
|
"use strict";
|
|
2813
2867
|
init_chant_product();
|
|
2814
2868
|
init_images();
|
|
2869
|
+
init_run_group();
|
|
2815
2870
|
init_words();
|
|
2816
2871
|
WAVES_OUTPUT_DIR = {
|
|
2817
2872
|
github: ".github/workflows",
|
|
@@ -3111,13 +3166,383 @@ async function prJobsFor(pipeline) {
|
|
|
3111
3166
|
const { flags, notes } = prLoopJobFlags(pipeline.jobs, pipeline.runner);
|
|
3112
3167
|
return { options: prLoopJobOptions(pipeline.jobs, pipeline.runner), flags, notes };
|
|
3113
3168
|
}
|
|
3114
|
-
var detected, JOBS_NEED_CHANT, GROUP_NAME;
|
|
3115
|
-
var init_jobs2 = __esm({
|
|
3116
|
-
"src/jobs.ts"() {
|
|
3169
|
+
var detected, JOBS_NEED_CHANT, GROUP_NAME;
|
|
3170
|
+
var init_jobs2 = __esm({
|
|
3171
|
+
"src/jobs.ts"() {
|
|
3172
|
+
"use strict";
|
|
3173
|
+
init_images2();
|
|
3174
|
+
JOBS_NEED_CHANT = "needs chant's runners, variables, passed secrets and parallelism on the pull request jobs (chant#4625), which this chant lacks";
|
|
3175
|
+
GROUP_NAME = /[^a-z0-9_-]+/g;
|
|
3176
|
+
}
|
|
3177
|
+
});
|
|
3178
|
+
|
|
3179
|
+
// src/init/detect.ts
|
|
3180
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
3181
|
+
import { basename, dirname as dirname5, join as join5, relative, sep } from "node:path";
|
|
3182
|
+
import { parseYAML as parseYAML2 } from "@intentius/chant/yaml";
|
|
3183
|
+
function repoRoot(dir) {
|
|
3184
|
+
let cur = dir;
|
|
3185
|
+
for (let i = 0; i < 64; i++) {
|
|
3186
|
+
if (existsSync5(join5(cur, ".git"))) return cur;
|
|
3187
|
+
const parent = dirname5(cur);
|
|
3188
|
+
if (parent === cur) return dir;
|
|
3189
|
+
cur = parent;
|
|
3190
|
+
}
|
|
3191
|
+
return dir;
|
|
3192
|
+
}
|
|
3193
|
+
function posixRel(root, path) {
|
|
3194
|
+
const rel = relative(root, path);
|
|
3195
|
+
return rel === "" ? "." : rel.split(sep).join("/");
|
|
3196
|
+
}
|
|
3197
|
+
function readYamlFile(file) {
|
|
3198
|
+
try {
|
|
3199
|
+
const doc = parseYAML2(readFileSync5(file, "utf8"));
|
|
3200
|
+
return doc && typeof doc === "object" && !Array.isArray(doc) ? doc : void 0;
|
|
3201
|
+
} catch {
|
|
3202
|
+
return void 0;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
function findPulumiProjects(root) {
|
|
3206
|
+
const out = [];
|
|
3207
|
+
const walk = (dir, depth) => {
|
|
3208
|
+
let entries;
|
|
3209
|
+
try {
|
|
3210
|
+
entries = readdirSync3(dir).sort();
|
|
3211
|
+
} catch {
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
const file = ["Pulumi.yaml", "Pulumi.yml"].find((n) => entries.includes(n));
|
|
3215
|
+
if (file) {
|
|
3216
|
+
const project = readProject(root, dir, join5(dir, file));
|
|
3217
|
+
if (project) out.push(project);
|
|
3218
|
+
}
|
|
3219
|
+
if (depth >= MAX_DEPTH) return;
|
|
3220
|
+
for (const name of entries) {
|
|
3221
|
+
if (SKIP.has(name) || name.startsWith(".")) continue;
|
|
3222
|
+
const path = join5(dir, name);
|
|
3223
|
+
try {
|
|
3224
|
+
if (statSync(path).isDirectory()) walk(path, depth + 1);
|
|
3225
|
+
} catch {
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
walk(root, 0);
|
|
3230
|
+
return out;
|
|
3231
|
+
}
|
|
3232
|
+
function readProject(root, dir, file) {
|
|
3233
|
+
const doc = readYamlFile(file);
|
|
3234
|
+
if (!doc || typeof doc.name !== "string" || doc.name === "") return void 0;
|
|
3235
|
+
const runtime = typeof doc.runtime === "string" ? doc.runtime : doc.runtime?.name;
|
|
3236
|
+
const backend = doc.backend;
|
|
3237
|
+
const configDir = typeof doc.stackConfigDir === "string" ? join5(dir, doc.stackConfigDir) : dir;
|
|
3238
|
+
return {
|
|
3239
|
+
name: doc.name,
|
|
3240
|
+
dir: posixRel(root, dir),
|
|
3241
|
+
...typeof runtime === "string" ? { runtime } : {},
|
|
3242
|
+
...backend && typeof backend.url === "string" ? { backendUrl: backend.url } : {},
|
|
3243
|
+
stacks: stackNames(configDir)
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
function stackNames(configDir) {
|
|
3247
|
+
let entries;
|
|
3248
|
+
try {
|
|
3249
|
+
entries = readdirSync3(configDir);
|
|
3250
|
+
} catch {
|
|
3251
|
+
return [];
|
|
3252
|
+
}
|
|
3253
|
+
const names = entries.map((n) => /^Pulumi\.(.+)\.ya?ml$/.exec(n)?.[1]).filter((s) => s !== void 0 && s !== "");
|
|
3254
|
+
return [...new Set(names)].sort(byEnvironment);
|
|
3255
|
+
}
|
|
3256
|
+
function environmentOf(stack) {
|
|
3257
|
+
const last = stack.split("/").pop().toLowerCase();
|
|
3258
|
+
const head = last.split(/[-_.]/)[0];
|
|
3259
|
+
return ENV_ORDER.includes(head) ? head : last;
|
|
3260
|
+
}
|
|
3261
|
+
function rank(stack) {
|
|
3262
|
+
const env = environmentOf(stack);
|
|
3263
|
+
const at = ENV_ORDER.indexOf(env);
|
|
3264
|
+
return at >= 0 ? at : PROD_RANK - 0.5;
|
|
3265
|
+
}
|
|
3266
|
+
function byEnvironment(a, b) {
|
|
3267
|
+
return rank(a) - rank(b) || a.localeCompare(b);
|
|
3268
|
+
}
|
|
3269
|
+
function stacksOf2(projects) {
|
|
3270
|
+
const names = /* @__PURE__ */ new Map();
|
|
3271
|
+
for (const p of projects) names.set(p.name, (names.get(p.name) ?? 0) + 1);
|
|
3272
|
+
const out = [];
|
|
3273
|
+
for (const p of projects) {
|
|
3274
|
+
const project = (names.get(p.name) ?? 0) > 1 ? `${p.name}-${slug(p.dir)}` : p.name;
|
|
3275
|
+
for (const stack of p.stacks) out.push({ key: `${project}/${stack}`, project: p.name, stack, dir: p.dir });
|
|
3276
|
+
}
|
|
3277
|
+
return out.sort((x, y) => byEnvironment(x.stack, y.stack) || x.key.localeCompare(y.key));
|
|
3278
|
+
}
|
|
3279
|
+
function slug(s) {
|
|
3280
|
+
const out = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3281
|
+
return out === "" ? "root" : out;
|
|
3282
|
+
}
|
|
3283
|
+
function originUrl(root) {
|
|
3284
|
+
const config2 = gitFile(root, "config");
|
|
3285
|
+
if (!config2) return void 0;
|
|
3286
|
+
let inOrigin = false;
|
|
3287
|
+
for (const raw of config2.split(/\r?\n/)) {
|
|
3288
|
+
const line = raw.trim();
|
|
3289
|
+
if (line.startsWith("[")) {
|
|
3290
|
+
inOrigin = /^\[remote\s+"origin"\]$/.test(line);
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
const m = inOrigin ? /^url\s*=\s*(.+)$/.exec(line) : null;
|
|
3294
|
+
if (m) return m[1].trim();
|
|
3295
|
+
}
|
|
3296
|
+
return void 0;
|
|
3297
|
+
}
|
|
3298
|
+
function gitFile(root, name) {
|
|
3299
|
+
const dotGit = join5(root, ".git");
|
|
3300
|
+
let gitDir = dotGit;
|
|
3301
|
+
try {
|
|
3302
|
+
if (statSync(dotGit).isFile()) {
|
|
3303
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync5(dotGit, "utf8"));
|
|
3304
|
+
if (!m) return void 0;
|
|
3305
|
+
gitDir = join5(root, m[1].trim());
|
|
3306
|
+
const common = join5(gitDir, "commondir");
|
|
3307
|
+
if (existsSync5(common) && !existsSync5(join5(gitDir, name))) gitDir = join5(gitDir, readFileSync5(common, "utf8").trim());
|
|
3308
|
+
}
|
|
3309
|
+
const path = join5(gitDir, name);
|
|
3310
|
+
return existsSync5(path) ? readFileSync5(path, "utf8") : void 0;
|
|
3311
|
+
} catch {
|
|
3312
|
+
return void 0;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
function remoteHost(url) {
|
|
3316
|
+
const scp = /^[^@/\s]+@([^:/\s]+):/.exec(url);
|
|
3317
|
+
if (scp) return scp[1].toLowerCase();
|
|
3318
|
+
try {
|
|
3319
|
+
return new URL(url).hostname.toLowerCase() || void 0;
|
|
3320
|
+
} catch {
|
|
3321
|
+
return void 0;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
function forgeOfRemote(url) {
|
|
3325
|
+
const host = remoteHost(url);
|
|
3326
|
+
if (!host) return void 0;
|
|
3327
|
+
if (host === "github.com" || host.endsWith(".github.com") || host.startsWith("github.")) return { kind: "github", because: `origin is on ${host}` };
|
|
3328
|
+
if (host === "gitlab.com") return { kind: "gitlab", because: `origin is on ${host}` };
|
|
3329
|
+
if (host === "codeberg.org") return { kind: "forgejo", because: `origin is on ${host}` };
|
|
3330
|
+
if (/(^|[.-])gitlab([.-]|$)/.test(host)) return { kind: "gitlab", because: `origin is on ${host}`, url: `https://${host}` };
|
|
3331
|
+
if (/(^|[.-])(forgejo|gitea)([.-]|$)/.test(host)) return { kind: "forgejo", because: `origin is on ${host}`, url: `https://${host}` };
|
|
3332
|
+
return void 0;
|
|
3333
|
+
}
|
|
3334
|
+
function detectForge(root) {
|
|
3335
|
+
const url = originUrl(root);
|
|
3336
|
+
const byRemote = url ? forgeOfRemote(url) : void 0;
|
|
3337
|
+
if (byRemote) return byRemote;
|
|
3338
|
+
if (existsSync5(join5(root, ".forgejo"))) return { kind: "forgejo", because: "the repository has .forgejo/" };
|
|
3339
|
+
if (existsSync5(join5(root, ".gitlab-ci.yml"))) return { kind: "gitlab", because: "the repository has .gitlab-ci.yml" };
|
|
3340
|
+
if (existsSync5(join5(root, ".github", "workflows"))) return { kind: "github", because: "the repository has .github/workflows/" };
|
|
3341
|
+
return void 0;
|
|
3342
|
+
}
|
|
3343
|
+
function defaultBranch(root) {
|
|
3344
|
+
const head = gitFile(root, "refs/remotes/origin/HEAD");
|
|
3345
|
+
const remote = head && /ref:\s*refs\/remotes\/origin\/(.+)/.exec(head)?.[1];
|
|
3346
|
+
if (remote) return remote.trim();
|
|
3347
|
+
const local = gitFile(root, "HEAD");
|
|
3348
|
+
const branch = local && /ref:\s*refs\/heads\/(.+)/.exec(local)?.[1];
|
|
3349
|
+
return branch ? branch.trim() : "main";
|
|
3350
|
+
}
|
|
3351
|
+
function findPulumiActions(root) {
|
|
3352
|
+
const out = [];
|
|
3353
|
+
for (const d of WORKFLOW_DIRS) {
|
|
3354
|
+
let names;
|
|
3355
|
+
try {
|
|
3356
|
+
names = readdirSync3(join5(root, d)).sort();
|
|
3357
|
+
} catch {
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
for (const n of names) {
|
|
3361
|
+
if (!/\.ya?ml$/.test(n)) continue;
|
|
3362
|
+
try {
|
|
3363
|
+
if (/uses:\s*["']?pulumi\/actions@/.test(readFileSync5(join5(root, d, n), "utf8"))) out.push(`${d}/${n}`);
|
|
3364
|
+
} catch {
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return out;
|
|
3369
|
+
}
|
|
3370
|
+
function detect(cwd) {
|
|
3371
|
+
const root = repoRoot(cwd);
|
|
3372
|
+
const projects = findPulumiProjects(root);
|
|
3373
|
+
const forge = detectForge(root);
|
|
3374
|
+
const preCommit = [".pre-commit-config.yaml", ".pre-commit-config.yml"].find((n) => existsSync5(join5(root, n)));
|
|
3375
|
+
return {
|
|
3376
|
+
root,
|
|
3377
|
+
projects,
|
|
3378
|
+
stacks: stacksOf2(projects),
|
|
3379
|
+
...forge ? { forge } : {},
|
|
3380
|
+
defaultBranch: defaultBranch(root),
|
|
3381
|
+
pulumiActions: findPulumiActions(root),
|
|
3382
|
+
...preCommit ? { preCommit } : {}
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
function repoName(root) {
|
|
3386
|
+
return slug(basename(root)).slice(0, 40).replace(/-+$/, "") || "plywood";
|
|
3387
|
+
}
|
|
3388
|
+
var SKIP, MAX_DEPTH, ENV_ORDER, PROD_RANK, WORKFLOW_DIRS;
|
|
3389
|
+
var init_detect = __esm({
|
|
3390
|
+
"src/init/detect.ts"() {
|
|
3391
|
+
"use strict";
|
|
3392
|
+
SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "vendor", "venv", ".venv", "__pycache__", "target", "bin", "obj"]);
|
|
3393
|
+
MAX_DEPTH = 8;
|
|
3394
|
+
ENV_ORDER = ["local", "dev", "development", "sandbox", "test", "testing", "qa", "ci", "stage", "staging", "preprod", "uat", "prod", "production"];
|
|
3395
|
+
PROD_RANK = ENV_ORDER.indexOf("prod");
|
|
3396
|
+
WORKFLOW_DIRS = [".github/workflows", ".forgejo/workflows", ".gitea/workflows"];
|
|
3397
|
+
}
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
// src/templates.ts
|
|
3401
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3402
|
+
function template(name) {
|
|
3403
|
+
return readFileSync6(new URL(`../templates/${name}`, import.meta.url), "utf8");
|
|
3404
|
+
}
|
|
3405
|
+
function fill(text, values) {
|
|
3406
|
+
return text.replace(/\{\{(\w+)\}\}/g, (all, key) => key in values ? values[key] : all);
|
|
3407
|
+
}
|
|
3408
|
+
var init_templates = __esm({
|
|
3409
|
+
"src/templates.ts"() {
|
|
3410
|
+
"use strict";
|
|
3411
|
+
}
|
|
3412
|
+
});
|
|
3413
|
+
|
|
3414
|
+
// src/init/render.ts
|
|
3415
|
+
import { emitYAMLEntry } from "@intentius/chant/yaml";
|
|
3416
|
+
function plywoodYml(keys) {
|
|
3417
|
+
const body = Object.entries(keys).filter(([, v]) => v !== void 0).map(([k, v]) => emitYAMLEntry(k, v)).join("\n");
|
|
3418
|
+
const text = fill(template("plywood.yml"), { keys: body });
|
|
3419
|
+
return text.endsWith("\n") ? text : text + "\n";
|
|
3420
|
+
}
|
|
3421
|
+
function chantConfigTs() {
|
|
3422
|
+
return template("chant.config.ts.tmpl");
|
|
3423
|
+
}
|
|
3424
|
+
function componentName(stack) {
|
|
3425
|
+
return slug(stack.key);
|
|
3426
|
+
}
|
|
3427
|
+
function exportName(stack, taken) {
|
|
3428
|
+
let base = stack.key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3429
|
+
if (base === "" || /^\d/.test(base)) base = `stack_${base}`;
|
|
3430
|
+
let name = base;
|
|
3431
|
+
for (let i = 2; taken.has(name); i++) name = `${base}_${i}`;
|
|
3432
|
+
taken.add(name);
|
|
3433
|
+
return name;
|
|
3434
|
+
}
|
|
3435
|
+
function stepInput(s) {
|
|
3436
|
+
return {
|
|
3437
|
+
dir: s.dir,
|
|
3438
|
+
stack: s.stack,
|
|
3439
|
+
member: s.project,
|
|
3440
|
+
...s.engine ? { engine: s.engine } : {},
|
|
3441
|
+
...s.backend ? { backend: s.backend } : {},
|
|
3442
|
+
...s.engineVersion ? { engineVersion: s.engineVersion, ...s.engineRoot ? { engineRoot: s.engineRoot } : {} } : {},
|
|
3443
|
+
...s.lockGrain ? { lockGrain: s.lockGrain } : {},
|
|
3444
|
+
...s.policyPacks?.length ? { policyPacks: s.policyPacks } : {}
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
function componentsFile(stacks2, after = {}, opts = {}) {
|
|
3448
|
+
const taken = /* @__PURE__ */ new Set();
|
|
3449
|
+
const lines2 = [
|
|
3450
|
+
"// Written by plywood init from the stacks it found; running it again rewrites",
|
|
3451
|
+
"// this file. Each stack is a chant component whose pulumi-up step previews it",
|
|
3452
|
+
"// on a pull request and updates it in its wave.",
|
|
3453
|
+
""
|
|
3454
|
+
];
|
|
3455
|
+
for (const s of stacks2) {
|
|
3456
|
+
const dependsOn = (after[s.key] ?? []).filter((u) => stacks2.some((x) => x.key === u)).map((u) => componentName({ key: u }));
|
|
3457
|
+
const step = { kind: "pulumi-up", ...stepInput(s), ...opts.reviewSuffix ? { reviewSuffix: opts.reviewSuffix } : {} };
|
|
3458
|
+
const component = { name: componentName(s), dependsOn, deploy: [{ phase: "up", steps: [step] }] };
|
|
3459
|
+
lines2.push(`export const ${exportName(s, taken)} = ${JSON.stringify(component)};`);
|
|
3460
|
+
}
|
|
3461
|
+
return lines2.join("\n") + "\n";
|
|
3462
|
+
}
|
|
3463
|
+
function wavesOf(stacks2, configured) {
|
|
3464
|
+
const byKey = new Map(stacks2.map((s) => [s.key, s]));
|
|
3465
|
+
if (configured && configured.length > 0) {
|
|
3466
|
+
return configured.map((keys, i) => ({ name: `wave-${i + 1}`, stacks: keys.map((k) => byKey.get(k)).filter((s) => s !== void 0) }));
|
|
3467
|
+
}
|
|
3468
|
+
const out = [];
|
|
3469
|
+
for (const s of stacks2) {
|
|
3470
|
+
const env = environmentOf(s.stack);
|
|
3471
|
+
let name = slug(env);
|
|
3472
|
+
if (!WAVE_NAME.test(name)) name = `wave-${out.length + 1}`;
|
|
3473
|
+
const wave = out.find((w) => w.name === name);
|
|
3474
|
+
if (wave) wave.stacks.push(s);
|
|
3475
|
+
else out.push({ name, stacks: [s] });
|
|
3476
|
+
}
|
|
3477
|
+
return out;
|
|
3478
|
+
}
|
|
3479
|
+
function waveRunner(runner) {
|
|
3480
|
+
if (runner === void 0) return void 0;
|
|
3481
|
+
if (typeof runner === "string" || Array.isArray(runner) || "group" in runner) return runner;
|
|
3482
|
+
const staged = runner;
|
|
3483
|
+
return staged.up ?? staged.default;
|
|
3484
|
+
}
|
|
3485
|
+
function wavesSpec(input) {
|
|
3486
|
+
const waves = (input.ordered ?? wavesOf(input.stacks, input.waves)).filter((w) => w.stacks.length > 0);
|
|
3487
|
+
return {
|
|
3488
|
+
name: OP_NAME,
|
|
3489
|
+
op: OP_NAME,
|
|
3490
|
+
gate: PR_GATE,
|
|
3491
|
+
branches: [input.branch],
|
|
3492
|
+
...input.runner !== void 0 ? { runner: input.runner } : {},
|
|
3493
|
+
step: { kind: "pulumi-up" },
|
|
3494
|
+
...input.quorum && input.quorum.length > 0 ? { approvals: { quorum: input.quorum } } : {},
|
|
3495
|
+
...input.notify ? { notify: input.notify } : {},
|
|
3496
|
+
waves: waves.map((w) => {
|
|
3497
|
+
const gate = w.stacks.map((s) => s.gate ?? input.gate).reduce((a, b) => STRICTNESS[b] > STRICTNESS[a] ? b : a);
|
|
3498
|
+
return { name: w.name, gate, runs: w.stacks.map((s) => ({ target: s.key, input: stepInput(s) })) };
|
|
3499
|
+
})
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
function jsonText(v) {
|
|
3503
|
+
return JSON.stringify(v, null, 2) + "\n";
|
|
3504
|
+
}
|
|
3505
|
+
function memberName(project, taken) {
|
|
3506
|
+
let base = slug(project.name).slice(0, 36).replace(/-+$/, "");
|
|
3507
|
+
if (!MEMBER_NAME.test(base)) base = "stack";
|
|
3508
|
+
let name = base;
|
|
3509
|
+
for (let i = 2; taken.has(name); i++) name = `${base}-${i}`;
|
|
3510
|
+
taken.add(name);
|
|
3511
|
+
return name;
|
|
3512
|
+
}
|
|
3513
|
+
function workspaceDeclaration(existing, input) {
|
|
3514
|
+
const doc = existing ? structuredClone(existing) : { name: input.name, schema: 1 };
|
|
3515
|
+
const members = Array.isArray(doc.members) ? doc.members : [];
|
|
3516
|
+
const taken = new Set(members.map((m) => String(m.name)));
|
|
3517
|
+
for (const p of input.projects) {
|
|
3518
|
+
if (members.some((m) => m.dir === p.dir || p.dir === "." && m.dir === void 0)) continue;
|
|
3519
|
+
members.push({ name: memberName(p, taken), dir: p.dir, kind: "pulumi" });
|
|
3520
|
+
}
|
|
3521
|
+
doc.members = members;
|
|
3522
|
+
const pins = Array.isArray(doc.pins) ? doc.pins : [];
|
|
3523
|
+
if (!pins.some((p) => p.package === PULUMI_LEXICON)) pins.push({ package: PULUMI_LEXICON, version: input.lexiconVersion });
|
|
3524
|
+
doc.pins = pins;
|
|
3525
|
+
const ci2 = doc.ci && typeof doc.ci === "object" && !Array.isArray(doc.ci) ? doc.ci : {};
|
|
3526
|
+
for (const [k, v] of Object.entries(input.ci)) if (v !== void 0) ci2[k] = v;
|
|
3527
|
+
if (Object.keys(ci2).length > 0) doc.ci = ci2;
|
|
3528
|
+
return doc;
|
|
3529
|
+
}
|
|
3530
|
+
function memberOfDir(doc, dir) {
|
|
3531
|
+
const members = doc && Array.isArray(doc.members) ? doc.members : [];
|
|
3532
|
+
const m = members.find((x) => x.dir === dir || dir === "." && x.dir === void 0);
|
|
3533
|
+
return typeof m?.name === "string" ? m.name : void 0;
|
|
3534
|
+
}
|
|
3535
|
+
var WAVE_NAME, STRICTNESS, MEMBER_NAME, PULUMI_LEXICON;
|
|
3536
|
+
var init_render = __esm({
|
|
3537
|
+
"src/init/render.ts"() {
|
|
3117
3538
|
"use strict";
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3539
|
+
init_templates();
|
|
3540
|
+
init_words();
|
|
3541
|
+
init_detect();
|
|
3542
|
+
WAVE_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
3543
|
+
STRICTNESS = { never: 0, "on-destructive": 1, always: 2 };
|
|
3544
|
+
MEMBER_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
3545
|
+
PULUMI_LEXICON = "@intentius/chant-lexicon-pulumi";
|
|
3121
3546
|
}
|
|
3122
3547
|
});
|
|
3123
3548
|
|
|
@@ -3150,6 +3575,16 @@ function explainRefusal(code, table, err, ctx = {}) {
|
|
|
3150
3575
|
if (refusal) err(`${PRODUCT}: ${refusalLine(refusal, ctx)}`);
|
|
3151
3576
|
return code;
|
|
3152
3577
|
}
|
|
3578
|
+
function approveLineIn(output) {
|
|
3579
|
+
const m = /Approve these plans to apply them:[ \t]*([^\r\n]+)/.exec(output);
|
|
3580
|
+
const line = m?.[1]?.trim();
|
|
3581
|
+
return line ? line : void 0;
|
|
3582
|
+
}
|
|
3583
|
+
function waveMovedLine(output) {
|
|
3584
|
+
const approve2 = approveLineIn(output);
|
|
3585
|
+
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.`;
|
|
3586
|
+
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.`;
|
|
3587
|
+
}
|
|
3153
3588
|
var REFUSALS;
|
|
3154
3589
|
var init_refusals = __esm({
|
|
3155
3590
|
"src/refusals.ts"() {
|
|
@@ -3160,8 +3595,8 @@ var init_refusals = __esm({
|
|
|
3160
3595
|
});
|
|
3161
3596
|
|
|
3162
3597
|
// src/up.ts
|
|
3163
|
-
import { existsSync as
|
|
3164
|
-
import { join as
|
|
3598
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
3599
|
+
import { join as join6 } from "node:path";
|
|
3165
3600
|
function approveTarget(gate, opts) {
|
|
3166
3601
|
if (opts.op) return { op: opts.op, gate };
|
|
3167
3602
|
if (PR_OP.test(gate)) return { op: gate, gate: PR_GATE };
|
|
@@ -3242,10 +3677,10 @@ async function approve(args, json, table, io) {
|
|
|
3242
3677
|
});
|
|
3243
3678
|
}
|
|
3244
3679
|
function wavesSpecAt(cwd, file = WAVES_SPEC_FILE) {
|
|
3245
|
-
const path =
|
|
3246
|
-
if (!
|
|
3680
|
+
const path = join6(cwd, file);
|
|
3681
|
+
if (!existsSync6(path)) return void 0;
|
|
3247
3682
|
try {
|
|
3248
|
-
const spec = unwrapWavesSpec(JSON.parse(
|
|
3683
|
+
const spec = unwrapWavesSpec(JSON.parse(readFileSync7(path, "utf8")));
|
|
3249
3684
|
return spec && typeof spec === "object" && !Array.isArray(spec) ? spec : void 0;
|
|
3250
3685
|
} catch {
|
|
3251
3686
|
return void 0;
|
|
@@ -3329,11 +3764,11 @@ async function override(args, json, table, io) {
|
|
|
3329
3764
|
return withChant(`${PRODUCT} override`, table, io, async (chant) => explainRefusal(await chant.runChantInProcess(argv, PRODUCT_WORDS), table, io.err));
|
|
3330
3765
|
}
|
|
3331
3766
|
function readUpSpec(cwd, file = WAVES_SPEC_FILE) {
|
|
3332
|
-
const path =
|
|
3333
|
-
if (!
|
|
3767
|
+
const path = join6(cwd, file);
|
|
3768
|
+
if (!existsSync6(path)) return { error: `no ${file}; '${PRODUCT} init' writes it from plywood.yml's waves` };
|
|
3334
3769
|
let spec;
|
|
3335
3770
|
try {
|
|
3336
|
-
spec = unwrapWavesSpec(JSON.parse(
|
|
3771
|
+
spec = unwrapWavesSpec(JSON.parse(readFileSync7(path, "utf8")));
|
|
3337
3772
|
} catch (e) {
|
|
3338
3773
|
return { error: `${file}: ${e.message}` };
|
|
3339
3774
|
}
|
|
@@ -3488,9 +3923,9 @@ var init_up = __esm({
|
|
|
3488
3923
|
// src/drift.ts
|
|
3489
3924
|
import { createHash as createHash2 } from "node:crypto";
|
|
3490
3925
|
import { resolveTargetCloudIdentity } from "@intentius/chant/ci-identity";
|
|
3491
|
-
import { existsSync as
|
|
3926
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, mkdtempSync, readdirSync as readdirSync4, readFileSync as readFileSync8, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3492
3927
|
import { tmpdir } from "node:os";
|
|
3493
|
-
import { dirname as
|
|
3928
|
+
import { dirname as dirname6, join as join7 } from "node:path";
|
|
3494
3929
|
function driftRows(doc) {
|
|
3495
3930
|
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
3931
|
}
|
|
@@ -3549,11 +3984,12 @@ async function drift2(args, json, table, io) {
|
|
|
3549
3984
|
const respond = respondFlag.value ?? driftResponseOf(doc);
|
|
3550
3985
|
const forge = forgeFlag.value;
|
|
3551
3986
|
return withChant(`${PRODUCT} drift`, table, io, async (chant) => {
|
|
3552
|
-
const dir = mkdtempSync(
|
|
3987
|
+
const dir = mkdtempSync(join7(tmpdir(), "plywood-drift-"));
|
|
3553
3988
|
const results = [];
|
|
3554
3989
|
try {
|
|
3555
3990
|
const attribute = await attributionFor(doc, io);
|
|
3556
|
-
|
|
3991
|
+
const root = dirname6(path);
|
|
3992
|
+
for (const stack of stacks2) results.push(await readStack(chant, stack, driftMember(root, doc, stack), dir, { resource: resource.value, env: envFlag.value, ...attribute ? { attribute } : {} }));
|
|
3557
3993
|
} finally {
|
|
3558
3994
|
rmSync(dir, { recursive: true, force: true });
|
|
3559
3995
|
}
|
|
@@ -3628,13 +4064,31 @@ async function driftNotice(chant, doc, results, environment, io) {
|
|
|
3628
4064
|
...notifyFetch ? { fetch: notifyFetch } : {}
|
|
3629
4065
|
});
|
|
3630
4066
|
}
|
|
3631
|
-
|
|
3632
|
-
const
|
|
3633
|
-
|
|
4067
|
+
function driftMember(root, doc, stack) {
|
|
4068
|
+
const project = stack.includes("/") ? stack.slice(0, stack.indexOf("/")) : stack;
|
|
4069
|
+
let declaration;
|
|
4070
|
+
try {
|
|
4071
|
+
declaration = JSON.parse(readFileSync8(join7(root, "chant.workspace.json"), "utf8"));
|
|
4072
|
+
} catch {
|
|
4073
|
+
return project;
|
|
4074
|
+
}
|
|
4075
|
+
const dir = stacksOf(doc)[stack]?.dir;
|
|
4076
|
+
const byDir = dir !== void 0 ? memberOfDir(declaration, dir) : void 0;
|
|
4077
|
+
if (byDir) return byDir;
|
|
4078
|
+
const names = (Array.isArray(declaration.members) ? declaration.members : []).flatMap((m) => typeof m.name === "string" ? [m.name] : []);
|
|
4079
|
+
if (names.includes(project)) return project;
|
|
4080
|
+
const slugged = names.find((n) => n === slug(project));
|
|
4081
|
+
if (slugged) return slugged;
|
|
4082
|
+
return names.length === 1 ? names[0] : project;
|
|
4083
|
+
}
|
|
4084
|
+
async function readStack(chant, stack, member, dir, opts) {
|
|
4085
|
+
const file = join7(dir, `${driftOpName(stack)}.json`);
|
|
4086
|
+
const pulumiStack = stack.slice(stack.indexOf("/") + 1);
|
|
4087
|
+
const argv = ["workspace", "drift", "--member", member, "--stack", pulumiStack, ...opts.resource ? ["--resource", opts.resource] : [], ...opts.env ? ["--env", opts.env] : [], "-o", file];
|
|
3634
4088
|
const code = await chant.runChantInProcess(argv, PRODUCT_WORDS);
|
|
3635
4089
|
let doc;
|
|
3636
4090
|
try {
|
|
3637
|
-
if (
|
|
4091
|
+
if (existsSync7(file)) doc = JSON.parse(readFileSync8(file, "utf8"));
|
|
3638
4092
|
} catch (e) {
|
|
3639
4093
|
return { stack, ok: false, drifted: 0, rows: [], error: `chant wrote a drift document plywood cannot read: ${e.message}` };
|
|
3640
4094
|
}
|
|
@@ -3667,7 +4121,7 @@ async function attributeStack(a, stack, doc) {
|
|
|
3667
4121
|
}
|
|
3668
4122
|
function specHasUpdate(cwd, op) {
|
|
3669
4123
|
try {
|
|
3670
|
-
const spec = JSON.parse(
|
|
4124
|
+
const spec = JSON.parse(readFileSync8(join7(cwd, DRIFT_SPEC_FILE), "utf8"));
|
|
3671
4125
|
return (spec.ops ?? []).some((o) => o.name === op && o.update !== void 0);
|
|
3672
4126
|
} catch {
|
|
3673
4127
|
return false;
|
|
@@ -3743,9 +4197,9 @@ function withNotifyVariables(fields, notify2) {
|
|
|
3743
4197
|
return { ...fields, variables: { ...notify2, ...fields.variables ?? {} } };
|
|
3744
4198
|
}
|
|
3745
4199
|
function writeIfChanged2(cwd, file, text) {
|
|
3746
|
-
const path =
|
|
3747
|
-
if (
|
|
3748
|
-
mkdirSync4(
|
|
4200
|
+
const path = join7(cwd, file);
|
|
4201
|
+
if (existsSync7(path) && readFileSync8(path, "utf8") === text) return false;
|
|
4202
|
+
mkdirSync4(dirname6(path), { recursive: true });
|
|
3749
4203
|
writeFileSync4(path, text);
|
|
3750
4204
|
return true;
|
|
3751
4205
|
}
|
|
@@ -3761,9 +4215,9 @@ async function writeDriftJobs(chant, forge, doc, words, table, io, env = "produc
|
|
|
3761
4215
|
const outDir = OP_OUTPUT_DIR2[forge];
|
|
3762
4216
|
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", DRIFT_SPEC_FILE, "--output", outDir], words);
|
|
3763
4217
|
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 ${
|
|
4218
|
+
const dir = join7(io.cwd, outDir);
|
|
4219
|
+
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}`)) : [];
|
|
4220
|
+
for (const f of files) io.err(`${PRODUCT} ci: drift job at ${join7(outDir, f).replace(/^\.\//, "")} on ${settings.schedule}`);
|
|
3767
4221
|
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
4222
|
return table.done;
|
|
3769
4223
|
}
|
|
@@ -3781,6 +4235,8 @@ var init_drift2 = __esm({
|
|
|
3781
4235
|
init_drift();
|
|
3782
4236
|
init_config();
|
|
3783
4237
|
init_stacks();
|
|
4238
|
+
init_detect();
|
|
4239
|
+
init_render();
|
|
3784
4240
|
init_routing();
|
|
3785
4241
|
init_images();
|
|
3786
4242
|
init_stages();
|
|
@@ -3837,8 +4293,8 @@ var init_tips = __esm({
|
|
|
3837
4293
|
});
|
|
3838
4294
|
|
|
3839
4295
|
// src/agents.ts
|
|
3840
|
-
import { existsSync as
|
|
3841
|
-
import { dirname as
|
|
4296
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4297
|
+
import { dirname as dirname7, join as join8 } from "node:path";
|
|
3842
4298
|
function plywoodMcpTools(ctx) {
|
|
3843
4299
|
const none = { type: "object", properties: {} };
|
|
3844
4300
|
const doc = () => {
|
|
@@ -3948,304 +4404,83 @@ function reviewOpFile(doc) {
|
|
|
3948
4404
|
"",
|
|
3949
4405
|
"export default Op({",
|
|
3950
4406
|
` 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";
|
|
4407
|
+
` overview: "A model reviews the pull request against its stacks' previews",`,
|
|
4408
|
+
" phases: [",
|
|
4409
|
+
` phase("Preview", [shell(${JSON.stringify(REVIEW_PREVIEW_LINE)})]),`,
|
|
4410
|
+
` phase("Review", [reviewAgent({ ${fields} })]),`,
|
|
4411
|
+
" ],",
|
|
4412
|
+
"});",
|
|
4413
|
+
""
|
|
4414
|
+
].join("\n");
|
|
4197
4415
|
}
|
|
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 {
|
|
4416
|
+
function reviewOpSpec(doc) {
|
|
4417
|
+
const settings = toChantSettings({ doc });
|
|
4418
|
+
const p = settings.pipeline;
|
|
4419
|
+
const engine2 = chantPipelineOptions({ ...p.image ? { image: p.image } : {}, ...p.engineSetup ? { engineSetup: p.engineSetup } : {} }, false);
|
|
4420
|
+
return {
|
|
4421
|
+
ops: [
|
|
4422
|
+
{
|
|
4423
|
+
name: REVIEW_OP,
|
|
4424
|
+
trigger: { kind: "pull_request" },
|
|
4425
|
+
reviewAgent: true,
|
|
4426
|
+
// The preview in the review job takes the preview identity; id-token is the one write the review job may hold.
|
|
4427
|
+
...settings.ci.identity ? { permissions: { "id-token": "write" } } : {}
|
|
4212
4428
|
}
|
|
4429
|
+
],
|
|
4430
|
+
options: {
|
|
4431
|
+
...engine2.beforeScript ? { beforeScript: engine2.beforeScript } : {},
|
|
4432
|
+
...engine2.image ? { image: engine2.image } : {}
|
|
4213
4433
|
}
|
|
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
4434
|
};
|
|
4231
4435
|
}
|
|
4232
|
-
function
|
|
4233
|
-
|
|
4436
|
+
function writeIfChanged3(cwd, file, text) {
|
|
4437
|
+
const path = join8(cwd, file);
|
|
4438
|
+
if (existsSync8(path) && readFileSync9(path, "utf8") === text) return false;
|
|
4439
|
+
mkdirSync5(dirname7(path), { recursive: true });
|
|
4440
|
+
writeFileSync5(path, text);
|
|
4441
|
+
return true;
|
|
4234
4442
|
}
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4443
|
+
async function writeReviewJobs(chant, forge, doc, words, table, io) {
|
|
4444
|
+
if (!reviewOf(doc)) return table.done;
|
|
4445
|
+
if (writeIfChanged3(io.cwd, REVIEW_OP_FILE, reviewOpFile(doc))) io.err(`${PRODUCT} ci: wrote ${REVIEW_OP_FILE}; commit it with the workflow`);
|
|
4446
|
+
if (writeIfChanged3(io.cwd, REVIEW_SPEC_FILE, JSON.stringify(reviewOpSpec(doc), null, 2) + "\n")) io.err(`${PRODUCT} ci: wrote ${REVIEW_SPEC_FILE}`);
|
|
4447
|
+
const outDir = OP_OUTPUT_DIR3[forge];
|
|
4448
|
+
const code = await chant.runChantInProcess(["run", "--generate", forge, "--spec", REVIEW_SPEC_FILE, "--output", outDir], words);
|
|
4449
|
+
if (code !== table.done) return code;
|
|
4450
|
+
const dir = join8(io.cwd, outDir);
|
|
4451
|
+
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}`)) : [];
|
|
4452
|
+
for (const f of files) io.err(`${PRODUCT} ci: review jobs at ${join8(outDir, f).replace(/^\.\//, "")}`);
|
|
4453
|
+
const r = reviewOf(doc);
|
|
4454
|
+
io.err(`${PRODUCT} ci: the review job reads the model's key from ${r.keyEnv}; set it from a secret`);
|
|
4455
|
+
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`);
|
|
4456
|
+
return table.done;
|
|
4457
|
+
}
|
|
4458
|
+
var PERSON_VERBS, READ_INSTRUCTIONS, REVIEW_OP, REVIEW_OP_FILE, REVIEW_SPEC_FILE, REVIEW_CHANGE_SET, REVIEW_PREVIEW_LINE, OP_OUTPUT_DIR3;
|
|
4459
|
+
var init_agents2 = __esm({
|
|
4460
|
+
"src/agents.ts"() {
|
|
4238
4461
|
"use strict";
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4462
|
+
init_chant_config();
|
|
4463
|
+
init_config_check();
|
|
4464
|
+
init_agents();
|
|
4465
|
+
init_config();
|
|
4466
|
+
init_stacks();
|
|
4467
|
+
init_images();
|
|
4468
|
+
init_stages();
|
|
4469
|
+
init_tips();
|
|
4470
|
+
init_words();
|
|
4471
|
+
PERSON_VERBS = ["up", "approve", "override"];
|
|
4472
|
+
READ_INSTRUCTIONS = `To change a stack, open a pull request: ${PRODUCT} previews it, and its update runs after a person approves the digest.`;
|
|
4473
|
+
REVIEW_OP = "plywood-review";
|
|
4474
|
+
REVIEW_OP_FILE = "ops/plywood-review.op.ts";
|
|
4475
|
+
REVIEW_SPEC_FILE = ".chant/ops/plywood-review.spec.json";
|
|
4476
|
+
REVIEW_CHANGE_SET = ".chant/pr/change-set.json";
|
|
4477
|
+
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`;
|
|
4478
|
+
OP_OUTPUT_DIR3 = { github: ".github/workflows", forgejo: ".forgejo/workflows", gitlab: "." };
|
|
4244
4479
|
}
|
|
4245
4480
|
});
|
|
4246
4481
|
|
|
4247
4482
|
// src/policy-presets.ts
|
|
4248
|
-
import { readFileSync as
|
|
4483
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4249
4484
|
function providersRego(allowed, namespace = "main") {
|
|
4250
4485
|
const set = allowed.length ? `{${[...new Set(allowed)].sort().map((p) => JSON.stringify(p)).join(", ")}}` : "set()";
|
|
4251
4486
|
return preset("plywood_providers.rego.tmpl").replace("{{namespace}}", namespace).replace("{{allowed}}", set);
|
|
@@ -4264,7 +4499,7 @@ var init_policy_presets = __esm({
|
|
|
4264
4499
|
PROVIDERS_PACK_CONFIG = "policy/plywood-providers.json";
|
|
4265
4500
|
PROVIDERS_REGO_FILE = "plywood_providers.rego";
|
|
4266
4501
|
PROVIDERS_POLICY = "provider-allowlist";
|
|
4267
|
-
preset = (rel) =>
|
|
4502
|
+
preset = (rel) => readFileSync10(new URL(`../presets/policy/${rel}`, import.meta.url), "utf8");
|
|
4268
4503
|
}
|
|
4269
4504
|
});
|
|
4270
4505
|
|
|
@@ -4617,155 +4852,6 @@ var init_presets = __esm({
|
|
|
4617
4852
|
}
|
|
4618
4853
|
});
|
|
4619
4854
|
|
|
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
4855
|
// src/stack-order.ts
|
|
4770
4856
|
import { join as join9 } from "node:path";
|
|
4771
4857
|
function chantReferenceReader() {
|
|
@@ -5400,6 +5486,11 @@ __export(init_exports, {
|
|
|
5400
5486
|
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5401
5487
|
import { emitYAMLEntry as emitYAMLEntry2 } from "@intentius/chant/yaml";
|
|
5402
5488
|
import { dirname as dirname9, join as join13, posix as posix3 } from "node:path";
|
|
5489
|
+
function chantPin() {
|
|
5490
|
+
const pin = dependencies["@intentius/chant"];
|
|
5491
|
+
if (!pin) throw new Error("packages/plywood/package.json has no @intentius/chant dependency");
|
|
5492
|
+
return pin;
|
|
5493
|
+
}
|
|
5403
5494
|
function parseInitArgs(args) {
|
|
5404
5495
|
if (args.includes("--print-prompt")) return { printPrompt: true };
|
|
5405
5496
|
let rest = args;
|
|
@@ -5620,7 +5711,7 @@ async function runInit(opts, table, io) {
|
|
|
5620
5711
|
const message = needsChantMessage("Writing the waves spec and the workflow");
|
|
5621
5712
|
files.push({ file: WAVES_SPEC_FILE2, status: "skipped", note: message }, { file: WORKFLOW_FILE[forge.kind], status: "skipped", note: "needs the same chant" });
|
|
5622
5713
|
} else {
|
|
5623
|
-
let spec = wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves);
|
|
5714
|
+
let spec = await wavesSpecStep(chant, stacks2, doc, settings, detected2.defaultBranch, waves, forge.kind);
|
|
5624
5715
|
if (settings.pipeline.jobs) {
|
|
5625
5716
|
try {
|
|
5626
5717
|
spec = withJobSettings(spec, settings.pipeline.jobs, forge.kind, { parallelism: await hasChant4625() });
|
|
@@ -5638,6 +5729,9 @@ async function runInit(opts, table, io) {
|
|
|
5638
5729
|
if (opts.bucketCheck) notes.push(...await bucketStep(detected2, doc, io.env, buckets));
|
|
5639
5730
|
adopt.push(...await adoptStep(root, detected2, doc, io.env, declaration.doc, opts.dryRun, files, notes, todo));
|
|
5640
5731
|
todo.push(...todoLines(detected2, doc, forge.kind, io.env));
|
|
5732
|
+
if (forge.kind === "gitlab" && chant && stacks2.length > 0 && await hasChantWavePreview()) {
|
|
5733
|
+
todo.push(`Add a ${GITLAB_READ_TOKEN} CI/CD variable (a project access token with the read_api scope, masked and protected) so a wave can read the merge request it applies; without it the wave refuses`);
|
|
5734
|
+
}
|
|
5641
5735
|
const failed = files.find((f) => f.status === "failed");
|
|
5642
5736
|
return {
|
|
5643
5737
|
ok: !failed,
|
|
@@ -5702,7 +5796,7 @@ async function preCommitStep(root, config2, dryRun) {
|
|
|
5702
5796
|
const after = readFileSync12(join13(root, DECLARATION_FILE), "utf8");
|
|
5703
5797
|
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
5798
|
}
|
|
5705
|
-
function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered) {
|
|
5799
|
+
async function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered, forge) {
|
|
5706
5800
|
const runner = waveRunner(chantPipelineOptions(settings.pipeline, false).runner);
|
|
5707
5801
|
const base = wavesSpec({
|
|
5708
5802
|
stacks: stacks2,
|
|
@@ -5714,7 +5808,8 @@ function wavesSpecStep(chant, stacks2, doc, settings, branch, ordered) {
|
|
|
5714
5808
|
// notify.relay only on a chant with the relay (chant#4611); an older chant refuses the setting.
|
|
5715
5809
|
...settings.pipeline.notify ? { notify: { ...notifyForChant(settings.pipeline.notify, chant) } } : {}
|
|
5716
5810
|
});
|
|
5717
|
-
|
|
5811
|
+
const named = chant.productWavesSpec ? chant.productWavesSpec(base, PRODUCT_WORDS) : base;
|
|
5812
|
+
return await hasChantWavePreview() ? withWavePreview(named, forge) : named;
|
|
5718
5813
|
}
|
|
5719
5814
|
async function workflowStep(chant, root, doc, settings, forge, opts, spec, table, io) {
|
|
5720
5815
|
const output = WORKFLOW_FILE[forge];
|
|
@@ -5872,6 +5967,7 @@ var init_init = __esm({
|
|
|
5872
5967
|
init_stacks();
|
|
5873
5968
|
init_types();
|
|
5874
5969
|
init_images();
|
|
5970
|
+
init_chant_next();
|
|
5875
5971
|
init_jobs2();
|
|
5876
5972
|
init_backend();
|
|
5877
5973
|
init_adopt();
|
|
@@ -5891,7 +5987,7 @@ var init_init = __esm({
|
|
|
5891
5987
|
CHANT_CONFIG_FILE = "chant.config.ts";
|
|
5892
5988
|
COMPONENTS_FILE = "plywood.component.ts";
|
|
5893
5989
|
WAVES_SPEC_FILE2 = `.chant/op-waves/${OP_NAME}.spec.json`;
|
|
5894
|
-
CHANT_PIN =
|
|
5990
|
+
CHANT_PIN = chantPin();
|
|
5895
5991
|
}
|
|
5896
5992
|
});
|
|
5897
5993
|
|
|
@@ -6244,8 +6340,25 @@ async function withChant(what, table, io, run) {
|
|
|
6244
6340
|
return run(chant);
|
|
6245
6341
|
}
|
|
6246
6342
|
function passthrough(argv, table, io) {
|
|
6343
|
+
if (argv[0] === "run" && argv[1] === "wave") return runWave(argv, table, io);
|
|
6247
6344
|
return withChant(`${PRODUCT} ${argv[0]}`, table, io, (chant) => chant.runChantInProcess(argv, PRODUCT_WORDS));
|
|
6248
6345
|
}
|
|
6346
|
+
async function runWave(argv, table, io) {
|
|
6347
|
+
const seen = [];
|
|
6348
|
+
const write = process.stderr.write;
|
|
6349
|
+
process.stderr.write = ((chunk, ...rest) => {
|
|
6350
|
+
seen.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
6351
|
+
return write.call(process.stderr, chunk, ...rest);
|
|
6352
|
+
});
|
|
6353
|
+
let code;
|
|
6354
|
+
try {
|
|
6355
|
+
code = await withChant(`${PRODUCT} run wave`, table, io, (chant) => chant.runChantInProcess(argv, PRODUCT_WORDS));
|
|
6356
|
+
} finally {
|
|
6357
|
+
process.stderr.write = write;
|
|
6358
|
+
}
|
|
6359
|
+
if (code === table.moved) io.err(`${PRODUCT}: ${waveMovedLine(seen.join(""))}`);
|
|
6360
|
+
return code;
|
|
6361
|
+
}
|
|
6249
6362
|
async function check(args, json, table, io) {
|
|
6250
6363
|
const file = takeFlag(args, "--file");
|
|
6251
6364
|
if (file.missing) return usage("--file takes a path", table, io);
|
|
@@ -6544,6 +6657,7 @@ var init_stages = __esm({
|
|
|
6544
6657
|
init_review_stacks2();
|
|
6545
6658
|
init_waves_workflow();
|
|
6546
6659
|
init_drift2();
|
|
6660
|
+
init_refusals();
|
|
6547
6661
|
init_agents2();
|
|
6548
6662
|
init_agent_jobs();
|
|
6549
6663
|
init_words();
|
|
@@ -7804,7 +7918,7 @@ var execChild = (bin, args, opts) => new Promise((done) => {
|
|
|
7804
7918
|
|
|
7805
7919
|
// src/cli.ts
|
|
7806
7920
|
init_words();
|
|
7807
|
-
var VERSION3 = true ? "0.
|
|
7921
|
+
var VERSION3 = true ? "0.3.0" : "0.0.0-dev";
|
|
7808
7922
|
var stdio = {
|
|
7809
7923
|
out: (s) => process.stdout.write(s + "\n"),
|
|
7810
7924
|
err: (s) => process.stderr.write(s + "\n")
|