qasim@wiki:~$

Wiring the Toolchain

Credentials, scanner install, SCM polling, and SMTP — the connective tissue between Jenkins, SonarQube, and Bitbucket.

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

Non-interactive shells do not source .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
Files analysed without type information in the scanner log means those files aren't covered by any tsconfig.json. Analysis still runs but with reduced rule coverage — worth fixing by extending the tsconfig include patterns.

2 · Connecting Jenkins to SonarQube

Step 1 — Store the token as a credential

Manage Jenkins → Credentials → System → Global → Add Credentials

KindSecret text
Secretthe SonarQube analysis token
IDsonarqube-token
Step 2 — Register the server

Manage Jenkins → System → SonarQube servers → Add SonarQube

NameSonarQube Server — referenced verbatim by withSonarQubeEnv()
Server URLhttp://10.10.1.231:9000 — no trailing slash
Authentication tokenselect sonarqube-token
The server Name is a hard-coded string in every Jenkinsfile. 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.
Step 3 — What 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.

  1. In Bitbucket: Personal settings → App passwords → Create app password
  2. Grant Repositories: Read only. The CI system never needs write access.
  3. 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
URL form dictates credential type. An 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.

ApproachDirectionExposureLatency
WebhookSCM → Jenkins (inbound)Controller must be publicSeconds
PollingJenkins → SCM (outbound)NonePoll 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.

Trading a few minutes of latency for zero inbound exposure is almost always the right call for internal CI. When a stakeholder asks for webhooks, the question to answer is whether the latency actually blocks anyone — for a staging-to-UAT promotion flow, it does not.

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 serversmtp.zoho.com
SMTP port587
Use TLSchecked (STARTTLS)
Credentialsaccount username + app-specific password
Default Content TypePlain Text
Two SMTP failures worth knowing in advance.

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

#CheckPass condition
1Scanner runs as jenkins usersudo -u jenkins /opt/sonar-scanner/bin/sonar-scanner --version prints a version
2SonarQube server name matches JenkinsfileString-identical to the withSonarQubeEnv() argument
3Token stored as Secret textCredential ID resolves in the SonarQube server config
4Bitbucket credential type matches URL schemeHTTPS URL ↔ username/password
5Polling configured, no webhook neededpollSCM present in the Jenkinsfile triggers
6System Admin email = SMTP usernamePrevents 553 relay rejection
7Test email deliversUse Test configuration by sending test e-mail in the E-mail Notification section
8Jenkins URL set correctly${env.BUILD_URL} in emails produces a working link