feat(updates): seamless release flow — push-notify, VERSION copy, robust mirror
- self-updater: per-instance notify secret (auto-generated), notifyAndApply() triggers an immediate check+apply for the publishing host - routes: POST /api/system/update-notify (X-DashCaddy-Notify-Secret gated, added to public-routes allowlist so TOTP doesn't block machine-to-machine) - dashcaddy-update.sh: include VERSION in backup/deploy/rollback copy lists; belt-and-suspenders write trigger.json commit to VERSION post-deploy. Fixes drift where /app/VERSION stayed at the old commit after self-update. - release.sh: mirror failures are non-fatal+loud; HTTP-verify get2 after rsync; auto-notify co-located instance via /opt/dashcaddy/updates/notify-secret (or honour DASHCADDY_NOTIFY_TARGETS for multi-instance setups).
This commit is contained in:
@@ -64,6 +64,13 @@ class SelfUpdater extends EventEmitter {
|
||||
|
||||
// Ensure directories exist
|
||||
this._ensureDirs();
|
||||
|
||||
// Notify-secret lives next to instance-id (alongside updates dir on Linux,
|
||||
// <caddyBase>/notify-secret on Windows). Auto-generated on first start.
|
||||
this.notifySecretFile = options.notifySecretFile
|
||||
|| process.env.DASHCADDY_NOTIFY_SECRET_FILE
|
||||
|| path.join(this.config.updatesDir, 'notify-secret');
|
||||
this.notifySecret = this._loadOrCreateNotifySecret();
|
||||
}
|
||||
|
||||
// ── Lifecycle ──
|
||||
@@ -118,6 +125,26 @@ class SelfUpdater extends EventEmitter {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
getNotifySecret() {
|
||||
return this.notifySecret;
|
||||
}
|
||||
|
||||
// Public wrapper for the auto-check+apply loop, used by the notify endpoint
|
||||
// so the publisher can wake an instance up immediately instead of waiting
|
||||
// for the next 30-min poll. Returns immediately; work runs async.
|
||||
notifyAndApply(triggeredBy = 'notify') {
|
||||
if (this.status !== 'idle' && this.status !== 'checking') {
|
||||
return { accepted: false, reason: `busy (status: ${this.status})`, status: this.status };
|
||||
}
|
||||
// Fire-and-forget; the response shouldn't block on the container rebuild.
|
||||
setImmediate(() => {
|
||||
this._autoCheckAndApply().catch(err =>
|
||||
console.error('[SelfUpdater] %s-triggered update error: %s', triggeredBy, err.message)
|
||||
);
|
||||
});
|
||||
return { accepted: true, triggeredBy };
|
||||
}
|
||||
|
||||
// ── Check for Updates ──
|
||||
|
||||
async checkForUpdate() {
|
||||
@@ -536,6 +563,24 @@ class SelfUpdater extends EventEmitter {
|
||||
return digest[0] % 100;
|
||||
}
|
||||
|
||||
_loadOrCreateNotifySecret() {
|
||||
try {
|
||||
if (fs.existsSync(this.notifySecretFile)) {
|
||||
const existing = fs.readFileSync(this.notifySecretFile, 'utf8').trim();
|
||||
if (existing) return existing;
|
||||
}
|
||||
} catch (_) { /* regenerate */ }
|
||||
|
||||
const secret = crypto.randomBytes(24).toString('base64url');
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(this.notifySecretFile), { recursive: true });
|
||||
fs.writeFileSync(this.notifySecretFile, `${secret}\n`, { mode: 0o600 });
|
||||
} catch (error) {
|
||||
console.warn('[SelfUpdater] Failed to persist notify secret:', error.message);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
_loadOrCreateInstanceId() {
|
||||
try {
|
||||
if (fs.existsSync(this.config.instanceIdFile)) {
|
||||
|
||||
Reference in New Issue
Block a user