Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
name: Handbook Build Check

# PR-only build verification for Dockerfile.handbook + the Goldens-assembled
# screenshots. Does NOT push to Docker Hub and does NOT deploy — that
# remains the job of handbook-deploy.yaml (staging push → DEV, develop push → PRD).
# PR-only build verification for the handbook CONTENT. Gates four things
# before a staging merge, all pure content checks with no live deploy:
# - screenshot-assembly: scripts/assemble-handbook-screenshots.sh runs and
# produces the expected PNG count
# - store-listing sync: scripts/assemble-handbook-store-listing.py output
# matches the committed handbook (+ its HTML
# sanitizer strips injection payloads)
# - legal-sync: scripts/assemble-handbook-legal.py output
# matches the committed handbook
# - image build + container smoke:
# `docker build` (no push) + a container smoke
# test: /healthz answers and /de/ sits behind the
# auth wall. The HTTP probes do not establish
# per-file existence (the auth gate answers 401
# before nginx ever looks a file up): screenshots
# are proven by their explicit count guard above,
# legal downloads by build-legal-downloads.sh's
# fail-fast per-base existence check during the
# image build
#
# Does NOT check out DFXswiss/api or RealUnitCH/web (those only happen in
# the real build inside handbook.yaml) and does NOT push to Docker Hub or
# deploy — that remains the job of handbook-deploy.yaml (staging push →
# PRD). The paths filter below mirrors the CONTENT inputs of the handbook
# image plus this workflow itself; the deploy WIRING (SSH, secrets,
# server rollout) are exercised exclusively by the deploy run, not by this
# check.
#
# Path filter covers everything that goes into the handbook image:
# - docs/handbook/** handbook HTML, README, en/de subtrees
Expand Down
89 changes: 26 additions & 63 deletions .github/workflows/handbook-deploy.yaml
Original file line number Diff line number Diff line change
@@ -1,88 +1,51 @@
name: Handbook CI/CD

# Per-branch handbook deploy pipeline, one environment per branch:
# push to `staging` → build from staging → deploy DEV (dev-handbook.realunit.app)
# push to `develop` → build from develop → deploy PRD (handbook.realunit.app)
# Handbook deploy pipeline: a push to `staging` deploys directly to PRD
# (handbook.realunit.app). There is no separate DEV environment anymore —
# the former dev-handbook.realunit.app instance is deliberately
# decommissioned and no longer served.
#
# DEV and PRD are independent runs keyed by the pushed branch; they no
# longer share a single run that fans out to both. The "DEV is green before
# PRD ships" guarantee is now provided by the branch-promotion flow itself:
# handbook content reaches `develop` only after it has been on `staging`
# (auto-staging-pr.yaml opens the staging → develop PR), i.e. after it was
# built and smoke-tested on DEV. Promoting to develop is what triggers PRD.
#
# This is a deliberate dev/prd-coupling exception scoped to the handbook
# alone — the wallet app itself still follows the strict develop→main
# separation.
# This also means the previous "DEV is green before PRD ships" guarantee is
# gone by design. The safety net for handbook CONTENT is now the PR check
# (handbook-build-check.yaml), which runs on every handbook-relevant PR —
# it gates screenshot-assembly, store-listing sync, legal-sync, and an
# image build + container smoke test, not the deploy WIRING itself (SSH,
# secrets, server rollout), which only the actual deploy run here
# exercises. There is no intermediate staging-only deploy left to catch
# problems before PRD sees them.
#
# All build/deploy/smoke logic lives in the reusable workflow
# .github/workflows/handbook.yaml. This file only wires the per-env
# parameters (source ref, image tag, smoke URL, deploy secrets) and routes
# the pushed branch to its environment via the per-job `if:` guards below.
# .github/workflows/handbook.yaml. This file only wires the PRD parameters
# (source ref, image tag, smoke URL, deploy secrets) and gates the single
# deploy job on the pushed branch via its `if:` guard below.

