name: Deploy # workflow_dispatch only, deliberately (see docs/PLAN.md's "the gate is manual dispatch"). No # automatic trigger on tag push: release.yml already runs there, and racing a build/push against # a deploy on the same event is worse than requiring one manual click after confirming the # release succeeded. on: workflow_dispatch: inputs: tag: description: Image tag to deploy (as pushed by release.yml) required: false default: latest env: # Pins the compose project name so the web_build named volume is predictably # `velodrome_web_build` regardless of which directory `docker compose` is invoked from, and so # it's the same name across the populate step and the `up -d` step below. COMPOSE_PROJECT_NAME: velodrome TAG: ${{ gitea.event.inputs.tag }} # The real .env (POSTGRES_SUPERUSER_PASSWORD, the VELODROME_* DSNs and secrets) is NOT in git — # see deploy/.env.example for the documented shape. It must already exist at this exact path on # the Unraid host before this workflow can succeed. See the PR description for the full manual # setup checklist. ENV_FILE: /mnt/user/appdata/velodrome/.env jobs: deploy: name: Deploy to production runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Same registry, same REGISTRY_TOKEN as release.yml — secrets.GITEA_TOKEN cannot push (and, # per the Gitea docs for this same limitation, should not be assumed to reliably pull # private packages either) from the container registry. - uses: docker/login-action@v3 with: registry: 192.168.0.3:3000 username: ${{ gitea.actor }} password: ${{ secrets.REGISTRY_TOKEN }} - name: Pull images for this tag working-directory: deploy run: | set -euo pipefail docker compose --env-file "$ENV_FILE" pull # DooD-safe: this is a one-shot `docker run` whose *command* (not a client-side path # argument) does the copying, into a named volume the daemon manages. No `docker cp`, no # `mv`/`rm -rf` on a host path, no bind-mount source path on this command line — all of # which would silently resolve against the ephemeral job container's own throwaway # filesystem instead of the real host under this act_runner's Docker-outside-of-Docker # setup. See deploy/docker-compose.yml's header comment for the full explanation. - name: Populate web_build volume from the web image run: | set -euo pipefail # `web` isn't a compose service, so the "Pull images" step above never touches it — # pull it explicitly here. Matters most when TAG=latest is reused across deploys: a # `docker run` on an already-cached tag would otherwise silently reuse stale local # content instead of fetching what release.yml just pushed. docker pull "192.168.0.3:3000/bbergle/velodrome-web:${TAG}" docker run --rm -v velodrome_web_build:/dest \ "192.168.0.3:3000/bbergle/velodrome-web:${TAG}" \ sh -c 'rm -rf /dest/* /dest/.[!.]* 2>/dev/null; cp -a /app/build/. /dest/' # Explicit one-off, run and must succeed *before* `up -d` — never from the api image's # entrypoint, so a failed migration fails this job loudly instead of crash-looping in a # container nobody's watching. VELODROME_DATABASE_URL_MIGRATE (the superuser DSN) is passed # only as an inline override here, never in the compose file's permanent `api` environment # block — see docker-compose.yml's comment and apps/api/README.md's "Why two database # connections" for why the long-running app process must never hold it. - name: Run database migrations working-directory: deploy run: | set -euo pipefail read_var() { grep -E "^$1=" "$ENV_FILE" | head -1 | cut -d= -f2- } SUPERUSER_PW="$(read_var POSTGRES_SUPERUSER_PASSWORD)" APP_PW="$(read_var VELODROME_DB_APP_PASSWORD)" AUTH_PW="$(read_var VELODROME_DB_AUTH_PASSWORD)" for name in SUPERUSER_PW:POSTGRES_SUPERUSER_PASSWORD APP_PW:VELODROME_DB_APP_PASSWORD AUTH_PW:VELODROME_DB_AUTH_PASSWORD; do var="${name%%:*}"; label="${name##*:}" if [ -z "${!var}" ]; then echo "::error::$label not found in $ENV_FILE" exit 1 fi done # VELODROME_DB_APP_PASSWORD/VELODROME_DB_AUTH_PASSWORD are needed here (Alembic's # baseline migration uses them to create the two runtime roles, per # apps/api/README.md's env var table) but are deliberately absent from the `api` # service's permanent `environment:` block in docker-compose.yml. `--env-file` only # feeds ${...} substitution *in the compose YAML* — it does NOT inject variables into # the container unless the service's `environment:` block names them. So they must be # passed here explicitly, the same way VELODROME_DATABASE_URL_MIGRATE is. (Verified # locally: omitting these two makes the migration fail loudly with "must be set before # running this migration" from alembic/versions/0001_baseline.py — good, it fails # closed rather than silently skipping role creation.) docker compose --env-file "$ENV_FILE" run --rm \ -e VELODROME_DATABASE_URL_MIGRATE="postgresql+asyncpg://postgres:${SUPERUSER_PW}@db:5432/velodrome" \ -e VELODROME_DB_APP_PASSWORD="${APP_PW}" \ -e VELODROME_DB_AUTH_PASSWORD="${AUTH_PW}" \ api alembic upgrade head - name: Bring up the stack working-directory: deploy run: | set -euo pipefail docker compose --env-file "$ENV_FILE" up -d - name: Wait for the API to become healthy run: | set -euo pipefail for i in $(seq 1 10); do if curl -fsS http://localhost:8090/api/v1/healthz; then echo "Healthy." exit 0 fi echo "Not ready yet (attempt $i/10), retrying..." sleep 3 done echo "::error::API did not become healthy after deploy." exit 1