Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/scripts/assert-guardrails-bite.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env sh
# Assert every guardrail FAILS on a real violation — not just that a clean
# project passes. A guardrail that stops biting is worse than no guardrail.
#
# Usage: assert-guardrails-bite.sh <rendered-project-dir>
# The dir must be a rendered project (a git repo, uv-synced). Each check injects
# a genuine violation, runs the specific `just` recipe, asserts it fails, and
# reverts. Exits non-zero if any guardrail failed to bite.
set -u

DEST=${1:?usage: assert-guardrails-bite.sh <rendered-project-dir>}
cd "$DEST" || exit 1
PKG=$(find src -mindepth 1 -maxdepth 1 -type d | head -1) # src/<package>

# Clean git baseline so each injected violation can be reverted.
just init-whitelist >/dev/null 2>&1 || true
git add -A >/dev/null 2>&1 || true
git -c user.email=ci@example.com -c user.name=ci commit -q --no-verify -m "chore: baseline" >/dev/null 2>&1 || true

PASS=0
FAIL=0
SKIP=0
expect_fail() { name=$1; shift; if "$@" >/tmp/bite.log 2>&1; then echo " NOT CAUGHT: $name"; FAIL=$((FAIL + 1)); else echo " caught: $name"; PASS=$((PASS + 1)); fi; }
expect_pass() { name=$1; shift; if "$@" >/tmp/bite.log 2>&1; then echo " ok (clean): $name"; PASS=$((PASS + 1)); else echo " FALSE POS: $name"; FAIL=$((FAIL + 1)); fi; }
restore() { git checkout -q -- . 2>/dev/null; git clean -fdq 2>/dev/null; }
add_dep() { python3 -c "import pathlib;p=pathlib.Path('pyproject.toml');p.write_text(p.read_text().replace('dependencies = []','dependencies = [\"$1\"]'))"; }

echo "=== guardrail-bite checks (package: $PKG) ==="

# 1. ruff S rules still fire in src/ (the scripts/** S-ignore must not leak)
printf 'import subprocess\n\n\ndef run(cmd: str) -> None:\n """Run."""\n subprocess.run(cmd, shell=True)\n' > "$PKG/danger.py"
expect_fail "ruff S (subprocess shell=True) in src" just lint
restore

# 2. deptry catches a missing (undeclared) dependency
printf 'import httpx\n\n\ndef go() -> int:\n """Go."""\n return len(httpx.__name__)\n' > "$PKG/usehttpx.py"
expect_fail "deptry DEP001 missing dependency" just deps
restore

# 3. deptry catches an unused declared dependency (ignore list not over-broad)
add_dep "httpx>=0.27"
expect_fail "deptry DEP002 unused dependency" just deps
restore

# 4. vulture catches new dead code not in the whitelist
printf '\n\ndef orphaned_never_used() -> None:\n """Orphan."""\n' >> "$PKG/__init__.py"
expect_fail "vulture new dead code" just dead-code
restore

# 5. slop fails on a hallucinated package, passes on a real one
add_dep "reqeusts-not-a-real-pkg-xyz>=1"
expect_fail "slop hallucinated package" just slop
restore # reset dependencies = [] so the next add_dep applies cleanly
add_dep "requests>=2"
expect_pass "slop real package" just slop
restore

# 6. xenon fails on a genuinely over-complex (grade C) function
cat > "$PKG/complex_mod.py" <<'PY'
def tangled(a: int, b: int, c: int, d: int) -> int:
"""Deliberately over-complex."""
t = 0
if a > 0:
t += 1
if b > 0:
t += 1
if c > 0:
t += 1
if d > 0:
t += 1
for i in range(a):
if i % 2 == 0 and b > 1:
t += 1
elif i % 3 == 0 or c > 2:
t += 2
elif i % 5 == 0 and d > 3:
t += 3
else:
t -= 1
while b > 0:
b -= 1
if b == c:
break
if b == d:
continue
return t
PY
expect_fail "xenon complexity (grade C)" just complexity
restore

# 7. type-complexity fails on a deeply nested annotation
printf 'x: dict[str, list[dict[str, tuple[int, str, float]]]] = {}\n' > "$PKG/deepann.py"
expect_fail "type-complexity nested annotation" just type-complexity
restore

