Skip to content

Commit 11408f9

Browse files
MartinCastroAlvarezmartin-castro-laminr-aiclaude
authored
ci: run the test suites (pytest + frontend) on every PR (#452) (#506)
* ci: run the lint + test gate on every PR (#452) CodeQL was the only server-side check, so test regressions merged onto `main` green — e.g. #401 broke tests/test_logentry.py, caught only on a later local run (#451). With many agents merging in parallel under CodeQL-only gating this keeps happening. Add `.github/workflows/ci.yml`: a backend job that runs scripts/lint.sh itself (LINT_PY_ONLY=1 — so local ≡ CI, including the pre-commit security hooks + pytest) and a frontend job (pnpm -r typecheck / pnpm lint / pnpm test / pnpm -r build). Actions SHA-pinned; least-privilege `contents: read`; no secrets, no id-token. This reverses the documented "no CI in pre-alpha (per repo-owner direction)" posture (the revisit named in OQ-A-001 / ACCEPTANCE Q-4). Reconcile every reference in the same change: SECURITY.md §8, the codeql.yml / lint.sh / pre-commit / workflows-README comments, the PM / security / architect decision logs, REVIEW_CHECKLIST, ACCEPTANCE Q-4, and a new ADR in docs/agents/decisions.md; OQ-A-001 is resolved + removed. Tier 5 (.github/workflows + SECURITY.md) — human review required; do not auto-merge. Marking the checks *required* in branch protection is a separate owner action (#452 / #331). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: refresh poetry.lock content-hash + pin Poetry so install works The first CI run surfaced a pre-existing problem on `main`: a fresh `poetry install` fails with "pyproject.toml changed significantly since poetry.lock was last generated" — pyproject was edited after the lock was last written, so its content-hash no longer matches. Local dev never hit this (an existing venv + `poetry run`), but a clean checkout (CI, new contributors) can't install. Regenerating the lock changes ONLY the content-hash line — zero package versions move — so this is a no-op for resolved dependencies. Pin CI to Poetry 2.1.4 (the lock's generator) so the hash check is deterministic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 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> --------- Co-authored-by: Martin Castro Laminrs <mcastro@laminr.ai> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8dda5a5 commit 11408f9

14 files changed

Lines changed: 216 additions & 51 deletions

File tree

.github/workflows/README.md

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

55
## What lives here
66

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).
714
- **`codeql.yml`** — CodeQL static analysis (Python + JS/TS) on push/PR and a
815
weekly schedule. This is the project's security dataflow scanner.
916
- **`release.yml`** — automated PyPI publishing. Triggered when a GitHub
@@ -16,10 +23,12 @@ GitHub Actions workflows for django-admin-react.
1623

1724
## What does not belong here
1825

19-
- The local quality gate (`ruff`/`black`/`bandit`/`pytest`/`pnpm lint`) lives
20-
in `scripts/lint.sh` and `.pre-commit-config.yaml`, not in CI — see
21-
`SECURITY.md` §8 for the (deliberately) local-only posture and issue #331
22-
for the forward CI-hardening plan.
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 /
31+
version-matrix remain).
2332
- Secrets. Workflows authenticate via OIDC (release) or the default
2433
`GITHUB_TOKEN` (CodeQL). No long-lived tokens are stored as secrets.
2534

