fe57dde57b
Add the rule-46 image-publish lane: a `publish` job gated on [lint, unit, integration] that builds the runtime Dockerfile in ci-builder and pushes git.fabledsword.com/bvandeusen/steward:<sha> always, :dev on dev and :latest on main. Authenticates with the REGISTRY_TOKEN repo secret (the injected GITHUB_TOKEN lacks write:package). Steward is the first family app to implement the publish lane; mechanics mirror CI-runner's build-ci-python.yml. Add compose.deploy.yml for a persistent remote instance that pulls :dev (no source bind-mounts, persistent app_data+pgdata volumes, bundled Postgres), plus a README "Remote dev instance" runbook and the POSTGRES_PASSWORD env key. Fix .env.example's database URL var to the canonical STEWARD_DATABASE_URL (single underscore) and document the publish lane in ci-requirements.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
5.4 KiB
YAML
130 lines
5.4 KiB
YAML
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
|