From b6e2c8403ae698501d3108d4480ba94ee42d4ee9 Mon Sep 17 00:00:00 2001 From: jediknight112 Date: Thu, 13 Aug 2026 14:18:03 -0500 Subject: [PATCH] [CA-10577] Set up SonarQube code analysis Adds SonarQube static-analysis coverage for example-sage-theme to satisfy the Engineering Excellence SonarQube check. - sonar-project.properties with the project key assigned by Security (wpengine_example-sage-theme_c16e3095-fcf4-41b8-b8b7-d60c822888f4). Sources scoped to wp-content/ (the Sage theme code). No sonar.language so PHP + JS/TS are auto-detected. Excludes vendored deps (node_modules/, vendor/) and TechDocs (docs/). - .github/workflows/sonar.yaml is the upstream augmented workflow fetched verbatim from wpengine/ai-github-tools (docs/examples/github-actions/sonar.yaml). Runs the SonarQube scan and quality gate on pushes to main and all PRs, and adds a Claude Code remediation step that posts SonarQube findings as PR comments. --- .github/workflows/sonar.yaml | 252 +++++++++++++++++++++++++++++++++++ sonar-project.properties | 21 +++ 2 files changed, 273 insertions(+) create mode 100644 .github/workflows/sonar.yaml create mode 100644 sonar-project.properties diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml new file mode 100644 index 0000000..4a7ba7b --- /dev/null +++ b/.github/workflows/sonar.yaml @@ -0,0 +1,252 @@ +# Derived from https://github.com/wpengine/ai-github-tools/blob/main/docs/examples/github-actions/sonar.yaml +# which is derived from https://github.com/wpengine/sec-sonar-testing/blob/main/.github/workflows/main.yml + +on: + # Trigger analysis when pushing in main or pull requests, and when creating + # a pull request. + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + +name: SonarQube Main Workflow +jobs: + sonarqube: + runs-on: ubuntu-latest + # --- Begin permissions changes + # GitHub workflow permissions for steps other than running Claude Code + permissions: + contents: read + pull-requests: write # For minimizing old SonarQube remediation comments + id-token: write # Needed to authenticate to Google Cloud + actions: read # required by the github_ci MCP server used by Claude Code + + env: + # GitHub permissions for Claude Code only + CLAUDE_PERMISSIONS: >- + { + "contents": "read", + "pull_requests": "write", + "actions": "read" + } + # --- End permissions changes + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@master # @master tag vs. commit hash is the prescribed pattern by our security team for sonarsource/ + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + - name: SonarQube Quality Gate check + # --- Begin change to continue on error + id: quality-gate + continue-on-error: true + # --- End change to continue on error + uses: sonarsource/sonarqube-quality-gate-action@master # @master tag vs. commit hash is the prescribed pattern by our security team for sonarsource/ + # Force to fail step after specific time + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + # --- Begin Claude Code remediation steps + # Skip AI remediation when SonarQube found no issues, hotspots, or quality gate failures on this PR. + - name: Check SonarQube for findings + id: sonar-results + if: github.event_name == 'pull_request' + continue-on-error: true + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + PR_NUMBER: ${{ github.event.pull_request.number }} + QUALITY_GATE_OUTCOME: ${{ steps.quality-gate.outcome }} + run: | + PROJECT_KEY=$(grep 'projectKey=' .scannerwork/report-task.txt | cut -d'=' -f2) + if [ -z "$PROJECT_KEY" ]; then + echo "::error::Could not read projectKey from .scannerwork/report-task.txt — did sonarqube-scan-action run?" + exit 1 + fi + + # Capture HTTP status separately so a misconfigured token / 5xx fails + # loudly here in seconds instead of silently coercing to ISSUES=1 + + # HOTSPOTS=1 + HAS_FINDINGS=true and burning Claude minutes on every PR. + sonar_get() { + local path="$1" out="$2" http + http=$(curl -s -o "$out" -w '%{http_code}' -u "${SONAR_TOKEN}:" "${SONAR_HOST_URL}${path}") + # `curl` writes 000 to %{http_code} on connection-level failures + # (DNS, TLS, timeout, unreachable). Treat anything outside 2xx/3xx + # as failure so a misconfigured host fails fast like a bad token. + if [ "$http" -lt 200 ] || [ "$http" -ge 400 ]; then + echo "::error::Sonar API returned HTTP ${http} for ${path}. Check SONAR_TOKEN, SONAR_HOST_URL, and projectKey=${PROJECT_KEY}." + head -c 500 "$out" >&2 || true + return 1 + fi + } + + issues_resp=$(mktemp); hotspots_resp=$(mktemp) + trap 'rm -f "$issues_resp" "$hotspots_resp"' EXIT + sonar_get "/api/issues/search?componentKeys=${PROJECT_KEY}&pullRequest=${PR_NUMBER}&resolved=false" "$issues_resp" || exit 1 + sonar_get "/api/measures/component?component=${PROJECT_KEY}&pullRequest=${PR_NUMBER}&metricKeys=security_hotspots" "$hotspots_resp" || exit 1 + ISSUES=$(jq -r '.total // 0' "$issues_resp") + HOTSPOTS=$(jq -r '.component.measures[0].value // "0"' "$hotspots_resp") + + if [ "${ISSUES}" -eq 0 ] && [ "${HOTSPOTS}" -eq 0 ] && [ "${QUALITY_GATE_OUTCOME}" != "failure" ]; then + HAS_FINDINGS=false + else + HAS_FINDINGS=true + fi + echo "Findings: ${HAS_FINDINGS} (Issues: ${ISSUES}, Hotspots: ${HOTSPOTS}, Quality gate: ${QUALITY_GATE_OUTCOME})" + echo "has_findings=${HAS_FINDINGS}" >> "$GITHUB_OUTPUT" + + # Skip AI remediation on pushes to PRs that already have human approvals. + - name: Check for existing approvals + id: check-approvals + if: github.event.action == 'synchronize' + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + APPROVALS=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" --jq '[.[] | select(.state == "APPROVED")] | length') + echo "has_approvals=$([ "$APPROVALS" -gt 0 ] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT" + + - name: SonarQube Remediation with Claude Code + id: claude-sonar + # Only run on pull requests, not pushes to main. + if: github.event_name == 'pull_request' + timeout-minutes: 15 # P95 runtime is ~6 min. 15 min catches hung runs without cutting off legitimate reviews. + uses: wpengine/ai-github-tools/.github/actions/wpe-claude-code-action@wpe-claude-code-action/v1 # major version is approved by our security team for this case vs commit hash + with: + # AI GitHub Tokens + claude_permissions: ${{ env.CLAUDE_PERMISSIONS }} + allowed_bots: '*' + pr_number: ${{ github.event.pull_request.number }} + outdated_pr_comments: '(\*\*|## )SonarQube .*(Analysis|Remediation|Review)' + # Skip Claude Code for skip label, existing approvals, or no SonarQube findings. + # The action still runs to minimize outdated PR comments. + skip_claude_code: >- + ${{ + contains(github.event.pull_request.labels.*.name, 'skip-claude-review') + || + steps.check-approvals.outputs.has_approvals == 'true' || + steps.sonar-results.outputs.has_findings == 'false' + }} + + # General Inputs + track_progress: true + workflow_type: sonarqube + # The default model for sonarqube workflows is set centrally to be appropriate for + # most repositories, but can be overridden if your repository has specific needs. + # default_model: haiku + + # Effort is not set for sonarqube (Haiku 4.5 does not support it). If you override + # to a Sonnet model, you can also set default_effort (e.g., medium). + # default_effort: medium + + # SonarQube Remediation Instructions + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} + + You have access to the SonarQube MCP server to retrieve code quality and security issues. + + The SonarQube scan has completed for this pull request. Your task is to query SonarQube + and make a plan to address any issues found. You will publish your plan as comment(s) on the pull request. + + Your task is to: + + 1. **Query SonarQube Issues** + - Use the sonarqube MCP server tools to retrieve issues for this PR + + 2. **Prioritize Issues** + - Focus on security vulnerabilities first + - Work on other issues in the order of severity + - Then address code quality issues + - Limit recommendations to the top 10 issues + + 3. **Analyze Each Issue** + - Read the affected files to understand context + - Identify the root cause of each issue + - Determine the best remediation approach + + 4. **Propose Fixes** + - PR summary comment + - Name your PR Review comment with a level-2 markdown heading: `## SonarQube Remediation Plan` + - Provide a section for current issues. For each current issue (in priority order), provide: + - Context information including the SonarQube engine ID, rule ID, rule title, severity, type, message, and code location. The local AI tools on developer machines do not have SonarQube access, so always include this information. + - For the top 10 legitimate issues, a suggested remediation or point to an inline comment + - For issues that seem illegitimate or unclear, say so rather than guessing. + - For the next 10 issues, omit remediation suggestions, but still provide context for developers to investigate on their own. + - links to the relevant SonarQube rule documentation, such as links to specific rules from https://rules.sonarsource.com/ + - If there are resolved prior issues, provide a section for them. For each resolved prior issue: + - provide the SonarQube engine ID, rule ID, rule title, severity, type, message, and code location. + - Inline comments + - Use GitHub inline comments to suggest smaller changes at the exact location + - Be concise: state the problem and suggest a fix in 1-3 sentences. + - Use `confirmed: true` when creating inline comments. + + Always use Context7 MCP when you need library/API documentation. + + Gotchas to avoid: + - DO NOT MAKE COMMITS DIRECTLY; USE COMMENTS TO SUGGEST CHANGES. + - Only provide each remediation suggestion in one of the PR summary comment or inline comments, not both. Avoid duplicating the same suggestion in both places. + - Do not add PR summary comments or inline comments about issues not reported by SonarQube. Only comment on actual SonarQube issues. Do not provide a general code review. Such comments are redundant with AI-based PR review, and create noise. + - Do not restate the PR description or explain what the code does. Focus on SonarQube issues. + - Do not list things that look good or provide general praise. + - If you cannot access the sonarqube MCP server, retry once. If you still cannot, do not proceed. Instead, comment "Unable to access SonarQube data, cannot perform review." Never attempt to synthesize what SonarQube would have said. + - Don't propose unnecessary changes or refactorings beyond fixing the issues + - Ensure fixes don't introduce new problems + - Never create inline comments to note that issues are resolved. + - Do not add inline comments for praise. + + Begin by querying SonarQube for issues on this PR. + # Tools + plugin_marketplaces: | + https://github.com/anthropics/claude-plugins-official.git + plugins: | + context7@claude-plugins-official + claude_args: | + --max-turns 50 + --mcp-config '{ + "mcpServers": { + "sonarqube": { + "command": "docker", + "args": ["run", "-i", "--rm", "-e", "SONARQUBE_URL", "-e", "SONARQUBE_TOKEN", "mcp/sonarqube"] + } + } + }' + --allowedTools " + mcp__context7__*, + mcp__sonarqube__*, + mcp__github_ci__*, + mcp__github_inline_comment__create_inline_comment, + Bash(gh pr comment:*), + Bash(gh pr diff:*), + Bash(gh pr view:*), + Read, + Grep, + Glob, + Task, + WebFetch, + WebSearch + " + + env: + SONARQUBE_URL: ${{ secrets.SONAR_HOST_URL }} + SONARQUBE_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: Enforce Quality Gate Result + id: enforce-quality-gate + if: ${{ !cancelled() }} + env: + QUALITY_GATE_OUTCOME: ${{ steps.quality-gate.outcome }} + run: | + if [ "$QUALITY_GATE_OUTCOME" = "failure" ]; then + echo "Quality Gate failed. See Claude Code comments for remediation suggestions." + exit 1 + fi + # --- End Claude Code remediation steps diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..44d12c6 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,21 @@ +# Server and project configuration +# Note: SONAR_HOST_URL secret in the workflow takes precedence at scan time. +# This entry documents the expected host and is a no-op when the secret is set. +sonar.host.url=https://sonar.wpengine.io/ +sonar.projectVersion=1.0 +sonar.sourceEncoding=UTF-8 +sonar.scm.provider=git + +# Project identifiers +sonar.projectName=example-sage-theme +sonar.projectKey=wpengine_example-sage-theme_c16e3095-fcf4-41b8-b8b7-d60c822888f4 + +# Paths to source code directories (relative paths) +sonar.sources=wp-content + +# Exclusions +sonar.exclusions=**/node_modules/**,**/vendor/**,docs/** + +# Paths to test code directories (relative paths) +sonar.tests=wp-content +sonar.test.inclusions=**/*Test.php,**/*.test.js,**/*.test.ts