docs(ci): document composite actions in ci-gitea.md
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m39s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m39s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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?
|
#### 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.
|
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 <<EOF # unquoted EOF — $VAR expands at the shell layer
|
||||||
|
GRAFANA_ADMIN_PASSWORD=$GRAFANA_ADMIN_PASSWORD
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Two load-bearing details:
|
||||||
|
|
||||||
|
- **Unquoted heredoc delimiter (`<<EOF`, not `<<'EOF'`).** With a quoted delimiter the shell writes the literal string `$GRAFANA_ADMIN_PASSWORD`, and `docker compose config --quiet` still passes (the variable is *present, just wrong*). The `deploy-obs` action guards against this with a five-key **non-empty** check (`grep -Eq "^KEY=.+"`) immediately after writing `obs-secrets.env`. `chmod 600` is the action's final operation so the file is never world-readable.
|
||||||
|
- **Every `run:` step declares `shell: bash`.** Composite actions do not inherit the workflow's default shell; a step without it fails to run.
|
||||||
|
|
||||||
|
### Adding an input to an action
|
||||||
|
|
||||||
|
To thread a new per-environment value (e.g. a new secret) through `deploy-obs`:
|
||||||
|
|
||||||
|
1. Add it under `inputs:` in `.gitea/actions/deploy-obs/action.yml` with `required: true` and **no `default:`**.
|
||||||
|
2. Map it in the relevant step's `env:` block: `NEW_KEY: ${{ inputs.new_key }}`.
|
||||||
|
3. Reference it as `$NEW_KEY` in the `run:` script — add a `NEW_KEY=$NEW_KEY` line to the heredoc **and** a matching entry to the five-key guard loop.
|
||||||
|
4. Pass it from **both** workflows' `with:` blocks. That is the whole point of the action: the contract lives in one place, so neither environment can silently drift.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Gitea vs GitHub Actions Differences
|
## Gitea vs GitHub Actions Differences
|
||||||
|
|
||||||
### Context Variable Names
|
### Context Variable Names
|
||||||
|
|||||||
Reference in New Issue
Block a user