mirror of
https://github.com/rsyslog/rsyslog.git
synced 2026-06-17 11:52:39 +02:00
Why: Mandate the local container testing validation per AGENTS.md before push. To make it completely robust, we prevent empty or invalid marker commits from bypassing the check, expand the file types that trigger re-run (including Python, Dockerfile, YAML metadata, and tests), and handle git history changes gracefully without crashes. Impact: Developer/AI agents are blocked from pushing if they touch C, Python, Docker, YAML, or test files without running container validation first. Before/After: Before, container validation wasn't wired, or empty/invalid markers could be bypassed or cause shell crashes. Now, the hook is fully integrated, robust against invalid commit hashes, and covers all relevant file extensions. Technical Overview: 1. Wire pre_push_container_gate.sh into hooks.json under PreToolUse for Bash. 2. Use git rev-parse to verify the validation marker commit hash actually exists in the local repository before running diffs. 3. Expand file matching regex to include .py, Dockerfile, MODULE_METADATA.yaml, and any files under tests/. 4. Gitignore .codex/container_validated.marker to keep mutable local developer state out of commits. With the help of AI-Agents: Antigravity
221 lines
5.9 KiB
Bash
Executable File
221 lines
5.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Codex pre-push hook: Enforces full local container testing (Tier 1) per AGENTS.md.
|
|
# Automatically checks if C/H or test files were changed since the last container run.
|
|
set -euo pipefail
|
|
|
|
payload="$(cat)"
|
|
|
|
if [[ -z "${payload}" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
should_gate="$(
|
|
PAYLOAD="$payload" python3 <<'PY'
|
|
import json
|
|
import os
|
|
import posixpath
|
|
import re
|
|
import shlex
|
|
import sys
|
|
|
|
def split_simple_commands(command: str) -> list[list[str]]:
|
|
lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|()")
|
|
lexer.whitespace_split = True
|
|
lexer.commenters = ""
|
|
commands = []
|
|
current = []
|
|
for token in lexer:
|
|
if re.fullmatch(r"[;&|()]+", token):
|
|
if current:
|
|
commands.append(current)
|
|
current = []
|
|
continue
|
|
current.append(token)
|
|
if current:
|
|
commands.append(current)
|
|
return commands
|
|
|
|
def strip_prefixes(words: list[str]) -> list[str]:
|
|
i = 0
|
|
assignment_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*")
|
|
while i < len(words):
|
|
token = words[i]
|
|
if token == "sudo":
|
|
i += 1
|
|
continue
|
|
if token in {"command", "builtin", "noglob", "time"}:
|
|
i += 1
|
|
continue
|
|
if token == "env":
|
|
i += 1
|
|
while i < len(words) and assignment_re.fullmatch(words[i]):
|
|
i += 1
|
|
continue
|
|
if assignment_re.fullmatch(token):
|
|
i += 1
|
|
continue
|
|
break
|
|
return words[i:]
|
|
|
|
def is_git_push(words: list[str]) -> bool:
|
|
words = strip_prefixes(words)
|
|
if not words or words[0] != "git":
|
|
return False
|
|
i = 1
|
|
while i < len(words):
|
|
token = words[i]
|
|
if token == "push":
|
|
return True
|
|
if token == "--":
|
|
return False
|
|
if token in {"-c", "-C", "--git-dir", "--work-tree"}:
|
|
i += 2
|
|
continue
|
|
if token.startswith("-"):
|
|
i += 1
|
|
continue
|
|
return False
|
|
return False
|
|
|
|
def unwrap_shell_command(words: list[str]) -> list[str] | None:
|
|
words = strip_prefixes(words)
|
|
if not words:
|
|
return None
|
|
|
|
shell_name = posixpath.basename(words[0])
|
|
if shell_name not in {"bash", "sh", "zsh"}:
|
|
return None
|
|
|
|
i = 1
|
|
while i < len(words):
|
|
token = words[i]
|
|
|
|
if token == "--":
|
|
i += 1
|
|
break
|
|
|
|
if token.startswith("-") and "c" in token[1:]:
|
|
if i + 1 >= len(words):
|
|
return None
|
|
return words[i + 1 :]
|
|
|
|
if token.startswith("-"):
|
|
i += 1
|
|
continue
|
|
|
|
return None
|
|
|
|
return None
|
|
|
|
def contains_git_push(words: list[str]) -> bool:
|
|
if is_git_push(words):
|
|
return True
|
|
|
|
nested_command = unwrap_shell_command(words)
|
|
if not nested_command:
|
|
return False
|
|
|
|
command = nested_command[0]
|
|
if not isinstance(command, str) or not command.strip():
|
|
return False
|
|
|
|
try:
|
|
commands = split_simple_commands(command)
|
|
except ValueError:
|
|
return False
|
|
|
|
return any(contains_git_push(simple_command) for simple_command in commands)
|
|
|
|
payload_raw = os.environ.get("PAYLOAD")
|
|
if payload_raw is None:
|
|
sys.exit(0)
|
|
try:
|
|
payload = json.loads(payload_raw)
|
|
except json.JSONDecodeError:
|
|
sys.exit(0)
|
|
|
|
if payload.get("hook_event_name") != "PreToolUse":
|
|
sys.exit(0)
|
|
if payload.get("tool_name") != "Bash":
|
|
sys.exit(0)
|
|
|
|
tool_input = payload.get("tool_input") or {}
|
|
command = tool_input.get("command")
|
|
if not isinstance(command, str) or not command.strip():
|
|
sys.exit(0)
|
|
|
|
try:
|
|
commands = split_simple_commands(command)
|
|
except ValueError:
|
|
sys.exit(0)
|
|
|
|
for simple_command in commands:
|
|
if contains_git_push(simple_command):
|
|
print("yes")
|
|
break
|
|
PY
|
|
)"
|
|
|
|
if [[ "${should_gate}" != "yes" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
# We are pushing! Let's check container validation.
|
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
repo_root="$(cd -- "${script_dir}/.." && pwd)"
|
|
cd "${repo_root}"
|
|
|
|
# Check if SKIP_CONTAINER_VALIDATION is set
|
|
if [[ "${SKIP_CONTAINER_VALIDATION:-0}" == "1" ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
# A file `.codex/container_validated.marker` is updated when container tests pass.
|
|
marker_file=".codex/container_validated.marker"
|
|
|
|
if [[ ! -f "${marker_file}" ]]; then
|
|
cat <<'EOF'
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "deny",
|
|
"permissionDecisionReason": "Push blocked: Full local container validation has not been run or completed successfully. Per AGENTS.md, you MUST run full local container validation (Tier 1) before pushing. Run your container validation or, if unavailable, export SKIP_CONTAINER_VALIDATION=1 and explain the blocker."
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
# Check if source, build, or test files changed in the commits since the marker
|
|
marker_commit="$(xargs < "${marker_file}" || true)"
|
|
|
|
if [[ -z "${marker_commit}" ]] || ! git rev-parse --verify "${marker_commit}^{commit}" >/dev/null 2>&1; then
|
|
cat <<'EOF'
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "deny",
|
|
"permissionDecisionReason": "Push blocked: The validation marker is empty or invalid. Please re-run the container validation to update the marker."
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
# Use git diff to see if any source, test, or docker files have been modified since the validation marker
|
|
changed_since_validation="$(git diff --name-only "${marker_commit}" HEAD | grep -E '\.(c|h|sh|py)$|Makefile\.am|configure\.ac|Dockerfile|MODULE_METADATA\.yaml|^tests/' || true)"
|
|
if [[ -n "${changed_since_validation}" ]]; then
|
|
cat <<'EOF'
|
|
{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "deny",
|
|
"permissionDecisionReason": "Push blocked: Changes to source, build, or test files have been made since the last local container validation run. Please run the local container validation (Tier 1) again on the updated code before pushing."
|
|
}
|
|
}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
|
|
exit 0
|