Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 162 additions & 32 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,28 @@ jobs:
# for the rationale. The new workflow also bundles a content-scanning
# gitleaks job; both are parallel + ≤30s each.

backend-unit-fast:
# Fail-fast lane for the most common AI/human iteration failure mode:
# broken unit tests. Runs in parallel with the heavy `backend` job and
# surfaces unit-test regressions in ~50s vs the full job's ~6m. Unit
# tests are pure (no DB, Redis, ES, or OpenSearch needed) — 710 cases
# complete locally in ~5s. The full `backend` job below still runs the
# same unit tests as part of its coverage-gated run, so coverage logic
# is untouched; this lane is an early signal, not a replacement.
name: backend (unit tests — fast lane)
backend-unit:
# Lane 1 of 2 that feed the coverage gate (Win 2′ of
# infra_pr_yml_split_backend_test_lanes). Replaces the previous
# `backend-unit-fast` lane, doing double duty:
#
# 1. Fast signal — unit tests run in ~30-60s (vs the heavy lane's ~7-8m),
# so a broken unit test still surfaces quickly. Service containers
# are NOT needed; unit tests are pure (no DB / Redis / ES / OS).
# 2. Coverage feed — runs with `--cov=backend` + `COVERAGE_FILE=.coverage.unit`
# so the data file lands at a unique name (NOT in coverage.py's
# parallel mode, which would litter every local-dev `pytest --cov`
# run with `.coverage.<host>.<pid>.<rand>` clutter). The single
# `.coverage.unit` file feeds the `backend-cov-gate` job that
# combines this lane's coverage with the heavy lane's and gates
# against the 80% fail_under threshold.
#
# `-n auto` parallelism is safe at the unit layer (the layer that
# broke under -n auto was integration, due to FK-teardown collisions —
# see the `backend-heavy` comment). The combined parallel + coverage
# cost is approximately a wash with the previous --no-cov serial run.
name: backend (unit + cov, parallel)
if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand All @@ -138,8 +151,31 @@ jobs:
- name: Install project deps (frozen)
run: uv sync --frozen

- name: pytest backend/tests/unit/
run: uv run pytest backend/tests/unit/ --no-cov -q
- name: pytest backend/tests/unit/ (-n auto, with coverage)
# `--cov-report=` (empty) suppresses the immediate terminal report —
# the report is generated by `backend-cov-gate` after the combine.
# `--cov-fail-under=0` disables pytest-cov's per-lane fail_under
# check (which would otherwise reject the unit lane at ~64% — unit
# tests alone don't exercise the full app; the 80% gate fires in
# `backend-cov-gate` against the COMBINED unit + heavy coverage).
env:
# Override the default `.coverage` base name so this lane's
# data file lands at `.coverage.unit`. `coverage combine` in
# `backend-cov-gate` matches any file starting with `.coverage.`
# so `.coverage.unit` is picked up alongside the heavy lane's
# `.coverage.heavy`.
COVERAGE_FILE: .coverage.unit
run: uv run pytest backend/tests/unit/ -n auto --cov=backend --cov-report= --cov-fail-under=0

- name: Upload partial coverage data (unit lane)
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-data-unit
path: .coverage.unit*
if-no-files-found: error
include-hidden-files: true
retention-days: 1

# -------------------------------------------------------------------------
# License headers — REUSE (https://reuse.software/) SPDX compliance.
Expand Down Expand Up @@ -358,8 +394,18 @@ jobs:
- name: vitest
run: pnpm --dir ui test

