Skip to content

Commit 67440b2

Browse files
authored
chore(governance): reduce required merge load and add pre-push lint/test gate (#357)
1 parent 7b0aa20 commit 67440b2

File tree

5 files changed

+127
-2
lines changed

5 files changed

+127
-2
lines changed

.githooks/pre-push

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
repo_root="$(git rev-parse --show-toplevel)"
5+
cd "$repo_root"
6+
7+
python scripts/ops/pre_push_gate.py

.github/workflows/codeql.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ name: "CodeQL Advanced"
1414
on:
1515
push:
1616
branches: [ "main" ]
17-
pull_request:
18-
branches: [ "main" ]
1917
schedule:
2018
- cron: '37 7 * * 1'
2119

CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Get-ChildItem apps | ForEach-Object {
2727
- Format/lint: `python -m isort --check lib apps` then `python -m black --check lib apps` and `python -m pylint lib/src apps/*/src`
2828
- Tests: `pytest lib/tests apps/**/tests --maxfail=1 --cov=. --cov-report=term-missing --cov-fail-under=75`
2929
- Coverage floor is 75% to match CI.
30+
- Optional (recommended) push gate setup: `git config core.hooksPath .githooks`
31+
- With push gate enabled, every push runs `scripts/ops/pre_push_gate.py`, which mirrors CI lint/test gate commands used by `.github/workflows/lint.yml` and `.github/workflows/test.yml`.
3032
- Run a service locally (example): `uvicorn main:app --reload --app-dir apps/ecommerce-catalog-search/src --port 8000`
3133
- Keep README and docs in sync when adding adapters, agents, or services.
3234

docs/governance/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ Detailed policy is defined in [Infrastructure Governance](infrastructure-governa
7474
- Minimize bypass actors to explicit break-glass identities only
7575
- Revalidate protections after any GitHub ruleset/permission change
7676

77+
### Required checks baseline for `main`
78+
79+
- Required checks should be limited to `lint` and `test` in strict mode to reduce merge queue pressure while preserving core quality gates.
80+
- Additional workflows (for example CodeQL and non-blocking governance audits) remain recommended but should not be configured as required merge checks unless explicitly approved.
81+
7782
## Verification Procedure (PR-only governance)
7883

7984
Use both checks below for governance hardening and drift detection:
@@ -95,6 +100,14 @@ Use both checks below for governance hardening and drift detection:
95100
- branch deletion blocked
96101
- bypass actors minimized to explicit allowlist
97102

103+
#### Admin enforcement command (GitHub API)
104+
105+
For repositories using branch protection (instead of organization-level rulesets), an admin can enforce the baseline with:
106+
107+
`gh api -X PUT repos/<owner>/<repo>/branches/main/protection -H "Accept: application/vnd.github+json" --input <payload.json>`
108+
109+
Where `<payload.json>` sets strict required checks to `lint` and `test` and keeps PR-only protections enabled.
110+
98111
### Current External Governance Gap
99112

100113
- Ruleset/protection enforcement itself is controlled by repository admin permissions and cannot be remediated by branch code changes alone.

scripts/ops/pre_push_gate.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env python3
2+
"""Run repository push gate checks aligned with CI lint/test workflows."""
3+
4+
from __future__ import annotations
5+
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
10+
11+
ROOT = Path(__file__).resolve().parents[2]
12+
13+
14+
def run_step(command: list[str], *, title: str) -> None:
15+
print(f"\n==> {title}")
16+
print("$", " ".join(command))
17+
subprocess.run(command, check=True, cwd=ROOT)
18+
19+
20+
def run_lint_gate() -> None:
21+
run_step(
22+
["python", "-m", "isort", "--check-only", "lib", "apps"],
23+
title="Lint gate: isort",
24+
)
25+
run_step(
26+
["python", "-m", "black", "--check", "lib", "apps"],
27+
title="Lint gate: black",
28+
)
29+
30+
pylint_targets = [str(ROOT / "lib" / "src")]
31+
pylint_targets.extend(
32+
str(path)
33+
for path in sorted((ROOT / "apps").glob("*/src"))
34+
if (path / "pyproject.toml").exists()
35+
)
36+
run_step(
37+
["python", "-m", "pylint", *pylint_targets],
38+
title="Lint gate: pylint",
39+
)
40+
41+
run_step(
42+
[
43+
"python",
44+
"scripts/ops/check_markdown_links.py",
45+
"--roots",
46+
"docs/governance",
47+
"docs/architecture/README.md",
48+
],
49+
title="Lint gate: governance/architecture links",
50+
)
51+
52+
stale_tokens = (
53+
"OPERATIONAL-WORKFLOWS.md",
54+
"REPOSITORY-SURFACES.md",
55+
"governance-map.md",
56+
)
57+
agents_root = ROOT / ".github" / "agents"
58+
stale_refs: list[str] = []
59+
if agents_root.exists():
60+
for file_path in sorted(agents_root.rglob("*.md")):
61+
content = file_path.read_text(encoding="utf-8", errors="ignore")
62+
if any(token in content for token in stale_tokens):
63+
stale_refs.append(str(file_path.relative_to(ROOT)))
64+
65+
if stale_refs:
66+
print("\nFound stale canonical governance references:")
67+
for item in stale_refs:
68+
print(f"- {item}")
69+
raise RuntimeError("Stale canonical governance references found in .github/agents")
70+
71+
72+
def run_test_gate() -> None:
73+
run_step(
74+
["pytest", "lib/tests", "--maxfail=1"],
75+
title="Test gate: lib tests",
76+
)
77+
78+
app_test_dirs = [
79+
str(path)
80+
for path in sorted((ROOT / "apps").glob("*/tests"))
81+
if path.is_dir() and path.name == "tests"
82+
]
83+
run_step(
84+
["pytest", *app_test_dirs, "--ignore=apps/ui/tests"],
85+
title="Test gate: app tests (excluding UI tests)",
86+
)
87+
88+
89+
def main() -> int:
90+
try:
91+
run_lint_gate()
92+
run_test_gate()
93+
except subprocess.CalledProcessError as exc:
94+
print(f"\nPush gate failed at exit code {exc.returncode}")
95+
return exc.returncode
96+
except Exception as exc: # pylint: disable=broad-exception-caught
97+
print(f"\nPush gate failed: {exc}")
98+
return 1
99+
100+
print("\nPush gate passed: lint + test checks match CI workflow expectations.")
101+
return 0
102+
103+
104+
if __name__ == "__main__":
105+
sys.exit(main())

0 commit comments

Comments
 (0)