chore(deps): Update GitHub Actions #224
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |