141 lines
6.4 KiB
YAML
141 lines
6.4 KiB
YAML
name: Deploy to TrueNAS
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# HOMELAB APP TEMPLATE — push-to-deploy workflow.
|
|
# Proven on InspectFlow + CannaManage. See homelab-release-runbook.md.
|
|
#
|
|
# Before first push, replace these placeholders everywhere in the repo:
|
|
# __PROJECT__ compose project name + container prefix (e.g. "myapp")
|
|
# __FRONTEND_PORT__ LAN host port the frontend publishes (e.g. 3001) — must be
|
|
# unique across all stacks on TrueNAS (see runbook §2 registry)
|
|
# __BACKEND_PORT__ LAN host port for backend debug (e.g. 8082) — unique too,
|
|
# or remove the backend ports block entirely if not needed
|
|
#
|
|
# Auto-deploys on push to main via the INSTANCE-LEVEL self-hosted Gitea Actions
|
|
# runner on TrueNAS (no per-repo runner registration needed). The runner mounts
|
|
# the host Docker socket, so `docker compose` acts on the TrueNAS daemon and
|
|
# (re)builds + restarts the live __PROJECT__ stack from the exact pushed commit.
|
|
#
|
|
# db is internal-only (no host publish) — reachable as db:5432 on the compose net.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
|
|
# Avoid overlapping deploys if pushes land in quick succession.
|
|
concurrency:
|
|
group: truenas-deploy-__PROJECT__
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
# Skip on the template repo itself (placeholders unsubstituted → would fail).
|
|
# Generated repos have a different name, so this guard passes for them.
|
|
if: ${{ gitea.repository != 'pplate/homelab-app-template' }}
|
|
env:
|
|
COMPOSE: docker compose -f docker-compose.yml -f docker-compose.truenas.yml -p __PROJECT__
|
|
# Production secrets — set in Gitea repo Settings → Actions → Secrets.
|
|
# AUTH_SECRET : NextAuth v5 session secret (rotating invalidates sessions)
|
|
# JWT_SECRET : base64 backend HMAC key (rotating invalidates all tokens)
|
|
# DB_PASSWORD : Postgres role password (must match the live DB role)
|
|
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
|
|
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
|
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
|
|
steps:
|
|
- name: Check out pushed commit
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Show toolchain
|
|
run: |
|
|
set -euo pipefail
|
|
docker version --format 'docker {{.Server.Version}}'
|
|
docker compose version
|
|
|
|
# NOTE: Backend tests and frontend lint are a LOCAL-ONLY gate. The
|
|
# self-hosted act runner uses Docker-in-Docker which doesn't support volume
|
|
# mounts for nested containers. Run them before pushing.
|
|
|
|
- name: Build images
|
|
run: |
|
|
set -euo pipefail
|
|
$COMPOSE build
|
|
|
|
- name: Ensure DB up & reconcile role password
|
|
run: |
|
|
set -euo pipefail
|
|
# Start just the db first (idempotent — reuses the running container
|
|
# and the persistent __PROJECT___pgdata volume).
|
|
$COMPOSE up -d db
|
|
echo "Waiting for db to accept connections ..."
|
|
for i in $(seq 1 20); do
|
|
if docker exec __PROJECT__-db pg_isready -U __PROJECT__ -q; then break; fi
|
|
echo " attempt $i/20 — waiting 3s"; sleep 3
|
|
done
|
|
# POSTGRES_PASSWORD only applies on FIRST volume init, so an existing
|
|
# volume still holds the old role password. Force the live role to match
|
|
# the rotated ${DB_PASSWORD} so the backend can authenticate. Local
|
|
# socket connections inside the container use trust auth (no password).
|
|
# Skipped when the secret is unset to avoid blanking the dev password.
|
|
if [ -n "${DB_PASSWORD:-}" ]; then
|
|
docker exec __PROJECT__-db psql -U __PROJECT__ -d __PROJECT__ \
|
|
-c "ALTER USER __PROJECT__ WITH PASSWORD '${DB_PASSWORD}';"
|
|
echo "✅ DB role password reconciled"
|
|
else
|
|
echo "⚠️ DB_PASSWORD secret not set — leaving role password unchanged"
|
|
fi
|
|
|
|
- name: Roll out stack
|
|
run: |
|
|
set -euo pipefail
|
|
$COMPOSE up -d --remove-orphans
|
|
|
|
- name: Wait for backend health
|
|
run: |
|
|
set -euo pipefail
|
|
echo "Waiting for backend health on :__BACKEND_PORT__ ..."
|
|
for i in $(seq 1 20); do
|
|
if wget -q -O /dev/null http://192.168.188.119:__BACKEND_PORT__/actuator/health; then
|
|
echo "✅ Backend healthy after ${i} attempt(s)"
|
|
exit 0
|
|
fi
|
|
echo " attempt $i/20 — waiting 6s"
|
|
sleep 6
|
|
done
|
|
echo "❌ Backend did not become healthy — recent logs:"
|
|
$COMPOSE logs --tail=40 backend
|
|
exit 1
|
|
|
|
- name: Verify frontend
|
|
run: |
|
|
set -euo pipefail
|
|
# Probe the frontend on its own loopback INSIDE the container via the
|
|
# bundled node runtime. Network-namespace-independent (no reliance on the
|
|
# host port being wired during a mid-recreate window, which caused a
|
|
# transient false-failure previously) and needs no wget/curl in the image.
|
|
# Any HTTP status < 500 counts as "up" — root returns 307 -> /login when
|
|
# unauthenticated, which is healthy.
|
|
echo "Waiting for frontend on container loopback :3000 ..."
|
|
for i in $(seq 1 20); do
|
|
if docker exec __PROJECT__-frontend node -e "require('http').get('http://127.0.0.1:3000/',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))"; then
|
|
echo "✅ Frontend responding after ${i} attempt(s)"
|
|
exit 0
|
|
fi
|
|
echo " attempt $i/20 — waiting 5s"
|
|
sleep 5
|
|
done
|
|
echo "❌ Frontend did not respond — recent logs:"
|
|
$COMPOSE logs --tail=40 frontend
|
|
exit 1
|
|
|
|
- name: Prune dangling images
|
|
run: docker image prune -f || true
|
|
|
|
- name: Deployment summary
|
|
run: |
|
|
echo "=== __PROJECT__ deployed to TrueNAS ==="
|
|
echo "Commit: ${GITHUB_SHA}"
|
|
echo "Backend: http://192.168.188.119:__BACKEND_PORT__"
|
|
echo "Frontend: http://192.168.188.119:__FRONTEND_PORT__"
|