name: CI on: push: branches: ['**'] pull_request: jobs: test: runs-on: ubuntu-latest container: image: node:22-alpine # No live database is needed here — every Payload-backed route in this # app is force-dynamic, so `next build` never touches Postgres. These # are dummy values only so a real config error (a missing secret) is # never silently masked by CI happening to run without one. # # NODE_ENV is deliberately NOT set here: `npm ci` treats a production # NODE_ENV as `--omit=dev`, which would skip eslint/typescript/etc. and # break Lint/Typecheck/Test. It's set below, scoped to the Build step. env: NEXT_TELEMETRY_DISABLED: "1" DATABASE_URI: postgresql://ci:ci@localhost:5432/ci PAYLOAD_SECRET: ci-only-not-a-real-secret MEETING_SESSION_SECRET: ci-only-not-a-real-secret ANALYTICS_SALT_SECRET: ci-only-not-a-real-secret CRON_SECRET: ci-only-not-a-real-secret NEXT_PUBLIC_SERVER_URL: http://localhost:3000 steps: - name: Checkout uses: actions/checkout@v4 - name: Install build dependencies (sharp needs libc6-compat on alpine) run: apk add --no-cache libc6-compat - name: Install dependencies run: npm ci - name: Lint run: npm run lint - name: Typecheck run: npx next typegen && npx tsc --noEmit - name: Test run: npm test - name: Build env: NODE_ENV: production run: npm run build # Builds once per commit that lands on main and has passed the checks # above, and pushes it to the registry tagged by commit SHA. release.yml # (triggered by a version tag) never rebuilds — it only promotes this # exact, already-tested image to the release tag + `latest` and deploys # it. This guarantees byte-for-byte that what gets deployed is what CI # verified, and makes tagging a commit that never passed CI on main fail # loudly (no image to promote) instead of silently rebuilding from # source and skipping the checks. build-image: needs: test if: github.ref == 'refs/heads/main' && github.event_name == 'push' runs-on: ubuntu-latest # The anouma-runner runs as a Docker container itself (gitea/runner:3), # with the host's docker.sock bind-mounted in but no `docker` CLI binary # of its own — a job with no `container:` here runs directly inside that # minimal runner image and gets "docker: command not found". # # A plain `docker:27-cli` image (Alpine, no Node.js) doesn't work either: # actions/checkout@v4 is a JS action and needs a Node runtime in the job # container, which that image doesn't have — it fails at Checkout with # exit 127. Using a Node image as the base guarantees Node is already # there (same reason the `test` job's node:22-alpine works), and the # Docker CLI is added via apt. The runner auto-mounts the same host # docker.sock into this container (sibling containers — see # container.docker_host in the runner's config.yaml), so `docker # build`/`push` operate on the host's real Docker daemon. release.yml's # promote-and-deploy job uses the same setup, for the same reasons. container: image: node:22-bookworm-slim steps: - name: Checkout uses: actions/checkout@v4 - name: Install Docker CLI run: | apt-get update apt-get install -y --no-install-recommends docker.io rm -rf /var/lib/apt/lists/* - name: Determine image name id: image run: | echo "registry=${REGISTRY:-git.maro.run}" >> "$GITHUB_OUTPUT" echo "repo=${REGISTRY_REPO:-maro/anouma}" >> "$GITHUB_OUTPUT" env: REGISTRY: git.maro.run REGISTRY_REPO: maro/anouma - name: Log in to registry env: REGISTRY_IMAGE: ${{ steps.image.outputs.registry }} REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_IMAGE" -u "$REGISTRY_USERNAME" --password-stdin - name: Build image env: IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }} # Tagged by commit SHA only — `latest` is reserved for whatever was # last actually released (see release.yml), not the newest main build. run: docker build -t "$IMAGE:$GITHUB_SHA" . - name: Push image env: IMAGE: ${{ steps.image.outputs.registry }}/${{ steps.image.outputs.repo }} run: docker push "$IMAGE:$GITHUB_SHA"