Previously release.yml fully rebuilt and re-ran lint/typecheck/test/build from source on every tag push, duplicating what ci.yml had already just verified on main. Now ci.yml builds and pushes the image (tagged by commit SHA) once, right after its checks pass, but only for pushes to main. release.yml no longer builds anything: it pulls that exact image, retags it as the release version + latest, and deploys it. This guarantees the deployed image is byte-for-byte what CI verified, and turns "tag a commit that was never pushed to main or hasn't finished CI" into a loud, immediate failure (no image to promote) instead of a rebuild that silently skips the checks.
119 lines
4.6 KiB
YAML
119 lines
4.6 KiB
YAML
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"
|