Record a heartbeat so a silent update checker is distinguishable from a dead one
There was no way to tell "running, nothing to pull" from "not running". The no-change path logs nothing by design, and PM2 reports a cron-restart process as `stopped` with ↺ 0 even while firing on schedule — I verified that against PM2 7.0.4: a one-minute cron fired four times without the counter moving once. scripts/.last-check is now rewritten on every run with the outcome. It is a single overwritten line, so it needs no trimming. The outcome is set by each exit path and written once in `finally`, rather than calling record at each return — the first version of this did the latter and already missed the build-failure path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
27a61fd25c
commit
0ba27161ce
@@ -62,3 +62,4 @@ scripts/update.log
|
||||
|
||||
# clicker virtualenv (Linux/Pi)
|
||||
clicker/.venv/
|
||||
scripts/.last-check
|
||||
|
||||
@@ -210,8 +210,23 @@ AutoFirmer, and restarts the clicker if anything under `clicker/` changed.
|
||||
**A failed build is never deployed.** The build runs before anything restarts, so
|
||||
a broken push leaves the previous build serving and logs the failure instead.
|
||||
|
||||
Everything it does is appended to `scripts/update.log`. To see what it would do
|
||||
without touching anything:
|
||||
### Checking it is alive
|
||||
|
||||
`scripts/.last-check` is rewritten on every check, whether or not anything came
|
||||
down:
|
||||
|
||||
```
|
||||
2026-08-31T00:43:27.155Z up to date at 27a61fd2
|
||||
```
|
||||
|
||||
This exists because the alternatives mislead. `update.log` only records real
|
||||
events, so it stays empty for days when nothing is pushed — and PM2 reports a
|
||||
cron-restart process as `stopped` with a restart count of `0` even while it is
|
||||
firing on schedule. Neither is evidence of a problem; `.last-check` is the
|
||||
signal to trust.
|
||||
|
||||
Everything that actually happens is appended to `scripts/update.log`. To see
|
||||
what it would do without touching anything:
|
||||
|
||||
```powershell
|
||||
node scripts\update-check.mjs --dry-run
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Writes the reporter settings into autotrader.sqlite before first launch.
|
||||
*
|
||||
* lib/db.ts seeds these keys with INSERT OR IGNORE, so values written here
|
||||
* survive the app's own startup seeding. Must be run from the project root —
|
||||
* lib/db.ts opens the database at process.cwd().
|
||||
*
|
||||
* node scripts/seed-settings.js <master_dashboard_url> <instance_name>
|
||||
*/
|
||||
const path = require('path');
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const [, , url, name] = process.argv;
|
||||
|
||||
const db = new Database(path.join(process.cwd(), 'autotrader.sqlite'));
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
const upsert = db.prepare(
|
||||
'INSERT INTO settings (key, value) VALUES (?, ?) ' +
|
||||
'ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
||||
);
|
||||
|
||||
if (url) { upsert.run('master_dashboard_url', url); console.log(' master_dashboard_url = ' + url); }
|
||||
if (name) { upsert.run('instance_name', name); console.log(' instance_name = ' + name); }
|
||||
|
||||
db.close();
|
||||
@@ -20,6 +20,7 @@ import { ensurePythonDeps } from './ensure-python-deps.mjs';
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const LOG_FILE = path.join(ROOT, 'scripts', 'update.log');
|
||||
const LOCK_FILE = path.join(ROOT, 'scripts', '.update.lock');
|
||||
const LAST_CHECK_FILE = path.join(ROOT, 'scripts', '.last-check');
|
||||
const BRANCH = process.env.AUTOFIRMER_BRANCH ?? 'master';
|
||||
const PORT = process.env.AUTOFIRMER_PORT ?? '3000';
|
||||
const BASE = `http://127.0.0.1:${PORT}`;
|
||||
@@ -50,6 +51,21 @@ function log(msg) {
|
||||
try { appendFileSync(LOG_FILE, line + '\n'); } catch { /* logging must never throw */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Record that a check happened, whether or not it found anything.
|
||||
*
|
||||
* Overwritten rather than appended, so it stays one line and needs no trimming.
|
||||
* Without it there is no way to tell "running, nothing to pull" from "not
|
||||
* running at all": the no-change path logs nothing by design, and PM2 reports a
|
||||
* cron-restart process as `stopped` with a restart count of 0 even while it is
|
||||
* firing on schedule.
|
||||
*/
|
||||
function recordCheck(status) {
|
||||
try {
|
||||
writeFileSync(LAST_CHECK_FILE, `${new Date().toISOString()} ${status}\n`);
|
||||
} catch { /* never let bookkeeping break an update */ }
|
||||
}
|
||||
|
||||
function run(cmd, args, { capture = false } = {}) {
|
||||
return spawnSync(cmd, args, {
|
||||
cwd: ROOT,
|
||||
@@ -88,12 +104,17 @@ async function main() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let outcome = 'interrupted';
|
||||
|
||||
try {
|
||||
git('fetch', 'origin', BRANCH);
|
||||
const local = git('rev-parse', 'HEAD');
|
||||
const remote = git('rev-parse', `origin/${BRANCH}`);
|
||||
|
||||
if (local === remote) return 0; // the common path: silent no-op
|
||||
if (local === remote) {
|
||||
outcome = `up to date at ${local.slice(0, 8)}`;
|
||||
return 0; // silent in the log by design
|
||||
}
|
||||
|
||||
log(`update available: ${local.slice(0, 8)} -> ${remote.slice(0, 8)}`);
|
||||
const changed = git('diff', '--name-only', local, remote).split('\n').filter(Boolean);
|
||||
@@ -115,7 +136,7 @@ async function main() {
|
||||
|
||||
if (changed.includes('package-lock.json')) {
|
||||
log('package-lock.json changed — npm install');
|
||||
if (run('npm', ['install']).status !== 0) { log('ABORTED: npm install failed'); return 1; }
|
||||
if (run('npm', ['install']).status !== 0) { log('ABORTED: npm install failed'); outcome = 'npm install failed'; return 1; }
|
||||
}
|
||||
|
||||
if (changed.includes('clicker/requirements.txt')) {
|
||||
@@ -128,11 +149,12 @@ async function main() {
|
||||
log('building');
|
||||
if (run('npm', ['run', 'build']).status !== 0) {
|
||||
log('ABORTED: build failed — the previous build is still serving, nothing was restarted');
|
||||
outcome = 'build failed — not deployed';
|
||||
return 1;
|
||||
}
|
||||
|
||||
log('restarting autofirmer');
|
||||
if (run('pm2', ['restart', 'autofirmer']).status !== 0) { log('pm2 restart autofirmer failed'); return 1; }
|
||||
if (run('pm2', ['restart', 'autofirmer']).status !== 0) { log('pm2 restart autofirmer failed'); outcome = 'pm2 restart failed'; return 1; }
|
||||
|
||||
if (changed.some((f) => f.startsWith('clicker/'))) {
|
||||
log('clicker changed — restarting it too');
|
||||
@@ -141,12 +163,17 @@ async function main() {
|
||||
|
||||
await warmUp();
|
||||
log(`updated to ${remote.slice(0, 8)}`);
|
||||
outcome = `updated to ${remote.slice(0, 8)}`;
|
||||
return 0;
|
||||
} catch (err) {
|
||||
log(`ERROR: ${err.message}`);
|
||||
outcome = `ERROR: ${err.message}`;
|
||||
return 1;
|
||||
} finally {
|
||||
if (!DRY_RUN) releaseLock();
|
||||
if (!DRY_RUN) {
|
||||
releaseLock();
|
||||
recordCheck(outcome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user