Files
recurring-checklist/scripts/alert.js
T
Waters ChurchandClaude Opus 5 39c65cf9de
Build apps / build (push) Successful in 1m30s
Deploy web to NAS / deploy (push) Successful in 1m14s
Make the website deploy report its own failures
The deploy was the one pipeline with no failure notification. It either worked or
you found out by noticing the site was stale — the same shape as the off-site
backup before today.

alert.js now takes CHECKLIST_ENV_FILE and CHECKLIST_NODE_MODULES, because the
deploy job runs in a container that has the repo but no server/.env and no
node_modules; both live on the NAS at the mounted deploy directory.

What this still does not catch, and the reason it is written down in the workflow:
a runner that wedges. Today the NAS runner claimed run 7, executed none of its six
steps, and Gitea timed it out fifteen minutes later — every step was marked failed
without one having run, so no in-job step could have reported it. The container
said "Up 2 hours" throughout and admitted on restart that it had been holding the
job. A stuck runner looks exactly like a slow one, and catching it needs something
watching from outside the run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 16:11:22 -04:00

83 lines
3.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
'use strict';
// alert.js — send one alert email, reusing the server's own SMTP settings.
//
// Exists because backup-to-drive.sh used to report failure only into
// ~/Library/Logs/checklist-drive-backup.log, where nobody was ever going to look. That is
// how the Drive copies went ten weeks stale in July 2026, and then — despite the script
// written to prevent exactly that — another nine days in September 2026.
//
// Usage: node scripts/alert.js "<subject>" "<body>"
// Exit 0 if the mail went out, 1 otherwise. Callers should NOT fail on a failed alert:
// the thing being reported matters more than the reporting of it.
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
// Overridable because the deploy job runs inside a container that has the repo checked out
// but no server/.env and no node_modules — those live on the NAS at the mounted deploy
// directory instead. Without this the deploy could not report its own failures.
const ENV_FILE = process.env.CHECKLIST_ENV_FILE || path.join(ROOT, 'server', '.env');
const NODE_MODULES = process.env.CHECKLIST_NODE_MODULES || path.join(ROOT, 'server', 'node_modules');
// Minimal .env reader — the server uses dotenv, but this script has to run from a plain
// LaunchAgent with no cwd guarantees and no node_modules of its own.
function readEnv(file) {
const out = {};
let text;
try { text = fs.readFileSync(file, 'utf8'); }
catch { return out; }
for (const raw of text.split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const eq = line.indexOf('=');
if (eq < 0) continue;
let v = line.slice(eq + 1).trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
out[line.slice(0, eq).trim()] = v;
}
return out;
}
async function main() {
const subject = process.argv[2];
const body = process.argv[3] || '';
if (!subject) { console.error('alert.js: no subject given'); process.exit(1); }
const env = readEnv(ENV_FILE);
// ALERT_EMAIL is for things that are broken; REPORT_EMAIL is for the Sunday digest. They
// are separate because an alert that only reaches a shared inbox is easy to scroll past,
// and because the digest should not become a channel people learn to ignore. Comma
// separated, and it falls back to REPORT_EMAIL so a machine whose .env predates this
// still alerts somewhere rather than silently not at all.
const to = (env.ALERT_EMAIL || env.REPORT_EMAIL || '')
.split(',').map(a => a.trim()).filter(Boolean).join(', ');
if (!to) { console.error(`alert.js: no ALERT_EMAIL or REPORT_EMAIL in ${ENV_FILE}`); process.exit(1); }
if (!env.SMTP_HOST || !env.SMTP_USER || !env.SMTP_PASS) {
console.error(`alert.js: SMTP is not configured in ${ENV_FILE}`); process.exit(1);
}
// nodemailer lives in server/node_modules; this script has none of its own.
let nodemailer;
try { nodemailer = require(path.join(NODE_MODULES, 'nodemailer')); }
catch { console.error(`alert.js: nodemailer not found under ${NODE_MODULES} — run \`npm install\` in server/`); process.exit(1); }
const transport = nodemailer.createTransport({
host: env.SMTP_HOST,
port: Number(env.SMTP_PORT || 587),
secure: String(env.SMTP_SECURE || 'false') === 'true',
auth: { user: env.SMTP_USER, pass: env.SMTP_PASS },
});
await transport.sendMail({
from: env.SMTP_FROM || env.SMTP_USER,
to,
subject,
text: `${body}\n\n-- \nSent by scripts/alert.js on ${require('os').hostname()} at ${new Date().toISOString()}\n`,
});
console.log(`alert.js: sent "${subject}" to ${to}`);
}
main().catch(err => { console.error('alert.js:', err && err.message ? err.message : err); process.exit(1); });