name: deploy on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: test-backend: runs-on: ubuntu-latest 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 # ubuntu-latest has node (so JS actions like checkout/cache run) but no # Rust. We intentionally avoid `container: rust:1-slim` because act_runner # runs JS actions with node *inside* the job container, and the slim Rust # image ships no node (checkout would fail with exit 127). - name: Install Rust + build deps run: | set -eu SUDO=""; [ "$(id -u)" = "0" ] || SUDO="sudo" $SUDO apt-get update $SUDO apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates curl curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - 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. if: github.event_name != 'pull_request' # Build on the host docker daemon directly (docker-outside-of-docker): # the runner shares the deploy host's daemon, so a plain `docker build` # reuses the host's layer cache and avoids buildx's docker-container # driver + the gha cache exporter — neither works against this single-host # act_runner, and there is no in-job daemon socket unless we mount it. container: image: docker.gitea.com/runner-images:ubuntu-latest volumes: - /var/run/docker.sock:/var/run/docker.sock 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" - name: Build & push backend + frontend env: REGISTRY_URL: ${{ secrets.REGISTRY_URL }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} IMAGE_TAG: ${{ steps.meta.outputs.image_tag }} VERSION: ${{ steps.meta.outputs.version }} run: | set -eu echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USERNAME" --password-stdin for svc in backend frontend; do img="$REGISTRY_URL/mangalord-$svc" docker build -t "$img:$IMAGE_TAG" -t "$img:latest" -t "$img:$VERSION" "./$svc" for tag in "$IMAGE_TAG" latest "$VERSION"; do docker push "$img:$tag"; done done docker logout "$REGISTRY_URL" 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 (the job mounts the host docker # socket) instead of SSHing out. The compose dir is bind-mounted at its # REAL host path so compose's relative bind-mounts (./mangalord/..., # ./Caddyfile) resolve; both paths must be 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 - /var/run/docker.sock:/var/run/docker.sock 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"