From 02fb16a0bdec13aaec9a2ec4500056e727f630ee Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 2 Jun 2026 19:25:32 +0200 Subject: [PATCH] docs(ci): document composite actions in ci-gitea.md Adds a Composite actions section covering the checkout-first ordering rule, the secrets-via-inputs + unquoted-heredoc constraint (with the five-key guard and shell: bash requirement), and a step-by-step for adding an input. Notes that the inline Reload Caddy example now lives in the reload-caddy action. Co-Authored-By: Claude Opus 4.8 --- docs/infrastructure/ci-gitea.md | 68 +++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/docs/infrastructure/ci-gitea.md b/docs/infrastructure/ci-gitea.md index 8d92890b..6d99f694 100644 --- a/docs/infrastructure/ci-gitea.md +++ b/docs/infrastructure/ci-gitea.md @@ -68,6 +68,8 @@ Job containers are unprivileged and do not share the host's PID/mount/network na Alpine is used instead of Ubuntu: ~5 MB vs ~70 MB, and the digest is pinned to a specific sha256 so any upstream change requires an explicit Renovate bump PR. `util-linux` (which ships `nsenter`) is not part of the Alpine base image but is installed at run time in ~1 s from the warm VPS cache. +This exact step now lives in the `reload-caddy` composite action (see [Composite actions](#composite-actions) below); both deploy workflows call it via `uses: ./.gitea/actions/reload-caddy`. The pinned digest moved with it, so Renovate's privileged-digest watch covers `.gitea/actions/**` as well as `.gitea/workflows/**`. + #### Why not `sudo systemctl` in the job container? Job containers run as root inside an unprivileged Docker namespace. There is no systemd PID 1 inside the container — `systemctl` would attempt to reach a socket that does not exist. `sudo` is not present in container images and would not help even if it were. @@ -170,6 +172,72 @@ See `docs/DEPLOYMENT.md §3.1` and ADR-015 for the full setup rationale. --- +## Composite actions + +The `nightly.yml` (staging) and `release.yml` (production) deploy workflows share their observability-stack deploy, Caddy reload, and smoke-test logic through three single-responsibility composite actions under `.gitea/actions/` (ADR-029). Before this, the shared logic was duplicated in both workflows and held together by `# Keep in sync with nightly.yml` comments — an unenforced honour-system invariant. + +| Action | Inputs | Purpose | +|---|---|---| +| `deploy-obs` | `grafana_admin_password`, `grafana_db_password`, `glitchtip_secret_key`, `postgres_password`, `postgres_host` | Deploy obs configs + secrets to `/opt/familienarchiv`, validate the compose config, start the stack, assert the five healthchecked services | +| `reload-caddy` | — | Reload host Caddy via the privileged-sibling + nsenter pattern | +| `smoke-test` | `host` | Verify the public surface (login reachable, HSTS pinned, Permissions-Policy present, `/actuator → 404`) | + +A workflow calls them by relative path, passing per-environment values as `with:` inputs: + +```yaml +- uses: ./.gitea/actions/deploy-obs + with: + grafana_admin_password: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} + grafana_db_password: ${{ secrets.GRAFANA_DB_PASSWORD }} + glitchtip_secret_key: ${{ secrets.GLITCHTIP_SECRET_KEY }} + postgres_password: ${{ secrets.STAGING_POSTGRES_PASSWORD }} + postgres_host: archiv-staging-db-1 +- uses: ./.gitea/actions/reload-caddy +- uses: ./.gitea/actions/smoke-test + with: + host: staging.raddatz.cloud +``` + +### Checkout-first ordering rule + +A local composite action (`uses: ./…`) only exists on disk **after** the repo is checked out. `actions/checkout@v4` MUST therefore be the **first step** of any job that calls one — if a future reorder moves checkout later, every `uses: ./.gitea/actions/…` call fails because the action file is not yet on disk. Both deploy workflows pin checkout as step 1 for exactly this reason. + +### Secrets inside composite actions + +The `secrets.*` context is **not** available inside a composite action. Secrets are passed in as `inputs`, mapped to an `env:` block, and referenced as `$VAR`: + +```yaml +inputs: + grafana_admin_password: + required: true # no default — a missing secret must fail loudly, never fall back to empty +runs: + using: composite + steps: + - shell: bash # composite steps do NOT default the shell — always declare it + env: + GRAFANA_ADMIN_PASSWORD: ${{ inputs.grafana_admin_password }} + run: | + cat > obs-secrets.env <