[grade=A urn:ump:seyasvbntxsjibq5jiaqmfowc6xgchwe4jvcc54jd6355ujgm75q] DC-122: self-updater hardening + auto-update on + docker disk discipline (v1.16.0)
- _isNewer: same-version releases are never 'newer' (commit labels are opaque stamps) — kills the same-version auto-apply regression loop - _autoCheckAndApply: identical version@sha256 never re-applied - dashcaddy-update.sh: truthful rollback verdicts (failed rebuild/health = exit 1 + failure result), exact frontend snapshot/restore (incl. update-introduced owned subtrees), contents-copy cp fallbacks with manifest reconciliation, JSON-encoded results/meta/stamp, prune on every exit path - self-updater: frontend-only Linux releases fail loudly (no more silent no-op success stuck in 'applying') - start.sh: DASHCADDY_UPDATE_ENABLED=true (Sami 2026-09-13), json-file log caps 10M x3, source->webroot sync via update-stamp contract - tests: _isNewer regression suite, functional cycle + json/escape suites (scripts/test-frontend-cycle.sh, scripts/test-json-escape.sh) 15 judge rounds: C,C,D,D,C,C,D,C,C,C,D,C,A
This commit is contained in:
@@ -305,6 +305,11 @@ class SelfUpdater extends EventEmitter {
|
||||
JSON.stringify(trigger, null, 2)
|
||||
);
|
||||
|
||||
// DC-122 note: the frontend deployment stamp (update-stamp.json) is
|
||||
// written by the HOST-side dashcaddy-update.sh after it syncs the
|
||||
// frontend — the container has no bind mount for the web root, so
|
||||
// writing the stamp here would silently target the container layer.
|
||||
|
||||
// The host-side systemd service will handle the rest.
|
||||
// After container restart, checkPostUpdateResult() reads the result.
|
||||
this._addToHistory({
|
||||
@@ -317,6 +322,13 @@ class SelfUpdater extends EventEmitter {
|
||||
channel: this.config.channel,
|
||||
instanceId: this.instanceId,
|
||||
});
|
||||
} else if (frontendSrc && this.config.hostFrontendDir && !isWindows) {
|
||||
// DC-122: frontend-only release on Linux with deferred host deploy —
|
||||
// there is no API component to trigger, and the container cannot
|
||||
// reach the web root itself. Fail loudly instead of silently
|
||||
// succeeding while deploying nothing. The enclosing catch records
|
||||
// the single 'failed' history entry and resets status.
|
||||
throw new Error('Frontend-only release cannot be applied on this install: no API component to trigger the host-side deploy. Publish a full release (dashcaddy-api + status).');
|
||||
} else if (isWindows) {
|
||||
// Windows: frontend updated, API needs manual restart
|
||||
this._addToHistory({
|
||||
@@ -383,7 +395,18 @@ class SelfUpdater extends EventEmitter {
|
||||
|
||||
if (historyIndex !== -1) {
|
||||
const pending = history[historyIndex];
|
||||
pending.status = result.success ? 'success' : 'rolled-back';
|
||||
// DC-122: truthful status. success:true → 'success'
|
||||
// success:false + error mentions rollback → 'rolled-back'
|
||||
// any other failure → 'failed'
|
||||
// (an explicit rollback whose rebuild/health also failed is NOT a
|
||||
// successful rollback — it must not be recorded as one)
|
||||
if (result.success) {
|
||||
pending.status = 'success';
|
||||
} else if (typeof result.error === 'string' && /rolled back/i.test(result.error)) {
|
||||
pending.status = 'rolled-back';
|
||||
} else {
|
||||
pending.status = 'failed';
|
||||
}
|
||||
pending.duration = result.duration;
|
||||
if (result.error) pending.error = result.error;
|
||||
if (result.version) pending.version = result.version;
|
||||
@@ -469,8 +492,18 @@ class SelfUpdater extends EventEmitter {
|
||||
try {
|
||||
const result = await this.checkForUpdate();
|
||||
if (result.available && result.remote) {
|
||||
// DC-122 defense-in-depth: never re-apply an identical
|
||||
// version@sha256 within this process lifetime, even if version
|
||||
// stamping after an apply fails and the next check still reports
|
||||
// "newer". Prevents repeated rebuild loops when auto-update is on.
|
||||
const ref = `${result.remote.version}@${result.remote.sha256 || ''}`;
|
||||
if (ref === this._lastAppliedRef) {
|
||||
log.info('updater', 'Skipping auto-apply: identical release already applied', { ref });
|
||||
return;
|
||||
}
|
||||
log.info('updater', 'Update available', { localVersion: result.local.version, remoteVersion: result.remote.version });
|
||||
await this.applyUpdate(result.remote);
|
||||
this._lastAppliedRef = ref;
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('updater', e, { phase: 'autoUpdate' });
|
||||
@@ -561,12 +594,12 @@ class SelfUpdater extends EventEmitter {
|
||||
|
||||
_isNewer(local, remote) {
|
||||
if (!remote || !remote.version) return false;
|
||||
const versionCompare = this._compareVersions(local.version || '0.0.0', remote.version);
|
||||
if (versionCompare < 0) return true;
|
||||
if (versionCompare > 0) return false;
|
||||
// Same version — check commit hash
|
||||
if (remote.commit && local.commit && remote.commit !== local.commit) return true;
|
||||
return false;
|
||||
// DC-122: same version ⇒ NOT newer, period. Commit hashes are opaque
|
||||
// build labels (pipelines stamp different formats — short SHA vs
|
||||
// timestamp-prefixed), so any inequality would read as "newer" and made
|
||||
// same-version installs re-apply stale tarballs in a loop once
|
||||
// DASHCADDY_UPDATE_ENABLED=true. A real release must bump semver.
|
||||
return this._compareVersions(local.version || '0.0.0', remote.version) < 0;
|
||||
}
|
||||
|
||||
_compareVersions(a, b) {
|
||||
|
||||
Reference in New Issue
Block a user