name: nightly # Builds and deploys the staging environment from main every night. # Runs on the self-hosted runner using Docker-out-of-Docker (the docker # socket is mounted in), so `docker compose build` produces images on # the host daemon and `docker compose up` consumes them directly — no # registry hop. # # Operational assumptions (see docs/DEPLOYMENT.md §3 for the full setup): # # 1. Single-tenant self-hosted runner. The "Write staging env file" step # writes every secret to .env.staging on the runner filesystem; the # `if: always()` cleanup step removes it. A multi-tenant runner # would need to switch to docker compose --env-file <(stdin) instead. # # 2. Host docker layer cache is authoritative. There is no # actions/cache; we rely on the host daemon to keep Maven and npm # layers warm between runs. A `docker system prune` on the host # will cause the next nightly build to be cold (5–10 min slower). # # Staging environment isolation: # - project name: archiv-staging # - host ports: backend 8081, frontend 3001 # - profile: staging (starts mailpit instead of a real SMTP relay) # # The obs-stack deploy, Caddy reload, and smoke test are shared with # release.yml via the composite actions under .gitea/actions/ (ADR-029). # actions/checkout MUST stay the first step: a local `uses: ./…` action # only exists on disk after checkout. # # Required Gitea secrets: # STAGING_POSTGRES_PASSWORD # STAGING_MINIO_PASSWORD # STAGING_MINIO_APP_PASSWORD # STAGING_OCR_TRAINING_TOKEN # STAGING_APP_ADMIN_USERNAME # STAGING_APP_ADMIN_PASSWORD # GRAFANA_ADMIN_PASSWORD # GRAFANA_DB_PASSWORD (read-only grafana_reader DB role, issue #651) # GLITCHTIP_SECRET_KEY # SENTRY_DSN (set after GlitchTip first-run; empty = Sentry disabled) on: schedule: - cron: "0 2 * * *" workflow_dispatch: env: # Ensures the backend Dockerfile's `RUN --mount=type=cache` lines are # honoured (Maven cache survives between runs). DOCKER_BUILDKIT: "1" jobs: deploy-staging: # `ubuntu-latest` matches our self-hosted runner's advertised label # (the runner has labels: ubuntu-latest / ubuntu-24.04 / ubuntu-22.04). # `self-hosted` would never match — no runner advertises it — so the # job parks in the queue forever. ADR-011's "single-tenant" promise # is at the repo level; sharing this runner between CI and deploys # for the same repo is within that boundary. runs-on: ubuntu-latest steps: # MUST be first: the composite actions below live under .gitea/actions/ # and only exist on disk once the repo is checked out (ADR-029). - uses: actions/checkout@v4 - name: Write staging env file run: | cat > .env.staging < /tmp/compose-rendered.yml grep -q '^[[:space:]]*target: /import$' /tmp/compose-rendered.yml \ || { echo "::error::backend is missing the /import bind mount (see #526)"; exit 1; } grep -A2 '^[[:space:]]*target: /import$' /tmp/compose-rendered.yml \ | grep -q 'read_only: true' \ || { echo "::error::backend /import mount is not read-only (see #526)"; exit 1; } - name: Build images # `--pull` forces re-fetching pinned base images so a CVE # re-publication of the same tag (e.g. node:20.19.0-alpine3.21, # postgres:16-alpine) is picked up instead of being served # from the host's stale Docker layer cache. run: | docker compose \ -f docker-compose.prod.yml \ -p archiv-staging \ --env-file .env.staging \ --profile staging \ build --pull - name: Deploy staging run: | docker compose \ -f docker-compose.prod.yml \ -p archiv-staging \ --env-file .env.staging \ --profile staging \ up -d --wait --remove-orphans # POSTGRES_HOST is derived from the Compose project name (archiv-staging) # and service name (db). A project rename requires updating this value. - 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 - name: Cleanup env file # LOAD-BEARING: `if: always()` is the linchpin of the ADR-011 # single-tenant runner trust model. Every secret in .env.staging # is plain text on the runner filesystem until this step runs. # If a future refactor drops `if: always()`, a failed deploy # leaves the env-file behind. Do not remove this conditional # without first re-evaluating ADR-011. if: always() run: rm -f .env.staging