Skip to content

chore(deps): Update GitHub Actions #224

chore(deps): Update GitHub Actions

chore(deps): Update GitHub Actions #224

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
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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
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: 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: SonarQube Quality Gate (Enhanced with LLM Governance)
# ============================================================================
sonarqube-quality-gate:
name: SonarQube Quality Gate
runs-on: ubuntu-latest
needs: [quality-checks, llm-governance]
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Download coverage reports
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: coverage-reports
- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@689fb39b34b9aa95ebc5f8f119343ddd51542402 # v4.2.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Wait for Quality Gate
uses: sonarsource/sonarqube-quality-gate-action@cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b # v1.2.0
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
id: quality-gate
- name: Quality Gate Result
run: |
echo "::group::Quality Gate Status"
echo "Quality Gate Status: ${{ steps.quality-gate.outputs.quality-gate-status }}"
echo "View detailed results: https://sonarcloud.io/project/overview?id=ByronWilliamsCPA_cookiecutter-python-template"
echo "::endgroup::"
- name: Quality Gate Status (Blocking)
if: steps.quality-gate.outputs.quality-gate-status == 'FAILED'
run: |
echo "::error::Quality Gate FAILED"
echo ""
echo "SonarQube quality gate did not pass. Review:"
echo "- Issues: https://sonarcloud.io/project/overview?id=ByronWilliamsCPA_cookiecutter-python-template"
echo "- Duplicate code warnings in template directories must be excluded via sonar-project.properties, not tolerated."
exit 1
- name: Generate unified governance report
if: always()
run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
## 🎯 Three-Layer Governance Report
### Layer 1: Production Runtime Risks (RAD)
- #CRITICAL tags: ${{ needs.llm-governance.outputs.rad_tags > 0 && '❌ FOUND' || '✅ None' }}
- Status: ${{ needs.llm-governance.outputs.rad_tags > 0 && '**BLOCKED**' || '**PASSED**' }}
### Layer 2: LLM Development Debt
- LLM debt tags: ${{ needs.llm-governance.outputs.llm_tags > 0 && '⚠️ FOUND' || '✅ None' }}
- Status: ${{ needs.llm-governance.outputs.llm_tags > 0 && '**WARNING**' || '**PASSED**' }}
### Layer 3: Automated Code Quality (SonarQube)
- Quality Gate: ${{ steps.quality-gate.outputs.quality-gate-status || 'N/A' }}
- Status: ${{ steps.quality-gate.outputs.quality-gate-status == 'PASSED' && '**PASSED**' || '**INFORMATIONAL** (non-blocking)' }}
### Overall Status
${{ needs.llm-governance.outputs.rad_tags > 0 && '❌ **PR BLOCKED** - Fix RAD tags before merging' || '✅ **READY TO MERGE**' }}
---
**Next Steps:**
- Fix all CRITICAL issues
- Verify and remove assumption tags
- Ensure quality gate passes
- Review SonarQube findings
EOF
# ============================================================================
# Job 4: Template Validation
# ============================================================================
validate-template:
name: Validate Cookiecutter Template
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- 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 Summary
runs-on: ubuntu-latest
needs: [quality-checks, llm-governance, sonarqube-quality-gate, validate-template]
if: always()
steps:
- 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 "- ✅ SonarQube Quality Gate" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Template Validation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Status: ${{ job.status }}**" >> $GITHUB_STEP_SUMMARY