diff --git a/.planning/STATE.md b/.planning/STATE.md index c05ac74ba..5e0a184a1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -392,10 +392,56 @@ sibling agent removed an unrelated unused-var). ### C2 — Auto-deployment -- [ ] Coolify webhook on GHCR push (or watchtower equivalent) -- [ ] Failure path: admin Telegram notify (if configured) + audit log entry -- [ ] Test by tagging next release and watching auto-deploy fire +- [x] Coolify webhook on GHCR push (`docker-publish.yml` → + Coolify deploy URL with Bearer; `continue-on-error: true`) +- [x] Failure path: admin Telegram notify via existing + `dispatchNotification(SYSTEM_ALERT)` + `system.deploy.{success,failure,unknown}` + audit-log entries +- [ ] DEFERRED — actual end-to-end test ("tag next release and watch + auto-deploy fire") requires Marc-side secret population (see + `phase-C2-report.md` "Marc-side follow-up"); code paths are + unit-tested - Detailed report: `.planning/phase-C2-report.md` +- Audit deep-dive: `docs/audit/v1415-auto-deploy.md` + +#### C2 status block — 2026-05-09T21:25+02:00 — done + +Two atomic commits on `origin/main`: + +- `34c967c` (sibling agent's `feat(achievements)` message — + shared-cwd race; **my** docker-publish.yml + .env.example + + docs/audit/v1415-auto-deploy.md hunks rode along; diff is correct, + message scope misleading; same pattern A2 / A4 / B1 / B-mobile / + B2 / B3 documented). +- `ad350fe` `feat(deploy): admin notification + audit log on deploy + success/failure` — clean, my files only (used `git commit -o + ` to defeat the race). + +Investigation: Coolify `4.0.0-beta.470` running on `apps-01` exposes a +Bearer-protected `GET /api/v1/deploy?uuid=…` endpoint. We trigger it +from the `Build & Publish Docker Image` workflow once `:latest` is on +GHCR (via 2 new GH repo secrets `COOLIFY_WEBHOOK` + `COOLIFY_TOKEN` — +Marc populates). Coolify in turn pings our new +`POST /api/internal/deploy-webhook` with success/failure status; that +endpoint timing-safe-validates `X-Deploy-Webhook-Secret` against env +`DEPLOY_WEBHOOK_SECRET`, audit-logs, and on failure pages every admin +via Telegram (existing `dispatchNotification`). Watchtower rejected +in favour of native Coolify path. + +Tests: 952 / 952 unit pass (was 940 at C2 start; +12 new for the +deploy-webhook handler covering auth, success, failure with admin +fan-out, unknown-state, no-admin warning, rate-limit, malformed +payload, forward-compat preservation). + +Files committed (4): `.github/workflows/docker-publish.yml`, +`.env.example`, `docs/audit/v1415-auto-deploy.md`, +`src/app/api/internal/deploy-webhook/{route.ts,__tests__/route.test.ts}`. +No touches to admin / dashboard / onboarding / doctor-report / charts / +achievements / docker-publish.yml-from-C3 scope. + +Marc-side follow-up (4 manual steps tracked in +`phase-C2-report.md`): Coolify token, 2 GH secrets, `DEPLOY_WEBHOOK_SECRET` +in app env + Coolify outgoing webhook config, no-op-commit smoke test. ### C3 — CI/e2e reliability audit diff --git a/.planning/phase-C2-report.md b/.planning/phase-C2-report.md new file mode 100644 index 000000000..0e6f6fc1a --- /dev/null +++ b/.planning/phase-C2-report.md @@ -0,0 +1,109 @@ +# Phase C2 — Auto-deployment via Coolify webhook + +**Status:** done +**Wall clock:** 2026-05-09T21:10 → 2026-05-09T21:25 Berlin (~15 min) +**Commits on `origin/main`:** + +| SHA | Message (real diff scope) | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `34c967c` | `feat(achievements): …` — sibling-agent commit message; **my** docker-publish.yml + .env.example + docs/audit/v1415-auto-deploy.md hunks rode along (5th run of the shared-cwd race documented in A2/A4/B1/B-mobile/B2/B3). | +| `ad350fe` | `feat(deploy): admin notification + audit log on deploy success/failure` — clean, my files only (`git commit -o` pathspec). | + +Linked deep-dive: [`docs/audit/v1415-auto-deploy.md`](../docs/audit/v1415-auto-deploy.md). + +## What changed + +### 1. Auto-deploy trigger (`docker-publish.yml`) + +After the `Build & push image` step, a new `Trigger Coolify deploy` step +fires `GET https://apps-01.bombeck.io/api/v1/deploy?uuid=…` with a Bearer +token. Step is `continue-on-error: true` (image is already on GHCR — a +Coolify outage must not retroactively fail the build) and skipped on PRs. +Two GitHub repository secrets gate it: `COOLIFY_WEBHOOK` (full URL) + +`COOLIFY_TOKEN` (Bearer token from Coolify UI). Missing secrets emit a +`::warning::` rather than calling Coolify with empty auth. + +### 2. Status webhook handler (`src/app/api/internal/deploy-webhook/`) + +`POST` accepts the free-form Coolify deploy-event payload, normalises it +defensively, writes one of `system.deploy.{success,failure,unknown}` +audit rows (with the full original payload preserved in `details.raw` for +forward-compat), and on `failure` fans a `SYSTEM_ALERT` through +`dispatchNotification()` to every admin user. `GET` is a reachability +check for Coolify's "Test" button. + +Auth: timing-safe `X-Deploy-Webhook-Secret` header compare against +`process.env.DEPLOY_WEBHOOK_SECRET`. Rate-limited 60/min/IP. No new +server dependencies (Next.js native, plus existing helpers from +`api-handler`, `rate-limit`, `auth/audit`, `notifications/dispatcher`). + +### 3. Documentation + +- `docs/audit/v1415-auto-deploy.md` — investigation, decision log + (Coolify webhook over Watchtower), failure modes, **Marc-side + follow-up steps**. +- `.env.example` — new `DEPLOY_WEBHOOK_SECRET` block with link to + the audit doc. + +## Verification + +- `pnpm test` — **952 / 952 pass** (was 940 at C2 start; +12 new tests + for the deploy-webhook handler). +- `pnpm typecheck` — pre-existing `dashboard-layout.test.ts` (A4 lane) + and `doctor-report-pdf-core.test.ts` (B6 lane) errors only; nothing new + in C2 scope. +- `pnpm lint` — no new errors in C2 files; pre-existing `doctor-report-dialog.tsx` + set-state-in-effect error untouched (B6 owns). +- Workflow YAML parses (`ruby -ryaml -e "YAML.load_file('.github/workflows/docker-publish.yml')"` — `yaml ok`). + +## Marc-side follow-up (4 manual steps) + +The code is fully shipped, but auto-deploy doesn't fire until Marc +populates the secrets. None of these are agent-doable. + +1. **Generate a Coolify API token** in `https://apps-01.bombeck.io/` + → Keys & Tokens → "Create New Token", permission `*`. Copy once, + it's not redisplayed. +2. **Add 2 GitHub repository secrets** (Settings → Secrets → Actions): + - `COOLIFY_WEBHOOK = https://apps-01.bombeck.io/api/v1/deploy?uuid=pg8wggwogo8c4gc4ks0kk4ss&force=false` + - `COOLIFY_TOKEN = ` +3. **Generate `DEPLOY_WEBHOOK_SECRET`** with `openssl rand -hex 32`, + then add it to: + - HealthLog container env on `apps-01` (Coolify UI → app → Environment Variables → `DEPLOY_WEBHOOK_SECRET`). + - Coolify outgoing webhook config (Settings → Notifications → Webhook): + - URL: `https://healthlog.bombeck.io/api/internal/deploy-webhook` + - Custom header: `X-Deploy-Webhook-Secret: ` + - Enable: deployment success + deployment failure +4. **Test** with a no-op commit on `main`. Expected behaviour: + - GHCR build runs and pushes `:latest`. + - GH Actions step "Trigger Coolify deploy" prints HTTP 200 with the + Coolify deploy job UUID. + - `apps-01` recreates the app container. + - Coolify pings `/api/internal/deploy-webhook`; AuditLog row + `system.deploy.success` appears in `/admin/audit-log`. + - `/api/version` reflects new build (or unchanged if no-op). + +## Race-condition note (operational) + +This is the **6th** consecutive phase to surface the shared-cwd staging +race. Commit-1 of C2 was absorbed into a sibling agent's +`feat(achievements)` commit message because their `git add -A` ran +between my `git add` and `git commit`. Recovered for commit-2 by using +`git commit -o ` which pre-stages exactly the listed paths +inside the same atomic syscall — no window for a sibling's index +change to leak in. Same recommendation as A2 / A4 / B1 / B-mobile / +B2 / B3: **v1.4.16 should mandate `superpowers:using-git-worktrees` +per parallel agent.** + +## Deferred — not in C2 scope + +- **Watchtower fallback** (Path B in the audit doc) — only if the + Coolify webhook proves unreliable across multiple v1.4.X releases. + Adding a polling daemon to the trusted compute base of `apps-01` is + not free. +- **Slack/Discord deploy channels** — pluggable through the same + `dispatchNotification` channel-abstraction; B3 already wired + notification reliability so adding a non-Telegram channel for + ops is a separate v1.4.16 ticket. +- **Bidirectional sync to GitHub Deployments API** — would require a + GitHub App token, separate scope.