79269da802
Addresses operator reports: Stop never finishes, the agent polls curator constantly, and stale-cached pages get mistaken for a failed deploy. - Stop is prompt: flip _running BEFORE any lock so /status + worker loops see "stopped" immediately, and add a stop/shrink checkpoint in _process (after decode, before the expensive detect+embed) that releases the job and bails — so a Stop doesn't wait out heavy GPU work. - Lazy curator polling: the queue snapshot is fetched only while a browser is actually watching (a /status hit within UI_IDLE_GRACE) and on a 5s cadence, not a constant background loop. The work loop's own lease/submit is curator's only visitor otherwise — nothing polls just to poll. - Build marker: VERSION is embedded in the page and reported on /status; the UI shows a "reload" banner when they differ, so a browser-cached page can't be mistaken for "the new image didn't deploy" (complements the no-store header). CI: the lint lane now also `ruff check`s agent/ and compileall-parses it, so the GPU agent is linted + syntax-checked before its image builds (build.yml only `docker build`s it). Fixed the agent's pre-existing UP037/B905 so it passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
186 lines
9.0 KiB
YAML
186 lines
9.0 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.
|
|
# - backend-lint-and-test: `pytest -m "not integration"`, no service containers.
|
|
# - frontend-build: vitest unit + vite build.
|
|
# - integration: pgvector + redis service containers; alembic + `pytest -m integration`.
|
|
|
|
on:
|
|
push:
|
|
branches: [dev, main]
|
|
# pull_request trigger intentionally absent — with branches: [dev, main]
|
|
# above, every PR commit already fires CI via the push event on dev. Adding
|
|
# pull_request would duplicate runs on dev→main PRs. FC has no fork PRs
|
|
# (single-operator Forgejo repo) so push coverage is complete.
|
|
|
|
jobs:
|
|
# Fast-fail lint lane. ruff is pre-installed in the ci-python image, so
|
|
# this runs with NO dependency install and surfaces the most common bounce
|
|
# class (lint: I001 / UP037 / ASYNC109 / W293 …) in seconds — instead of
|
|
# after the backend job's ~30-60s wheel install. ruff is static analysis,
|
|
# so no DB/secret env is needed.
|
|
lint:
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Ruff lint
|
|
# agent/ included so the GPU-agent is linted before its image is built
|
|
# (build.yml only `docker build`s it — this is where it gets checked).
|
|
run: ruff check backend/ tests/ alembic/ agent/
|
|
- name: Agent syntax check
|
|
# The agent's runtime deps (torch/transformers/ultralytics) aren't in the
|
|
# CI image, so we can't import it — but compileall parses every module,
|
|
# catching syntax errors before the image build.
|
|
run: python -m compileall -q agent/fc_agent
|
|
|
|
backend-lint-and-test:
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
|
env:
|
|
# DB_PASSWORD and SECRET_KEY are required by config.py at import time
|
|
# even though unit tests don't actually touch the DB or use the secret.
|
|
DB_PASSWORD: ci_unit_test_placeholder
|
|
SECRET_KEY: ci_unit_test_placeholder
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Cache step removed 2026-05-26: act_runner's cache backend has been
|
|
# broken on this homelab runner since 2026-05-15 (first as request-
|
|
# timeout warnings, then as hard "Cannot find module .../dist/restore/
|
|
# index.js" failures that tank the whole job). The cache step targeted
|
|
# ~/.cache/pip but the install below uses `uv pip install` primarily,
|
|
# whose own cache lives at ~/.cache/uv — so the cache step's real
|
|
# benefit was marginal even when working. Cost of removal: ~30s of
|
|
# wheel downloads per job. Future re-enable: mount ~/.cache/uv as a
|
|
# docker volume at the runner level (skips actions/cache entirely),
|
|
# or fix the runner-side cache backend (clear /var/run/act/actions/*,
|
|
# pin act_runner version, etc.).
|
|
|
|
- name: Install Python deps
|
|
# ruff is pre-installed in the ci-python image (see CI-Runner/CI-python/
|
|
# Dockerfile's RUFF_VERSION). Per FabledRulebook ci-runners.md, toolchain
|
|
# versions live on the runner image, not here.
|
|
# uv: 5-10x faster wheel resolve than pip for cold caches.
|
|
# Falls back to pip install on uv-missing runners (older images).
|
|
run: |
|
|
if command -v uv >/dev/null 2>&1; then
|
|
uv pip install --system -r requirements.txt pytest pytest-asyncio
|
|
else
|
|
pip install -r requirements.txt pytest pytest-asyncio
|
|
fi
|
|
|
|
# Ruff moved to the dedicated fast `lint` job above (fails in seconds,
|
|
# no dep install). This job is now unit tests only.
|
|
- name: Pytest (unit only — integration runs in the integration job)
|
|
run: pytest tests/ -v -m "not integration"
|
|
|
|
frontend-build:
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
|
defaults:
|
|
run:
|
|
working-directory: frontend
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
# No package-lock.json is tracked yet (we don't run npm locally per
|
|
# feedback-no-local-runs). Using `npm install` instead of `npm ci`.
|
|
# If we want strict lockfile-based reproducibility later, commit a
|
|
# package-lock.json and flip this back to `npm ci`.
|
|
- run: npm install --no-audit --no-fund
|
|
# `npm run check` (vue-tsc --noEmit) skipped: the frontend is pure JS
|
|
# with no .ts files and no JSDoc annotations, so vue-tsc has nothing
|
|
# to type-check. Re-enable once we add a tsconfig.json and either
|
|
# convert to TS or add JSDoc.
|
|
- run: npm run test:unit
|
|
- run: npm run build
|
|
|
|
# Single integration job — collapsed from a 3-way shard split on 2026-06-04.
|
|
# The shards existed to parallelize ~8.5min of integration tests; once the
|
|
# throwaway Postgres runs with fsync OFF (the durability step below) the whole
|
|
# suite runs in ~45s, so the split only triplicated the ~2min fixed overhead
|
|
# (container + `uv pip install` + `alembic upgrade head`) and burned 3 of 6
|
|
# runner slots for no wall-clock gain. One job now: spin up once, install
|
|
# once, migrate once, run every integration test.
|
|
#
|
|
# The docker-ps filter scopes to THIS job's own Postgres/Redis service
|
|
# containers by job name. act_runner strips underscores from job names when
|
|
# labelling containers (`int_api` matched nothing on 2026-05-25), so the name
|
|
# stays separator-free (`integration`). The step prints `docker ps -a` first
|
|
# so a future naming-convention shift surfaces in the log without a
|
|
# guess-and-push cycle.
|
|
#
|
|
# Pre-baking requirements.txt into ci-python:3.14 is intentionally NOT done —
|
|
# per ci-requirements.md, FC is the only Python consumer of that image and the
|
|
# CI-Runner "add deps to image when used by >1 project" rule keeps it per-job.
|
|
integration:
|
|
runs-on: python-ci
|
|
container:
|
|
image: git.fabledsword.com/bvandeusen/ci-python:3.14
|
|
env:
|
|
DB_USER: fabledcurator
|
|
DB_PASSWORD: ci_integration
|
|
DB_PORT: "5432"
|
|
DB_NAME: fabledcurator_test
|
|
SECRET_KEY: ci_integration_placeholder
|
|
services:
|
|
postgres:
|
|
image: pgvector/pgvector:pg16
|
|
env:
|
|
POSTGRES_USER: fabledcurator
|
|
POSTGRES_PASSWORD: ci_integration
|
|
POSTGRES_DB: fabledcurator_test
|
|
options: >-
|
|
--health-cmd "pg_isready -U fabledcurator"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 10
|
|
redis:
|
|
image: redis:7-alpine
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 10
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- name: Integration suite (resolve service IPs, migrate, test)
|
|
run: |
|
|
set -eux
|
|
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=pgvector/pgvector:pg16" -q | head -n1)
|
|
RD=$(docker ps --filter "name=integration" --filter "ancestor=redis:7-alpine" -q | head -n1)
|
|
test -n "$PG" && test -n "$RD"
|
|
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG")
|
|
RD_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$RD")
|
|
test -n "$PG_IP" && test -n "$RD_IP"
|
|
export DB_HOST="$PG_IP"
|
|
export CELERY_BROKER_URL="redis://$RD_IP:6379/0"
|
|
export CELERY_RESULT_BACKEND="redis://$RD_IP:6379/0"
|
|
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 -r requirements.txt pytest pytest-asyncio
|
|
else
|
|
pip install -r requirements.txt pytest pytest-asyncio
|
|
fi
|
|
# Relax durability on the throwaway CI Postgres so the per-test
|
|
# TRUNCATE's commit-fsync — the integration teardown's dominant cost
|
|
# (~1.5-2s/test, which collapsed the suite from ~13min to ~45s) — is
|
|
# skipped. fsync/full_page_writes are sighup GUCs and synchronous_commit
|
|
# is user-context, so ALTER SYSTEM + pg_reload_conf() applies them with
|
|
# NO restart. Ephemeral DB ⇒ fsync-off is safe. Non-fatal so a perms
|
|
# surprise can't red the job; fabledcurator is the postgres image's
|
|
# bootstrap superuser.
|
|
python -c "import os,psycopg; c=psycopg.connect(host=os.environ['DB_HOST'],port=5432,user=os.environ['DB_USER'],password=os.environ['DB_PASSWORD'],dbname=os.environ['DB_NAME'],autocommit=True); [c.execute(q) for q in ('ALTER SYSTEM SET fsync=off','ALTER SYSTEM SET synchronous_commit=off','ALTER SYSTEM SET full_page_writes=off','SELECT pg_reload_conf()')]; c.close()" || echo 'WARN: durability GUC relax failed (continuing)'
|
|
alembic upgrade head
|
|
pytest tests/ -v -m integration --durations=15
|