backend:
name: backend (tests + coverage)
backend-heavy:
# Lane 2 of 2 that feed the coverage gate (Win 2′ of
# infra_pr_yml_split_backend_test_lanes). Runs contract + integration
# tests SERIALLY (no `-n auto`) because the integration layer hit
# FK-teardown collisions under parallel workers (PR #291 first run —
# `query_sets_cluster_id_fkey` violation when one worker held a FK
# reference to a cluster being deleted in another worker's teardown).
# Contract tests bundle here too — several boot the FastAPI app via
# `LifespanManager` (touches Postgres / Redis at startup), so they
# need the service containers this lane provides anyway. `pytest-xdist`
# remains in dev deps for local opt-in on the unit layer.
name: backend (contract + integration + cov)
# TEMP CI-budget kill-switch — see the SKIP_HEAVY_CI note at the top of
# this file. When the repo variable SKIP_HEAVY_CI == 'true', skip this
# >1-min job to conserve GitHub Actions minutes. Default (var unset) → runs.
Expand Down Expand Up @@ -472,7 +518,18 @@ jobs:
# and GHA does not auto-cancel sibling jobs). See
# chore_pr_yml_parallelize_backend_job/idea.md (Win-1 residual).

- name: pytest (unit + integration + contract) with coverage
- name: pytest (contract + integration) with coverage
# Unit tests moved to the `backend-unit` lane (which runs in parallel
# with this job under `-n auto`). `--cov-report=` (empty) suppresses
# the immediate report — the final combined report + 80% gate run in
# `backend-cov-gate` after both lanes upload their data files.
# `--cov-fail-under=0` disables pytest-cov's per-lane fail_under
# check (the contract+integration lane alone measures at ~61% without
# the unit lane's contribution; the 80% gate fires in
# `backend-cov-gate` against the combined coverage). `COVERAGE_FILE`
# writes to `.coverage.heavy` (vs the unit lane's `.coverage.unit`)
# so the two artifacts coexist in `backend-cov-gate`'s cwd at
# combine time without colliding.
env:
DATABASE_URL_FILE: ${{ runner.temp }}/database_url
POSTGRES_PASSWORD_FILE: ${{ runner.temp }}/postgres_password
Expand All @@ -487,30 +544,22 @@ jobs:
# The health integration test will skip without a running API; that
# behavior is acceptable until the deploy workflow exists.
RELYLOOP_API_URL: http://localhost:8000
COVERAGE_FILE: .coverage.heavy
run: |
# CI-perf #3 (pytest-xdist `-n auto`) was attempted on the first
# PR #291 CI run and reverted: the integration test layer hit FK
# collisions (query_sets_cluster_id_fkey violation when parallel
# tests held a FK reference to a cluster being deleted in another
# worker's teardown). pytest-xdist remains in dev deps for local
# opt-in (`pytest -n auto` works fine on the unit-test layer).
# A follow-up may split this job into a parallel-safe
# "unit + contract" lane (`-n auto`) + a serial "integration" lane
# (with a `coverage combine` merge/gate job) to recover the
# unit+contract portion's runtime while keeping integration serial.
# See planned_features/02_mvp2/infra_pr_yml_split_backend_test_lanes/idea.md.
uv run pytest backend/tests/ \
uv run pytest backend/tests/contract backend/tests/integration \
--cov=backend \
--cov-report=xml \
--cov-report=term-missing
--cov-report= \
--cov-fail-under=0

- name: Upload coverage XML
- name: Upload partial coverage data (heavy lane)
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-xml
path: coverage.xml
if-no-files-found: warn
name: coverage-data-heavy
path: .coverage.heavy*
if-no-files-found: error
include-hidden-files: true
retention-days: 1

- name: Verify source-of-truth enum comments (feat_studies_ui Story 4.2)
# Fails when a backend Literal / frozenset gains or loses a value but
Expand All @@ -537,6 +586,87 @@ jobs:
# smoke-test job runs on a fresh runner so it never sees that path).
run: bash scripts/ci/verify_install_builds_all_services.sh

backend-cov-gate:
# Coverage combine + 80% gate. Downloads partial `.coverage.*` data
# files from `backend-unit` (parallel unit lane) and `backend-heavy`
# (serial contract + integration lane), merges them via
# `coverage combine`, and fails the build if combined coverage is
# below the 80% threshold set in pyproject.toml's
# [tool.coverage.report] fail_under = 80.
#
# Win 2′ of infra_pr_yml_split_backend_test_lanes. The single tradeoff
# vs the previous all-in-one `backend (tests + coverage)` job: the gate
# is reported in its own job rather than inline with the test output,
# so a developer hunting a coverage regression in PR-checks UI clicks
# one more level to see the missing-line report. Acceptable for the
# ~1.5 min wall-clock recovery.
name: backend (coverage gate)
if: ${{ vars.SKIP_HEAVY_CI != 'true' }}
needs: [backend-unit, backend-heavy]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
version: "0.5.7"

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"

