CI Integration with GitHub Actions, GitLab CI, Jenkins, and Other Runners: Guided Hands-On Workflow
Build a portable local analysis launcher, generate coverage before scanning, then translate the same evidence contract into a pinned GitHub Actions workflow with explicit Quality Gate enforcement.
Learning objectives
- Create a small tested Python project whose coverage report is generated before analysis.
-
Implement a provider-neutral local launcher that records runner,
revision, tests, coverage, scanner,
ceTaskId, Compute Engine, and gate evidence. - Run the launcher once in asynchronous mode and once with explicit Quality Gate waiting.
- Translate the same evidence ordering into a GitHub Actions workflow using reviewed/pinned action revisions.
-
Retain first-failure logs and reports with an
always()evidence-upload step while keeping secrets redacted. - Explain how the same contract maps to GitLab and Jenkins without duplicating those platforms' full courses.
1. Disposable lab contract
| Item | Lab assumption |
|---|---|
| SonarQube |
Community Build 26.9.0.129388 at
http://localhost:9000
|
| Scanner | SonarScanner CLI 8.1.0.6389; modern distribution uses Java 21 embedded/provisioned runtime semantics |
| Project |
sq-ch19-ci-lab, main branch only for Community
Build
|
| Python test stack |
Python 3.13 in GitHub example; pytest==9.1.1,
pytest-cov==7.1.0
|
| GitHub scan action |
v8.2.1, pinned in the teaching YAML to commit
22918119ff8e1ca75a623e15c8296b6ea4fbe28f
|
| GitHub gate action |
v1.2.0, pinned to
cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b
|
| Credentials |
Disposable SonarQube project-analysis token; GitHub example
reads secrets.SONAR_TOKEN
|
2. Create the tested fixture and pin the test producer
mkdir -p sq-ch19-ci-lab/src sq-ch19-ci-lab/tests sq-ch19-ci-lab/ci
cd sq-ch19-ci-lab
git init -b main
git config user.email "learner@example.invalid"
git config user.name "SonarQube Learner"
cat > src/calculator.py <<'PYCODE'
def classify(value: int) -> str:
if value < 0:
return "negative"
if value == 0:
return "zero"
return "positive"
PYCODE
cat > tests/test_calculator.py <<'PYCODE'
from src.calculator import classify
def test_classify():
assert classify(-1) == "negative"
assert classify(0) == "zero"
assert classify(2) == "positive"
PYCODE
cat > requirements-ci.txt <<'EOF'
pytest==9.1.1
pytest-cov==7.1.0
EOF
cat > sonar-project.properties <<'EOF'
sonar.projectKey=sq-ch19-ci-lab
sonar.projectName=SQ Chapter 19 CI Lab
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
sonar.sourceEncoding=UTF-8
EOF
cat > .gitignore <<'EOF'
.venv/
__pycache__/
.pytest_cache/
.coverage
coverage.xml
.scannerwork/
evidence/
.sonar/
EOF
git add .
git commit -m "chapter19 CI fixture"
git rev-parse HEAD
Create the disposable project in SonarQube first, then create a project-analysis token. Export it only in the current shell. The token is not part of the fixture.
3. Provider-neutral launcher: preserve evidence around each boundary
The launcher below deliberately separates test/report production from scanner execution and preserves evidence even when the scanner fails. It does not echo the token and it refuses to run when the token/host URL are missing.
cat > ci/run-analysis.sh <<'BASH'
#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
: "${SONAR_HOST_URL:?SONAR_HOST_URL is required}"
: "${SONAR_TOKEN:?SONAR_TOKEN is required}"
: "${SONAR_PROJECT_KEY:=sq-ch19-ci-lab}"
: "${SONAR_GATE_WAIT:=false}"
mkdir -p evidence
{
date -u +'%Y-%m-%dT%H:%M:%SZ'
printf 'project=%s\n' "$SONAR_PROJECT_KEY"
printf 'revision='; git rev-parse HEAD
printf 'shallow='; git rev-parse --is-shallow-repository
python --version
sonar-scanner --version
printf 'SONAR_TOKEN=set\n'
printf 'SONAR_HOST_URL=%s\n' "$SONAR_HOST_URL"
} | tee evidence/preflight.txt
# 1) Build/test/report evidence comes first.
set +e
python -m pytest -q \
--junitxml=evidence/junit.xml \
--cov=src --cov-report=term \
--cov-report=xml:coverage.xml \
2>&1 | tee evidence/test.log
TEST_RC=${PIPESTATUS[0]}
set -e
if [ "$TEST_RC" -ne 0 ]; then
printf 'test_exit=%s\n' "$TEST_RC" | tee evidence/final-state.txt
exit "$TEST_RC"
fi
test -s coverage.xml
cp coverage.xml evidence/coverage.xml
# 2) Scanner. Environment variables carry authentication.
scan_args=()
if [ "$SONAR_GATE_WAIT" = "true" ]; then
scan_args+=("-Dsonar.qualitygate.wait=true" "-Dsonar.qualitygate.timeout=300")
fi
set +e
sonar-scanner -X "${scan_args[@]}" 2>&1 | tee evidence/scanner.log
SCAN_RC=${PIPESTATUS[0]}
set -e
printf 'scanner_exit=%s\n' "$SCAN_RC" >> evidence/final-state.txt
# Preserve task metadata whenever upload reached that point, even if gate waiting failed.
if [ -f .scannerwork/report-task.txt ]; then
cp .scannerwork/report-task.txt evidence/report-task.txt
CE_TASK_ID="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
printf 'ceTaskId=%s\n' "$CE_TASK_ID" >> evidence/final-state.txt
fi
exit "$SCAN_RC"
BASH
chmod +x ci/run-analysis.sh
4. Run locally: first asynchronous, then explicit gate enforcement
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements-ci.txt
python -m pip freeze | tee evidence-packages.txt
export SONAR_HOST_URL="http://localhost:9000"
export SONAR_PROJECT_KEY="sq-ch19-ci-lab"
read -rsp "Disposable Sonar token: " SONAR_TOKEN; echo
export SONAR_TOKEN
# Run 1: default asynchronous scanner behavior.
SONAR_GATE_WAIT=false ./ci/run-analysis.sh
cat evidence/report-task.txt
# Query the saved ceTaskId after preserving the scanner evidence.
CE_TASK_ID="$(sed -n 's/^ceTaskId=//p' evidence/report-task.txt)"
curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
"$SONAR_HOST_URL/api/ce/task?id=$CE_TASK_ID" \
| tee evidence/ce-task.json
# After CE is SUCCESS, query the gate independently.
curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
"$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY" \
| tee evidence/gate.json
Run 1 proves the separation: a zero scanner exit does not tell you the gate result until the background task is complete.
# Run 2: make the CI/scanner step wait for the gate.
rm -rf evidence .scannerwork
SONAR_GATE_WAIT=true ./ci/run-analysis.sh
printf 'wait-mode scanner exit=%s\n' "$?"
If the project gate fails, the wait-mode scanner step fails even if
analysis/report upload itself succeeded. Preserve
scanner.log and report-task.txt before
changing policy or rerunning.
5. Translate the contract into GitHub Actions
For Community Build, restrict this executable workflow to
main. The SonarSource scan and gate actions are pinned
to immutable commits in this teaching example; record the
human-readable release next to each SHA so update review is
intentional.
name: SonarQube CI
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
analyze:
runs-on: ubuntu-latest
env:
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
steps:
- name: Checkout full history
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
cache: 'pip'
- name: Install test tools
run: python -m pip install -r requirements-ci.txt
- name: Test and create coverage before scan
shell: bash
run: |
set -o pipefail
mkdir -p evidence
git rev-parse HEAD | tee evidence/revision.txt
python -m pytest -q \
--junitxml=evidence/junit.xml \
--cov=src --cov-report=term \
--cov-report=xml:coverage.xml \
2>&1 | tee evidence/test.log
test -s coverage.xml
cp coverage.xml evidence/coverage.xml
- name: SonarQube scan
uses: SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
- name: Preserve task identity
run: |
mkdir -p evidence
cp .scannerwork/report-task.txt evidence/report-task.txt
sed -n 's/^ceTaskId=/ceTaskId=/p' .scannerwork/report-task.txt \
| tee evidence/ce-task-id.txt
- name: Enforce Quality Gate
uses: SonarSource/sonarqube-quality-gate-action@cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b # v1.2.0
timeout-minutes: 10
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }}
- name: Retain evidence even after a failure
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sonarqube-ci-evidence-${{ github.run_id }}
if-no-files-found: warn
path: |
evidence/
coverage.xml
.scannerwork/report-task.txt
secrets.SONAR_TOKEN. Do not copy the value into
YAML, job outputs, artifact names, or debugging
echo statements. A self-hosted SonarQube instance must
also be reachable from the chosen runner through an authorized
network path.
6. Map the same evidence contract to GitLab and Jenkins
| State | GitLab CI/CD | Jenkins |
|---|---|---|
| Full history | GIT_DEPTH: "0" |
Configure SCM checkout with adequate history; verify with Git. |
| Scanner cache |
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar",
cache .sonar/cache
|
Plugin/tool installation or controlled scanner cache on agents. |
| Secret | Masked/protected SONAR_TOKEN CI variable |
Jenkins Credentials + configured SonarQube installation |
| Gate wait |
sonar.qualitygate.wait=true when the job must
fail on gate
|
waitForQualityGate; webhook to
/sonarqube-webhook/ is mandatory
|
| Evidence retention | artifacts: when: always |
post { always { archiveArtifacts ... } } |
7. Challenge: choose the correct layer
The GitHub job reaches the gate step but fails with “metadata file not found.” Tests and the scan log look successful. What should you inspect?
- Check whether the scan ran in the same workspace/base directory as the gate action.
-
Check whether
.scannerwork/report-task.txtwas actually created. - Check whether a cleanup or separate job boundary removed the scanner workspace.
- Do not lower the Quality Gate or regenerate a token: neither causes a missing metadata file.
Knowledge check
Why pin SonarSource actions to immutable commits in the example?
It makes the executed action revision auditable and prevents an unreviewed tag movement from silently changing the pipeline. The release number remains in the comment for update tracking.
Why upload evidence with if: always()?
So first-failure test/report/task artifacts survive even when an earlier step or gate fails.
What does the Quality Gate Action read by default?
The scanner metadata file at
.scannerwork/report-task.txt, which links the CI
job to the server background task.
Why does the workflow run tests before the scan?
Because coverage/test outputs are scanner inputs. They must exist before the analysis report is created and uploaded.
Does using GitHub Actions enable Community Build pull-request analysis?
No. CI execution and licensed branch/PR analysis are separate product capabilities.
Official references and version notes
-
SonarQube Community Build — CI integration overview
— current gate-wait mechanisms, Jenkins/GitHub/Bitbucket options,
sonar.qualitygate.wait, and 300-second default timeout. -
Adding analysis to GitHub Actions
— current Scan Action v8 guidance,
SONAR_TOKEN/SONAR_HOST_URL, Community Build main-only workflow, and full-history checkout recommendation. - SonarQube Scan Action v8.2.1 — current action release used/pinned in the teaching workflow.
-
SonarQube Quality Gate Check Action v1.2.0
— current gate-action release; default scanner metadata file is
.scannerwork/report-task.txt. -
Adding analysis to GitLab CI/CD
— Docker executor,
GIT_DEPTH: "0",SONAR_USER_HOMEcache, CI variables, and gate wait guidance. - Jenkins integration key features — SonarQube Scanner plugin behavior and Quality Gate integration.
-
Jenkins pipeline pause
—
withSonarQubeEnv, mandatory/sonarqube-webhook/, andwaitForQualityGate. - Verifying code checkout — full SCM history and shallow-clone failure guidance.
- SonarScanner CLI 8.1.0.6389 — scanner baseline.
- SonarQube release announcements — Community Build 26.9.0.129388, Server 2026 Release 4.1, and 2026 Release 1.5 LTA.
Rechecked 2026-09-08. Mandatory labs target SonarQube Community
Build 26.9.0.129388 and SonarScanner CLI
8.1.0.6389. The GitHub example pins SonarQube
Scan Action v8.2.1 to commit
22918119ff8e1ca75a623e15c8296b6ea4fbe28f, Quality
Gate Check Action v1.2.0 to
cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b, checkout
v6.1.0 to d23441a48e516b6c34aea4fa41551a30e30af803,
setup-python v6.2.0 to
a309ff8b426b58ec0e2a45f0f869d46889d02405, and
upload-artifact v4.6.2 to
ea165f8d65b6e75b540449e92b4886f43607fa02. Current
SonarSource Community Build guidance restricts GitHub analysis to
the main branch and recommends full Git history; GitLab examples
use GIT_DEPTH: "0". Jenkins requires the SonarQube
Scanner plugin (2.11+ per current docs) and a SonarQube webhook
for waitForQualityGate. Recheck action/plugin
versions, runner requirements, scanner/JRE behavior, provider
deprecations, and edition boundaries before copying the examples
into another release.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.