Wiring the Toolchain
Credentials, scanner install, SCM polling, and SMTP — the connective tissue between Jenkins, SonarQube, and Bitbucket.
Part 3 of 7
1 · sonar-scanner CLI
The Jenkins SonarQube plugin can manage the scanner as a tool, but installing it to a fixed path is more predictable and makes the Jenkinsfile explicit about what it runs.
cd /opt
sudo unzip sonar-scanner-cli-6.2.1.4610-linux-x64.zip
sudo mv sonar-scanner-6.2.1.4610-linux-x64 sonar-scanner
sudo chown -R root:root /opt/sonar-scanner
# Verify as the jenkins user, not as root
sudo -u jenkins /opt/sonar-scanner/bin/sonar-scanner --version
The PATH trap
.bashrc.
Jenkins sh steps run in a non-interactive, non-login shell. Adding PATH exports to ~/.bashrc alone means the tool works when you test it manually with sudo -u jenkins -i, then fails inside the pipeline with command not found. This costs an afternoon the first time it happens.
Two robust options — use either, not neither:
# Option A — export in BOTH files
for f in /var/lib/jenkins/.bashrc /var/lib/jenkins/.profile; do
echo 'export PATH="/opt/sonar-scanner/bin:$PATH"' | sudo tee -a "$f"
done
sudo chown jenkins:jenkins /var/lib/jenkins/.bashrc /var/lib/jenkins/.profile
# Verify the way Jenkins will actually invoke it
sudo -u jenkins bash -c 'sonar-scanner --version'
# Option B — call by absolute path in the Jenkinsfile (preferred: explicit, no env dependency)
sh '/opt/sonar-scanner/bin/sonar-scanner -Dsonar.projectKey=$SONAR_PROJECT_KEY'
Option B is used throughout this series. It removes an entire class of environment-dependent failure.
sonar-project.properties
Committed to the repo root. The scanner reads it automatically; CLI flags override it.
# sonar-project.properties
sonar.projectKey=simplex_kfc_api_server_stg
sonar.projectName=Simplex KFC API Server Staging
sonar.sources=src
sonar.tests=test
sonar.sourceEncoding=UTF-8
# TypeScript: point at tsconfig so the analyser gets type information
sonar.typescript.tsconfigPath=tsconfig.json
# Coverage report produced by jest --coverage
sonar.javascript.lcov.reportPaths=coverage/lcov.info
# Never analyse build output or dependencies
sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,**/*.spec.ts
tsconfig.json. Analysis still runs but with reduced rule coverage — worth fixing by extending the tsconfig include patterns.2 · Connecting Jenkins to SonarQube
Manage Jenkins → Credentials → System → Global → Add Credentials
| Kind | Secret text |
| Secret | the SonarQube analysis token |
| ID | sonarqube-token |
Manage Jenkins → System → SonarQube servers → Add SonarQube
| Name | SonarQube Server — referenced verbatim by withSonarQubeEnv() |
| Server URL | http://10.10.1.231:9000 — no trailing slash |
| Authentication token | select sonarqube-token |
withSonarQubeEnv('SonarQube Server') fails if the configured name differs by even one character. Choose a short, stable name — renaming it later means editing every pipeline in the estate.withSonarQubeEnv actually does
It injects SONAR_HOST_URL and SONAR_AUTH_TOKEN into the shell environment, and — critically — records the analysis task ID so a later waitForQualityGate knows which report to poll for.
stage('SonarQube Scan') {
steps {
withSonarQubeEnv('SonarQube Server') {
sh '/opt/sonar-scanner/bin/sonar-scanner -Dsonar.projectKey=$SONAR_PROJECT_KEY'
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
waitForQualityGate must be in the same pipeline run as the withSonarQubeEnv block that produced the report. The timeout wrapper matters: without it, a stuck Compute Engine task hangs the build indefinitely and holds an executor.
3 · Connecting Jenkins to Bitbucket
HTTPS with an app password (recommended)
Simpler than SSH: no key distribution, works with folder-scoped credentials, and rotates cleanly.
- In Bitbucket: Personal settings → App passwords → Create app password
- Grant Repositories: Read only. The CI system never needs write access.
- In Jenkins: Credentials → Add → Username with password
- Username — the Bitbucket account name
- Password — the generated app password
- ID — e.g.
bitbucket-ci
# Repository URL form to use in the job config
https://bitbucket.org/<workspace>/<repo>.git
# NOT the SSH form, unless you have deliberately set up an SSH credential
git@bitbucket.org:<workspace>/<repo>.git
https:// URL needs a username/password credential; a git@ URL needs an SSH private key credential. Pairing an SSH URL with a username/password credential produces an authentication failure that reads like a permissions problem.SCM polling instead of webhooks
A webhook requires Bitbucket to reach into the network to hit Jenkins — meaning the controller must be internet-facing. Polling inverts the direction: Jenkins reaches out on a schedule.
| Approach | Direction | Exposure | Latency |
|---|---|---|---|
| Webhook | SCM → Jenkins (inbound) | Controller must be public | Seconds |
| Polling | Jenkins → SCM (outbound) | None | Poll interval |
triggers {
// Every ~7 minutes, 00:00-02:59 and 05:00-23:59, Mon-Sat
pollSCM('H/7 0-2,5-23 * * 1-6')
}
The H is a Jenkins extension meaning "hash the job name to pick a consistent minute offset". With dozens of jobs, H/7 spreads polls evenly instead of firing them all simultaneously and hammering the SCM API.
4 · SMTP notifications
Without email, a failed gate is invisible until someone opens Jenkins. Configure both the built-in mailer and Email Extension — some plugins use one, some the other.
Manage Jenkins → System → Extended E-mail Notification
| SMTP server | smtp.zoho.com |
| SMTP port | 587 |
| Use TLS | checked (STARTTLS) |
| Credentials | account username + app-specific password |
| Default Content Type | Plain Text |
553 relay error / "sender address rejected". Most providers require the envelope sender to match the authenticated account. In Jenkins, Manage Jenkins → System → Jenkins Location → System Admin e-mail address must be identical to the SMTP username. A mismatch here is the single most common cause of Jenkins email failing.
Microsoft 365 SMTP AUTH disabled. Basic SMTP authentication is disabled tenant-wide by default on modern M365 tenants. Unless a tenant admin explicitly re-enables it for the mailbox, no amount of Jenkins configuration will work — use a provider that supports app passwords instead.
Making email failures non-fatal
A notification problem should never fail an otherwise-good build. Wrap every send:
script {
try {
emailext(
to: "${committerEmail}, lead@example.com",
subject: "[STG] ${status} — ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Console: ${env.BUILD_URL}console"
)
} catch (Exception e) {
echo "WARNING: notification failed (${e.message})"
}
}
Emailing the person who broke it
Notifying a team alias trains everyone to ignore the alias. Notifying the committer directly gets the fix.
def committerEmail = sh(script: "git log -1 --pretty=format:'%ae'", returnStdout: true).trim()
def committerName = sh(script: "git log -1 --pretty=format:'%an'", returnStdout: true).trim()
def commitMsg = sh(script: "git log -1 --pretty=format:'%s'", returnStdout: true).trim()
// Fall back to a team address if the commit has no usable author email
def notifyTo = committerEmail?.trim() ? committerEmail : 'devops@example.com'
Connectivity verification
Test each link independently before running a pipeline. Debugging one hop at a time is far faster than reading a failed build log.
# Jenkins → SonarQube
sudo -u jenkins curl -sS -o /dev/null -w '%{http_code}\n' \
-u $SONAR_TOKEN: http://10.10.1.231:9000/api/system/status
# Expect 200
# Jenkins → Bitbucket (uses the stored credential via the job, but verify DNS/TLS reachability)
sudo -u jenkins curl -sS -o /dev/null -w '%{http_code}\n' https://bitbucket.org
# Expect 200
# Jenkins → SMTP
nc -zv smtp.zoho.com 587
# Jenkins → deploy target (detail in Part 5)
sudo -u jenkins ssh -i /var/lib/jenkins/.ssh/deploy_key \
-o StrictHostKeyChecking=no ubuntu@10.10.1.223 'echo reachable'
Integration checklist
| # | Check | Pass condition |
|---|---|---|
| 1 | Scanner runs as jenkins user | sudo -u jenkins /opt/sonar-scanner/bin/sonar-scanner --version prints a version |
| 2 | SonarQube server name matches Jenkinsfile | String-identical to the withSonarQubeEnv() argument |
| 3 | Token stored as Secret text | Credential ID resolves in the SonarQube server config |
| 4 | Bitbucket credential type matches URL scheme | HTTPS URL ↔ username/password |
| 5 | Polling configured, no webhook needed | pollSCM present in the Jenkinsfile triggers |
| 6 | System Admin email = SMTP username | Prevents 553 relay rejection |
| 7 | Test email delivers | Use Test configuration by sending test e-mail in the E-mail Notification section |
| 8 | Jenkins URL set correctly | ${env.BUILD_URL} in emails produces a working link |