- name: Install project deps (frozen)
# Need coverage.py + the project source tree (for the source path
# mapping in [tool.coverage.paths]). `uv sync` is the simplest way
# to materialize both.
run: uv sync --frozen

- name: Download partial coverage data (unit lane)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
with:
name: coverage-data-unit
# All `.coverage.*` files land in the workspace root so
# `coverage combine` picks them up.
path: .

- name: Download partial coverage data (heavy lane)
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
with:
name: coverage-data-heavy
path: .

- name: Combine coverage data files
# `coverage combine` merges every file starting with `.coverage.`
# in the cwd into a single `.coverage` database. In our setup
# that's `.coverage.unit` (from backend-unit) and `.coverage.heavy`
# (from backend-heavy), downloaded by the two preceding steps.
# The [tool.coverage.paths] mapping in pyproject.toml maps per-runner
# absolute paths (e.g. /home/runner/work/relyloop/relyloop/backend)
# back to the canonical `backend/` source so the combined report
# deduplicates correctly across runners.
run: uv run coverage combine

- name: Coverage report (--fail-under=80)
# `fail_under = 80` is set in pyproject.toml's [tool.coverage.report]
# — `coverage report` honors it and exits non-zero if combined
# coverage drops below.
run: uv run coverage report --show-missing

- name: Generate coverage XML (for downstream tooling)
run: uv run coverage xml

- name: Upload combined coverage XML
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-xml
path: coverage.xml
if-no-files-found: warn

frontend:
name: frontend (lint + typecheck + tests + build)
# TEMP CI-budget kill-switch — see the SKIP_HEAVY_CI note at top of file.
Expand Down
2 changes: 1 addition & 1 deletion docs/00_overview/BACKLOG_DASHBOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# RelyLoop BACKLOG Dashboard

_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`backlog_dashboard.html`](backlog_dashboard.html) in a browser._
_Reflects feature-folder state as of **2026-06-16** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`backlog_dashboard.html`](backlog_dashboard.html) in a browser._

## Next up

Expand Down
2 changes: 1 addition & 1 deletion docs/00_overview/DASHBOARD.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RelyLoop — Release Roadmap

_Top-level index across MVP1 → GA v1+ as of **2026-06-09**. Click a release name to drill into the per-release dashboard. Theme labels sourced from [`docs/01_architecture/tech-stack.md` §"Canonical release matrix"](../01_architecture/tech-stack.md). For the rich local view, open [`dashboard.html`](dashboard.html) in a browser._
_Top-level index across MVP1 → GA v1+ as of **2026-06-16**. Click a release name to drill into the per-release dashboard. Theme labels sourced from [`docs/01_architecture/tech-stack.md` §"Canonical release matrix"](../01_architecture/tech-stack.md). For the rich local view, open [`dashboard.html`](dashboard.html) in a browser._

## Releases

Expand Down
2 changes: 1 addition & 1 deletion docs/00_overview/GA_DASHBOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# RelyLoop GA Dashboard

_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`ga_dashboard.html`](ga_dashboard.html) in a browser._
_Reflects feature-folder state as of **2026-06-16** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`ga_dashboard.html`](ga_dashboard.html) in a browser._

## Next up

Expand Down
2 changes: 1 addition & 1 deletion docs/00_overview/MVP1_DASHBOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# RelyLoop MVP1 Dashboard

_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`mvp1_dashboard.html`](mvp1_dashboard.html) in a browser._
_Reflects feature-folder state as of **2026-06-16** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`mvp1_dashboard.html`](mvp1_dashboard.html) in a browser._

## Next up

Expand Down
Loading
Loading