@reddoorla/maintenance 0.33.0 → 0.35.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/README.md +2 -0
- package/dist/cli/bin.d.ts +21 -0
- package/dist/cli/bin.js +988 -629
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +230 -46
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/forms/index.d.ts +108 -0
- package/dist/forms/index.js +97 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index.d.ts +55 -4
- package/dist/index.js +351 -114
- package/dist/index.js.map +1 -1
- package/dist/recipes/sync-configs.js +67 -10
- package/dist/recipes/sync-configs.js.map +1 -1
- package/dist/types-RXY-vY-5.d.ts +9 -0
- package/dist/util/git.d.ts +37 -1
- package/dist/util/git.js +26 -0
- package/dist/util/git.js.map +1 -1
- package/package.json +15 -2
package/dist/cli/bin.js
CHANGED
|
@@ -19,13 +19,17 @@ function defaultCredentialsPath() {
|
|
|
19
19
|
}
|
|
20
20
|
function parseEnvFile(contents) {
|
|
21
21
|
const out = {};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
const lines = contents.split(/\r?\n/);
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const trimmed = lines[i].trim();
|
|
25
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
26
|
+
const line = trimmed.replace(/^export\s+/, "");
|
|
25
27
|
const eq = line.indexOf("=");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const key = eq > 0 ? line.slice(0, eq).trim() : "";
|
|
29
|
+
if (eq <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
30
|
+
console.warn(`credentials: skipping unparseable line ${i + 1}: ${trimmed}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
29
33
|
let value = line.slice(eq + 1).trim();
|
|
30
34
|
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
31
35
|
value = value.slice(1, -1);
|
|
@@ -57,76 +61,19 @@ var init_credentials = __esm({
|
|
|
57
61
|
}
|
|
58
62
|
});
|
|
59
63
|
|
|
60
|
-
// src/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const child = spawnImpl(cmd, [...args], {
|
|
70
|
-
cwd: opts.cwd,
|
|
71
|
-
env: opts.env ?? process.env,
|
|
72
|
-
stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
|
|
73
|
-
// Detach ONLY when a timeout can fire: the child then leads its own
|
|
74
|
-
// process group, so the timeout can kill the WHOLE tree (vite, and
|
|
75
|
-
// Chromium under lhci/playwright) via process.kill(-pid), not just the
|
|
76
|
-
// npx/pnpm wrapper. Without it, killing the wrapper orphaned the
|
|
77
|
-
// grandchildren — a zombie vite squatting its port, Chrome left running.
|
|
78
|
-
// We do NOT detach timeout-less streaming calls (pnpm install/up):
|
|
79
|
-
// detaching gains nothing there (no timeout → no group-kill) and would
|
|
80
|
-
// break terminal Ctrl-C, which only reaches the foreground group — i.e.
|
|
81
|
-
// it would re-orphan the very children this guards. We never unref() the
|
|
82
|
-
// child since we still await it.
|
|
83
|
-
detached: opts.timeoutMs !== void 0
|
|
84
|
-
});
|
|
85
|
-
const cap = (acc, chunk) => {
|
|
86
|
-
if (acc.length >= maxOutputBytes) return acc;
|
|
87
|
-
const next = acc + chunk;
|
|
88
|
-
return next.length > maxOutputBytes ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER : next;
|
|
89
|
-
};
|
|
90
|
-
let stdout = "";
|
|
91
|
-
let stderr = "";
|
|
92
|
-
if (!streaming) {
|
|
93
|
-
child.stdout?.on("data", (chunk) => stdout = cap(stdout, String(chunk)));
|
|
94
|
-
child.stderr?.on("data", (chunk) => stderr = cap(stderr, String(chunk)));
|
|
95
|
-
}
|
|
96
|
-
const killGroup = (sig) => {
|
|
97
|
-
if (child.pid === void 0) return;
|
|
98
|
-
try {
|
|
99
|
-
killImpl(-child.pid, sig);
|
|
100
|
-
} catch {
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
let killTimer;
|
|
104
|
-
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
105
|
-
killGroup("SIGTERM");
|
|
106
|
-
killTimer = setTimeout(() => killGroup("SIGKILL"), killGraceMs);
|
|
107
|
-
killTimer.unref();
|
|
108
|
-
reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
|
|
109
|
-
}, opts.timeoutMs) : void 0;
|
|
110
|
-
const clearTimers = () => {
|
|
111
|
-
if (timer) clearTimeout(timer);
|
|
112
|
-
if (killTimer) clearTimeout(killTimer);
|
|
113
|
-
};
|
|
114
|
-
child.on("error", (err) => {
|
|
115
|
-
clearTimers();
|
|
116
|
-
reject(err);
|
|
117
|
-
});
|
|
118
|
-
child.on("close", (code) => {
|
|
119
|
-
clearTimers();
|
|
120
|
-
resolve12({ code: code ?? -1, stdout, stderr });
|
|
121
|
-
});
|
|
122
|
-
});
|
|
64
|
+
// src/util/url.ts
|
|
65
|
+
function isHttpUrl(s) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = new URL(s);
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
123
73
|
}
|
|
124
|
-
var
|
|
125
|
-
|
|
126
|
-
"src/audits/util/spawn.ts"() {
|
|
74
|
+
var init_url = __esm({
|
|
75
|
+
"src/util/url.ts"() {
|
|
127
76
|
"use strict";
|
|
128
|
-
TRUNCATION_MARKER = "\n\u2026[output truncated]";
|
|
129
|
-
defaultSpawn = makeSpawn();
|
|
130
77
|
}
|
|
131
78
|
});
|
|
132
79
|
|
|
@@ -171,6 +118,7 @@ __export(websites_exports, {
|
|
|
171
118
|
mapRow: () => mapRow,
|
|
172
119
|
siteSlug: () => siteSlug,
|
|
173
120
|
updateA11yCounts: () => updateA11yCounts,
|
|
121
|
+
updateAuditFields: () => updateAuditFields,
|
|
174
122
|
updateDepsCounts: () => updateDepsCounts,
|
|
175
123
|
updateGitHubSignals: () => updateGitHubSignals,
|
|
176
124
|
updateLaunched: () => updateLaunched,
|
|
@@ -255,23 +203,19 @@ async function getWebsiteBySlug(base, slug) {
|
|
|
255
203
|
});
|
|
256
204
|
return rows.find((w) => siteSlug(w.name) === slug) ?? null;
|
|
257
205
|
}
|
|
258
|
-
|
|
259
|
-
|
|
206
|
+
function scoreFields(scores) {
|
|
207
|
+
return {
|
|
260
208
|
pScore: scores.performance,
|
|
261
209
|
rScore: scores.accessibility,
|
|
262
210
|
bpScore: scores.bestPractices,
|
|
263
211
|
seoScore: scores.seo,
|
|
264
212
|
"Last lighthouse audit at": (/* @__PURE__ */ new Date()).toISOString()
|
|
265
213
|
};
|
|
266
|
-
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
267
214
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"A11y Violations": counts.violations
|
|
271
|
-
};
|
|
272
|
-
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
215
|
+
function a11yFields(counts) {
|
|
216
|
+
return { "A11y Violations": counts.violations };
|
|
273
217
|
}
|
|
274
|
-
|
|
218
|
+
function depsFields(counts) {
|
|
275
219
|
const fields = {
|
|
276
220
|
"Deps Drifted": counts.drifted,
|
|
277
221
|
"Deps Major Behind": counts.majorBehind
|
|
@@ -279,16 +223,36 @@ async function updateDepsCounts(base, recordId, counts) {
|
|
|
279
223
|
if (counts.outdated !== null) {
|
|
280
224
|
fields["Deps Outdated"] = counts.outdated;
|
|
281
225
|
}
|
|
282
|
-
|
|
226
|
+
return fields;
|
|
283
227
|
}
|
|
284
|
-
|
|
285
|
-
|
|
228
|
+
function securityFields(counts) {
|
|
229
|
+
return {
|
|
286
230
|
"Security Vulns Critical": counts.critical,
|
|
287
231
|
"Security Vulns High": counts.high,
|
|
288
232
|
"Security Vulns Moderate": counts.moderate,
|
|
289
233
|
"Security Vulns Low": counts.low
|
|
290
234
|
};
|
|
235
|
+
}
|
|
236
|
+
async function updateScores(base, recordId, scores) {
|
|
237
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields: scoreFields(scores) }]);
|
|
238
|
+
}
|
|
239
|
+
async function updateA11yCounts(base, recordId, counts) {
|
|
240
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields: a11yFields(counts) }]);
|
|
241
|
+
}
|
|
242
|
+
async function updateDepsCounts(base, recordId, counts) {
|
|
243
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields: depsFields(counts) }]);
|
|
244
|
+
}
|
|
245
|
+
async function updateSecurityCounts(base, recordId, counts) {
|
|
246
|
+
await base(WEBSITES_TABLE).update([{ id: recordId, fields: securityFields(counts) }]);
|
|
247
|
+
}
|
|
248
|
+
async function updateAuditFields(base, recordId, audits) {
|
|
249
|
+
const fields = {};
|
|
250
|
+
if (audits.scores) Object.assign(fields, scoreFields(audits.scores));
|
|
251
|
+
if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));
|
|
252
|
+
if (audits.deps) Object.assign(fields, depsFields(audits.deps));
|
|
253
|
+
if (audits.security) Object.assign(fields, securityFields(audits.security));
|
|
291
254
|
await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
|
|
255
|
+
return fields;
|
|
292
256
|
}
|
|
293
257
|
async function updateGitHubSignals(base, recordId, signals) {
|
|
294
258
|
const fields = {
|
|
@@ -327,16 +291,28 @@ function fromAirtableBase(base, opts = {}) {
|
|
|
327
291
|
);
|
|
328
292
|
}
|
|
329
293
|
const websites = await listWebsites(base);
|
|
330
|
-
return websites.filter((w) => AUDITABLE_STATUSES.has(w.status ?? "") && w.url.length > 0).
|
|
294
|
+
return websites.filter((w) => AUDITABLE_STATUSES.has(w.status ?? "") && w.url.length > 0).flatMap((w) => {
|
|
331
295
|
const slug = siteSlug(w.name);
|
|
296
|
+
if (slug.length === 0) {
|
|
297
|
+
console.warn(
|
|
298
|
+
`[inventory] skipping "${w.name}" (row ${w.id}): Name has no slug-able characters (empty slug)`
|
|
299
|
+
);
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
332
302
|
const site = {
|
|
333
303
|
path: `${workdir}/${slug}`,
|
|
334
304
|
name: slug,
|
|
335
|
-
deployedUrl: w.url,
|
|
336
305
|
meta: { airtableRowId: w.id, displayName: w.name }
|
|
337
306
|
};
|
|
307
|
+
if (isHttpUrl(w.url)) {
|
|
308
|
+
site.deployedUrl = w.url;
|
|
309
|
+
} else {
|
|
310
|
+
console.warn(
|
|
311
|
+
`[inventory] skipping deployed audit for "${w.name}": url is not http(s): ${JSON.stringify(w.url)}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
338
314
|
if (w.gitRepo) site.gitRepo = w.gitRepo;
|
|
339
|
-
return site;
|
|
315
|
+
return [site];
|
|
340
316
|
});
|
|
341
317
|
};
|
|
342
318
|
}
|
|
@@ -345,6 +321,7 @@ var init_airtable = __esm({
|
|
|
345
321
|
"src/inventory/airtable.ts"() {
|
|
346
322
|
"use strict";
|
|
347
323
|
init_websites();
|
|
324
|
+
init_url();
|
|
348
325
|
AUDITABLE_STATUSES = /* @__PURE__ */ new Set(["maintenance", "launch period"]);
|
|
349
326
|
}
|
|
350
327
|
});
|
|
@@ -357,7 +334,7 @@ __export(lighthouse_airtable_exports, {
|
|
|
357
334
|
resolveSlugFromCwd: () => resolveSlugFromCwd
|
|
358
335
|
});
|
|
359
336
|
import { readFile as readFile7 } from "fs/promises";
|
|
360
|
-
import { join as
|
|
337
|
+
import { join as join10 } from "path";
|
|
361
338
|
function hasRealScores(result) {
|
|
362
339
|
if (result.audit !== "lighthouse") return false;
|
|
363
340
|
const details = result.details ?? {};
|
|
@@ -382,7 +359,7 @@ function lighthouseScoresFromResult(result) {
|
|
|
382
359
|
}
|
|
383
360
|
async function resolveSlugFromCwd(cwd) {
|
|
384
361
|
try {
|
|
385
|
-
const pkgPath =
|
|
362
|
+
const pkgPath = join10(cwd, "package.json");
|
|
386
363
|
const raw = await readFile7(pkgPath, "utf-8");
|
|
387
364
|
const pkg = JSON.parse(raw);
|
|
388
365
|
if (!pkg.name) throw new Error("package.json has no 'name' field");
|
|
@@ -488,30 +465,34 @@ async function writeAuditsToAirtable(args) {
|
|
|
488
465
|
throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
|
|
489
466
|
}
|
|
490
467
|
const writes = [];
|
|
468
|
+
const audits = {};
|
|
491
469
|
const lhHasScores = hasRealScores(lhResult);
|
|
492
470
|
if (lhHasScores) {
|
|
493
471
|
const scores = lighthouseScoresFromResult(lhResult);
|
|
494
|
-
|
|
472
|
+
audits.scores = scores;
|
|
495
473
|
writes.push({ audit: "lighthouse", counts: scores });
|
|
496
474
|
}
|
|
497
475
|
const a11y = results.find((r) => r.audit === "a11y");
|
|
498
476
|
if (a11y && hasA11yCounts(a11y)) {
|
|
499
477
|
const counts = a11yCountsFromResult(a11y);
|
|
500
|
-
|
|
478
|
+
audits.a11y = counts;
|
|
501
479
|
writes.push({ audit: "a11y", counts });
|
|
502
480
|
}
|
|
503
481
|
const deps = results.find((r) => r.audit === "deps");
|
|
504
482
|
if (deps && hasDepsCounts(deps)) {
|
|
505
483
|
const counts = depsCountsFromResult(deps);
|
|
506
|
-
|
|
484
|
+
audits.deps = counts;
|
|
507
485
|
writes.push({ audit: "deps", counts });
|
|
508
486
|
}
|
|
509
487
|
const sec = results.find((r) => r.audit === "security");
|
|
510
488
|
if (sec && hasSecurityCounts(sec)) {
|
|
511
489
|
const counts = securityCountsFromResult(sec);
|
|
512
|
-
|
|
490
|
+
audits.security = counts;
|
|
513
491
|
writes.push({ audit: "security", counts });
|
|
514
492
|
}
|
|
493
|
+
if (Object.keys(audits).length > 0) {
|
|
494
|
+
await updateAuditFields(base, target.id, audits);
|
|
495
|
+
}
|
|
515
496
|
if (!lhHasScores) {
|
|
516
497
|
const persisted = writes.map((w) => w.audit);
|
|
517
498
|
throw Object.assign(
|
|
@@ -566,187 +547,16 @@ var init_write_audits_to_airtable = __esm({
|
|
|
566
547
|
}
|
|
567
548
|
});
|
|
568
549
|
|
|
569
|
-
// src/
|
|
570
|
-
function
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
case "ERROR":
|
|
576
|
-
return "failing";
|
|
577
|
-
case "PENDING":
|
|
578
|
-
case "EXPECTED":
|
|
579
|
-
return "pending";
|
|
580
|
-
default:
|
|
581
|
-
return "none";
|
|
582
|
-
}
|
|
550
|
+
// src/reports/airtable/reports.ts
|
|
551
|
+
function toReportType(raw) {
|
|
552
|
+
if (raw && REPORT_TYPES.includes(raw)) return raw;
|
|
553
|
+
if (raw)
|
|
554
|
+
console.warn(`[reports] unknown Report type ${JSON.stringify(raw)} \u2014 treating as Maintenance`);
|
|
555
|
+
return "Maintenance";
|
|
583
556
|
}
|
|
584
|
-
function
|
|
585
|
-
|
|
586
|
-
const env = { ...process.env, GH_TOKEN: deps.token };
|
|
587
|
-
async function gh(args) {
|
|
588
|
-
const r = await spawn2("gh", args, { env, timeoutMs: 6e4 });
|
|
589
|
-
if (r.code !== 0) throw new Error(`gh ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);
|
|
590
|
-
return r.stdout;
|
|
591
|
-
}
|
|
592
|
-
return {
|
|
593
|
-
async openPullRequest(repo, pr) {
|
|
594
|
-
const out = await gh([
|
|
595
|
-
"pr",
|
|
596
|
-
"create",
|
|
597
|
-
"--repo",
|
|
598
|
-
repo,
|
|
599
|
-
"--head",
|
|
600
|
-
pr.head,
|
|
601
|
-
"--base",
|
|
602
|
-
pr.base,
|
|
603
|
-
"--title",
|
|
604
|
-
pr.title,
|
|
605
|
-
"--body",
|
|
606
|
-
pr.body
|
|
607
|
-
]);
|
|
608
|
-
return { url: out.trim() };
|
|
609
|
-
},
|
|
610
|
-
async enableRepoAutoMerge(repo) {
|
|
611
|
-
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-F", "allow_auto_merge=true"]);
|
|
612
|
-
},
|
|
613
|
-
async protectBranch(repo, branch, requiredChecks) {
|
|
614
|
-
const args = [
|
|
615
|
-
"api",
|
|
616
|
-
"-X",
|
|
617
|
-
"PUT",
|
|
618
|
-
`repos/${repo}/branches/${branch}/protection`,
|
|
619
|
-
"-H",
|
|
620
|
-
"Accept: application/vnd.github+json",
|
|
621
|
-
"-F",
|
|
622
|
-
"required_status_checks[strict]=true",
|
|
623
|
-
...requiredChecks.flatMap((c) => ["-f", `required_status_checks[contexts][]=${c}`]),
|
|
624
|
-
"-F",
|
|
625
|
-
"enforce_admins=true",
|
|
626
|
-
"-F",
|
|
627
|
-
"required_pull_request_reviews=null",
|
|
628
|
-
"-F",
|
|
629
|
-
"restrictions=null"
|
|
630
|
-
];
|
|
631
|
-
await gh(args);
|
|
632
|
-
},
|
|
633
|
-
async setRepoSecret(repo, name, value) {
|
|
634
|
-
await gh(["secret", "set", name, "--repo", repo, "--body", value]);
|
|
635
|
-
},
|
|
636
|
-
async repoExists(repo) {
|
|
637
|
-
const r = await spawn2("gh", ["api", `repos/${repo}`], { env, timeoutMs: 6e4 });
|
|
638
|
-
return r.code === 0;
|
|
639
|
-
},
|
|
640
|
-
async defaultBranch(repo) {
|
|
641
|
-
const out = await gh(["api", `repos/${repo}`, "--jq", ".default_branch"]);
|
|
642
|
-
return out.trim();
|
|
643
|
-
},
|
|
644
|
-
// filesOnBranch and branchProtectionContexts call `spawn` directly (not the
|
|
645
|
-
// throwing `gh()` helper) because a 404 is an expected, meaningful answer —
|
|
646
|
-
// "file/protection absent" — not an error. The remaining readers use `gh()`
|
|
647
|
-
// since a non-200 there is a genuine failure (e.g. missing token scope).
|
|
648
|
-
async filesOnBranch(repo, branch, paths) {
|
|
649
|
-
const present = [];
|
|
650
|
-
for (const p of paths) {
|
|
651
|
-
const r = await spawn2("gh", [`api`, `repos/${repo}/contents/${p}?ref=${branch}`], {
|
|
652
|
-
env,
|
|
653
|
-
timeoutMs: 6e4
|
|
654
|
-
});
|
|
655
|
-
if (r.code === 0) present.push(p);
|
|
656
|
-
}
|
|
657
|
-
return present;
|
|
658
|
-
},
|
|
659
|
-
async branchProtectionContexts(repo, branch) {
|
|
660
|
-
const r = await spawn2(
|
|
661
|
-
"gh",
|
|
662
|
-
[
|
|
663
|
-
"api",
|
|
664
|
-
`repos/${repo}/branches/${branch}/protection`,
|
|
665
|
-
"--jq",
|
|
666
|
-
".required_status_checks.contexts[]?"
|
|
667
|
-
],
|
|
668
|
-
{ env, timeoutMs: 6e4 }
|
|
669
|
-
);
|
|
670
|
-
if (r.code !== 0) return [];
|
|
671
|
-
return r.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
672
|
-
},
|
|
673
|
-
async secretExists(repo, name) {
|
|
674
|
-
const out = await gh(["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
675
|
-
return out.split("\n").map((l) => l.trim()).includes(name);
|
|
676
|
-
},
|
|
677
|
-
async autoMergeEnabled(repo) {
|
|
678
|
-
const out = await gh(["api", `repos/${repo}`, "--jq", ".allow_auto_merge"]);
|
|
679
|
-
return out.trim() === "true";
|
|
680
|
-
},
|
|
681
|
-
async findOpenSelfUpdatingPR(repo) {
|
|
682
|
-
const out = await gh([
|
|
683
|
-
"api",
|
|
684
|
-
`repos/${repo}/pulls?state=open`,
|
|
685
|
-
"--jq",
|
|
686
|
-
'.[] | select(.head.ref | startswith("maint/self-updating-")) | .html_url'
|
|
687
|
-
]);
|
|
688
|
-
const first = out.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
689
|
-
return first ?? null;
|
|
690
|
-
},
|
|
691
|
-
async openPullRequests(repo) {
|
|
692
|
-
const [owner, name, ...rest] = repo.split("/");
|
|
693
|
-
if (!owner || !name || rest.length > 0) {
|
|
694
|
-
throw new Error(`openPullRequests: expected "owner/repo", got "${repo}"`);
|
|
695
|
-
}
|
|
696
|
-
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}";
|
|
697
|
-
const out = await gh([
|
|
698
|
-
"api",
|
|
699
|
-
"graphql",
|
|
700
|
-
"-f",
|
|
701
|
-
`query=${query}`,
|
|
702
|
-
"-F",
|
|
703
|
-
`owner=${owner}`,
|
|
704
|
-
"-F",
|
|
705
|
-
`name=${name}`
|
|
706
|
-
]);
|
|
707
|
-
const parsed = JSON.parse(out);
|
|
708
|
-
const nodes = parsed.data?.repository?.pullRequests?.nodes ?? [];
|
|
709
|
-
return nodes.map((n) => ({
|
|
710
|
-
number: n.number,
|
|
711
|
-
title: n.title,
|
|
712
|
-
url: n.url,
|
|
713
|
-
headRef: n.headRefName,
|
|
714
|
-
ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state)
|
|
715
|
-
}));
|
|
716
|
-
},
|
|
717
|
-
async defaultBranchStatus(repo) {
|
|
718
|
-
const [owner, name, ...rest] = repo.split("/");
|
|
719
|
-
if (!owner || !name || rest.length > 0) {
|
|
720
|
-
throw new Error(`defaultBranchStatus: expected "owner/repo", got "${repo}"`);
|
|
721
|
-
}
|
|
722
|
-
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){defaultBranchRef{target{... on Commit{committedDate statusCheckRollup{state}}}}}}";
|
|
723
|
-
const out = await gh([
|
|
724
|
-
"api",
|
|
725
|
-
"graphql",
|
|
726
|
-
"-f",
|
|
727
|
-
`query=${query}`,
|
|
728
|
-
"-F",
|
|
729
|
-
`owner=${owner}`,
|
|
730
|
-
"-F",
|
|
731
|
-
`name=${name}`
|
|
732
|
-
]);
|
|
733
|
-
const parsed = JSON.parse(out);
|
|
734
|
-
const target = parsed.data?.repository?.defaultBranchRef?.target;
|
|
735
|
-
return {
|
|
736
|
-
ciState: mapRollupState(target?.statusCheckRollup?.state),
|
|
737
|
-
lastCommitAt: target?.committedDate ?? null
|
|
738
|
-
};
|
|
739
|
-
}
|
|
740
|
-
};
|
|
557
|
+
function isPendingApproval(r) {
|
|
558
|
+
return r.draftReady && !r.approvedToSend && r.sentAt === null;
|
|
741
559
|
}
|
|
742
|
-
var init_gh = __esm({
|
|
743
|
-
"src/github/gh.ts"() {
|
|
744
|
-
"use strict";
|
|
745
|
-
init_spawn();
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
// src/reports/airtable/reports.ts
|
|
750
560
|
function mapRow2(rec) {
|
|
751
561
|
const f = rec.fields;
|
|
752
562
|
const linkSites = f["Site"] ?? [];
|
|
@@ -755,7 +565,7 @@ function mapRow2(rec) {
|
|
|
755
565
|
id: rec.id,
|
|
756
566
|
reportId: String(f["Report ID"] ?? ""),
|
|
757
567
|
siteId: linkSites[0] ?? "",
|
|
758
|
-
reportType: f["Report type"]
|
|
568
|
+
reportType: toReportType(f["Report type"]),
|
|
759
569
|
period: f["Period"] ?? null,
|
|
760
570
|
periodStart: f["Period start"] ?? null,
|
|
761
571
|
periodEnd: f["Period end"] ?? null,
|
|
@@ -821,6 +631,16 @@ async function createDraft(base, input) {
|
|
|
821
631
|
async function setDraftReady(base, recordId, ready) {
|
|
822
632
|
await base(REPORTS_TABLE).update([{ id: recordId, fields: { "Draft ready": ready } }]);
|
|
823
633
|
}
|
|
634
|
+
async function updateReportScores(base, recordId, scores, completedOn) {
|
|
635
|
+
const fields = {
|
|
636
|
+
"Lighthouse \u2014 Performance": scores.performance,
|
|
637
|
+
"Lighthouse \u2014 Accessibility": scores.accessibility,
|
|
638
|
+
"Lighthouse \u2014 Best Practices": scores.bestPractices,
|
|
639
|
+
"Lighthouse \u2014 SEO": scores.seo
|
|
640
|
+
};
|
|
641
|
+
if (completedOn) fields["Completed on"] = ymd(completedOn);
|
|
642
|
+
await base(REPORTS_TABLE).update([{ id: recordId, fields }]);
|
|
643
|
+
}
|
|
824
644
|
async function listSendableReports(base) {
|
|
825
645
|
const out = [];
|
|
826
646
|
await base(REPORTS_TABLE).select({
|
|
@@ -844,13 +664,12 @@ async function listReportsForSite(base, siteId) {
|
|
|
844
664
|
return (await listAllReports(base)).filter((r) => r.siteId === siteId);
|
|
845
665
|
}
|
|
846
666
|
async function stampSent(base, recordId, sentAt, messageId) {
|
|
667
|
+
const fields = { "Sent at": sentAt.toISOString() };
|
|
668
|
+
if (messageId !== null) fields["Resend message ID"] = messageId;
|
|
847
669
|
await base(REPORTS_TABLE).update([
|
|
848
670
|
{
|
|
849
671
|
id: recordId,
|
|
850
|
-
fields
|
|
851
|
-
"Sent at": sentAt.toISOString(),
|
|
852
|
-
"Resend message ID": messageId
|
|
853
|
-
}
|
|
672
|
+
fields
|
|
854
673
|
}
|
|
855
674
|
]);
|
|
856
675
|
}
|
|
@@ -865,11 +684,12 @@ async function findReportByPeriod(base, siteId, reportType, period) {
|
|
|
865
684
|
});
|
|
866
685
|
return rows.find((r) => r.siteId === siteId) ?? null;
|
|
867
686
|
}
|
|
868
|
-
var REPORTS_TABLE;
|
|
687
|
+
var REPORTS_TABLE, REPORT_TYPES;
|
|
869
688
|
var init_reports = __esm({
|
|
870
689
|
"src/reports/airtable/reports.ts"() {
|
|
871
690
|
"use strict";
|
|
872
691
|
REPORTS_TABLE = "Reports";
|
|
692
|
+
REPORT_TYPES = ["Maintenance", "Testing", "Launch"];
|
|
873
693
|
}
|
|
874
694
|
});
|
|
875
695
|
|
|
@@ -937,18 +757,18 @@ var init_copy = __esm({
|
|
|
937
757
|
// src/reports/maintenance-email/assets/index.ts
|
|
938
758
|
import { readFile as readFile13 } from "fs/promises";
|
|
939
759
|
import { existsSync as existsSync3 } from "fs";
|
|
940
|
-
import { dirname as dirname4, join as
|
|
760
|
+
import { dirname as dirname4, join as join25 } from "path";
|
|
941
761
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
942
762
|
function resolveAssetsDir() {
|
|
943
763
|
if (cachedAssetsDir) return cachedAssetsDir;
|
|
944
764
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
945
765
|
while (true) {
|
|
946
|
-
const srcCandidate =
|
|
766
|
+
const srcCandidate = join25(dir, "src", "reports", "maintenance-email", "assets", "check.png");
|
|
947
767
|
if (existsSync3(srcCandidate)) {
|
|
948
768
|
cachedAssetsDir = dirname4(srcCandidate);
|
|
949
769
|
return cachedAssetsDir;
|
|
950
770
|
}
|
|
951
|
-
const distCandidate =
|
|
771
|
+
const distCandidate = join25(dir, "dist", "reports", "maintenance-email", "assets", "check.png");
|
|
952
772
|
if (existsSync3(distCandidate)) {
|
|
953
773
|
cachedAssetsDir = dirname4(distCandidate);
|
|
954
774
|
return cachedAssetsDir;
|
|
@@ -965,8 +785,8 @@ function resolveAssetsDir() {
|
|
|
965
785
|
async function loadBundledImages() {
|
|
966
786
|
const assetsDir = resolveAssetsDir();
|
|
967
787
|
const [check, blurred] = await Promise.all([
|
|
968
|
-
readFile13(
|
|
969
|
-
readFile13(
|
|
788
|
+
readFile13(join25(assetsDir, "check.png")),
|
|
789
|
+
readFile13(join25(assetsDir, "blurredTests.jpg"))
|
|
970
790
|
]);
|
|
971
791
|
return {
|
|
972
792
|
check: {
|
|
@@ -993,9 +813,19 @@ var init_assets = __esm({
|
|
|
993
813
|
}
|
|
994
814
|
});
|
|
995
815
|
|
|
816
|
+
// src/util/html.ts
|
|
817
|
+
function escapeHtml(s) {
|
|
818
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
819
|
+
}
|
|
820
|
+
var init_html = __esm({
|
|
821
|
+
"src/util/html.ts"() {
|
|
822
|
+
"use strict";
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
996
826
|
// src/reports/maintenance-email/template.ts
|
|
997
827
|
function fmtDate(d) {
|
|
998
|
-
if (!d) return "";
|
|
828
|
+
if (!d || Number.isNaN(d.getTime())) return "";
|
|
999
829
|
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
1000
830
|
const dd = String(d.getUTCDate()).padStart(2, "0");
|
|
1001
831
|
const yyyy = d.getUTCFullYear();
|
|
@@ -1004,9 +834,6 @@ function fmtDate(d) {
|
|
|
1004
834
|
function fmtUsers(n) {
|
|
1005
835
|
return n.toLocaleString("en-US");
|
|
1006
836
|
}
|
|
1007
|
-
function escapeXml(s) {
|
|
1008
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1009
|
-
}
|
|
1010
837
|
function trendText(color, text) {
|
|
1011
838
|
return `<mj-text color="${color}" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${text}</mj-text>`;
|
|
1012
839
|
}
|
|
@@ -1024,14 +851,14 @@ function analyticsTrendLine(cur, prev) {
|
|
|
1024
851
|
return trendText(TREND_NEUTRAL, `No change vs last period (${fmtUsers(prev)})`);
|
|
1025
852
|
}
|
|
1026
853
|
function maintenanceChecksSection(copy, searchPosition) {
|
|
1027
|
-
const googleLabel = searchPosition !== void 0 ? `Page 1 Google Result (#${searchPosition})` : copy.maintenanceChecks[3];
|
|
854
|
+
const googleLabel = searchPosition !== void 0 ? `Page 1 Google Result (#${searchPosition})` : copy.maintenanceChecks[3] ?? "";
|
|
1028
855
|
const rows = copy.maintenanceChecks.map((label, i) => i === 3 ? googleLabel : label);
|
|
1029
856
|
return rows.map(
|
|
1030
857
|
(label, i) => `
|
|
1031
858
|
<mj-section background-color="white" padding="0px"${i === rows.length - 1 ? ' padding-bottom="36px"' : ""}>
|
|
1032
859
|
<mj-group>
|
|
1033
860
|
<mj-column padding-left="0px" width="90%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""}>
|
|
1034
|
-
<mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${label}</mj-text>
|
|
861
|
+
<mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
|
|
1035
862
|
</mj-column>
|
|
1036
863
|
<mj-column width="10%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""} padding-top="15px">
|
|
1037
864
|
<mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
|
|
@@ -1047,7 +874,7 @@ function testingChecklistSection(copy) {
|
|
|
1047
874
|
<mj-section background-color="#F4F4F4" padding="0px"${i === rows.length - 1 ? ' padding-bottom="60px"' : ""}>
|
|
1048
875
|
<mj-group>
|
|
1049
876
|
<mj-column width="90%" padding-left="0px"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""}>
|
|
1050
|
-
<mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${label}</mj-text>
|
|
877
|
+
<mj-text height="25px" padding-left="0px" color="#757575" padding-top="20px" padding-bottom="7.5px" font-size="16px">${escapeXml(label)}</mj-text>
|
|
1051
878
|
</mj-column>
|
|
1052
879
|
<mj-column width="10%"${i < rows.length - 1 ? ' border-bottom="solid #CCCCCC 1px"' : ""} padding-top="15px">
|
|
1053
880
|
<mj-image align="right" padding-right="0px" width="20px" height="20px" padding-top="2.5px" padding-bottom="15px" src="${CHECK_PNG}" />
|
|
@@ -1083,7 +910,7 @@ function commentarySection(text, copy) {
|
|
|
1083
910
|
<mj-section background-color="white">
|
|
1084
911
|
<mj-column>
|
|
1085
912
|
<mj-text color="#C00" font-size="20px" font-weight="700" padding-top="55px">${escapeXml(copy.notesHeader)}</mj-text>
|
|
1086
|
-
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(text).replace(/\n/g, "<br/>")}</mj-text>
|
|
913
|
+
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="16px" font-weight="300" line-height="24px">${escapeXml(text).replace(/\r\n?|\n/g, "<br/>")}</mj-text>
|
|
1087
914
|
</mj-column>
|
|
1088
915
|
</mj-section>`;
|
|
1089
916
|
}
|
|
@@ -1093,7 +920,7 @@ function hasHeaderDims(data) {
|
|
|
1093
920
|
function headerImageTag(data) {
|
|
1094
921
|
const src = `cid:${data.headerImageCid}`;
|
|
1095
922
|
const alt = `${escapeXml(data.siteName)} maintenance report`;
|
|
1096
|
-
const href = escapeXml(data.siteUrl);
|
|
923
|
+
const href = isHttpUrl(data.siteUrl) ? escapeXml(data.siteUrl) : "#";
|
|
1097
924
|
if (hasHeaderDims(data)) {
|
|
1098
925
|
return `<mj-image href="${href}" src="${src}" alt="${alt}" width="${data.headerWidth}px" css-class="rd-header" container-background-color="${data.headerBgColor}" />`;
|
|
1099
926
|
}
|
|
@@ -1170,7 +997,7 @@ function buildMjml(data) {
|
|
|
1170
997
|
(line, i) => i === copy.contact.length - 1 ? `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" padding-top="0px" line-height="30px" padding-bottom="36px">${escapeXml(line)}</mj-text>` : `<mj-text font-family="helvetica, sans-serif" font-size="24px" font-weight="300" line-height="30px">${escapeXml(line)}</mj-text>`
|
|
1171
998
|
).join("\n ")}
|
|
1172
999
|
<mj-divider border-width="1px" border-style="solid" border-color="#CCCCCC" padding="0" />
|
|
1173
|
-
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()}
|
|
1000
|
+
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" padding-top="24px" line-height="20px" font-style="italic">Copyright ${(/* @__PURE__ */ new Date()).getUTCFullYear()} ${escapeXml(copy.footerOrg)}. All rights reserved.</mj-text>
|
|
1174
1001
|
<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="700" line-height="16px" padding-top="0" padding-bottom="0px">Our mailing address is:</mj-text>
|
|
1175
1002
|
${[copy.footerOrg, ...copy.footerAddress].map(
|
|
1176
1003
|
(line) => `<mj-text color="#757575" font-family="helvetica, sans-serif" font-size="12px" font-weight="300" line-height="16px" padding-top="0" padding-bottom="0px">${escapeXml(line)}</mj-text>`
|
|
@@ -1180,12 +1007,15 @@ function buildMjml(data) {
|
|
|
1180
1007
|
</mj-body>
|
|
1181
1008
|
</mjml>`;
|
|
1182
1009
|
}
|
|
1183
|
-
var CHECK_PNG, BLURRED_TESTS, TREND_UP, TREND_NEUTRAL;
|
|
1010
|
+
var escapeXml, CHECK_PNG, BLURRED_TESTS, TREND_UP, TREND_NEUTRAL;
|
|
1184
1011
|
var init_template = __esm({
|
|
1185
1012
|
"src/reports/maintenance-email/template.ts"() {
|
|
1186
1013
|
"use strict";
|
|
1187
1014
|
init_copy();
|
|
1188
1015
|
init_assets();
|
|
1016
|
+
init_html();
|
|
1017
|
+
init_url();
|
|
1018
|
+
escapeXml = escapeHtml;
|
|
1189
1019
|
CHECK_PNG = `cid:${CHECK_CID}`;
|
|
1190
1020
|
BLURRED_TESTS = `cid:${BLURRED_CID}`;
|
|
1191
1021
|
TREND_UP = "#2E7D32";
|
|
@@ -1272,6 +1102,11 @@ var init_render = __esm({
|
|
|
1272
1102
|
});
|
|
1273
1103
|
|
|
1274
1104
|
// src/reports/airtable/attachments.ts
|
|
1105
|
+
function looksLikeHtml(bytes) {
|
|
1106
|
+
const start = bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191 ? 3 : 0;
|
|
1107
|
+
const head = Buffer.from(bytes.slice(start, start + 64)).toString("ascii").replace(/^[\s]+/, "").toLowerCase();
|
|
1108
|
+
return head.startsWith("<!doctype html") || head.startsWith("<html") || head.startsWith("<head");
|
|
1109
|
+
}
|
|
1275
1110
|
async function fetchAttachmentBytes(url) {
|
|
1276
1111
|
const res = await fetch(url);
|
|
1277
1112
|
if (!res.ok) {
|
|
@@ -1281,7 +1116,14 @@ async function fetchAttachmentBytes(url) {
|
|
|
1281
1116
|
}
|
|
1282
1117
|
const contentType = res.headers.get("content-type") ?? "application/octet-stream";
|
|
1283
1118
|
const ab = await res.arrayBuffer();
|
|
1284
|
-
|
|
1119
|
+
const bytes = new Uint8Array(ab);
|
|
1120
|
+
const isImageType = contentType.toLowerCase().startsWith("image/");
|
|
1121
|
+
if (!isImageType && looksLikeHtml(bytes)) {
|
|
1122
|
+
throw new Error(
|
|
1123
|
+
`Airtable attachment did not return image data (content-type="${contentType}", body looks like an HTML page \u2014 the signed URL may have expired) (url=${url})`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
return { bytes, contentType };
|
|
1285
1127
|
}
|
|
1286
1128
|
async function uploadAttachment(recordId, fieldName, body, filename, contentType) {
|
|
1287
1129
|
const apiKey = process.env.AIRTABLE_PAT;
|
|
@@ -1342,10 +1184,31 @@ var init_resend = __esm({
|
|
|
1342
1184
|
}
|
|
1343
1185
|
});
|
|
1344
1186
|
|
|
1187
|
+
// src/reports/send/idempotency.ts
|
|
1188
|
+
function isIdempotencyConflict(err) {
|
|
1189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1190
|
+
if (/idempotency key has been used/i.test(message)) return true;
|
|
1191
|
+
const e = err;
|
|
1192
|
+
if (e.name === "invalid_idempotent_request") return true;
|
|
1193
|
+
if (e.statusCode === 409) return true;
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
var init_idempotency = __esm({
|
|
1197
|
+
"src/reports/send/idempotency.ts"() {
|
|
1198
|
+
"use strict";
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1345
1202
|
// src/alerts/digest-collectors.ts
|
|
1346
1203
|
function dashboardUrl(baseUrl, siteName) {
|
|
1347
1204
|
return `${baseUrl.replace(/\/$/, "")}/s/${siteSlug(siteName)}`;
|
|
1348
1205
|
}
|
|
1206
|
+
function gitHubSignalsStale(swept, now) {
|
|
1207
|
+
if (swept === null) return true;
|
|
1208
|
+
const ageMs = now.getTime() - Date.parse(swept);
|
|
1209
|
+
if (!Number.isFinite(ageMs)) return true;
|
|
1210
|
+
return ageMs > GITHUB_SIGNALS_STALE_DAYS * MS_PER_DAY2;
|
|
1211
|
+
}
|
|
1349
1212
|
function collectVulnAlerts(sites, baseUrl) {
|
|
1350
1213
|
const items = [];
|
|
1351
1214
|
for (const s of sites) {
|
|
@@ -1403,42 +1266,48 @@ function collectDeliveryFailures(reports, sitesById, baseUrl) {
|
|
|
1403
1266
|
}
|
|
1404
1267
|
return items;
|
|
1405
1268
|
}
|
|
1406
|
-
function
|
|
1269
|
+
function collectRenovateAlerts(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
1407
1270
|
const items = [];
|
|
1408
|
-
for (const
|
|
1271
|
+
for (const s of sites) {
|
|
1272
|
+
if (gitHubSignalsStale(s.githubSignalsAt, now)) continue;
|
|
1273
|
+
const n = s.renovateFailingCis ?? 0;
|
|
1274
|
+
if (n <= 0) continue;
|
|
1409
1275
|
items.push({
|
|
1410
|
-
key: `renovate:${
|
|
1276
|
+
key: `renovate:${s.id}`,
|
|
1411
1277
|
kind: "renovate",
|
|
1412
|
-
siteName:
|
|
1413
|
-
title:
|
|
1414
|
-
url:
|
|
1278
|
+
siteName: s.name,
|
|
1279
|
+
title: `${n} Renovate ${n === 1 ? "PR" : "PRs"} failing CI`,
|
|
1280
|
+
url: dashboardUrl(baseUrl, s.name),
|
|
1415
1281
|
severity: "warning",
|
|
1416
|
-
metric:
|
|
1282
|
+
metric: n
|
|
1417
1283
|
});
|
|
1418
1284
|
}
|
|
1419
|
-
|
|
1285
|
+
return items;
|
|
1286
|
+
}
|
|
1287
|
+
function collectCiAlerts(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
1288
|
+
const items = [];
|
|
1289
|
+
for (const s of sites) {
|
|
1290
|
+
if (gitHubSignalsStale(s.githubSignalsAt, now)) continue;
|
|
1291
|
+
if (s.defaultBranchCi !== "failing") continue;
|
|
1420
1292
|
items.push({
|
|
1421
|
-
key:
|
|
1422
|
-
kind: "
|
|
1423
|
-
siteName:
|
|
1424
|
-
title:
|
|
1293
|
+
key: `ci:${s.id}`,
|
|
1294
|
+
kind: "ci",
|
|
1295
|
+
siteName: s.name,
|
|
1296
|
+
title: "Default-branch CI failing",
|
|
1297
|
+
url: dashboardUrl(baseUrl, s.name),
|
|
1425
1298
|
severity: "warning",
|
|
1426
|
-
metric:
|
|
1299
|
+
metric: 1
|
|
1427
1300
|
});
|
|
1428
1301
|
}
|
|
1429
1302
|
return items;
|
|
1430
1303
|
}
|
|
1431
|
-
|
|
1432
|
-
const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();
|
|
1433
|
-
if (!token) return void 0;
|
|
1434
|
-
return makeGitHub({ token }).openPullRequests;
|
|
1435
|
-
}
|
|
1436
|
-
var LIGHTHOUSE_FLOOR, LIGHTHOUSE_CATEGORIES2;
|
|
1304
|
+
var GITHUB_SIGNALS_STALE_DAYS, MS_PER_DAY2, LIGHTHOUSE_FLOOR, LIGHTHOUSE_CATEGORIES2;
|
|
1437
1305
|
var init_digest_collectors = __esm({
|
|
1438
1306
|
"src/alerts/digest-collectors.ts"() {
|
|
1439
1307
|
"use strict";
|
|
1440
1308
|
init_websites();
|
|
1441
|
-
|
|
1309
|
+
GITHUB_SIGNALS_STALE_DAYS = 3;
|
|
1310
|
+
MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
|
|
1442
1311
|
LIGHTHOUSE_FLOOR = 75;
|
|
1443
1312
|
LIGHTHOUSE_CATEGORIES2 = [
|
|
1444
1313
|
{ field: "pScore", slug: "performance", label: "Performance" },
|
|
@@ -1449,48 +1318,6 @@ var init_digest_collectors = __esm({
|
|
|
1449
1318
|
}
|
|
1450
1319
|
});
|
|
1451
1320
|
|
|
1452
|
-
// src/alerts/renovate.ts
|
|
1453
|
-
function isRenovatePR(pr) {
|
|
1454
|
-
return RENOVATE_HEAD_PREFIXES.some((p) => pr.headRef.startsWith(p));
|
|
1455
|
-
}
|
|
1456
|
-
function isFailingRenovatePR(pr) {
|
|
1457
|
-
return isRenovatePR(pr) && pr.ciState === "failing";
|
|
1458
|
-
}
|
|
1459
|
-
function siteLabel2(site) {
|
|
1460
|
-
const display = site.meta?.["displayName"];
|
|
1461
|
-
if (typeof display === "string" && display.length > 0) return display;
|
|
1462
|
-
if (typeof site.name === "string" && site.name.length > 0) return site.name;
|
|
1463
|
-
return "unknown";
|
|
1464
|
-
}
|
|
1465
|
-
async function collectRenovateFailures(sites, probe) {
|
|
1466
|
-
const findings = [];
|
|
1467
|
-
const skipped = [];
|
|
1468
|
-
for (const site of sites) {
|
|
1469
|
-
const repo = site.gitRepo;
|
|
1470
|
-
if (!repo) continue;
|
|
1471
|
-
let prs;
|
|
1472
|
-
try {
|
|
1473
|
-
prs = await probe(repo);
|
|
1474
|
-
} catch {
|
|
1475
|
-
skipped.push(repo);
|
|
1476
|
-
continue;
|
|
1477
|
-
}
|
|
1478
|
-
for (const pr of prs) {
|
|
1479
|
-
if (isFailingRenovatePR(pr)) {
|
|
1480
|
-
findings.push({ site: siteLabel2(site), repo, pr });
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
return { findings, skipped };
|
|
1485
|
-
}
|
|
1486
|
-
var RENOVATE_HEAD_PREFIXES;
|
|
1487
|
-
var init_renovate = __esm({
|
|
1488
|
-
"src/alerts/renovate.ts"() {
|
|
1489
|
-
"use strict";
|
|
1490
|
-
RENOVATE_HEAD_PREFIXES = ["renovate/", "renovate-"];
|
|
1491
|
-
}
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
1321
|
// src/alerts/digest-state.ts
|
|
1495
1322
|
function diffAttention(items, prior, today) {
|
|
1496
1323
|
const tagged = [];
|
|
@@ -1563,9 +1390,6 @@ __export(digest_exports, {
|
|
|
1563
1390
|
renderDigestHtml: () => renderDigestHtml,
|
|
1564
1391
|
runDigest: () => runDigest
|
|
1565
1392
|
});
|
|
1566
|
-
function esc(s) {
|
|
1567
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1568
|
-
}
|
|
1569
1393
|
function readySection(items) {
|
|
1570
1394
|
const heading = `<h2 style="color:${RED2};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px">Ready for your yes</h2>`;
|
|
1571
1395
|
if (items.length === 0) {
|
|
@@ -1573,11 +1397,11 @@ function readySection(items) {
|
|
|
1573
1397
|
}
|
|
1574
1398
|
const rows = items.map((it) => {
|
|
1575
1399
|
const safeUrl = it.dashboardUrl.startsWith("https://") ? it.dashboardUrl : void 0;
|
|
1576
|
-
const link = safeUrl ? `<a href="${
|
|
1400
|
+
const link = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">review & approve</a>` : `review & approve`;
|
|
1577
1401
|
return `
|
|
1578
1402
|
<tr>
|
|
1579
1403
|
<td style="color:${GREY2};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">
|
|
1580
|
-
<strong style="color:#222">${
|
|
1404
|
+
<strong style="color:#222">${escapeHtml(it.siteName)}</strong> \u2014 ${escapeHtml(it.reportType)} (${escapeHtml(it.period)})
|
|
1581
1405
|
\u2014 ${link}
|
|
1582
1406
|
</td>
|
|
1583
1407
|
</tr>`;
|
|
@@ -1608,7 +1432,7 @@ function attentionSection(items) {
|
|
|
1608
1432
|
);
|
|
1609
1433
|
const rows = sorted.map((it) => {
|
|
1610
1434
|
const safeUrl = it.url?.startsWith("https://") ? it.url : void 0;
|
|
1611
|
-
const titleHtml = safeUrl ? `<a href="${
|
|
1435
|
+
const titleHtml = safeUrl ? `<a href="${escapeHtml(safeUrl)}" style="${ANCHOR_STYLE}">${escapeHtml(it.title)}</a>` : escapeHtml(it.title);
|
|
1612
1436
|
return `
|
|
1613
1437
|
<tr>
|
|
1614
1438
|
<td style="color:${GREY2};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px">${attentionBadge(it.status)}${titleHtml}</td>
|
|
@@ -1616,7 +1440,7 @@ function attentionSection(items) {
|
|
|
1616
1440
|
}).join("");
|
|
1617
1441
|
return `
|
|
1618
1442
|
<tr>
|
|
1619
|
-
<td style="color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px">${
|
|
1443
|
+
<td style="color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px">${escapeHtml(siteName)}</td>
|
|
1620
1444
|
</tr>
|
|
1621
1445
|
${rows}`;
|
|
1622
1446
|
}).join("");
|
|
@@ -1625,18 +1449,8 @@ function attentionSection(items) {
|
|
|
1625
1449
|
function digestDateKey(d) {
|
|
1626
1450
|
return d.toISOString().slice(0, 10);
|
|
1627
1451
|
}
|
|
1628
|
-
function isIdempotencyConflict(err) {
|
|
1629
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1630
|
-
if (/idempotency key has been used/i.test(message)) return true;
|
|
1631
|
-
const e = err;
|
|
1632
|
-
if (e.name === "invalid_idempotent_request") return true;
|
|
1633
|
-
if (e.statusCode === 409) return true;
|
|
1634
|
-
return false;
|
|
1635
|
-
}
|
|
1636
1452
|
async function listPendingApproval(base) {
|
|
1637
|
-
return (await listAllReports(base)).filter(
|
|
1638
|
-
(r) => r.draftReady && !r.approvedToSend && r.sentAt === null
|
|
1639
|
-
);
|
|
1453
|
+
return (await listAllReports(base)).filter(isPendingApproval);
|
|
1640
1454
|
}
|
|
1641
1455
|
function runCollector(label, fn) {
|
|
1642
1456
|
try {
|
|
@@ -1646,33 +1460,17 @@ function runCollector(label, fn) {
|
|
|
1646
1460
|
return [];
|
|
1647
1461
|
}
|
|
1648
1462
|
}
|
|
1649
|
-
async function runCollectorAsync(label, fn) {
|
|
1650
|
-
try {
|
|
1651
|
-
return await fn();
|
|
1652
|
-
} catch (e) {
|
|
1653
|
-
console.warn(`\u26A0 attention collector "${label}" failed: ${e.message}`);
|
|
1654
|
-
return [];
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
1463
|
async function collectAttention(deps) {
|
|
1658
1464
|
const reports = deps.reports ?? await listAllReports(deps.base);
|
|
1659
1465
|
const websites = deps.websites ?? await listWebsites(deps.base);
|
|
1466
|
+
const now = deps.now ?? /* @__PURE__ */ new Date();
|
|
1660
1467
|
const sitesById = new Map(websites.map((w) => [w.id, w]));
|
|
1661
|
-
const renovate = deps.renovateProbe ? await runCollectorAsync("renovate", async () => {
|
|
1662
|
-
const sites = websites.map((w) => ({
|
|
1663
|
-
path: "",
|
|
1664
|
-
name: w.name,
|
|
1665
|
-
meta: {},
|
|
1666
|
-
...w.gitRepo ? { gitRepo: w.gitRepo } : {}
|
|
1667
|
-
}));
|
|
1668
|
-
const result = await collectRenovateFailures(sites, deps.renovateProbe);
|
|
1669
|
-
return renovateFindingsToAttention(result);
|
|
1670
|
-
}) : [];
|
|
1671
1468
|
return [
|
|
1672
1469
|
...runCollector("vuln", () => collectVulnAlerts(websites, deps.baseUrl)),
|
|
1673
1470
|
...runCollector("delivery", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),
|
|
1674
1471
|
...runCollector("lighthouse", () => collectLighthouseAlerts(websites, deps.baseUrl)),
|
|
1675
|
-
...renovate
|
|
1472
|
+
...runCollector("renovate", () => collectRenovateAlerts(websites, deps.baseUrl, now)),
|
|
1473
|
+
...runCollector("ci", () => collectCiAlerts(websites, deps.baseUrl, now))
|
|
1676
1474
|
];
|
|
1677
1475
|
}
|
|
1678
1476
|
async function runDigest(options) {
|
|
@@ -1682,25 +1480,26 @@ async function runDigest(options) {
|
|
|
1682
1480
|
const reports = await listAllReports(base);
|
|
1683
1481
|
const websites = await listWebsites(base);
|
|
1684
1482
|
const sites = new Map(websites.map((w) => [w.id, w]));
|
|
1685
|
-
const pending = reports.filter(
|
|
1483
|
+
const pending = reports.filter(isPendingApproval);
|
|
1686
1484
|
const readyForYourYes = [];
|
|
1485
|
+
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
1687
1486
|
for (const r of pending) {
|
|
1688
1487
|
const site = sites.get(r.siteId);
|
|
1689
1488
|
if (!site) continue;
|
|
1489
|
+
const slug = siteSlug(site.name);
|
|
1690
1490
|
readyForYourYes.push({
|
|
1691
1491
|
siteName: site.name,
|
|
1692
1492
|
reportType: r.reportType,
|
|
1693
1493
|
period: r.period ?? "\u2014",
|
|
1694
|
-
dashboardUrl: `${
|
|
1494
|
+
dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl
|
|
1695
1495
|
});
|
|
1696
1496
|
}
|
|
1697
|
-
const renovateProbe = buildRenovateProbe();
|
|
1698
1497
|
const collected = await collectAttention({
|
|
1699
1498
|
base,
|
|
1700
1499
|
baseUrl: options.baseUrl,
|
|
1701
1500
|
websites,
|
|
1702
1501
|
reports,
|
|
1703
|
-
|
|
1502
|
+
now: today
|
|
1704
1503
|
});
|
|
1705
1504
|
const prior = await readDigestState(base);
|
|
1706
1505
|
const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));
|
|
@@ -1781,9 +1580,10 @@ var init_digest = __esm({
|
|
|
1781
1580
|
init_reports();
|
|
1782
1581
|
init_websites();
|
|
1783
1582
|
init_resend();
|
|
1583
|
+
init_idempotency();
|
|
1784
1584
|
init_digest_collectors();
|
|
1785
|
-
init_renovate();
|
|
1786
1585
|
init_digest_state();
|
|
1586
|
+
init_html();
|
|
1787
1587
|
GREY2 = "#757575";
|
|
1788
1588
|
RED2 = "#C00";
|
|
1789
1589
|
ANCHOR_STYLE = `color:${RED2};font-family:helvetica,sans-serif`;
|
|
@@ -1892,30 +1692,6 @@ async function sendOne(client, base, site, report) {
|
|
|
1892
1692
|
`Report ${report.reportId} has no Lighthouse scores \u2014 all four cells (Lighthouse \u2014 Performance / Accessibility / Best Practices / SEO) must be numeric on the Reports row; one non-numeric or blank cell nulls all four`
|
|
1893
1693
|
);
|
|
1894
1694
|
}
|
|
1895
|
-
const original = await fetchAttachmentBytes(site.headerImage.url);
|
|
1896
|
-
const header = await prepareHeaderImage(original.bytes);
|
|
1897
|
-
const bundled = await loadBundledImages();
|
|
1898
|
-
const slug = siteSlug(site.name);
|
|
1899
|
-
const cidName = `${slug}-header`;
|
|
1900
|
-
const { html } = await renderReportHtml({
|
|
1901
|
-
siteName: site.name,
|
|
1902
|
-
siteUrl: site.url,
|
|
1903
|
-
reportType: report.reportType,
|
|
1904
|
-
completedOn: report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date(),
|
|
1905
|
-
lighthouse: report.lighthouse,
|
|
1906
|
-
gaUsersCurrent: report.gaUsersCurrent ?? void 0,
|
|
1907
|
-
gaUsersPrevious: report.gaUsersPrevious ?? void 0,
|
|
1908
|
-
searchPosition: report.searchFoundPage1 && report.searchPosition !== null ? report.searchPosition : void 0,
|
|
1909
|
-
lastTestedDate: report.lastTestedDate ? new Date(report.lastTestedDate) : null,
|
|
1910
|
-
commentary: report.commentary,
|
|
1911
|
-
copy: resolveCopy(site),
|
|
1912
|
-
headerImageCid: cidName,
|
|
1913
|
-
headerWidth: header.displayWidth,
|
|
1914
|
-
headerHeight: header.displayHeight,
|
|
1915
|
-
headerBgColor: header.placeholderColor
|
|
1916
|
-
});
|
|
1917
|
-
const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();
|
|
1918
|
-
const subject = report.subjectOverride ?? `${site.name} \u2014 ${monthYear(reportDate)} ${report.reportType} Report`;
|
|
1919
1695
|
const explicitTo = parseAddresses(site.reportRecipientsTo);
|
|
1920
1696
|
const fallbackTo = parseAddresses(site.pointOfContact);
|
|
1921
1697
|
const to = explicitTo ?? fallbackTo ?? [];
|
|
@@ -1941,6 +1717,30 @@ async function sendOne(client, base, site, report) {
|
|
|
1941
1717
|
}
|
|
1942
1718
|
}
|
|
1943
1719
|
}
|
|
1720
|
+
const original = await fetchAttachmentBytes(site.headerImage.url);
|
|
1721
|
+
const header = await prepareHeaderImage(original.bytes);
|
|
1722
|
+
const bundled = await loadBundledImages();
|
|
1723
|
+
const slug = siteSlug(site.name);
|
|
1724
|
+
const cidName = `${slug}-header`;
|
|
1725
|
+
const { html } = await renderReportHtml({
|
|
1726
|
+
siteName: site.name,
|
|
1727
|
+
siteUrl: site.url,
|
|
1728
|
+
reportType: report.reportType,
|
|
1729
|
+
completedOn: report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date(),
|
|
1730
|
+
lighthouse: report.lighthouse,
|
|
1731
|
+
gaUsersCurrent: report.gaUsersCurrent ?? void 0,
|
|
1732
|
+
gaUsersPrevious: report.gaUsersPrevious ?? void 0,
|
|
1733
|
+
searchPosition: report.searchFoundPage1 && report.searchPosition !== null ? report.searchPosition : void 0,
|
|
1734
|
+
lastTestedDate: report.lastTestedDate ? new Date(report.lastTestedDate) : null,
|
|
1735
|
+
commentary: report.commentary,
|
|
1736
|
+
copy: resolveCopy(site),
|
|
1737
|
+
headerImageCid: cidName,
|
|
1738
|
+
headerWidth: header.displayWidth,
|
|
1739
|
+
headerHeight: header.displayHeight,
|
|
1740
|
+
headerBgColor: header.placeholderColor
|
|
1741
|
+
});
|
|
1742
|
+
const reportDate = report.completedOn ? new Date(report.completedOn) : /* @__PURE__ */ new Date();
|
|
1743
|
+
const subject = report.subjectOverride ?? `${site.name} \u2014 ${monthYear(reportDate)} ${report.reportType} Report`;
|
|
1944
1744
|
const payload = {
|
|
1945
1745
|
from: FROM_ADDRESS2,
|
|
1946
1746
|
to,
|
|
@@ -1976,7 +1776,17 @@ async function sendOne(client, base, site, report) {
|
|
|
1976
1776
|
idempotencyKey: `report:${report.id}`
|
|
1977
1777
|
};
|
|
1978
1778
|
if (cc) payload.cc = cc;
|
|
1979
|
-
|
|
1779
|
+
let result;
|
|
1780
|
+
try {
|
|
1781
|
+
result = await client.send(payload);
|
|
1782
|
+
} catch (err) {
|
|
1783
|
+
if (isIdempotencyConflict(err)) {
|
|
1784
|
+
await stampSent(base, report.id, /* @__PURE__ */ new Date(), null);
|
|
1785
|
+
console.log(`\u21BB already sent (idempotency conflict), stamped: ${report.reportId}`);
|
|
1786
|
+
return "idempotent-conflict";
|
|
1787
|
+
}
|
|
1788
|
+
throw err;
|
|
1789
|
+
}
|
|
1980
1790
|
await stampSent(base, report.id, /* @__PURE__ */ new Date(), result.messageId);
|
|
1981
1791
|
return result.messageId;
|
|
1982
1792
|
}
|
|
@@ -2016,6 +1826,7 @@ var init_orchestrate = __esm({
|
|
|
2016
1826
|
init_assets();
|
|
2017
1827
|
init_header_image();
|
|
2018
1828
|
init_resend();
|
|
1829
|
+
init_idempotency();
|
|
2019
1830
|
FROM_ADDRESS2 = "Reddoor Reports <reports@reddoorla.com>";
|
|
2020
1831
|
REPLY_TO = "info@reddoorla.com";
|
|
2021
1832
|
MONTHS2 = [
|
|
@@ -2045,8 +1856,85 @@ import { cac } from "cac";
|
|
|
2045
1856
|
import { resolve as resolve2 } from "path";
|
|
2046
1857
|
import { Listr } from "listr2";
|
|
2047
1858
|
|
|
2048
|
-
// src/audits/
|
|
2049
|
-
|
|
1859
|
+
// src/audits/util/spawn.ts
|
|
1860
|
+
import { spawn } from "child_process";
|
|
1861
|
+
import { StringDecoder } from "string_decoder";
|
|
1862
|
+
var TRUNCATION_MARKER = "\n\u2026[output truncated]";
|
|
1863
|
+
function makeSpawn(internals = {}) {
|
|
1864
|
+
const spawnImpl = internals.spawnImpl ?? spawn;
|
|
1865
|
+
const killImpl = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));
|
|
1866
|
+
const killGraceMs = internals.killGraceMs ?? 5e3;
|
|
1867
|
+
const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;
|
|
1868
|
+
return (cmd, args, opts = {}) => new Promise((resolve12, reject) => {
|
|
1869
|
+
const streaming = opts.streaming === true;
|
|
1870
|
+
const child = spawnImpl(cmd, [...args], {
|
|
1871
|
+
cwd: opts.cwd,
|
|
1872
|
+
env: opts.env ?? process.env,
|
|
1873
|
+
stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
|
|
1874
|
+
// Detach ONLY when a timeout can fire: the child then leads its own
|
|
1875
|
+
// process group, so the timeout can kill the WHOLE tree (vite, and
|
|
1876
|
+
// Chromium under lhci/playwright) via process.kill(-pid), not just the
|
|
1877
|
+
// npx/pnpm wrapper. Without it, killing the wrapper orphaned the
|
|
1878
|
+
// grandchildren — a zombie vite squatting its port, Chrome left running.
|
|
1879
|
+
// We do NOT detach timeout-less streaming calls (pnpm install/up):
|
|
1880
|
+
// detaching gains nothing there (no timeout → no group-kill) and would
|
|
1881
|
+
// break terminal Ctrl-C, which only reaches the foreground group — i.e.
|
|
1882
|
+
// it would re-orphan the very children this guards. We never unref() the
|
|
1883
|
+
// child since we still await it.
|
|
1884
|
+
detached: opts.timeoutMs !== void 0
|
|
1885
|
+
});
|
|
1886
|
+
const cap = (acc, chunk) => {
|
|
1887
|
+
if (acc.length >= maxOutputBytes) return acc;
|
|
1888
|
+
const next = acc + chunk;
|
|
1889
|
+
return next.length > maxOutputBytes ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER : next;
|
|
1890
|
+
};
|
|
1891
|
+
let stdout = "";
|
|
1892
|
+
let stderr = "";
|
|
1893
|
+
const outDecoder = new StringDecoder("utf-8");
|
|
1894
|
+
const errDecoder = new StringDecoder("utf-8");
|
|
1895
|
+
if (!streaming) {
|
|
1896
|
+
child.stdout?.on(
|
|
1897
|
+
"data",
|
|
1898
|
+
(chunk) => stdout = cap(stdout, outDecoder.write(chunk))
|
|
1899
|
+
);
|
|
1900
|
+
child.stderr?.on(
|
|
1901
|
+
"data",
|
|
1902
|
+
(chunk) => stderr = cap(stderr, errDecoder.write(chunk))
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
const killGroup = (sig) => {
|
|
1906
|
+
if (child.pid === void 0) return;
|
|
1907
|
+
try {
|
|
1908
|
+
killImpl(-child.pid, sig);
|
|
1909
|
+
} catch {
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
let killTimer;
|
|
1913
|
+
const timer = opts.timeoutMs ? setTimeout(() => {
|
|
1914
|
+
killGroup("SIGTERM");
|
|
1915
|
+
killTimer = setTimeout(() => killGroup("SIGKILL"), killGraceMs);
|
|
1916
|
+
killTimer.unref();
|
|
1917
|
+
reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
|
|
1918
|
+
}, opts.timeoutMs) : void 0;
|
|
1919
|
+
const clearTimers = () => {
|
|
1920
|
+
if (timer) clearTimeout(timer);
|
|
1921
|
+
if (killTimer) clearTimeout(killTimer);
|
|
1922
|
+
};
|
|
1923
|
+
child.on("error", (err) => {
|
|
1924
|
+
clearTimers();
|
|
1925
|
+
reject(err);
|
|
1926
|
+
});
|
|
1927
|
+
child.on("close", (code) => {
|
|
1928
|
+
clearTimers();
|
|
1929
|
+
if (!streaming) {
|
|
1930
|
+
stdout = cap(stdout, outDecoder.end());
|
|
1931
|
+
stderr = cap(stderr, errDecoder.end());
|
|
1932
|
+
}
|
|
1933
|
+
resolve12({ code: code ?? -1, stdout, stderr });
|
|
1934
|
+
});
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
var defaultSpawn = makeSpawn();
|
|
2050
1938
|
|
|
2051
1939
|
// src/audits/deps.ts
|
|
2052
1940
|
import { readFile } from "fs/promises";
|
|
@@ -2054,7 +1942,7 @@ import { join as join3 } from "path";
|
|
|
2054
1942
|
|
|
2055
1943
|
// src/util/site.ts
|
|
2056
1944
|
function siteLabel(site) {
|
|
2057
|
-
return site.name
|
|
1945
|
+
return site.name || site.path;
|
|
2058
1946
|
}
|
|
2059
1947
|
|
|
2060
1948
|
// src/configs/baseline-versions.ts
|
|
@@ -2096,9 +1984,6 @@ var baselineVersions = {
|
|
|
2096
1984
|
"@zerodevx/svelte-img": "^2.1.2"
|
|
2097
1985
|
};
|
|
2098
1986
|
|
|
2099
|
-
// src/audits/deps.ts
|
|
2100
|
-
init_spawn();
|
|
2101
|
-
|
|
2102
1987
|
// src/audits/deps-outdated.ts
|
|
2103
1988
|
import { stat } from "fs/promises";
|
|
2104
1989
|
import { join as join2 } from "path";
|
|
@@ -2278,7 +2163,6 @@ async function lintAudit(ctx) {
|
|
|
2278
2163
|
}
|
|
2279
2164
|
|
|
2280
2165
|
// src/audits/security.ts
|
|
2281
|
-
init_spawn();
|
|
2282
2166
|
function classify(v) {
|
|
2283
2167
|
if (v.critical > 0 || v.high > 0) return "fail";
|
|
2284
2168
|
if (v.moderate > 0 || v.low > 0) return "warn";
|
|
@@ -2453,9 +2337,6 @@ var lighthouseConfig = {
|
|
|
2453
2337
|
}
|
|
2454
2338
|
};
|
|
2455
2339
|
|
|
2456
|
-
// src/audits/lighthouse.ts
|
|
2457
|
-
init_spawn();
|
|
2458
|
-
|
|
2459
2340
|
// src/audits/util/site-config.ts
|
|
2460
2341
|
import { readFile as readFile3 } from "fs/promises";
|
|
2461
2342
|
import { join as join5 } from "path";
|
|
@@ -2557,7 +2438,8 @@ function categoryFromAssertion(a) {
|
|
|
2557
2438
|
return colonIdx >= 0 ? a.name.slice(colonIdx + 1) : a.name;
|
|
2558
2439
|
}
|
|
2559
2440
|
function messageForAssertion(a) {
|
|
2560
|
-
|
|
2441
|
+
const actual = typeof a.actual === "number" ? a.actual.toFixed(2) : "n/a";
|
|
2442
|
+
return `${a.name} ${a.operator} ${a.expected} (actual: ${actual})`;
|
|
2561
2443
|
}
|
|
2562
2444
|
async function parseLhciResults(resultsDir, label, raw) {
|
|
2563
2445
|
const manifest = await readLhrEntries(resultsDir);
|
|
@@ -2724,7 +2606,6 @@ var playwrightA11yConfig = defineConfig({
|
|
|
2724
2606
|
});
|
|
2725
2607
|
|
|
2726
2608
|
// src/audits/a11y.ts
|
|
2727
|
-
init_spawn();
|
|
2728
2609
|
var RESULTS_REL = ".reddoor-a11y/results.json";
|
|
2729
2610
|
async function readJsonMaybe2(path) {
|
|
2730
2611
|
try {
|
|
@@ -2932,7 +2813,7 @@ function timedSpawn(timeoutMs) {
|
|
|
2932
2813
|
async function runOneAudit(site, name) {
|
|
2933
2814
|
if (!(name in REGISTRY)) throw new Error(`unknown audit: ${name}`);
|
|
2934
2815
|
const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
|
|
2935
|
-
const label = site.name
|
|
2816
|
+
const label = site.name || site.path;
|
|
2936
2817
|
try {
|
|
2937
2818
|
return await REGISTRY[name]({ site, spawn: spawn2 });
|
|
2938
2819
|
} catch (err) {
|
|
@@ -2959,11 +2840,12 @@ import { resolve, extname } from "path";
|
|
|
2959
2840
|
// src/inventory/local.ts
|
|
2960
2841
|
import { basename } from "path";
|
|
2961
2842
|
function localPath(path, opts = {}) {
|
|
2962
|
-
const site = { path, name: opts.name
|
|
2843
|
+
const site = { path, name: opts.name || basename(path) };
|
|
2963
2844
|
return async () => [site];
|
|
2964
2845
|
}
|
|
2965
2846
|
|
|
2966
2847
|
// src/inventory/json.ts
|
|
2848
|
+
init_url();
|
|
2967
2849
|
import { readFile as readFile6 } from "fs/promises";
|
|
2968
2850
|
import { isAbsolute } from "path";
|
|
2969
2851
|
function validate(raw) {
|
|
@@ -2987,7 +2869,15 @@ function validate(raw) {
|
|
|
2987
2869
|
if (typeof e.name === "string") site.name = e.name;
|
|
2988
2870
|
if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
|
|
2989
2871
|
if (typeof e.gitRepo === "string") site.gitRepo = e.gitRepo;
|
|
2990
|
-
if (typeof e.deployedUrl === "string")
|
|
2872
|
+
if (typeof e.deployedUrl === "string") {
|
|
2873
|
+
if (isHttpUrl(e.deployedUrl)) {
|
|
2874
|
+
site.deployedUrl = e.deployedUrl;
|
|
2875
|
+
} else {
|
|
2876
|
+
console.warn(
|
|
2877
|
+
`[inventory] entry ${i}: ignoring deployedUrl that is not http(s): ${JSON.stringify(e.deployedUrl)}`
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2991
2881
|
if (typeof e.meta === "object" && e.meta !== null) {
|
|
2992
2882
|
site.meta = e.meta;
|
|
2993
2883
|
}
|
|
@@ -2996,7 +2886,15 @@ function validate(raw) {
|
|
|
2996
2886
|
}
|
|
2997
2887
|
function fromJsonFile(path) {
|
|
2998
2888
|
return async () => {
|
|
2999
|
-
const
|
|
2889
|
+
const text = await readFile6(path, "utf-8");
|
|
2890
|
+
let raw;
|
|
2891
|
+
try {
|
|
2892
|
+
raw = JSON.parse(text);
|
|
2893
|
+
} catch (e) {
|
|
2894
|
+
throw new Error(`could not parse inventory file ${path}: ${e.message}`, {
|
|
2895
|
+
cause: e
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
3000
2898
|
return validate(raw);
|
|
3001
2899
|
};
|
|
3002
2900
|
}
|
|
@@ -3041,14 +2939,95 @@ async function resolveSites(input) {
|
|
|
3041
2939
|
}
|
|
3042
2940
|
|
|
3043
2941
|
// src/cli/fleet/clone-if-needed.ts
|
|
3044
|
-
init_spawn();
|
|
3045
2942
|
import { stat as stat2, readdir as readdir2, mkdir } from "fs/promises";
|
|
3046
2943
|
import { isAbsolute as isAbsolute2, join as join8 } from "path";
|
|
2944
|
+
|
|
2945
|
+
// src/util/git.ts
|
|
2946
|
+
import { execFile } from "child_process";
|
|
2947
|
+
import { promisify } from "util";
|
|
2948
|
+
var exec = promisify(execFile);
|
|
2949
|
+
async function git(cwd, args) {
|
|
2950
|
+
return exec("git", args, { cwd, env: process.env });
|
|
2951
|
+
}
|
|
2952
|
+
function branchName(recipe, when = /* @__PURE__ */ new Date()) {
|
|
2953
|
+
const compact = when.toISOString().replace(/[-:.]/g, "");
|
|
2954
|
+
return `maint/${recipe}-${compact}`;
|
|
2955
|
+
}
|
|
2956
|
+
async function currentBranch(cwd) {
|
|
2957
|
+
const { stdout } = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2958
|
+
return stdout.trim();
|
|
2959
|
+
}
|
|
2960
|
+
async function isWorkingTreeClean(cwd) {
|
|
2961
|
+
const { stdout } = await git(cwd, ["status", "--porcelain"]);
|
|
2962
|
+
return stdout.trim().length === 0;
|
|
2963
|
+
}
|
|
2964
|
+
async function createBranch(cwd, name) {
|
|
2965
|
+
await git(cwd, ["checkout", "-b", name]);
|
|
2966
|
+
}
|
|
2967
|
+
async function checkoutBranch(cwd, name) {
|
|
2968
|
+
await git(cwd, ["checkout", name]);
|
|
2969
|
+
}
|
|
2970
|
+
async function forceCheckoutBranch(cwd, name) {
|
|
2971
|
+
await git(cwd, ["checkout", "-f", name]);
|
|
2972
|
+
}
|
|
2973
|
+
async function deleteBranch(cwd, name) {
|
|
2974
|
+
await git(cwd, ["branch", "-D", name]);
|
|
2975
|
+
}
|
|
2976
|
+
async function stageAll(cwd) {
|
|
2977
|
+
await git(cwd, ["add", "-A"]);
|
|
2978
|
+
}
|
|
2979
|
+
async function listTrackedFiles(cwd) {
|
|
2980
|
+
const { stdout } = await git(cwd, ["ls-files"]);
|
|
2981
|
+
return stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2982
|
+
}
|
|
2983
|
+
async function removeFromIndex(cwd, paths) {
|
|
2984
|
+
if (paths.length === 0) return;
|
|
2985
|
+
await git(cwd, ["rm", "-r", "--cached", "--", ...paths]);
|
|
2986
|
+
}
|
|
2987
|
+
async function commit(cwd, message) {
|
|
2988
|
+
await stageAll(cwd);
|
|
2989
|
+
const { stdout: status } = await git(cwd, ["status", "--porcelain"]);
|
|
2990
|
+
if (status.trim().length === 0) return null;
|
|
2991
|
+
await git(cwd, ["commit", "-m", message]);
|
|
2992
|
+
const { stdout: sha } = await git(cwd, ["rev-parse", "HEAD"]);
|
|
2993
|
+
return sha.trim();
|
|
2994
|
+
}
|
|
2995
|
+
var OWNER_REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
2996
|
+
function isOwnerRepo(repo) {
|
|
2997
|
+
if (repo.includes("..")) return false;
|
|
2998
|
+
return OWNER_REPO_RE.test(repo);
|
|
2999
|
+
}
|
|
3000
|
+
function sameOwnerRepo(a, b) {
|
|
3001
|
+
const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);
|
|
3002
|
+
const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);
|
|
3003
|
+
if (na === null || nb === null) return false;
|
|
3004
|
+
return na.toLowerCase() === nb.toLowerCase();
|
|
3005
|
+
}
|
|
3006
|
+
function parseOwnerRepo(remoteUrl) {
|
|
3007
|
+
const trimmed = remoteUrl.trim().replace(/\.git$/, "").replace(/\/$/, "");
|
|
3008
|
+
const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);
|
|
3009
|
+
const path = scp ? scp[1] : trimmed.replace(/^https?:\/\/[^/]+\//, "");
|
|
3010
|
+
const segments = path.split("/").filter(Boolean);
|
|
3011
|
+
if (segments.length < 2) return null;
|
|
3012
|
+
return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
|
|
3013
|
+
}
|
|
3014
|
+
async function getRemoteUrl(cwd) {
|
|
3015
|
+
const { stdout } = await git(cwd, ["remote", "get-url", "origin"]);
|
|
3016
|
+
return stdout.trim();
|
|
3017
|
+
}
|
|
3018
|
+
async function push(cwd, branch) {
|
|
3019
|
+
await git(cwd, ["push", "-u", "origin", branch]);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/cli/fleet/clone-if-needed.ts
|
|
3047
3023
|
function deriveNameFromRepoUrl(repoUrl) {
|
|
3048
3024
|
const slash = repoUrl.split("/").pop() ?? repoUrl;
|
|
3049
3025
|
return slash.replace(/\.git$/, "");
|
|
3050
3026
|
}
|
|
3051
3027
|
function assertSafeName(name) {
|
|
3028
|
+
if (name.length === 0) {
|
|
3029
|
+
throw new Error("unsafe site name (empty)");
|
|
3030
|
+
}
|
|
3052
3031
|
if (isAbsolute2(name)) {
|
|
3053
3032
|
throw new Error(`unsafe site name (absolute path not allowed): ${name}`);
|
|
3054
3033
|
}
|
|
@@ -3076,6 +3055,24 @@ async function isNonEmptyDir(path) {
|
|
|
3076
3055
|
return false;
|
|
3077
3056
|
}
|
|
3078
3057
|
}
|
|
3058
|
+
function expectedRepoRef(site) {
|
|
3059
|
+
return site.gitRepo ?? site.repoUrl ?? void 0;
|
|
3060
|
+
}
|
|
3061
|
+
async function assertCheckoutMatches(site, path, spawn2) {
|
|
3062
|
+
const expected = expectedRepoRef(site);
|
|
3063
|
+
if (!expected) return;
|
|
3064
|
+
const r = await spawn2("git", ["-C", path, "remote", "get-url", "origin"], {
|
|
3065
|
+
timeoutMs: 3e4
|
|
3066
|
+
});
|
|
3067
|
+
if (r.code !== 0) return;
|
|
3068
|
+
const origin = r.stdout.trim();
|
|
3069
|
+
if (origin.length === 0) return;
|
|
3070
|
+
if (!sameOwnerRepo(origin, expected)) {
|
|
3071
|
+
throw new Error(
|
|
3072
|
+
`checkout at ${path} is the wrong repo: origin is ${JSON.stringify(origin)} but site expects ${JSON.stringify(expected)} (slug collision?) \u2014 refusing to reuse it`
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3079
3076
|
var GIT_REPO_RE = /^[\w.-]+\/[\w.-]+$/;
|
|
3080
3077
|
function resolveCloneUrl(site) {
|
|
3081
3078
|
if (site.repoUrl) return site.repoUrl;
|
|
@@ -3086,22 +3083,26 @@ function resolveCloneUrl(site) {
|
|
|
3086
3083
|
return `https://github.com/${site.gitRepo}.git`;
|
|
3087
3084
|
}
|
|
3088
3085
|
async function cloneIfNeeded(site, opts) {
|
|
3089
|
-
|
|
3086
|
+
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
3087
|
+
if (await isNonEmptyDir(site.path)) {
|
|
3088
|
+
await assertCheckoutMatches(site, site.path, spawn2);
|
|
3089
|
+
return site;
|
|
3090
|
+
}
|
|
3090
3091
|
const repoUrl = resolveCloneUrl(site);
|
|
3091
3092
|
if (!repoUrl) {
|
|
3092
3093
|
throw new Error(
|
|
3093
3094
|
`site path does not exist (${site.path}) and no repoUrl or gitRepo is set \u2014 cannot clone`
|
|
3094
3095
|
);
|
|
3095
3096
|
}
|
|
3096
|
-
const name = site.name
|
|
3097
|
+
const name = site.name || deriveNameFromRepoUrl(repoUrl);
|
|
3097
3098
|
assertSafeName(name);
|
|
3098
3099
|
assertSafeRepoUrl(repoUrl);
|
|
3099
3100
|
const target = join8(opts.workdir, name);
|
|
3100
3101
|
await mkdir(opts.workdir, { recursive: true });
|
|
3101
3102
|
if (await isNonEmptyDir(target)) {
|
|
3103
|
+
await assertCheckoutMatches(site, target, spawn2);
|
|
3102
3104
|
return { ...site, name, path: target };
|
|
3103
3105
|
}
|
|
3104
|
-
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
3105
3106
|
const result = await spawn2("git", ["clone", "--", repoUrl, target], {
|
|
3106
3107
|
cwd: opts.workdir,
|
|
3107
3108
|
timeoutMs: 5 * 6e4
|
|
@@ -3112,6 +3113,52 @@ async function cloneIfNeeded(site, opts) {
|
|
|
3112
3113
|
return { ...site, name, path: target };
|
|
3113
3114
|
}
|
|
3114
3115
|
|
|
3116
|
+
// src/cli/fleet/prepare-sites.ts
|
|
3117
|
+
async function prepareFleetSites(sites, opts) {
|
|
3118
|
+
const clone = opts.clone ?? cloneIfNeeded;
|
|
3119
|
+
const needsCheckout = opts.needsCheckout ?? (() => true);
|
|
3120
|
+
const settled = await Promise.all(
|
|
3121
|
+
sites.map(async (site) => {
|
|
3122
|
+
if (!needsCheckout(site)) return { ok: true, site };
|
|
3123
|
+
try {
|
|
3124
|
+
return { ok: true, site: await clone(site, { workdir: opts.workdir }) };
|
|
3125
|
+
} catch (e) {
|
|
3126
|
+
return { ok: false, site: site.name || site.path, reason: e.message };
|
|
3127
|
+
}
|
|
3128
|
+
})
|
|
3129
|
+
);
|
|
3130
|
+
const prepared = [];
|
|
3131
|
+
const skipped = [];
|
|
3132
|
+
for (const r of settled) {
|
|
3133
|
+
if (r.ok) prepared.push(r.site);
|
|
3134
|
+
else skipped.push({ site: r.site, reason: r.reason });
|
|
3135
|
+
}
|
|
3136
|
+
return { prepared, skipped };
|
|
3137
|
+
}
|
|
3138
|
+
function formatSkippedNotice(skipped) {
|
|
3139
|
+
if (skipped.length === 0) return null;
|
|
3140
|
+
const detail = skipped.map((s) => `${s.site} (${s.reason})`).join("; ");
|
|
3141
|
+
return `\u26A0 ${skipped.length} site(s) skipped (could not prepare): ${detail}`;
|
|
3142
|
+
}
|
|
3143
|
+
function appendSkipNotice(output, skipped) {
|
|
3144
|
+
const notice = formatSkippedNotice(skipped);
|
|
3145
|
+
return notice ? `${output}
|
|
3146
|
+
|
|
3147
|
+
${notice}` : output;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// src/cli/commands/audit.ts
|
|
3151
|
+
init_url();
|
|
3152
|
+
|
|
3153
|
+
// src/util/fleet-workdir.ts
|
|
3154
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3155
|
+
import { join as join9 } from "path";
|
|
3156
|
+
function fleetWorkdir() {
|
|
3157
|
+
const home = process.env.HOME?.trim();
|
|
3158
|
+
const base = home ? home : tmpdir2();
|
|
3159
|
+
return join9(base, ".reddoor-maint", "sites");
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3115
3162
|
// src/cli/commands/audit.ts
|
|
3116
3163
|
function parseOnly(value) {
|
|
3117
3164
|
if (!value) return void 0;
|
|
@@ -3177,7 +3224,7 @@ function buildAuditTasks(sites, which, results, renderer, concurrency) {
|
|
|
3177
3224
|
}
|
|
3178
3225
|
return new Listr(
|
|
3179
3226
|
sites.map((site) => {
|
|
3180
|
-
const label = site.name
|
|
3227
|
+
const label = site.name || site.path;
|
|
3181
3228
|
return {
|
|
3182
3229
|
title: label,
|
|
3183
3230
|
task: async (_ctx, task) => {
|
|
@@ -3245,10 +3292,10 @@ function applyDeployedUrl(sites, url) {
|
|
|
3245
3292
|
{ exitCode: 2 }
|
|
3246
3293
|
);
|
|
3247
3294
|
}
|
|
3248
|
-
|
|
3249
|
-
new URL(url)
|
|
3250
|
-
|
|
3251
|
-
|
|
3295
|
+
if (!isHttpUrl(url)) {
|
|
3296
|
+
throw Object.assign(new Error(`--url must be an http(s) URL (got: ${JSON.stringify(url)})`), {
|
|
3297
|
+
exitCode: 2
|
|
3298
|
+
});
|
|
3252
3299
|
}
|
|
3253
3300
|
return [{ ...sites[0], deployedUrl: url }];
|
|
3254
3301
|
}
|
|
@@ -3278,18 +3325,25 @@ async function runAuditCommand(site, opts) {
|
|
|
3278
3325
|
cwd
|
|
3279
3326
|
});
|
|
3280
3327
|
sites = applyDeployedUrl(sites, opts.url);
|
|
3328
|
+
let skippedPrep = [];
|
|
3281
3329
|
if (opts.fleet) {
|
|
3282
|
-
const workdir = opts.workdir ??
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3330
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
3331
|
+
const prep = await prepareFleetSites(sites, {
|
|
3332
|
+
workdir,
|
|
3333
|
+
needsCheckout: (s) => auditNeedsCheckout(s, which)
|
|
3334
|
+
});
|
|
3335
|
+
sites = prep.prepared;
|
|
3336
|
+
skippedPrep = prep.skipped;
|
|
3288
3337
|
}
|
|
3289
3338
|
const results = [];
|
|
3290
3339
|
const renderer = rendererFor(opts.json);
|
|
3291
3340
|
await buildAuditTasks(sites, which, results, renderer, parseConcurrency(opts.concurrency)).run();
|
|
3292
3341
|
let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
|
|
3342
|
+
const skipNotice = formatSkippedNotice(skippedPrep);
|
|
3343
|
+
if (skipNotice && !opts.json) output += `
|
|
3344
|
+
|
|
3345
|
+
${skipNotice}`;
|
|
3346
|
+
let writeBackFailed = false;
|
|
3293
3347
|
if (opts.writeAirtable !== void 0) {
|
|
3294
3348
|
const { openBase: openBase2, readAirtableConfig: readAirtableConfig2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3295
3349
|
const { listWebsites: listWebsites2 } = await Promise.resolve().then(() => (init_websites(), websites_exports));
|
|
@@ -3298,7 +3352,8 @@ async function runAuditCommand(site, opts) {
|
|
|
3298
3352
|
const base = openBase2(readAirtableConfig2());
|
|
3299
3353
|
const websites = await listWebsites2(base);
|
|
3300
3354
|
const fleetWrite = await writeFleetAuditsToAirtable2({ base, websites, results });
|
|
3301
|
-
|
|
3355
|
+
if (fleetWrite.failed.length > 0) writeBackFailed = true;
|
|
3356
|
+
if (!opts.json) output += `
|
|
3302
3357
|
|
|
3303
3358
|
${formatFleetWriteSummary2(fleetWrite)}`;
|
|
3304
3359
|
} else {
|
|
@@ -3322,7 +3377,7 @@ ${formatFleetWriteSummary2(fleetWrite)}`;
|
|
|
3322
3377
|
],
|
|
3323
3378
|
{ renderer }
|
|
3324
3379
|
).run();
|
|
3325
|
-
if (writeSummary) output += `
|
|
3380
|
+
if (writeSummary && !opts.json) output += `
|
|
3326
3381
|
|
|
3327
3382
|
${formatWriteSummary(writeSummary)}`;
|
|
3328
3383
|
}
|
|
@@ -3331,16 +3386,20 @@ ${formatWriteSummary(writeSummary)}`;
|
|
|
3331
3386
|
if (notice && !opts.json) output += `
|
|
3332
3387
|
|
|
3333
3388
|
${notice}`;
|
|
3334
|
-
|
|
3389
|
+
const code = Math.max(
|
|
3390
|
+
auditExitCode(results, opts.failOnViolations === true),
|
|
3391
|
+
writeBackFailed ? 1 : 0
|
|
3392
|
+
);
|
|
3393
|
+
return { output, code };
|
|
3335
3394
|
}
|
|
3336
3395
|
|
|
3337
3396
|
// src/cli/commands/sync-configs.ts
|
|
3338
3397
|
import { readFile as readFile9 } from "fs/promises";
|
|
3339
|
-
import { join as
|
|
3398
|
+
import { join as join12, resolve as resolve3 } from "path";
|
|
3340
3399
|
|
|
3341
3400
|
// src/recipes/sync-configs.ts
|
|
3342
3401
|
import { readFile as readFile8, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
3343
|
-
import { join as
|
|
3402
|
+
import { join as join11, dirname } from "path";
|
|
3344
3403
|
|
|
3345
3404
|
// src/recipes/sync-configs/templates.ts
|
|
3346
3405
|
var eslint = {
|
|
@@ -3590,59 +3649,6 @@ function findTrackedArtifacts(tracked, canonical) {
|
|
|
3590
3649
|
return matched;
|
|
3591
3650
|
}
|
|
3592
3651
|
|
|
3593
|
-
// src/util/git.ts
|
|
3594
|
-
import { execFile } from "child_process";
|
|
3595
|
-
import { promisify } from "util";
|
|
3596
|
-
var exec = promisify(execFile);
|
|
3597
|
-
async function git(cwd, args) {
|
|
3598
|
-
return exec("git", args, { cwd, env: process.env });
|
|
3599
|
-
}
|
|
3600
|
-
function branchName(recipe, when = /* @__PURE__ */ new Date()) {
|
|
3601
|
-
const compact = when.toISOString().replace(/[-:.]/g, "");
|
|
3602
|
-
return `maint/${recipe}-${compact}`;
|
|
3603
|
-
}
|
|
3604
|
-
async function isWorkingTreeClean(cwd) {
|
|
3605
|
-
const { stdout } = await git(cwd, ["status", "--porcelain"]);
|
|
3606
|
-
return stdout.trim().length === 0;
|
|
3607
|
-
}
|
|
3608
|
-
async function createBranch(cwd, name) {
|
|
3609
|
-
await git(cwd, ["checkout", "-b", name]);
|
|
3610
|
-
}
|
|
3611
|
-
async function stageAll(cwd) {
|
|
3612
|
-
await git(cwd, ["add", "-A"]);
|
|
3613
|
-
}
|
|
3614
|
-
async function listTrackedFiles(cwd) {
|
|
3615
|
-
const { stdout } = await git(cwd, ["ls-files"]);
|
|
3616
|
-
return stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
3617
|
-
}
|
|
3618
|
-
async function removeFromIndex(cwd, paths) {
|
|
3619
|
-
if (paths.length === 0) return;
|
|
3620
|
-
await git(cwd, ["rm", "-r", "--cached", "--", ...paths]);
|
|
3621
|
-
}
|
|
3622
|
-
async function commit(cwd, message) {
|
|
3623
|
-
await stageAll(cwd);
|
|
3624
|
-
const { stdout: status } = await git(cwd, ["status", "--porcelain"]);
|
|
3625
|
-
if (status.trim().length === 0) return null;
|
|
3626
|
-
await git(cwd, ["commit", "-m", message]);
|
|
3627
|
-
const { stdout: sha } = await git(cwd, ["rev-parse", "HEAD"]);
|
|
3628
|
-
return sha.trim();
|
|
3629
|
-
}
|
|
3630
|
-
function parseOwnerRepo(remoteUrl) {
|
|
3631
|
-
const trimmed = remoteUrl.trim().replace(/\.git$/, "").replace(/\/$/, "");
|
|
3632
|
-
const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);
|
|
3633
|
-
const path = scp ? scp[1] : trimmed.replace(/^https?:\/\/[^/]+\//, "");
|
|
3634
|
-
const segments = path.split("/").filter(Boolean);
|
|
3635
|
-
if (segments.length < 2) return null;
|
|
3636
|
-
return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
|
|
3637
|
-
}
|
|
3638
|
-
async function getRemoteUrl(cwd) {
|
|
3639
|
-
const { stdout } = await git(cwd, ["remote", "get-url", "origin"]);
|
|
3640
|
-
return stdout.trim();
|
|
3641
|
-
}
|
|
3642
|
-
async function push(cwd, branch) {
|
|
3643
|
-
await git(cwd, ["push", "-u", "origin", branch]);
|
|
3644
|
-
}
|
|
3645
|
-
|
|
3646
3652
|
// src/recipes/_with-recipe.ts
|
|
3647
3653
|
async function withRecipe(body) {
|
|
3648
3654
|
const label = siteLabel(body.site);
|
|
@@ -3671,19 +3677,60 @@ async function withRecipe(body) {
|
|
|
3671
3677
|
if (!body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
|
|
3672
3678
|
throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
|
|
3673
3679
|
}
|
|
3680
|
+
let original = null;
|
|
3681
|
+
try {
|
|
3682
|
+
original = await currentBranch(body.site.path);
|
|
3683
|
+
} catch {
|
|
3684
|
+
original = null;
|
|
3685
|
+
}
|
|
3674
3686
|
const branch = branchName(body.name);
|
|
3675
3687
|
await createBranch(body.site.path, branch);
|
|
3676
|
-
const
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3688
|
+
const restoreOriginal = async () => {
|
|
3689
|
+
if (original === null || original === branch) return;
|
|
3690
|
+
try {
|
|
3691
|
+
await checkoutBranch(body.site.path, original);
|
|
3692
|
+
} catch (err) {
|
|
3693
|
+
console.warn(
|
|
3694
|
+
`warning: could not restore branch ${original} after ${body.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
3695
|
+
);
|
|
3684
3696
|
}
|
|
3685
|
-
}
|
|
3697
|
+
};
|
|
3698
|
+
const restoreAfterFailure = async () => {
|
|
3699
|
+
if (original === null || original === branch) return;
|
|
3700
|
+
try {
|
|
3701
|
+
await forceCheckoutBranch(body.site.path, original);
|
|
3702
|
+
} catch (err) {
|
|
3703
|
+
console.warn(
|
|
3704
|
+
`warning: could not force-restore branch ${original} after failed ${body.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
3705
|
+
);
|
|
3706
|
+
return;
|
|
3707
|
+
}
|
|
3708
|
+
try {
|
|
3709
|
+
await deleteBranch(body.site.path, branch);
|
|
3710
|
+
} catch (err) {
|
|
3711
|
+
console.warn(
|
|
3712
|
+
`warning: could not delete recipe branch ${branch} after failed ${body.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
3713
|
+
);
|
|
3714
|
+
}
|
|
3715
|
+
};
|
|
3716
|
+
const shas = [];
|
|
3717
|
+
let result;
|
|
3718
|
+
try {
|
|
3719
|
+
result = await body.apply(planned.plan, {
|
|
3720
|
+
cwd: body.site.path,
|
|
3721
|
+
branch,
|
|
3722
|
+
commit: async (msg) => {
|
|
3723
|
+
const sha = await commit(body.site.path, msg);
|
|
3724
|
+
if (sha) shas.push(sha);
|
|
3725
|
+
return sha;
|
|
3726
|
+
}
|
|
3727
|
+
});
|
|
3728
|
+
} catch (err) {
|
|
3729
|
+
await restoreAfterFailure();
|
|
3730
|
+
throw err;
|
|
3731
|
+
}
|
|
3686
3732
|
if (result.kind === "failed") {
|
|
3733
|
+
await restoreAfterFailure();
|
|
3687
3734
|
return {
|
|
3688
3735
|
recipe: body.name,
|
|
3689
3736
|
site: label,
|
|
@@ -3692,6 +3739,9 @@ async function withRecipe(body) {
|
|
|
3692
3739
|
notes: result.notes
|
|
3693
3740
|
};
|
|
3694
3741
|
}
|
|
3742
|
+
if (shas.length === 0) {
|
|
3743
|
+
await restoreOriginal();
|
|
3744
|
+
}
|
|
3695
3745
|
const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;
|
|
3696
3746
|
return {
|
|
3697
3747
|
recipe: body.name,
|
|
@@ -3739,7 +3789,7 @@ async function readMaybe(path) {
|
|
|
3739
3789
|
async function planTemplateDiffs(cwd, templates) {
|
|
3740
3790
|
const diffs = [];
|
|
3741
3791
|
for (const t of templates) {
|
|
3742
|
-
const existing = await readMaybe(
|
|
3792
|
+
const existing = await readMaybe(join11(cwd, t.path));
|
|
3743
3793
|
if (existing === t.contents) continue;
|
|
3744
3794
|
if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {
|
|
3745
3795
|
continue;
|
|
@@ -3752,7 +3802,7 @@ async function planTemplateDiffs(cwd, templates) {
|
|
|
3752
3802
|
return diffs;
|
|
3753
3803
|
}
|
|
3754
3804
|
async function planGitignore(cwd) {
|
|
3755
|
-
const existing = await readMaybe(
|
|
3805
|
+
const existing = await readMaybe(join11(cwd, ".gitignore"));
|
|
3756
3806
|
const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
|
|
3757
3807
|
const tracked = await listTrackedFiles(cwd);
|
|
3758
3808
|
const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
|
|
@@ -3760,7 +3810,7 @@ async function planGitignore(cwd) {
|
|
|
3760
3810
|
return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
|
|
3761
3811
|
}
|
|
3762
3812
|
async function applyGitignore(cwd, plan) {
|
|
3763
|
-
await writeFile3(
|
|
3813
|
+
await writeFile3(join11(cwd, ".gitignore"), plan.content, "utf-8");
|
|
3764
3814
|
if (plan.toUntrack.length > 0) {
|
|
3765
3815
|
await removeFromIndex(cwd, plan.toUntrack);
|
|
3766
3816
|
}
|
|
@@ -3783,7 +3833,7 @@ async function syncConfigs(site, opts = {}) {
|
|
|
3783
3833
|
},
|
|
3784
3834
|
apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
|
|
3785
3835
|
for (const t of templateDiffs) {
|
|
3786
|
-
const dest =
|
|
3836
|
+
const dest = join11(site.path, t.path);
|
|
3787
3837
|
await mkdir2(dirname(dest), { recursive: true });
|
|
3788
3838
|
await writeFile3(dest, t.contents, "utf-8");
|
|
3789
3839
|
await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
@@ -3814,7 +3864,7 @@ function parseOnly2(value) {
|
|
|
3814
3864
|
async function dryPlanGitignore(cwd) {
|
|
3815
3865
|
let existing;
|
|
3816
3866
|
try {
|
|
3817
|
-
existing = await readFile9(
|
|
3867
|
+
existing = await readFile9(join12(cwd, ".gitignore"), "utf-8");
|
|
3818
3868
|
} catch {
|
|
3819
3869
|
return "would create .gitignore";
|
|
3820
3870
|
}
|
|
@@ -3829,7 +3879,7 @@ async function dryPlan(cwd, which) {
|
|
|
3829
3879
|
for (const t of templateTargets) {
|
|
3830
3880
|
let existing = "";
|
|
3831
3881
|
try {
|
|
3832
|
-
existing = await readFile9(
|
|
3882
|
+
existing = await readFile9(join12(cwd, t.path), "utf-8");
|
|
3833
3883
|
} catch {
|
|
3834
3884
|
}
|
|
3835
3885
|
if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
|
|
@@ -3853,32 +3903,34 @@ async function runSyncConfigsCommand(site, opts) {
|
|
|
3853
3903
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
3854
3904
|
cwd
|
|
3855
3905
|
});
|
|
3906
|
+
let skipped = [];
|
|
3856
3907
|
if (opts.fleet) {
|
|
3857
|
-
const workdir = opts.workdir ??
|
|
3858
|
-
|
|
3908
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
3909
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
3910
|
+
sites = prep.prepared;
|
|
3911
|
+
skipped = prep.skipped;
|
|
3859
3912
|
}
|
|
3860
3913
|
if (opts.dry) {
|
|
3861
3914
|
const blocks = [];
|
|
3862
3915
|
for (const s of sites) {
|
|
3863
|
-
blocks.push(`[${s.name
|
|
3916
|
+
blocks.push(`[${s.name || s.path}]
|
|
3864
3917
|
` + await dryPlan(s.path, which));
|
|
3865
3918
|
}
|
|
3866
|
-
return { output: blocks.join("\n\n"), code: 0 };
|
|
3919
|
+
return { output: appendSkipNotice(blocks.join("\n\n"), skipped), code: 0 };
|
|
3867
3920
|
}
|
|
3868
3921
|
const results = [];
|
|
3869
3922
|
for (const s of sites) results.push(await syncConfigs(s, which ? { which } : {}));
|
|
3870
3923
|
const output = results.map(formatResult).join("\n");
|
|
3871
3924
|
const code = results.some((r) => r.status === "failed") ? 1 : 0;
|
|
3872
|
-
return { output, code };
|
|
3925
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
3873
3926
|
}
|
|
3874
3927
|
|
|
3875
3928
|
// src/cli/commands/bump-deps.ts
|
|
3876
3929
|
import { resolve as resolve4 } from "path";
|
|
3877
3930
|
|
|
3878
3931
|
// src/recipes/bump-deps.ts
|
|
3879
|
-
init_spawn();
|
|
3880
3932
|
import { stat as stat3 } from "fs/promises";
|
|
3881
|
-
import { join as
|
|
3933
|
+
import { join as join13 } from "path";
|
|
3882
3934
|
async function exists2(path) {
|
|
3883
3935
|
try {
|
|
3884
3936
|
await stat3(path);
|
|
@@ -3907,10 +3959,10 @@ async function bumpDeps(site, opts = {}) {
|
|
|
3907
3959
|
// land on top of whatever else was in the tree.
|
|
3908
3960
|
checkTreeFirst: true,
|
|
3909
3961
|
plan: async () => {
|
|
3910
|
-
const hasPnpmLock = await exists2(
|
|
3962
|
+
const hasPnpmLock = await exists2(join13(site.path, "pnpm-lock.yaml"));
|
|
3911
3963
|
if (!hasPnpmLock) {
|
|
3912
|
-
const hasNpmLock = await exists2(
|
|
3913
|
-
const hasYarnLock = await exists2(
|
|
3964
|
+
const hasNpmLock = await exists2(join13(site.path, "package-lock.json"));
|
|
3965
|
+
const hasYarnLock = await exists2(join13(site.path, "yarn.lock"));
|
|
3914
3966
|
if (hasNpmLock || hasYarnLock) {
|
|
3915
3967
|
const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
3916
3968
|
return {
|
|
@@ -3965,15 +4017,18 @@ async function runBumpDepsCommand(site, opts) {
|
|
|
3965
4017
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
3966
4018
|
cwd
|
|
3967
4019
|
});
|
|
4020
|
+
let skipped = [];
|
|
3968
4021
|
if (opts.fleet) {
|
|
3969
|
-
const workdir = opts.workdir ??
|
|
3970
|
-
|
|
4022
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
4023
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
4024
|
+
sites = prep.prepared;
|
|
4025
|
+
skipped = prep.skipped;
|
|
3971
4026
|
}
|
|
3972
4027
|
const results = [];
|
|
3973
4028
|
for (const s of sites) results.push(await bumpDeps(s, { group }));
|
|
3974
4029
|
const output = results.map(formatResult2).join("\n");
|
|
3975
4030
|
const code = results.some((r) => r.status === "failed") ? 1 : 0;
|
|
3976
|
-
return { output, code };
|
|
4031
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
3977
4032
|
}
|
|
3978
4033
|
|
|
3979
4034
|
// src/cli/commands/self-updating.ts
|
|
@@ -3981,7 +4036,7 @@ import { resolve as resolve5 } from "path";
|
|
|
3981
4036
|
|
|
3982
4037
|
// src/recipes/self-updating/index.ts
|
|
3983
4038
|
import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
|
|
3984
|
-
import { dirname as dirname2, join as
|
|
4039
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
3985
4040
|
|
|
3986
4041
|
// src/github/config.ts
|
|
3987
4042
|
function readGitHubConfig() {
|
|
@@ -3991,25 +4046,230 @@ function readGitHubConfig() {
|
|
|
3991
4046
|
return { token, renovateToken };
|
|
3992
4047
|
}
|
|
3993
4048
|
|
|
4049
|
+
// src/github/gh.ts
|
|
4050
|
+
function assertUrlSegment(kind, value) {
|
|
4051
|
+
const structural = /[\s?#%\\]|\.\./;
|
|
4052
|
+
if (value.length === 0 || value.startsWith("/") || structural.test(value)) {
|
|
4053
|
+
throw new Error(
|
|
4054
|
+
`unsafe ${kind} for gh api path (illegal characters or traversal): ${JSON.stringify(value)}`
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
function mapRollupState(state) {
|
|
4059
|
+
switch (state) {
|
|
4060
|
+
case "SUCCESS":
|
|
4061
|
+
return "passing";
|
|
4062
|
+
case "FAILURE":
|
|
4063
|
+
case "ERROR":
|
|
4064
|
+
return "failing";
|
|
4065
|
+
case "PENDING":
|
|
4066
|
+
case "EXPECTED":
|
|
4067
|
+
return "pending";
|
|
4068
|
+
default:
|
|
4069
|
+
return "none";
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
function makeGitHub(deps) {
|
|
4073
|
+
const spawn2 = deps.spawn ?? defaultSpawn;
|
|
4074
|
+
const env = { ...process.env, GH_TOKEN: deps.token };
|
|
4075
|
+
async function gh(args) {
|
|
4076
|
+
const r = await spawn2("gh", args, { env, timeoutMs: 6e4 });
|
|
4077
|
+
if (r.code !== 0) throw new Error(`gh ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);
|
|
4078
|
+
return r.stdout;
|
|
4079
|
+
}
|
|
4080
|
+
return {
|
|
4081
|
+
async openPullRequest(repo, pr) {
|
|
4082
|
+
const out = await gh([
|
|
4083
|
+
"pr",
|
|
4084
|
+
"create",
|
|
4085
|
+
"--repo",
|
|
4086
|
+
repo,
|
|
4087
|
+
"--head",
|
|
4088
|
+
pr.head,
|
|
4089
|
+
"--base",
|
|
4090
|
+
pr.base,
|
|
4091
|
+
"--title",
|
|
4092
|
+
pr.title,
|
|
4093
|
+
"--body",
|
|
4094
|
+
pr.body
|
|
4095
|
+
]);
|
|
4096
|
+
return { url: out.trim() };
|
|
4097
|
+
},
|
|
4098
|
+
async enableRepoAutoMerge(repo) {
|
|
4099
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-F", "allow_auto_merge=true"]);
|
|
4100
|
+
},
|
|
4101
|
+
async protectBranch(repo, branch, requiredChecks) {
|
|
4102
|
+
assertUrlSegment("branch", branch);
|
|
4103
|
+
const args = [
|
|
4104
|
+
"api",
|
|
4105
|
+
"-X",
|
|
4106
|
+
"PUT",
|
|
4107
|
+
`repos/${repo}/branches/${branch}/protection`,
|
|
4108
|
+
"-H",
|
|
4109
|
+
"Accept: application/vnd.github+json",
|
|
4110
|
+
"-F",
|
|
4111
|
+
"required_status_checks[strict]=true",
|
|
4112
|
+
...requiredChecks.flatMap((c) => ["-f", `required_status_checks[contexts][]=${c}`]),
|
|
4113
|
+
"-F",
|
|
4114
|
+
"enforce_admins=true",
|
|
4115
|
+
"-F",
|
|
4116
|
+
"required_pull_request_reviews=null",
|
|
4117
|
+
"-F",
|
|
4118
|
+
"restrictions=null"
|
|
4119
|
+
];
|
|
4120
|
+
await gh(args);
|
|
4121
|
+
},
|
|
4122
|
+
async setRepoSecret(repo, name, value) {
|
|
4123
|
+
await gh(["secret", "set", name, "--repo", repo, "--body", value]);
|
|
4124
|
+
},
|
|
4125
|
+
async repoExists(repo) {
|
|
4126
|
+
const r = await spawn2("gh", ["api", `repos/${repo}`], { env, timeoutMs: 6e4 });
|
|
4127
|
+
return r.code === 0;
|
|
4128
|
+
},
|
|
4129
|
+
async defaultBranch(repo) {
|
|
4130
|
+
const out = await gh(["api", `repos/${repo}`, "--jq", ".default_branch"]);
|
|
4131
|
+
return out.trim();
|
|
4132
|
+
},
|
|
4133
|
+
// filesOnBranch and branchProtectionContexts call `spawn` directly (not the
|
|
4134
|
+
// throwing `gh()` helper) because a 404 is an expected, meaningful answer —
|
|
4135
|
+
// "file/protection absent" — not an error. The remaining readers use `gh()`
|
|
4136
|
+
// since a non-200 there is a genuine failure (e.g. missing token scope).
|
|
4137
|
+
async filesOnBranch(repo, branch, paths) {
|
|
4138
|
+
assertUrlSegment("branch", branch);
|
|
4139
|
+
const present = [];
|
|
4140
|
+
for (const p of paths) {
|
|
4141
|
+
assertUrlSegment("path", p);
|
|
4142
|
+
const r = await spawn2("gh", [`api`, `repos/${repo}/contents/${p}?ref=${branch}`], {
|
|
4143
|
+
env,
|
|
4144
|
+
timeoutMs: 6e4
|
|
4145
|
+
});
|
|
4146
|
+
if (r.code === 0) present.push(p);
|
|
4147
|
+
}
|
|
4148
|
+
return present;
|
|
4149
|
+
},
|
|
4150
|
+
async branchProtectionContexts(repo, branch) {
|
|
4151
|
+
assertUrlSegment("branch", branch);
|
|
4152
|
+
const r = await spawn2(
|
|
4153
|
+
"gh",
|
|
4154
|
+
[
|
|
4155
|
+
"api",
|
|
4156
|
+
`repos/${repo}/branches/${branch}/protection`,
|
|
4157
|
+
"--jq",
|
|
4158
|
+
".required_status_checks.contexts[]?"
|
|
4159
|
+
],
|
|
4160
|
+
{ env, timeoutMs: 6e4 }
|
|
4161
|
+
);
|
|
4162
|
+
if (r.code !== 0) return [];
|
|
4163
|
+
return r.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
4164
|
+
},
|
|
4165
|
+
async secretExists(repo, name) {
|
|
4166
|
+
const out = await gh(["api", `repos/${repo}/actions/secrets`, "--jq", ".secrets[].name"]);
|
|
4167
|
+
return out.split("\n").map((l) => l.trim()).includes(name);
|
|
4168
|
+
},
|
|
4169
|
+
async autoMergeEnabled(repo) {
|
|
4170
|
+
const out = await gh(["api", `repos/${repo}`, "--jq", ".allow_auto_merge"]);
|
|
4171
|
+
return out.trim() === "true";
|
|
4172
|
+
},
|
|
4173
|
+
async findOpenSelfUpdatingPR(repo) {
|
|
4174
|
+
const out = await gh([
|
|
4175
|
+
"api",
|
|
4176
|
+
`repos/${repo}/pulls?state=open`,
|
|
4177
|
+
"--jq",
|
|
4178
|
+
'.[] | select(.head.ref | startswith("maint/self-updating-")) | .html_url'
|
|
4179
|
+
]);
|
|
4180
|
+
const first = out.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
4181
|
+
return first ?? null;
|
|
4182
|
+
},
|
|
4183
|
+
async openPullRequests(repo) {
|
|
4184
|
+
const [owner, name, ...rest] = repo.split("/");
|
|
4185
|
+
if (!owner || !name || rest.length > 0) {
|
|
4186
|
+
throw new Error(`openPullRequests: expected "owner/repo", got "${repo}"`);
|
|
4187
|
+
}
|
|
4188
|
+
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}";
|
|
4189
|
+
const out = await gh([
|
|
4190
|
+
"api",
|
|
4191
|
+
"graphql",
|
|
4192
|
+
"-f",
|
|
4193
|
+
`query=${query}`,
|
|
4194
|
+
"-F",
|
|
4195
|
+
`owner=${owner}`,
|
|
4196
|
+
"-F",
|
|
4197
|
+
`name=${name}`
|
|
4198
|
+
]);
|
|
4199
|
+
const parsed = JSON.parse(out);
|
|
4200
|
+
const nodes = parsed.data?.repository?.pullRequests?.nodes ?? [];
|
|
4201
|
+
return nodes.map((n) => ({
|
|
4202
|
+
number: n.number,
|
|
4203
|
+
title: n.title,
|
|
4204
|
+
url: n.url,
|
|
4205
|
+
headRef: n.headRefName,
|
|
4206
|
+
ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state)
|
|
4207
|
+
}));
|
|
4208
|
+
},
|
|
4209
|
+
async defaultBranchStatus(repo) {
|
|
4210
|
+
const [owner, name, ...rest] = repo.split("/");
|
|
4211
|
+
if (!owner || !name || rest.length > 0) {
|
|
4212
|
+
throw new Error(`defaultBranchStatus: expected "owner/repo", got "${repo}"`);
|
|
4213
|
+
}
|
|
4214
|
+
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){defaultBranchRef{target{... on Commit{committedDate statusCheckRollup{state}}}}}}";
|
|
4215
|
+
const out = await gh([
|
|
4216
|
+
"api",
|
|
4217
|
+
"graphql",
|
|
4218
|
+
"-f",
|
|
4219
|
+
`query=${query}`,
|
|
4220
|
+
"-F",
|
|
4221
|
+
`owner=${owner}`,
|
|
4222
|
+
"-F",
|
|
4223
|
+
`name=${name}`
|
|
4224
|
+
]);
|
|
4225
|
+
const parsed = JSON.parse(out);
|
|
4226
|
+
const target = parsed.data?.repository?.defaultBranchRef?.target;
|
|
4227
|
+
return {
|
|
4228
|
+
ciState: mapRollupState(target?.statusCheckRollup?.state),
|
|
4229
|
+
lastCommitAt: target?.committedDate ?? null
|
|
4230
|
+
};
|
|
4231
|
+
}
|
|
4232
|
+
};
|
|
4233
|
+
}
|
|
4234
|
+
|
|
3994
4235
|
// src/recipes/self-updating/index.ts
|
|
3995
|
-
init_gh();
|
|
3996
4236
|
var SELF_UPDATING_CONFIGS = ["ci", "renovate-action", "renovate-config"];
|
|
3997
4237
|
var REQUIRED_CHECK = "ci / ci";
|
|
3998
4238
|
function resultOf(site, status, notes, commits = []) {
|
|
3999
4239
|
return { recipe: "self-updating", site: siteLabel(site), status, commits, notes };
|
|
4000
4240
|
}
|
|
4001
4241
|
async function resolveRepo(site) {
|
|
4002
|
-
if (site.gitRepo)
|
|
4242
|
+
if (site.gitRepo) {
|
|
4243
|
+
if (!isOwnerRepo(site.gitRepo)) {
|
|
4244
|
+
throw new Error(
|
|
4245
|
+
`refusing to act on malformed repo identity: expected "owner/repo", got ${JSON.stringify(site.gitRepo)}`
|
|
4246
|
+
);
|
|
4247
|
+
}
|
|
4248
|
+
return site.gitRepo;
|
|
4249
|
+
}
|
|
4250
|
+
let fromOrigin;
|
|
4003
4251
|
try {
|
|
4004
|
-
|
|
4252
|
+
fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));
|
|
4005
4253
|
} catch {
|
|
4006
4254
|
return null;
|
|
4007
4255
|
}
|
|
4256
|
+
if (fromOrigin === null) return null;
|
|
4257
|
+
if (!isOwnerRepo(fromOrigin)) {
|
|
4258
|
+
throw new Error(
|
|
4259
|
+
`refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
return fromOrigin;
|
|
4008
4263
|
}
|
|
4009
4264
|
async function selfUpdating(site, deps = {}) {
|
|
4010
4265
|
const templates = templatesByName([...SELF_UPDATING_CONFIGS]);
|
|
4011
4266
|
const paths = templates.map((t) => t.path);
|
|
4012
|
-
|
|
4267
|
+
let repo;
|
|
4268
|
+
try {
|
|
4269
|
+
repo = await resolveRepo(site);
|
|
4270
|
+
} catch (err) {
|
|
4271
|
+
return resultOf(site, "failed", err instanceof Error ? err.message : String(err));
|
|
4272
|
+
}
|
|
4013
4273
|
if (!repo) {
|
|
4014
4274
|
return resultOf(
|
|
4015
4275
|
site,
|
|
@@ -4025,6 +4285,8 @@ async function selfUpdating(site, deps = {}) {
|
|
|
4025
4285
|
const base = await github.defaultBranch(repo).catch(() => "main");
|
|
4026
4286
|
const actions = [];
|
|
4027
4287
|
const commits = [];
|
|
4288
|
+
let original = null;
|
|
4289
|
+
let maintBranch = null;
|
|
4028
4290
|
try {
|
|
4029
4291
|
const present = await github.filesOnBranch(repo, base, paths);
|
|
4030
4292
|
if (present.length < paths.length) {
|
|
@@ -4035,10 +4297,15 @@ async function selfUpdating(site, deps = {}) {
|
|
|
4035
4297
|
if (!await isWorkingTreeClean(site.path)) {
|
|
4036
4298
|
return resultOf(site, "failed", "working tree not clean \u2014 commit or stash first");
|
|
4037
4299
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4300
|
+
try {
|
|
4301
|
+
original = await currentBranch(site.path);
|
|
4302
|
+
} catch {
|
|
4303
|
+
original = null;
|
|
4304
|
+
}
|
|
4305
|
+
maintBranch = branchName("self-updating");
|
|
4306
|
+
await createBranch(site.path, maintBranch);
|
|
4040
4307
|
for (const t of templates) {
|
|
4041
|
-
const dest =
|
|
4308
|
+
const dest = join14(site.path, t.path);
|
|
4042
4309
|
await mkdir3(dirname2(dest), { recursive: true });
|
|
4043
4310
|
await writeFile4(dest, t.contents, "utf-8");
|
|
4044
4311
|
}
|
|
@@ -4047,9 +4314,9 @@ async function selfUpdating(site, deps = {}) {
|
|
|
4047
4314
|
"ci: enable self-updating (CI + Renovate auto-merge)"
|
|
4048
4315
|
);
|
|
4049
4316
|
if (sha) commits.push(sha);
|
|
4050
|
-
await (deps.pushBranch ?? push)(site.path,
|
|
4317
|
+
await (deps.pushBranch ?? push)(site.path, maintBranch);
|
|
4051
4318
|
const pr = await github.openPullRequest(repo, {
|
|
4052
|
-
head:
|
|
4319
|
+
head: maintBranch,
|
|
4053
4320
|
base,
|
|
4054
4321
|
title: "Enable self-updating (CI + Renovate)",
|
|
4055
4322
|
body: "Adds the unified CI gate, nightly Renovate, and auto-merge for patch/minor updates."
|
|
@@ -4061,8 +4328,10 @@ async function selfUpdating(site, deps = {}) {
|
|
|
4061
4328
|
await github.enableRepoAutoMerge(repo);
|
|
4062
4329
|
actions.push("enabled auto-merge");
|
|
4063
4330
|
}
|
|
4064
|
-
|
|
4065
|
-
|
|
4331
|
+
const existingContexts = await github.branchProtectionContexts(repo, base);
|
|
4332
|
+
if (!existingContexts.includes(REQUIRED_CHECK)) {
|
|
4333
|
+
const contexts = [.../* @__PURE__ */ new Set([...existingContexts, REQUIRED_CHECK])];
|
|
4334
|
+
await github.protectBranch(repo, base, contexts);
|
|
4066
4335
|
actions.push(`required "${REQUIRED_CHECK}" check on ${base}`);
|
|
4067
4336
|
}
|
|
4068
4337
|
if (!await github.secretExists(repo, "RENOVATE_TOKEN")) {
|
|
@@ -4073,6 +4342,16 @@ async function selfUpdating(site, deps = {}) {
|
|
|
4073
4342
|
const done = actions.length ? ` (completed: ${actions.join("; ")})` : "";
|
|
4074
4343
|
const message = err instanceof Error ? err.message : String(err);
|
|
4075
4344
|
return resultOf(site, "failed", `${message}${done}`, commits);
|
|
4345
|
+
} finally {
|
|
4346
|
+
if (maintBranch !== null && original !== null && original !== maintBranch) {
|
|
4347
|
+
try {
|
|
4348
|
+
await checkoutBranch(site.path, original);
|
|
4349
|
+
} catch (err) {
|
|
4350
|
+
console.warn(
|
|
4351
|
+
`warning: could not restore branch ${original} after self-updating: ${err instanceof Error ? err.message : String(err)}`
|
|
4352
|
+
);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4076
4355
|
}
|
|
4077
4356
|
return actions.length ? resultOf(site, "applied", actions.join("; "), commits) : resultOf(site, "noop", "already self-updating", commits);
|
|
4078
4357
|
}
|
|
@@ -4091,20 +4370,26 @@ async function runSelfUpdatingCommand(site, opts) {
|
|
|
4091
4370
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
4092
4371
|
cwd
|
|
4093
4372
|
});
|
|
4373
|
+
let skipped = [];
|
|
4094
4374
|
if (opts.fleet) {
|
|
4095
|
-
const workdir = opts.workdir ??
|
|
4096
|
-
|
|
4375
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
4376
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
4377
|
+
sites = prep.prepared;
|
|
4378
|
+
skipped = prep.skipped;
|
|
4097
4379
|
}
|
|
4098
4380
|
if (opts.dry) {
|
|
4099
4381
|
return {
|
|
4100
|
-
output:
|
|
4382
|
+
output: appendSkipNotice(
|
|
4383
|
+
sites.map((s) => `[${s.name || s.path}] would enable self-updating`).join("\n"),
|
|
4384
|
+
skipped
|
|
4385
|
+
),
|
|
4101
4386
|
code: 0
|
|
4102
4387
|
};
|
|
4103
4388
|
}
|
|
4104
4389
|
const results = [];
|
|
4105
4390
|
for (const s of sites) results.push(await selfUpdating(s));
|
|
4106
4391
|
return {
|
|
4107
|
-
output: results.map(formatResult3).join("\n"),
|
|
4392
|
+
output: appendSkipNotice(results.map(formatResult3).join("\n"), skipped),
|
|
4108
4393
|
code: results.some((r) => r.status === "failed") ? 1 : 0
|
|
4109
4394
|
};
|
|
4110
4395
|
}
|
|
@@ -4113,7 +4398,7 @@ async function runSelfUpdatingCommand(site, opts) {
|
|
|
4113
4398
|
import { resolve as resolve6 } from "path";
|
|
4114
4399
|
|
|
4115
4400
|
// src/recipes/svelte-5/index.ts
|
|
4116
|
-
import { join as
|
|
4401
|
+
import { join as join20 } from "path";
|
|
4117
4402
|
|
|
4118
4403
|
// src/util/pkg.ts
|
|
4119
4404
|
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
@@ -4161,11 +4446,8 @@ function bumpDep(pkg, name, version2, opts = {}) {
|
|
|
4161
4446
|
return next;
|
|
4162
4447
|
}
|
|
4163
4448
|
|
|
4164
|
-
// src/recipes/svelte-5/index.ts
|
|
4165
|
-
init_spawn();
|
|
4166
|
-
|
|
4167
4449
|
// src/recipes/svelte-5/step-bump-versions.ts
|
|
4168
|
-
import { join as
|
|
4450
|
+
import { join as join15 } from "path";
|
|
4169
4451
|
var SVELTE_5_VERSIONS = {
|
|
4170
4452
|
svelte: "^5.55.5",
|
|
4171
4453
|
"@sveltejs/kit": "^2.59.0",
|
|
@@ -4178,7 +4460,7 @@ var SVELTE_5_VERSIONS = {
|
|
|
4178
4460
|
"typescript-svelte-plugin": "^0.3.52"
|
|
4179
4461
|
};
|
|
4180
4462
|
async function bumpToSvelte5Versions(cwd) {
|
|
4181
|
-
const pkgPath =
|
|
4463
|
+
const pkgPath = join15(cwd, "package.json");
|
|
4182
4464
|
const pkg = await readPackageJson(pkgPath);
|
|
4183
4465
|
let next = pkg;
|
|
4184
4466
|
for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
|
|
@@ -4191,7 +4473,7 @@ async function bumpToSvelte5Versions(cwd) {
|
|
|
4191
4473
|
|
|
4192
4474
|
// src/recipes/svelte-5/step-svelte-config.ts
|
|
4193
4475
|
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
4194
|
-
import { join as
|
|
4476
|
+
import { join as join16 } from "path";
|
|
4195
4477
|
var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
|
|
4196
4478
|
var IMPORT_FROM_VITE_PLUGIN = new RegExp(
|
|
4197
4479
|
String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
|
|
@@ -4232,7 +4514,7 @@ function dropPreprocessKey(source) {
|
|
|
4232
4514
|
return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
|
|
4233
4515
|
}
|
|
4234
4516
|
async function migrateSvelteConfig(cwd) {
|
|
4235
|
-
const path =
|
|
4517
|
+
const path = join16(cwd, "svelte.config.js");
|
|
4236
4518
|
let src;
|
|
4237
4519
|
try {
|
|
4238
4520
|
src = await readFile11(path, "utf-8");
|
|
@@ -4248,7 +4530,6 @@ async function migrateSvelteConfig(cwd) {
|
|
|
4248
4530
|
}
|
|
4249
4531
|
|
|
4250
4532
|
// src/recipes/svelte-5/step-svelte-migrate.ts
|
|
4251
|
-
init_spawn();
|
|
4252
4533
|
async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
4253
4534
|
try {
|
|
4254
4535
|
const { code, stderr } = await spawn2(
|
|
@@ -4270,10 +4551,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
|
|
|
4270
4551
|
}
|
|
4271
4552
|
|
|
4272
4553
|
// src/recipes/svelte-5/step-tailwind-upgrade.ts
|
|
4273
|
-
|
|
4274
|
-
import { join as join16 } from "path";
|
|
4554
|
+
import { join as join17 } from "path";
|
|
4275
4555
|
async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
4276
|
-
const pkg = await readPackageJson(
|
|
4556
|
+
const pkg = await readPackageJson(join17(cwd, "package.json"));
|
|
4277
4557
|
const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
|
|
4278
4558
|
if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
|
|
4279
4559
|
if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
|
|
@@ -4295,7 +4575,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
|
|
|
4295
4575
|
|
|
4296
4576
|
// src/recipes/svelte-5/step-gotchas.ts
|
|
4297
4577
|
import { readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
|
|
4298
|
-
import { join as
|
|
4578
|
+
import { join as join18 } from "path";
|
|
4299
4579
|
import { glob as glob2 } from "tinyglobby";
|
|
4300
4580
|
|
|
4301
4581
|
// src/recipes/svelte-5/codemods/on-event-to-handler.ts
|
|
@@ -4641,7 +4921,7 @@ async function planGotchaCodemods(cwd) {
|
|
|
4641
4921
|
const changes = [];
|
|
4642
4922
|
const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
|
|
4643
4923
|
for (const rel of relPaths) {
|
|
4644
|
-
const path =
|
|
4924
|
+
const path = join18(cwd, rel);
|
|
4645
4925
|
const before = await readFile12(path, "utf-8");
|
|
4646
4926
|
const after = CODEMODS.reduce((s, fn) => fn(s), before);
|
|
4647
4927
|
if (after !== before) changes.push({ rel, after });
|
|
@@ -4651,13 +4931,12 @@ async function planGotchaCodemods(cwd) {
|
|
|
4651
4931
|
async function applyGotchaCodemods(cwd) {
|
|
4652
4932
|
const changes = await planGotchaCodemods(cwd);
|
|
4653
4933
|
for (const c of changes) {
|
|
4654
|
-
await writeFile7(
|
|
4934
|
+
await writeFile7(join18(cwd, c.rel), c.after, "utf-8");
|
|
4655
4935
|
}
|
|
4656
4936
|
return { filesChanged: changes.length };
|
|
4657
4937
|
}
|
|
4658
4938
|
|
|
4659
4939
|
// src/recipes/svelte-5/step-verify.ts
|
|
4660
|
-
init_spawn();
|
|
4661
4940
|
async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
4662
4941
|
let install;
|
|
4663
4942
|
try {
|
|
@@ -4676,7 +4955,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
|
|
|
4676
4955
|
|
|
4677
4956
|
// src/recipes/svelte-5/step-summary.ts
|
|
4678
4957
|
import { writeFile as writeFile8 } from "fs/promises";
|
|
4679
|
-
import { join as
|
|
4958
|
+
import { join as join19 } from "path";
|
|
4680
4959
|
async function writeMigrationSummary(input) {
|
|
4681
4960
|
const lines = [
|
|
4682
4961
|
`# Svelte 4 \u2192 5 migration summary`,
|
|
@@ -4693,7 +4972,7 @@ async function writeMigrationSummary(input) {
|
|
|
4693
4972
|
`- Verify Playwright a11y tests still pass.`
|
|
4694
4973
|
];
|
|
4695
4974
|
const content = lines.join("\n") + "\n";
|
|
4696
|
-
const path =
|
|
4975
|
+
const path = join19(input.cwd, "MIGRATION_SVELTE_5.md");
|
|
4697
4976
|
await writeFile8(path, content, "utf-8");
|
|
4698
4977
|
return path;
|
|
4699
4978
|
}
|
|
@@ -4701,7 +4980,7 @@ async function writeMigrationSummary(input) {
|
|
|
4701
4980
|
// src/recipes/svelte-5/index.ts
|
|
4702
4981
|
async function alreadyOnSvelte5(cwd) {
|
|
4703
4982
|
try {
|
|
4704
|
-
const pkg = await readPackageJson(
|
|
4983
|
+
const pkg = await readPackageJson(join20(cwd, "package.json"));
|
|
4705
4984
|
const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
|
|
4706
4985
|
return !!v && /^\^?5\./.test(v);
|
|
4707
4986
|
} catch {
|
|
@@ -4776,9 +5055,12 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
|
|
|
4776
5055
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
4777
5056
|
cwd
|
|
4778
5057
|
});
|
|
5058
|
+
let skipped = [];
|
|
4779
5059
|
if (opts.fleet) {
|
|
4780
|
-
const workdir = opts.workdir ??
|
|
4781
|
-
|
|
5060
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
5061
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
5062
|
+
sites = prep.prepared;
|
|
5063
|
+
skipped = prep.skipped;
|
|
4782
5064
|
}
|
|
4783
5065
|
const results = [];
|
|
4784
5066
|
for (const s of sites) {
|
|
@@ -4788,7 +5070,7 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
|
|
|
4788
5070
|
}
|
|
4789
5071
|
const output = results.map(formatResult4).join("\n");
|
|
4790
5072
|
const code = results.some((r) => r.status === "failed") ? 1 : 0;
|
|
4791
|
-
return { output, code };
|
|
5073
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
4792
5074
|
}
|
|
4793
5075
|
|
|
4794
5076
|
// src/cli/commands/convert-to-pnpm.ts
|
|
@@ -4796,8 +5078,7 @@ import { resolve as resolve7 } from "path";
|
|
|
4796
5078
|
|
|
4797
5079
|
// src/recipes/convert-to-pnpm.ts
|
|
4798
5080
|
import { rm as rm3, stat as stat4 } from "fs/promises";
|
|
4799
|
-
import { join as
|
|
4800
|
-
init_spawn();
|
|
5081
|
+
import { join as join21 } from "path";
|
|
4801
5082
|
|
|
4802
5083
|
// src/recipes/convert-to-pnpm/script-rewrites.ts
|
|
4803
5084
|
function rewriteScriptForPnpm(script) {
|
|
@@ -4830,9 +5111,9 @@ async function exists3(path) {
|
|
|
4830
5111
|
async function convertToPnpm(site, opts = {}) {
|
|
4831
5112
|
const spawn2 = opts.spawn ?? defaultSpawn;
|
|
4832
5113
|
const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
|
|
4833
|
-
const pnpmLockPath =
|
|
4834
|
-
const npmLockPath =
|
|
4835
|
-
const yarnLockPath =
|
|
5114
|
+
const pnpmLockPath = join21(site.path, "pnpm-lock.yaml");
|
|
5115
|
+
const npmLockPath = join21(site.path, "package-lock.json");
|
|
5116
|
+
const yarnLockPath = join21(site.path, "yarn.lock");
|
|
4836
5117
|
return withRecipe({
|
|
4837
5118
|
name: "convert-to-pnpm",
|
|
4838
5119
|
site,
|
|
@@ -4855,7 +5136,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
4855
5136
|
if (hasYarnLock) await rm3(yarnLockPath, { force: true });
|
|
4856
5137
|
const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
|
|
4857
5138
|
await commit2(`chore(pnpm): remove ${sourceLock}`);
|
|
4858
|
-
const pkgPath =
|
|
5139
|
+
const pkgPath = join21(cwd, "package.json");
|
|
4859
5140
|
const pkg = await readPackageJson(pkgPath);
|
|
4860
5141
|
const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
|
|
4861
5142
|
if (pkg.scripts && typeof pkg.scripts === "object") {
|
|
@@ -4868,7 +5149,7 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
4868
5149
|
}
|
|
4869
5150
|
await writePackageJson(pkgPath, next);
|
|
4870
5151
|
await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
|
|
4871
|
-
await rm3(
|
|
5152
|
+
await rm3(join21(cwd, "node_modules"), { recursive: true, force: true });
|
|
4872
5153
|
const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
|
|
4873
5154
|
if (installResult.code !== 0) {
|
|
4874
5155
|
return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
|
|
@@ -4893,15 +5174,18 @@ async function runConvertToPnpmCommand(site, opts) {
|
|
|
4893
5174
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
4894
5175
|
cwd
|
|
4895
5176
|
});
|
|
5177
|
+
let skipped = [];
|
|
4896
5178
|
if (opts.fleet) {
|
|
4897
|
-
const workdir = opts.workdir ??
|
|
4898
|
-
|
|
5179
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
5180
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
5181
|
+
sites = prep.prepared;
|
|
5182
|
+
skipped = prep.skipped;
|
|
4899
5183
|
}
|
|
4900
5184
|
const results = [];
|
|
4901
5185
|
for (const s of sites) results.push(await convertToPnpm(s));
|
|
4902
5186
|
const output = results.map(formatResult5).join("\n");
|
|
4903
5187
|
const code = results.some((r) => r.status === "failed") ? 1 : 0;
|
|
4904
|
-
return { output, code };
|
|
5188
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
4905
5189
|
}
|
|
4906
5190
|
|
|
4907
5191
|
// src/cli/commands/onboard.ts
|
|
@@ -4909,18 +5193,17 @@ import { resolve as resolve8 } from "path";
|
|
|
4909
5193
|
|
|
4910
5194
|
// src/recipes/onboard.ts
|
|
4911
5195
|
import { stat as stat5 } from "fs/promises";
|
|
4912
|
-
import { join as
|
|
4913
|
-
init_spawn();
|
|
5196
|
+
import { join as join23 } from "path";
|
|
4914
5197
|
|
|
4915
5198
|
// src/util/self-version.ts
|
|
4916
5199
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
4917
5200
|
import { fileURLToPath } from "url";
|
|
4918
|
-
import { dirname as dirname3, join as
|
|
5201
|
+
import { dirname as dirname3, join as join22 } from "path";
|
|
4919
5202
|
function selfPackageVersion(callerImportMetaUrl) {
|
|
4920
5203
|
try {
|
|
4921
5204
|
let dir = dirname3(fileURLToPath(callerImportMetaUrl));
|
|
4922
5205
|
while (true) {
|
|
4923
|
-
const candidate =
|
|
5206
|
+
const candidate = join22(dir, "package.json");
|
|
4924
5207
|
if (existsSync2(candidate)) {
|
|
4925
5208
|
const raw = readFileSync2(candidate, "utf-8");
|
|
4926
5209
|
const pkg = JSON.parse(raw);
|
|
@@ -4991,13 +5274,13 @@ async function onboard(site, opts = {}) {
|
|
|
4991
5274
|
name: "onboard",
|
|
4992
5275
|
site,
|
|
4993
5276
|
plan: async () => {
|
|
4994
|
-
if (!await exists4(
|
|
5277
|
+
if (!await exists4(join23(site.path, "pnpm-lock.yaml"))) {
|
|
4995
5278
|
return {
|
|
4996
5279
|
kind: "failed",
|
|
4997
5280
|
notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
|
|
4998
5281
|
};
|
|
4999
5282
|
}
|
|
5000
|
-
const pkgPath =
|
|
5283
|
+
const pkgPath = join23(site.path, "package.json");
|
|
5001
5284
|
const pkg = await readPackageJson(pkgPath);
|
|
5002
5285
|
const toAdd = [];
|
|
5003
5286
|
if (!isDeclared(pkg, PACKAGE_NAME)) {
|
|
@@ -5020,7 +5303,7 @@ async function onboard(site, opts = {}) {
|
|
|
5020
5303
|
return { kind: "apply", plan: { pkg, toAdd } };
|
|
5021
5304
|
},
|
|
5022
5305
|
apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
|
|
5023
|
-
const pkgPath =
|
|
5306
|
+
const pkgPath = join23(cwd, "package.json");
|
|
5024
5307
|
let next = pkg;
|
|
5025
5308
|
for (const dep of toAdd) {
|
|
5026
5309
|
next = bumpDep(next, dep.name, dep.version);
|
|
@@ -5071,9 +5354,12 @@ async function runOnboardCommand(site, opts) {
|
|
|
5071
5354
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
5072
5355
|
cwd
|
|
5073
5356
|
});
|
|
5357
|
+
let skipped = [];
|
|
5074
5358
|
if (opts.fleet) {
|
|
5075
|
-
const workdir = opts.workdir ??
|
|
5076
|
-
|
|
5359
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
5360
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
5361
|
+
sites = prep.prepared;
|
|
5362
|
+
skipped = prep.skipped;
|
|
5077
5363
|
}
|
|
5078
5364
|
const results = [];
|
|
5079
5365
|
for (const s of sites) {
|
|
@@ -5081,7 +5367,7 @@ async function runOnboardCommand(site, opts) {
|
|
|
5081
5367
|
}
|
|
5082
5368
|
const output = results.map(formatResult6).join("\n");
|
|
5083
5369
|
const code = results.some((r) => r.status === "failed") ? 1 : 0;
|
|
5084
|
-
return { output, code };
|
|
5370
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
5085
5371
|
}
|
|
5086
5372
|
|
|
5087
5373
|
// src/cli/commands/svelte-codemods.ts
|
|
@@ -5089,7 +5375,7 @@ import { resolve as resolve9 } from "path";
|
|
|
5089
5375
|
|
|
5090
5376
|
// src/recipes/svelte-codemods.ts
|
|
5091
5377
|
import { writeFile as writeFile9 } from "fs/promises";
|
|
5092
|
-
import { join as
|
|
5378
|
+
import { join as join24 } from "path";
|
|
5093
5379
|
async function svelteCodemods(site) {
|
|
5094
5380
|
return withRecipe({
|
|
5095
5381
|
name: "svelte-codemods",
|
|
@@ -5103,7 +5389,7 @@ async function svelteCodemods(site) {
|
|
|
5103
5389
|
},
|
|
5104
5390
|
apply: async (changes, { commit: commit2, cwd }) => {
|
|
5105
5391
|
for (const c of changes) {
|
|
5106
|
-
await writeFile9(
|
|
5392
|
+
await writeFile9(join24(cwd, c.rel), c.after, "utf-8");
|
|
5107
5393
|
}
|
|
5108
5394
|
await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
|
|
5109
5395
|
return { kind: "ok" };
|
|
@@ -5125,15 +5411,18 @@ async function runSvelteCodemodsCommand(site, opts) {
|
|
|
5125
5411
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
5126
5412
|
cwd
|
|
5127
5413
|
});
|
|
5414
|
+
let skipped = [];
|
|
5128
5415
|
if (opts.fleet) {
|
|
5129
|
-
const workdir = opts.workdir ??
|
|
5130
|
-
|
|
5416
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
5417
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
5418
|
+
sites = prep.prepared;
|
|
5419
|
+
skipped = prep.skipped;
|
|
5131
5420
|
}
|
|
5132
5421
|
const results = [];
|
|
5133
5422
|
for (const s of sites) results.push(await svelteCodemods(s));
|
|
5134
5423
|
const output = results.map(formatResult7).join("\n");
|
|
5135
5424
|
const code = results.some((r) => r.status === "failed") ? 1 : 0;
|
|
5136
|
-
return { output, code };
|
|
5425
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
5137
5426
|
}
|
|
5138
5427
|
|
|
5139
5428
|
// src/cli/commands/report.ts
|
|
@@ -5179,8 +5468,15 @@ function findDueReports(websites, reports, today) {
|
|
|
5179
5468
|
for (const site of websites) {
|
|
5180
5469
|
if (site.status !== null && !ELIGIBLE_STATUSES.has(site.status)) continue;
|
|
5181
5470
|
for (const type of ["Maintenance", "Testing"]) {
|
|
5182
|
-
const
|
|
5183
|
-
|
|
5471
|
+
const rawFreq = type === "Maintenance" ? site.maintenanceFreq : site.testingFreq;
|
|
5472
|
+
const freq = typeof rawFreq === "string" ? rawFreq.trim() : rawFreq;
|
|
5473
|
+
if (freq === "None" || freq === "") continue;
|
|
5474
|
+
if (!(freq in MONTHS)) {
|
|
5475
|
+
console.warn(
|
|
5476
|
+
`\u26A0 ${site.name}: unrecognized ${type === "Maintenance" ? "maintenance" : "testing"} frequency '${rawFreq}' \u2014 not scheduling; fix the Airtable value`
|
|
5477
|
+
);
|
|
5478
|
+
continue;
|
|
5479
|
+
}
|
|
5184
5480
|
const lastSent = lastSentForType(reports, site.id, type);
|
|
5185
5481
|
const fallback = type === "Maintenance" ? site.maintenanceDay : site.testingDay;
|
|
5186
5482
|
const baseIso = lastSent ?? fallback;
|
|
@@ -5214,11 +5510,11 @@ import { dirname as dirname6 } from "path";
|
|
|
5214
5510
|
|
|
5215
5511
|
// src/reports/ga/config.ts
|
|
5216
5512
|
init_credentials();
|
|
5217
|
-
import { dirname as dirname5, join as
|
|
5513
|
+
import { dirname as dirname5, join as join26 } from "path";
|
|
5218
5514
|
function readGaConfig() {
|
|
5219
5515
|
const subject = process.env.GA_SUBJECT?.trim();
|
|
5220
5516
|
if (!subject) return null;
|
|
5221
|
-
const keyPath = process.env.GA_SA_KEY_PATH?.trim() ||
|
|
5517
|
+
const keyPath = process.env.GA_SA_KEY_PATH?.trim() || join26(dirname5(defaultCredentialsPath()), "ga-service-account.json");
|
|
5222
5518
|
return { subject, keyPath };
|
|
5223
5519
|
}
|
|
5224
5520
|
|
|
@@ -5377,6 +5673,10 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
5377
5673
|
return { reportRow: null, htmlPath: path, html, softFailures };
|
|
5378
5674
|
}
|
|
5379
5675
|
if (base === null) throw new Error("base required when previewOnly=false");
|
|
5676
|
+
if (options.completeRowId) {
|
|
5677
|
+
await finishDraftRow(base, options.completeRowId, slug, periodEnd, html);
|
|
5678
|
+
return { reportRow: options.existingRow ?? null, htmlPath: null, html, softFailures };
|
|
5679
|
+
}
|
|
5380
5680
|
const reportId = `${siteRow.name} \u2014 ${reportType} \u2014 ${periodEnd.toISOString().slice(0, 10)}`;
|
|
5381
5681
|
const created = await createDraft(base, {
|
|
5382
5682
|
reportId,
|
|
@@ -5392,11 +5692,14 @@ async function draftReportForSite(base, siteRow, reportType, options = {}) {
|
|
|
5392
5692
|
...search ? { searchFoundPage1: search.foundOnPage1 } : {},
|
|
5393
5693
|
...search?.foundOnPage1 && search.position !== null ? { searchPosition: search.position } : {}
|
|
5394
5694
|
});
|
|
5395
|
-
|
|
5396
|
-
await uploadAttachment(created.id, "Rendered HTML", html, htmlFilename, "text/html");
|
|
5397
|
-
await setDraftReady(base, created.id, true);
|
|
5695
|
+
await finishDraftRow(base, created.id, slug, periodEnd, html);
|
|
5398
5696
|
return { reportRow: created, htmlPath: null, html, softFailures };
|
|
5399
5697
|
}
|
|
5698
|
+
async function finishDraftRow(base, rowId, slug, periodEnd, html) {
|
|
5699
|
+
const htmlFilename = `${slug}-${periodEnd.toISOString().slice(0, 10)}.html`;
|
|
5700
|
+
await uploadAttachment(rowId, "Rendered HTML", html, htmlFilename, "text/html");
|
|
5701
|
+
await setDraftReady(base, rowId, true);
|
|
5702
|
+
}
|
|
5400
5703
|
var NO_ENRICHMENT = { value: null, softFailed: false };
|
|
5401
5704
|
async function fetchGaUsers(siteRow, periodStart, periodEnd) {
|
|
5402
5705
|
const cfg = readGaConfig();
|
|
@@ -5484,12 +5787,39 @@ async function draftDueReports(base, today) {
|
|
|
5484
5787
|
let skipped = 0;
|
|
5485
5788
|
for (const item of due) {
|
|
5486
5789
|
const period = reportPeriodKey(item.dueDate);
|
|
5487
|
-
const
|
|
5790
|
+
const existing = reports.find(
|
|
5488
5791
|
(r) => r.siteId === item.site.id && r.reportType === item.reportType && r.period === period
|
|
5489
5792
|
);
|
|
5490
|
-
if (
|
|
5793
|
+
if (existing) {
|
|
5794
|
+
if (existing.draftReady) {
|
|
5795
|
+
skipped++;
|
|
5796
|
+
lines.push(`\u2022 skipped (already drafted ${period}): ${item.site.name} ${item.reportType}`);
|
|
5797
|
+
continue;
|
|
5798
|
+
}
|
|
5799
|
+
try {
|
|
5800
|
+
const result = await draftReportForSite(base, item.site, item.reportType, {
|
|
5801
|
+
period,
|
|
5802
|
+
completeRowId: existing.id,
|
|
5803
|
+
existingRow: existing
|
|
5804
|
+
});
|
|
5805
|
+
existing.draftReady = true;
|
|
5806
|
+
lines.push(
|
|
5807
|
+
`\u2713 completed half-made draft: ${result.reportRow?.reportId ?? existing.reportId}`
|
|
5808
|
+
);
|
|
5809
|
+
if (result.softFailures.length > 0) softFailedSites++;
|
|
5810
|
+
} catch (e) {
|
|
5811
|
+
lines.push(`\u2717 failed: ${item.site.name} ${item.reportType} \u2014 ${e.message}`);
|
|
5812
|
+
}
|
|
5813
|
+
continue;
|
|
5814
|
+
}
|
|
5815
|
+
const pendingEarlier = reports.find(
|
|
5816
|
+
(r) => r.siteId === item.site.id && r.reportType === item.reportType && r.sentAt === null && r.period !== null && r.period < period
|
|
5817
|
+
);
|
|
5818
|
+
if (pendingEarlier) {
|
|
5491
5819
|
skipped++;
|
|
5492
|
-
lines.push(
|
|
5820
|
+
lines.push(
|
|
5821
|
+
`\u2022 skipped: ${item.site.name} ${item.reportType} already has an unsent ${pendingEarlier.period} draft pending approval`
|
|
5822
|
+
);
|
|
5493
5823
|
continue;
|
|
5494
5824
|
}
|
|
5495
5825
|
try {
|
|
@@ -5502,7 +5832,7 @@ async function draftDueReports(base, today) {
|
|
|
5502
5832
|
}
|
|
5503
5833
|
}
|
|
5504
5834
|
if (skipped > 0) {
|
|
5505
|
-
lines.push(`\u2022 ${skipped} already drafted this period`);
|
|
5835
|
+
lines.push(`\u2022 ${skipped} already drafted or pending this period`);
|
|
5506
5836
|
}
|
|
5507
5837
|
if (softFailedSites > 0) {
|
|
5508
5838
|
lines.push(
|
|
@@ -5532,7 +5862,7 @@ import { resolve as resolve10 } from "path";
|
|
|
5532
5862
|
|
|
5533
5863
|
// src/recipes/a11y-fixtures-page/index.ts
|
|
5534
5864
|
import { access, mkdir as mkdir5, writeFile as writeFile11 } from "fs/promises";
|
|
5535
|
-
import { dirname as dirname7, join as
|
|
5865
|
+
import { dirname as dirname7, join as join27 } from "path";
|
|
5536
5866
|
|
|
5537
5867
|
// src/recipes/a11y-fixtures-page/template.ts
|
|
5538
5868
|
var A11Y_FIXTURES_PAGE_RELATIVE = "src/routes/dev/a11y-fixtures/+page.svelte";
|
|
@@ -5583,7 +5913,7 @@ async function fileExists(path) {
|
|
|
5583
5913
|
}
|
|
5584
5914
|
}
|
|
5585
5915
|
async function a11yFixturesPage(site) {
|
|
5586
|
-
const target =
|
|
5916
|
+
const target = join27(site.path, A11Y_FIXTURES_PAGE_RELATIVE);
|
|
5587
5917
|
return withRecipe({
|
|
5588
5918
|
name: "a11y-fixtures-page",
|
|
5589
5919
|
site,
|
|
@@ -5678,15 +6008,18 @@ async function runInitCommand(site, opts) {
|
|
|
5678
6008
|
...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
|
|
5679
6009
|
cwd
|
|
5680
6010
|
});
|
|
6011
|
+
let skipped = [];
|
|
5681
6012
|
if (opts.fleet) {
|
|
5682
|
-
const workdir = opts.workdir ??
|
|
5683
|
-
|
|
6013
|
+
const workdir = opts.workdir ?? fleetWorkdir();
|
|
6014
|
+
const prep = await prepareFleetSites(sites, { workdir });
|
|
6015
|
+
sites = prep.prepared;
|
|
6016
|
+
skipped = prep.skipped;
|
|
5684
6017
|
}
|
|
5685
6018
|
const results = [];
|
|
5686
6019
|
for (const s of sites) results.push(await init(s));
|
|
5687
6020
|
const output = results.map(formatResult8).join("\n\n");
|
|
5688
6021
|
const code = results.some((r) => exitCodeFor(r) !== 0) ? 1 : 0;
|
|
5689
|
-
return { output, code };
|
|
6022
|
+
return { output: appendSkipNotice(output, skipped), code };
|
|
5690
6023
|
}
|
|
5691
6024
|
|
|
5692
6025
|
// src/cli/commands/launch.ts
|
|
@@ -5755,7 +6088,12 @@ async function launch(site, deps = {}) {
|
|
|
5755
6088
|
let report;
|
|
5756
6089
|
try {
|
|
5757
6090
|
const existing = await findReportByPeriod(base, target.id, "Launch", period);
|
|
5758
|
-
|
|
6091
|
+
if (existing) {
|
|
6092
|
+
await updateReportScores(base, existing.id, scores, today);
|
|
6093
|
+
report = existing;
|
|
6094
|
+
} else {
|
|
6095
|
+
report = await createDraft(base, draftInputFor(target, scores, today, period));
|
|
6096
|
+
}
|
|
5759
6097
|
} catch (err) {
|
|
5760
6098
|
steps.push({ name: "draft", result: errorOf(err) });
|
|
5761
6099
|
return stop();
|
|
@@ -5849,8 +6187,16 @@ async function runLaunchCommand(site, opts) {
|
|
|
5849
6187
|
init_client();
|
|
5850
6188
|
init_websites();
|
|
5851
6189
|
|
|
6190
|
+
// src/alerts/renovate.ts
|
|
6191
|
+
var RENOVATE_HEAD_PREFIXES = ["renovate/", "renovate-"];
|
|
6192
|
+
function isRenovatePR(pr) {
|
|
6193
|
+
return RENOVATE_HEAD_PREFIXES.some((p) => pr.headRef.startsWith(p));
|
|
6194
|
+
}
|
|
6195
|
+
function isFailingRenovatePR(pr) {
|
|
6196
|
+
return isRenovatePR(pr) && pr.ciState === "failing";
|
|
6197
|
+
}
|
|
6198
|
+
|
|
5852
6199
|
// src/audits/github-signals.ts
|
|
5853
|
-
init_renovate();
|
|
5854
6200
|
async function collectGitHubSignals(sites, deps, onSkip = () => {
|
|
5855
6201
|
}) {
|
|
5856
6202
|
const rows = [];
|
|
@@ -5877,8 +6223,10 @@ async function collectGitHubSignals(sites, deps, onSkip = () => {
|
|
|
5877
6223
|
}
|
|
5878
6224
|
|
|
5879
6225
|
// src/cli/commands/github-signals.ts
|
|
5880
|
-
init_gh();
|
|
5881
6226
|
init_write_audits_to_airtable();
|
|
6227
|
+
function githubSignalsExitCode(written, failed) {
|
|
6228
|
+
return failed > written ? 1 : 0;
|
|
6229
|
+
}
|
|
5882
6230
|
async function runGitHubSignalsCommand(opts) {
|
|
5883
6231
|
if (!opts.fleet || !opts.writeAirtable) {
|
|
5884
6232
|
return { output: "github-signals currently supports only --fleet --write-airtable", code: 2 };
|
|
@@ -5935,18 +6283,18 @@ async function runGitHubSignalsCommand(opts) {
|
|
|
5935
6283
|
for (const repo of skipped) result.failed.push({ slug: repo, error: "probe failed (skipped)" });
|
|
5936
6284
|
return {
|
|
5937
6285
|
output: formatFleetWriteSummary(result),
|
|
5938
|
-
code: result.
|
|
6286
|
+
code: githubSignalsExitCode(result.written.length, result.failed.length)
|
|
5939
6287
|
};
|
|
5940
6288
|
}
|
|
5941
6289
|
|
|
5942
6290
|
// src/cli/version.ts
|
|
5943
6291
|
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
5944
|
-
import { dirname as dirname8, join as
|
|
6292
|
+
import { dirname as dirname8, join as join28 } from "path";
|
|
5945
6293
|
function resolvePackageVersion(fromDir) {
|
|
5946
6294
|
try {
|
|
5947
6295
|
let dir = fromDir;
|
|
5948
6296
|
while (true) {
|
|
5949
|
-
const candidate =
|
|
6297
|
+
const candidate = join28(dir, "package.json");
|
|
5950
6298
|
if (existsSync4(candidate)) {
|
|
5951
6299
|
const raw = readFileSync5(candidate, "utf-8");
|
|
5952
6300
|
const pkg = JSON.parse(raw);
|
|
@@ -5989,7 +6337,8 @@ async function runOrExit(fn, opts) {
|
|
|
5989
6337
|
try {
|
|
5990
6338
|
const { output, code } = await fn();
|
|
5991
6339
|
console.log(output);
|
|
5992
|
-
process.
|
|
6340
|
+
process.exitCode = code;
|
|
6341
|
+
return;
|
|
5993
6342
|
} catch (err) {
|
|
5994
6343
|
const e = err;
|
|
5995
6344
|
console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
|
|
@@ -6110,5 +6459,15 @@ cli.command(
|
|
|
6110
6459
|
);
|
|
6111
6460
|
cli.help();
|
|
6112
6461
|
cli.version(version);
|
|
6462
|
+
cli.on("command:*", () => {
|
|
6463
|
+
const unknown = cli.args[0] ?? process.argv[2] ?? "";
|
|
6464
|
+
console.error(
|
|
6465
|
+
`error: unknown command '${unknown}'. Run 'reddoor-maint --help' to see available commands.`
|
|
6466
|
+
);
|
|
6467
|
+
process.exit(1);
|
|
6468
|
+
});
|
|
6113
6469
|
cli.parse();
|
|
6470
|
+
export {
|
|
6471
|
+
runOrExit
|
|
6472
|
+
};
|
|
6114
6473
|
//# sourceMappingURL=bin.js.map
|