Skip to content

ci: run the test suites (pytest + frontend) on every PR (#452)#506

Merged
MartinCastroAlvarez merged 3 commits into
mainfrom
ci/test-gate-452
May 27, 2026
Merged

ci: run the test suites (pytest + frontend) on every PR (#452)#506
MartinCastroAlvarez merged 3 commits into
mainfrom
ci/test-gate-452

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

@MartinCastroAlvarez MartinCastroAlvarez commented May 27, 2026

Summary

CodeQL was the only server-side check, so test regressions merged onto main green — #401 broke tests/test_logentry.py, caught only on a later local run (#451). This adds CI that runs the test suites on every PR + push to main, so a red suite can't merge.

Closes the core of #452. Sub-task of #331.

Scope (per owner direction: ship CI now, fix linters next)

  • backendpoetry run pytest (with coverage). Verified: 532 passed locally.
  • frontendpnpm -r typecheckpnpm lintpnpm testpnpm -r build. Already green in CI.

Why not the Python lint gate yet? While building this, CI surfaced that scripts/lint.sh isn't satisfiable on a clean tree: it runs two formatters (ruff format + black) whose output mutually conflicts (after ruff format, black --check wants to undo it), plus a little flake8/pylint debt. The #401/#451 regression was a test break, and the suites are green, so CI enforces tests today. Wiring the (de-conflicted) lint gate into CI is the immediate follow-up — that's the "fix the linters in main next" task.

It also fixes a latent blocker: a fresh poetry install failed because poetry.lock's content-hash was stale vs pyproject.toml (local dev never hit it — existing venv). Regenerating touched only the hash line (zero version moves); Poetry is pinned to the lock's generator (2.1.4) for a deterministic check.

Security posture

Least-privilege contents: read; every action SHA-pinned (same pins as codeql.yml/release.yml); no id-token, no PyPI, no secrets.

Reconciled in this PR

SECURITY.md §8; the codeql.yml / scripts/lint.sh / .pre-commit-config.yaml / workflows-README comments; the PM/security/architect decision logs + decisions.md ADR; ACCEPTANCE.md Q-4; OQ-A-001 resolved/removed.

Owner follow-ups

  • De-conflict the lint gate (drop one formatter) + clear flake8/pylint debt, then add the lint step here.
  • Mark backend + frontend required in Settings → Branches.
  • Optional Python/Django version matrix.

🤖 Generated with Claude Code

Copy link
Copy Markdown
Owner Author

@MartinCastroAlvarez MartinCastroAlvarez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security + CI review — Reviewer lane (independent; did not author). Tier 5 → I am NOT merging; this is for the owner.

Design of ci.yml: excellent. Least-privilege contents: read, no id-token, no secrets, SHA-pinned actions, concurrency: cancel-in-progress, and — the right call — the backend job runs scripts/lint.sh itself rather than re-listing tools, so the gate keeps one source of truth. Docs (SECURITY.md §8, the DECISIONS files, READMEs) are updated in lockstep, and you correctly leave the required-checks flip as a separate owner branch-protection action. The intent (catch the test/lint regressions that slip through CodeQL-only gating, e.g. #401#451) is exactly right.

But it is not green, and the failure is informative — please do not merge or make required until it's resolved. The backend job fails, and I traced every failing hook. None is a real security violation, but the gate is red on main from day one:

The root cause: CI runs the pre-commit hooks with --all-files, whereas local runs hit only changed files. Several hooks are not --all-files-clean:

  1. no-csrf-exempt (pattern csrf_exempt) — all hits are docstrings that explain the rule ("No @csrf_exempt — Django's middleware enforces", in urls.py, create.py, auth.py, …). There is no actual @csrf_exempt anywhere in the package. Pure false positive.
  2. no-objects-all-in-api (pattern objects\.(all|filter)\() — hits the same rule-explaining docstrings plus one real line: recent_actions.py:67 LogEntry.objects.filter(user__pk=user_pk) (the #502 feed, merged). That query is correct — it's the audit-log, scoped strictly to request.user, the same shape as Django's own get_admin_log; rule 10's "start from ModelAdmin.get_queryset" doesn't apply to LogEntry (no consumer ModelAdmin owns it). The hook just can't tell.
  3. no-partial-tokens — matches a token prefix string (ghp_/AKIA…/BEGIN … PRIVATE) in a file that isn't on its exclude: list (likely one of the docs/DECISIONS this PR added that quotes the example). gitleaks (the real secret scanner) passed — there is no actual secret.
  4. ruff / ruff-format / black — these are the auto-fixing pre-commit variants; under --all-files they rewrite files and report "Failed" on any pre-existing drift. CI should run them in check mode (ruff format --check, black --check) so they report without mutating.

Confirmed clean (no action): no real CSRF exemption; recent_actions.py (#502) is staff-gated, Cache-Control: no-store, scoped to request.user.pk, and gates its deep-link on has_view_permission (_target_for, line 120). gitleaks green.

To make this gate green + safe to require, the pre-commit house-rule hooks need to be --all-files-safe (this is itself security tooling — Tier 5). Concretely:

  • no-csrf-exempt / no-objects-all-in-api: ignore comment/docstring lines (or add a scoped exclude: like no-partial-tokens already uses), and allow LogEntry.objects.* in recent_actions.py.
  • no-partial-tokens: add the doc/DECISIONS file(s) this PR touched to the exclude: list.
  • Run the formatters in --check mode in the CI path of scripts/lint.sh (or have CI call the check-only targets), so auto-fixers don't self-report failure.

I'm filing this hook-hardening as a discrete blocker issue and linking it here. Once those land and this job is green, this is a strong, mergeable hardening — the missing server-side enforcement #452/#331 call for.

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner Author

Filed the hook-hardening blocker as #514 (added to the board) — that needs to land before this job goes green / can be required.

@MartinCastroAlvarez MartinCastroAlvarez changed the title ci: run the lint + test gate on every PR (#452) ci: run the test suites (pytest + frontend) on every PR (#452) May 27, 2026
@MartinCastroAlvarez
Copy link
Copy Markdown
Owner Author

Heads up: this is now unblocked. Your Backend (pytest) job failed on a single assertion — test_s15_no_objects_all_or_filter_in_api flagging recent_actions.py:67 (1 failed, 545 passed). That was a real S-15 violation in the merged #505 recent-actions view; #523 just landed on main and moves the LogEntry query into audit.py, restoring the guard. A rebase onto current main should turn the backend gate green (the other 545 tests already pass). The pygrep-hook false positives are a separate pre-commit-only concern (#514) and don't affect this CI gate, which uses the AST-based test_security.py.

martin-castro-laminr-ai and others added 3 commits May 28, 2026 01:31
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>
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>
…ollow-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>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 11408f9 into main May 27, 2026
5 checks passed
Copy link
Copy Markdown
Contributor

@martin-castro-laminr-ai martin-castro-laminr-ai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — architect + security (merging)

Reviewed as software-architect + security-expert.

This closes a real gap. CodeQL was the only server-side gate, so test regressions merged green (#401#451). Adding pytest + the pnpm gate (typecheck + lint + test + build) on every PR and push to main is the right server-side enforcement and the core of #452.

[S] Supply-chain / least-privilege — verified:

  • Top-level permissions: contents: read; no job requests write, id-token, or secrets. ✅
  • Every third-party action is SHA-pinned (checkout, setup-python, pnpm/action-setup, setup-node) — a tag can be moved, a SHA can't. Consistent with codeql.yml / release.yml and #331. ✅
  • It's a gate, not a publisher — no PyPI access. ✅
  • concurrency … cancel-in-progress avoids burning minutes on stale commits. ✅

poetry.lock: diff is the content-hash line only — zero version moves. Poetry pinned to 2.1.4 (the lock's generator) for a deterministic --frozen check. Good catch on the stale-hash latent blocker.

Scope discipline: deferring the Python lint gate is the right call and honestly documented (SECURITY.md §8, the workflow header, #514) — ruff format + black mutually conflict, so the gate isn't satisfiable on a clean tree yet. Shipping the test gate now (the actual #401/#451 failure mode) and de-conflicting the linter as the tracked follow-up (#514) is correct sequencing.

Docs reconciled in-PR: SECURITY.md §8, decisions logs, ACCEPTANCE Q-4, OQ-A-001.

Owner follow-up noted: mark backend + frontend required in Settings → Branches (can't be done from a PR).

Tier 5/6 (SECURITY.md + .github/workflows/). Merging under the owner's standing direct-merge authorization; security-positive, fully green, this review on the record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants