@reddoorla/maintenance 0.60.1 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/airtable-HHAWHBNM.js +9 -0
- package/dist/{announce-Q2DXP5OO.js → announce-54EBITHX.js} +16 -6
- package/dist/announce-54EBITHX.js.map +1 -0
- package/dist/{bump-deps-RYHQIKW4.js → bump-deps-2U6GIHM5.js} +2 -2
- package/dist/{chunk-BWLUNY7L.js → chunk-2T3HZ3DB.js} +24 -14
- package/dist/chunk-2T3HZ3DB.js.map +1 -0
- package/dist/{chunk-D3LFQNJK.js → chunk-3HM4S422.js} +2 -2
- package/dist/{chunk-LMXIZITU.js → chunk-4REUHZAW.js} +40 -4
- package/dist/chunk-4REUHZAW.js.map +1 -0
- package/dist/chunk-5L3FPQIJ.js +92 -0
- package/dist/chunk-5L3FPQIJ.js.map +1 -0
- package/dist/{client-JA6QLEDW.js → chunk-6ZKM7OIJ.js} +5 -4
- package/dist/{chunk-KC4HU7BC.js → chunk-76VIUWR3.js} +31 -6
- package/dist/chunk-76VIUWR3.js.map +1 -0
- package/dist/{chunk-L4ULXHIN.js → chunk-BLD6AYJO.js} +15 -1
- package/dist/chunk-BLD6AYJO.js.map +1 -0
- package/dist/chunk-BMDBOPN3.js +211 -0
- package/dist/chunk-BMDBOPN3.js.map +1 -0
- package/dist/{chunk-HB7QPECR.js → chunk-KTPPKO2C.js} +6 -6
- package/dist/chunk-L5F664ZA.js +45 -0
- package/dist/chunk-L5F664ZA.js.map +1 -0
- package/dist/{chunk-XB6T4ETK.js → chunk-OSRFDGQM.js} +21 -1
- package/dist/chunk-OSRFDGQM.js.map +1 -0
- package/dist/{chunk-6CSCLBAA.js → chunk-SAOCVNX7.js} +2 -2
- package/dist/{chunk-5PHCZ6L5.js → chunk-SS7BKOIX.js} +89 -2
- package/dist/chunk-SS7BKOIX.js.map +1 -0
- package/dist/{chunk-TZN2G6FD.js → chunk-TYRCYHRA.js} +3 -3
- package/dist/{chunk-HG5DTFNF.js → chunk-XKKTK6WA.js} +17 -1
- package/dist/chunk-XKKTK6WA.js.map +1 -0
- package/dist/{chunk-JNXCDUYB.js → chunk-XVR7UM6Z.js} +4 -4
- package/dist/{chunk-HPSGCDWY.js → chunk-YYUY2OVP.js} +2 -2
- package/dist/{chunk-TLZ4V2BE.js → chunk-ZEX2T6JH.js} +3 -2
- package/dist/{chunk-TLZ4V2BE.js.map → chunk-ZEX2T6JH.js.map} +1 -1
- package/dist/cli/bin.js +16 -15
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +3 -2
- package/dist/cli/commands/audit.js +29 -8
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-GTMJTXBK.js +11 -0
- package/dist/{convert-to-pnpm-KNCXU6FM.js → convert-to-pnpm-JG3767ZQ.js} +2 -2
- package/dist/{db-AXKMRTVA.js → db-Y6BVTNH7.js} +3 -3
- package/dist/{digest-RVIOHUXR.js → digest-ABJROO4H.js} +13 -197
- package/dist/digest-ABJROO4H.js.map +1 -0
- package/dist/{github-signals-UL2BBS3Q.js → github-signals-XTJLKOXJ.js} +26 -5
- package/dist/github-signals-XTJLKOXJ.js.map +1 -0
- package/dist/index.d.ts +62 -5
- package/dist/index.js +345 -206
- package/dist/index.js.map +1 -1
- package/dist/{init-KD56UENX.js → init-DMAIKUQE.js} +8 -8
- package/dist/{launch-UWCI3JRM.js → launch-UZPPB4IE.js} +10 -9
- package/dist/{launch-UWCI3JRM.js.map → launch-UZPPB4IE.js.map} +1 -1
- package/dist/{lighthouse-airtable-N3KCBPVA.js → lighthouse-airtable-HTEW6BBT.js} +3 -3
- package/dist/migrate-BFOG2OCP.js +7 -0
- package/dist/{onboard-SCZ3UKZ2.js → onboard-TTIC3YZI.js} +2 -2
- package/dist/{orchestrate-D72PB6NG.js → orchestrate-5GGOL5EX.js} +7 -4
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/recipes/sync-configs.js +2 -2
- package/dist/{renovate-dispatch-NG5KUQKY.js → renovate-dispatch-QDDMTATD.js} +3 -3
- package/dist/{report-CN56TGMK.js → report-SZGZIDDG.js} +6 -6
- package/dist/{self-updating-CFHXJSAX.js → self-updating-2RLUP4AA.js} +4 -4
- package/dist/{svelte-codemods-HNITLWQM.js → svelte-codemods-BPLKSVIA.js} +2 -2
- package/dist/{sync-configs-BZXBYKTI.js → sync-configs-VFRV3TJ4.js} +4 -4
- package/dist/{types-QG-QhCYh.d.ts → types-Dg4mHvyf.d.ts} +6 -1
- package/dist/{upgrade-6YDUELFA.js → upgrade-WCFD2TRN.js} +2 -2
- package/dist/{websites-IYP7IGLU.js → websites-HM2NMZMR.js} +2 -2
- package/dist/{write-audits-to-airtable-VDGHCZGQ.js → write-audits-to-airtable-QF26PCAR.js} +5 -4
- package/dist/write-audits-to-airtable-QF26PCAR.js.map +1 -0
- package/package.json +1 -1
- package/dist/airtable-YZ6VMSQZ.js +0 -9
- package/dist/announce-Q2DXP5OO.js.map +0 -1
- package/dist/chunk-5PHCZ6L5.js.map +0 -1
- package/dist/chunk-BWLUNY7L.js.map +0 -1
- package/dist/chunk-HG5DTFNF.js.map +0 -1
- package/dist/chunk-KC4HU7BC.js.map +0 -1
- package/dist/chunk-L4ULXHIN.js.map +0 -1
- package/dist/chunk-LMXIZITU.js.map +0 -1
- package/dist/chunk-XB6T4ETK.js.map +0 -1
- package/dist/digest-RVIOHUXR.js.map +0 -1
- package/dist/github-signals-UL2BBS3Q.js.map +0 -1
- package/dist/migrate-P2LS4WXI.js +0 -7
- /package/dist/{airtable-YZ6VMSQZ.js.map → airtable-HHAWHBNM.js.map} +0 -0
- /package/dist/{bump-deps-RYHQIKW4.js.map → bump-deps-2U6GIHM5.js.map} +0 -0
- /package/dist/{chunk-D3LFQNJK.js.map → chunk-3HM4S422.js.map} +0 -0
- /package/dist/{client-JA6QLEDW.js.map → chunk-6ZKM7OIJ.js.map} +0 -0
- /package/dist/{chunk-HB7QPECR.js.map → chunk-KTPPKO2C.js.map} +0 -0
- /package/dist/{chunk-6CSCLBAA.js.map → chunk-SAOCVNX7.js.map} +0 -0
- /package/dist/{chunk-TZN2G6FD.js.map → chunk-TYRCYHRA.js.map} +0 -0
- /package/dist/{chunk-JNXCDUYB.js.map → chunk-XVR7UM6Z.js.map} +0 -0
- /package/dist/{chunk-HPSGCDWY.js.map → chunk-YYUY2OVP.js.map} +0 -0
- /package/dist/{lighthouse-airtable-N3KCBPVA.js.map → client-GTMJTXBK.js.map} +0 -0
- /package/dist/{convert-to-pnpm-KNCXU6FM.js.map → convert-to-pnpm-JG3767ZQ.js.map} +0 -0
- /package/dist/{db-AXKMRTVA.js.map → db-Y6BVTNH7.js.map} +0 -0
- /package/dist/{init-KD56UENX.js.map → init-DMAIKUQE.js.map} +0 -0
- /package/dist/{migrate-P2LS4WXI.js.map → lighthouse-airtable-HTEW6BBT.js.map} +0 -0
- /package/dist/{orchestrate-D72PB6NG.js.map → migrate-BFOG2OCP.js.map} +0 -0
- /package/dist/{onboard-SCZ3UKZ2.js.map → onboard-TTIC3YZI.js.map} +0 -0
- /package/dist/{websites-IYP7IGLU.js.map → orchestrate-5GGOL5EX.js.map} +0 -0
- /package/dist/{renovate-dispatch-NG5KUQKY.js.map → renovate-dispatch-QDDMTATD.js.map} +0 -0
- /package/dist/{report-CN56TGMK.js.map → report-SZGZIDDG.js.map} +0 -0
- /package/dist/{self-updating-CFHXJSAX.js.map → self-updating-2RLUP4AA.js.map} +0 -0
- /package/dist/{svelte-codemods-HNITLWQM.js.map → svelte-codemods-BPLKSVIA.js.map} +0 -0
- /package/dist/{sync-configs-BZXBYKTI.js.map → sync-configs-VFRV3TJ4.js.map} +0 -0
- /package/dist/{upgrade-6YDUELFA.js.map → upgrade-WCFD2TRN.js.map} +0 -0
- /package/dist/{write-audits-to-airtable-VDGHCZGQ.js.map → websites-HM2NMZMR.js.map} +0 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-4NAOR44V.js";
|
|
6
6
|
import {
|
|
7
7
|
resolveSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-SAOCVNX7.js";
|
|
9
9
|
import {
|
|
10
10
|
convertToPnpm
|
|
11
11
|
} from "./chunk-2X322ZES.js";
|
|
@@ -48,4 +48,4 @@ async function runConvertToPnpmCommand(site, opts) {
|
|
|
48
48
|
export {
|
|
49
49
|
runConvertToPnpmCommand
|
|
50
50
|
};
|
|
51
|
-
//# sourceMappingURL=convert-to-pnpm-
|
|
51
|
+
//# sourceMappingURL=convert-to-pnpm-JG3767ZQ.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/cli/commands/db.ts
|
|
2
2
|
async function runDbCommand(action, opts) {
|
|
3
3
|
if (action === "migrate") {
|
|
4
|
-
const { readDbConfig } = await import("./client-
|
|
4
|
+
const { readDbConfig } = await import("./client-GTMJTXBK.js");
|
|
5
5
|
const cfg = opts.url ? { url: opts.url } : readDbConfig();
|
|
6
|
-
const { runMigrations } = await import("./migrate-
|
|
6
|
+
const { runMigrations } = await import("./migrate-BFOG2OCP.js");
|
|
7
7
|
const { createClient } = await import("@libsql/client");
|
|
8
8
|
const client = createClient(cfg.url === ":memory:" ? { url: ":memory:" } : cfg);
|
|
9
9
|
const ran = await runMigrations(client);
|
|
@@ -20,4 +20,4 @@ async function runDbCommand(action, opts) {
|
|
|
20
20
|
export {
|
|
21
21
|
runDbCommand
|
|
22
22
|
};
|
|
23
|
-
//# sourceMappingURL=db-
|
|
23
|
+
//# sourceMappingURL=db-Y6BVTNH7.js.map
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectAnalyticsFailures,
|
|
3
|
+
collectCiAlerts,
|
|
4
|
+
collectDeliveryFailures,
|
|
5
|
+
collectLighthouseAlerts,
|
|
6
|
+
collectRenovateAlerts,
|
|
7
|
+
collectVulnAlerts,
|
|
8
|
+
diffAttention,
|
|
9
|
+
readDigestState,
|
|
10
|
+
writeDigestState
|
|
11
|
+
} from "./chunk-BMDBOPN3.js";
|
|
1
12
|
import {
|
|
2
13
|
isIdempotencyConflict
|
|
3
14
|
} from "./chunk-OGGRFBL4.js";
|
|
@@ -9,7 +20,7 @@ import {
|
|
|
9
20
|
import {
|
|
10
21
|
listWebsites,
|
|
11
22
|
siteSlug
|
|
12
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-BLD6AYJO.js";
|
|
13
24
|
import {
|
|
14
25
|
openBase,
|
|
15
26
|
readAirtableConfig
|
|
@@ -19,201 +30,6 @@ import {
|
|
|
19
30
|
} from "./chunk-WEKL2HYJ.js";
|
|
20
31
|
import "./chunk-6VAL7XJT.js";
|
|
21
32
|
|
|
22
|
-
// src/alerts/digest-collectors.ts
|
|
23
|
-
function dashboardUrl(baseUrl, siteName) {
|
|
24
|
-
return `${baseUrl.replace(/\/$/, "")}/s/${siteSlug(siteName)}`;
|
|
25
|
-
}
|
|
26
|
-
var GITHUB_SIGNALS_STALE_DAYS = 3;
|
|
27
|
-
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
28
|
-
function gitHubSignalsStale(swept, now) {
|
|
29
|
-
if (swept === null) return true;
|
|
30
|
-
const ageMs = now.getTime() - Date.parse(swept);
|
|
31
|
-
if (!Number.isFinite(ageMs)) return true;
|
|
32
|
-
return ageMs > GITHUB_SIGNALS_STALE_DAYS * MS_PER_DAY;
|
|
33
|
-
}
|
|
34
|
-
var AUTO_FIX_EXHAUSTED_CYCLES = 3;
|
|
35
|
-
function collectVulnAlerts(sites, baseUrl) {
|
|
36
|
-
const items = [];
|
|
37
|
-
for (const s of sites) {
|
|
38
|
-
const critical = s.securityVulnsCritical ?? 0;
|
|
39
|
-
const high = s.securityVulnsHigh ?? 0;
|
|
40
|
-
const metric = critical + high;
|
|
41
|
-
if (metric <= 0) continue;
|
|
42
|
-
const attempts = s.securityAutoFixAttempts ?? 0;
|
|
43
|
-
const exhausted = attempts >= AUTO_FIX_EXHAUSTED_CYCLES;
|
|
44
|
-
const noun = metric === 1 ? "vuln" : "vulns";
|
|
45
|
-
items.push({
|
|
46
|
-
key: `vuln:${s.id}`,
|
|
47
|
-
kind: "vuln",
|
|
48
|
-
siteName: s.name,
|
|
49
|
-
title: exhausted ? `${metric} critical/high ${noun} \u2014 auto-fix failed (${attempts}\xD7)` : `${metric} critical/high ${noun}`,
|
|
50
|
-
url: dashboardUrl(baseUrl, s.name),
|
|
51
|
-
severity: exhausted || critical > 0 ? "critical" : "warning",
|
|
52
|
-
metric,
|
|
53
|
-
...exhausted ? { autoFixExhausted: true } : {}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return items;
|
|
57
|
-
}
|
|
58
|
-
var LIGHTHOUSE_FLOOR = 75;
|
|
59
|
-
var LIGHTHOUSE_CATEGORIES = [
|
|
60
|
-
{ field: "pScore", slug: "performance", label: "Performance" },
|
|
61
|
-
{ field: "rScore", slug: "accessibility", label: "Accessibility" },
|
|
62
|
-
{ field: "bpScore", slug: "best-practices", label: "Best Practices" },
|
|
63
|
-
{ field: "seoScore", slug: "seo", label: "SEO" }
|
|
64
|
-
];
|
|
65
|
-
function collectLighthouseAlerts(sites, baseUrl) {
|
|
66
|
-
const items = [];
|
|
67
|
-
for (const s of sites) {
|
|
68
|
-
for (const cat of LIGHTHOUSE_CATEGORIES) {
|
|
69
|
-
const score = s[cat.field];
|
|
70
|
-
if (score === null || score >= LIGHTHOUSE_FLOOR) continue;
|
|
71
|
-
items.push({
|
|
72
|
-
key: `lighthouse:${s.id}:${cat.slug}`,
|
|
73
|
-
kind: "lighthouse",
|
|
74
|
-
siteName: s.name,
|
|
75
|
-
title: `Lighthouse ${cat.label} ${score} (below ${LIGHTHOUSE_FLOOR})`,
|
|
76
|
-
url: dashboardUrl(baseUrl, s.name),
|
|
77
|
-
severity: "warning",
|
|
78
|
-
metric: 100 - score
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return items;
|
|
83
|
-
}
|
|
84
|
-
function collectDeliveryFailures(reports, sitesById, baseUrl) {
|
|
85
|
-
const items = [];
|
|
86
|
-
for (const r of reports) {
|
|
87
|
-
if (r.deliveryStatus !== "bounced" && r.deliveryStatus !== "complained") continue;
|
|
88
|
-
const site = sitesById.get(r.siteId);
|
|
89
|
-
if (!site) continue;
|
|
90
|
-
const complained = r.deliveryStatus === "complained";
|
|
91
|
-
items.push({
|
|
92
|
-
key: `delivery:${r.id}`,
|
|
93
|
-
kind: "delivery",
|
|
94
|
-
siteName: site.name,
|
|
95
|
-
title: complained ? "Spam complaint on a sent report" : "A sent report bounced",
|
|
96
|
-
url: dashboardUrl(baseUrl, site.name),
|
|
97
|
-
severity: complained ? "critical" : "warning",
|
|
98
|
-
metric: 1
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return items;
|
|
102
|
-
}
|
|
103
|
-
function collectRenovateAlerts(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
104
|
-
const items = [];
|
|
105
|
-
for (const s of sites) {
|
|
106
|
-
if (gitHubSignalsStale(s.githubSignalsAt, now)) continue;
|
|
107
|
-
const n = s.renovateFailingCis ?? 0;
|
|
108
|
-
if (n <= 0) continue;
|
|
109
|
-
items.push({
|
|
110
|
-
key: `renovate:${s.id}`,
|
|
111
|
-
kind: "renovate",
|
|
112
|
-
siteName: s.name,
|
|
113
|
-
title: `${n} Renovate ${n === 1 ? "PR" : "PRs"} failing CI`,
|
|
114
|
-
url: dashboardUrl(baseUrl, s.name),
|
|
115
|
-
severity: "warning",
|
|
116
|
-
metric: n
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
return items;
|
|
120
|
-
}
|
|
121
|
-
function collectCiAlerts(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
122
|
-
const items = [];
|
|
123
|
-
for (const s of sites) {
|
|
124
|
-
if (gitHubSignalsStale(s.githubSignalsAt, now)) continue;
|
|
125
|
-
if (s.defaultBranchCi !== "failing") continue;
|
|
126
|
-
items.push({
|
|
127
|
-
key: `ci:${s.id}`,
|
|
128
|
-
kind: "ci",
|
|
129
|
-
siteName: s.name,
|
|
130
|
-
title: "Default-branch CI failing",
|
|
131
|
-
url: dashboardUrl(baseUrl, s.name),
|
|
132
|
-
severity: "warning",
|
|
133
|
-
metric: 1
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return items;
|
|
137
|
-
}
|
|
138
|
-
var ANALYTICS_SOFT_FAIL_STALE_DAYS = 45;
|
|
139
|
-
function collectAnalyticsFailures(sites, baseUrl, now = /* @__PURE__ */ new Date()) {
|
|
140
|
-
const items = [];
|
|
141
|
-
for (const s of sites) {
|
|
142
|
-
const at = s.analyticsSoftFailAt;
|
|
143
|
-
if (at === null) continue;
|
|
144
|
-
const ageMs = now.getTime() - Date.parse(at);
|
|
145
|
-
if (Number.isFinite(ageMs) && ageMs > ANALYTICS_SOFT_FAIL_STALE_DAYS * MS_PER_DAY) continue;
|
|
146
|
-
items.push({
|
|
147
|
-
key: `analytics:${s.id}`,
|
|
148
|
-
kind: "analytics",
|
|
149
|
-
siteName: s.name,
|
|
150
|
-
title: "GA/Search enrichment failing (analytics blank)",
|
|
151
|
-
url: dashboardUrl(baseUrl, s.name),
|
|
152
|
-
severity: "warning",
|
|
153
|
-
metric: 1
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
return items;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// src/alerts/digest-state.ts
|
|
160
|
-
function diffAttention(items, prior, today) {
|
|
161
|
-
const tagged = [];
|
|
162
|
-
const next = {};
|
|
163
|
-
for (const it of items) {
|
|
164
|
-
const was = prior[it.key];
|
|
165
|
-
let status;
|
|
166
|
-
let firstFlaggedAt;
|
|
167
|
-
if (!was) {
|
|
168
|
-
status = "new";
|
|
169
|
-
firstFlaggedAt = today;
|
|
170
|
-
} else if (it.metric > was.metric) {
|
|
171
|
-
status = "worse";
|
|
172
|
-
firstFlaggedAt = was.firstFlaggedAt;
|
|
173
|
-
} else {
|
|
174
|
-
status = "standing";
|
|
175
|
-
firstFlaggedAt = was.firstFlaggedAt;
|
|
176
|
-
}
|
|
177
|
-
tagged.push({ ...it, status });
|
|
178
|
-
next[it.key] = { metric: it.metric, firstFlaggedAt };
|
|
179
|
-
}
|
|
180
|
-
return { tagged, next };
|
|
181
|
-
}
|
|
182
|
-
var DIGEST_STATE_TABLE = "Digest State";
|
|
183
|
-
async function readDigestState(base) {
|
|
184
|
-
const rows = [];
|
|
185
|
-
await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).eachPage((records, fetchNextPage) => {
|
|
186
|
-
for (const rec of records) rows.push({ id: rec.id, fields: rec.fields });
|
|
187
|
-
fetchNextPage();
|
|
188
|
-
});
|
|
189
|
-
const first = rows[0];
|
|
190
|
-
if (!first) return {};
|
|
191
|
-
const raw = first.fields["Snapshot"];
|
|
192
|
-
if (typeof raw !== "string") return {};
|
|
193
|
-
try {
|
|
194
|
-
return JSON.parse(raw);
|
|
195
|
-
} catch {
|
|
196
|
-
return {};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async function writeDigestState(base, snap, updatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
200
|
-
const rows = [];
|
|
201
|
-
await base(DIGEST_STATE_TABLE).select({ maxRecords: 1, pageSize: 1 }).eachPage((records, fetchNextPage) => {
|
|
202
|
-
for (const rec of records) rows.push({ id: rec.id });
|
|
203
|
-
fetchNextPage();
|
|
204
|
-
});
|
|
205
|
-
const fields = {
|
|
206
|
-
Snapshot: JSON.stringify(snap),
|
|
207
|
-
"Updated At": updatedAt
|
|
208
|
-
};
|
|
209
|
-
const existing = rows[0];
|
|
210
|
-
if (existing) {
|
|
211
|
-
await base(DIGEST_STATE_TABLE).update([{ id: existing.id, fields }]);
|
|
212
|
-
} else {
|
|
213
|
-
await base(DIGEST_STATE_TABLE).create([{ fields }]);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
33
|
// src/reports/digest.ts
|
|
218
34
|
var GREY = "#757575";
|
|
219
35
|
var RED = "#C00";
|
|
@@ -410,4 +226,4 @@ export {
|
|
|
410
226
|
renderDigestHtml,
|
|
411
227
|
runDigest
|
|
412
228
|
};
|
|
413
|
-
//# sourceMappingURL=digest-
|
|
229
|
+
//# sourceMappingURL=digest-ABJROO4H.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention, readDigestState, writeDigestState } from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n} from \"../alerts/attention.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review & approve</a>`\n : `review & approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n/** Single-operator fleet — fallback when OPERATOR_EMAIL is unset. */\nconst DIGEST_OPERATOR_FALLBACK = \"info@reddoorla.com\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n};\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n const prior = await readDigestState(base);\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n const needsAttention = tagged;\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // On a skip, `collected` is [] so `next` is {} — still persist it so a key that\n // resolved on a quiet day clears and a later recurrence diffs as NEW (spec §10).\n // Wrapped: a write failure can't fail the skip.\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention });\n const client = options.resend ?? defaultResendClient();\n const to = [process.env.OPERATOR_EMAIL?.trim() || DIGEST_OPERATOR_FALLBACK];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,17 +3,26 @@ import {
|
|
|
3
3
|
} from "./chunk-PZQ3WED2.js";
|
|
4
4
|
import {
|
|
5
5
|
formatFleetWriteSummary
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-4REUHZAW.js";
|
|
7
|
+
import "./chunk-YYUY2OVP.js";
|
|
8
8
|
import {
|
|
9
9
|
makeGitHub
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-OSRFDGQM.js";
|
|
11
|
+
import {
|
|
12
|
+
detectSignalEvents,
|
|
13
|
+
fleetSweptEvent
|
|
14
|
+
} from "./chunk-5L3FPQIJ.js";
|
|
15
|
+
import {
|
|
16
|
+
recordFleetEventsBestEffort
|
|
17
|
+
} from "./chunk-L5F664ZA.js";
|
|
18
|
+
import "./chunk-6ZKM7OIJ.js";
|
|
19
|
+
import "./chunk-XKKTK6WA.js";
|
|
11
20
|
import "./chunk-HVOOCK6L.js";
|
|
12
21
|
import {
|
|
13
22
|
listWebsites,
|
|
14
23
|
siteSlug,
|
|
15
24
|
updateGitHubSignals
|
|
16
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-BLD6AYJO.js";
|
|
17
26
|
import {
|
|
18
27
|
openBase,
|
|
19
28
|
readAirtableConfig
|
|
@@ -82,6 +91,9 @@ async function runGitHubSignalsCommand(opts) {
|
|
|
82
91
|
const sweptAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
83
92
|
const result = { written: [], failed: [] };
|
|
84
93
|
const byRepo = new Map(websites.filter((w) => w.gitRepo).map((w) => [w.gitRepo, w]));
|
|
94
|
+
const events = [];
|
|
95
|
+
const sweptMs = Date.parse(sweptAt);
|
|
96
|
+
const since24h = new Date(sweptMs - 24 * 60 * 60 * 1e3).toISOString();
|
|
85
97
|
for (const row of rows) {
|
|
86
98
|
const target = byRepo.get(row.repo);
|
|
87
99
|
if (!target) {
|
|
@@ -99,11 +111,20 @@ async function runGitHubSignalsCommand(opts) {
|
|
|
99
111
|
siteName: target.name,
|
|
100
112
|
writes: [{ audit: "github-signals", counts: row }]
|
|
101
113
|
});
|
|
114
|
+
const since = target.githubSignalsAt ?? since24h;
|
|
115
|
+
let merged = [];
|
|
116
|
+
try {
|
|
117
|
+
merged = await gh.mergedRenovatePullRequests(row.repo, since);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
events.push(...detectSignalEvents(target, row, merged, sweptAt));
|
|
102
121
|
} catch (e) {
|
|
103
122
|
result.failed.push({ slug: siteSlug(row.site), error: e.message });
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
for (const repo of skipped) result.failed.push({ slug: repo, error: "probe failed (skipped)" });
|
|
126
|
+
events.push(fleetSweptEvent("github-signals", result.written.length, sweptAt));
|
|
127
|
+
await recordFleetEventsBestEffort(events, /* @__PURE__ */ new Date());
|
|
107
128
|
return {
|
|
108
129
|
output: formatFleetWriteSummary(result),
|
|
109
130
|
code: githubSignalsExitCode(result.written.length, result.failed.length)
|
|
@@ -113,4 +134,4 @@ export {
|
|
|
113
134
|
githubSignalsExitCode,
|
|
114
135
|
runGitHubSignalsCommand
|
|
115
136
|
};
|
|
116
|
-
//# sourceMappingURL=github-signals-
|
|
137
|
+
//# sourceMappingURL=github-signals-XTJLKOXJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audits/github-signals.ts","../src/cli/commands/github-signals.ts"],"sourcesContent":["import type { Site } from \"../types.js\";\nimport type { CiState, PullRequestSummary } from \"../github/gh.js\";\nimport { isFailingRenovatePR } from \"../alerts/renovate.js\";\n\n/** One swept row, ready for the Airtable writer (slug-keyed by `site`). */\nexport type GitHubSignalsRow = {\n site: string; // the site name/slug the writer matches on\n repo: string; // owner/repo\n renovateFailingCis: number;\n ciState: CiState;\n lastCommitAt: string | null;\n};\n\n/** Injected GitHub reads (so the sweep is pure + testable). */\nexport type GitHubSignalsDeps = {\n openPullRequests: (repo: string) => Promise<PullRequestSummary[]>;\n defaultBranchStatus: (repo: string) => Promise<{ ciState: CiState; lastCommitAt: string | null }>;\n};\n\n/** Per repo-backed site: count its failing Renovate PRs + read its default-branch\n * status. Sites without `gitRepo` are skipped (not errors). A repo whose probe\n * throws is reported via `onSkip` and produces no row — one GitHub hiccup never\n * sinks the sweep. PURE over `deps`. */\nexport async function collectGitHubSignals(\n sites: Site[],\n deps: GitHubSignalsDeps,\n onSkip: (s: { repo: string }) => void = () => {},\n): Promise<GitHubSignalsRow[]> {\n const rows: GitHubSignalsRow[] = [];\n for (const s of sites) {\n const repo = s.gitRepo;\n if (!repo) continue;\n const name = s.name;\n if (!name) continue;\n try {\n const prs = await deps.openPullRequests(repo);\n const status = await deps.defaultBranchStatus(repo);\n rows.push({\n site: name,\n repo,\n renovateFailingCis: prs.filter(isFailingRenovatePR).length,\n ciState: status.ciState,\n lastCommitAt: status.lastCommitAt,\n });\n } catch {\n onSkip({ repo });\n }\n }\n return rows;\n}\n","import { openBase, readAirtableConfig } from \"../../reports/airtable/client.js\";\nimport { listWebsites, siteSlug, updateGitHubSignals } from \"../../reports/airtable/websites.js\";\nimport type { Site } from \"../../types.js\";\nimport { collectGitHubSignals } from \"../../audits/github-signals.js\";\nimport { makeGitHub } from \"../../github/gh.js\";\nimport {\n formatFleetWriteSummary,\n type FleetWriteResult,\n} from \"../../audits/write-audits-to-airtable.js\";\nimport { detectSignalEvents, fleetSweptEvent } from \"../../audits/fleet-event-detectors.js\";\nimport { recordFleetEventsBestEffort } from \"../../audits/fleet-events-writer.js\";\nimport type { FleetEvent } from \"../../db/fleet-events.js\";\n\n/** Exit code for a fleet github-signals run. Exit 1 when failures are the\n * MAJORITY of the fleet (`failed > written`), not only on a total wipeout —\n * a run where 11/12 repos failed but 1 wrote should still signal an outage.\n * All-success or a minority of flakes (e.g. 1/12 failed) stays exit 0. The\n * no-token clean-skip returns 0 separately (before any probe runs). */\nexport function githubSignalsExitCode(written: number, failed: number): number {\n return failed > written ? 1 : 0;\n}\n\n/** `github-signals --fleet --write-airtable`: sweep every repo-backed site for its\n * Renovate-failing count + default-branch CI state + last-commit date, write each\n * row serially (Airtable ~5 req/sec), and emit FLEET_WRITE_SUMMARY for CI. A\n * missing fleet token is a clean skip (local runs), not a failure. */\nexport async function runGitHubSignalsCommand(opts: {\n fleet?: boolean | undefined;\n writeAirtable?: boolean | undefined;\n}): Promise<{ output: string; code: number }> {\n if (!opts.fleet || !opts.writeAirtable) {\n return { output: \"github-signals currently supports only --fleet --write-airtable\", code: 2 };\n }\n const token = process.env.RENOVATE_TOKEN?.trim() || process.env.GH_TOKEN?.trim();\n if (!token) {\n return {\n output: \"github-signals skipped: no RENOVATE_TOKEN/GH_TOKEN (fleet read) configured.\",\n code: 0,\n };\n }\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const gh = makeGitHub({ token });\n const sites: Site[] = websites.map((w) => ({\n path: \"\",\n name: w.name,\n meta: {},\n ...(w.gitRepo ? { gitRepo: w.gitRepo } : {}),\n }));\n\n const skipped: string[] = [];\n const rows = await collectGitHubSignals(\n sites,\n {\n openPullRequests: (r) => gh.openPullRequests(r),\n defaultBranchStatus: (r) => gh.defaultBranchStatus(r),\n },\n ({ repo }) => skipped.push(repo),\n );\n\n const sweptAt = new Date().toISOString();\n const result: FleetWriteResult = { written: [], failed: [] };\n const byRepo = new Map(websites.filter((w) => w.gitRepo).map((w) => [w.gitRepo, w]));\n const events: FleetEvent[] = [];\n const sweptMs = Date.parse(sweptAt);\n const since24h = new Date(sweptMs - 24 * 60 * 60 * 1000).toISOString();\n // Serial: Airtable's ~5 req/sec limit (matches writeFleetAuditsToAirtable).\n for (const row of rows) {\n const target = byRepo.get(row.repo);\n if (!target) {\n result.failed.push({ slug: siteSlug(row.site), error: \"no Websites row matched\" });\n continue;\n }\n try {\n await updateGitHubSignals(base, target.id, {\n renovateFailingCis: row.renovateFailingCis,\n ciState: row.ciState,\n lastCommitAt: row.lastCommitAt,\n sweptAt,\n });\n result.written.push({\n siteName: target.name,\n writes: [{ audit: \"github-signals\", counts: row }],\n });\n // Fleet-activity events for this repo: merged Renovate PRs since the last sweep\n // (watermark = the row's prior GitHub Signals At, else a 24h fallback) + a\n // CI-recovered transition. A PR-fetch hiccup drops only this repo's PR events.\n const since = target.githubSignalsAt ?? since24h;\n let merged: Awaited<ReturnType<typeof gh.mergedRenovatePullRequests>> = [];\n try {\n merged = await gh.mergedRenovatePullRequests(row.repo, since);\n } catch {\n // PR list unavailable this run — skip pr_automerged for this repo, keep ci_recovered\n }\n events.push(...detectSignalEvents(target, row, merged, sweptAt));\n } catch (e) {\n result.failed.push({ slug: siteSlug(row.site), error: (e as Error).message });\n }\n }\n for (const repo of skipped) result.failed.push({ slug: repo, error: \"probe failed (skipped)\" });\n\n events.push(fleetSweptEvent(\"github-signals\", result.written.length, sweptAt));\n await recordFleetEventsBestEffort(events, new Date());\n\n // Exit non-zero when failures are the MAJORITY of the fleet, not only on a\n // total wipeout. A run where 11/12 repos failed but 1 wrote used to return 0,\n // masking a large outage. The nightly cron step is `continue-on-error`, so a\n // non-zero here is an operator-visibility signal, not a red build.\n return {\n output: formatFleetWriteSummary(result),\n code: githubSignalsExitCode(result.written.length, result.failed.length),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as Site, a as AuditResult, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-
|
|
2
|
-
export { C as ConfigName } from './types-
|
|
1
|
+
import { S as Site, a as AuditResult, A as AuditName, b as RecipeResult, R as RecipeName, I as InventoryProvider } from './types-Dg4mHvyf.js';
|
|
2
|
+
export { C as ConfigName } from './types-Dg4mHvyf.js';
|
|
3
3
|
import * as airtable_lib_airtable_base_js from 'airtable/lib/airtable_base.js';
|
|
4
4
|
import { F as FormType } from './types-RXY-vY-5.js';
|
|
5
5
|
export { SyncConfigsOptions, syncConfigs } from './recipes/sync-configs.js';
|
|
@@ -68,10 +68,33 @@ type BrowserRunner = {
|
|
|
68
68
|
close?: () => Promise<void>;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
/** Result of probing a site's latest PRODUCTION Netlify deploy. Every field is
|
|
72
|
+
* nullable: a network/parse failure (or a site with no deploys yet) degrades to
|
|
73
|
+
* all-nulls rather than throwing, so the audit always returns a verdict.
|
|
74
|
+
* - `state` is Netlify's raw deploy state (`ready`/`error`/`building`/…), lower-cased,
|
|
75
|
+
* or null when it couldn't be read.
|
|
76
|
+
* - `deployedAt` is the deploy's publish/create time (ISO), or null.
|
|
77
|
+
* - `logUrl` links to the deploy (its live URL or the Netlify admin/build-log page).
|
|
78
|
+
* - `errorMessage` is Netlify's `error_message` for a failed build, or null. */
|
|
79
|
+
type NetlifyDeployCheck = {
|
|
80
|
+
state: string | null;
|
|
81
|
+
deployedAt: string | null;
|
|
82
|
+
logUrl: string | null;
|
|
83
|
+
errorMessage: string | null;
|
|
84
|
+
};
|
|
85
|
+
/** Injected IO so the check is unit-testable without hitting the Netlify API.
|
|
86
|
+
* `fetchLatestProductionDeploy` returns the parsed first (latest) production
|
|
87
|
+
* deploy object, or null when the site has none / the call failed. `now` is the
|
|
88
|
+
* clock for the checked-at stamp. */
|
|
89
|
+
type NetlifyDeployDeps = {
|
|
90
|
+
fetchLatestProductionDeploy: (siteId: string) => Promise<NetlifyDeployCheck | null>;
|
|
91
|
+
now: Date;
|
|
92
|
+
};
|
|
93
|
+
|
|
71
94
|
type AuditContext = {
|
|
72
95
|
site: Site;
|
|
73
96
|
spawn?: SpawnFn;
|
|
74
|
-
/** Clock injection (domain + browser audits). Defaults to `new Date()`. */
|
|
97
|
+
/** Clock injection (domain + browser + netlify-deploy audits). Defaults to `new Date()`. */
|
|
75
98
|
now?: Date;
|
|
76
99
|
/** DNS/TLS injection for the domain audit (tests). Defaults to real DNS+TLS. */
|
|
77
100
|
domainDeps?: DomainDeps;
|
|
@@ -79,6 +102,8 @@ type AuditContext = {
|
|
|
79
102
|
discoverDeps?: DiscoverDeps;
|
|
80
103
|
/** Playwright runner injection for the browser audit (tests). Defaults to real Playwright. */
|
|
81
104
|
browserRunner?: BrowserRunner;
|
|
105
|
+
/** Netlify API injection for the netlify-deploy audit (tests). Defaults to a real API call. */
|
|
106
|
+
netlifyDeployDeps?: NetlifyDeployDeps;
|
|
82
107
|
};
|
|
83
108
|
|
|
84
109
|
declare function depsAudit(ctx: AuditContext): Promise<AuditResult>;
|
|
@@ -326,6 +351,16 @@ type WebsiteRow = {
|
|
|
326
351
|
* expires (null = unresolved or no usable cert); `domainCheckedAt` is when it last ran. */
|
|
327
352
|
certDaysRemaining: number | null;
|
|
328
353
|
domainCheckedAt: string | null;
|
|
354
|
+
/** Netlify site id — the IDENTITY the `netlify-deploy` audit needs to query the
|
|
355
|
+
* Netlify API. Read-only input (operator-set in Airtable); null = not on Netlify
|
|
356
|
+
* (or not wired) → that audit skips. Never derived from the URL. */
|
|
357
|
+
netlifyId: string | null;
|
|
358
|
+
/** Latest PRODUCTION deploy health (the `netlify-deploy` audit). `deployStatus` is
|
|
359
|
+
* Netlify's deploy state lower-cased (`ready`/`error`/`building`/…), null = none / not
|
|
360
|
+
* checked; `lastDeployAt` is when it deployed; `deployLogUrl` links to the deploy/log. */
|
|
361
|
+
deployStatus: string | null;
|
|
362
|
+
lastDeployAt: string | null;
|
|
363
|
+
deployLogUrl: string | null;
|
|
329
364
|
/** Deployed-URL browser probe (the `browser` audit): cross-engine render OK, mobile render OK,
|
|
330
365
|
* internal-links OK + broken count, and when it last ran (one timestamp gates all three). */
|
|
331
366
|
crossbrowserOk: boolean | null;
|
|
@@ -380,7 +415,11 @@ type ResolvedCopy = {
|
|
|
380
415
|
announceBody: string;
|
|
381
416
|
announceImprovementResend: string;
|
|
382
417
|
announceImprovementSvelte5: string;
|
|
418
|
+
/** The cadence lead-in, ending at the em dash. Rendered followed by `announceCadenceNote`
|
|
419
|
+
* in italics. */
|
|
383
420
|
announceCadence: string;
|
|
421
|
+
/** The reassurance tail, rendered italic after `announceCadence`. */
|
|
422
|
+
announceCadenceNote: string;
|
|
384
423
|
announceOpenDoor: string;
|
|
385
424
|
};
|
|
386
425
|
|
|
@@ -418,6 +457,10 @@ type ReportData = {
|
|
|
418
457
|
* `| undefined` is explicit so callers can pass `undefined` under exactOptionalPropertyTypes. */
|
|
419
458
|
gaUsersCurrent?: number | undefined;
|
|
420
459
|
gaUsersPrevious?: number | undefined;
|
|
460
|
+
/** Length in days of the GA "current" window (compared against the equal-length prior window).
|
|
461
|
+
* Drives the analytics trend label ("vs the previous N days"); omitted → the generic
|
|
462
|
+
* "last period". `| undefined` for exactOptionalPropertyTypes. */
|
|
463
|
+
gaPeriodDays?: number | undefined;
|
|
421
464
|
/** Site's rounded average Google position for its query, when on page 1 (from Search Console).
|
|
422
465
|
* `undefined` = not on page 1, not checked, or unconfigured — rendered as today's plain check. */
|
|
423
466
|
searchPosition?: number | undefined;
|
|
@@ -721,6 +764,8 @@ type AttentionItem = {
|
|
|
721
764
|
autoFixExhausted?: boolean;
|
|
722
765
|
};
|
|
723
766
|
|
|
767
|
+
type FleetEventType = "pr_automerged" | "vuln_cleared" | "ci_recovered" | "site_launched" | "fleet_swept" | "cert_renewed";
|
|
768
|
+
|
|
724
769
|
type Tier = "attention" | "watch" | "healthy";
|
|
725
770
|
type SiteCard = {
|
|
726
771
|
site: WebsiteRow;
|
|
@@ -765,6 +810,16 @@ type CockpitSummary = {
|
|
|
765
810
|
/** Count of NEW submissions across the fleet (optional for back-compat). */
|
|
766
811
|
newSubmissions?: number;
|
|
767
812
|
};
|
|
813
|
+
/** A render-ready row for the cockpit "Recently" lane. `url` is an external link
|
|
814
|
+
* (PR) when present; `slug` links to `/s/<slug>` when the event names a site. */
|
|
815
|
+
type RecentEntry = {
|
|
816
|
+
type: FleetEventType;
|
|
817
|
+
summary: string;
|
|
818
|
+
siteName: string | null;
|
|
819
|
+
slug: string | null;
|
|
820
|
+
url: string | null;
|
|
821
|
+
ts: string;
|
|
822
|
+
};
|
|
768
823
|
type CockpitModel = {
|
|
769
824
|
summary: CockpitSummary;
|
|
770
825
|
/** All visible sites, ordered: attention (worst-first) → watch (A-Z) → healthy (A-Z). */
|
|
@@ -777,14 +832,16 @@ type CockpitModel = {
|
|
|
777
832
|
caught: number;
|
|
778
833
|
through: number;
|
|
779
834
|
} | null;
|
|
835
|
+
/** Recent fleet-activity events for the "Recently" lane (optional for back-compat). */
|
|
836
|
+
recent?: RecentEntry[];
|
|
780
837
|
};
|
|
781
838
|
|
|
782
839
|
/**
|
|
783
840
|
* Render the fleet cockpit as a single HTML document. Pure function: no Airtable
|
|
784
841
|
* access, no env reads, no I/O. The Netlify function handler builds the
|
|
785
842
|
* CockpitModel (visible-site filter, tiering, NEW/WORSE badging, pending list)
|
|
786
|
-
* and hands it here. Renders the doc shell +
|
|
787
|
-
*
|
|
843
|
+
* and hands it here. Renders the doc shell + verdict bar + the per-site Needs-you
|
|
844
|
+
* feed + the collapsed Fleet browse panel (one flat, filterable card grid).
|
|
788
845
|
*/
|
|
789
846
|
declare function renderCockpitHtml(model: CockpitModel): string;
|
|
790
847
|
|