# 8. gitleaks blocks a real (non-example) secret — only if the binary is present
if command -v gitleaks >/dev/null; then
# Assemble the fake token at runtime so THIS script never contains a
# contiguous ghp_<36> literal (GitHub push protection would block committing
# it). At runtime leak.py gets the full token, which gitleaks then detects.
printf 'TOKEN = "ghp_%s"\n' 'wWPw5k4aXcaT4fNP0UcnZwJUVFk6LO0pINUx' > leak.py
git add leak.py
if gitleaks git --staged >/tmp/bite.log 2>&1; then echo " NOT CAUGHT: gitleaks real secret"; FAIL=$((FAIL + 1)); else echo " caught: gitleaks real secret"; PASS=$((PASS + 1)); fi
git reset -q leak.py; rm -f leak.py
else
echo " skipped: gitleaks real secret (binary not installed)"; SKIP=$((SKIP + 1))
fi

echo "=== $PASS caught, $FAIL not caught, $SKIP skipped ==="
[ "$FAIL" -eq 0 ]
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# The template's OWN CI (the template repo has none otherwise). Renders the
# template with copier and runs the generated project's floor, so the template
# can't silently ship a broken guardrail. This is separate from the release
# pipeline, which only runs inside generated projects.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
render-and-floor:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
license: ["MIT"]
include:
- python-version: "3.12"
license: "none" # guards the license=none rendering fix
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- uses: extractions/setup-just@v3
- name: Render template (py ${{ matrix.python-version }}, license ${{ matrix.license }})
run: |
# Render from a non-git copy of the working tree (avoids copier's VCS
# ref resolution and any shallow-clone edge cases).
mkdir -p "$RUNNER_TEMP/src"
cp copier.yml "$RUNNER_TEMP/src/"
cp -r template "$RUNNER_TEMP/src/template"
uvx copier copy --defaults \
--data project_name='CI Smoke' \
--data github_username=ci \
--data author_name='CI' \
--data author_email='ci@example.com' \
--data python_version=${{ matrix.python-version }} \
--data license=${{ matrix.license }} \
"$RUNNER_TEMP/src" "$RUNNER_TEMP/render"
- name: Initialize git in the render (hotspots churn needs git log)
working-directory: ${{ runner.temp }}/render
run: |
git init -q -b main
git config user.email ci@example.com
git config user.name ci
git add -A
git commit -q -m 'chore: initial render'
- name: uv sync
working-directory: ${{ runner.temp }}/render
run: uv sync
- name: Seed vulture allowlist
working-directory: ${{ runner.temp }}/render
run: just init-whitelist
- name: Floor - check
working-directory: ${{ runner.temp }}/render
run: just check
- name: Floor - audit
working-directory: ${{ runner.temp }}/render
run: just audit
- name: Floor - slop
working-directory: ${{ runner.temp }}/render
run: just slop
- name: Floor - insights
working-directory: ${{ runner.temp }}/render
run: just insights
- name: Assert license=none rendering
if: matrix.license == 'none'
working-directory: ${{ runner.temp }}/render
run: |
grep -qF 'All rights reserved.' README.md
! grep -qxF 'none' README.md
! grep -q '^license = ' pyproject.toml

guardrails-bite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- uses: extractions/setup-just@v3
- name: Render template
run: |
mkdir -p "$RUNNER_TEMP/src"
cp copier.yml "$RUNNER_TEMP/src/"
cp -r template "$RUNNER_TEMP/src/template"
uvx copier copy --defaults \
--data project_name='CI Smoke' \
--data github_username=ci \
--data author_name='CI' \
--data author_email='ci@example.com' \
--data python_version='3.12' \
--data license=MIT \
"$RUNNER_TEMP/src" "$RUNNER_TEMP/render"
- name: Initialize git + sync
working-directory: ${{ runner.temp }}/render
run: |
git init -q -b main
git config user.email ci@example.com
git config user.name ci
git add -A
git commit -q -m 'chore: initial render'
uv sync
- name: Assert every guardrail bites on a real violation
run: sh .github/scripts/assert-guardrails-bite.sh "$RUNNER_TEMP/render"

secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history for a complete secret scan
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # required for PR scans
2 changes: 1 addition & 1 deletion template/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ high-risk files.

## License

{{ license }}
{% if license == 'none' %}All rights reserved.{% else %}{{ license }}{% endif %}
3 changes: 3 additions & 0 deletions template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "{{ project_slug }}"
version = "0.1.0"
description = "{{ description }}"
{% if license != 'none' -%}
license = "{{ license }}"
{% endif -%}
authors = [{ name = "{{ author_name }}", email = "{{ author_email }}" }]
readme = "README.md"
requires-python = ">={{ python_version }}"
Expand Down
Loading