Files
2026-03-07 19:09:36 +01:00

122 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Scope guard for AIdriven pull requests.
This script ensures that files modified in a pull request conform to the
repository's labelbased allowlist and denylist policy. It is intended to
be run in CI to prevent agents (or humans) from modifying files outside
their allocated scope.
Usage: python3 tools/scope_guard.py
Environment variables:
GITHUB_EVENT_PATH: path to the GitHub event JSON for PR events. Used to
extract labels attached to the pull request.
DEFAULT_AI_LABEL: fallback label (default: "ai:impl") if no ai:* label
is present on the PR.
The script reads the list of changed files from `git diff --name-only` and
compares them against the allowlist for the detected label. If any file
falls outside the allowlist or matches the denylist, the script reports an
error and exits with status 1. Otherwise it prints a success message and
exits with status 0.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import List
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tools.scope_policy import explain_scope, is_path_allowed
def get_labels_from_event() -> List[str]:
"""Return a list of label names from the GitHub event JSON, if present."""
event_path = os.environ.get("GITHUB_EVENT_PATH")
if not event_path or not os.path.exists(event_path):
return []
try:
with open(event_path, "r", encoding="utf-8") as f:
data = json.load(f)
labels = []
# Pull request event
if "pull_request" in data:
labels = [lbl.get("name", "") for lbl in data["pull_request"].get("labels", [])]
elif "issue" in data:
labels = [lbl.get("name", "") for lbl in data["issue"].get("labels", [])]
return labels
except Exception:
return []
def detect_label() -> str:
"""
Determine which ai:* label applies to the current change set. The first
ai:* label found on the pull request is used. If none exists, the value
of the DEFAULT_AI_LABEL environment variable is used (default 'ai:impl').
"""
labels = get_labels_from_event()
for lbl in labels:
if lbl.startswith("ai:"):
return lbl
return os.environ.get("DEFAULT_AI_LABEL", "ai:impl")
def get_changed_files() -> List[str]:
"""
Return a list of files changed relative to the default branch. This uses
`git diff --name-only` against `origin/main` if available, otherwise
compares against the previous commit. When run in GitHub Actions with
fetchdepth=0, `origin/main` will exist.
"""
try:
# Try diffing against origin/main
result = subprocess.run([
"git", "diff", "--name-only", "origin/main"
], capture_output=True, text=True, check=True)
files = result.stdout.strip().splitlines()
if files:
return files
except Exception:
pass
# Fallback to previous commit
result = subprocess.run([
"git", "diff", "--name-only", "HEAD~1"
], capture_output=True, text=True, check=True)
return result.stdout.strip().splitlines()
def is_allowed(file_path: str, label: str) -> bool:
"""Check if a file_path is allowed for the given label."""
return is_path_allowed(label, file_path)
def main() -> int:
label = detect_label()
changed_files = get_changed_files()
if not changed_files:
print(f"No changed files detected. Scope guard passes by default for label {label}.")
return 0
disallowed = []
for file in changed_files:
if not is_allowed(file, label):
disallowed.append(file)
if disallowed:
print("Error: the following files are outside the allowed scope for label '", label, "':", sep="")
for f in disallowed:
print(f" - {f}")
print("See tools/scope_guard.py for the policy details.")
return 1
print(f"Scope guard passed. {explain_scope(label)}")
return 0
if __name__ == "__main__":
sys.exit(main())