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 virtualenv (Linux/Pi)
|
||||||
clicker/.venv/
|
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 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.
|
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
|
### Checking it is alive
|
||||||
without touching anything:
|
|
||||||
|
`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
|
```powershell
|
||||||
node scripts\update-check.mjs --dry-run
|
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 ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const LOG_FILE = path.join(ROOT, 'scripts', 'update.log');
|
const LOG_FILE = path.join(ROOT, 'scripts', 'update.log');
|
||||||
const LOCK_FILE = path.join(ROOT, 'scripts', '.update.lock');
|
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 BRANCH = process.env.AUTOFIRMER_BRANCH ?? 'master';
|
||||||
const PORT = process.env.AUTOFIRMER_PORT ?? '3000';
|
const PORT = process.env.AUTOFIRMER_PORT ?? '3000';
|
||||||
const BASE = `http://127.0.0.1:${PORT}`;
|
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 */ }
|
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 } = {}) {
|
function run(cmd, args, { capture = false } = {}) {
|
||||||
return spawnSync(cmd, args, {
|
return spawnSync(cmd, args, {
|
||||||
cwd: ROOT,
|
cwd: ROOT,
|
||||||
@@ -88,12 +104,17 @@ async function main() {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let outcome = 'interrupted';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
git('fetch', 'origin', BRANCH);
|
git('fetch', 'origin', BRANCH);
|
||||||
const local = git('rev-parse', 'HEAD');
|
const local = git('rev-parse', 'HEAD');
|
||||||
const remote = git('rev-parse', `origin/${BRANCH}`);
|
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)}`);
|
log(`update available: ${local.slice(0, 8)} -> ${remote.slice(0, 8)}`);
|
||||||
const changed = git('diff', '--name-only', local, remote).split('\n').filter(Boolean);
|
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')) {
|
if (changed.includes('package-lock.json')) {
|
||||||
log('package-lock.json changed — npm install');
|
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')) {
|
if (changed.includes('clicker/requirements.txt')) {
|
||||||
@@ -128,11 +149,12 @@ async function main() {
|
|||||||
log('building');
|
log('building');
|
||||||
if (run('npm', ['run', 'build']).status !== 0) {
|
if (run('npm', ['run', 'build']).status !== 0) {
|
||||||
log('ABORTED: build failed — the previous build is still serving, nothing was restarted');
|
log('ABORTED: build failed — the previous build is still serving, nothing was restarted');
|
||||||
|
outcome = 'build failed — not deployed';
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
log('restarting autofirmer');
|
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/'))) {
|
if (changed.some((f) => f.startsWith('clicker/'))) {
|
||||||
log('clicker changed — restarting it too');
|
log('clicker changed — restarting it too');
|
||||||
@@ -141,12 +163,17 @@ async function main() {
|
|||||||
|
|
||||||
await warmUp();
|
await warmUp();
|
||||||
log(`updated to ${remote.slice(0, 8)}`);
|
log(`updated to ${remote.slice(0, 8)}`);
|
||||||
|
outcome = `updated to ${remote.slice(0, 8)}`;
|
||||||
return 0;
|
return 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(`ERROR: ${err.message}`);
|
log(`ERROR: ${err.message}`);
|
||||||
|
outcome = `ERROR: ${err.message}`;
|
||||||
return 1;
|
return 1;
|
||||||
} finally {
|
} finally {
|
||||||
if (!DRY_RUN) releaseLock();
|
if (!DRY_RUN) {
|
||||||
|
releaseLock();
|
||||||
|
recordCheck(outcome);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user