.github/workflows/ci.yml

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.
3+
#
4+
# WHY: until now the lint/test pipeline was local-only — `scripts/lint.sh`
5+
# + the pre-commit hooks, run on a contributor's laptop before the Merger
6+
# pulled a PR (the deliberate "no CI in pre-alpha" posture). With many
7+
# agents merging in parallel and only CodeQL gating server-side, test
8+
# regressions slipped onto `main` green (e.g. #401 broke
9+
# `tests/test_logentry.py`, undetected until a later full local run —
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
12+
# `SECURITY.md` §8 / `docs/agents/decisions.md` / OQ-A-001.
13+
#
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.
23+
#
24+
# SECURITY POSTURE:
25+
# - Least-privilege: top-level `contents: read`; no job needs write.
26+
# - All third-party actions are pinned to a full commit SHA (a tag can
27+
# be moved, a SHA cannot) — consistent with codeql.yml / release.yml
28+
# and the supply-chain hardening in #331.
29+
# - This is a *gate*, not a publisher: it has no access to PyPI, no
30+
# `id-token`, and no stored secrets.
31+
#
32+
# NOTE: making these checks *required* (branch protection) is a separate
33+
# owner action in repo Settings → Branches — see #452 / #331.
34+
name: ci
35+
36+
on:
37+
push:
38+
branches: [main]
39+
pull_request:
40+
branches: [main]
41+
42+
permissions:
43+
contents: read
44+
45+
# A newer push to the same ref supersedes an in-flight run — don't burn
46+
# minutes finishing a run whose commit is already stale.
47+
concurrency:
48+
group: ci-${{ github.workflow }}-${{ github.ref }}
49+
cancel-in-progress: true
50+
51+
jobs:
52+
backend:
53+
name: Backend (pytest)
54+
runs-on: ubuntu-latest
55+
steps:
56+
- name: Checkout
57+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
58+
59+
- name: Set up Python
60+
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
61+
with:
62+
python-version: "3.12"
63+
64+
- name: Install Poetry
65+
# Poetry is pinned to the version that generated poetry.lock (see
66+
# its header). `poetry install` compares the lock's content-hash
67+
# against the one it computes from pyproject; a different Poetry
68+
# can compute it differently and reject an otherwise-valid lock.
69+
# Bump this in lockstep whenever the lock is regenerated.
70+
run: pipx install poetry==2.1.4
71+
72+
- name: Install dependencies (locked)
73+
run: poetry install --no-interaction
74+
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
80+
81+
frontend:
82+
name: Frontend (typecheck + lint + test + build)
83+
runs-on: ubuntu-latest
84+
defaults:
85+
run:
86+
working-directory: frontend
87+
steps:
88+
- name: Checkout
89+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
90+
91+
- name: Set up pnpm
92+
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
93+
with:
94+
version: 9
95+
96+
- name: Set up Node
97+
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
98+
with:
99+
node-version: 22
100+
cache: pnpm
101+
cache-dependency-path: frontend/pnpm-lock.yaml
102+
103+
- name: Install dependencies (frozen lockfile)
104+
run: pnpm install --frozen-lockfile
105+
106+
- name: Typecheck (tsc --noEmit, every package)
107+
run: pnpm -r typecheck
108+
109+
- name: Lint (eslint --max-warnings 0 + stylelint + dark-mode coverage)
110+
run: pnpm lint
111+
112+
- name: Unit tests (vitest)
113+
run: pnpm test
114+
115+
- name: Build (Vite bundle, every package)
116+
run: pnpm -r build

