02138f5728
The py3.12-node22 runner doesn't ship pipx, so the previous commit's pipx install ruff failed with command not found. Switched to the same venv pattern the test job uses.
184 lines
6.1 KiB
YAML
184 lines
6.1 KiB
YAML
# CI runs first; build only proceeds if all checks pass.
|
|
#
|
|
# Push to any branch: typecheck + lint + test
|
|
# Push to dev: gates + build :dev + :<sha>
|
|
# Tag v* (release): gates + build :latest + :<sha> + :<version>
|
|
#
|
|
# 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 + :<version>.
|
|
#
|
|
# 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
|
|
name: CI & Build
|
|
|
|
on:
|
|
push:
|
|
branches: [dev, main]
|
|
tags: ["v*"]
|
|
paths:
|
|
- "src/**"
|
|
- "frontend/**"
|
|
- "tests/**"
|
|
- "pyproject.toml"
|
|
- "alembic/**"
|
|
- "alembic.ini"
|
|
- "Dockerfile"
|
|
- "assets/**"
|
|
- "fable-mcp/**"
|
|
- ".forgejo/workflows/ci.yml"
|
|
|
|
# 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
|
|
IMAGE: git.fabledsword.com/bvandeusen/fabledassistant
|
|
|
|
jobs:
|
|
typecheck:
|
|
name: TypeScript typecheck
|
|
runs-on: py3.12-node22
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: "22"
|
|
cache: "npm"
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
working-directory: frontend
|
|
|
|
- name: Type check
|
|
run: npx vue-tsc --noEmit
|
|
working-directory: frontend
|
|
|
|
lint:
|
|
name: Python lint
|
|
runs-on: py3.12-node22
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Install ruff
|
|
# Isolated venv avoids polluting the runner's system
|
|
# site-packages (the old --break-system-packages was a smell)
|
|
# and pipx isn't available on the py3.12-node22 runner image.
|
|
run: |
|
|
python3.12 -m venv /opt/ruff-venv
|
|
/opt/ruff-venv/bin/pip install --quiet ruff
|
|
|
|
- name: Lint
|
|
run: /opt/ruff-venv/bin/ruff check src/
|
|
|
|
test:
|
|
name: Python tests
|
|
runs-on: py3.12-node22
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
# Python 3.12 is pre-installed in the runner-base image (py3.12-node22).
|
|
- name: Cache pip wheels
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: pip-${{ hashFiles('pyproject.toml') }}
|
|
restore-keys: pip-
|
|
|
|
- name: Create virtual environment
|
|
run: python3.12 -m venv /opt/venv
|
|
|
|
- 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/ -q
|
|
|
|
build:
|
|
name: Build & push image
|
|
needs: [typecheck, lint, test]
|
|
# Build on dev branch pushes and version tag pushes only.
|
|
# 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
|
|
|
|
- name: Generate image tags and version
|
|
id: tags
|
|
run: |
|
|
TAGS="${{ env.IMAGE }}:${{ github.sha }}"
|
|
BUILD_VERSION="dev"
|
|
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then
|
|
TAGS="$TAGS,${{ env.IMAGE }}:dev"
|
|
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
|
TAGS="$TAGS,${{ env.IMAGE }}:latest,${{ env.IMAGE }}:${{ github.ref_name }}"
|
|
BUILD_VERSION="${{ github.ref_name }}"
|
|
fi
|
|
echo "value=$TAGS" >> $GITHUB_OUTPUT
|
|
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: |
|
|
docker system prune -af || true
|
|
docker builder prune --keep-storage 5g -f || true
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Log in to Forgejo registry
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ secrets.REGISTRY_USER }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
push: true
|
|
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
|