Skip to content

Commit b30ddb8

Browse files
[AAP-52663] Refactor SonarCloud workflow to use workflow_run pattern (ansible#857)
## Summary Refactor the SonarCloud analysis workflow to use the `workflow_run` trigger pattern instead of executing directly in the CI workflow. This provides better separation of concerns and ensures coverage data is always available before analysis runs. ## Changes ### 1. Split SonarCloud workflow into two jobs - **sonar-pr-analysis**: Runs for pull requests with PR-specific quality gate analysis - **sonar-branch-analysis**: Runs for branch pushes (devel/stable-*) with full codebase analysis ### 2. Security improvements - Pin third-party actions (`dawidd6/action-download-artifact`, `SonarSource/sonarcloud-github-action`) to commit SHAs - Add `permissions: read-all` for principle of least privilege - Use environment variables for sensitive data ### 3. Robustness improvements - Better error handling with clear error messages when PR metadata is missing - Use `gh api` with `jq` for PR metadata extraction (more reliable than octokit action) - Comprehensive logging showing analysis decisions - Coverage availability detection and status reporting ### 4. Removed direct SonarCloud execution from CI - Lines 69-74 removed from `.github/workflows/ci.yml` - SonarCloud now runs via `workflow_run` trigger after CI completes - Ensures coverage artifact is always available ## Testing - Workflow will trigger on next PR to verify PR analysis job - Branch push to devel will verify branch analysis job ## JIRA https://issues.redhat.com/browse/AAP-52663 Co-authored-by: Claude <[email protected]>
1 parent cfcb7b9 commit b30ddb8

File tree

2 files changed

+116
-158
lines changed

2 files changed

+116
-158
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,6 @@ jobs:
6666
name: coverage
6767
path: coverage.xml
6868

69-
- name: SonarCloud Scan (on push)
70-
uses: SonarSource/sonarqube-scan-action@master
71-
if: matrix.tests.sonar && github.event_name == 'push' && github.repository == 'ansible/django-ansible-base'
72-
env:
73-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74-
SONAR_TOKEN: ${{ secrets.CICD_ORG_SONAR_TOKEN_CICD_BOT }}
75-
7669
- name: Upload jUnit XML test results
7770
if: matrix.tests.junit-xml-upload && github.event_name == 'push' && github.repository == 'ansible/django-ansible-base' && github.ref_name == 'devel'
7871
continue-on-error: true

.github/workflows/sonar-pr.yml

Lines changed: 116 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1,207 +1,172 @@
1-
# SonarCloud PR Workflow for django-ansible-base
1+
# SonarCloud Analysis Workflow for django-ansible-base
22
#
3-
# This workflow runs on every pull request and push to the repository.
3+
# This workflow runs SonarCloud analysis triggered by CI workflow completion.
4+
# It is split into two separate jobs for clarity and maintainability:
45
#
5-
# Steps overview:
6-
# 1. Download coverage data from CI workflow
7-
# 2. Check for backport labels (skip if found)
8-
# 3. Check for Python file changes (skip if none)
9-
# 4. Extract PR info and set environment
10-
# 5. Run SonarCloud analysis with quality gate
6+
# FLOW: CI completes → workflow_run triggers this workflow → appropriate job runs
117
#
12-
# What files are scanned:
13-
# - All Python files (.py) in the repository
14-
# - Excludes: tests, scripts, dev environments, external collections
15-
# - Quality gate focuses on new/changed code in PR only
8+
# JOB 1: sonar-pr-analysis (for PRs)
9+
# - Triggered by: workflow_run (CI on pull_request)
10+
# - Steps: Download coverage → Get PR info → Run SonarCloud PR analysis
11+
# - Scans: All files in the PR
12+
# - Quality gate: Focuses on new/changed code in PR only
13+
#
14+
# JOB 2: sonar-branch-analysis (for long-lived branches)
15+
# - Triggered by: workflow_run (CI on push to devel/stable-*)
16+
# - Steps: Download coverage → Run SonarCloud branch analysis
17+
# - Scans: Full codebase
18+
# - Quality gate: Focuses on overall project health
19+
#
20+
# This ensures coverage data is always available from CI before analysis runs.
1621

1722

1823
# With much help from:
1924
# https://community.sonarsource.com/t/how-to-use-sonarcloud-with-a-forked-repository-on-github/7363/30
2025
# https://community.sonarsource.com/t/how-to-use-sonarcloud-with-a-forked-repository-on-github/7363/32
2126
name: SonarCloud
2227
on:
23-
workflow_run:
28+
workflow_run: # This is triggered by CI being completed.
2429
workflows:
2530
- CI
2631
types:
2732
- completed
28-
workflow_dispatch:
29-
inputs:
30-
pr_number:
31-
description: 'PR number (for push events, use 0)'
32-
required: true
33-
type: string
34-
event_type:
35-
description: 'Event type (push or pull_request)'
36-
required: true
37-
type: string
38-
commit_sha:
39-
description: 'Commit SHA'
40-
required: true
41-
type: string
42-
repository:
43-
description: 'Repository name'
44-
required: true
45-
type: string
46-
ci_run_id:
47-
description: 'CI workflow run ID (for downloading coverage artifact)'
48-
required: false
49-
type: string
5033
permissions: read-all
5134
jobs:
52-
sonar:
53-
name: Upload to SonarCloud
35+
sonar-pr-analysis:
36+
name: SonarCloud PR Analysis
5437
runs-on: ubuntu-latest
5538
if: |
56-
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request') ||
57-
(github.event_name == 'workflow_dispatch')
39+
github.event.workflow_run.conclusion == 'success' &&
40+
github.event.workflow_run.event == 'pull_request' &&
41+
startsWith(github.repository, 'ansible') && endsWith(github.repository, 'django-ansible-base')
5842
steps:
59-
- uses: actions/checkout@v3
43+
- uses: actions/checkout@v4
44+
with:
45+
show-progress: false
6046

61-
# Download coverage artifact (for both workflow_run and workflow_dispatch)
47+
# Download coverage artifact from CI workflow
6248
- name: Download coverage artifact
63-
if: |
64-
github.event_name == 'workflow_run' ||
65-
(github.event_name == 'workflow_dispatch' && inputs.ci_run_id != '')
6649
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615
6750
with:
6851
github_token: ${{ secrets.GITHUB_TOKEN }}
6952
workflow: CI
70-
run_id: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || inputs.ci_run_id }}
53+
run_id: ${{ github.event.workflow_run.id }}
7154
name: coverage
7255

