Files
anouma/.gitea/workflows/ci.yml
T
maro f1cd07b3ad
CI / test (push) Failing after 11m55s
CI / build-image (push) Skipped
CI/CD: build the Docker image once on main, promote it on release tags
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.
2026-09-06 22:24:07 +02:00

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"