on:
push:
branches: [staging, develop]
paths:
- "docs/handbook/**"
- "test/goldens/**"
- "scripts/assemble-handbook-screenshots.sh"
- "scripts/assemble-handbook-store-listing.py"
- "scripts/templates/store-listing.html.tmpl"
# Store-listing section is baked into the image from the Fastlane
# metadata; a metadata- or screenshot-only change must redeploy too.
- "ios/fastlane/metadata/**"
- "ios/fastlane/screenshots/**"
- "android/fastlane/metadata/**"
- "Dockerfile.handbook"
- "handbook.nginx.conf"
- "handbook.htpasswd"
- ".github/workflows/handbook-deploy.yaml"
- ".github/workflows/handbook.yaml"
branches: [staging]
workflow_dispatch:

permissions:
contents: read

# One concurrency lane PER BRANCH so a staging (DEV) deploy and a develop
# (PRD) deploy never serialize against — or cancel — each other. Within a
# single lane a manual workflow_dispatch still can't race a push: both would
# build the same env's tag in parallel, the later build would win on Docker
# Hub, and the target server could pull either depending on timing.
# cancel-in-progress is false so a mid-rollout deploy is never killed (we'd
# otherwise risk recreating the container halfway through).
# One concurrency lane (keyed by branch) so a push and a manual
# workflow_dispatch on the same branch never race — or cancel — each other:
# both would otherwise build the same :latest tag in parallel, the later
# build would win on Docker Hub, and the target server could pull either
# depending on timing. cancel-in-progress is false so a mid-rollout deploy is
# never killed (we'd otherwise risk recreating the container halfway
# through).
concurrency:
group: handbook-deploy-${{ github.ref_name }}
cancel-in-progress: false

jobs:
# Push to staging → DEV. Distinct image tag (:beta) from PRD (:latest) so a
# staging build can never clobber the develop build on Docker Hub (both
# used to push :beta, which raced whenever the two branches were pushed
# close together).
deploy-dev:
if: github.ref_name == 'staging'
uses: ./.github/workflows/handbook.yaml
with:
environment: DEV
ref: staging
docker_tag: dfxswiss/realunit-app-handbook:beta
smoke_url: https://dev-handbook.realunit.app/healthz
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_DEV_SSH_KEY }}
DEPLOY_SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_DEV_SSH_KNOWN_HOSTS }}
DEPLOY_USER: ${{ secrets.DEPLOY_DEV_USER }}
DEPLOY_HOST: ${{ secrets.DEPLOY_DEV_HOST }}

# Push to develop → PRD.
# Push to staging → PRD.
deploy-prd:
if: github.ref_name == 'develop'
if: github.ref_name == 'staging'
uses: ./.github/workflows/handbook.yaml
with:
environment: PRD
ref: develop
ref: staging
docker_tag: dfxswiss/realunit-app-handbook:latest
smoke_url: https://handbook.realunit.app/healthz
secrets:
Expand Down
50 changes: 26 additions & 24 deletions .github/workflows/handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,31 @@ name: Handbook reusable CI/CD
# Reusable workflow that builds the handbook nginx image, publishes it to
# Docker Hub, and tells the target server to pull + recreate the container.
#
# Caller (handbook-deploy.yaml) provides the per-environment parameters
# (source ref, tag, smoke URL, deploy secrets) so the deploy logic itself
# lives in exactly one place. Each pushed branch is an independent caller
# run — staging → DEV, develop → PRD — so there is no DEV→PRD `needs:`
# ordering here; "DEV green before PRD" comes from the staging→develop
# promotion flow (see handbook-deploy.yaml header).
# Caller (handbook-deploy.yaml) provides the PRD parameters (source ref,
# tag, smoke URL, deploy secrets) so the deploy logic itself lives in
# exactly one place. A push to staging deploys directly to PRD
# (handbook.realunit.app); the former DEV instance is retired. There is no
# DEV→PRD `needs:` ordering here — the PR check (handbook-build-check.yaml)
# gates handbook CONTENT (screenshot-assembly, store-listing sync,
# legal-sync, image build + container smoke) before a change can reach
# staging; it does not exercise this reusable workflow's deploy WIRING
# itself (SSH, secrets, server rollout) — see handbook-deploy.yaml header.
#
# Mirrors the shared SSH-deploy workflow templates from the internal ops repo.

