Skip to content

chore(deps): Update GitHub Actions to 961eb17 #350

chore(deps): Update GitHub Actions to 961eb17

chore(deps): Update GitHub Actions to 961eb17 #350

Workflow file for this run

# CI/CD Pipeline with LLM Governance Integration
# Comprehensive code quality, security, and LLM debt detection
#
# Features:
# - Standard quality checks (tests, linting, type checking)
# - SonarQube quality gate enforcement
# - LLM anti-pattern detection
# - Assumption tag verification
# - Security scanning
#
# Three-Layer Governance:
# Layer 1: Production Runtime Risks (RAD tags)
# Layer 2: LLM Development Debt (LLM tags)
# Layer 3: Automated Code Quality (SonarQube)
name: CI
on:
push:
branches: [main, master, develop]
pull_request:
types: [opened, synchronize, reopened]
branches: [main, master, develop]
workflow_dispatch:
permissions:
contents: read
pull-requests: write
checks: write
env:
PYTHON_VERSION: '3.12'
jobs:
# ============================================================================
# Job 1: Standard Quality Checks
# ============================================================================
quality-checks:
name: Code Quality Checks
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden the runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
uv sync --all-extras
- name: Install Node.js tools
run: |
npm install -g markdownlint-cli
- name: Run Ruff format check
run: |
echo "::group::Ruff Format Check"
uv run ruff format --check --diff hooks/ || {
echo "::error::Code formatting issues detected. Run 'uv run ruff format hooks/' to fix."
exit 1
}
echo "::endgroup::"
- name: Run Ruff linter
run: |
echo "::group::Ruff Linting"
uv run ruff check hooks/ --output-format=github
echo "::endgroup::"
- name: Run BasedPyright type checker
run: |
echo "::group::BasedPyright Type Checking"
uv run basedpyright hooks/ || {
echo "::error::Type checking errors detected. Run 'uv run basedpyright hooks/' to see details."
exit 1
}
echo "::endgroup::"
- name: Run tests with coverage
run: |
echo "::group::Test Execution"
if [ -d "tests" ]; then
uv run pytest \
--cov=hooks \
--cov-report=xml:coverage.xml \
--cov-report=term-missing \
--cov-report=html \
--cov-branch \
-v
else
echo "::warning::No tests directory found"
echo '<?xml version="1.0" ?><coverage version="1.0"></coverage>' > coverage.xml
fi
echo "::endgroup::"
- name: Security scan with Bandit
run: |
echo "::group::Bandit Security Scan"
uv run bandit -r hooks/ -f json -o bandit-report.json || {
echo "::warning::Security issues detected"
cat bandit-report.json
}
echo "::endgroup::"
- name: Dependency vulnerability scan
run: |
echo "::group::pip-audit Dependency Scan"
# pip-audit cannot scan an environment that contains an editable
# install of the project itself (the project is not on PyPI, so
# pip-audit cannot resolve its metadata). Workaround: export the
# locked third-party dependency set via uv to requirements.txt
# format, then audit that file. This audits the same dependency
# surface as before without pulling the project into the scan.
# Vulnerabilities in the project's OWN code are caught by Bandit
# and other scanners elsewhere.
uv export --no-dev --no-emit-project --format requirements-txt > /tmp/audit-deps.txt 2>/dev/null
uv run pip-audit --strict --ignore-vuln PYSEC-2022-42969 \
-r /tmp/audit-deps.txt || {
echo "::error::Vulnerable dependencies detected. Review and update affected packages."
exit 1
}
echo "::endgroup::"
- name: Upload coverage
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-reports
path: |
coverage.xml
htmlcov/
retention-days: 7
# ============================================================================
# Job 2: LLM Governance Checks
# ============================================================================
llm-governance:
name: LLM Governance & Assumption Verification
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
rad_tags: ${{ steps.check-tags.outputs.rad_tags }}
llm_tags: ${{ steps.check-tags.outputs.llm_tags }}
total_tags: ${{ steps.check-tags.outputs.total_tags }}
steps:
- name: Harden the runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Check for unverified assumption tags
id: check-tags
run: |
echo "::group::Scanning for Unverified Assumption Tags"
# Check for RAD tags (Layer 1: Production Runtime Risks).
# Visibility counts (informational):
CRITICAL_TAGS=$(grep -r "#CRITICAL" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
ASSUME_TAGS=$(grep -r "#ASSUME" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
EDGE_TAGS=$(grep -r "#EDGE" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
# Gating count (failure threshold): only tags that lack a paired
# #VERIFY directive within 5 lines are counted as unverified debt.
# Tags paired with #VERIFY are considered fully documented per the
# RAD methodology and intentional. See scripts/check_unverified_assumption_tags.py.
UNVERIFIED_RAD=$(python3 scripts/check_unverified_assumption_tags.py --root hooks --verbose 2>&1 1>/tmp/unverified-count.txt && cat /tmp/unverified-count.txt || echo "0")
# Check for LLM debt tags (Layer 2: LLM Development Debt)
LLM_MOCK=$(grep -r "#LLM-MOCK" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
LLM_PLACEHOLDER=$(grep -r "#LLM-PLACEHOLDER" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
LLM_LOGIC=$(grep -r "#LLM-LOGIC" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
LLM_SCAFFOLD=$(grep -r "#LLM-SCAFFOLD" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
LLM_INFERRED=$(grep -r "#LLM-INFERRED" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
LLM_TEST_FIRST=$(grep -r "#LLM-TEST-FIRST" --include="*.py" hooks/ 2>/dev/null | wc -l || echo "0")
TOTAL_RAD=$((CRITICAL_TAGS + ASSUME_TAGS))
TOTAL_LLM=$((LLM_MOCK + LLM_PLACEHOLDER + LLM_LOGIC + LLM_SCAFFOLD + LLM_INFERRED + LLM_TEST_FIRST))
TOTAL_TAGS=$((TOTAL_RAD + TOTAL_LLM))
echo "rad_tags=$TOTAL_RAD" >> $GITHUB_OUTPUT
echo "unverified_rad=$UNVERIFIED_RAD" >> $GITHUB_OUTPUT
echo "llm_tags=$TOTAL_LLM" >> $GITHUB_OUTPUT
echo "total_tags=$TOTAL_TAGS" >> $GITHUB_OUTPUT
echo "### Assumption Tag Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Layer 1 (Production Runtime Risks):**" >> $GITHUB_STEP_SUMMARY
echo "- #CRITICAL tags: $CRITICAL_TAGS" >> $GITHUB_STEP_SUMMARY
echo "- #ASSUME tags: $ASSUME_TAGS" >> $GITHUB_STEP_SUMMARY
echo "- #EDGE tags: $EDGE_TAGS" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Layer 2 (LLM Development Debt):**" >> $GITHUB_STEP_SUMMARY
echo "- #LLM-MOCK: $LLM_MOCK" >> $GITHUB_STEP_SUMMARY
echo "- #LLM-PLACEHOLDER: $LLM_PLACEHOLDER" >> $GITHUB_STEP_SUMMARY
echo "- #LLM-LOGIC: $LLM_LOGIC" >> $GITHUB_STEP_SUMMARY
echo "- #LLM-SCAFFOLD: $LLM_SCAFFOLD" >> $GITHUB_STEP_SUMMARY
echo "- #LLM-INFERRED: $LLM_INFERRED" >> $GITHUB_STEP_SUMMARY
echo "- #LLM-TEST-FIRST: $LLM_TEST_FIRST" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Total unverified tags: $TOTAL_TAGS**" >> $GITHUB_STEP_SUMMARY
if [ "$UNVERIFIED_RAD" -gt 0 ]; then
echo "::error::Found $UNVERIFIED_RAD unverified production-risk tags (#CRITICAL or #ASSUME without a #VERIFY directive within 5 lines). See workflow log for file:line locations."
fi
if [ "$TOTAL_LLM" -gt 0 ]; then
echo "::warning::Found $TOTAL_LLM unverified LLM debt tags"
fi
echo "::endgroup::"
- name: Block PR if unverified critical tags found
# Only blocks on tags that lack a paired #VERIFY directive within
# 5 lines. Tags with adjacent #VERIFY are considered fully
# documented per the RAD methodology and do NOT block merge.
if: steps.check-tags.outputs.unverified_rad > 0
run: |
echo "::error::❌ PR blocked: Found ${{ steps.check-tags.outputs.unverified_rad }} unverified production-risk tags (#CRITICAL or #ASSUME without paired #VERIFY)"
echo ""
echo "Unverified tag locations:"
python3 scripts/check_unverified_assumption_tags.py --root hooks --verbose >/dev/null 2>&1 || true
python3 scripts/check_unverified_assumption_tags.py --root hooks --verbose 2>&1 1>/dev/null || true
echo ""
echo "See CLAUDE.md and docs/response-aware-development.md for verification workflow"
exit 1
- name: Detect LLM anti-patterns
run: |
echo "::group::LLM Anti-Pattern Detection"
# Hardcoded localhost/URLs
echo "Checking for hardcoded localhost/URLs..."
if grep -rE "(localhost|127\.0\.0\.1|http://|https://)" --include="*.py" hooks/ | \
grep -v "#LLM-PLACEHOLDER" | grep -v "^[[:space:]]*#"; then
echo "::warning::Found hardcoded URLs without #LLM-PLACEHOLDER tag"
fi
# Hardcoded secrets patterns
echo "Checking for potential hardcoded secrets..."
if grep -rE "(api_key|password|secret|token)\s*=\s*[\"'][^\"']+[\"']" --include="*.py" hooks/ | \
grep -v "#LLM-PLACEHOLDER" | grep -v "^[[:space:]]*#"; then
echo "::error::Potential hardcoded secret detected"
exit 1
fi
# Magic numbers
echo "Checking for magic numbers..."
MAGIC_NUMBERS=$(grep -rE "\b[0-9]{2,}\b" --include="*.py" hooks/ | \
grep -v "#LLM-PLACEHOLDER" | \
grep -v "^[[:space:]]*#" | \
wc -l || echo "0")
if [ "$MAGIC_NUMBERS" -gt 10 ]; then
echo "::warning::Found $MAGIC_NUMBERS potential magic numbers without constants"
fi
echo "::endgroup::"
# ============================================================================
# Job 3: Template Validation
# ----------------------------------------------------------------------------
# Note: SonarCloud scanning + quality gate are handled by the dedicated
# sonarcloud.yml workflow (thin caller for the org reusable workflow at
# ByronWilliamsCPA/.github/.github/workflows/python-sonarcloud.yml). A second
# inline scan here caused two concurrent analyses for the same projectKey on
# the same SHA, racing each other and producing the 'task not found' 404 from
# sonarqube-quality-gate-action. See PR #69.
# ============================================================================
validate-template:
name: Validate Cookiecutter Template
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Harden the runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install cookiecutter
run: |
pip install cookiecutter
- name: Generate test project
run: |
echo "::group::Template Generation Test"
cd /tmp
cookiecutter --no-input /home/runner/work/cookiecutter-python-template/cookiecutter-python-template
echo "✓ Template generated successfully"
ls -la my_python_project/
echo "::endgroup::"
- name: Validate generated project structure
run: |
echo "::group::Structure Validation"
cd /tmp/my_python_project
# Check key files exist
test -f README.md || { echo "::error::README.md missing"; exit 1; }
test -f pyproject.toml || { echo "::error::pyproject.toml missing"; exit 1; }
test -f CLAUDE.md || { echo "::error::CLAUDE.md missing"; exit 1; }
test -d src || { echo "::error::src/ directory missing"; exit 1; }
test -d tests || { echo "::error::tests/ directory missing"; exit 1; }
echo "✓ All key files and directories present"
echo "::endgroup::"
- name: Check for LLM governance in generated project
run: |
echo "::group::LLM Governance Validation"
cd /tmp/my_python_project
# Check if CLAUDE.md exists
if [ ! -f CLAUDE.md ]; then
echo "::error::CLAUDE.md missing from generated project"
exit 1
fi
echo "✓ CLAUDE.md exists in generated project"
# Optional: Check if LLM governance is documented (don't fail if missing)
if grep -q "#LLM-MOCK\|#LLM-PLACEHOLDER\|#CRITICAL" CLAUDE.md 2>/dev/null; then
echo "✓ LLM governance tags found in documentation"
else
echo "ℹ LLM governance tags documentation is optional"
fi
echo "::endgroup::"
# ============================================================================
# Summary Job
# ============================================================================
ci-summary:
name: CI Gate
runs-on: ubuntu-latest
needs: [quality-checks, llm-governance, validate-template]
if: always()
permissions:
contents: read
steps:
- name: Harden the runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Generate final summary
run: |
echo "### 🚀 CI Pipeline Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All governance layers checked:" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Code Quality Checks" >> $GITHUB_STEP_SUMMARY
echo "- ✅ LLM Governance Validation" >> $GITHUB_STEP_SUMMARY
echo "- ✅ SonarCloud Analysis (via reusable workflow)" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Template Validation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Status: ${{ job.status }}**" >> $GITHUB_STEP_SUMMARY