73-
# Set PR number based on trigger type
74-
- name: Set PR number and event type
75-
run: |
76-
if [ "${{ github.event_name }}" = "workflow_run" ]; then
77-
# Extract from coverage.xml for workflow_run
78-
echo "PR_NUMBER=$(grep -m 1 '<!-- PR' coverage.xml | awk '{print $3}')" >> $GITHUB_ENV
79-
echo "EVENT_TYPE=pull_request" >> $GITHUB_ENV
80-
echo "COMMIT_SHA=${{ github.event.workflow_run.head_sha }}" >> $GITHUB_ENV
81-
echo "REPO_NAME=${{ github.event.repository.full_name }}" >> $GITHUB_ENV
82-
else
83-
# Use inputs for workflow_dispatch
84-
echo "PR_NUMBER=${{ inputs.pr_number }}" >> $GITHUB_ENV
85-
echo "EVENT_TYPE=${{ inputs.event_type }}" >> $GITHUB_ENV
86-
echo "COMMIT_SHA=${{ inputs.commit_sha }}" >> $GITHUB_ENV
87-
echo "REPO_NAME=${{ inputs.repository }}" >> $GITHUB_ENV
88-
fi
89-
90-
- name: Get PR info
91-
if: env.EVENT_TYPE == 'pull_request' && env.PR_NUMBER != '0'
92-
uses: octokit/[email protected]
93-
id: pr_info
94-
with:
95-
route: GET /repos/{repo}/pulls/{number}
96-
repo: ${{ env.REPO_NAME }}
97-
number: ${{ env.PR_NUMBER }}
56+
# Extract PR metadata from workflow_run event
57+
- name: Set PR metadata and prepare for analysis
9858
env:
59+
COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
60+
REPO_NAME: ${{ github.event.repository.full_name }}
61+
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
9962
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
100-
101-
- name: Print SonarCloud Analysis Decision Summary
102-
id: print_summary
10363
run: |
10464
echo "🔍 SonarCloud Analysis Decision Summary"
10565
echo "========================================"
106-
107-
# Check CI Event type
108-
if [ "${{ env.EVENT_TYPE }}" = "pull_request" ]; then
109-
echo "├── CI Event: ✅ Pull Request"
110-
111-
# Check backport labels (only for PRs)
112-
if [ "${{ env.PR_NUMBER }}" != "0" ]; then
113-
echo "├── Backport Label: ➖ N/A (Skip check for workflow_dispatch)"
114-
115-
# Check Python changes for PRs
116-
files=$(gh api repos/${{ env.REPO_NAME }}/pulls/${{ env.PR_NUMBER }}/files --jq '.[].filename')
117-
118-
# Get file extensions for summary
119-
extensions=$(echo "$files" | sed 's/.*\.//' | sort | uniq | tr '\n' ',' | sed 's/,$//')
120-
121-
# Check if any Python files were changed
122-
python_files=$(echo "$files" | grep '\.py$' || true)
123-
if [ -z "$python_files" ]; then
124-
echo "├── Python Changes: ❌ None (.$extensions only)"
125-
echo "└── Result: ⏭️ Skip - \"No Python code changes detected\""
126-
exit 0
127-
else
128-
python_count=$(echo "$python_files" | wc -l)
129-
echo "├── Python Changes: ✅ Found ($python_count files)"
130-
echo "└── Result: ✅ Proceed - \"Running SonarCloud analysis\""
131-
fi
132-
else
133-
echo "├── Backport Label: ➖ N/A (Not a PR)"
134-
echo "└── Result: ✅ Proceed - \"Running SonarCloud analysis\""
135-
fi
66+
echo "├── CI Event: ✅ Pull Request"
67+
echo "├── Repo: $REPO_NAME"
68+
69+
# Extract PR number from coverage.xml
70+
if [ -f coverage.xml ]; then
71+
PR_NUMBER=$(grep -m 1 '<!-- PR' coverage.xml | awk '{print $3}' || echo "")
13672
else
137-
# For push events, always proceed
138-
echo "├── CI Event: ✅ Push"
139-
echo "├── Backport Label: ➖ N/A (Push event)"
140-
echo "├── Python Changes: ➖ N/A (Full codebase scan)"
141-
echo "└── Result: ✅ Proceed - \"Running SonarCloud analysis\""
73+
PR_NUMBER=""
14274
fi
143-
env:
144-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
14575
146-
- name: Check if analysis should proceed
147-
id: check_proceed
148-
run: |
149-
# This step only runs if we got past the summary step
150-
# which means all conditions are met for analysis
151-
echo "proceed=true" >> $GITHUB_OUTPUT
152-
echo "✅ All conditions met - proceeding with SonarCloud analysis"
153-
154-
- name: Set PR info into env
155-
if: steps.check_proceed.outputs.proceed == 'true' && env.EVENT_TYPE == 'pull_request' && env.PR_NUMBER != '0' && steps.pr_info.outputs.data != ''
156-
run: |
157-
echo "PR_BASE=${{ fromJson(steps.pr_info.outputs.data).base.ref }}" >> $GITHUB_ENV
158-
echo "PR_HEAD=${{ fromJson(steps.pr_info.outputs.data).head.ref }}" >> $GITHUB_ENV
76+
echo "├── PR Number from coverage.xml: #${PR_NUMBER:-<not found>}"
15977
160-
- name: Get changed Python files for Sonar
161-
if: steps.check_proceed.outputs.proceed == 'true' && env.EVENT_TYPE == 'pull_request' && env.PR_NUMBER != '0'
162-
run: |
163-
files=$(gh api repos/${{ env.REPO_NAME }}/pulls/${{ env.PR_NUMBER }}/files --jq '.[].filename')
164-
echo "All changed files in PR:"
165-
echo "$files"
166-
python_files=$(echo "$files" | grep '\.py$' || true)
167-
if [ -n "$python_files" ]; then
168-
echo "Changed Python files:"
169-
echo "$python_files"
170-
# Convert to comma-separated list for sonar.inclusions
171-
inclusions=$(echo "$python_files" | tr '\n' ',' | sed 's/,$//')
172-
echo "SONAR_INCLUSIONS=$inclusions" >> $GITHUB_ENV
173-
echo "Will scan these Python files: $inclusions"
78+
if [ -z "$PR_NUMBER" ]; then
79+
echo "##[error]❌ FATAL: PR number not found in coverage.xml"
80+
echo "##[error]This job requires a PR number to run PR analysis."
81+
echo "##[error]The CI workflow should have injected the PR number into coverage.xml."
82+
exit 1
17483
fi
175-
env:
176-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17784
178-
- name: Add base branch (for PRs)
179-
if: steps.check_proceed.outputs.proceed == 'true' && env.EVENT_TYPE == 'pull_request' && env.PR_NUMBER != '0'
85+
# Get PR metadata from GitHub API
86+
PR_DATA=$(gh api "repos/$REPO_NAME/pulls/$PR_NUMBER")
87+
PR_BASE=$(echo "$PR_DATA" | jq -r '.base.ref')
88+
PR_HEAD=$(echo "$PR_DATA" | jq -r '.head.ref')
89+
90+
# Print summary
91+
echo "├── Base Branch: $PR_BASE"
92+
echo "├── Head Branch: $PR_HEAD"
93+
echo "├── Coverage Data: ✅ Available"
94+
echo "└── Result: ✅ Running SonarCloud PR analysis"
95+
96+
# Export to GitHub env for later steps
97+
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
98+
echo "PR_BASE=$PR_BASE" >> $GITHUB_ENV
99+
echo "PR_HEAD=$PR_HEAD" >> $GITHUB_ENV
100+
echo "COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV
101+
echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV
102+
103+
- name: Add base branch
104+
if: env.PR_NUMBER != ''
180105
run: |
181106
gh pr checkout ${{ env.PR_NUMBER }}
182107
env:
183108
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
184109

