@intentius/behold 0.29.14 → 0.29.15
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/cli.js +66 -34
- package/package.json +1 -1
- package/web/app.js +1 -1
- package/web/kiosk-popover.js +2 -2
- package/web/kiosk-popover.test.js +1 -1
- package/web/kiosk-repo.js +18 -0
- package/web/kiosk-repo.test.js +28 -1
- package/web/kiosk-vendor.js +6 -0
- package/web/kiosk-vendor.test.js +2 -1
- package/web/kiosk.js +56 -30
- package/web/kiosk.test.js +19 -9
- package/web/terragucci-change.js +2 -2
- package/web/terragucci-components-live.js +22 -2
- package/web/terragucci-components-live.test.js +29 -3
package/dist/cli.js
CHANGED
|
@@ -2465,6 +2465,9 @@ function discoverTerragruntUnits(repo) {
|
|
|
2465
2465
|
};
|
|
2466
2466
|
}).sort((a, b) => a.dir < b.dir ? -1 : 1);
|
|
2467
2467
|
}
|
|
2468
|
+
function hasTerragruntUnits(dir) {
|
|
2469
|
+
return discoverTerragruntUnits(dir).length > 0;
|
|
2470
|
+
}
|
|
2468
2471
|
function terragruntPicture(repo, units, decl) {
|
|
2469
2472
|
const onDisk = new Set(units.map((u) => u.dir));
|
|
2470
2473
|
const declaredOnly = [];
|
|
@@ -11199,7 +11202,7 @@ var init_declaration = __esm({
|
|
|
11199
11202
|
|
|
11200
11203
|
// src/cli.ts
|
|
11201
11204
|
import { resolve as resolve35, dirname as dirname21, join as join53 } from "node:path";
|
|
11202
|
-
import { realpathSync as realpathSync3, existsSync as existsSync32, readFileSync as
|
|
11205
|
+
import { realpathSync as realpathSync3, existsSync as existsSync32, readFileSync as readFileSync41, rmSync as rmSync4 } from "node:fs";
|
|
11203
11206
|
import { fileURLToPath as fileURLToPath12 } from "node:url";
|
|
11204
11207
|
import { spawn as spawn9, spawnSync as spawnSync4 } from "node:child_process";
|
|
11205
11208
|
|
|
@@ -11257,7 +11260,7 @@ import { serve } from "@hono/node-server";
|
|
|
11257
11260
|
import { fileURLToPath as fileURLToPath10 } from "node:url";
|
|
11258
11261
|
import { execFile as execFile5 } from "node:child_process";
|
|
11259
11262
|
import { promisify as promisify3 } from "node:util";
|
|
11260
|
-
import { existsSync as existsSync29, readFileSync as
|
|
11263
|
+
import { existsSync as existsSync29, readFileSync as readFileSync37 } from "node:fs";
|
|
11261
11264
|
import { basename as basename5, dirname as dirname18, join as join49, relative as relative11, resolve as resolve32, sep as sep10 } from "node:path";
|
|
11262
11265
|
|
|
11263
11266
|
// src/recents.ts
|
|
@@ -15148,6 +15151,7 @@ function resourcesByRoot(ir, rootOf2) {
|
|
|
15148
15151
|
|
|
15149
15152
|
// src/server.ts
|
|
15150
15153
|
init_terragucci_components();
|
|
15154
|
+
init_terragrunt_units();
|
|
15151
15155
|
|
|
15152
15156
|
// src/reconcile.ts
|
|
15153
15157
|
var DISRUPTION_SEVERITY = {
|
|
@@ -24944,7 +24948,7 @@ function terragucciRoutes(app, opts, dirs) {
|
|
|
24944
24948
|
|
|
24945
24949
|
// src/view-repos.ts
|
|
24946
24950
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
24947
|
-
import { existsSync as existsSync23, readdirSync as readdirSync13 } from "node:fs";
|
|
24951
|
+
import { existsSync as existsSync23, readdirSync as readdirSync13, readFileSync as readFileSync30 } from "node:fs";
|
|
24948
24952
|
import { join as join42, resolve as resolve26 } from "node:path";
|
|
24949
24953
|
var NAME2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
|
|
24950
24954
|
function repoName(value) {
|
|
@@ -24997,10 +25001,37 @@ function viewReposMiddleware(app, repos) {
|
|
|
24997
25001
|
return scope.run(target, () => next());
|
|
24998
25002
|
});
|
|
24999
25003
|
}
|
|
25004
|
+
var SHA = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
25005
|
+
function checkoutCommit(dir) {
|
|
25006
|
+
const read = (f) => {
|
|
25007
|
+
try {
|
|
25008
|
+
return readFileSync30(join42(dir, ".git", f), "utf8");
|
|
25009
|
+
} catch {
|
|
25010
|
+
return void 0;
|
|
25011
|
+
}
|
|
25012
|
+
};
|
|
25013
|
+
const head = read("HEAD")?.trim();
|
|
25014
|
+
if (!head) return void 0;
|
|
25015
|
+
if (SHA.test(head)) return head;
|
|
25016
|
+
const ref2 = /^ref: (refs\/[^\s]+)$/.exec(head)?.[1];
|
|
25017
|
+
if (!ref2 || ref2.split("/").includes("..")) return void 0;
|
|
25018
|
+
const loose = read(ref2)?.trim();
|
|
25019
|
+
if (loose && SHA.test(loose)) return loose;
|
|
25020
|
+
for (const line of (read("packed-refs") ?? "").split("\n")) {
|
|
25021
|
+
const [sha, name] = line.trim().split(" ");
|
|
25022
|
+
if (name === ref2 && sha && SHA.test(sha)) return sha;
|
|
25023
|
+
}
|
|
25024
|
+
return void 0;
|
|
25025
|
+
}
|
|
25000
25026
|
function viewRepoFacts(repos, servedDir) {
|
|
25001
25027
|
if (!repos) return {};
|
|
25002
25028
|
const here = [...repos.repos].find(([, t2]) => t2.projectDir === servedDir)?.[0];
|
|
25003
|
-
|
|
25029
|
+
const commits = {};
|
|
25030
|
+
for (const [name, t2] of repos.repos) {
|
|
25031
|
+
const sha = checkoutCommit(t2.projectDir);
|
|
25032
|
+
if (sha) commits[name] = sha;
|
|
25033
|
+
}
|
|
25034
|
+
return { ...here ? { viewRepo: here } : {}, viewRepos: [...repos.repos.keys()], ...Object.keys(commits).length ? { viewCommits: commits } : {} };
|
|
25004
25035
|
}
|
|
25005
25036
|
|
|
25006
25037
|
// node_modules/@intentius/chant/src/lifecycle/gate-audit.ts
|
|
@@ -25455,7 +25486,7 @@ function lifecycleHistoryRoute(app, base, opts = {}) {
|
|
|
25455
25486
|
}
|
|
25456
25487
|
|
|
25457
25488
|
// src/lifecycle-releases.ts
|
|
25458
|
-
import { existsSync as existsSync24, readFileSync as
|
|
25489
|
+
import { existsSync as existsSync24, readFileSync as readFileSync31 } from "node:fs";
|
|
25459
25490
|
import { join as join43 } from "node:path";
|
|
25460
25491
|
|
|
25461
25492
|
// node_modules/@intentius/chant/src/lifecycle/legacy-digest.ts
|
|
@@ -25572,7 +25603,7 @@ function waveOrder(checkout) {
|
|
|
25572
25603
|
const p = join43(checkout, WAVES_SPEC_FILE);
|
|
25573
25604
|
if (!existsSync24(p)) return void 0;
|
|
25574
25605
|
try {
|
|
25575
|
-
const doc = JSON.parse(
|
|
25606
|
+
const doc = JSON.parse(readFileSync31(p, "utf8"));
|
|
25576
25607
|
const names = (doc.waves ?? []).map((w) => w?.name).filter((n) => typeof n === "string" && n.length > 0);
|
|
25577
25608
|
return names.length ? [...new Set(names)] : void 0;
|
|
25578
25609
|
} catch {
|
|
@@ -25725,7 +25756,7 @@ function releasesRoutes(app, base, opts = {}) {
|
|
|
25725
25756
|
}
|
|
25726
25757
|
|
|
25727
25758
|
// src/sql-yodeler-lane.ts
|
|
25728
|
-
import { existsSync as existsSync25, readdirSync as readdirSync14, readFileSync as
|
|
25759
|
+
import { existsSync as existsSync25, readdirSync as readdirSync14, readFileSync as readFileSync32 } from "node:fs";
|
|
25729
25760
|
import { join as join44 } from "node:path";
|
|
25730
25761
|
var YODEL_APPLY_OP = "yodel-apply";
|
|
25731
25762
|
var YODEL_HEAD_PLANS_DIR = headPlansPath(YODEL_APPLY_OP, "").dir;
|
|
@@ -25761,7 +25792,7 @@ function readYodelWaves(checkout) {
|
|
|
25761
25792
|
const p = join44(checkout, WAVES_SPEC_FILE);
|
|
25762
25793
|
if (!existsSync25(p)) return void 0;
|
|
25763
25794
|
try {
|
|
25764
|
-
return parseYodelWaves(
|
|
25795
|
+
return parseYodelWaves(readFileSync32(p, "utf8"));
|
|
25765
25796
|
} catch (e) {
|
|
25766
25797
|
return { path: WAVES_SPEC_FILE, waves: [], error: e instanceof Error ? e.message : String(e) };
|
|
25767
25798
|
}
|
|
@@ -25890,7 +25921,7 @@ function sqlYodelerLaneRoute(app, checkout, opts = {}) {
|
|
|
25890
25921
|
|
|
25891
25922
|
// src/terragucci-estate.ts
|
|
25892
25923
|
init_yaml();
|
|
25893
|
-
import { existsSync as existsSync26, readFileSync as
|
|
25924
|
+
import { existsSync as existsSync26, readFileSync as readFileSync33, statSync as statSync14 } from "node:fs";
|
|
25894
25925
|
import { createRequire as createRequire7 } from "node:module";
|
|
25895
25926
|
import { dirname as dirname14, extname, join as join45, resolve as resolve28 } from "node:path";
|
|
25896
25927
|
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
@@ -25899,7 +25930,7 @@ var RUN_SCHEMA2 = "terragucci.run/v1";
|
|
|
25899
25930
|
function configProjects(file) {
|
|
25900
25931
|
let text2;
|
|
25901
25932
|
try {
|
|
25902
|
-
text2 =
|
|
25933
|
+
text2 = readFileSync33(file, "utf8");
|
|
25903
25934
|
} catch {
|
|
25904
25935
|
return void 0;
|
|
25905
25936
|
}
|
|
@@ -25968,8 +25999,8 @@ function runViewValidator(dirs = []) {
|
|
|
25968
25999
|
if (hit) return hit;
|
|
25969
26000
|
let v;
|
|
25970
26001
|
try {
|
|
25971
|
-
const version = JSON.parse(
|
|
25972
|
-
const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(
|
|
26002
|
+
const version = JSON.parse(readFileSync33(join45(dist, "..", "package.json"), "utf8")).version ?? "";
|
|
26003
|
+
const validate = new Ajv20204({ strict: false, allErrors: false }).compile(JSON.parse(readFileSync33(schema, "utf8")));
|
|
25973
26004
|
v = {
|
|
25974
26005
|
validation: { by: "schema", terragucci: version },
|
|
25975
26006
|
check(doc) {
|
|
@@ -26232,7 +26263,7 @@ function controlRepoInfo(dirs) {
|
|
|
26232
26263
|
}
|
|
26233
26264
|
|
|
26234
26265
|
// src/yodel-report.ts
|
|
26235
|
-
import { readdirSync as readdirSync15, readFileSync as
|
|
26266
|
+
import { readdirSync as readdirSync15, readFileSync as readFileSync34, statSync as statSync15 } from "node:fs";
|
|
26236
26267
|
import { createRequire as createRequire8 } from "node:module";
|
|
26237
26268
|
import { basename as basename4, dirname as dirname15, join as join46, resolve as resolve29 } from "node:path";
|
|
26238
26269
|
import { fileURLToPath as fileURLToPath9 } from "node:url";
|
|
@@ -26669,7 +26700,7 @@ function packageRoot(from) {
|
|
|
26669
26700
|
}
|
|
26670
26701
|
for (let d = dirname15(entry); d !== dirname15(d); d = dirname15(d)) {
|
|
26671
26702
|
try {
|
|
26672
|
-
const pkg = JSON.parse(
|
|
26703
|
+
const pkg = JSON.parse(readFileSync34(join46(d, "package.json"), "utf8"));
|
|
26673
26704
|
if (pkg.name === "@intentius/sql-yodeler") return d;
|
|
26674
26705
|
} catch {
|
|
26675
26706
|
}
|
|
@@ -26681,11 +26712,11 @@ function yodelValidator(dirs = []) {
|
|
|
26681
26712
|
const root = packageRoot(from);
|
|
26682
26713
|
if (!root) continue;
|
|
26683
26714
|
try {
|
|
26684
|
-
const version = JSON.parse(
|
|
26715
|
+
const version = JSON.parse(readFileSync34(join46(root, "package.json"), "utf8")).version ?? "";
|
|
26685
26716
|
const key = `${root}\0${version}`;
|
|
26686
26717
|
let v = installed.get(key);
|
|
26687
26718
|
if (!v) {
|
|
26688
|
-
const load2 = (n) => JSON.parse(
|
|
26719
|
+
const load2 = (n) => JSON.parse(readFileSync34(join46(root, "schemas", `${n}.schema.json`), "utf8"));
|
|
26689
26720
|
v = compile({ common: load2("common"), report: load2("report"), drift: load2("drift") }, { by: "schema", from: "package", sqlYodeler: version });
|
|
26690
26721
|
installed.set(key, v);
|
|
26691
26722
|
}
|
|
@@ -26719,7 +26750,7 @@ function readYodelReport(source2, validator2) {
|
|
|
26719
26750
|
for (const file of files) {
|
|
26720
26751
|
let doc;
|
|
26721
26752
|
try {
|
|
26722
|
-
doc = JSON.parse(
|
|
26753
|
+
doc = JSON.parse(readFileSync34(file, "utf8"));
|
|
26723
26754
|
} catch (e) {
|
|
26724
26755
|
if (file === named) refuse14(`${basename4(file)} is not JSON: ${e instanceof Error ? e.message : String(e)}`, remedy);
|
|
26725
26756
|
ignored.push(basename4(file));
|
|
@@ -26806,7 +26837,7 @@ function yodelReportRoute(app, source2, dirs) {
|
|
|
26806
26837
|
|
|
26807
26838
|
// src/demo-pins.ts
|
|
26808
26839
|
init_terraform_member();
|
|
26809
|
-
import { existsSync as existsSync27, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as
|
|
26840
|
+
import { existsSync as existsSync27, lstatSync as lstatSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync35, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
26810
26841
|
import { dirname as dirname16, join as join47, resolve as resolve30 } from "node:path";
|
|
26811
26842
|
var isRecord21 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
26812
26843
|
function linkDemoPins(work, probes = {}) {
|
|
@@ -26814,7 +26845,7 @@ function linkDemoPins(work, probes = {}) {
|
|
|
26814
26845
|
if (!existsSync27(file)) return { ok: true, linked: [] };
|
|
26815
26846
|
let decl;
|
|
26816
26847
|
try {
|
|
26817
|
-
decl = JSON.parse(
|
|
26848
|
+
decl = JSON.parse(readFileSync35(file, "utf8"));
|
|
26818
26849
|
} catch {
|
|
26819
26850
|
return { ok: true, linked: [] };
|
|
26820
26851
|
}
|
|
@@ -26872,13 +26903,13 @@ function linksTo(link, target) {
|
|
|
26872
26903
|
}
|
|
26873
26904
|
|
|
26874
26905
|
// src/demos.ts
|
|
26875
|
-
import { readFileSync as
|
|
26906
|
+
import { readFileSync as readFileSync36, existsSync as existsSync28, cpSync, mkdirSync as mkdirSync8 } from "node:fs";
|
|
26876
26907
|
import { dirname as dirname17, isAbsolute as isAbsolute5, join as join48, relative as relative10, resolve as resolve31, sep as sep9 } from "node:path";
|
|
26877
26908
|
import { spawn as spawn8, spawnSync as spawnSync3 } from "node:child_process";
|
|
26878
26909
|
function readCatalog(file, catalog) {
|
|
26879
26910
|
let raw;
|
|
26880
26911
|
try {
|
|
26881
|
-
raw = JSON.parse(
|
|
26912
|
+
raw = JSON.parse(readFileSync36(file, "utf8"));
|
|
26882
26913
|
} catch {
|
|
26883
26914
|
return [];
|
|
26884
26915
|
}
|
|
@@ -27080,7 +27111,7 @@ function tierFailure(tier, message2) {
|
|
|
27080
27111
|
function beholdVersion() {
|
|
27081
27112
|
try {
|
|
27082
27113
|
const here = dirname18(fileURLToPath10(import.meta.url));
|
|
27083
|
-
return JSON.parse(
|
|
27114
|
+
return JSON.parse(readFileSync37(join49(here, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
27084
27115
|
} catch {
|
|
27085
27116
|
return "unknown";
|
|
27086
27117
|
}
|
|
@@ -27127,7 +27158,7 @@ async function captureFrame(projectDir, env, frames, broadcaster) {
|
|
|
27127
27158
|
}
|
|
27128
27159
|
}
|
|
27129
27160
|
function carveRoutes(app, reportPath, demo) {
|
|
27130
|
-
const load2 = () => readCarveReport(reportPath, (p) =>
|
|
27161
|
+
const load2 = () => readCarveReport(reportPath, (p) => readFileSync37(p, "utf8"));
|
|
27131
27162
|
const carveDirs = [dirname18(reportPath), ...demo ? [demo.out] : []];
|
|
27132
27163
|
const carveStates = () => readCarveStates(carveDirs, nodeCarveIo, 0);
|
|
27133
27164
|
const emittedNodes = async (states) => {
|
|
@@ -27559,7 +27590,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
27559
27590
|
const dirs = [];
|
|
27560
27591
|
const declared = [];
|
|
27561
27592
|
for (const f of discoverOpIrs(estateDirs)) {
|
|
27562
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
27593
|
+
const parsed = readOpIr(f.path, (p) => readFileSync37(p, "utf8"));
|
|
27563
27594
|
if (!parsed.ok) continue;
|
|
27564
27595
|
const mine = declaredConvergeOps([parsed.ir]);
|
|
27565
27596
|
if (!mine.length) continue;
|
|
@@ -28135,7 +28166,8 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
28135
28166
|
const titles = waveTitles(ir, decl);
|
|
28136
28167
|
const { svg } = renderGraph(ir, { waveTitles: titles });
|
|
28137
28168
|
const waves = Object.keys(ir.groups.byWave ?? {}).length;
|
|
28138
|
-
const
|
|
28169
|
+
const noun = hasTerragruntUnits(cfg.projectDir) ? "unit" : "root";
|
|
28170
|
+
const note = `${ir.nodes.length} ${noun}${ir.nodes.length === 1 ? "" : "s"} in ${waves} wave${waves === 1 ? "" : "s"}, from ${TERRAGUCCI_COMPONENTS_FILE}`;
|
|
28139
28171
|
return {
|
|
28140
28172
|
ir,
|
|
28141
28173
|
svg,
|
|
@@ -28213,7 +28245,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
28213
28245
|
}
|
|
28214
28246
|
const parsedOps = [];
|
|
28215
28247
|
for (const f of files) {
|
|
28216
|
-
const parsed = readOpIr(f.path, (p) =>
|
|
28248
|
+
const parsed = readOpIr(f.path, (p) => readFileSync37(p, "utf8"));
|
|
28217
28249
|
if (!parsed.ok) return c.json(parsed.refusal, 422);
|
|
28218
28250
|
parsedOps.push(parsed.ir);
|
|
28219
28251
|
}
|
|
@@ -29197,7 +29229,7 @@ init_chant();
|
|
|
29197
29229
|
|
|
29198
29230
|
// src/workspace-convert.ts
|
|
29199
29231
|
init_chant();
|
|
29200
|
-
import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as
|
|
29232
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync9, readFileSync as readFileSync38, renameSync as renameSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
29201
29233
|
import { basename as basename6, dirname as dirname19, join as join50, relative as relative12, resolve as resolve33, sep as sep11 } from "node:path";
|
|
29202
29234
|
import { shortStackNames as shortStackNames2 } from "@intentius/pinhole";
|
|
29203
29235
|
init_project();
|
|
@@ -29219,7 +29251,7 @@ function installedVersion(pkg, dir) {
|
|
|
29219
29251
|
const manifest = join50(at2, "node_modules", ...pkg.split("/"), "package.json");
|
|
29220
29252
|
if (existsSync30(manifest)) {
|
|
29221
29253
|
try {
|
|
29222
|
-
const v = JSON.parse(
|
|
29254
|
+
const v = JSON.parse(readFileSync38(manifest, "utf8")).version;
|
|
29223
29255
|
return typeof v === "string" ? v : void 0;
|
|
29224
29256
|
} catch {
|
|
29225
29257
|
return void 0;
|
|
@@ -29319,7 +29351,7 @@ function applyConversion(plan) {
|
|
|
29319
29351
|
}
|
|
29320
29352
|
|
|
29321
29353
|
// src/export.ts
|
|
29322
|
-
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as
|
|
29354
|
+
import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync8, copyFileSync, cpSync as cpSync2, readFileSync as readFileSync39, readdirSync as readdirSync16, existsSync as existsSync31, realpathSync as realpathSync2 } from "node:fs";
|
|
29323
29355
|
import { homedir as homedir3 } from "node:os";
|
|
29324
29356
|
import { join as join51, dirname as dirname20, basename as basename7, relative as relative13, resolve as resolve34, sep as sep12 } from "node:path";
|
|
29325
29357
|
import { createHash as createHash11 } from "node:crypto";
|
|
@@ -29452,7 +29484,7 @@ async function runExport(cfg, outDir, opts = {}) {
|
|
|
29452
29484
|
keyToFile
|
|
29453
29485
|
};
|
|
29454
29486
|
writeFileSync8(join51(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
29455
|
-
const html =
|
|
29487
|
+
const html = readFileSync39(join51(webDir(), "index.html"), "utf8").replace(
|
|
29456
29488
|
/<\/head>/i,
|
|
29457
29489
|
` <script>window.__BEHOLD_STATIC__ = true;</script>
|
|
29458
29490
|
</head>`
|
|
@@ -29586,7 +29618,7 @@ async function publishBundle(outDir, target, client) {
|
|
|
29586
29618
|
}
|
|
29587
29619
|
const last = ["manifest.json", "index.html"];
|
|
29588
29620
|
const order = [...files.filter((f) => !last.includes(f)), ...last.filter((f) => files.includes(f))];
|
|
29589
|
-
for (const f of order) await client.put(at2(f),
|
|
29621
|
+
for (const f of order) await client.put(at2(f), readFileSync39(join51(outDir, f)), contentType(f), CONTENT_ADDRESSED.test(f) ? IMMUTABLE : NO_CACHE);
|
|
29590
29622
|
const wrote = new Set(files);
|
|
29591
29623
|
const removed = [];
|
|
29592
29624
|
for (const f of previous) {
|
|
@@ -29638,7 +29670,7 @@ It's just files \u2014 GitHub Pages, S3, nginx, or Cloudflare Pages
|
|
|
29638
29670
|
|
|
29639
29671
|
// src/publish-snapshots.ts
|
|
29640
29672
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
29641
|
-
import { readFileSync as
|
|
29673
|
+
import { readFileSync as readFileSync40 } from "node:fs";
|
|
29642
29674
|
import { join as join52 } from "node:path";
|
|
29643
29675
|
var DEFAULT_KEEP = 30;
|
|
29644
29676
|
var SNAPSHOT_REPORTS_BASE = "../../../";
|
|
@@ -29727,7 +29759,7 @@ async function publishSnapshot(outDir, target, commit, client, opts = {}) {
|
|
|
29727
29759
|
const keep = opts.keep ?? DEFAULT_KEEP;
|
|
29728
29760
|
if (!Number.isInteger(keep) || keep < 1) throw new Error(`--keep takes a whole number of at least 1, not ${keep}`);
|
|
29729
29761
|
const at2 = (f) => `${target.prefix}/${f}`;
|
|
29730
|
-
const local = JSON.parse(
|
|
29762
|
+
const local = JSON.parse(readFileSync40(join52(outDir, "manifest.json"), "utf8"));
|
|
29731
29763
|
const generated = local.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
29732
29764
|
const entry = { commit: sha, at: commit.at ?? generated, project: opts.project ?? local.projectDir ?? "", generated };
|
|
29733
29765
|
const bundle = await publishBundle(outDir, { bucket: target.bucket, prefix: at2(sha) }, client);
|
|
@@ -30264,7 +30296,7 @@ async function runCarve(rest) {
|
|
|
30264
30296
|
process.exit(2);
|
|
30265
30297
|
}
|
|
30266
30298
|
const reportPath = resolve35(fileArg);
|
|
30267
|
-
const parsed = readCarveReport(reportPath, (p) =>
|
|
30299
|
+
const parsed = readCarveReport(reportPath, (p) => readFileSync41(p, "utf8"));
|
|
30268
30300
|
if (!parsed.ok) {
|
|
30269
30301
|
process.stderr.write(`behold carve: ${parsed.refusal.error}
|
|
30270
30302
|
${parsed.refusal.remedy}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/behold",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.15",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
|
package/web/app.js
CHANGED
|
@@ -4173,7 +4173,7 @@ function kioskGlow() {
|
|
|
4173
4173
|
};
|
|
4174
4174
|
style.textContent = hoverCss(links, { root: boxes, wave: (w) => `#graph rect[data-group-id="wave-${w}"]` }, "filter: drop-shadow(0 0 2px var(--focus)) drop-shadow(0 0 7px var(--focus));");
|
|
4175
4175
|
}
|
|
4176
|
-
/** The dots by each root's name: on its box's title (Resources) or its card (
|
|
4176
|
+
/** The dots by each root's name: on its box's title (Resources) or its card (Components). */
|
|
4177
4177
|
function kioskDots(svg) {
|
|
4178
4178
|
for (const d of svg.querySelectorAll("[data-k-dot]")) d.remove();
|
|
4179
4179
|
const roots = kioskRootList();
|
package/web/kiosk-popover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// behold#571: the kiosk frame's popover. In the kiosk the graph is framed
|
|
2
2
|
// bare (`?embed=1&bare=1`), with no Inspect pane, so a visitor pointing at a
|
|
3
3
|
// card sees what the plan does to it here instead: in the Resources view, the
|
|
4
|
-
// resource's planned change one attribute a line; in
|
|
4
|
+
// resource's planned change one attribute a line; in Components, the
|
|
5
5
|
// root's resources and which of them change. Pure logic: what the popover
|
|
6
6
|
// says, where it goes, and when it stays open. app.js draws it.
|
|
7
7
|
//
|
|
@@ -148,7 +148,7 @@ export function resourceCardId(answer, root, resourceId) {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* What the popover says about one root's card (
|
|
151
|
+
* What the popover says about one root's card (Components): its
|
|
152
152
|
* resources, each "changes", "no change" or "known after apply", and how
|
|
153
153
|
* many change. `resources` is the components zoom's resources facet for the
|
|
154
154
|
* card (`[{ kind, id }]`).
|
|
@@ -95,7 +95,7 @@ describe("what the popover says about a resource (behold#571)", () => {
|
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
describe("what the popover says about a root, in
|
|
98
|
+
describe("what the popover says about a root, in Components (behold#571)", () => {
|
|
99
99
|
it("lists its resources, each changes, no change or known after apply, and counts the changes", () => {
|
|
100
100
|
const resources = [
|
|
101
101
|
{ kind: "terraform_remote_state", id: "app/data.terraform_remote_state.network" },
|
package/web/kiosk-repo.js
CHANGED
|
@@ -23,3 +23,21 @@ export function repoUrl(url, repo) {
|
|
|
23
23
|
q.set("repo", repo);
|
|
24
24
|
return `${path}?${q.toString()}`;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
const sameCommit = (a, b) => typeof a === "string" && typeof b === "string" && !!a && !!b && (a.startsWith(b) || b.startsWith(a));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The checkout that holds the step's commit (behold#571). A step names the
|
|
31
|
+
* commit it is about (demo.json's `commit`: a pull request's head, then its
|
|
32
|
+
* merge); the repo's own checkout may sit on another one. Of `name` and the
|
|
33
|
+
* served checkouts named `<name>.<anything>`, the one whose commit
|
|
34
|
+
* (`/api/project`'s viewCommits) is the step's; `name` when none is, at the
|
|
35
|
+
* reset and on the base, which draw the base.
|
|
36
|
+
*/
|
|
37
|
+
export function stepCheckout(name, demo, served, commits) {
|
|
38
|
+
if (!name || !demo || !demo.commit || !commits || !Array.isArray(served)) return name;
|
|
39
|
+
if (demo.scenario === "base" || demo.step === "reset") return name;
|
|
40
|
+
const at = (n) => sameCommit(commits[n], demo.commit);
|
|
41
|
+
if (at(name)) return name;
|
|
42
|
+
return served.find((n) => n.startsWith(`${name}.`) && at(n)) || name;
|
|
43
|
+
}
|
package/web/kiosk-repo.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// behold#571: the kiosk asks for its repo's checkout by name, on a behold that serves several.
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
-
import { repoParam, repoUrl } from "./kiosk-repo.js";
|
|
3
|
+
import { repoParam, repoUrl, stepCheckout } from "./kiosk-repo.js";
|
|
4
4
|
import { frameSrc } from "./kiosk.js";
|
|
5
5
|
import { pageRepo, withRepo } from "./view-repo.js";
|
|
6
6
|
|
|
@@ -26,6 +26,33 @@ describe("repoUrl", () => {
|
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
describe("stepCheckout (behold#571)", () => {
|
|
30
|
+
const served = ["demo", "demo.change", "demo-terragrunt", "demo-terragrunt.change"];
|
|
31
|
+
const HEAD = "9eec25f7aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
|
32
|
+
const MERGE = "12f512fcbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
33
|
+
const BASE = "0d14834dcccccccccccccccccccccccccccccccc";
|
|
34
|
+
const commits = { demo: BASE, "demo.change": HEAD, "demo-terragrunt": BASE };
|
|
35
|
+
const step = (step, commit) => ({ scenario: "b-waves", step, commit });
|
|
36
|
+
it("draws the checkout at the pull request's head while the step is about it", () => {
|
|
37
|
+
expect(stepCheckout("demo", step("pull-request", HEAD), served, commits)).toBe("demo.change");
|
|
38
|
+
expect(stepCheckout("demo", step("planned", HEAD.slice(0, 8)), served, commits)).toBe("demo.change");
|
|
39
|
+
});
|
|
40
|
+
it("follows the merge once the driver checked it out, and draws the repo's checkout until then", () => {
|
|
41
|
+
expect(stepCheckout("demo", step("merged", MERGE), served, commits)).toBe("demo");
|
|
42
|
+
expect(stepCheckout("demo", step("merged", MERGE), served, { ...commits, "demo.change": MERGE })).toBe("demo.change");
|
|
43
|
+
});
|
|
44
|
+
it("draws the repo's own checkout at the reset, on the base, and with no commit or commits", () => {
|
|
45
|
+
expect(stepCheckout("demo", step("reset", HEAD), served, commits)).toBe("demo");
|
|
46
|
+
expect(stepCheckout("demo", { scenario: "base", step: "applied", commit: HEAD }, served, commits)).toBe("demo");
|
|
47
|
+
expect(stepCheckout("demo", step("pull-request", null), served, commits)).toBe("demo");
|
|
48
|
+
expect(stepCheckout("demo", step("pull-request", HEAD), served, null)).toBe("demo");
|
|
49
|
+
expect(stepCheckout(null, step("pull-request", HEAD), served, commits)).toBe(null);
|
|
50
|
+
});
|
|
51
|
+
it("picks only the repo's own checkouts", () => {
|
|
52
|
+
expect(stepCheckout("demo-terragrunt", step("pull-request", HEAD), served, commits)).toBe("demo-terragrunt");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
29
56
|
describe("the frame", () => {
|
|
30
57
|
it("names the repo it draws, and nothing without one", () => {
|
|
31
58
|
expect(frameSrc("http://h", "resources")).toBe("/?embed=1&bare=1&zoom=resources&host=http%3A%2F%2Fh");
|
package/web/kiosk-vendor.js
CHANGED
|
@@ -361,8 +361,14 @@ export function notRunRepo(tool, served) {
|
|
|
361
361
|
return name && Array.isArray(served) && served.includes(name) ? name : null;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/** What a tool calls the thing with its own state: a Terragrunt unit, a root otherwise (behold#571). */
|
|
365
|
+
export function rootNoun(tool) {
|
|
366
|
+
return tool === "terragrunt" ? { one: "unit", many: "units" } : { one: "root", many: "roots" };
|
|
367
|
+
}
|
|
368
|
+
|
|
364
369
|
/** The legend's words, without naming a competing engine on an engine's page. */
|
|
365
370
|
export function rootWord(tool) {
|
|
371
|
+
if (tool === "terragrunt") return "a Terragrunt unit";
|
|
366
372
|
return namesOther("Terraform", tool) ? "a root" : "a Terraform root";
|
|
367
373
|
}
|
|
368
374
|
|
package/web/kiosk-vendor.test.js
CHANGED
|
@@ -183,7 +183,8 @@ describe("the branding table (behold#571)", () => {
|
|
|
183
183
|
expect(namesOther("runs on choudoufu", "terraform")).toBe(true);
|
|
184
184
|
expect(namesOther("works with OpenTofu", "terraform")).toBe(false);
|
|
185
185
|
expect(rootWord("choudoufu")).toBe("a root");
|
|
186
|
-
expect(rootWord("terragrunt")).toBe("a
|
|
186
|
+
expect(rootWord("terragrunt")).toBe("a Terragrunt unit");
|
|
187
|
+
expect(rootWord("terraform")).toBe("a Terraform root");
|
|
187
188
|
});
|
|
188
189
|
it("never counts a resource address, a bucket or a path as a brand", () => {
|
|
189
190
|
for (const t of ["terraform_data.app", "data.terraform_remote_state.network", "state in demo-terraform-state", "s3://demo-terraform-state/edge.tfstate", "developer.hashicorp.com/terraform", ".terraform.lock.hcl", "terragrunt-cache"]) {
|
package/web/kiosk.js
CHANGED
|
@@ -17,8 +17,8 @@ import { PICK_HOLD_MS, annMark, mergeRun, startRuns } from "./kiosk-runs.js";
|
|
|
17
17
|
import { PANEL_STEPS, PANEL_TITLE, approvalModel, cycleRuns, rowFacts, shortDigest } from "./kiosk-approval.js";
|
|
18
18
|
import { getThemeName, mountThemePicker, onThemeChange } from "./theme.js";
|
|
19
19
|
import { cssString, hoverCss, hoverFromMessage, hoverLinks, hoverMessage, isLightBg, rootSegments, rootText } from "./kiosk-roots.js";
|
|
20
|
-
import { creditLines, filterIndex, keepRun, liveEstimate, liveFor, namesOther, notRunRepo, notRunWords, replayAt, replayBanner, replayIndex, rootWord, runAsOf, scrubDemo, servesRepo, vendorOf } from "./kiosk-vendor.js";
|
|
21
|
-
import { repoParam, repoUrl } from "./kiosk-repo.js";
|
|
20
|
+
import { creditLines, filterIndex, keepRun, liveEstimate, liveFor, namesOther, notRunRepo, notRunWords, replayAt, replayBanner, replayIndex, rootNoun, rootWord, runAsOf, scrubDemo, servesRepo, vendorOf } from "./kiosk-vendor.js";
|
|
21
|
+
import { repoParam, repoUrl, stepCheckout } from "./kiosk-repo.js";
|
|
22
22
|
import { STATE_WORDS, changingRoots, costLine, pinsCommit, planFitsPin, pullPin, readEdges, rootsCount, totalsLine, waveRows, blastRows } from "./terragucci-change.js";
|
|
23
23
|
|
|
24
24
|
/** The scenarios terragucci#1014 numbers. An id not here is shown by the title demo.json carries. */
|
|
@@ -353,30 +353,29 @@ export function frameSrc(origin, zoom = "resources", repo = null, theme = null)
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
// ---------------------------------------------------------------------------
|
|
356
|
-
// #571: which of behold's two views the frame shows.
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
356
|
+
// #571: which of behold's two views the frame shows. On a terragucci repo
|
|
357
|
+
// (`terragucci.components.json`) every step leads to the components zoom:
|
|
358
|
+
// one card per root in a box per wave, titled with the job that applies it,
|
|
359
|
+
// in step with the jobs pane. Before the merge it draws the pull request's
|
|
360
|
+
// planned waves, and from the merge on the run's. Resources, what the change
|
|
361
|
+
// touches resource by resource, is one click away. A viewer's click on the
|
|
362
|
+
// toggle holds a view for PICK_HOLD_MS after their last click, as a run
|
|
363
|
+
// picked in the sidebar does; then the step leads again.
|
|
364
|
+
|
|
365
|
+
/** The two views the toggle offers, with behold's zoom for each, and what each shows. */
|
|
365
366
|
export const VIEWS = [
|
|
366
|
-
{ id: "resources", words: "Resources", zoom: "resources" },
|
|
367
|
-
{ id: "components", words: "
|
|
367
|
+
{ id: "resources", words: "Resources", zoom: "resources", shows: "each resource the change touches" },
|
|
368
|
+
{ id: "components", words: "Components", zoom: "components", shows: "the pipeline's jobs and waves" },
|
|
368
369
|
];
|
|
369
370
|
|
|
370
371
|
/**
|
|
371
|
-
* The view a step shows: the
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* doesn't know, and `stalled`, keep the components: the jobs pane says where
|
|
375
|
-
* the pipeline stopped.
|
|
372
|
+
* The view a step shows: the components at every step (behold#571), the
|
|
373
|
+
* pull request's planned waves before the merge and the run's from it on.
|
|
374
|
+
* A step behold doesn't know keeps the components too.
|
|
376
375
|
*/
|
|
377
376
|
export const STEP_VIEW = {
|
|
378
|
-
"pull-request": "
|
|
379
|
-
planned: "
|
|
377
|
+
"pull-request": "components",
|
|
378
|
+
planned: "components",
|
|
380
379
|
merged: "components",
|
|
381
380
|
applying: "components",
|
|
382
381
|
waiting: "components",
|
|
@@ -406,16 +405,26 @@ export function kioskView(demo, { components = false, pick = null, now = Date.no
|
|
|
406
405
|
const left = pick.at + PICK_HOLD_MS - now;
|
|
407
406
|
if (left > 0) return { view: pick.view, pinned: true, secs: Math.ceil(left / 1000) };
|
|
408
407
|
}
|
|
409
|
-
return { view: demo ? stepView(demo.step) : "
|
|
408
|
+
return { view: demo ? stepView(demo.step) : "components", pinned: false, secs: null };
|
|
410
409
|
}
|
|
411
410
|
|
|
412
411
|
/** The line under the view saying how to read it, for the view shown. */
|
|
413
412
|
export function legendWords(view, tool = null) {
|
|
414
413
|
const root = rootWord(tool);
|
|
414
|
+
const n = rootNoun(tool);
|
|
415
|
+
const rest = `an arrow is one ${n.one} reading another's outputs, dashed while it reads planned values. A wave applies its ${n.many} together, after the waves it reads.`;
|
|
415
416
|
if (view === "components") {
|
|
416
|
-
return `Each card is ${root} with its own state, in a box per wave titled with the
|
|
417
|
+
return `Components shows the pipeline's jobs and waves. Each card is ${root} with its own state, in a box per wave titled with the job that applies it; ${rest}`;
|
|
417
418
|
}
|
|
418
|
-
return `Each box is ${root} with its own state;
|
|
419
|
+
return `Each box is ${root} with its own state; ${rest}`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
/** The tool the page shows: its own on a tool's page, else the running scenario's (demo.json `tool`). */
|
|
424
|
+
export function nounTool(s) {
|
|
425
|
+
if (s.vendor) return s.vendor;
|
|
426
|
+
const d = s.demo && s.demo.demo;
|
|
427
|
+
return d && typeof d.tool === "string" ? d.tool : null;
|
|
419
428
|
}
|
|
420
429
|
|
|
421
430
|
/** The behold zoom for a view. */
|
|
@@ -425,7 +434,7 @@ export function viewZoom(view) {
|
|
|
425
434
|
}
|
|
426
435
|
|
|
427
436
|
/**
|
|
428
|
-
* The toggle, "Resources |
|
|
437
|
+
* The toggle, "Resources | Components", over the frame: the view shown
|
|
429
438
|
* pressed, and a line saying whether the step leads or a click holds it.
|
|
430
439
|
* Not drawn at all when the repo offers no components zoom.
|
|
431
440
|
*/
|
|
@@ -440,21 +449,24 @@ export function renderViewToggle(host, shown, { components = false, onPick = ()
|
|
|
440
449
|
for (const v of VIEWS) {
|
|
441
450
|
const b = el("button", `k-view${v.id === shown.view ? " k-view-on" : ""}`, v.words);
|
|
442
451
|
b.type = "button";
|
|
452
|
+
b.setAttribute("title", v.shows);
|
|
443
453
|
b.setAttribute("aria-pressed", String(v.id === shown.view));
|
|
444
454
|
b.addEventListener("click", () => onPick(v.id));
|
|
445
455
|
group.appendChild(b);
|
|
446
456
|
}
|
|
447
457
|
host.appendChild(group);
|
|
448
|
-
|
|
458
|
+
const on = VIEWS.find((v) => v.id === shown.view);
|
|
459
|
+
const lead = shown.pinned ? `held by a click; the step leads again in ${shown.secs} s` : "follows the step";
|
|
460
|
+
host.appendChild(el("span", "k-view-note", on ? `${on.shows} · ${lead}` : lead));
|
|
449
461
|
}
|
|
450
462
|
|
|
451
463
|
/** The one-line facts under the view: the pull request's totals, cost and blast radius, as #568 has them. */
|
|
452
|
-
export function factLines(demo, answer) {
|
|
464
|
+
export function factLines(demo, answer, tool = null) {
|
|
453
465
|
const pr = demo ? matchingPull(answer, demo) : null;
|
|
454
466
|
if (!pr) return [];
|
|
455
467
|
const out = [];
|
|
456
468
|
const totals = totalsLine(pr.totals);
|
|
457
|
-
if (totals) out.push(`pull request #${pr.run.pull_request} changes ${rootsCount(pr.totals_roots) ||
|
|
469
|
+
if (totals) out.push(`pull request #${pr.run.pull_request} changes ${rootsCount(pr.totals_roots, rootNoun(tool)) || `across ${rootNoun(tool).many}`}: ${totals}`);
|
|
458
470
|
const b = blastRows(answer);
|
|
459
471
|
if (b && b.changed.length) out.push(`blast radius: ${b.changed.join(", ")}${b.downstream.length ? `; downstream, nearest first: ${b.downstream.join(", ")}` : ""}`);
|
|
460
472
|
if (b && b.unplanned.length) out.push(`unchanged: ${b.unplanned.join(", ")}`);
|
|
@@ -593,9 +605,9 @@ function drawBar(bar, s, now, foot) {
|
|
|
593
605
|
|
|
594
606
|
if (foot) {
|
|
595
607
|
// The legend reads the graph; with no graph shown there is nothing for it to explain.
|
|
596
|
-
if (!s.noReports) foot.appendChild(el("span", "k-legend", legendWords(s.view || "resources", s
|
|
608
|
+
if (!s.noReports) foot.appendChild(el("span", "k-legend", legendWords(s.view || "resources", nounTool(s))));
|
|
597
609
|
// behold#571: no line on where the narration was read from: a viewer new to the demo reads the facts.
|
|
598
|
-
if (!s.noReports) for (const f of factLines(demo, s.answer)) foot.appendChild(elRoots("span", "k-fact", f, s));
|
|
610
|
+
if (!s.noReports) for (const f of factLines(demo, s.answer, nounTool(s))) foot.appendChild(elRoots("span", "k-fact", f, s));
|
|
599
611
|
}
|
|
600
612
|
}
|
|
601
613
|
|
|
@@ -739,7 +751,10 @@ export function startKiosk({ bar, frame, foot, side, main, views, tools = null }
|
|
|
739
751
|
// name. `viewRepos` is what it serves, `aimed` the repo the reads are for.
|
|
740
752
|
s.viewRepos = null;
|
|
741
753
|
s.aimed = null;
|
|
742
|
-
|
|
754
|
+
s.viewCommits = null;
|
|
755
|
+
const repoNamed = () => repoParam(s.repo, s.viewRepos) || (vendor && s.mode === "none" ? notRunRepo(tool, s.viewRepos) : null);
|
|
756
|
+
// behold#571: the checkout at the step's commit, so a pull request's new roots and resources are drawn.
|
|
757
|
+
const wantRepo = () => stepCheckout(repoNamed(), s.demo && s.demo.demo, s.viewRepos, s.viewCommits);
|
|
743
758
|
const fetchServed = fetchJson;
|
|
744
759
|
fetchJson = (url) => fetchServed(repoUrl(url, wantRepo()));
|
|
745
760
|
/** When the page moves to another served repo, read its project, reports and run view again, and point the frame at it. */
|
|
@@ -839,6 +854,8 @@ export function startKiosk({ bar, frame, foot, side, main, views, tools = null }
|
|
|
839
854
|
const stage = d ? demoStage(d) : "none";
|
|
840
855
|
if (stage === "pull-request" || stage === "either") void reports(true);
|
|
841
856
|
if (stage === "apply" || stage === "either") void progress(true);
|
|
857
|
+
// behold#571: a commit no served checkout was on when last asked; the driver may have checked it out since.
|
|
858
|
+
if (d && d.commit && s.viewRepos && !Object.values(s.viewCommits || {}).some((c) => c.startsWith(d.commit) || d.commit.startsWith(c))) void readCommits();
|
|
842
859
|
}
|
|
843
860
|
};
|
|
844
861
|
// behold#571: the replay's clock, read on every draw.
|
|
@@ -1034,10 +1051,19 @@ export function startKiosk({ bar, frame, foot, side, main, views, tools = null }
|
|
|
1034
1051
|
frameSrcNow = src;
|
|
1035
1052
|
frame.src = src;
|
|
1036
1053
|
};
|
|
1054
|
+
/** behold#571: the commit each served checkout holds, read again when a step names a new one. */
|
|
1055
|
+
const readCommits = () => fetchJson("/api/project").then((p) => {
|
|
1056
|
+
if (p && !p.error && Array.isArray(p.viewRepos)) {
|
|
1057
|
+
s.viewRepos = p.viewRepos;
|
|
1058
|
+
s.viewCommits = p.viewCommits || null;
|
|
1059
|
+
draw();
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1037
1062
|
const readProject = () => fetchJson("/api/project").then((p) => {
|
|
1038
1063
|
// behold#571: the repos this behold serves, and which one this answer is.
|
|
1039
1064
|
if (p && !p.error && Array.isArray(p.viewRepos)) {
|
|
1040
1065
|
s.viewRepos = p.viewRepos;
|
|
1066
|
+
s.viewCommits = p.viewCommits || null;
|
|
1041
1067
|
if (s.aimed === null && p.viewRepo) s.aimed = p.viewRepo;
|
|
1042
1068
|
}
|
|
1043
1069
|
s.components = !!(p && !p.error && p.terragucciComponents);
|
package/web/kiosk.test.js
CHANGED
|
@@ -50,6 +50,7 @@ describe("frameSrc (#569)", () => {
|
|
|
50
50
|
describe("factLines (#569)", () => {
|
|
51
51
|
it("says the pull request's totals and blast radius", () => {
|
|
52
52
|
expect(factLines(demo("planned"), answer)).toEqual(["pull request #7 changes across 3 roots: 3 update", "blast radius: app, edge, network", "unchanged: other"]);
|
|
53
|
+
expect(factLines(demo("planned"), answer, "terragrunt")[0]).toBe("pull request #7 changes across 3 units: 3 update");
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
|
|
@@ -207,18 +208,19 @@ describe("the approval panel's drawing (behold#571)", () => {
|
|
|
207
208
|
});
|
|
208
209
|
|
|
209
210
|
describe("which view the frame shows (#571)", () => {
|
|
210
|
-
it("shows the
|
|
211
|
-
for (const step of ["pull-request", "planned"]) expect(stepView(step)).toBe("
|
|
212
|
-
for (const step of ["merged", "applying", "waiting", "approved", "failed", "resumed", "applied", "reset"]) expect(stepView(step)).toBe("components");
|
|
211
|
+
it("shows the components at every step, the pull request's planned waves included", () => {
|
|
212
|
+
for (const step of ["pull-request", "planned", "merged", "applying", "waiting", "approved", "failed", "resumed", "applied", "reset"]) expect(stepView(step)).toBe("components");
|
|
213
213
|
expect(Object.keys(STEP_VIEW).sort()).toEqual(Object.keys(STEP_WORDS).sort());
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it("follows the step, and only the resources when the repo offers no components zoom", () => {
|
|
217
|
-
expect(kioskView(demo("planned"), { components: true })).toEqual({ view: "
|
|
217
|
+
expect(kioskView(demo("planned"), { components: true })).toEqual({ view: "components", pinned: false, secs: null });
|
|
218
|
+
expect(kioskView(demo("planned"), { components: true, pick: { view: "resources", at: 0 }, now: 1000 }).view).toBe("resources");
|
|
218
219
|
expect(kioskView(demo("waiting"), { components: true })).toEqual({ view: "components", pinned: false, secs: null });
|
|
219
220
|
expect(kioskView(demo("waiting"), { components: false })).toEqual({ view: "resources", pinned: false, secs: null });
|
|
220
221
|
expect(kioskView(demo("waiting"), { components: false, pick: { view: "components", at: 0 }, now: 1000 }).view).toBe("resources");
|
|
221
|
-
expect(kioskView(null, { components: true }).view).toBe("
|
|
222
|
+
expect(kioskView(null, { components: true }).view).toBe("components");
|
|
223
|
+
expect(kioskView(null, { components: false }).view).toBe("resources");
|
|
222
224
|
});
|
|
223
225
|
|
|
224
226
|
it("holds a clicked view for 90 s after the last click, then follows the step again", () => {
|
|
@@ -242,8 +244,15 @@ describe("which view the frame shows (#571)", () => {
|
|
|
242
244
|
});
|
|
243
245
|
|
|
244
246
|
it("says how to read the view shown", () => {
|
|
245
|
-
expect(legendWords("components")).toMatch(/^Each card is a Terraform root .* titled with the
|
|
247
|
+
expect(legendWords("components")).toMatch(/^Components shows the pipeline's jobs and waves\. Each card is a Terraform root .* titled with the job that applies it/);
|
|
246
248
|
expect(legendWords("resources")).toMatch(/^Each box is a Terraform root/);
|
|
249
|
+
// behold#571: a Terragrunt scenario names its units.
|
|
250
|
+
for (const v of ["components", "resources"]) {
|
|
251
|
+
const words = legendWords(v, "terragrunt");
|
|
252
|
+
expect(words).toMatch(/is a Terragrunt unit with its own state/);
|
|
253
|
+
expect(words).toMatch(/one unit reading another's outputs.*applies its units together/);
|
|
254
|
+
expect(words).not.toMatch(/\broots?\b/);
|
|
255
|
+
}
|
|
247
256
|
});
|
|
248
257
|
|
|
249
258
|
// A DOM for the toggle: elements with attributes and click listeners.
|
|
@@ -267,13 +276,14 @@ describe("which view the frame shows (#571)", () => {
|
|
|
267
276
|
renderViewToggle(host, { view: "components", pinned: false, secs: null }, { components: true, onPick: (v) => picked.push(v) });
|
|
268
277
|
expect(host.hidden).toBe(false);
|
|
269
278
|
const buttons = host.children[0].children;
|
|
270
|
-
expect(buttons.map((b) => [b.textContent, b.attrs["aria-pressed"]])).toEqual([["Resources", "false"], ["
|
|
271
|
-
expect(host.children[1].textContent).toBe("follows the step");
|
|
279
|
+
expect(buttons.map((b) => [b.textContent, b.attrs["aria-pressed"]])).toEqual([["Resources", "false"], ["Components", "true"]]);
|
|
280
|
+
expect(host.children[1].textContent).toBe("the pipeline's jobs and waves · follows the step");
|
|
281
|
+
expect(buttons[1].attrs.title).toBe("the pipeline's jobs and waves");
|
|
272
282
|
buttons[0].listeners.click();
|
|
273
283
|
expect(picked).toEqual(["resources"]);
|
|
274
284
|
|
|
275
285
|
renderViewToggle(host, { view: "resources", pinned: true, secs: 42 }, { components: true });
|
|
276
|
-
expect(host.children[1].textContent).toBe("held by a click; the step leads again in 42 s");
|
|
286
|
+
expect(host.children[1].textContent).toBe("each resource the change touches · held by a click; the step leads again in 42 s");
|
|
277
287
|
|
|
278
288
|
renderViewToggle(host, { view: "resources", pinned: false, secs: null }, { components: false });
|
|
279
289
|
expect(host.hidden).toBe(true);
|
package/web/terragucci-change.js
CHANGED
|
@@ -129,9 +129,9 @@ export function totalsLine(totals) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/** #568 step 2: "across 3 roots", from `totals_roots` (report minor 35); null on an older report, which has no count. */
|
|
132
|
-
export function rootsCount(n) {
|
|
132
|
+
export function rootsCount(n, noun = { one: "root", many: "roots" }) {
|
|
133
133
|
if (n === undefined || n === null) return null;
|
|
134
|
-
return `across ${n} ${n === 1 ?
|
|
134
|
+
return `across ${n} ${n === 1 ? noun.one : noun.many}`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const money = (n) => (n === null || n === undefined ? "no figure" : n.toFixed(2));
|
|
@@ -62,16 +62,36 @@ export function runsCommit(pin, progress) {
|
|
|
62
62
|
export function jobStatus(index, commit, jobName) {
|
|
63
63
|
const runs = index && index.doc && Array.isArray(index.doc.runs) ? index.doc.runs : [];
|
|
64
64
|
let skipped = null;
|
|
65
|
+
// behold#571: a newer run on the commit that skipped this job but ran
|
|
66
|
+
// another (a /terragucci apply re-run applies every wave in its
|
|
67
|
+
// apply-comment job) is the latest run for the wave.
|
|
68
|
+
let later = null;
|
|
65
69
|
for (const run of runs) {
|
|
66
70
|
if (!sameSha(run.commit, commit)) continue;
|
|
67
71
|
const job = (run.jobs || []).find((j) => j.name === jobName);
|
|
68
72
|
if (!job) continue;
|
|
69
|
-
if (job.status !== "skipped")
|
|
73
|
+
if (job.status !== "skipped") {
|
|
74
|
+
return { status: job.status, words: JOB_WORDS[job.status] || job.status, tone: JOB_TONE[job.status] || "idle", run: run.id, ...(later ? { superseded: later } : {}) };
|
|
75
|
+
}
|
|
76
|
+
if (!later && (run.jobs || []).some((j) => j.status !== "skipped")) later = run.id;
|
|
70
77
|
skipped = skipped || { status: "skipped", words: JOB_WORDS.skipped, tone: "idle", run: run.id };
|
|
71
78
|
}
|
|
72
79
|
return skipped;
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
/** A wave's state from the run view, as a box's title says it after a re-run. */
|
|
83
|
+
export const WAVE_WORDS = { waiting: "held at the gate", applying: "running", applied: "applied", failed: "failed", refused: "refused" };
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The status a wave's box shows: its job's, unless a later run on the commit
|
|
87
|
+
* took the wave over (`superseded`); then the run view's state for the wave,
|
|
88
|
+
* which is the latest run's.
|
|
89
|
+
*/
|
|
90
|
+
export function waveStatus(status, state) {
|
|
91
|
+
if (!status || !status.superseded || !state || !WAVE_WORDS[state]) return status;
|
|
92
|
+
return { status: state, words: WAVE_WORDS[state], tone: toneOf(state), run: status.superseded };
|
|
93
|
+
}
|
|
94
|
+
|
|
75
95
|
/** "apply-wave-2 · held at the gate", or the job alone without a status. */
|
|
76
96
|
export function waveTitle(jobName, status) {
|
|
77
97
|
return status ? `${jobName} · ${status.words}` : jobName;
|
|
@@ -122,9 +142,9 @@ export function componentMarks(answer, progress, facets, { runs = null, pin = nu
|
|
|
122
142
|
const waveNumbers = [...new Set(jobs.map((j) => j.wave).filter((n) => typeof n === "number"))].sort((a, b) => a - b);
|
|
123
143
|
for (const n of waveNumbers) {
|
|
124
144
|
const jobName = jobs.find((j) => j.wave === n).jobName;
|
|
125
|
-
const status = commit ? jobStatus(runs, commit, jobName) : null;
|
|
126
145
|
const members = jobs.filter((j) => j.wave === n).map((j) => cards[j.component]).filter(Boolean);
|
|
127
146
|
const w = waveState(answer, progress, stage, n);
|
|
147
|
+
const status = commit ? waveStatus(jobStatus(runs, commit, jobName), w) : null;
|
|
128
148
|
const facts = [];
|
|
129
149
|
for (const m of members) for (const f of m.facts) if (!facts.includes(f)) facts.push(f);
|
|
130
150
|
// The plan a waiting wave is approved for first, as it's what the approval
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { JOB_WORDS, componentMarks, jobStatus, runsCommit, waveOfKey, waveTitle } from "./terragucci-components-live.js";
|
|
2
|
+
import { JOB_WORDS, componentMarks, jobStatus, runsCommit, waveOfKey, waveStatus, waveTitle } from "./terragucci-components-live.js";
|
|
3
3
|
|
|
4
4
|
// #571: the live marks on a terragucci repo's components zoom. The facets are
|
|
5
5
|
// the components graph's (src/__fixtures__/terragucci-components: network and
|
|
@@ -53,8 +53,8 @@ describe("a wave job's status from the pipeline's runs (#571)", () => {
|
|
|
53
53
|
run(10, "0000000", [job("apply-wave-2", "failure")]),
|
|
54
54
|
);
|
|
55
55
|
expect(jobStatus(index, MERGE, "apply-wave-2")).toEqual({ status: "running", words: "running", tone: "busy", run: 12 });
|
|
56
|
-
// The resume skipped wave 1: the merge's run says how it went.
|
|
57
|
-
expect(jobStatus(index, MERGE, "apply-wave-1")).toEqual({ status: "success", words: "succeeded", tone: "ok", run: 11 });
|
|
56
|
+
// The resume skipped wave 1: the merge's run says how it went, and that a later run went on from it.
|
|
57
|
+
expect(jobStatus(index, MERGE, "apply-wave-1")).toEqual({ status: "success", words: "succeeded", tone: "ok", run: 11, superseded: 12 });
|
|
58
58
|
// A short sha names the same commit.
|
|
59
59
|
expect(jobStatus(index, MERGE.slice(0, 8), "apply-wave-2").run).toBe(12);
|
|
60
60
|
});
|
|
@@ -116,6 +116,32 @@ describe("the component marks (#571)", () => {
|
|
|
116
116
|
expect(m.waves[3].title).toBe("apply-wave-3 · skipped");
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it("titles a wave a re-run applied with the latest run's state, not the merge's failure (behold#571)", () => {
|
|
120
|
+
// As the demo's failure scenario has them: the merge's run fails wave 2; each
|
|
121
|
+
// /terragucci apply re-run applies the waves in its apply-comment job and skips the wave jobs.
|
|
122
|
+
const comment = (id, status) => run(id, MERGE, [job("apply-comment", status), job("apply-wave-1", "skipped"), job("apply-wave-2", "skipped"), job("apply-wave-3", "skipped")]);
|
|
123
|
+
const merge = run(11, MERGE, [job("apply-comment", "skipped"), job("apply-wave-1", "success"), job("apply-wave-2", "failure"), job("apply-wave-3", "queued")]);
|
|
124
|
+
const applied = { ...progress("applied"), states: progress("applied").states.map((w) => ({ ...w, state: "applied" })) };
|
|
125
|
+
const m = componentMarks(answer, applied, facets, { runs: runs(comment(13, "success"), comment(12, "waiting"), merge), pin: MERGE });
|
|
126
|
+
expect(m.cards.app).toMatchObject({ state: "applied" });
|
|
127
|
+
expect(m.waves[2]).toMatchObject({ state: "applied", tone: "ok", title: "apply-wave-2 · applied" });
|
|
128
|
+
expect(m.waves[3].title).toBe("apply-wave-3 · applied");
|
|
129
|
+
// While the re-run waits at the gate, the box says so.
|
|
130
|
+
const held = componentMarks(answer, progress("waiting"), facets, { runs: runs(comment(12, "waiting"), merge), pin: MERGE });
|
|
131
|
+
expect(held.waves[2].title).toBe("apply-wave-2 · held at the gate");
|
|
132
|
+
// Before any re-run, the merge's own failure.
|
|
133
|
+
const failed = componentMarks(answer, progress("failed"), facets, { runs: runs(merge), pin: MERGE });
|
|
134
|
+
expect(failed.waves[2].title).toBe("apply-wave-2 · failed");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("keeps the job's status when no later run took the wave over, or the run view has no state for it", () => {
|
|
138
|
+
const st = { status: "failure", words: "failed", tone: "bad", run: 11 };
|
|
139
|
+
expect(waveStatus(st, "applied")).toBe(st);
|
|
140
|
+
expect(waveStatus({ ...st, superseded: 12 }, null)).toMatchObject({ status: "failure" });
|
|
141
|
+
expect(waveStatus({ ...st, superseded: 12 }, "applied")).toEqual({ status: "applied", words: "applied", tone: "ok", run: 12 });
|
|
142
|
+
expect(waveStatus(null, "applied")).toBe(null);
|
|
143
|
+
});
|
|
144
|
+
|
|
119
145
|
it("draws the pull request's plan before the merge, with no job status", () => {
|
|
120
146
|
const m = componentMarks(answer, null, facets, { runs: runs(run(11, MERGE, [job("apply-wave-2", "waiting")])), pin: `pull-41-a1b2c3d4` });
|
|
121
147
|
expect(m.stage).toBe("pull-request");
|