name: CI on: pull_request: push: branches: [master] concurrency: group: ci-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: PYTHONDONTWRITEBYTECODE: 1 jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libcairo2-dev - uses: astral-sh/setup-uv@v6 with: python-version-file: ".python-version" enable-cache: true - run: uv sync --locked - name: Pre-commit hooks (ruff, formatting, file hygiene) run: uv run prek run --all-files --show-diff-on-failure - name: Django system checks run: uv run python manage.py check - name: Check for missing migrations run: uv run python manage.py makemigrations --check --dry-run test: name: Test runs-on: ubuntu-latest env: DATABASE_ENGINE: sqlite3 DATABASE_NAME: db.sqlite3 steps: - uses: actions/checkout@v4 - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libcairo2-dev - uses: astral-sh/setup-uv@v6 with: python-version-file: ".python-version" enable-cache: true - uses: actions/setup-node@v7 with: node-version: 24 cache: npm - run: uv sync --locked - name: Build frontend assets run: | npm ci npm run build - run: uv run python manage.py collectstatic --noinput - name: Run tests run: uv run pytest -n auto --cov - name: Coveralls run: uv run coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Each architecture builds on a native runner: emulating arm64 under QEMU (compiling pycairo, npm, uv) was the slow part docker-build: name: Docker build (${{ matrix.platform }}) runs-on: ${{ matrix.runner }} needs: [lint, test] permissions: contents: read packages: write strategy: fail-fast: true matrix: include: - platform: linux/amd64 runner: ubuntu-latest - platform: linux/arm64 runner: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v4 # Only authenticate when we are actually going to push (never for PRs, which may come from forks) - name: Log in to GHCR if: github.event_name == 'push' uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Registry references must be lowercase, but the repository name is not - name: Set image name run: echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - id: build name: Build (and push by digest on master) uses: docker/build-push-action@v6 with: context: . platforms: ${{ matrix.platform }} build-args: GIT_SHA=${{ github.sha }} outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=${{ github.event_name == 'push' }} cache-from: type=gha,scope=${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ matrix.platform }} - name: Export digest if: github.event_name == 'push' env: DIGEST: ${{ steps.build.outputs.digest }} run: | mkdir -p "$RUNNER_TEMP/digests" touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" - name: Upload digest if: github.event_name == 'push' uses: actions/upload-artifact@v4 with: name: digests-${{ strategy.job-index }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 docker: name: Docker image runs-on: ubuntu-latest needs: [docker-build] if: github.event_name == 'push' outputs: tag: ${{ steps.sha-tag.outputs.tag }} permissions: contents: read packages: write steps: - uses: actions/download-artifact@v4 with: path: ${{ runner.temp }}/digests pattern: digests-* merge-multiple: true - uses: docker/setup-buildx-action@v4 - name: Log in to GHCR uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - id: meta uses: docker/metadata-action@v5 with: images: ghcr.io/${{ github.repository }} tags: | type=raw,value=latest type=sha - id: sha-tag name: Pick the commit tag env: TAGS: ${{ steps.meta.outputs.tags }} run: | ref="$(grep -m1 ':sha-' <<< "$TAGS")" test -n "$ref" echo "tag=${ref##*:}" >> "$GITHUB_OUTPUT" - name: Create multi-arch manifest working-directory: ${{ runner.temp }}/digests run: | IMAGE="ghcr.io/${GITHUB_REPOSITORY,,}" docker buildx imagetools create \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ $(printf "$IMAGE@sha256:%s " *) deploy: name: Deploy runs-on: ubuntu-latest needs: [docker] if: github.event_name == 'push' && github.ref == 'refs/heads/master' environment: production concurrency: deploy-production steps: - name: Deploy the image built for this commit env: DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} TAG: ${{ needs.docker.outputs.tag }} run: | install -m 700 -d ~/.ssh printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts ssh deploy@rigs.nottinghamtec.co.uk "$TAG"