185-
- name: SonarCloud Scan (Pull Request)
186-
if: steps.check_proceed.outputs.proceed == 'true' && env.EVENT_TYPE == 'pull_request' && env.PR_NUMBER != '0'
187-
uses: SonarSource/sonarqube-scan-action@v5
110+
- name: SonarCloud Scan
111+
uses: SonarSource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92
188112
env:
189113
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
190-
SONAR_TOKEN: ${{ secrets[format('{0}', vars.SONAR_TOKEN_SECRET_NAME)] }}
114+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
191115
with:
192116
args: >
193117
-Dsonar.scm.revision=${{ env.COMMIT_SHA }}
194118
-Dsonar.pullrequest.key=${{ env.PR_NUMBER }}
195119
-Dsonar.pullrequest.branch=${{ env.PR_HEAD }}
196120
-Dsonar.pullrequest.base=${{ env.PR_BASE }}
197-
${{ env.SONAR_INCLUSIONS && format('-Dsonar.inclusions={0}', env.SONAR_INCLUSIONS) || '' }}
198121
199-
- name: SonarCloud Scan (Push)
200-
if: steps.check_proceed.outputs.proceed == 'true' && env.EVENT_TYPE == 'push'
201-
uses: SonarSource/sonarqube-scan-action@v5
122+
sonar-branch-analysis:
123+
name: SonarCloud Branch Analysis
124+
runs-on: ubuntu-latest
125+
if: |
126+
github.event_name == 'workflow_run' &&
127+
github.event.workflow_run.conclusion == 'success' &&
128+
github.event.workflow_run.event == 'push' &&
129+
startsWith(github.repository, 'ansible') && endsWith(github.repository, 'django-ansible-base')
130+
steps:
131+
- uses: actions/checkout@v4
132+
with:
133+
show-progress: false
134+
135+
# Download coverage artifact from CI workflow (optional for branch pushes)
136+
- name: Download coverage artifact
137+
continue-on-error: true
138+
uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615
139+
with:
140+
github_token: ${{ secrets.GITHUB_TOKEN }}
141+
workflow: CI
142+
run_id: ${{ github.event.workflow_run.id }}
143+
name: coverage
144+
145+
- name: Print SonarCloud Analysis Summary
146+
env:
147+
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
148+
REPO_NAME: ${{ github.event.repository.full_name }}
149+
run: |
150+
echo "🔍 SonarCloud Analysis Summary"
151+
echo "=============================="
152+
echo "├── CI Event: ✅ Push (via workflow_run)"
153+
echo "├── Repo: $REPO_NAME"
154+
echo "├── Branch: $BRANCH_NAME"
155+
156+
if [ -f coverage.xml ]; then
157+
echo "├── Coverage Data: ✅ Available"
158+
else
159+
echo "├── Coverage Data: ⚠️ Not available (analysis will proceed without coverage)"
160+
fi
161+
162+
echo "└── Result: ✅ Running SonarCloud branch analysis"
163+
164+
- name: SonarCloud Scan
165+
uses: SonarSource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92
202166
env:
203167
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
204-
SONAR_TOKEN: ${{ secrets[format('{0}', vars.SONAR_TOKEN_SECRET_NAME)] }}
168+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
205169
with:
206170
args: >
207-
-Dsonar.scm.revision=${{ env.COMMIT_SHA }}
171+
-Dsonar.scm.revision=${{ github.event.workflow_run.head_sha }}
172+
-Dsonar.branch.name=${{ github.event.workflow_run.head_branch }}

0 commit comments

Comments
 (0)