[Phase C] Pre-commit / pre-push hooks for local enforcement (#544) (#553)

* feat(devsecops): add pre-commit/pre-push hooks for local enforcement Phase C local enforcement for the DevSecOps Guardrails epic (jmservera/SquadScope-Coordinator#33), closes #544. Adds .pre-commit-config.yaml mirroring the CI guardrails: - ruff (lint + format) on commit — matches the Lint workflow. - checkov (github_actions/dockerfile/secrets) on push — matches Checkov workflow. - pytest (tests/) on push — matches the CI test job. - docker build verification on push via scripts/hooks/docker_build_check.sh, a no-op until a Containerfile/Dockerfile is added. Tool versions are pinned to match CI (ruff 0.15.7, checkov 3.2.533). Documents install (`pre-commit install --hook-type pre-commit --hook-type pre-push`) and the `--no-verify` emergency bypass in docs/devsecops/pre-commit.md, and updates the copilot-instructions Git hooks section. Depends on #543 (Phase B clean baseline). Verified: `pre-commit run --all-files` passes both stages; `pytest tests/` → 1209 passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: gitignore ruff/pytest tool caches The local pre-commit hooks (#544) run ruff and pytest, which create .ruff_cache/ and .pytest_cache/. Ignore them to keep the tree clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed Jun 27, 2026 at 00:32 UTC a2a20f360ad5329e610cea0668cb5412e5a9912c
5 files changed +205 -5
.github/copilot-instructions.md
+12 -5
@@ -71,11 +71,18 @@ zizmor .github/workflows/
71
72 ### Git hooks
73
74 -When pre-push/pre-commit hooks are added (Phase C), install them with the
75 -documented setup command and keep tool versions in sync with CI. **Emergency
76 -skip:** `git commit --no-verify` / `git push --no-verify` bypasses local hooks —
77 -use only for genuine emergencies and follow up by fixing the skipped findings.
78 -Never disable the CI gates themselves to land a change.
74 +Local pre-commit/pre-push hooks live in `.pre-commit-config.yaml` (ruff,
75 +checkov, pytest, docker build). Install them once and keep tool versions in
76 +sync with CI — see `docs/devsecops/pre-commit.md`:
77 +
78 +```bash
79 +pip install pre-commit
80 +pre-commit install --hook-type pre-commit --hook-type pre-push
81 +```
82 +
83 +**Emergency skip:** `git commit --no-verify` / `git push --no-verify` bypasses
84 +local hooks — use only for genuine emergencies and follow up by fixing the
85 +skipped findings. Never disable the CI gates themselves to land a change.
86
87 ### Ownership
88
.gitignore
+5
@@ -23,6 +23,11 @@ venv/
23 __pycache__/
24 *.py[cod]
25
26 +# Tooling caches (ruff / pytest — created by local pre-commit hooks)
27 +.ruff_cache/
28 +.pytest_cache/
29 +.mypy_cache/
30 +
31 # Secrets / environment files
32 .env
33 .env.*
.pre-commit-config.yaml new
+93
@@ -0,0 +1,93 @@
1 +# Local DevSecOps guardrail enforcement (pre-commit / pre-push).
2 +#
3 +# DevSecOps Guardrails epic (jmservera/SquadScope-Coordinator#33), issue #544.
4 +# Mirrors the CI gates so violations are caught before they reach a PR:
5 +# - ruff (lint + format) -> matches the Lint workflow
6 +# - checkov (IaC/Actions) -> matches the Checkov workflow
7 +# - pytest (unit tests) -> matches the CI test job
8 +# - docker build -> verifies a Containerfile/Dockerfile if present
9 +#
10 +# Fast checks (ruff) run on every commit. Slower checks (checkov, pytest,
11 +# docker build) run on push to keep the commit loop quick.
12 +#
13 +# Install (one-time):
14 +# pip install pre-commit
15 +# pre-commit install --hook-type pre-commit --hook-type pre-push
16 +#
17 +# Run on demand:
18 +# pre-commit run --all-files
19 +#
20 +# Tool versions are pinned to match CI (ruff 0.15.7, checkov 3.2.533). When you
21 +# bump a version in CI, bump the matching `rev`/dependency here too.
22 +#
23 +# Emergency bypass (hotfixes only):
24 +# git commit --no-verify
25 +# git push --no-verify
26 +# Bypassing skips LOCAL hooks only — the CI gates still run. Follow up by fixing
27 +# any skipped findings; never weaken a CI gate to land a change.
28 +
29 +default_install_hook_types: [pre-commit, pre-push]
30 +default_stages: [pre-commit]
31 +
32 +repos:
33 + # ── Ruff: Python lint + format (matches CI `ruff check` / `ruff format --check`)
34 + - repo: https://github.com/astral-sh/ruff-pre-commit
35 + rev: v0.15.7
36 + hooks:
37 + - id: ruff
38 + name: ruff (lint, autofix)
39 + args: [--fix]
40 + - id: ruff-format
41 + name: ruff (format)
42 +
43 + # ── Checkov: IaC / container / GitHub Actions misconfiguration scan (pre-push)
44 + - repo: https://github.com/bridgecrewio/checkov
45 + rev: "3.2.533"
46 + hooks:
47 + - id: checkov
48 + name: checkov (IaC / Actions scan)
49 + stages: [pre-push]
50 + # Trigger on workflow / container changes; the hook always scans the
51 + # whole repo (`checkov -d .`) to mirror CI rather than per-file.
52 + files: ^(\.github/workflows/.*\.ya?ml|.*Dockerfile.*|.*Containerfile.*)$
53 + pass_filenames: false
54 + args:
55 + - --framework
56 + - github_actions
57 + - dockerfile
58 + - secrets
59 + - --skip-path
60 + - node_modules
61 + - --skip-path
62 + - .venv
63 + - --skip-path
64 + - public
65 + - --skip-path
66 + - resources
67 + - --skip-path
68 + - themes
69 + - --compact
70 + - --quiet
71 +
72 + # ── Local hooks (use repo-installed tooling)
73 + - repo: local
74 + hooks:
75 + # pytest: run the unit suite before pushing (matches the CI test job).
76 + - id: pytest
77 + name: pytest (unit tests)
78 + entry: python3 -m pytest tests/ -q
79 + language: system
80 + stages: [pre-push]
81 + pass_filenames: false
82 + always_run: true
83 +
84 + # docker build: verify a Containerfile/Dockerfile builds, when one exists.
85 + # No-op today (the repo ships no container file); activates automatically
86 + # once a Containerfile/Dockerfile is added.
87 + - id: docker-build
88 + name: docker build (Containerfile, if present)
89 + entry: scripts/hooks/docker_build_check.sh
90 + language: script
91 + stages: [pre-push]
92 + pass_filenames: false
93 + always_run: true
docs/devsecops/pre-commit.md new
+64
@@ -0,0 +1,64 @@
1 +# Pre-commit / pre-push hooks (local enforcement)
2 +
3 +> Issue: jmservera/SquadScope#544 · Epic: jmservera/SquadScope-Coordinator#33
4 +> Phase C — local enforcement. Mirrors the CI guardrails so violations are
5 +> caught before they reach a PR.
6 +
7 +The repo ships a [`.pre-commit-config.yaml`](../../.pre-commit-config.yaml) that
8 +runs the same guardrails as CI:
9 +
10 +| Hook | Stage | Mirrors |
11 +|------|-------|---------|
12 +| `ruff` (lint, `--fix`) | commit | Lint workflow (`ruff check .`) |
13 +| `ruff-format` | commit | Lint workflow (`ruff format --check .`) |
14 +| `checkov` | push | Checkov workflow (IaC / Actions scan) |
15 +| `pytest` | push | CI test job (`pytest tests/`) |
16 +| `docker-build` | push | builds a `Containerfile`/`Dockerfile` when present (no-op today) |
17 +
18 +Fast checks (ruff) run on every **commit**; slower checks (checkov, pytest,
19 +docker build) run on **push** to keep the commit loop quick.
20 +
21 +## Install (one-time)
22 +
23 +```bash
24 +pip install pre-commit
25 +# Install both hook types so commit-stage and push-stage hooks are wired up:
26 +pre-commit install --hook-type pre-commit --hook-type pre-push
27 +```
28 +
29 +To run everything on demand (e.g. before opening a PR):
30 +
31 +```bash
32 +pre-commit run --all-files
33 +```
34 +
35 +## Tool versions
36 +
37 +Hook versions are **pinned to match CI** so local and CI results agree:
38 +
39 +- `ruff` → `0.15.7` (`rev: v0.15.7`)
40 +- `checkov` → `3.2.533` (`rev: "3.2.533"`)
41 +
42 +`pytest` and the `docker-build` check are `local` hooks that use the
43 +repo-installed tooling (`pip install -r requirements.txt` plus `pytest`). When
44 +you bump a tool version in CI, bump the matching `rev`/dependency in
45 +`.pre-commit-config.yaml` too.
46 +
47 +## Emergency bypass (hotfixes only)
48 +
49 +```bash
50 +git commit --no-verify
51 +git push --no-verify
52 +```
53 +
54 +`--no-verify` skips **local** hooks only — the CI gates still run on the PR.
55 +Use it only for genuine emergencies and follow up by fixing any skipped
56 +findings. Never weaken or disable a CI gate to land a change: CI must be
57 +correct, not just green.
58 +
59 +## Notes
60 +
61 +- The `docker-build` hook is a no-op until a `Containerfile`/`Dockerfile` is
62 + added at the repo root, after which it builds the image on push.
63 +- The `checkov` hook only triggers on changes to `.github/workflows/**` or
64 + container files, but always scans the whole repo (`checkov -d .`) to mirror CI.
scripts/hooks/docker_build_check.sh new
+31
@@ -0,0 +1,31 @@
1 +#!/usr/bin/env bash
2 +# Pre-push hook: build the container image when a Containerfile/Dockerfile is
3 +# present, to catch broken builds locally before they reach CI.
4 +#
5 +# DevSecOps Guardrails epic (jmservera/SquadScope-Coordinator#33), issue #544.
6 +# SquadScope ships no container file today, so this hook is a no-op until one is
7 +# added at the repo root (Containerfile or Dockerfile).
8 +set -euo pipefail
9 +
10 +containerfile=""
11 +for candidate in Containerfile Dockerfile; do
12 + if [ -f "$candidate" ]; then
13 + containerfile="$candidate"
14 + break
15 + fi
16 +done
17 +
18 +if [ -z "$containerfile" ]; then
19 + echo "docker-build: no Containerfile/Dockerfile found — skipping."
20 + exit 0
21 +fi
22 +
23 +if ! command -v docker >/dev/null 2>&1; then
24 + echo "docker-build: '$containerfile' present but docker is not installed." >&2
25 + echo "docker-build: install Docker or push with --no-verify (then fix locally)." >&2
26 + exit 1
27 +fi
28 +
29 +echo "docker-build: building image from $containerfile ..."
30 +docker build -f "$containerfile" -t squadscope-local-build-check .
31 +echo "docker-build: build succeeded."