name: Backend CI # Runs the full backend pytest suite on every change to backend/. This is the # net that catches regressions in the panel — including the agent gateway, # registry, pairing, and the panel<->agent command loop exercised end-to-end # by tests/test_agent_poll_e2e.py — instead of finding out at install time. # main is deliberately absent from `push`: release.yml gates itself on this # workflow via `ci-gate`, so a push to main already runs the suite once. Listing # main here ran it a second time, standalone, on every merge. on: push: branches: [dev] paths: - 'backend/**' - '.github/workflows/backend-ci.yml' pull_request: branches: [main, dev] paths: - 'backend/**' - '.github/workflows/backend-ci.yml' workflow_call: jobs: # The ratchet needs to see the WHOLE suite, so it cannot live inside a shard — # each shard only collects its own quarter. Its own job costs no wall-clock # time: it runs beside the shards and finishes in seconds. ratchet: name: test-count ratchet runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip cache-dependency-path: backend/requirements.txt - name: Install dependencies working-directory: backend run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest - name: Test-count ratchet # Fails if collected-test count drops below backend/tests/BASELINE_COUNT. # Converts "code + its tests vanished together" (the 2026-07 data loss, # where a green suite hid ~363 lost tests) from invisible to a red X. # Lowering the floor requires editing BASELINE_COUNT in the same commit. working-directory: backend run: python tests/check_test_count.py - name: Undefined names # A name used before it exists only fails when that line runs, so a # rarely-taken branch ships broken: two routes that always 500'd on a # missing import, and a Stop button that crashed on a shadowing local # import. Pyflakes catches both statically in about a second. working-directory: backend run: | pip install ruff==0.14.14 ruff check --select F821,F823 app tests # Sharded because this job WAS the entire wait: 21m50s of a ~22m pipeline, # while every other workflow finished inside a minute. Splitting the suite # over 4 runners cuts the critical path to roughly a quarter. # # Sharding rather than pytest-xdist is deliberate: each shard is its own VM, # so the process-shared state that makes in-process parallelism unsafe here # (templates.json, APPS_DIR — see the per-PID DB dance in tests/conftest.py) # simply isn't shared. No test code has to change. # # NOTE: with no .test_durations file, pytest-split balances by test COUNT, # not by time — and these tests range from 0.1s to 10s+. If one shard lands # most of the slow ones it becomes the new critical path. If that shows up, # run once with `--store-durations` and commit backend/.test_durations to # switch it to duration-based balancing. pytest: name: pytest (${{ matrix.group }}/4) runs-on: ubuntu-latest strategy: # Report every failing shard in one pass instead of hiding shards 2-4 # behind the first failure — one round of fixes instead of four. fail-fast: false matrix: group: [1, 2, 3, 4] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip cache-dependency-path: backend/requirements.txt - name: Install dependencies working-directory: backend run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pytest-split - name: Run tests (shard ${{ matrix.group }} of 4) # Scoped to `tests` rather than a bare `pytest`. This collects the same # suite in CI (backend/dev-data/ is gitignored), while keeping the # command reproducible on a dev box: a # bare pytest there tries to collect the locally deployed apps under # backend/dev-data/ and dies during collection. Copy this line verbatim # to debug a red shard locally. working-directory: backend run: python -m pytest tests -v --splits 4 --group ${{ matrix.group }} # The @real_binaries leg (plan 82 §E): the same service code the shards run # against fake_subprocess meets the REAL parsers here — the panel's # generated crontab text goes through `crontab -`/`crontab -l`, and every # rendered vhost flavor through `nginx -t`. This is the class of bug the # #117 cron fix taught us write-only assertions cannot see. The tests skip # themselves everywhere unless SERVERKIT_REAL_BINARIES=1, so the shards # above never mutate a runner's crontab by surprise. real-binaries: name: real binaries runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip cache-dependency-path: backend/requirements.txt - name: Install nginx and cron run: | sudo apt-get update -qq sudo apt-get install -y -qq nginx-core cron - name: Install dependencies working-directory: backend run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest - name: Run real-binaries tests working-directory: backend env: SERVERKIT_REAL_BINARIES: '1' run: python -m pytest tests -m real_binaries -v # The @docker_builds leg: generated buildpack Dockerfiles meet the REAL # docker daemon — detect() -> generate_dockerfile() -> `docker build` -> # `docker run` -> HTTP probe, per stack. Same lesson as real-binaries: # test_buildpacks.py asserts on Dockerfile TEXT, which cannot see a bug # that only fails at build or boot time (the `npm install --omit=dev || # npm install` dead fallback exited 0 and shipped `vite: not found` to a # user's deploy). The tests skip themselves everywhere unless # SERVERKIT_DOCKER_BUILDS=1, so the shards above never pull images. docker-builds: name: docker builds runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' cache: pip cache-dependency-path: backend/requirements.txt - name: Install dependencies working-directory: backend run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest - name: Run docker-builds tests working-directory: backend env: SERVERKIT_DOCKER_BUILDS: '1' run: python -m pytest tests -m docker_builds -v