on:
workflow_call:
inputs:
environment:
description: "Target environment label (dev | prd) — used in job name + smoke log lines"
description: "Target environment label (prd) — used in job name + smoke log lines"
required: true
type: string
ref:
description: "Git ref of THIS repo to build the handbook from — the pushed branch (staging for DEV, develop for PRD). Determines the deployed handbook 'Stand'."
description: "Git ref of THIS repo to build the handbook from — staging (the sole deploy source; DEV is retired). Determines the deployed handbook 'Stand'."
required: true
type: string
docker_tag:
description: "Full Docker image tag, e.g. dfxswiss/realunit-app-handbook:beta (DEV) / :latest (PRD)"
description: "Full Docker image tag, e.g. dfxswiss/realunit-app-handbook:latest (PRD)"
required: true
type: string
smoke_url:
Expand Down Expand Up @@ -63,25 +66,24 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
with:
# Build the handbook from the environment's source branch: staging
# for DEV, develop for PRD (passed by handbook-deploy.yaml as the
# pushed branch). This ref governs the deployed handbook content;
# the DFXswiss/api checkout below is a SEPARATE, intentionally
# develop-pinned source for the mail previews and does not vary
# with this input.
# Build the handbook from staging — the only branch that deploys
# now (passed by handbook-deploy.yaml as the pushed branch; the
# former develop-triggered DEV/PRD split is retired). This ref
# governs the deployed handbook content; the DFXswiss/api checkout
# below is a SEPARATE, intentionally develop-pinned source for the
# mail previews and does not vary with this input.
ref: ${{ inputs.ref }}

