Skip to content

GH#17503: protect executable template code blocks from simplification #19499

GH#17503: protect executable template code blocks from simplification

GH#17503: protect executable template code blocks from simplification #19499

Workflow file for this run

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"