Files
layue13 05f8fad38a
Gatekeep evals / eval-floor (push) Successful in 8s
Dogfood: run Gatekeep on this repo's PRs
Add .github/workflows/gatekeep-pr.yml with uses: ./ and
OPENROUTER_API_KEY from secrets. Forge-agnostic API helper for Gitea.
2026-10-26 20:40:51 +00:00

179 lines
6.2 KiB
Bash

#!/usr/bin/env bash
# Deterministic fail-closed policy floor + lightweight security scan.
set -euo pipefail
ROOT="${GATEKEEP_ROOT:-.}"
ACTION="${GATEKEEP_ACTION:-.}"
OUT="$ROOT/.gatekeep/out"
mkdir -p "$OUT"
# shellcheck source=lib-config.sh
source "$ACTION/scripts/lib-config.sh"
POLICY="$(gatekeep_config_file policy.json)"
EVENT="${GITHUB_EVENT_PATH:-}"
if [[ -z "$EVENT" || ! -f "$EVENT" ]]; then
echo "GITHUB_EVENT_PATH missing" >&2
exit 1
fi
PR_NUMBER=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["pull_request"]["number"])' "$EVENT")
REPO="${GITHUB_REPOSITORY:?}"
OWNER="${REPO%%/*}"
NAME="${REPO##*/}"
FORGE="$ACTION/scripts/forge_api.py"
python3 - "$FORGE" "$OWNER" "$NAME" "$PR_NUMBER" "$OUT" <<'PY'
import json, pathlib, sys, subprocess
forge, owner, name, pr, out = sys.argv[1:6]
out = pathlib.Path(out)
def api(method, path):
p = subprocess.run(
[sys.executable, forge, method, path],
capture_output=True, text=True,
)
if p.returncode != 0:
return None
try:
return json.loads(p.stdout) if p.stdout.strip() else None
except Exception:
return None
files_data = api("GET", f"/repos/{owner}/{name}/pulls/{pr}/files") or []
# Gitea may paginate poorly; accept list
if isinstance(files_data, dict):
files_data = files_data.get("files") or []
filenames = []
add = delete = 0
for f in files_data:
filenames.append(f.get("filename") or f.get("name") or "")
add += int(f.get("additions") or 0)
delete += int(f.get("deletions") or 0)
filenames = [f for f in filenames if f]
(out / "changed-files.txt").write_text("\n".join(filenames) + ("\n" if filenames else ""))
pr_data = api("GET", f"/repos/{owner}/{name}/pulls/{pr}") or {}
if not add and not delete:
add = int(pr_data.get("additions") or 0)
delete = int(pr_data.get("deletions") or 0)
labels = []
for l in pr_data.get("labels") or []:
if isinstance(l, dict):
labels.append(l.get("name") or "")
else:
labels.append(str(l))
head_sha = ((pr_data.get("head") or {}).get("sha")) or ""
ci = "unknown"
if head_sha:
st = api("GET", f"/repos/{owner}/{name}/commits/{head_sha}/statuses") or []
# Gitea returns list; derive combined
if isinstance(st, list) and st:
states = [s.get("state") for s in st]
if any(s == "failure" or s == "error" for s in states):
ci = "failure"
elif any(s == "pending" for s in states):
ci = "pending"
elif all(s == "success" for s in states):
ci = "success"
else:
ci = states[0] or "unknown"
elif isinstance(st, dict):
ci = st.get("state") or "unknown"
meta = {"additions": add, "deletions": delete, "labels": labels, "ci": ci, "head_sha": head_sha}
(out / "pr-meta.json").write_text(json.dumps(meta, indent=2))
print(json.dumps({"files": len(filenames), "add": add, "del": delete, "ci": ci}))
PY
ADD=$(python3 -c 'import json; print(json.load(open("'"$OUT"'/pr-meta.json")).get("additions",0))')
DEL=$(python3 -c 'import json; print(json.load(open("'"$OUT"'/pr-meta.json")).get("deletions",0))')
LABELS=$(python3 -c 'import json; print(",".join(json.load(open("'"$OUT"'/pr-meta.json")).get("labels") or []))')
CI_STATE=$(python3 -c 'import json; print(json.load(open("'"$OUT"'/pr-meta.json")).get("ci","unknown"))')
python3 - "$POLICY" "$OUT/changed-files.txt" "$ADD" "$DEL" "$LABELS" "$CI_STATE" "$OUT" <<'PY'
import json, fnmatch, sys, pathlib
policy = json.load(open(sys.argv[1]))
files = [l.strip() for l in open(sys.argv[2]) if l.strip()]
add, delete = int(sys.argv[3] or 0), int(sys.argv[4] or 0)
labels = [x.strip().lower() for x in (sys.argv[5] or "").split(",") if x.strip()]
ci = sys.argv[6]
out = pathlib.Path(sys.argv[7])
rank = {"low": 0, "medium": 1, "high": 2}
floor = "low"
reasons = []
def bump(t, why):
global floor
if rank[t] > rank[floor]:
floor = t
reasons.append(why)
for lab in labels:
if lab in [x.lower() for x in policy.get("force_high_labels", [])]:
bump("high", f"label forces high: {lab}")
if lab in [x.lower() for x in policy.get("force_medium_labels", [])]:
bump("medium", f"label forces medium: {lab}")
def match(path, glob):
g = glob.replace("**/", "*").replace("**", "*")
return fnmatch.fnmatch(path, glob) or fnmatch.fnmatch(path, g) or fnmatch.fnmatch(path.split("/")[-1], glob)
for f in files:
for g in policy.get("high_path_globs", []):
if match(f, g):
bump("high", f"high-path {f} ~ {g}")
for g in policy.get("medium_path_globs", []):
if match(f, g):
bump("medium", f"medium-path {f} ~ {g}")
if len(files) > int(policy.get("low_max_files", 8)):
bump("medium", f"file count {len(files)} > low cap")
if add + delete > int(policy.get("low_max_diff_lines", 200)):
bump("medium", f"diff lines {add+delete} > low cap")
if ci in ("failure", "error"):
bump("high", f"CI status {ci}")
seen=set(); uniq=[]
for r in reasons:
if r not in seen:
seen.add(r); uniq.append(r)
result = {"floor": floor, "reasons": uniq, "files": files, "additions": add, "deletions": delete, "labels": labels, "ci": ci}
(out / "floor.json").write_text(json.dumps(result, indent=2))
print(floor)
print("---")
print("\n".join(uniq))
PY
FLOOR_JSON="$OUT/floor.json"
FLOOR=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["floor"])' "$FLOOR_JSON")
REASONS=$(python3 -c 'import json,sys; print(" | ".join(json.load(open(sys.argv[1]))["reasons"]))' "$FLOOR_JSON")
SCAN_OUT="$OUT/security-scan.json"
bash "$ACTION/scripts/security-scan.sh" "$OUT/changed-files.txt" "$SCAN_OUT" || true
SCAN_HARD=$(python3 -c 'import json,sys; print("true" if json.load(open(sys.argv[1])).get("hard_fail") else "false")' "$SCAN_OUT")
if [[ "$SCAN_HARD" == "true" && "$FLOOR" != "high" ]]; then
FLOOR=high
REASONS="${REASONS} | deterministic security scan hard-fail"
python3 - "$FLOOR_JSON" <<'PY'
import json,sys
p=json.load(open(sys.argv[1]))
p["floor"]="high"
p["reasons"].append("deterministic security scan hard-fail")
json.dump(p, open(sys.argv[1],"w"), indent=2)
PY
fi
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "floor=$FLOOR" >> "$GITHUB_OUTPUT"
echo "reasons=$REASONS" >> "$GITHUB_OUTPUT"
echo "scan_hard_fail=$SCAN_HARD" >> "$GITHUB_OUTPUT"
fi
echo "Policy floor: $FLOOR (scan_hard_fail=$SCAN_HARD)" >&2