The smoke step previously curled the public hostname unconditionally, which routes the runner's request via DNS → router → back into the same host. Many SOHO routers do not implement hairpin NAT (or do so only after a firmware update), so the deploy may pass on day one and silently fail on day 90. --resolve "<host>:443:127.0.0.1" pins the hostname to the runner's loopback while keeping SNI on the public name (so the cert validates correctly and the Caddy vhost block matches). The smoke test now verifies that the Caddy-on-the-same-host is serving the right hostname end-to-end, with no router dependency. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
112 lines
4.1 KiB
YAML
112 lines
4.1 KiB
YAML
name: release
|
||
|
||
# Builds and deploys the production environment on `v*` tag push.
|
||
# Runs on the self-hosted runner via Docker-out-of-Docker; images are
|
||
# tagged with the actual git tag (e.g. v1.0.0) so rollback is
|
||
# `TAG=<previous> docker compose -f docker-compose.prod.yml -p archiv-production up -d --wait`
|
||
#
|
||
# Operational assumptions (see docs/DEPLOYMENT.md §3 for the full setup):
|
||
#
|
||
# 1. Single-tenant self-hosted runner. The "Write production env file"
|
||
# step writes every secret to .env.production 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 release build to be cold (5–10 min slower).
|
||
#
|
||
# Production environment:
|
||
# - project name: archiv-production
|
||
# - host ports: backend 8080, frontend 3000
|
||
# - profile: (none) — mailpit is excluded; real SMTP relay is used
|
||
#
|
||
# Required Gitea secrets:
|
||
# PROD_POSTGRES_PASSWORD
|
||
# PROD_MINIO_PASSWORD
|
||
# PROD_MINIO_APP_PASSWORD
|
||
# PROD_OCR_TRAINING_TOKEN
|
||
# PROD_APP_ADMIN_USERNAME (CRITICAL: see docs/DEPLOYMENT.md)
|
||
# PROD_APP_ADMIN_PASSWORD (CRITICAL: locked in on first deploy)
|
||
# MAIL_HOST
|
||
# MAIL_PORT
|
||
# MAIL_USERNAME
|
||
# MAIL_PASSWORD
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v*"
|
||
|
||
env:
|
||
DOCKER_BUILDKIT: "1"
|
||
|
||
jobs:
|
||
deploy-production:
|
||
runs-on: self-hosted
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: Write production env file
|
||
run: |
|
||
cat > .env.production <<EOF
|
||
TAG=${{ gitea.ref_name }}
|
||
PORT_BACKEND=8080
|
||
PORT_FRONTEND=3000
|
||
APP_DOMAIN=archiv.raddatz.cloud
|
||
POSTGRES_PASSWORD=${{ secrets.PROD_POSTGRES_PASSWORD }}
|
||
MINIO_PASSWORD=${{ secrets.PROD_MINIO_PASSWORD }}
|
||
MINIO_APP_PASSWORD=${{ secrets.PROD_MINIO_APP_PASSWORD }}
|
||
OCR_TRAINING_TOKEN=${{ secrets.PROD_OCR_TRAINING_TOKEN }}
|
||
APP_ADMIN_USERNAME=${{ secrets.PROD_APP_ADMIN_USERNAME }}
|
||
APP_ADMIN_PASSWORD=${{ secrets.PROD_APP_ADMIN_PASSWORD }}
|
||
MAIL_HOST=${{ secrets.MAIL_HOST }}
|
||
MAIL_PORT=${{ secrets.MAIL_PORT }}
|
||
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
|
||
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
|
||
MAIL_SMTP_AUTH=true
|
||
MAIL_STARTTLS_ENABLE=true
|
||
APP_MAIL_FROM=noreply@raddatz.cloud
|
||
EOF
|
||
|
||
- name: Build images
|
||
# `--pull` forces re-fetching pinned base images so a CVE
|
||
# re-publication of the same tag is picked up rather than served
|
||
# from the host's stale Docker layer cache.
|
||
run: |
|
||
docker compose \
|
||
-f docker-compose.prod.yml \
|
||
-p archiv-production \
|
||
--env-file .env.production \
|
||
build --pull
|
||
|
||
- name: Deploy production
|
||
run: |
|
||
docker compose \
|
||
-f docker-compose.prod.yml \
|
||
-p archiv-production \
|
||
--env-file .env.production \
|
||
up -d --wait --remove-orphans
|
||
|
||
- name: Smoke test deployed environment
|
||
# See nightly.yml — same three checks, against the prod vhost.
|
||
# --resolve pins archiv.raddatz.cloud to the runner's loopback so
|
||
# the smoke test does NOT depend on hairpin NAT on the host router.
|
||
run: |
|
||
set -e
|
||
HOST="archiv.raddatz.cloud"
|
||
URL="https://$HOST"
|
||
RESOLVE="--resolve $HOST:443:127.0.0.1"
|
||
echo "Smoke test: $URL (pinned to 127.0.0.1)"
|
||
curl -fsS $RESOLVE --max-time 10 "$URL/login" -o /dev/null
|
||
curl -fsS $RESOLVE --max-time 10 -I "$URL/" | grep -qi 'strict-transport-security'
|
||
status=$(curl -s $RESOLVE -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health")
|
||
[ "$status" = "404" ] || { echo "expected 404 from /actuator/health, got $status"; exit 1; }
|
||
echo "All smoke checks passed"
|
||
|
||
- name: Cleanup env file
|
||
if: always()
|
||
run: rm -f .env.production
|