.github/workflows/codeql.yml

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
# CodeQL static analysis.
22
#
3-
# This is the ONE CI workflow the package ships (SECURITY.md §8 keeps
4-
# the lint/test pipeline local-only for v0.x; CodeQL is the documented
5-
# exception — issue #144, post-public-flip hardening). It runs GitHub's
6-
# semantic code analysis over the Python package and the TypeScript
7-
# frontend, surfacing findings in the repository's Security tab where
8-
# external reporters and maintainers both look. Free for public repos.
9-
#
10-
# It complements, not replaces, the local gates: `bandit` / `ruff -S`
11-
# (Python) and `eslint` (frontend) still run via `scripts/lint.sh`;
12-
# CodeQL adds dataflow-based detection (injection, path traversal,
13-
# unsafe deserialization) those rule-based linters can miss.
3+
# Dataflow-based security analysis (Python + TypeScript), surfacing
4+
# findings in the repository's Security tab where external reporters and
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` /
7+
# `ruff -S` (Python) and `eslint` (frontend) are rule-based; CodeQL adds
8+
# dataflow detection (injection, path traversal, unsafe deserialization)
9+
# those linters can miss. Added in #144 (post-public-flip hardening);
10+
# the test CI gate followed in #452.
1411
name: CodeQL
1512

1613
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 part of the local-only quality gate. There is no CI
12-
# in this repo by design (repo-owner direction); a developer who
13-
# disables this hook still has to clear `scripts/lint.sh` before the
14-
# Merger will pull 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 is revisited before leaving pre-alpha and recorded in [`docs/agents/decisions.md`](docs/agents/decisions.md). | Decision-log entry. |
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: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,21 @@ Every endpoint added must include all of these tests before merging:
174174
- TestPyPI may be used for verification by the maintainer with a
175175
separate token; same hygiene rules apply.
176176

177-
## 8. Static analysis (local-only — no CI in v0.x)
177+
## 8. Static analysis (local + CI)
178+
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.
178192

179193
Run via `./scripts/lint.sh`:
180194

@@ -187,8 +201,11 @@ Run via `./scripts/lint.sh`:
187201
- `mypy` (best-effort; tightening planned for v1.x)
188202
- `bandit -r django_admin_react`
189203
- `pytest -q` (including `tests/test_security.py`)
190-
- Frontend: `prettier --check`, `pnpm -r typecheck`, `pnpm -r lint`
191-
(`eslint` wires up in PR #6).
204+
- Frontend: `pnpm -r typecheck`, `pnpm lint` (eslint `--max-warnings 0`
205+
+ stylelint + dark-mode coverage), `pnpm test` (vitest), `pnpm -r build`.
206+
207+
Making the CI checks **required** in branch protection is a separate
208+
owner action (#452 / #331).
192209

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

docs/agents/decisions.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ Newest decisions on top.
77

88
---
99

10+
## 2026-05-28 — Reverse "no CI": the test suites now run server-side
11+
12+
The prior "no CI in pre-alpha (per repo-owner direction)" posture was
13+
revisited (the trigger named in OQ-A-001 / ACCEPTANCE Q-4) and reversed.
14+
Under CodeQL-only gating, test regressions were merging onto `main` green
15+
with many agents working in parallel (e.g. #401 broke
16+
`tests/test_logentry.py`, caught only on a later local run — #451).
17+
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.
29+
30+
---
31+
1032
## 2026-05-27 — Ship a concrete recommended CSP (QSEC-03 resolved)
1133

1234
Security lane (`claude-security-opus47-2026-05-27`). The SPA shell loads

docs/agents/product-manager/DECISIONS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +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`. No CI/CD (per repo-owner direction).**
64-
Local linters via `scripts/lint.sh` are the gate.
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.
6567
[`docs/agents/decisions.md`](../../docs/agents/decisions.md) (engineering)
6668
13. **`docs/agents/` handoff convention adopted** — durable role state
6769
survives session loss. — [`docs/agents/decisions.md`](../DECISIONS.md)

docs/agents/security-expert/DECISIONS.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +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-
- **No CI in v1.** The Merger runs `scripts/lint.sh` locally; security
35-
scans (`ruff S`, `bandit`, `pip-audit`, secret grep) are part of
36-
that script. Acceptance criteria below treat the local pipeline as
37-
authoritative until CI is reintroduced. — repo-owner directive
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
3841
- **Frontend never holds permission state alone.** `@dar/data` may
3942
cache `permissions: {view, add, change, delete}` from the API for
4043
UI courtesy (hide buttons), but **every** write call re-verifies

docs/agents/security-expert/REVIEW_CHECKLIST.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,8 @@ human takes it from there.
204204

205205
- "We'll add tests in a follow-up PR."
206206
- "It's only a refactor, no review needed."
207-
- "CI will catch it." (We have no CI.)
207+
- "CI will catch it." (CI runs the gate (#452), but a green pipeline is
208+
the floor, not the review — dataflow/logic bugs still need human eyes.)
208209
- "The frontend handles it." (Defense in depth; backend must enforce.)
209210
- "It's behind staff auth." (Necessary, not sufficient — the
210211
backend still must obey `ModelAdmin`.)

0 commit comments

Comments
 (0)