name: deploy on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: test-backend: runs-on: ubuntu-latest container: image: rust:1-slim services: postgres: image: postgres:16-alpine env: POSTGRES_USER: mangalord POSTGRES_PASSWORD: mangalord POSTGRES_DB: mangalord options: >- --health-cmd "pg_isready -U mangalord" --health-interval 5s --health-timeout 5s --health-retries 10 env: DATABASE_URL: postgres://mangalord:mangalord@postgres:5432/mangalord steps: - uses: actions/checkout@v4 - name: Install build deps run: | apt-get update apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates - name: Cache cargo registry and target uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git backend/target key: cargo-${{ runner.os }}-${{ hashFiles('backend/Cargo.lock') }} restore-keys: | cargo-${{ runner.os }}- - name: cargo test working-directory: backend run: cargo test --locked test-frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' cache: npm cache-dependency-path: frontend/package-lock.json - name: npm ci working-directory: frontend run: npm ci - name: vitest working-directory: frontend run: npm test build-and-push: runs-on: ubuntu-latest needs: [test-backend, test-frontend] # PRs only run the test jobs; build + deploy are reserved for # post-merge pushes to main. Without this gate every PR would push # a tagged image to the registry and SSH-deploy to prod. if: github.event_name != 'pull_request' outputs: image_tag: ${{ steps.meta.outputs.image_tag }} version: ${{ steps.meta.outputs.version }} steps: - uses: actions/checkout@v4 - name: Resolve image tags id: meta run: | version="$(grep -m1 '^version' backend/Cargo.toml | cut -d'"' -f2)" frontend_version="$(grep -m1 '"version"' frontend/package.json | cut -d'"' -f4)" if [ "$version" != "$frontend_version" ]; then echo "Version mismatch: backend=$version frontend=$frontend_version" >&2 exit 1 fi echo "image_tag=${GITHUB_SHA}" >> "$GITHUB_OUTPUT" echo "version=${version}" >> "$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@v3 - name: docker login uses: docker/login-action@v3 with: registry: ${{ secrets.REGISTRY_URL }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Build & push backend uses: docker/build-push-action@v5 with: context: ./backend push: true tags: | ${{ secrets.REGISTRY_URL }}/mangalord-backend:latest ${{ secrets.REGISTRY_URL }}/mangalord-backend:${{ steps.meta.outputs.image_tag }} ${{ secrets.REGISTRY_URL }}/mangalord-backend:${{ steps.meta.outputs.version }} cache-from: type=gha,scope=backend cache-to: type=gha,mode=max,scope=backend - name: Build & push frontend uses: docker/build-push-action@v5 with: context: ./frontend push: true tags: | ${{ secrets.REGISTRY_URL }}/mangalord-frontend:latest ${{ secrets.REGISTRY_URL }}/mangalord-frontend:${{ steps.meta.outputs.image_tag }} ${{ secrets.REGISTRY_URL }}/mangalord-frontend:${{ steps.meta.outputs.version }} cache-from: type=gha,scope=frontend cache-to: type=gha,mode=max,scope=frontend deploy: runs-on: ubuntu-latest needs: build-and-push if: github.event_name != 'pull_request' # Single-host deploy: the runner lives on the same box as the stack, so we # drive the host docker daemon directly (act_runner shares its socket via # `docker_host: "-"`) instead of SSHing out. The compose dir is bind-mounted # at its REAL host path so compose's relative bind-mounts (./mangalord/..., # ./Caddyfile) resolve; this requires `/mnt/ssd/docker-data` in the runner's # container.valid_volumes. The central compose references the images as # registry.mc02.dev/mangalord-*:${MANGALORD_TAG:-latest}, so we only pull # and recreate the two mangalord services at the freshly built SHA. container: image: docker:cli volumes: - /mnt/ssd/docker-data:/mnt/ssd/docker-data steps: - name: Deploy to the local stack working-directory: /mnt/ssd/docker-data env: REGISTRY_URL: ${{ secrets.REGISTRY_URL }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }} run: | set -eu echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USERNAME" --password-stdin export MANGALORD_TAG="$IMAGE_TAG" docker compose pull mangalord-backend mangalord-frontend docker compose up -d mangalord-backend mangalord-frontend docker image prune -f docker logout "$REGISTRY_URL"