| |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import re |
| import sys |
| from pathlib import Path |
| from typing import Dict, List |
|
|
| REPO_ROOT = Path(__file__).resolve().parents[2] |
| COMPARATOR_RE = re.compile(r"(==|~=|>=|<=|!=|>|<)") |
|
|
|
|
| def _validate_requirements(path: Path) -> List[str]: |
| errors: List[str] = [] |
| if not path.exists(): |
| return [f"Missing requirements file: {path}"] |
|
|
| lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() |
| for idx, raw in enumerate(lines, start=1): |
| line = raw.strip() |
| if not line or line.startswith("#"): |
| continue |
| if line.startswith(("-r", "--requirement", "--extra-index-url", "--index-url")): |
| continue |
| if "git+" in line.lower() or line.startswith(("-e ", "--editable ")): |
| errors.append(f"{path}:{idx}: disallowed editable/git dependency: {line}") |
| continue |
| if "@" in line and "://" in line: |
| errors.append(f"{path}:{idx}: disallowed URL dependency: {line}") |
| continue |
| if not COMPARATOR_RE.search(line): |
| errors.append(f"{path}:{idx}: dependency must be version constrained: {line}") |
| return errors |
|
|
|
|
| def _validate_npm_lock(path: Path) -> List[str]: |
| errors: List[str] = [] |
| if not path.exists(): |
| errors.append(f"Missing npm lockfile: {path}") |
| return errors |
|
|
|
|
| def main() -> int: |
| parser = argparse.ArgumentParser(description="Checks dependency policy for backend/frontend lock hygiene.") |
| parser.add_argument( |
| "--requirements", |
| default=str(REPO_ROOT / "backend" / "requirements.txt"), |
| help="Path to backend requirements.txt", |
| ) |
| parser.add_argument( |
| "--npm-lock", |
| default=str(REPO_ROOT / "frontend" / "package-lock.json"), |
| help="Path to frontend package-lock.json", |
| ) |
| parser.add_argument("--out", default="", help="Optional output JSON path") |
| args = parser.parse_args() |
|
|
| req_path = Path(args.requirements) |
| lock_path = Path(args.npm_lock) |
|
|
| req_errors = _validate_requirements(req_path) |
| lock_errors = _validate_npm_lock(lock_path) |
| errors = req_errors + lock_errors |
|
|
| report: Dict[str, object] = { |
| "ok": not errors, |
| "requirements_path": str(req_path), |
| "npm_lock_path": str(lock_path), |
| "errors": errors, |
| } |
| payload = json.dumps(report, indent=2) |
| if args.out: |
| out = Path(args.out) |
| out.parent.mkdir(parents=True, exist_ok=True) |
| out.write_text(payload + "\n", encoding="utf-8") |
| print(f"Wrote dependency policy report: {out}") |
| print(payload) |
| return 0 if not errors else 2 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|