# RealUnit mail previews are the single source of truth in DFXswiss/api
# (scripts/generate-realunit-previews.js). We pull them in at build time
# instead of checking them into this repo, so a change to a template or
# i18n string in the api repo flows into the handbook image on the next
# build without a manual sync PR. A handbook-relevant push to staging
# (→DEV) or develop (→PRD) — or a manual workflow_dispatch on
# handbook-deploy.yaml in this repo — rebuilds the image and re-pulls
# api@develop here — so mail-template changes in the api repo only flow
# in after such a push/dispatch in this repo. There is NO auto-dispatch
# from the api repo. The api ref stays `develop` for both environments
# (it does not track the handbook's staging/develop split).
# build without a manual sync PR. A push to staging (→ PRD) — or a
# manual workflow_dispatch on handbook-deploy.yaml in this repo —
# rebuilds the image and re-pulls api@develop here — so mail-template
# changes in the api repo only flow in after such a push/dispatch in
# this repo. There is NO auto-dispatch from the api repo. The api ref
# stays `develop` regardless of the handbook's own deploy branch.
- name: Check out DFXswiss/api at develop into _api-checkout/
uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -299,8 +301,8 @@ jobs:
# balance artifacts above, we pull them in at build time instead of
# committing them here, so a re-accepted web baseline flows into the
# handbook image on the next build without a manual sync PR. The web ref
# stays `develop` for both environments (it does not track the handbook's
# staging/develop split) — mirroring the api@develop pin above: develop is
# stays `develop` regardless of the handbook's single deploy branch
# (`staging`) — mirroring the api@develop pin above: develop is
# RealUnitCH/web's integration branch, i.e. the latest accepted baselines.
# RealUnitCH/web is a PUBLIC repo, so the runner's default GITHUB_TOKEN is
# enough to clone it (no cross-repo PAT). If it is ever flipped to private,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ Tier 1 specs live under `test/integration/**` and run inside the same `flutter t
| `auto-tag.yaml` | Push `develop` | Creates the next `vX.Y.Z` patch tag (PATCH = previous + 1, MINOR/MAJOR from pubspec floor) |
| `release.yaml` | Tag `v*` · manual | Single store-release pipeline. Guard job routes by PATCH: `vX.Y.0` → production candidate (GitHub release, prerelease: false); `vX.Y.Z` (Z >= 1) → internal release (GitHub pre-release). Both lanes deploy Android + iOS to Play Internal + TestFlight; production promotion stays manual in the store backends. |
| `store-metadata.yaml` | Push `main` under `*/fastlane/metadata/**` or `ios/fastlane/screenshots/**` · manual `workflow_dispatch` | Sync App Store + Play Store listing text + screenshots without rebuilding the app. A `preflight` gate rejects `FIXME-` placeholders and over-length text fields before either store upload runs. |
| `handbook-deploy.yaml` | Push `staging` (→ DEV) or `develop` (→ PRD) under `docs/handbook/**`, `Dockerfile.handbook`, `handbook.nginx.conf`, `handbook.htpasswd`, or the workflow files · manual | Builds the handbook image from the pushed branch and deploys it to the matching environment via the reusable `handbook.yaml`: `staging` → DEV (`:beta`, dev-handbook.realunit.app), `develop` → PRD (`:latest`, handbook.realunit.app). Independent per-branch runs with distinct image tags; "DEV green before PRD" is enforced by the staging→develop promotion flow, not an in-run `needs:` |
| `handbook-deploy.yaml` | Push `staging` · manual | Builds the handbook image from `staging` and deploys it to PRD via the reusable `handbook.yaml` (`:latest`, handbook.realunit.app). Content is pre-gated by `handbook-build-check.yaml` (screenshot-assembly, store-listing sync, legal-sync, and image build + container smoke); this workflow alone exercises the deploy WIRING (SSH, secrets, rollout) |
| `handbook.yaml` | Called by `handbook-deploy.yaml` (`workflow_call`) | Reusable build → Docker Hub push → server pull/recreate → smoke check, parameterised per environment |

