Add mobile view-only support + Woodpecker CI/CD pipeline #1
@@ -0,0 +1,84 @@
|
|||||||
|
# See deployment-plan.md's "CI/CD plan" section for the full rationale.
|
||||||
|
#
|
||||||
|
# Shape: every push (any branch) lints, typechecks, and auto-deploys to a
|
||||||
|
# single shared staging environment (its own Cloudflare Workers + its own
|
||||||
|
# Supabase project — never shares data with production). Production is
|
||||||
|
# never touched automatically — promoting to it is a manual "Deploy" button
|
||||||
|
# click on a main-branch pipeline run in the Woodpecker UI (Woodpecker's
|
||||||
|
# deploy event), which is why deploy-production is gated on `event: deploy`
|
||||||
|
# rather than `event: push`.
|
||||||
|
#
|
||||||
|
# Debian-based node image (not Alpine) for every step: the Supabase CLI's
|
||||||
|
# downloaded binary has had musl/Alpine compatibility issues in the past.
|
||||||
|
# Woodpecker shares one workspace across all steps in a pipeline run, so
|
||||||
|
# `npm ci` in the install step is enough for every later step to reuse.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: node:22-bookworm
|
||||||
|
commands:
|
||||||
|
- npm ci
|
||||||
|
|
||||||
|
- name: lint
|
||||||
|
image: node:22-bookworm
|
||||||
|
commands:
|
||||||
|
- npm run lint
|
||||||
|
|
||||||
|
- name: typecheck
|
||||||
|
image: node:22-bookworm
|
||||||
|
commands:
|
||||||
|
- npx tsc -b
|
||||||
|
|
||||||
|
- name: deploy-staging
|
||||||
|
image: node:22-bookworm
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
environment:
|
||||||
|
VITE_SUPABASE_URL:
|
||||||
|
from_secret: staging_supabase_url
|
||||||
|
VITE_SUPABASE_ANON_KEY:
|
||||||
|
from_secret: staging_supabase_anon_key
|
||||||
|
CLOUDFLARE_API_TOKEN:
|
||||||
|
from_secret: cloudflare_api_token
|
||||||
|
CLOUDFLARE_ACCOUNT_ID:
|
||||||
|
from_secret: cloudflare_account_id
|
||||||
|
SUPABASE_ACCESS_TOKEN:
|
||||||
|
from_secret: supabase_access_token
|
||||||
|
SUPABASE_PROJECT_REF:
|
||||||
|
from_secret: staging_supabase_project_ref
|
||||||
|
SUPABASE_DB_PASSWORD:
|
||||||
|
from_secret: staging_supabase_db_password
|
||||||
|
commands:
|
||||||
|
- npm run build
|
||||||
|
- npx wrangler deploy --config wrangler.app.jsonc --env staging
|
||||||
|
- npx wrangler deploy --config wrangler.site.jsonc --env staging
|
||||||
|
- npx supabase db push --project-ref $SUPABASE_PROJECT_REF --password "$SUPABASE_DB_PASSWORD"
|
||||||
|
- npx supabase functions deploy admin-user-action --project-ref $SUPABASE_PROJECT_REF
|
||||||
|
|
||||||
|
- name: deploy-production
|
||||||
|
image: node:22-bookworm
|
||||||
|
when:
|
||||||
|
- event: deploy
|
||||||
|
branch: main
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
environment:
|
||||||
|
VITE_SUPABASE_URL:
|
||||||
|
from_secret: prod_supabase_url
|
||||||
|
VITE_SUPABASE_ANON_KEY:
|
||||||
|
from_secret: prod_supabase_anon_key
|
||||||
|
CLOUDFLARE_API_TOKEN:
|
||||||
|
from_secret: cloudflare_api_token
|
||||||
|
CLOUDFLARE_ACCOUNT_ID:
|
||||||
|
from_secret: cloudflare_account_id
|
||||||
|
SUPABASE_ACCESS_TOKEN:
|
||||||
|
from_secret: supabase_access_token
|
||||||
|
SUPABASE_PROJECT_REF:
|
||||||
|
from_secret: prod_supabase_project_ref
|
||||||
|
SUPABASE_DB_PASSWORD:
|
||||||
|
from_secret: prod_supabase_db_password
|
||||||
|
commands:
|
||||||
|
- npm run build
|
||||||
|
- npx wrangler deploy --config wrangler.app.jsonc
|
||||||
|
- npx wrangler deploy --config wrangler.site.jsonc
|
||||||
|
- npx supabase db push --project-ref $SUPABASE_PROJECT_REF --password "$SUPABASE_DB_PASSWORD"
|
||||||
|
- npx supabase functions deploy admin-user-action --project-ref $SUPABASE_PROJECT_REF
|
||||||
+71
-69
@@ -1,11 +1,11 @@
|
|||||||
# Deployment & CI/CD planning notes
|
# Deployment & CI/CD planning notes
|
||||||
|
|
||||||
Discussion notes only — nothing here has been implemented yet. Written so a
|
Started as discussion-only notes; hosting and CI/CD are now both live (see
|
||||||
different Claude Code conversation on this repo can pick up where this one
|
their sections below for what's actually implemented vs. still open).
|
||||||
left off without re-deriving it. Nothing below is committed to as final; it's
|
Written so a different Claude Code conversation on this repo can pick up
|
||||||
a record of what was discussed and concluded, plus open decisions.
|
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
|
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)
|
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
|
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).
|
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
|
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
|
Woodpecker CI already in use elsewhere. `.woodpecker.yml` now exists at the
|
||||||
this repo.
|
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
|
**Actual flow**: any push (any branch) runs lint + typecheck, then
|
||||||
and stops → a human clicks a "Deploy" button to push to staging, and
|
auto-deploys to a shared staging environment — no button needed. Production
|
||||||
separately clicks it again to promote to production. Not fully automatic on
|
is never touched automatically; promoting to it is a manual **Deploy**
|
||||||
merge — deploys to both environments are manual/pushbutton.
|
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
|
**Two fully separate environments exist, each with its own everything** (no
|
||||||
built-in **deploy event** for exactly this. Enable **"Allow deployments"** in
|
shared data or infra between them, or with production):
|
||||||
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.:
|
|
||||||
|
|
||||||
```yaml
|
| | Staging | Production |
|
||||||
steps:
|
|---|---|---|
|
||||||
- name: deploy-staging
|
| Supabase project | `onovxozixdxpdngxafxt` ("Diagrav Staging") | `wmombshptajyrawdbroj` ("Diagrav") |
|
||||||
when:
|
| App Worker | `diagrav-app-staging` → `staging-app.diagrav.com` | `diagrav-app` → `app.diagrav.com` |
|
||||||
- event: deploy
|
| Site Worker | `diagrav-site-staging` → `staging.diagrav.com` | `diagrav-site` → `diagrav.com` |
|
||||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "staging"'
|
|
||||||
- name: deploy-prod
|
|
||||||
when:
|
|
||||||
- event: deploy
|
|
||||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
|
||||||
```
|
|
||||||
|
|
||||||
**What needs to exist**:
|
This uses both of Supabase Free's "2 active projects" slots — no headroom
|
||||||
1. **Two Supabase projects** — staging and prod. This exactly fills the free
|
for a third project if one's ever wanted later (see cost findings above).
|
||||||
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.
|
|
||||||
|
|
||||||
**Difficulty estimate discussed**: roughly a focused afternoon — nothing here
|
**How each piece works**:
|
||||||
fights the tooling (no containers to orchestrate, no server process to run).
|
- **Wrangler**: `wrangler.app.jsonc` / `wrangler.site.jsonc` each gained an
|
||||||
Breakdown: `.woodpecker.yml` (~1–2 hrs), second Supabase project + migration
|
`env.staging` block (own `name` + own `routes`) — `assets.directory` isn't
|
||||||
check (~30 min), frontend hosting target wiring (~30–60 min), secrets +
|
per-environment in Wrangler, which is fine, since it's rebuilt with
|
||||||
"Allow deployments" toggle (~15 min), end-to-end test/debug (~1–2 hrs).
|
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)
|
## Open decisions (not yet made)
|
||||||
|
|
||||||
- Which frontend host: SaaS (Vercel/Netlify/Cloudflare Pages) vs self-hosted
|
- Frontend host and the marketing site's placement are both **decided and
|
||||||
alongside the existing Gitea/Woodpecker infra.
|
live**, superseding the two bullets that used to be here: Cloudflare
|
||||||
- Where the marketing site (`site/`, see above) lives relative to the app —
|
Workers (assets-only, no server code) for both, subdomain split
|
||||||
leaning subdomain (`diagrav.com` + `app.diagrav.com`, per the links already
|
(`diagrav.com` marketing site + `app.diagrav.com` app), matching the
|
||||||
in `site/index.html`) over same-domain/different-path, but not finalized —
|
links already in `site/index.html`.
|
||||||
and whether it gets its own line in the eventual `.woodpecker.yml` or
|
|
||||||
piggybacks on the frontend deploy step.
|
|
||||||
- Whether/when to add diagram-snapshot retention (the storage-risk item
|
- Whether/when to add diagram-snapshot retention (the storage-risk item
|
||||||
above) — not done yet, just identified.
|
above) — not done yet, just identified.
|
||||||
- Actual `.woodpecker.yml` has not been written yet — this doc captures the
|
- The production `supabase_db_password` gap noted above.
|
||||||
plan/shape only, per the user's request to keep this conversation
|
|
||||||
discussion-only and not make repo changes here.
|
|
||||||
|
|||||||
+20
-2
@@ -1,7 +1,9 @@
|
|||||||
// Deploys the built Vite app (dist/) as an assets-only Cloudflare Worker.
|
// Deploys the built Vite app (dist/) as an assets-only Cloudflare Worker.
|
||||||
// No server-side code — this is purely static hosting for app.diagrav.com.
|
// No server-side code — this is purely static hosting for app.diagrav.com.
|
||||||
// Build first (with production VITE_* env vars baked in), then:
|
// Build first (with the right VITE_* env vars baked in for the target
|
||||||
// npx wrangler deploy --config wrangler.app.jsonc
|
// environment — see the CI/CD section of deployment-plan.md), then:
|
||||||
|
// npx wrangler deploy --config wrangler.app.jsonc (production)
|
||||||
|
// npx wrangler deploy --config wrangler.app.jsonc --env staging
|
||||||
{
|
{
|
||||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||||
"name": "diagrav-app",
|
"name": "diagrav-app",
|
||||||
@@ -15,5 +17,21 @@
|
|||||||
"pattern": "app.diagrav.com",
|
"pattern": "app.diagrav.com",
|
||||||
"custom_domain": true
|
"custom_domain": true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
// Own Worker, own route, own build (with staging's VITE_SUPABASE_* baked
|
||||||
|
// in) — never shares runtime state with production. `assets.directory`
|
||||||
|
// isn't overridable per-environment in Wrangler, which is fine here: it's
|
||||||
|
// the same dist/ folder either way, just rebuilt with different env vars
|
||||||
|
// immediately before each deploy.
|
||||||
|
"env": {
|
||||||
|
"staging": {
|
||||||
|
"name": "diagrav-app-staging",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": "staging-app.diagrav.com",
|
||||||
|
"custom_domain": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -1,7 +1,8 @@
|
|||||||
// Deploys the hand-written marketing site (site/) as an assets-only
|
// Deploys the hand-written marketing site (site/) as an assets-only
|
||||||
// Cloudflare Worker for the bare diagrav.com domain. No build step —
|
// Cloudflare Worker for the bare diagrav.com domain. No build step —
|
||||||
// serves the folder as-is:
|
// serves the folder as-is:
|
||||||
// npx wrangler deploy --config wrangler.site.jsonc
|
// npx wrangler deploy --config wrangler.site.jsonc (production)
|
||||||
|
// npx wrangler deploy --config wrangler.site.jsonc --env staging
|
||||||
{
|
{
|
||||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||||
"name": "diagrav-site",
|
"name": "diagrav-site",
|
||||||
@@ -15,5 +16,18 @@
|
|||||||
"pattern": "diagrav.com",
|
"pattern": "diagrav.com",
|
||||||
"custom_domain": true
|
"custom_domain": true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
// Own Worker, own route — the marketing site has no build step/env vars,
|
||||||
|
// so this is just a separate deploy target, not a separate build.
|
||||||
|
"env": {
|
||||||
|
"staging": {
|
||||||
|
"name": "diagrav-site-staging",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": "staging.diagrav.com",
|
||||||
|
"custom_domain": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user