Compare commits
2 Commits
fix/553-pa
...
test-failu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99aab6c530 | ||
|
|
9def37e208 |
84
.gitea/workflows/minimum-pr-gate.yml
Normal file
84
.gitea/workflows/minimum-pr-gate.yml
Normal 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"
|
||||
13
README.md
13
README.md
@@ -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 non‑empty 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
|
||||
|
||||
@@ -18,8 +18,8 @@ from urllib import request
|
||||
DEFAULT_BASE_URL = "https://forge.alexanderwhitestone.com/api/v1"
|
||||
DEFAULT_OWNER = "Timmy_Foundation"
|
||||
DEFAULT_REPO = "timmy-home"
|
||||
DEFAULT_TOKEN_FILE = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "gitea" / "token"
|
||||
DEFAULT_FAILOVER_STATUS = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "failover_status.json"
|
||||
DEFAULT_TOKEN_FILE = Path.home() / ".config" / "gitea" / "token"
|
||||
DEFAULT_FAILOVER_STATUS = Path.home() / ".timmy" / "failover_status.json"
|
||||
DEFAULT_RESTART_STATE_DIR = Path("/var/lib/timmy/restarts")
|
||||
DEFAULT_HEARTBEAT_FILE = Path("/var/lib/timmy/heartbeats/fleet_health.last")
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def get_token():
|
||||
f = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "gitea" / "token"
|
||||
f = Path.home() / ".config" / "gitea" / "token"
|
||||
if f.exists():
|
||||
return f.read_text().strip()
|
||||
return os.environ.get("GITEA_TOKEN", "")
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import Any, Dict, List
|
||||
|
||||
# Configuration
|
||||
GITEA_BASE = "https://forge.alexanderwhitestone.com/api/v1"
|
||||
TOKEN_PATH = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "gitea", "token")
|
||||
TOKEN_PATH = os.path.expanduser("~/.config/gitea/token")
|
||||
ORG = "Timmy_Foundation"
|
||||
REPO = "timmy-home"
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from urllib.request import Request, urlopen
|
||||
|
||||
API_BASE = "https://forge.alexanderwhitestone.com/api/v1"
|
||||
ORG = "Timmy_Foundation"
|
||||
DEFAULT_TOKEN_PATH = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "gitea", "token")
|
||||
DEFAULT_TOKEN_PATH = os.path.expanduser("~/.config/gitea/token")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
||||
@@ -27,7 +27,7 @@ from pathlib import Path
|
||||
import re
|
||||
|
||||
GITEA_URL = "https://forge.alexanderwhitestone.com"
|
||||
GITEA_TOKEN_PATH = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "gitea" / "token"
|
||||
GITEA_TOKEN_PATH = Path.home() / ".config" / "gitea" / "token"
|
||||
ORG = "Timmy_Foundation"
|
||||
|
||||
REPOS = [
|
||||
|
||||
@@ -12,14 +12,12 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
TIMMY_HOME = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy"))
|
||||
STATUS_FILE = TIMMY_HOME / "failover_status.json"
|
||||
SPEC_FILE = TIMMY_HOME / "fleet_dispatch.json"
|
||||
OUTPUT_FILE = TIMMY_HOME / "dispatch_plan.json"
|
||||
STATUS_FILE = Path.home() / ".timmy" / "failover_status.json"
|
||||
SPEC_FILE = Path.home() / ".timmy" / "fleet_dispatch.json"
|
||||
OUTPUT_FILE = Path.home() / ".timmy" / "dispatch_plan.json"
|
||||
|
||||
|
||||
def load_json(path: Path, default: Any):
|
||||
|
||||
@@ -13,7 +13,7 @@ FLEET = {
|
||||
"bezalel": "167.99.126.228"
|
||||
}
|
||||
|
||||
STATUS_FILE = Path(os.environ.get("TIMMY_HOME", Path.home() / ".timmy")) / "failover_status.json"
|
||||
STATUS_FILE = Path.home() / ".timmy" / "failover_status.json"
|
||||
|
||||
def check_health(host):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user