## Release versioning
Expand Down
45 changes: 23 additions & 22 deletions docs/handbook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ open docs/handbook/de/index.html
```

Den gleichen Multi-Stage-Build macht `Dockerfile.handbook` automatisch beim
deployten Image (`handbook.realunit.app` / `dev-handbook.realunit.app`).
deployten Image (`handbook.realunit.app`).

## Screenshots regenerieren

Expand All @@ -37,7 +37,7 @@ Workflow:
`github-actions[bot]` zurück auf den Branch (siehe
[`../visual-regression-tests.md`](../visual-regression-tests.md))
4. Pullen → der nächste Handbook-Deploy zeigt die neue Baseline automatisch
(Push auf `staging` → DEV bzw. `develop` → PRD).
(Push auf `staging` → PRD).

## Selektive Läufe (Teilmenge)

Expand Down Expand Up @@ -117,32 +117,33 @@ divergieren Image-Stand und Source-of-Truth.

### Trigger

Der Handbook-Deploy läuft pro Branch in genau ein Environment: ein `push`
auf `staging` deployt nach **DEV** (`dev-handbook.realunit.app`), ein `push`
auf `develop` nach **PRD** (`handbook.realunit.app`) — jeweils mit Änderungen
unter handbook-relevanten Pfaden (siehe `handbook-deploy.yaml`) oder per
manuellem `workflow_dispatch` auf `handbook-deploy.yaml` in **diesem** Repo
(das Ziel-Environment richtet sich nach dem dispatchten Branch). DEV und PRD
nutzen getrennte Image-Tags (`:beta` bzw. `:latest`), damit sich staging- und
develop-Builds nicht gegenseitig überschreiben. Eine reine Mail-Template-,
i18n- oder Generator-Änderung im api-Repo löst **keinen** automatischen
Rebuild aus — sie fliesst erst mit dem nächsten Handbook-Deploy hier rein.
Der Handbook-Deploy läuft in genau ein Environment (PRD): jeder `push` auf
`staging` deployt `handbook.realunit.app` (Image `:latest`) — es gibt keinen
Pfad-Filter mehr, jeder Merge nach `staging` löst den Deploy aus, unabhängig
davon, welche Dateien er ändert. Alternativ per manuellem `workflow_dispatch`
auf `handbook-deploy.yaml` in **diesem** Repo. Die frühere separate
DEV-Instanz (`dev-handbook.realunit.app`, Image `:beta`) ist stillgelegt und
wird nicht mehr bespielt; das Sicherheitsnetz für Handbook-INHALTE ist jetzt
der PR-Check `handbook-build-check.yaml`, der bei jedem handbook-relevanten
PR läuft (Screenshot-Assemblierung, Store-Listing-Sync, Legal-Sync sowie
Image-Build + Container-Smoke) — die Deploy-VERDRAHTUNG selbst (SSH/Secrets/
Rollout in `handbook-deploy.yaml`) prüft erst der Deploy-Lauf selbst. Eine
reine Mail-Template-, i18n- oder Generator-Änderung im api-Repo löst
**keinen** automatischen Rebuild aus — sie fliesst erst mit dem nächsten
Handbook-Deploy hier rein.

Wer eine reine Mail-Änderung sofort live haben will, hat zwei Optionen
im realunit-app-Repo:

```bash
# Variante A: No-op-Touch unter einem handbook-relevanten Pfad,
# damit der path-Filter von handbook-deploy.yaml zieht. `--allow-empty`
# alleine reicht NICHT — der Push muss eine Datei unter docs/handbook/
# (oder Dockerfile.handbook / handbook.nginx.conf / handbook.htpasswd /
# einen der beiden handbook-Workflows) tatsächlich anfassen.
# Variante A: No-op-Touch, dann push auf staging — es gibt keinen
# Pfad-Filter mehr, jeder Push auf staging löst den Deploy aus.
touch docs/handbook/.sync && git add docs/handbook/.sync \
&& git commit -m "chore(handbook): pull latest mail templates from api" \
&& git push origin develop
&& git push origin staging

# Variante B: manuell dispatchen (kein Commit nötig)
gh workflow run handbook-deploy.yaml --ref develop
gh workflow run handbook-deploy.yaml --ref staging
```

### Lokal regenerieren
Expand Down Expand Up @@ -236,15 +237,15 @@ Handbook-Build eingezogen: der Step "Stage web e2e baselines from web repo" in
`tests/__screenshots__/{desktop-chromium,tablet-chromium,mobile-safari}/` nach
`docs/handbook/web/` (gitignored). Single Source of Truth ist das web-Repo; die
Ref bleibt — wie der api-Checkout — auf `develop` gepinnt (Integrations-Branch =
aktuell akzeptierte Baselines), unabhängig vom DEV/PRD-Ziel.
aktuell akzeptierte Baselines), unabhängig vom Deploy-Branch `staging`.

Kommt im web-Repo eine View hinzu oder weg, failt der Build am
`EXPECTED_WEB_BASELINE_COUNT`-Guard des Steps — dann die Zahl in `handbook.yaml`
**und** die Bild-Karten in `#spec-web` (`docs/handbook/de/index.html`) im selben
Zug anpassen. Eine reine Website-Änderung im web-Repo löst hier **keinen**
automatischen Rebuild aus — sie fliesst erst mit dem nächsten Handbook-Deploy
rein (Push auf `staging` → DEV bzw. `develop` → PRD, oder manueller
`workflow_dispatch` auf `handbook-deploy.yaml`).
rein (Push auf `staging` → PRD, oder manueller `workflow_dispatch --ref staging`
auf `handbook-deploy.yaml`).

### Lokal ansehen

Expand Down
Loading
Loading