Add CI/CD pipeline: Woodpecker, auto-staging / manual-prod
ci/woodpecker/push/woodpecker Pipeline was successful

Adds .woodpecker.yml (lint + typecheck on every push, auto-deploy to a
shared staging environment on every push, manual Deploy-button promotion to
production on main) and env.staging blocks in both wrangler configs so
staging gets its own Workers (diagrav-app-staging/diagrav-site-staging at
staging-app.diagrav.com/staging.diagrav.com) rather than sharing anything
with production.

Staging also got its own fully separate Supabase Cloud project (own
database, own Auth config reusing the same Google OAuth client with an
extra redirect URI, own Resend-backed SMTP) — migrated, seeded with the
public catalog, and verified end-to-end with a real sign-in through the
deployed app before wiring any of this into CI.

Updates deployment-plan.md's CI/CD section to match what actually got
built, superseding the earlier manual-pushbutton-for-both-environments
version of the plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
2026-09-29 10:36:15 -05:00
co-authored by Claude Sonnet 5
parent fc37ee5e8b
commit d847a0f255
4 changed files with 192 additions and 74 deletions
+71 -69
View File
@@ -1,11 +1,11 @@
# Deployment & CI/CD planning notes
Discussion notes only — nothing here has been implemented yet. Written so a
different Claude Code conversation on this repo can pick up where this one
left off without re-deriving it. Nothing below is committed to as final; it's
a record of what was discussed and concluded, plus open decisions.
Started as discussion-only notes; hosting and CI/CD are now both live (see
their sections below for what's actually implemented vs. still open).
Written so a different Claude Code conversation on this repo can pick up
where this one left off without re-deriving it.
## Hosting shape (discussed, not yet implemented)
## Hosting shape — **implemented**
The app is a static frontend (Vite build, no server of its own) + Supabase
backend (Postgres, Auth, RLS, one Edge Function). [organized-ideas.md](organized-ideas.md)
@@ -110,79 +110,81 @@ Estimated cost: **$0/mo** to start (Supabase Free + Vercel/Netlify/Cloudflare
Pages free tier + Resend free tier for auth email), **$25/mo** once you want
Supabase Pro (no project auto-pause, higher limits across the board).
## CI/CD plan: Gitea + Woodpecker, pushbutton staging/prod
## CI/CD: Woodpecker, auto-staging / manual-prod — **implemented**
Repo is self-hosted on Gitea (`git.halfbinary.net/aarbit/av-planner`) with
Woodpecker CI already in use elsewhere. No `.woodpecker.yml` exists yet in
this repo.
Woodpecker CI already in use elsewhere. `.woodpecker.yml` now exists at the
repo root. The flow below supersedes an earlier version of this section that
proposed manual pushbutton deploys for *both* staging and production —
revisited and simplified to auto-deploy staging instead.
**Desired flow**: PR merges to `main` → pipeline runs lint/typecheck/build
and stops → a human clicks a "Deploy" button to push to staging, and
separately clicks it again to promote to production. Not fully automatic on
merge — deploys to both environments are manual/pushbutton.
**Actual flow**: any push (any branch) runs lint + typecheck, then
auto-deploys to a shared staging environment — no button needed. Production
is never touched automatically; promoting to it is a manual **Deploy**
button click (Woodpecker's built-in deploy event) on a `main`-branch
pipeline run, typing `production` as the deploy target
(`$CI_PIPELINE_DEPLOY_TARGET`). Requires **"Allow deployments"** enabled in
the repo's Woodpecker project settings.
**Mechanism (confirmed against current Woodpecker docs)**: Woodpecker has a
built-in **deploy event** for exactly this. Enable **"Allow deployments"** in
the repo's Woodpecker project settings, and any successful pipeline run gets
a **Deploy** button in the UI. Clicking it re-runs that pipeline with
`event: deploy` and a `deploy_to` string you type (e.g. `staging` or
`production`), exposed to steps as `$CI_PIPELINE_DEPLOY_TARGET`. Steps gate
on it, e.g.:
**Two fully separate environments exist, each with its own everything** (no
shared data or infra between them, or with production):
```yaml
steps:
- name: deploy-staging
when:
- event: deploy
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "staging"'
- name: deploy-prod
when:
- event: deploy
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
```
| | Staging | Production |
|---|---|---|
| Supabase project | `onovxozixdxpdngxafxt` ("Diagrav Staging") | `wmombshptajyrawdbroj` ("Diagrav") |
| App Worker | `diagrav-app-staging` → `staging-app.diagrav.com` | `diagrav-app` → `app.diagrav.com` |
| Site Worker | `diagrav-site-staging` → `staging.diagrav.com` | `diagrav-site` → `diagrav.com` |
**What needs to exist**:
1. **Two Supabase projects** — staging and prod. This exactly fills the free
tier's "2 active projects" cap (see above) — no free-tier headroom left
for a third project if one is ever wanted later.
2. **Two frontend hosting targets** — two sites/deployments (SaaS host or
self-hosted alongside Gitea/Woodpecker — not yet decided which).
3. **Known wrinkle**: because Vite bakes `VITE_SUPABASE_URL`/`VITE_SUPABASE_ANON_KEY`
in at build time, you can't build once and promote the identical artifact
to both environments — each deploy target needs its own build with its
own env vars baked in. Not hard, just means "deploy" reruns the build
rather than reusing one artifact. A non-issue at this app's scale.
4. **Migrations run per-target** via Supabase CLI, scripted per environment:
```bash
supabase link --project-ref $SUPABASE_PROJECT_REF
supabase db push
supabase functions deploy admin-user-action
```
5. **Secrets needed in Woodpecker** (scoped per target): `SUPABASE_ACCESS_TOKEN`,
staging/prod `SUPABASE_PROJECT_REF`, staging/prod `VITE_SUPABASE_URL` /
`VITE_SUPABASE_ANON_KEY`, plus whatever the frontend host needs (API
token, or SSH key if self-hosted).
6. **Google OAuth**: needs its own authorized redirect URI added in Google
Cloud console for each of the staging and prod domains — one-time setup,
easy to forget.
This uses both of Supabase Free's "2 active projects" slots — no headroom
for a third project if one's ever wanted later (see cost findings above).
**Difficulty estimate discussed**: roughly a focused afternoon — nothing here
fights the tooling (no containers to orchestrate, no server process to run).
Breakdown: `.woodpecker.yml` (~1–2 hrs), second Supabase project + migration
check (~30 min), frontend hosting target wiring (~30–60 min), secrets +
"Allow deployments" toggle (~15 min), end-to-end test/debug (~1–2 hrs).
**How each piece works**:
- **Wrangler**: `wrangler.app.jsonc` / `wrangler.site.jsonc` each gained an
`env.staging` block (own `name` + own `routes`) — `assets.directory` isn't
per-environment in Wrangler, which is fine, since it's rebuilt with
different `VITE_*` values before each deploy anyway (the "known wrinkle"
below).
- **Known wrinkle (as anticipated)**: Vite bakes `VITE_SUPABASE_URL`/
`VITE_SUPABASE_ANON_KEY` in at build time, so `npm run build` reruns once
per target with that target's env vars set, rather than promoting one
artifact between environments.
- **Migrations**: `supabase db push --project-ref $REF --password
$SUPABASE_DB_PASSWORD` per target (no persistent `supabase link` — every
command that needs a project takes `--project-ref` directly, which also
meant this never touched the local repo's own `supabase link` state,
still pointed at production for everyday local dev).
- **Google OAuth**: staging reuses production's *same* OAuth client rather
than a new one — just one more authorized redirect URI
(`https://onovxozixdxpdngxafxt.supabase.co/auth/v1/callback`) added to it
in Google Cloud Console, since a client can authorize many redirect URIs.
- **SMTP**: staging reuses the same Resend account/verified domain as
production, with a distinct `smtp_sender_name` ("Diagrav (Staging)") so
the two are distinguishable in an inbox.
- **Staging catalog data**: seeded once by applying `supabase/seed.sql`
directly against the new project (not part of the pipeline — a one-time
setup step, same as production's original seeding).
**Secrets in Woodpecker** (repo Settings → Secrets): `cloudflare_api_token`,
`cloudflare_account_id`, `supabase_access_token` (shared); per-target
`{staging,prod}_supabase_url`, `{staging,prod}_supabase_anon_key`,
`{staging,prod}_supabase_project_ref`, `{staging,prod}_supabase_db_password`.
**Outstanding**: the production `supabase_db_password` secret — Supabase
doesn't expose an existing project's DB password via the Management API, so
this needs either the password from wherever it was originally saved, or a
deliberate decision to rotate it (a live production credential change,
not something to do silently). The staging path has been fully verified
end-to-end (migrations, seed, Auth, a real sign-in through the deployed
app); production's manual deploy path has not been exercised yet — no
reason to touch real prod while proving the pipeline out.
## Open decisions (not yet made)
- Which frontend host: SaaS (Vercel/Netlify/Cloudflare Pages) vs self-hosted
alongside the existing Gitea/Woodpecker infra.
- Where the marketing site (`site/`, see above) lives relative to the app —
leaning subdomain (`diagrav.com` + `app.diagrav.com`, per the links already
in `site/index.html`) over same-domain/different-path, but not finalized —
and whether it gets its own line in the eventual `.woodpecker.yml` or
piggybacks on the frontend deploy step.
- Frontend host and the marketing site's placement are both **decided and
live**, superseding the two bullets that used to be here: Cloudflare
Workers (assets-only, no server code) for both, subdomain split
(`diagrav.com` marketing site + `app.diagrav.com` app), matching the
links already in `site/index.html`.
- Whether/when to add diagram-snapshot retention (the storage-risk item
above) — not done yet, just identified.
- Actual `.woodpecker.yml` has not been written yet — this doc captures the
plan/shape only, per the user's request to keep this conversation
discussion-only and not make repo changes here.
- The production `supabase_db_password` gap noted above.