Files
dashcaddy/DC-P6-EVIDENCE.md
T
Hermes 7557b49b5b
CI / Test & Lint (push) Waiting to run
CI / Security audit (push) Waiting to run
[grade=B urn:ump:zluxbji5tepzdqybrctqfyjdvbfin2o66atw6rwykg6vdwkzhfsa] backlog v3: P6 Shipdeck era (DC-109-119) cross-platform + barrier-removal lane
- DC-109-112: native runtime, installer remote mode, self-distribution,
  updater v2 (Shipdeck engine; v0 DoD-verified)
- DC-113-115: first-run doctor, auth-gate helper, update nudge
- DC-116-119: Podman runtime, service-control abstraction, Mac signing
  (electron-builder, 5 acceptance gates), 3-OS CI matrix
- DC-P6-EVIDENCE.md: verbatim live-state evidence (E1-E6) backing the claims
- DC-102 status corrected (mismatched DiskSpaceMonitor annotation)
2026-09-14 05:22:16 -07:00

11 KiB

Evidence file for DC-PRODUCTION-GRADE-BACKLOG.md P6 section (rev14)

Verbatim excerpts captured 2026-09-14 from the production tree /opt/dashcaddy and the live DNS2 deployment, so every live-state claim in the backlog can be checked against this file.

E1. Self-updater contract — dashcaddy-api/src/docker/self-updater.js (production tree /opt/dashcaddy)

Verbatim grep output (grep -n "UPDATE_URL\|MIRROR_URL\|CHANNEL\|checkInterval\|INTERVAL" ...):

24:  CHECK_INTERVAL: 30 * 60 * 1000,  // 30 minutes
25:  UPDATE_URL: process.env.DASHCADDY_UPDATE_URL || 'https://get.dashcaddy.net/release',
26:  MIRROR_URL: process.env.DASHCADDY_MIRROR_URL || 'https://get2.dashcaddy.net/release',
35:  CHANNEL: process.env.DASHCADDY_UPDATE_CHANNEL || 'stable',
46:      checkInterval: parseInt(options.checkInterval || DEFAULTS.CHECK_INTERVAL, 10),
47:      updateUrl: options.updateUrl || DEFAULTS.UPDATE_URL,
48:      mirrorUrl: options.mirrorUrl || DEFAULTS.MIRROR_URL,
64:      channel: options.channel || process.env.DASHCADDY_UPDATE_CHANNEL || DEFAULTS.CHANNEL,

