name: CI # CI lanes per FabledRulebook forgejo.md "CI philosophy": # - lint: ruff only, no dep install — fast-fail for the common lint bounce. # - unit: pytest -m "not integration"; no service containers, no DB. # - integration: postgres service container; boots the real app + runs all # (core + bundled-plugin) migrations. # Single repo: first-party plugins are bundled in-tree under plugins/, so the # unit lane imports them directly — no cross-repo checkout. on: push: branches: [dev, main] # pull_request intentionally absent — push on [dev, main] already fires CI for # every dev commit and dev→main PRs. Single-operator repo, no fork PRs. jobs: # Fast-fail lint lane. ruff is pre-installed in the ci-python image, so this # runs with NO dependency install and surfaces lint bounces in seconds. lint: runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 steps: - uses: actions/checkout@v4 - name: Ruff lint run: ruff check steward/ plugins/ tests/ unit: runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 steps: - uses: actions/checkout@v4 - name: Install Steward + test deps # uv: 5-10x faster wheel resolve than pip on cold caches. Falls back to # pip on uv-missing runners. Steward declares its deps in pyproject; the # [dev] extra adds pytest + pytest-asyncio. run: | if command -v uv >/dev/null 2>&1; then uv pip install --system -e '.[dev]' else pip install -e '.[dev]' fi - name: Pytest (unit only — integration runs in its own lane) run: pytest tests/ -v -m "not integration" integration: runs-on: python-ci container: image: git.fabledsword.com/bvandeusen/ci-python:3.14 env: STEWARD_SECRET_KEY: ci_integration_placeholder services: postgres: image: postgres:16-alpine env: POSTGRES_USER: steward POSTGRES_PASSWORD: steward POSTGRES_DB: steward options: >- --health-cmd "pg_isready -U steward" --health-interval 10s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v4 - name: Boot + migrate against Postgres # act_runner (swarm-runner) puts service containers on the default bridge # with no embedded DNS, so the hostname `postgres` won't resolve — we # discover the service container's bridge IP via the docker socket. The # job name `integration` (no underscore) is the docker-ps name filter. run: | set -euxo pipefail echo "=== container landscape (diagnostic for filter scoping) ===" docker ps -a --format '{{.ID}} {{.Image}} -> {{.Names}}' echo "=== end landscape ===" PG=$(docker ps --filter "name=integration" --filter "ancestor=postgres:16-alpine" -q | head -n1) test -n "$PG" PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG") test -n "$PG_IP" export STEWARD_DATABASE_URL="postgresql+asyncpg://steward:steward@$PG_IP:5432/steward" for i in $(seq 1 60); do (echo > "/dev/tcp/$PG_IP/5432") >/dev/null 2>&1 && break sleep 2 done if command -v uv >/dev/null 2>&1; then uv pip install --system -e '.[dev,ansible]' else pip install -e '.[dev,ansible]' fi pytest tests/integration -v -m integration --durations=10 # Image-publish lane — FabledRulebook rule 46: every push to dev/main publishes # an immutable commit-SHA image (the rollback unit); dev moves :dev, main moves # :latest. Gated on the three test lanes via `needs:` so a red commit never # produces a :dev image. Mechanics mirror CI-runner's build-ci-python.yml. publish: needs: [lint, unit, integration] runs-on: python-ci container: # ci-builder carries docker + buildx + git. The docker socket is # auto-mounted by act_runner (valid_volumes) — do NOT add a # container.options "-v /var/run/docker.sock:..." mount; that duplicate # mount fails the job at "Set up job". image: git.fabledsword.com/bvandeusen/ci-builder:latest steps: - uses: actions/checkout@v4 - name: Registry login # The injected GITHUB_TOKEN lacks write:package scope, so docker push # 401s with it. REGISTRY_TOKEN is a repo Actions secret holding a # Forgejo token scoped read:package + write:package. run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.fabledsword.com -u bvandeusen --password-stdin - name: Build + push (commit-SHA always; :dev on dev, :latest on main) run: | set -euxo pipefail IMAGE=git.fabledsword.com/bvandeusen/steward docker build -t "$IMAGE:${{ github.sha }}" . docker push "$IMAGE:${{ github.sha }}" if [ "${{ github.ref }}" = "refs/heads/dev" ]; then docker tag "$IMAGE:${{ github.sha }}" "$IMAGE:dev" docker push "$IMAGE:dev" elif [ "${{ github.ref }}" = "refs/heads/main" ]; then docker tag "$IMAGE:${{ github.sha }}" "$IMAGE:latest" docker push "$IMAGE:latest" fi - name: Prune dangling layers if: always() run: docker image prune -f