[grade=A urn:ump:seyasvbntxsjibq5jiaqmfowc6xgchwe4jvcc54jd6355ujgm75q] DC-122: self-updater hardening + auto-update on + docker disk discipline (v1.16.0)
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

- _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:
DashCaddy Polish Loop
2026-09-13 05:32:14 -07:00
parent 939fdbb68b
commit 70e252c8a5
7 changed files with 631 additions and 46 deletions
+40 -7
View File
@@ -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) {