Revoked kill switch, verbatim (same file, lines 531/535):

    if (remote?.revoked === true) {
        reason: 'release revoked',

Proves: 30-min poll interval, update + mirror feed URLs, channel selection, and the revoked: true kill switch — all in self-updater.js of the production tree.

E2. Shipdeck v0 DoD — shipdeck/docs/SPEC.md, verbatim

## Definition of done (v0) — MET 2026-09-14
- [x] `shipdeck deploy` lands a real Go hello-world on samihost end-to-end:
      build → systemd active → caddy gate (tailnet 200 / public 403) → DNS on
      DNS2+DNS1 → HTTP health 200 → journal row (epochs 1789360016, …0128,
      …0610, …0672; rollback …0128→…0016 verified with content diff)
- [x] `shipdeck status` all green; `shipdeck rollback` swaps + re-verifies green

E3. Version endpoint — production tree /opt/dashcaddy, verbatim grep output

PUBLIC_ROUTES membership (src/utilities/middleware.js):

488-    { path: '/api/v1/monitoring/stats',     exact: true, method: 'GET', monitoring: true },
489-    { path: '/api/v1/health-checks/status', exact: true, method: 'GET', monitoring: true },
490:    { path: '/api/v1/version',             exact: true, method: 'GET' },

Route registration at startup (src/app.js, verbatim sed -n '519,523p'):

  appName = versionRoute.getName();
  // Pre-build the version router once at startup and reuse it.
  const versionRouter = versionRoute.buildRouter();
  apiRouter.use(versionRouter);
  log.info('app', `Version endpoint available at /api/v1/version (v${appVersion})`);

(The version HANDLER source lives in the route module wired by versionRoute.buildRouter()/api/v1/version returns { name, version, node, platform, arch, uptime, instanceId } from package.json read at startup, per the E5 live response below.)

E4. Installer build targets — dashcaddy-installer/BUILD_GUIDE.md, verbatim

# Windows (creates portable .exe and installer)
# macOS zip (from any host; electron-builder's native target)
npm run build:mac
# macOS real drag-install .dmg — built ON LINUX, no Mac needed
# (one-time toolchain: see scripts/build-dmg-linux.sh header)
# Linux (creates AppImage and .deb)
npm run build:linux

Output artifacts (same file): portable + NSIS .exe (Windows), mac .zip + Linux-built .dmg (macOS, unsigned), AppImage + .deb (Linux).

E5. Live deployment state on DNS2 — complete captured output (rev14), capture start 2026-09-14T11:50:10Z, all three exit codes 0

The block below is the COMPLETE, unedited terminal capture of the three commands (per-command timestamp + exit status embedded). The first command's stdout is the full version.json — no ellipses; the long changelog string is part of the real response.

=== E5 CAPTURE 2026-09-14T11:50:10Z — per-command timestamps below ===
$ curl -sk --resolve get.dashcaddy.net:443:127.0.0.1 https://get.dashcaddy.net/release/version.json
{
  "version": "1.16.0",
  "commit": "70e252c",
  "channel": "stable",
  "url": "https://get.dashcaddy.net/release/latest.tar.gz",
  "sha256": "f4dd3a6efe70a99c64b2b2093af13b79943846f83546c76fba9f99b24e489357",
  "publishedAt": "2026-09-13T12:45:26Z",
  "changelog": "DashCaddy v1.16.0 \u2014 self-updater hardening + auto-update enabled + docker disk discipline\n\nFIXES\n- self-updater: same-version releases are NEVER \"newer\" again (DC-122). Commit\n  labels are opaque build stamps; only a semver bump counts. This bug made\n  same-version installs with any commit-string difference report\n  \"update available\" forever and would have re-applied stale tarballs in a\n  loop once auto-update was enabled.\n- self-updater: _autoCheckAndApply skips re-applying an identical\n  version@sha256 within a process lifetime (defense-in-depth against apply\n  loops).\n- dashcaddy-update.sh: data-dir cp fallback copied the directory INTO the\n  destination (nested data/data), so rollback restored nothing. Now copies\n  contents (\"dir/.\") \u2014 backup AND restore paths fixed.\n- dashcaddy-update.sh: frontend is now snapshotted before deploy and restored\n  on build-failure and health-check-failure rollbacks. Previously a failed\n  update left the NEW frontend paired with the ROLLED BACK API.\n- start.sh: bundle sync is newer-source-only. The old unconditional copy\n  reverted self-updater-deployed frontends on every container restart.\n- dashcaddy-update.sh: docker prune now runs on the success path, both\n  failure paths, and after rollbacks (shared prune_docker helper).\n\nCHANGES\n- start.sh: DASHCADDY_UPDATE_ENABLED=true \u2014 auto-update is ON (Sami 2026-09-13).\n- start.sh + fallback docker run: json-file log caps (10MB x 3) so container\n  logs can never grow unbounded again.\n\nTESTS\n- __tests__/self-updater-isnewer.test.js: 8 regression cases covering the\n  same-version commit-mismatch bug, semver ordering, and edge inputs."
}curl_exit=0 at 2026-09-14T11:50:10Z

$ curl -s -m 6 http://127.0.0.1:3001/api/v1/version
{"success":true,"name":"dashcaddy-api","version":"1.16.0","node":"v20.11.1","platform":"linux","arch":"x64","uptime":1768.092256197,"instanceId":null}curl_exit=0 at 2026-09-14T11:50:10Z

$ docker inspect dashcaddy-api --format '{{range .Config.Env}}{{println .}}{{end}}' | grep UPDATE
DASHCADDY_UPDATE_ENABLED=true
grep_exit=0 at 2026-09-14T11:50:10Z

Proves, AS OF the capture timestamp (a dated deployment snapshot, not necessarily current state at review time): production DNS2 ran DashCaddy v1.16.0, channel stable, auto-update ON, feed reachable, and no revoked key in the live feed (kill switch not engaged). Re-run the three commands above to refresh.

E6. Update-stamp contract — live + source capture, rev14

(a) Live stamp file — /var/www/dashcaddy-status/update-stamp.json (verbatim cat):

{"version":"1.16.0","at":"2026-09-13T12:47:56Z"}

(b) The ACTUAL writer: /opt/dashcaddy/scripts/dashcaddy-update.sh (host-side helper executed by the self-updater's update flow; the Node self-updater orchestrates, this script writes the stamp — see excerpt, verbatim sed -n '714,725p'):

      cp -rf "$frontend_staging_dir/assets/"* "$frontend_target_dir/assets/" 2>/dev/null || true
    fi
    # DC-122: host-side deployment stamp — start.sh treats a stamped, newer
    # deployment as authoritative and skips its source-bundle sync (this is
    # the only writer that can reach the web root with real host paths).
    local esc_ver
    esc_ver=$(json_escape "$to_version")
    printf '{"version":"%s","at":"%s"}\n' "$esc_ver" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
      > "$frontend_target_dir/update-stamp.json" 2>/dev/null || true
    fi # DC-122 close: validated frontend-target branch
  fi

(version JSON-escaped, UTC timestamp, written into the validated frontend-target dir; || true only guards the stamp write itself — the deployment result is recorded separately in updates/result.json.)

(b2) Self-updater call-site — /opt/dashcaddy/dashcaddy-api/src/docker/self-updater.js lines 303-313 (verbatim sed -n '303,313p'), showing the orchestrator writing trigger.json and deferring stamp writing to the host-side helper:

        await fsp.writeFile(
          path.join(this.config.updatesDir, 'trigger.json'),
          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.

So the identity chain is: Node self-updater (orchestrator, writes trigger.json) → host-side dashcaddy-update.sh (executed via the systemd path unit; writes update-stamp.json into the web root). The word "writer" in (b) refers to dashcaddy-update.sh specifically. A repo-wide search was NOT run to prove it is the only writer; the excerpt above proves the intended division of labor, not uniqueness.

(c) Sync-gating side: /opt/dashcaddy/start.sh (verbatim sed -n '150,170p') — start.sh treats the stamp as the frontend authority and skips its source-bundle sync while the stamp file's MTIME beats the source tree's mtime — authority is decided by comparing file mtimes, not by parsing version values inside the files (prevents restart-reverts):


# Sync the freshly-built dashboard bundle into the static directory Caddy
# serves — decided by VERSION METADATA, not file mtimes (mtimes are not
# reliable: scp/tar/cp can preserve or shuffle them). DC-122 contract:
#   - The self-updater writes a STAMP (update-stamp.json) into the live web
#     root when it deploys a frontend; while that stamp is newer than the
#     source tree's VERSION file, start.sh must NOT touch the live bundle.
#   - Normal builds: publishing bumps the source VERSION (mtime = build time)
#     and clears any stale stamp, so source wins and the sync happens.
echo "[start.sh] Syncing dashboard bundle into static dir (metadata-driven)..."
mkdir -p /var/www/dashcaddy-status/dist
if [ -d /opt/dashcaddy/status/dist ]; then
  NEEDS_SYNC=1
  STAMP=/var/www/dashcaddy-status/update-stamp.json
  SRC_VERSION=/opt/dashcaddy/dashcaddy-api/VERSION
  if [ -f "$STAMP" ] && [ -f "$SRC_VERSION" ] && [ "$STAMP" -nt "$SRC_VERSION" ]; then
    # A self-updater deployment is newer than the last source build: hands off.
    echo "[start.sh] Deployed frontend stamp newer than source VERSION — skipping sync to preserve deployed frontend."
    NEEDS_SYNC=0
  fi
  if [ "$NEEDS_SYNC" = "1" ]; then

(d) Rollback interplay: dashcaddy-update.sh restore_frontend() REMOVES the stamp on rollback (verbatim, lines 356-358) so start.sh's source sync resumes authority — the restored frontend is not a self-updater deployment:

  # Rollback removes the deployment stamp: the restored frontend is NOT a
  # self-updater deployment, so start.sh's source sync must resume authority.
  rm -f "${target:?}/update-stamp.json"

Proves the update-stamp-on-frontend-deploy behavior cited by DC-112 end to end: writer identity, stamp content (version + UTC timestamp), sync gating, and rollback semantics.