The host-side updater only backed up code + data/, leaving trigger.json and
result.json unarchived. After a failed update, operators had to reconstruct
'what was being attempted' by joining timestamps across files. Now the
backup captures both files into a 'update-state/' subdir alongside code +
data backups, keyed by from-version.
- New `backup_update_state()` function in dashcaddy-update.sh: idempotent,
tolerates absent files (cleans up empty subdir), tolerates chattr +i
(unlock/copy/relock).
- Wired into main() right after `backup_data_dir`, before `cleanup_old_backups`.
- Deliberately does NOT auto-restore trigger.json on rollback — the rollback
handler reads a fresh trigger.json written by the operator/container;
restoring the previous attempt's trigger would clobber the active rollback
request. Backups are read-only forensic evidence.
- New `dashcaddy-api/scripts/test-dashcaddy-update-backup.sh` (14 assertions,
5 test groups): both-files-present, partial-present, no-files-present,
idempotency, main() flow ordering. All 14 pass.
- Synced the duplicate at `dashcaddy-api/scripts/dashcaddy-update.sh`
(md5-identical to scripts/dashcaddy-update.sh).
Tests: 1214/1214 pass (zero change). Lint: 150 warnings, all pre-existing
in untouched files (zero new warnings introduced).
The host-side updater has been silently broken in two ways:
1. Empty staging directories would cause rm -rf of live routes/src with no
replacement, leaving the host tree gutted while the container kept serving
from its own image. Now deploy_tree() refuses to delete unless the staging
source has actual files.
2. chattr +i on critical files (used to protect security-hotfixed routes from
being clobbered by upstream tarballs) caused rm -rf to partially execute
then fail under set -e, leaving the host in a half-deleted state. Now
deploy_tree() scans for immutable files, unlocks them before replace,
and re-locks them after — so security-locked files survive every update.
Also adds:
- Channel gate: trigger.json channel=prerelease/beta/rc/alpha is rejected
unless ALLOW_PRERELEASE=true is set in /opt/dashcaddy/updates/channel.conf.
Default is 'stable only', safe for production. Staging hosts opt in.
- channel.conf.example documenting the new opt-in mechanism.
Verified end-to-end: manual trigger.json → path unit fired → routes (53 files)
+ src (62 files) deployed → container rebuilt → health check passed. totp.js
remained locked with security edits intact.
- VERSION: bump from 1.14.4 to 1.14.6 to match package.json (HEAD had stale value)
- middleware.js: apply existing totpLimiter (10/15min) to /totp/setup endpoint
(was previously unmetered, allowing secret enumeration)
- dashcaddy-update.sh: hook post-deploy-patches.sh into the update flow
so the container can survive transitions between broken → fixed tarballs
- start.sh: add --add-host flags for get.dashcaddy.net and get2.dashcaddy.net
so the container can resolve the release server (was failing with ENOTFOUND)