diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 25b6eab..af9973a 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,13 +1,16 @@ # CI runs first; build only proceeds if all checks pass. # -# Push to dev: typecheck + lint + test → build :dev + : -# Tag v* (release): typecheck + lint + test → build :latest + : + : -# Pull request: no CI run — code is validated on dev push before any PR is opened +# Push to any branch: typecheck + lint + test +# Push to dev: gates + build :dev + : +# Tag v* (release): gates + build :latest + : + : # # To cut a release: # Create a release via the Forgejo UI on main with a v* tag name. # The tag push triggers this workflow; build job pushes :latest + :. # +# PRs aren't triggered on purpose — this is a solo dev→main flow, so +# gating on branch push is already enough. +# # Required secrets (repo → Settings → Secrets → Actions): # REGISTRY_USER — your Forgejo username # REGISTRY_TOKEN — Forgejo PAT with write:packages scope @@ -15,7 +18,7 @@ name: CI & Build on: push: - branches: [dev] + branches: [dev, main] tags: ["v*"] paths: - "src/**" @@ -28,8 +31,18 @@ on: - "assets/**" - "fable-mcp/**" - ".forgejo/workflows/ci.yml" - # pull_request trigger intentionally omitted — all changes go through dev - # first, where CI already runs on push. PR runs would be redundant duplication. + +# Cancel older runs on the same branch when a newer push lands. Tag runs +# get their own group implicitly (refs/tags/v1.2.3 ≠ refs/heads/dev) and +# are never cancelled, so a release build can't kill itself mid-flight. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} + +# Least-privilege default. Jobs that need more (build pushes to the +# registry) upgrade explicitly. +permissions: + contents: read env: REGISTRY: git.fabledsword.com @@ -63,7 +76,9 @@ jobs: - uses: actions/checkout@v6 - name: Install ruff - run: pip install --break-system-packages ruff + # pipx keeps ruff isolated instead of polluting the system + # site-packages (the old --break-system-packages was a smell). + run: pipx install ruff - name: Lint run: ruff check src/ @@ -76,7 +91,7 @@ jobs: # Python 3.12 is pre-installed in the runner-base image (py3.12-node22). - name: Cache pip wheels - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-${{ hashFiles('pyproject.toml') }} @@ -88,22 +103,28 @@ jobs: - name: Install package with dev deps run: | /opt/venv/bin/pip install --upgrade pip setuptools wheel + # http-ece's build-time dep on cryptography fails under PEP 517 + # isolation on this runner image — install it without isolation + # first so the editable install below finds it already built. /opt/venv/bin/pip install --no-build-isolation http-ece /opt/venv/bin/pip install -e ".[dev]" - name: Run tests - run: /opt/venv/bin/python -m pytest tests/ -v + run: /opt/venv/bin/python -m pytest tests/ -q build: name: Build & push image needs: [typecheck, lint, test] # Build on dev branch pushes and version tag pushes only. - # main branch pushes run CI for safety but do not build — + # main branch pushes run the gates for safety but do not build — # the release tag (v*) is the sole trigger for a production image. if: | github.event_name == 'push' && (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) runs-on: py3.12-node22 + permissions: + contents: read + packages: write steps: - uses: actions/checkout@v6 @@ -122,11 +143,14 @@ jobs: echo "build_version=$BUILD_VERSION" >> $GITHUB_OUTPUT - name: Free disk space + # Self-hosted runner housekeeping. Two-step cleanup: + # 1. Prune dangling containers/images globally (stops the runner + # from accumulating cruft from past failed builds). + # 2. Trim the BuildKit layer cache to a 5GB ceiling so the pip + # mount cache survives but old intermediate layers don't + # accumulate indefinitely. run: | - # Remove all unused images (including old :SHA tags) and containers. docker system prune -af || true - # Keep the local BuildKit cache bounded so pip mount cache survives - # but stale intermediate layers don't accumulate indefinitely. docker builder prune --keep-storage 5g -f || true - name: Set up Docker Buildx @@ -147,3 +171,10 @@ jobs: provenance: false tags: ${{ steps.tags.outputs.value }} build-args: BUILD_VERSION=${{ steps.tags.outputs.build_version }} + # Registry-backed layer cache. Pull from :cache to prime + # BuildKit, push updated layers back to :cache so the next + # build starts warm even if the runner's local cache was + # pruned. `mode=max` exports all intermediate layers, not + # just the final image, which is what gives the ~80% speedup. + cache-from: type=registry,ref=${{ env.IMAGE }}:cache + cache-to: type=registry,ref=${{ env.IMAGE }}:cache,mode=max