Wave-D reconcile report at .planning/phase-D-reconcile-report.md; deferred items seeded to .planning/v1419-backlog.md; strategic v1.5 items appended to .planning/v15-backlog.md from the v1.4.18 Product-Lead review. STATE.md marks Wave D done. Prettier sweep also touched the planning markdown table widths; no content change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.8 KiB
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.
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— newDEPLOY_WEBHOOK_SECRETblock 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-existingdashboard-layout.test.ts(A4 lane) anddoctor-report-pdf-core.test.ts(B6 lane) errors only; nothing new in C2 scope.pnpm lint— no new errors in C2 files; pre-existingdoctor-report-dialog.tsxset-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.
- Generate a Coolify API token in
https://apps-01.bombeck.io/→ Keys & Tokens → "Create New Token", permission*. Copy once, it's not redisplayed. - Add 2 GitHub repository secrets (Settings → Secrets → Actions):
COOLIFY_WEBHOOK = https://apps-01.bombeck.io/api/v1/deploy?uuid=pg8wggwogo8c4gc4ks0kk4ss&force=falseCOOLIFY_TOKEN = <token from step 1>
- Generate
DEPLOY_WEBHOOK_SECRETwithopenssl 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: <value> - Enable: deployment success + deployment failure
- URL:
- HealthLog container env on
- 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-01recreates the app container.- Coolify pings
/api/internal/deploy-webhook; AuditLog rowsystem.deploy.successappears in/admin/audit-log. /api/versionreflects new build (or unchanged if no-op).
- GHCR build runs and pushes
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 <pathspec> 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-01is not free. - Slack/Discord deploy channels — pluggable through the same
dispatchNotificationchannel-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.