Removes the 'What Parents Say' section and all supporting elements: Frontend: - Drop the OfstedParentView type, the parent_view field, the survey section and the 'X% would recommend' callouts in the primary and secondary detail views, the Parents nav item, and the parent-view CSS. Backend: - Remove the FactParentView model, its loading in data_loader, and parent_view from the school-details API response. - Bump SCHEMA_VERSION to 6 and add an idempotent drop step (DROP TABLE IF EXISTS marts.fact_parent_view) to the CLI migration; add scripts/sql/drop_fact_parent_view.sql to apply directly to the dbt-owned marts DBs on staging and prod. Pipeline: - Delete the stg_parent_view + fact_parent_view dbt models and their source/schema entries, the tap-uk-parent-view Meltano extractor, and the monthly Parent View DAG; drop it from the Dockerfile and the staging bootstrap docs. The rest of dbt (which builds every mart the app reads) is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.8 KiB
SDLC & Deployment Pipeline
SchoolCompare uses a fully automated staging → production pipeline on Gitea Actions. AI writes the code on feature branches; the pipeline verifies every change on a staging environment before promoting the exact same images to production. Human input is directional only: feature requests, PR review if desired, and intervention when a gate fails.
The flow
feature branch (AI-authored)
│ PR to main
▼
PR checks (.gitea/workflows/pr-checks.yml)
typecheck + unit tests + backend smoke + image builds (no push)
+ Claude code review posted as a PR comment (severe findings fail the check)
│ merge (branch protection requires green checks)
▼
Deploy pipeline (.gitea/workflows/deploy.yml)
1. build & push images → tags sha-<sha>, staging
2. staging Portainer webhook → wait for staging health
3. Playwright E2E journeys against staging
4. retag sha-<sha> → :prod (same bytes — build once, promote the image)
previous :prod saved as :prod-previous
5. prod Portainer webhook → wait for prod health
Key principle: build once, promote the exact image. Production pins :prod,
which only moves after the E2E gate passes on staging. Nothing tags :latest
anymore.
Branch & PR workflow
mainis protected: no direct pushes, PRs require green status checks.- All work (human or AI) happens on feature branches → PR to
main. - Merging to
mainis the release action. If staging or the E2E gate fails, production is untouched.
Environments
| Production | Staging | |
|---|---|---|
| Portainer stack file | docker-compose.portainer.yml |
docker-compose.portainer.staging.yml |
| Image tag | :prod |
:staging |
| Container prefix | sc_ / schoolcompare_ |
sc_staging_ |
| Frontend macvlan IP | 10.0.1.150 | STAGING_FRONTEND_IP (default 10.0.1.151) |
| Postgres macvlan IP | 10.0.1.189 | STAGING_DB_IP (default 10.0.1.190) |
| Airflow UI port | 8080 | 8081 |
| Volumes | stack-prefixed | stack-prefixed (fully isolated) |
Staging gets :staging images on every merge to main — even ones that later
fail the E2E gate. That's the point: staging absorbs the risk.
Gitea repository secrets
| Secret | Purpose |
|---|---|
REGISTRY_TOKEN |
push images to privaterepo.sitaru.org (already set) |
CLAUDE_CODE_OAUTH_TOKEN |
Claude Code subscription auth for the PR review — generate with claude setup-token on your machine |
PORTAINER_STAGING_WEBHOOK |
staging stack redeploy webhook URL |
PORTAINER_PROD_WEBHOOK |
production stack redeploy webhook URL |
STAGING_BASE_URL |
e.g. http://10.0.1.151:3000 — health poll + E2E target |
PROD_BASE_URL |
e.g. http://10.0.1.150:3000 — post-promotion health poll |
One-time setup checklist
- Create the staging stack in Portainer from
docker-compose.portainer.staging.yml(stack name e.g.schoolcompare-staging). Set the same environment variables as prod plusSTAGING_DB_IP/STAGING_FRONTEND_IPif the defaults clash. - Enable webhooks on both stacks (Portainer → Stack → Webhook) and store
the URLs as
PORTAINER_STAGING_WEBHOOK/PORTAINER_PROD_WEBHOOK. Remove the old hardcoded webhook usage (now gone from the workflows). - Add the remaining secrets listed above in Gitea → repo → Settings → Actions → Secrets.
- Protect
mainin Gitea → Settings → Branches: require PRs, require the pr-checks status checks (frontend, backend, builds, ai-review) to pass. - Bootstrap staging data via Airflow (no prod dump — staging populates
itself from source, exercising the pipeline image end-to-end):
- Open the staging Airflow UI (
http://<host>:8081) and trigger, in order:school_data_daily,school_data_monthly_ofsted, then the manual-scheduleschool_data_annual_eesandschool_data_annual_idaci. - First runs download from government sources (GIAS, Ofsted, EES, IDACI), run dbt, and sync Typesense — expect the initial backfill to take a while.
- The scheduled DAGs then keep staging fresh exactly like prod.
- Open the staging Airflow UI (
- Switch the prod stack to
:prodtags — the repo'sdocker-compose.portainer.ymlis already updated; redeploy the prod stack from it. Until the first pipeline run promotes an image, tag the current images manually:docker buildx imagetools create -t <image>:prod <image>:latestfor each of the three images.
Rollback
Every promotion first re-points :prod-previous at the outgoing :prod.
To roll back:
for img in backend frontend pipeline; do
docker buildx imagetools create \
-t privaterepo.sitaru.org/tudor/school_compare-$img:prod \
privaterepo.sitaru.org/tudor/school_compare-$img:prod-previous
done
curl -fsSk -X POST "$PORTAINER_PROD_WEBHOOK"
Or promote any older build directly: imagetools create -t <image>:prod <image>:sha-<shortsha>.
E2E suite
Lives in e2e/ (own package — CI installs it without the app's node_modules).
Journeys: home + name search, postcode search, school detail, two-school
comparison, rankings table. Run locally against any environment:
cd e2e && npm ci
BASE_URL=http://10.0.1.151:3000 npx playwright test
Tests assert data invariants (results exist, charts render), not exact numbers, so scheduled data refreshes don't break the gate.
AI code review
scripts/ci/ai_review.py pipes the PR diff through headless Claude Code
(claude -p, authenticated with the subscription OAuth token — no API
billing), posts the structured findings as a PR comment using the per-run
token Gitea Actions provides automatically (secrets.GITEA_TOKEN — no setup
needed), and fails the check only when a finding is rated
severe (would break prod, leak data, or corrupt data). Minor findings are
informational and never block a merge.