From 7557b49b5b5c5863b2a9d348bfd7a12ee7e1fe35 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 14 Sep 2026 05:21:41 -0700 Subject: [PATCH] [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) --- DC-P6-EVIDENCE.md | 201 +++++++++++++++++++++++++++++++++ DC-PRODUCTION-GRADE-BACKLOG.md | 122 +++++++++++++++++++- 2 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 DC-P6-EVIDENCE.md diff --git a/DC-P6-EVIDENCE.md b/DC-P6-EVIDENCE.md new file mode 100644 index 0000000..03ee144 --- /dev/null +++ b/DC-P6-EVIDENCE.md @@ -0,0 +1,201 @@ +# 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): +```js + if (remote?.revoked === true) { +``` +```js + 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'`): +```js + 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`): +```json +{"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'`): +```bash + 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: +```js + 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: +```bash + # 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. + diff --git a/DC-PRODUCTION-GRADE-BACKLOG.md b/DC-PRODUCTION-GRADE-BACKLOG.md index d81a101..0a139c0 100644 --- a/DC-PRODUCTION-GRADE-BACKLOG.md +++ b/DC-PRODUCTION-GRADE-BACKLOG.md @@ -1,8 +1,10 @@ -# DashCaddy Production-Grade Backlog (v2) +# DashCaddy Production-Grade Backlog (v3) -> Generated 2026-08-12 from a full codebase audit. +> Generated 2026-08-12 from a full codebase audit; last revised 2026-09-14 (P6 added). > v1 items (P0-1 through P2-7) are ALL DONE. -> Current state: 1539 tests, 86.55% statement coverage, 0 ESLint errors, 173 warnings. +> NOTE: the "Current Health Snapshot" below is HISTORICAL (2026-08-12 audit snapshot), +> kept for trend reference — re-run the audit before quoting these numbers. +> Snapshot values (not current): 1539 tests, 86.55% statement coverage, 0 ESLint errors, 173 warnings. ## Current Health Snapshot - **Tests:** 1539 passing across 63 suites @@ -257,7 +259,7 @@ - **impact:** Users set a disk budget (e.g., "DashCaddy gets 20GB") and the system auto-manages cleanup. The #1 reason people abandon self-hosting is disk filling up silently. This solves it. ### DC-102: One-click deploy should auto-generate Caddyfile entry + DNS record -- **status:** already done (DiskSpaceMonitor) +- **status:** pending (prior "already done (DiskSpaceMonitor)" annotation was a mismatched status note — DiskSpaceMonitor is a DC-101 disk-budget component, not a deploy-chain implementation; the deploy chain itself is not wired) - **details:** When a user deploys an app from the catalog, DashCaddy should automatically: (1) Create the Docker container, (2) Add a Caddyfile reverse_proxy block with TLS for `appname.tld`, (3) Create a DNS record pointing to the host, (4) Reload Caddy, (5) Add the service to the dashboard with health check. Currently steps 2-4 are manual. Fix: add a `deployApp(serviceId, options)` function that orchestrates the full chain. The Caddyfile generation can use the admin API (POST to :2019) so no file editing needed. DNS record creation uses the existing Technitium/Cloudflare DNS provider integration. Effort: ~4 hr. - **impact:** This is THE core value proposition. Without this, DashCaddy is just Portainer with extra steps. With this, it's a self-hosting platform. @@ -293,6 +295,115 @@ --- +## P6 — Shipdeck era: cross-platform & barrier removal (added 2026-09-14, Sami directive) + +> Source: "further improve DashCaddy and remove barriers to quality usage of it +> on all platforms including PC, Mac and Linux" — planned alongside the Shipdeck +> v0 landing (`shipdeck/docs/SPEC.md` in the shipdeck repo, "Definition of done +> (v0) — MET 2026-09-14": hello-world on samihost, build → systemd active → +> caddy gate (tailnet 200 / public 403) → DNS on DNS2+DNS1 → HTTP health 200 → +> journal row, verified rollback). The Shipdeck SPEC names DashCaddy-family use: +> "the +> deploy engine under the DashCaddy family (DashCaddy panel drives the CLI)"; +> these items wire that in. Build order: Lane A deployment (DC-109–112) → +> Lane B onboarding (DC-113–115) → Lane C platform parity (DC-116–119). +> +> **Lane acceptance criteria (lane-level outcomes; a lane is done when these hold):** +> - Lane A: on a Linux/systemd host where Docker is absent or unused, a real +> service completes a fresh shipdeck deploy, one in-place update, and one +> rollback — each verified green. Docker regression criteria (must all hold +> after each lane item): existing Docker services still deploy/update/roll +> back through the unchanged Docker path; existing services.json configs +> load without migration errors; the full Jest suite passes at or above the +> current gate. Secrets are never logged. This lane owns the +> rollback-failure requirement: DC-112 must auto-rollback and alert on +> failed post-update health. +> - Lane B: on a fresh environment (new browser profile / clean VM), the +> doctor detects each seeded environment fault, the auth helper names the +> failed hop, and the update nudge stays silent when current. +> - Lane C: every parity claim is CI-proven per OS (build + boot + smoke), +> not grep-audited. + +### DC-109: Native (Docker-free) service runtime — services gain `runtime: docker|native` +- **status:** pending +- **details:** Extend the service model with a runtime field. `docker` = today's behavior, unchanged. `native` = the Shipdeck pipeline: local build → tarball → scp → `/opt//releases//` + `current` symlink → systemd unit → Caddy block → DNS → verify → rollback. All implemented and DoD-verified in Shipdeck v0 (`shipdeck/docs/SPEC.md` — "Definition of done (v0) — MET 2026-09-14": hello-world on samihost, tailnet 200 / public 403, DNS on DNS2+DNS1, journal row, verified rollback). A host running only native services needs NO Docker. **Scope note: DC-109–112 initially target remote Linux/systemd hosts; local macOS/Windows native service parity arrives via DC-117.** Effort: ~6 hr (services.json schema + deploy routing + status surfacing). +- **impact:** Removes the largest install dependency identified in this lane's analysis (local Docker Desktop) for PC/Mac users deploying to a remote server; native rollback is one command instead of `docker build` on the VPS. + +### DC-110: Installer "remote server" mode — SSH target, zero local dependencies +- **status:** pending +- **details:** The Electron installer asks: this machine (classic 5-step wizard, Docker mode) or remote server (SSH host + key, Shipdeck mode). Remote mode skips the Docker/Caddy dependency checks entirely — it configures the remote host and prints the dashboard URL. This is the Mac unlock: no Docker Desktop account, no local engine, works from any laptop. Single-host seed of DC-108 (fleet). Open design points to settle at build time: whether remote mode also provisions remote Caddy/DNS or guides the operator through them, SSH host-key verification policy, and secret-handling (keys never logged, never stored plaintext beyond the user's chosen location). Effort: ~5 hr. +- **impact:** Turns "install DashCaddy" from a multi-step dependency hunt into a short wizard; removes the largest single install dependency (local Docker) for non-Linux users. + +### DC-111: DashCaddy dogfoods its own distribution via Shipdeck (native self-host path) +- **status:** pending +- **details:** Publish a native (non-Docker) DashCaddy flavor: release tarball + generated systemd unit + `current` symlink swap, deployed by Shipdeck. `start.sh` stays for the Docker flavor; the native flavor makes updates a symlink swap instead of `docker build` on the user's VPS. Same release feed (version.json + `revoked` kill switch). Effort: ~4 hr. +- **impact:** Installs DashCaddy on hosts without Docker; dogfoods the native path we are selling. + +### DC-112: Updater v2 — release-dir + symlink-swap updates for the native engine +- **status:** pending +- **details:** Generalize the self-updater shipped in DashCaddy v1.16.0 to the native flavor. Verifiable source of the existing contract: `dashcaddy-api/src/docker/self-updater.js:531` relative to the production-tree root `/opt/dashcaddy` — 30-min release-feed poll (`CHECK_INTERVAL: 30 * 60 * 1000` at line 24), channel selection (`CHANNEL` at line 35), `"revoked": true` kill switch (that line), update-stamp on frontend deploys — written by the host-side `scripts/dashcaddy-update.sh` helper which the self-updater orchestrates (verbatim excerpts, live stamp file, and sync-gate capture in `DC-P6-EVIDENCE.md`, E1/E6); the deploy/rollback flow is also documented in the `dashcaddy-ops` skill, "Self-updater (v1.16.0+, DC-122)" section). Native flavor: download tarball → new release dir → swap `current` → restart → health check → auto-rollback on failed health. Docker path untouched. Effort: ~5 hr. +- **impact:** Native installs get the same hands-off updates and rollback safety Docker installs already have. + +### DC-113: First-run doctor — preflight checks + one-click fixes +- **status:** pending +- **details:** One screen at first run (and from Help): Docker reachable? Caddy binary + admin port? ports 80/443 free? DNS resolvable? disk space? Each check shows fix instructions or a one-click fix where safe. Kills the "blank page on gated service" support class (documented failure mode). Effort: ~4 hr. +- **impact:** Support experience to date (DashCaddy sessions 2026-05→09) has been dominated by environment/config issues rather than code bugs; the doctor turns that class into self-service. + +### DC-114: "Why am I seeing this?" helper on the TOTP/auth gate +- **status:** pending +- **details:** The auth gate is a recurring confusion point (documented in the dashcaddy skill reference `totp-session-ip-key-inconsistency.md`, including the operator report "I keep providing a code and it doesn't work"). Login page gets inline diagnostics: which hop failed, cookie status, IP-consistency note, retry guidance. Server returns structured reason codes instead of bare 401. Effort: ~3 hr. +- **impact:** Converts the documented auth-gate drop-off case into a guided flow. + +### DC-115: Update nudge in the dashboard +- **status:** pending +- **details:** Footer badge comparing running version vs latest release feed; links to the update flow; dismissible; silent when current. The public version endpoint is verified in the live tree: `/api/v1/version` is in `PUBLIC_ROUTES` (`src/utilities/middleware.js`) and wired at startup (`src/app.js`). Latest-version source = the same release feed the self-updater polls (v1.16.0 contract). Effort: ~2 hr. +- **impact:** Users running months-old builds file phantom bugs; the nudge keeps fleets current. + +### DC-116: Podman as a supported container runtime +- **status:** pending +- **details:** Podman speaks Docker's socket API, so most DashCaddy container paths work against it with runtime detection + docs + a CI smoke test (rootless/quadlet notes included). Positions DashCaddy for orgs that cannot run Docker Desktop (licensing) and answers the "Docker vs open-source alternatives" wave with support instead of migration. Effort: ~4 hr. +- **impact:** Business-friendly runtime choice; removes licensing objections in the sellable tier. + +### DC-117: Service-control abstraction (systemd/launchd/Windows service) + per-OS static Pylon +- **status:** pending +- **details:** One service-control API over systemctl / launchd / sc.exe; Pylon ships as a single static agent per OS (no Node runtime required on managed hosts). `platform-paths.js` stays the single source of truth for paths (v1.12.0 lesson). Effort: ~8 hr. +- **impact:** True cross-platform management, not Linux-with-caveats. + +### DC-118: Mac Gatekeeper trust path — electron-builder native signing + notarization +- **status:** pending +- **details:** The Linux-built .dmg/.zip are unsigned → macOS Gatekeeper blocks/scare-warns on first open, and NO lightweight measure removes that friction: **ad-hoc signing does not establish developer identity and does not satisfy Gatekeeper distribution trust** (needs ~$99/yr Apple Developer Program). The fix: **electron-builder's built-in mac signing + notarization** — it signs nested frameworks/helpers with entitlements before the outer app (never hand-rolled `codesign --deep`) and submits notarization itself. Requires `electron-builder >= 24` (verify the version pinned in `dashcaddy-installer/package.json` at implementation time). + Implementation shape (config, not a hand-written script — the script gets written and exercised IN this item's PR where a `macos-latest` runner can prove it): + ```js + // electron-builder.config.js (mac section) — shape valid for electron-builder 24.x–26.x. + // PIN the actual major against dashcaddy-installer/package.json at implementation time; + // if the pinned major is >= 27, migrate per its mac.sign/notarize schema change before use. + mac: { + identity: "Developer ID Application: (${TEAMID})", + hardenedRuntime: true, + gatekeeperAssess: true, + entitlements: "build/entitlements.mac.plist", + notarize: true, // auto notarize + staple on CI (24.x–26.x shape) + forceCodeSigning: true, // FATAL on missing credentials — no silent unsigned artifacts + } + ``` + CI env (Actions secrets only): `CSC_LINK` (the base64-decoded .p12 file path) + `CSC_KEY_PASSWORD` (the .p12's password) — with a CSC_LINK p12, electron-builder imports it into its OWN temporary keychain internally, so the PR must NOT hand-roll keychain lifecycle (no custom create/unlock/partition-list code); notarization uses `APPLE_ID` + `APPLE_APP_SPECIFIC_PASSWORD` + `APPLE_TEAM_ID`. + **Acceptance criteria (this item is done only when all pass on a real `macos-latest` run):** + 1. `electron-builder --mac` exits 0 with signing + notarization enabled. + 2. Produced `.dmg`: `xcrun stapler validate ` passes and `spctl -a -t open --context context:primary-signature-id -v ` reports accepted. + 3. Produced `.zip`: extract it, then run `spctl -a -t exec -v ` on the contained app — must report accepted. Stapling is per-bundle: the `.app` inside the ZIP carries electron-builder's staple; the ZIP container itself cannot be stapled, and Gatekeeper on macOS 12+ re-queries Apple's notarization service online at first open to assess the signed `.app` inside. + 4. Notary submission id logged; on failure, `xcrun notarytool log …` output is attached before any retry. + 5. First-open gate on a clean macOS VM must exercise the real download path with quarantine applied: download via a browser, OR confirm/add the xattr explicitly after any non-browser transfer (`xattr -w com.apple.quarantine "0081;00000000;Safari;" ` — do NOT rely on plain `curl`/`scp` to set it; macOS may not apply quarantine to CLI-downloaded files). First open then shows no Gatekeeper block. + Drafting-review pitfalls from the 2026-09-14 adversarial rounds are HISTORICAL and inapplicable to this electron-builder approach (they applied to an earlier hand-rolled shell-script draft: keychain passwords, `mktemp -u`, search-list restore, filename gating — all now handled by electron-builder's internal keychain management). The version-validation rule stands: at implementation, confirm the pinned electron-builder major's actual `mac.sign`, `notarize`, and `forceCodeSigning` schema against its docs — do not trust the broad 24.x–26.x shape above without checking. + The Linux .dmg build this slots into is documented in `dashcaddy-installer/BUILD_GUIDE.md` (libguestfs HFS+ volume + libdmg-hfsplus UDZO). Effort: ~2 hr once enrolled (+$99/yr Apple Developer Program). +- **impact:** Today the first Mac impression is a security warning; the trust path must be fixed before paid acquisition, and only real signing + notarization does it. + +### DC-119: Cross-platform CI matrix — boot it on all 3 OSes per release +- **status:** pending +- **details:** Per release: build API + installer on Windows/macOS/Linux runners, boot the API, run smoke probes (version + health), run the installer's dependency-checker in report mode. Replaces grep-audits with runtime proof — matches the reproducibility principle (Sami 2026-06: "other people can do the same things and expect reproducibility"). Effort: ~6 hr. +- **impact:** Platform-parity claims become tested facts, not hopes. + +--- + ## Summary by Priority | Priority | Count | Effort | Theme | @@ -305,4 +416,5 @@ | P3.5 | 9 (DC-086–094) | ~14.5 hr | Operational maturity | | P4 | 6 (DC-095–100) | ~16.5 hr | Advanced features | | P5 | 8 (DC-101–108) | ~29 hr | Product vision: self-hosting platform | -| **Total** | **47** | **~110.5 hr** | | +| P6 | 11 (DC-109–119) | ~49 hr | Shipdeck era: cross-platform & barrier removal | +| **Total** | **58** | **~159.5 hr** | |