The master saw a scheduler's last run time and nothing of what it did. Each tick now records, per lane, its outcome, a line on what happened (what fired, or the spread, contract or hold cap that stopped it) and why the lane's accounts sat it out, counted by reason with a few names each. It rides the scheduler status as lastTicks. Each report also carries the auto-updater's state from the files it keeps and from git: its last check, a failed update waiting to retry, how far the checkout is behind origin, and tracked files edited by hand, which stop the fast-forward pull. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
/**
|
|
* The auto-updater's state on this machine (scripts/update-check.mjs), for
|
|
* the master: its last check, an update that failed and waits to retry, how
|
|
* far the checkout is behind origin, and tracked files edited by hand — which
|
|
* stop the fast-forward pull. Read from the files the updater keeps and from
|
|
* git, and cached: the reporter asks every 30 seconds.
|
|
*/
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import type { UpdaterStatus } from './master-protocol';
|
|
|
|
const CACHE_MS = 2 * 60_000;
|
|
const BRANCH = process.env.AUTOFIRMER_BRANCH ?? 'master';
|
|
|
|
// Per process: instrumentation.ts and the route handlers each load this module.
|
|
const g = globalThis as typeof globalThis & { __updaterStatus?: { at: number; status: UpdaterStatus } };
|
|
|
|
function readScript(file: string): string | null {
|
|
try {
|
|
return readFileSync(path.join(process.cwd(), 'scripts', file), 'utf8');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function git(args: string[]): string | null {
|
|
try {
|
|
return execFileSync('git', args, {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 5_000,
|
|
}).trimEnd();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function readFailure(): UpdaterStatus['failed'] {
|
|
try {
|
|
const f = JSON.parse(readScript('.update-failed') ?? 'null') as Record<string, unknown> | null;
|
|
if (!f || typeof f.target !== 'string') return null;
|
|
const at = Number(f.at);
|
|
return {
|
|
target: f.target.slice(0, 12),
|
|
step: String(f.step ?? '').slice(0, 40),
|
|
at: Number.isFinite(at) ? new Date(at).toISOString() : '',
|
|
attempts: Number(f.count) || 1,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function updaterStatus(now = Date.now()): UpdaterStatus {
|
|
const cached = g.__updaterStatus;
|
|
if (cached && now - cached.at < CACHE_MS) return cached.status;
|
|
|
|
// "<ISO time> <status>", rewritten on every check.
|
|
const last = /^(\S+)\s+(.*)$/.exec(readScript('.last-check')?.trim() ?? '');
|
|
const behind = git(['rev-list', '--count', `HEAD..origin/${BRANCH}`]);
|
|
const changed = git(['status', '--porcelain', '--untracked-files=no']);
|
|
const status: UpdaterStatus = {
|
|
lastCheckAt: last ? last[1] : null,
|
|
lastCheck: last ? last[2].slice(0, 200) : null,
|
|
failed: readFailure(),
|
|
builtCommit: readScript('.built-commit')?.trim().slice(0, 12) || null,
|
|
behind: behind !== null && /^\d+$/.test(behind) ? Number(behind) : null,
|
|
// "XY path" per line; the status column can start with a space.
|
|
localChanges: changed
|
|
? changed.split('\n').map((line) => line.slice(3).trim()).filter(Boolean).slice(0, 10)
|
|
: [],
|
|
};
|
|
g.__updaterStatus = { at: now, status };
|
|
return status;
|
|
}
|