Compare commits

..

2 Commits

Author SHA1 Message Date
Timmy Burn
99aab6c530 ci-test: add empty markdown to trigger gate failure
Some checks failed
Minimum PR Gate / minimum-pr-gate (pull_request) Failing after 19s
Self-Healing Smoke / self-healing-smoke (pull_request) Failing after 26s
Agent PR Gate / gate (pull_request) Failing after 56s
Smoke Test / smoke (pull_request) Failing after 24s
Agent PR Gate / report (pull_request) Successful in 20s
2026-04-28 22:52:58 -04:00
Timmy Burn
9def37e208 ci: add minimum PR gate (#521)
Some checks failed
Minimum PR Gate / minimum-pr-gate (pull_request) Failing after 17s
Agent PR Gate / gate (pull_request) Failing after 59s
Self-Healing Smoke / self-healing-smoke (pull_request) Failing after 27s
Smoke Test / smoke (pull_request) Failing after 25s
Agent PR Gate / report (pull_request) Successful in 20s
Adds .gitea/workflows/minimum-pr-gate.yml which enforces a lightweight
check on every pull request: Python syntax on changed files, secret scan,
and markdown sanity. Also documents the gate in README.

Closes #521
2026-04-28 22:52:06 -04:00
7 changed files with 111 additions and 12 deletions

View File

@@ -0,0 +1,84 @@
name: Minimum PR Gate
on:
pull_request:
branches: [main]
workflow_dispatch:
jobs:
minimum-pr-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine changed files
id: changes
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
CHANGED=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
else
CHANGED=$(git ls-files)
fi
echo "changed=${CHANGED}" >> $GITHUB_OUTPUT
echo "Changed files:"
echo "$CHANGED"
- name: Python syntax check
if: steps.changes.outputs.changed != ''
run: |
CHANGED_FILES="${{ steps.changes.outputs.changed }}"
PY_FILES=$(echo "$CHANGED_FILES" | grep '\.py$' || true)
if [ -z "$PY_FILES" ]; then
echo "No Python files changed."
exit 0
fi
echo "Checking Python syntax on:"
echo "$PY_FILES"
echo "$PY_FILES" | while IFS= read -r f; do
python3 -m py_compile "$f" || { echo "FAIL: syntax error in $f"; exit 1; }
done
echo "PASS: Python syntax"
- name: Secret scan
if: steps.changes.outputs.changed != ''
run: |
CHANGED_FILES="${{ steps.changes.outputs.changed }}"
SCAN_FILES=$(echo "$CHANGED_FILES" | grep -E '\.(py|yaml|yml|sh|json)$' || true)
if [ -z "$SCAN_FILES" ]; then
echo "No files to scan for secrets."
exit 0
fi
echo "Scanning files for secrets:"
echo "$SCAN_FILES"
if echo "$SCAN_FILES" | xargs -r grep -E 'sk-or-|sk-ant-|ghp_|AKIA' 2>/dev/null | \
grep -v '.gitea' | grep -v 'detect_secrets' | grep -v 'test_trajectory_sanitize' | grep -v 'test_secret_detection' | grep -q .; then
echo "FAIL: Secrets or hardcoded tokens detected"
exit 1
fi
echo "PASS: No secrets detected"
- name: Markdown sanity check
if: steps.changes.outputs.changed != ''
run: |
CHANGED_FILES="${{ steps.changes.outputs.changed }}"
MD_FILES=$(echo "$CHANGED_FILES" | grep '\.md$' || true)
if [ -z "$MD_FILES" ]; then
echo "No markdown files changed."
exit 0
fi
echo "Checking markdown sanity on:"
echo "$MD_FILES"
echo "$MD_FILES" | while IFS= read -r f; do
if [ ! -s "$f" ]; then
echo "FAIL: empty markdown file: $f"
exit 1
fi
if ! grep -q '[^[:space:]]' "$f"; then
echo "FAIL: markdown file contains only whitespace: $f"
exit 1
fi
done
echo "PASS: Markdown sanity"

View File

@@ -99,6 +99,19 @@ python3 scripts/detect_secrets.py /tmp/test_secret.py
# Should report: OpenAI API key detected
```
## CI / PR Gate
A lightweight minimum PR gate runs automatically on every pull request targeting `main`. The gate performs:
- **Python syntax**: All changed Python files must compile without errors.
- **Secret scan**: Changed code files are scanned for common hardcoded tokens (OpenAI, Anthropic, GitHub, AWS keys).
- **Markdown sanity**: Changed Markdown documentation files must be nonempty and contain meaningful text.
The workflow is defined in `.gitea/workflows/minimum-pr-gate.yml`. It can also be triggered manually from the *Actions* panel (workflow_dispatch).
This gate protects the repository from introducing broken code, leaked credentials, or empty documentation.
## Development
### Running Tests

0
empty.md Normal file
View File

View File

@@ -6,8 +6,8 @@ from pathlib import Path
MODEL = "NousResearch_Hermes-4-14B-Q4_K_M.gguf"
URL = "http://localhost:8081/v1/chat/completions"
SOUL = (Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "SOUL.md").read_text()
OUT = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "test-results" / f'local_decision_session_{time.strftime("%Y%m%d_%H%M%S")}.md'
SOUL = Path.home().joinpath('.timmy/SOUL.md').read_text()
OUT = Path.home().joinpath('.timmy/test-results', f'local_decision_session_{time.strftime("%Y%m%d_%H%M%S")}.md')
OUT.parent.mkdir(parents=True, exist_ok=True)
messages = [

View File

@@ -10,9 +10,9 @@ from pathlib import Path
LLAMA_HEALTH = "http://localhost:8081/health"
LLAMA_MODELS = "http://localhost:8081/v1/models"
HERMES_AGENT_ROOT = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")) / "hermes-agent"
SESSION_DIR = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")) / "sessions"
REPORT_DIR = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "test-results"
HERMES_AGENT_ROOT = Path.home() / ".hermes" / "hermes-agent"
SESSION_DIR = Path.home() / ".hermes" / "sessions"
REPORT_DIR = Path.home() / ".timmy" / "test-results"
REPORT_DIR.mkdir(parents=True, exist_ok=True)
REPORT_PATH = REPORT_DIR / f"local_timmy_proof_{time.strftime('%Y%m%d_%H%M%S')}.md"

View File

@@ -5,8 +5,8 @@ import os
from pathlib import Path
from datetime import datetime
DB_PATH = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "metrics" / "model_metrics.db"
REPORT_PATH = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "SOVEREIGN_HEALTH.md"
DB_PATH = Path.home() / ".timmy" / "metrics" / "model_metrics.db"
REPORT_PATH = Path.home() / "timmy" / "SOVEREIGN_HEALTH.md"
def generate_report():
if not DB_PATH.exists():

View File

@@ -75,8 +75,8 @@ def check_config_files():
"""Scan ~/.hermes and ~/.timmy config files for cloud dependencies."""
findings = []
config_dirs = [
Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")),
Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")),
Path.home() / ".hermes",
Path.home() / ".timmy",
]
for config_dir in config_dirs:
@@ -145,7 +145,8 @@ def check_cron_jobs():
cloud_lines = []
local_lines = []
for line in crontab.split("\n"):
for line in crontab.split("
"):
if line.startswith("#") or not line.strip():
continue
for provider in CLOUD_PROVIDERS:
@@ -186,7 +187,8 @@ def check_tmux_sessions():
if result.returncode != 0:
return [Finding("tmux", "unknown", "No tmux sessions or tmux not running")]
sessions = result.stdout.strip().split("\n")
sessions = result.stdout.strip().split("
")
findings.append(Finding("tmux", "local", f"{len(sessions)} session(s) active: {', '.join(sessions[:5])}"))
except Exception as e:
@@ -254,7 +256,7 @@ def check_api_keys():
findings.append(Finding("env_keys", "local", "No cloud API keys in environment"))
# Check auth.json
auth_path = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")) / "auth.json"
auth_path = Path.home() / ".hermes" / "auth.json"
if auth_path.exists():
try:
auth = json.loads(auth_path.read_text())