Skip to content

Commit fdbd486

Browse files
ci: scope CI to the test suites (pytest + frontend); lint gate is a follow-up
Per owner direction (#452): ship the test gate now, fix the linters next. The backend job runs `poetry run pytest` rather than `scripts/lint.sh`. Reason: the local lint gate isn't satisfiable on a clean tree — it runs two formatters (`ruff format` + `black`) whose output mutually conflicts, plus a little flake8/pylint debt. The #401/#451 incident that motivated #452 was a *test* regression, and the test suites pass green, so CI enforces those today; wiring the (de-conflicted) Python lint gate into CI is a tracked follow-up. Drops the pre-commit install (only the lint gate needed it). Reconciles SECURITY.md §8, the codeql/lint.sh/pre-commit/workflows-README comments, and the decision logs (PM/security/architect, ACCEPTANCE Q-4) to "tests in CI; lint gate local + CI follow-up". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 73e652f commit fdbd486

11 files changed

Lines changed: 89 additions & 77 deletions

File tree

.github/workflows/README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ GitHub Actions workflows for django-admin-react.
44

55
## What lives here
66

7-
- **`ci.yml`** — the lint + test gate on every PR and push to `main`:
8-
backend (`scripts/lint.sh` → ruff/black/isort/flake8/pylint/mypy/bandit
9-
+ the pre-commit security hooks + `pytest`) and frontend
10-
(`pnpm -r typecheck`, `pnpm lint`, `pnpm test`, `pnpm -r build`). A red
11-
suite blocks merge. Added in #452 (reversing the prior local-only
12-
posture). Marking the checks **required** is an owner branch-protection
13-
action (#452 / #331).
7+
- **`ci.yml`** — the test gate on every PR and push to `main`: backend
8+
`pytest` (with coverage) and the frontend `pnpm` gate (`-r typecheck`,
9+
`lint`, `test`, `-r build`). A red suite blocks merge. Added in #452
10+
(reversing the prior local-only posture). The Python *lint* gate
11+
(`scripts/lint.sh`) is not in CI yet — it runs two formatters that
12+
conflict, so it's de-conflicted first (follow-up off #452). Marking the
13+
checks **required** is an owner branch-protection action (#452 / #331).
1414
- **`codeql.yml`** — CodeQL static analysis (Python + JS/TS) on push/PR and a
1515
weekly schedule. This is the project's security dataflow scanner.
1616
- **`release.yml`** — automated PyPI publishing. Triggered when a GitHub
@@ -23,11 +23,11 @@ GitHub Actions workflows for django-admin-react.
2323

2424
## What does not belong here
2525

26-
- The *definition* of the quality gate. `ci.yml` does not duplicate the
27-
tool list — the backend job runs `scripts/lint.sh` and the frontend job
28-
runs the package.json scripts, so the gate has one source of truth. See
29-
`SECURITY.md` §8 for the local + CI posture and #331 for the forward
30-
hardening plan (SHA-pinning is already done; required-checks /
26+
- The Python *lint* gate (`scripts/lint.sh` + `.pre-commit-config.yaml`).
27+
CI runs `pytest` and the frontend `pnpm` gate; the Python lint gate
28+
isn't wired into CI yet (it must be de-conflicted first — two formatters
29+
disagree). See `SECURITY.md` §8 and #331 for the forward hardening plan
30+
(SHA-pinning is already done; lint-in-CI / required-checks /
3131
version-matrix remain).
3232
- Secrets. Workflows authenticate via OIDC (release) or the default
3333
`GITHUB_TOKEN` (CodeQL). No long-lived tokens are stored as secrets.

.github/workflows/ci.yml

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
1-
# The lint + test gate, run on every pull request and on push to `main`.
1+
# CI test gate — run the test suites on every pull request and on push to
2+
# `main`, so a red suite can't merge.
23
#
34
# WHY: until now the lint/test pipeline was local-only — `scripts/lint.sh`
45
# + the pre-commit hooks, run on a contributor's laptop before the Merger
56
# pulled a PR (the deliberate "no CI in pre-alpha" posture). With many
67
# agents merging in parallel and only CodeQL gating server-side, test
78
# regressions slipped onto `main` green (e.g. #401 broke
89
# `tests/test_logentry.py`, undetected until a later full local run —
9-
# #451). This workflow makes the gate enforced server-side so a red suite
10-
# can't merge. Resolves #452; the no-CI revisit recorded in
10+
# #451). This enforces the suites server-side. Resolves the core of #452
11+
# (run the test suites in CI); the no-CI revisit is recorded in
1112
# `SECURITY.md` §8 / `docs/agents/decisions.md` / OQ-A-001.
1213
#
13-
# SOURCE OF TRUTH: the backend job runs `scripts/lint.sh` itself (with
14-
# `LINT_PY_ONLY=1`) so CI runs the *exact* gate a developer runs locally —
15-
# including the pre-commit security hooks (bandit + the house-rule pygrep
16-
# checks: no-objects-all, no-csrf-exempt, no-user-has-perm,
17-
# no-dar-api-from-pages, no-partial-tokens). The frontend job mirrors
18-
# #452's acceptance explicitly (typecheck + lint + test + build), because
19-
# `scripts/lint.sh`'s frontend block intentionally stops at lint and does
20-
# not run vitest or the Vite build.
14+
# SCOPE: the backend job runs `pytest` (the regression that motivated
15+
# #452 was a broken test). The frontend job runs the full `pnpm` gate —
16+
# typecheck + lint + test + build — which is already self-consistent and
17+
# green. Enforcing the *Python lint* gate in CI is a deliberate near-term
18+
# follow-up: `scripts/lint.sh` currently runs two formatters (`ruff
19+
# format` + `black`) whose output conflicts, so it is not yet satisfiable
20+
# on a clean tree. That gets de-conflicted and the small existing lint
21+
# debt cleared first (follow-up off #452), then the lint step is added
22+
# here.
2123
#
2224
# SECURITY POSTURE:
2325
# - Least-privilege: top-level `contents: read`; no job needs write.
@@ -48,7 +50,7 @@ concurrency:
4850

4951
jobs:
5052
backend:
51-
name: Backend (lint + pytest)
53+
name: Backend (pytest)
5254
runs-on: ubuntu-latest
5355
steps:
5456
- name: Checkout
@@ -59,23 +61,22 @@ jobs:
5961
with:
6062
python-version: "3.12"
6163

62-
- name: Install Poetry + pre-commit
64+
- name: Install Poetry
6365
# Poetry is pinned to the version that generated poetry.lock (see
6466
# its header). `poetry install` compares the lock's content-hash
6567
# against the one it computes from pyproject; a different Poetry
6668
# can compute it differently and reject an otherwise-valid lock.
6769
# Bump this in lockstep whenever the lock is regenerated.
68-
run: pipx install poetry==2.1.4 && pipx install pre-commit
70+
run: pipx install poetry==2.1.4
6971

7072
- name: Install dependencies (locked)
7173
run: poetry install --no-interaction
7274

73-
# Runs the same gate as a local `LINT_PY_ONLY=1 bash scripts/lint.sh`:
74-
# pre-commit (bandit + the security pygrep hooks) then ruff / ruff
75-
# format / black / isort / flake8 / pylint -E / mypy (package,
76-
# blocking) / bandit / pytest (with coverage, per pyproject addopts).
77-
- name: Python gate (scripts/lint.sh)
78-
run: LINT_PY_ONLY=1 bash scripts/lint.sh
75+
# pytest with coverage (per pyproject `addopts`), including
76+
# tests/test_security.py. `filterwarnings = ["error"]` means a new
77+
# warning fails the run.
78+
- name: Tests (pytest)
79+
run: poetry run pytest
7980

8081
frontend:
8182
name: Frontend (typecheck + lint + test + build)

.github/workflows/codeql.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
#
33
# Dataflow-based security analysis (Python + TypeScript), surfacing
44
# findings in the repository's Security tab where external reporters and
5-
# maintainers both look. Free for public repos. It complements the lint +
6-
# test gate in `ci.yml` (and the local `scripts/lint.sh`): `bandit` /
5+
# maintainers both look. Free for public repos. It complements the test
6+
# gate in `ci.yml` and the local lint gate (`scripts/lint.sh`): `bandit` /
77
# `ruff -S` (Python) and `eslint` (frontend) are rule-based; CodeQL adds
88
# dataflow detection (injection, path traversal, unsafe deserialization)
99
# those linters can miss. Added in #144 (post-public-flip hardening);
10-
# the lint/test CI gate followed in #452.
10+
# the test CI gate followed in #452.
1111
name: CodeQL
1212

1313
on:

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
# Anything below the [BLOCK] threshold in
99
# `docs/agents/security-expert/REVIEW_CHECKLIST.md` §1 aborts the commit.
1010
#
11-
# This file is the commit-time half of the quality gate. The same hooks
12-
# run server-side in CI (`.github/workflows/ci.yml` invokes
13-
# `scripts/lint.sh`, which runs `pre-commit run --all-files`), so a
14-
# developer who disables this local hook still hits the gate on their PR.
11+
# This file is the commit-time half of the quality gate, run together
12+
# with `scripts/lint.sh` before a PR. CI (`.github/workflows/ci.yml`)
13+
# currently runs the test suites, not these hooks; wiring the lint/security
14+
# hooks into CI is a follow-up (#452).
1515

1616
repos:
1717
# -------------------------------------------------------------------

ACCEPTANCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ Merger runs the pipeline locally before squash-merge.
436436
| Q-1 | `./scripts/lint.sh` is the merge gate. A merge that bypassed it is a policy violation and a revert candidate. | Forum status / PR body. |
437437
| Q-2 | `./scripts/build.sh` is the release gate. Both the SPA build and `poetry build` must succeed. | Run output before tagging. |
438438
| Q-3 | A PR that touches `pyproject.toml` runtime deps, frontend root `package.json` deps, or `LICENSE` includes a locally-recorded `pip-audit` (Python) and `pnpm audit` (npm) clean run in the PR body. | PR-body inspection. |
439-
| Q-4 | The "no CI" decision was revisited and reversed before leaving pre-alpha: the lint + test gate now runs in CI (`.github/workflows/ci.yml`), recorded in [`docs/agents/decisions.md`](docs/agents/decisions.md). Marking the checks required in branch protection is the remaining owner action (#452). | Decision-log entry + `ci.yml`. |
439+
| Q-4 | The "no CI" decision was revisited and reversed before leaving pre-alpha: the test suites now run in CI (`.github/workflows/ci.yml` — backend `pytest` + frontend gate), recorded in [`docs/agents/decisions.md`](docs/agents/decisions.md). Wiring the Python lint gate into CI and marking checks required in branch protection are the remaining follow-ups (#452). | Decision-log entry + `ci.yml`. |
440440

441441
### 3.8 Packaging
442442

SECURITY.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,19 @@ Every endpoint added must include all of these tests before merging:
176176

177177
## 8. Static analysis (local + CI)
178178

179-
The gate runs in two places, both required: locally for fast feedback
180-
(`./scripts/lint.sh` + the pre-commit hooks) **and** server-side in CI
181-
(`.github/workflows/ci.yml`) so a red suite cannot merge. The earlier
182-
"local-only, no CI in v0.x" posture was revisited and reversed (issue
183-
#452 — regressions were slipping onto `main` under CodeQL-only gating;
184-
see `docs/agents/decisions.md`). The CI backend job runs `scripts/lint.sh`
185-
directly, so the two stay in lockstep by construction.
179+
The earlier "local-only, no CI in v0.x" posture was revisited and
180+
reversed (issue #452 — regressions were slipping onto `main` under
181+
CodeQL-only gating; see `docs/agents/decisions.md`). **The test suites
182+
now run server-side in CI** (`.github/workflows/ci.yml`): backend
183+
`pytest` and the frontend `pnpm` gate (typecheck + lint + test + build),
184+
so a red suite cannot merge.
185+
186+
Enforcing the **Python lint gate** in CI is a near-term follow-up:
187+
`scripts/lint.sh` currently runs two formatters (`ruff format` + `black`)
188+
whose output conflicts, so it isn't satisfiable on a clean tree yet. The
189+
local script below is still the authoritative lint gate; it gets
190+
de-conflicted and the small existing debt cleared first, then the lint
191+
step is added to CI.
186192

187193
Run via `./scripts/lint.sh`:
188194

@@ -198,9 +204,8 @@ Run via `./scripts/lint.sh`:
198204
- Frontend: `pnpm -r typecheck`, `pnpm lint` (eslint `--max-warnings 0`
199205
+ stylelint + dark-mode coverage), `pnpm test` (vitest), `pnpm -r build`.
200206

201-
CI additionally runs the pre-commit security hooks (bandit + the
202-
house-rule pygrep checks) on every PR. Making the CI checks **required**
203-
in branch protection is a separate owner action (#452 / #331).
207+
Making the CI checks **required** in branch protection is a separate
208+
owner action (#452 / #331).
204209

205210
Dependency audit runs separately via `./scripts/audit-deps.sh`
206211
(see §6).

docs/agents/decisions.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,25 @@ Newest decisions on top.
77

88
---
99

10-
## 2026-05-28 — Reverse "no CI": the lint + test gate now runs server-side
10+
## 2026-05-28 — Reverse "no CI": the test suites now run server-side
1111

1212
The prior "no CI in pre-alpha (per repo-owner direction)" posture was
1313
revisited (the trigger named in OQ-A-001 / ACCEPTANCE Q-4) and reversed.
1414
Under CodeQL-only gating, test regressions were merging onto `main` green
1515
with many agents working in parallel (e.g. #401 broke
1616
`tests/test_logentry.py`, caught only on a later local run — #451).
1717

18-
- **CI now runs the full gate on every PR + push to `main`**
19-
(`.github/workflows/ci.yml`): backend job runs `scripts/lint.sh` itself
20-
(so local ≡ CI, incl. the pre-commit security hooks + `pytest`),
21-
frontend job runs `pnpm -r typecheck` / `pnpm lint` / `pnpm test` /
22-
`pnpm -r build`. Actions SHA-pinned; least-privilege `contents: read`.
23-
— issue #452, ci.yml. **Tier 5 (workflows + SECURITY.md §8) —
24-
human-reviewed.**
25-
- Still outstanding (owner action, tracked in #452 / #331): mark the CI
26-
checks **required** in branch protection; an optional Python/Django
27-
version matrix.
18+
- **CI now runs the test suites on every PR + push to `main`**
19+
(`.github/workflows/ci.yml`): backend `pytest` (with coverage) and the
20+
frontend gate (`pnpm -r typecheck` / `pnpm lint` / `pnpm test` /
21+
`pnpm -r build`). Actions SHA-pinned; least-privilege `contents: read`.
22+
— issue #452, ci.yml. Owner-directed merge despite Tier 5 (workflows +
23+
SECURITY.md §8).
24+
- **Follow-ups** (off #452 / #331): wire the Python *lint* gate into CI —
25+
blocked first on de-conflicting `scripts/lint.sh`, which runs two
26+
formatters (`ruff format` + `black`) whose output conflicts, plus a
27+
little pylint/flake8 debt to clear; mark the CI checks **required** in
28+
branch protection; optional Python/Django version matrix.
2829

2930
---
3031

docs/agents/product-manager/DECISIONS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ Authoring session: `claude-pm-ux-opus47`.
6060
11. **Lighthouse-equivalent budget:** FCP < 1.5 s cached, LCP < 2.5 s
6161
cold on the reference profile (M-class laptop, 10 Mbps).
6262
[`PRODUCT_VISION.md`](../../PRODUCT_VISION.md) §7
63-
12. **No `--force` push to `main`.** The lint + test gate runs both
64-
locally (`scripts/lint.sh`) and in CI (`.github/workflows/ci.yml`,
65-
added in #452 — the prior "no CI" stance was revisited and reversed).
63+
12. **No `--force` push to `main`.** The test suites run in CI
64+
(`.github/workflows/ci.yml`, added in #452 — the prior "no CI" stance
65+
was reversed); the lint gate (`scripts/lint.sh`) runs locally, with
66+
lint-in-CI a follow-up.
6667
[`docs/agents/decisions.md`](../../docs/agents/decisions.md) (engineering)
6768
13. **`docs/agents/` handoff convention adopted** — durable role state
6869
survives session loss. — [`docs/agents/decisions.md`](../DECISIONS.md)

docs/agents/security-expert/DECISIONS.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ agents see it without reading this folder.
3131
- **CSRF is mandatory.** No view in the package is `@csrf_exempt`.
3232
Tests must assert that a missing or invalid `X-CSRFToken` header
3333
on `POST` / `PATCH` / `DELETE` returns `403`. — invariant
34-
- **The gate runs locally and in CI.** `scripts/lint.sh` is the local
35-
gate; `.github/workflows/ci.yml` runs that same script (plus the
36-
frontend gate) server-side on every PR so a red suite can't merge
37-
(#452 — the earlier "no CI" stance was reversed). Security scans
38-
(`ruff S`, `bandit`, the pre-commit secret/pygrep hooks) run in both;
39-
`pip-audit` stays a local pre-merge step (§6). — #452
34+
- **Tests run in CI; the lint gate stays local (for now).**
35+
`.github/workflows/ci.yml` runs `pytest` + the frontend gate
36+
server-side on every PR so a red suite can't merge (#452 — the earlier
37+
"no CI" stance was reversed). The Python lint/security gate
38+
(`scripts/lint.sh`: `ruff S`, `bandit`, the pre-commit secret/pygrep
39+
hooks) plus `pip-audit` stay local pre-merge steps until the gate is
40+
de-conflicted and wired into CI (follow-up off #452). — #452
4041
- **Frontend never holds permission state alone.** `@dar/data` may
4142
cache `permissions: {view, add, change, delete}` from the API for
4243
UI courtesy (hide buttons), but **every** write call re-verifies

docs/agents/software-architect/DECISIONS.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99

1010
## 2026-05-28
1111

12-
- **OQ-A-001 resolved — "no CI" reversed.** The lint + test gate now
13-
runs server-side on every PR (`.github/workflows/ci.yml`); the backend
14-
job runs `scripts/lint.sh` so local ≡ CI. Trigger: regressions slipping
15-
onto `main` under CodeQL-only gating (#401 / #451). Cross-role record:
12+
- **OQ-A-001 resolved — "no CI" reversed.** The test suites now run
13+
server-side on every PR (`.github/workflows/ci.yml`): backend `pytest`
14+
+ the frontend gate. Trigger: regressions slipping onto `main` under
15+
CodeQL-only gating (#401 / #451). Cross-role record:
1616
[`docs/agents/decisions.md`](../../docs/agents/decisions.md)
17-
(2026-05-28). — #452. Remaining owner action: required-checks branch
17+
(2026-05-28). — #452. Follow-ups: wire the Python lint gate into CI
18+
(de-conflict `ruff format`/`black` first), required-checks branch
1819
protection.
1920

2021
---

0 commit comments

Comments
 (0)