GH#17503: protect executable template code blocks from simplification #19499
Workflow file for this run
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
| name: Code Quality Analysis | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| paths-ignore: | |
| - 'TODO.md' | |
| - 'todo/**' | |
| - 'README.md' | |
| - 'CHANGELOG.md' | |
| pull_request: | |
| branches: [ main ] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| # Only cancel in-progress runs on PR branches, not main. | |
| # Cancelled runs show as "failing" on the badge — rapid main pushes | |
| # (e.g. pulse auto-merges) would keep the badge red indefinitely. | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| jobs: | |
| # Cross-platform shellcheck: catches macOS-only assumptions in shell scripts | |
| # (t1748: Linux/WSL2 platform support) | |
| cross-platform-shellcheck: | |
| name: ShellCheck (${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| fail-fast: false | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Install ShellCheck (macOS) | |
| if: runner.os == 'macOS' | |
| run: brew install shellcheck | |
| - name: ShellCheck (cross-platform) | |
| run: | | |
| echo "Running ShellCheck on ${{ matrix.os }}..." | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_shell_files | |
| errors=0 | |
| while IFS= read -r file; do | |
| [ -n "$file" ] || continue | |
| if shellcheck -S error "$file" > /dev/null 2>&1; then | |
| echo "OK $file" | |
| else | |
| echo "FAIL $file" | |
| shellcheck -S error -f gcc "$file" 2>&1 | |
| errors=$((errors + 1)) | |
| fi | |
| done <<< "$LINT_SH_FILES" | |
| if [ "$errors" -gt 0 ]; then | |
| echo "" | |
| echo "$errors script(s) failed ShellCheck on ${{ matrix.os }}" | |
| exit 1 | |
| fi | |
| echo "All shell scripts passed ShellCheck on ${{ matrix.os }}" | |
| - name: Platform detection smoke test | |
| run: | | |
| echo "Testing platform-detect.sh on ${{ matrix.os }}..." | |
| bash .agents/scripts/platform-detect.sh | |
| echo "Platform detection: OK" | |
| # Bash 3.2 compatibility: catches "\n"/"\t" in double-quoted strings and other | |
| # bash 4+ constructs that break on macOS default bash (GH#17371) | |
| bash32-compat: | |
| name: Bash 3.2 Compatibility | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Bash 3.2 compatibility check | |
| run: | | |
| echo "Checking Bash 3.2 compatibility across all shell scripts..." | |
| # Source shared file-discovery helper (single source of truth for exclusions) | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_shell_files | |
| # Read threshold from config file (GH#17371 — ratchetable like other checks) | |
| THRESHOLD=69 | |
| if [ -f ".agents/configs/complexity-thresholds.conf" ]; then | |
| val=$(grep '^BASH32_COMPAT_THRESHOLD=' .agents/configs/complexity-thresholds.conf | cut -d= -f2 || true) | |
| if [ -n "$val" ] && [ "$val" -eq "$val" ] 2>/dev/null; then | |
| THRESHOLD=$val | |
| fi | |
| fi | |
| violations=0 | |
| # Self-skip: linters-local.sh grep patterns contain the forbidden strings | |
| # as search targets, not as bash code. | |
| self_skip="linters-local.sh" | |
| while IFS= read -r file; do | |
| [ -n "$file" ] || continue | |
| basename_file=$(basename "$file") | |
| [ "$basename_file" = "$self_skip" ] && continue | |
| [ -f "$file" ] || continue | |
| # "\t" or "\n" in string concatenation (likely wants $'\t' or $'\n') | |
| # Only flag += or = assignments, not awk/sed/printf/echo -e/python contexts | |
| matches=$(grep -nE '\+="\\[tn]|="\\[tn]' "$file" 2>/dev/null \ | |
| | grep -vE '^[0-9]+:[[:space:]]*#' \ | |
| | grep -vE 'awk|sed|printf|echo.*-e|python|f\.write|gsub|join|split|print |replace|coords|excerpt|delimiter|regex|pattern' \ | |
| || true) | |
| if [ -n "$matches" ]; then | |
| while IFS= read -r line; do | |
| echo "FAIL $file:$line [\"\\t\"/\"\\n\" — use \$'\\t' or \$'\\n' for actual whitespace]" | |
| violations=$((violations + 1)) | |
| done <<< "$matches" | |
| fi | |
| # Associative arrays (bash 4.0+) | |
| assoc=$(grep -nE '^[[:space:]]*(declare|local|typeset)[[:space:]]+-A[[:space:]]' "$file" 2>/dev/null \ | |
| | grep -vE '^[0-9]+:[[:space:]]*#' || true) | |
| if [ -n "$assoc" ]; then | |
| while IFS= read -r line; do | |
| echo "FAIL $file:$line [associative array — bash 4.0+]" | |
| violations=$((violations + 1)) | |
| done <<< "$assoc" | |
| fi | |
| # Namerefs (bash 4.3+) | |
| nameref=$(grep -nE '^[[:space:]]*(declare|local)[[:space:]]+-n[[:space:]]' "$file" 2>/dev/null \ | |
| | grep -vE '^[0-9]+:[[:space:]]*#' || true) | |
| if [ -n "$nameref" ]; then | |
| while IFS= read -r line; do | |
| echo "FAIL $file:$line [nameref -- bash 4.3+]" | |
| violations=$((violations + 1)) | |
| done <<< "$nameref" | |
| fi | |
| done <<< "$LINT_SH_FILES" | |
| echo "" | |
| echo "Bash 3.2 compatibility violations: $violations" | |
| # Threshold from .agents/configs/complexity-thresholds.conf (GH#17371). | |
| # Reduce after each batch of bash32-compat cleanup PRs merges. | |
| if [ "$violations" -gt "$THRESHOLD" ]; then | |
| echo "::error::Bash 3.2 compatibility regression: $violations violations (threshold: $THRESHOLD)" | |
| echo "macOS ships bash 3.2 — these constructs will break on macOS." | |
| echo "See .agents/reference/bash-compat.md for alternatives." | |
| exit 1 | |
| fi | |
| echo "Within threshold ($THRESHOLD)" | |
| framework-validation: | |
| name: Framework Validation | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| # Fetch enough history for merge-base detection in safety-policy-check.sh. | |
| # Without this, stale PR branches that predate new policy markers cause | |
| # false-positive CI failures because the check can't distinguish regressions | |
| # from missing markers. GH#6902. | |
| fetch-depth: 0 | |
| - name: Framework Structure Check | |
| run: | | |
| echo "🚀 AI-Assisted DevOps Framework Validation" | |
| echo "==========================================" | |
| echo "📁 Core Structure:" | |
| echo "✅ AGENTS.md: $(test -f AGENTS.md && echo 'Present' || echo 'Missing')" | |
| echo "✅ README.md: $(test -f README.md && echo 'Present' || echo 'Missing')" | |
| echo "✅ LICENSE: $(test -f LICENSE && echo 'Present' || echo 'Missing')" | |
| echo "✅ .agents/ directory: $(test -d .agents && echo 'Present' || echo 'Missing')" | |
| echo "" | |
| echo "📊 Framework Statistics:" | |
| echo "📚 Documentation files: $(find . -name '*.md' | wc -l)" | |
| echo "🔧 Agent scripts: $(find .agents/scripts -name '*.sh' 2>/dev/null | wc -l || echo '0')" | |
| echo "📖 Agent subagents: $(find .agents -mindepth 2 -name '*.md' 2>/dev/null | wc -l || echo '0')" | |
| echo "" | |
| echo "🔍 Quality Checks:" | |
| # Check agent scripts exist and are executable | |
| if [ -d ".agents/scripts" ]; then | |
| script_count=$(find .agents/scripts -name "*.sh" | wc -l) | |
| echo "✅ Agent scripts found: $script_count" | |
| else | |
| echo "❌ .agents/scripts directory missing" | |
| exit 1 | |
| fi | |
| # Check AGENTS.md exists in .agents/ | |
| if [ -f ".agents/AGENTS.md" ]; then | |
| echo "✅ .agents/AGENTS.md present" | |
| else | |
| echo "❌ .agents/AGENTS.md missing" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "🎯 Framework Validation: COMPLETE" | |
| # Security check - ensure no obvious API keys in repository | |
| echo "" | |
| echo "🔐 Security Validation:" | |
| echo "✅ API keys stored securely in local storage (~/.config/aidevops/)" | |
| echo "✅ GitHub Actions use repository secrets (SONAR_TOKEN, CODACY_API_TOKEN)" | |
| echo "✅ Private scripts directory (.agents/scripts-private/) gitignored" | |
| echo "✅ Security documentation and best practices implemented" | |
| - name: JSON Config Validation | |
| run: | | |
| echo "🔍 Validating JSON config files..." | |
| errors=0 | |
| # Validate all .json files tracked by git | |
| while IFS= read -r file; do | |
| if python3 -m json.tool "$file" > /dev/null 2>&1; then | |
| echo "✅ $file" | |
| else | |
| echo "❌ $file - invalid JSON" | |
| python3 -m json.tool "$file" 2>&1 | head -5 | |
| errors=$((errors + 1)) | |
| fi | |
| done < <(git ls-files '*.json') | |
| # Also validate .json.txt template files | |
| # These files may contain SPDX license header comments (lines starting with #) | |
| # which are stripped before JSON validation. | |
| while IFS= read -r file; do | |
| if grep -v '^#' "$file" | python3 -m json.tool > /dev/null 2>&1; then | |
| echo "✅ $file" | |
| else | |
| echo "❌ $file - invalid JSON" | |
| grep -v '^#' "$file" | python3 -m json.tool 2>&1 | head -5 | |
| errors=$((errors + 1)) | |
| fi | |
| done < <(git ls-files '*.json.txt') | |
| if [ "$errors" -gt 0 ]; then | |
| echo "" | |
| echo "❌ $errors JSON file(s) failed validation" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "✅ All JSON files valid" | |
| - name: ShellCheck Lint | |
| run: | | |
| echo "Validating shell scripts with ShellCheck..." | |
| errors=0 | |
| # Source shared file-discovery helper (single source of truth for exclusions) | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_shell_files | |
| while IFS= read -r file; do | |
| [ -n "$file" ] || continue | |
| if shellcheck -S error "$file" > /dev/null 2>&1; then | |
| echo "OK $file" | |
| else | |
| echo "FAIL $file" | |
| shellcheck -S error -f gcc "$file" 2>&1 | |
| errors=$((errors + 1)) | |
| fi | |
| done <<< "$LINT_SH_FILES" | |
| if [ "$errors" -gt 0 ]; then | |
| echo "" | |
| echo "$errors script(s) failed ShellCheck (severity: error)" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "All shell scripts passed ShellCheck" | |
| - name: Secret Safety Policy Check | |
| run: | | |
| echo "Running secret safety policy checks..." | |
| bash .agents/scripts/safety-policy-check.sh | |
| echo "Secret safety policy checks passed" | |
| - name: Profile README Boundary Guard | |
| run: | | |
| echo "Running profile README boundary regression guard..." | |
| bash .agents/scripts/tests/test-profile-readme-boundary.sh | |
| echo "Profile README boundary guard passed" | |
| complexity-check: | |
| name: Complexity Analysis | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install analysis tools | |
| run: | | |
| pip install lizard pyflakes | |
| - name: Shell function complexity | |
| run: | | |
| # Source shared file-discovery helper (single source of truth for exclusions) | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_shell_files | |
| # Read threshold from config file (GH#5628 — ratchetable thresholds) | |
| THRESHOLD=420 | |
| if [ -f ".agents/configs/complexity-thresholds.conf" ]; then | |
| val=$(grep '^FUNCTION_COMPLEXITY_THRESHOLD=' .agents/configs/complexity-thresholds.conf | cut -d= -f2 || true) | |
| if [ -n "$val" ] && [ "$val" -eq "$val" ] 2>/dev/null; then | |
| THRESHOLD=$val | |
| fi | |
| fi | |
| echo "Checking shell function complexity (>100 lines = blocking)..." | |
| violations=0 | |
| while IFS= read -r file; do | |
| [ -n "$file" ] || continue | |
| result=$(awk ' | |
| /^[a-zA-Z_][a-zA-Z0-9_]*\(\)[[:space:]]*\{/ { fname=$1; sub(/\(\)/, "", fname); start=NR; next } | |
| fname && /^\}$/ { lines=NR-start; if(lines>100) printf "BLOCK %s:%d %s() %d lines\n", FILENAME, start, fname, lines; fname="" } | |
| ' "$file") | |
| if [ -n "$result" ]; then | |
| echo "$result" | |
| count=$(echo "$result" | wc -l) | |
| violations=$((violations + count)) | |
| fi | |
| done <<< "$LINT_SH_FILES" | |
| echo "" | |
| echo "Blocking violations (>100 lines): $violations" | |
| # Threshold from .agents/configs/complexity-thresholds.conf (GH#5628). | |
| # Reduce after each batch of simplification-debt PRs merges. | |
| if [ "$violations" -gt "$THRESHOLD" ]; then | |
| echo "::error::Function complexity regression: $violations violations (threshold: $THRESHOLD)" | |
| exit 1 | |
| fi | |
| echo "Within threshold ($THRESHOLD)" | |
| - name: Shell nesting depth | |
| run: | | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_shell_files | |
| # Read threshold from config file (GH#5628 — ratchetable thresholds) | |
| THRESHOLD=260 | |
| if [ -f ".agents/configs/complexity-thresholds.conf" ]; then | |
| val=$(grep '^NESTING_DEPTH_THRESHOLD=' .agents/configs/complexity-thresholds.conf | cut -d= -f2 || true) | |
| if [ -n "$val" ] && [ "$val" -eq "$val" ] 2>/dev/null; then | |
| THRESHOLD=$val | |
| fi | |
| fi | |
| echo "Checking shell nesting depth (>8 levels = blocking)..." | |
| violations=0 | |
| while IFS= read -r file; do | |
| [ -n "$file" ] || continue | |
| max_depth=$(awk ' | |
| BEGIN { depth=0; max_depth=0 } | |
| /^[[:space:]]*#/ { next } | |
| /[[:space:]]*(if|for|while|until|case)[[:space:]]/ { depth++; if(depth>max_depth) max_depth=depth } | |
| /[[:space:]]*(fi|done|esac)[[:space:]]*$/ || /^[[:space:]]*(fi|done|esac)$/ { if(depth>0) depth-- } | |
| END { print max_depth } | |
| ' "$file") | |
| if [ "$max_depth" -gt 8 ]; then | |
| echo "BLOCK $file: nesting depth $max_depth" | |
| violations=$((violations + 1)) | |
| fi | |
| done <<< "$LINT_SH_FILES" | |
| echo "" | |
| echo "Blocking violations (>8 depth): $violations" | |
| # Threshold from .agents/configs/complexity-thresholds.conf (GH#5628). | |
| # Reduce after each batch of simplification-debt PRs merges. | |
| if [ "$violations" -gt "$THRESHOLD" ]; then | |
| echo "::error::Nesting depth regression: $violations violations (threshold: $THRESHOLD)" | |
| exit 1 | |
| fi | |
| echo "Within threshold ($THRESHOLD)" | |
| - name: File size check | |
| run: | | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_shell_files | |
| lint_python_files | |
| # Read threshold from config file (GH#5628 — ratchetable thresholds) | |
| THRESHOLD=40 | |
| if [ -f ".agents/configs/complexity-thresholds.conf" ]; then | |
| val=$(grep '^FILE_SIZE_THRESHOLD=' .agents/configs/complexity-thresholds.conf | cut -d= -f2 || true) | |
| if [ -n "$val" ] && [ "$val" -eq "$val" ] 2>/dev/null; then | |
| THRESHOLD=$val | |
| fi | |
| fi | |
| echo "Checking file sizes (>1500 lines = blocking)..." | |
| violations=0 | |
| for file_list in "$LINT_SH_FILES" "$LINT_PY_FILES"; do | |
| while IFS= read -r file; do | |
| [ -n "$file" ] || continue | |
| lc=$(wc -l < "$file") | |
| if [ "$lc" -gt 1500 ]; then | |
| echo "BLOCK $file: $lc lines" | |
| violations=$((violations + 1)) | |
| fi | |
| done <<< "$file_list" | |
| done | |
| echo "" | |
| echo "Blocking violations (>1500 lines): $violations" | |
| # Threshold from .agents/configs/complexity-thresholds.conf (GH#5628). | |
| # Reduce after each batch of simplification-debt PRs merges. | |
| if [ "$violations" -gt "$THRESHOLD" ]; then | |
| echo "::error::File size regression: $violations violations (threshold: $THRESHOLD)" | |
| exit 1 | |
| fi | |
| echo "Within threshold ($THRESHOLD)" | |
| - name: Python cyclomatic complexity (Lizard) | |
| run: | | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_python_files | |
| echo "Checking Python cyclomatic complexity (CCN > 8)..." | |
| if [ -z "$LINT_PY_FILES" ]; then | |
| echo "No Python files found" | |
| exit 0 | |
| fi | |
| # Run lizard with warnings-only mode (CCN > 8) | |
| lizard_output=$(echo "$LINT_PY_FILES" | xargs lizard --CCN 8 --warnings_only 2>/dev/null || true) | |
| if [ -n "$lizard_output" ]; then | |
| echo "$lizard_output" | |
| violations=$(echo "$lizard_output" | grep -c "warning:" || echo "0") | |
| echo "" | |
| echo "Python complexity violations: $violations" | |
| # Advisory — Codacy is the hard gate for Python | |
| echo "::warning::$violations Python functions exceed cyclomatic complexity 8" | |
| else | |
| echo "No Python complexity violations" | |
| fi | |
| - name: Python import check (Pyflakes) | |
| run: | | |
| source .agents/scripts/lint-file-discovery.sh | |
| lint_python_files | |
| echo "Checking Python imports..." | |
| if [ -z "$LINT_PY_FILES" ]; then | |
| echo "No Python files found" | |
| exit 0 | |
| fi | |
| pyflakes_output=$(echo "$LINT_PY_FILES" | xargs pyflakes 2>/dev/null || true) | |
| if [ -n "$pyflakes_output" ]; then | |
| echo "$pyflakes_output" | |
| violations=$(echo "$pyflakes_output" | grep -c . || echo "0") | |
| echo "" | |
| echo "Pyflakes issues: $violations" | |
| echo "::warning::$violations Python import/usage issues found" | |
| else | |
| echo "No Pyflakes issues" | |
| fi | |
| sonarcloud: | |
| name: SonarCloud Analysis | |
| runs-on: ubuntu-latest | |
| # Job-level env so that step `if:` conditions can reference env.SONAR_TOKEN | |
| # (step-level env: blocks are applied AFTER the if: is evaluated) | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 # Shallow clones should be disabled for better analysis | |
| - name: SonarCloud Scan | |
| if: env.SONAR_TOKEN != '' | |
| uses: SonarSource/sonarqube-scan-action@40f5b61913e891f9d316696628698051136015be | |
| # SonarCloud may fail on fork PR merge refs even when SONAR_TOKEN is | |
| # available (e.g., maintainer re-runs). Non-fatal — other quality tools | |
| # (Codacy, CodeFactor, Qlty) still provide coverage. | |
| continue-on-error: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: SonarCloud Scan Skipped | |
| if: env.SONAR_TOKEN == '' | |
| run: | | |
| echo "::warning::SONAR_TOKEN not configured — skipping SonarCloud scan" | |
| echo "Add SONAR_TOKEN to repository secrets to enable SonarCloud analysis" | |
| - name: Codacy Integration Check | |
| # Codacy analysis runs via the Codacy Production GitHub App (not this workflow). | |
| # This step only verifies the API token is configured for Codacy API access. | |
| # The "Codacy Static Code Analysis" check run is posted by the Codacy App | |
| # independently on each PR commit. | |
| run: | | |
| if [[ -n "$CODACY_API_TOKEN" ]]; then | |
| echo "Codacy API token: configured" | |
| echo "Codacy GitHub App: runs analysis independently on PRs" | |
| echo "Dashboard: https://app.codacy.com/gh/marcusquinn/aidevops" | |
| else | |
| echo "::notice::CODACY_API_TOKEN not configured — Codacy API access unavailable" | |
| echo "Note: Codacy GitHub App analysis still runs if the app is installed" | |
| fi | |
| - name: Quality Analysis Summary | |
| run: | | |
| echo "Code Quality Analysis Summary" | |
| echo "================================" | |
| if [[ -n "$SONAR_TOKEN" ]]; then | |
| echo "SonarCloud: Analysis completed (via workflow action)" | |
| else | |
| echo "SonarCloud: Skipped (SONAR_TOKEN not configured)" | |
| fi | |
| echo "Codacy: Runs via GitHub App (independent of this workflow)" | |
| echo "" | |
| echo "View Results:" | |
| echo " SonarCloud: https://sonarcloud.io/project/overview?id=marcusquinn_aidevops" | |
| echo " Codacy: https://app.codacy.com/gh/marcusquinn/aidevops" | |
| echo "" | |
| echo "Code Quality Pipeline: COMPLETE" |