Every screenshot and every pipeline in this deck comes from a real local Jenkins 2.580.1
JENKINS_HOMEJenkins is an open-source automation server, written in Java. You run it yourself; it watches your repositories, schedules jobs (builds, tests, deployments, anything scriptable) onto machines you provide, and records every run. Almost everything beyond the core is a plugin.
| When | What happened | Source |
|---|---|---|
| 2004 | Kohsuke Kawaguchi starts Hudson while working at Sun Microsystems | contributors.jenkins.io |
| 2010 | Oracle acquires Sun; a dispute follows over control of the Hudson name | InfoQ, Jan 2011 |
| 29 Jan 2011 | Community vote closes: 214 to rename, 14 for the status quo; the project continues as Jenkins (Oracle kept Hudson, so in practice a fork) | jenkins.io blog |
| 26 Apr 2016 | Jenkins 2.0: Pipeline (the Jenkinsfile) becomes the headline way to define jobs | jenkins.io blog |
| 12 Mar 2019 | Founding project of the Continuous Delivery Foundation (Linux Foundation), with Jenkins X, Spinnaker and Tekton | jenkins.io blog |
| Aug 2020 | First project to graduate in the CD Foundation | jenkins.io blog |
Two release lines: weekly and LTS (long-term support, a stabilised weekly every few months). This deck ran the LTS release 2.580.1.
agent { label 'linux' } picks any node carrying itThe local Jenkins behind this deck has one executor on the built-in node: fine for a one-person localhost demo, not for a team. See Using agents.
jenkins-plugin-cliJENKINS_HOMEconfig.xml, jobs/ (one dir per job, with build records), plugins/, secrets/, workspace/For this deck the Timestamper plugin had to be added before options { timestamps() } would run: the step names you can use depend on what is installed.
Manage Jenkins → Plugins → Installed, filtered to "pipeline" (local Jenkins 2.580.1). Docs: Managing plugins
Dashboard: every job, its last result (S), a "weather" icon for recent stability (W), last success/failure and duration. The deck's demo jobs are the Intro_* rows.
New Item: pick a job type. Pipeline for one Jenkinsfile; Multibranch Pipeline for one job per branch/PR; Freestyle is the classic form-driven job.
| Freestyle | Pipeline | |
|---|---|---|
| Defined in | web forms (stored as job XML) | a Jenkinsfile (Groovy DSL) |
| Reviewable | no diff, no PR | versioned with the code |
| Stages | one linear build | stages, parallel branches, multiple agents |
| Survives restart | build is lost | pipelines resume (durable) |
| Use for | a quick one-off | everything else |
Jenkinsfile. This deck's first pipeline is read from demo/Jenkinsfile in its own repoKeep pipeline-as-code in the repo: the pipeline changes in the same PR as the code it builds.
Configure → Pipeline for job Intro_params_and_post: an inline script, run in the Groovy sandbox.
pipeline {
agent any
stages {
stage('Build') {
steps { sh 'echo building' }
}
stage('Test') {
steps {
script {
for (n in ['unit', 'smoke']) {
sh "echo running ${n} tests"
}
}
}
}
}
post {
always { echo 'cleanup' }
}
}
node {
try {
stage('Build') {
sh 'echo building'
}
stage('Test') {
for (n in ['unit', 'smoke']) {
sh "echo running ${n} tests"
}
}
} finally {
echo 'cleanup'
}
}
Declarative: a fixed structure (pipeline → stages → steps), validated before it runs, with post, when, options built in. Escape into Groovy with script { }. Start here.
Scripted: plain Groovy around node and stage. Loops, maps and try/finally anywhere; you write your own cleanup and error handling. Use when declarative genuinely can't express it.
pipeline {
agent any
options {
timeout(time: 10, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
}
environment {
APP_NAME = 'calc'
}
stages {
stage('Setup') {
steps {
dir('demo') {
sh '[ -x .venv/bin/pytest ] || { python3 -m venv .venv && .venv/bin/pip install -q pytest; }'
}
}
}
stage('Test') {
steps {
dir('demo') {
// delete last build's report first: a reused workspace keeps it
sh 'rm -rf reports && .venv/bin/pytest --junitxml=reports/junit.xml'
}
}
}
stage('Package') {
steps {
dir('demo') {
sh 'rm -rf dist && mkdir dist && tar czf dist/${APP_NAME}-${BUILD_NUMBER}.tar.gz calc.py'
}
}
}
}
post {
always { junit 'demo/reports/*.xml' }
success { archiveArtifacts artifacts: 'demo/dist/*.tar.gz', fingerprint: true }
failure { echo "Build ${env.BUILD_NUMBER} failed: see the console" }
}
}
Intro_hello_pipeline, "Pipeline script from SCM", script path demo/JenkinsfileBUILD_NUMBERtest_mean_of_empty_list; mean([]) divides by zero → FAILURE, 1 of 4 failedThe module, tests and Jenkinsfile are in demo/.
pipeline {
agent any
parameters {
choice(name: 'TARGET', choices: ['staging', 'production'], description: 'Where to deploy')
booleanParam(name: 'RUN_SLOW_TESTS', defaultValue: false, description: 'Also run the slow suite')
booleanParam(name: 'FAIL', defaultValue: false, description: 'Make the Unit stage fail')
}
options {
timeout(time: 5, unit: 'MINUTES')
}
stages {
stage('Checks') {
parallel {
stage('Lint') {
steps { sh 'echo lint ok' }
}
stage('Unit') {
steps { sh "echo unit; test ${params.FAIL} = false" }
}
stage('Slow') {
when { expression { params.RUN_SLOW_TESTS } }
steps { sh 'sleep 3; echo slow ok' }
}
}
}
stage('Upload') {
steps {
withCredentials([string(credentialsId: 'results-upload-token', variable: 'TOKEN')]) {
// single quotes: the shell expands $TOKEN, Groovy never sees it
sh 'echo "token is ${#TOKEN} chars: $TOKEN"'
}
}
}
stage('Deploy') {
when { expression { params.TARGET == 'production' } }
steps { echo "Deploying to ${params.TARGET}" }
}
}
post {
always { echo "post/always: ${currentBuild.currentResult}" }
success { echo 'post/success: notify the channel' }
failure { echo 'post/failure: email the committer' }
}
}
agent: where to run (any, { label 'x' }, per stage too)parameters: a form on "Build with Parameters"; read as params.Xoptions: timeout, timestamps, buildDiscarder, retry…environment: env vars for every step (see Common Gotchas)parallel: sibling stages run concurrently (given executors)when: skip a stage unless a condition holdspost: always, success, failure, unstable, changed, fixed, cleanup…FAIL=true: Unit fails; Upload and Deploy skipped; post/failure ranRUN_SLOW_TESTS=true TARGET=production: all stages runReference: Pipeline syntax
pipeline {
agent any
triggers {
cron('H 2 * * 1-5') // nightly, Mon-Fri; H spreads the start minute
pollSCM('H/15 * * * *') // poll the repo every 15 min (webhooks are better)
}
stages {
stage('Nightly') {
steps { echo 'nightly build' }
}
}
}
pipeline {
agent { label 'linux' }
stages {
stage('Release') {
when { branch 'main' }
steps { echo 'only on main' }
}
}
post {
fixed { echo 'back to green' }
cleanup { deleteDir() }
}
}
H hashes the job name into the field, so 100 nightly jobs don't all start at 02:00when { branch 'main' } only means something in a multibranch job (see Multibranch Pipelines)/job/NAME/build, or /job/NAME/buildWithParameters once the job has parameters
Stage View (Pipeline: Stage View plugin) on the job page: one row per build, one column per stage, with timings. #3 is red from Test onwards.
Pipeline Graph View (/job/NAME/N/stages) of Intro_params_and_post #3: the parallel Checks fan out, with each step's log beneath.
The console (/job/NAME/N/console) is still the ground truth: every step, its shell trace, and the [Lint] / [Unit] prefixes that tell interleaved parallel output apart. Replay re-runs a build with an edited script; Restart from Stage re-runs from a chosen stage.
Manage Jenkins → Credentials: IDs and descriptions only; values are never shown again after saving.
withCredentials([...]) { } binds a secret to an env var for that block only, and masks it in the log
Correct (single quotes, the Upload stage in Declarative Anatomy): the shell expands $TOKEN; the log prints ****.
Insecure: sh "echo token=${TOKEN}" (double quotes) expands the secret in Groovy before the command reaches the agent: it is copied into the process arguments (visible with ps), and any shell metacharacters in it run. Jenkins warns; the fix is single quotes. See String interpolation.
The job page after five builds: the archived tarball (with its fingerprint) and the Test Result Trend: 3 tests, then 4 with one failure at #3, then 4 passing.
archiveArtifacts copies files from the workspace into the build record on the controller (keep them small; use an artifact repository for big ones)fingerprint: true records an MD5 so you can trace which builds produced or used that exact file
Build #3's test report: test_mean_of_empty_list failed (age 1: it failed for the first time in this build).
junit 'reports/*.xml' reads JUnit XML (pytest, JUnit, cargo-nextest, ctest… all emit it)post { always { } } so failures are still recordedWhen ten repos copy the same 80 lines of Jenkinsfile, move them into a shared library: a Git repo of Groovy that pipelines load by name.
my-library/
├── vars/ # global steps: vars/greet.groovy → greet()
│ └── greet.groovy
├── src/ # Groovy classes (org/acme/Build.groovy)
└── resources/ # files read with libraryResource
vars/greet.groovy// A custom step: any pipeline that loads the library can call greet 'name'
def call(String name = 'world') {
echo "Hello, ${name}, from the shared library"
}
@Library('intro-lib') _
pipeline {
agent any
stages {
stage('Greet') {
steps { greet 'Jenkins' }
}
}
}
Loading library intro-lib@main:demo/shared-lib/
Hello, Jenkins, from the shared library
@Library('name@v1.2') _ pins a version; the trailing _ is the annotation's targetdemo/shared-lib, via the "library path" setting)Docs: Extending with shared libraries. A bigger example drives SimEng 07's regressions.
env.BRANCH_NAME, env.CHANGE_ID (PR number) are set, and when { branch 'main' } / when { changeRequest() } workNot run here. Both are configured in the UI against a hosted forge (credentials, webhooks), which this localhost-only Jenkins doesn't have. See Branches and pull requests.
Blue Ocean was a redesigned UI for pipelines (visual graph, PR-centric views, a pipeline editor). Many tutorials still show it.
jenkins.io: "Blue Ocean has been deprecated since July 2026. It will not receive further security fixes or functionality updates." Don't install it on a new controller.
Source: jenkins.io/doc/book/blueocean. Plugin status changes; check the page before deciding.
A Jenkins controller holds credentials for your repos, registries and clouds, and runs code from every branch. Treat it as production infrastructure.
JENKINS_HOME, secrets included (controller isolation)/script) is full admin powerwithCredentials, never in a Jenkinsfile, a parameter or an echoJENKINS_HOME/secrets securely| Aspect | Jenkins | GitHub Actions | GitLab CI/CD |
|---|---|---|---|
| Hosting | you run the controller and agents | hosted runners, or self-hosted runners | GitLab.com runners, self-managed GitLab, or your own runners |
| Config | Jenkinsfile: Groovy DSL | .github/workflows/*.yml | .gitlab-ci.yml |
| Forge | any Git (or SVN…) host | GitHub only | GitLab (can mirror external repos) |
| Extensibility | 2,100+ plugins; powerful, and each is code you must keep updated | Marketplace actions (pin them by SHA) | built-in features, CI/CD components, templates |
| Maintenance | high: upgrades, plugin compatibility, backups, agents | low on hosted runners | low on GitLab.com; moderate self-managed |
| Cost | free software; you pay for machines and people's time | free for public repos on standard runners; private repos get plan minutes, then pay per minute | plan-based compute minutes for GitLab-hosted runners (Free tier: 400/month); or register your own runners |
| Control | total: air-gapped networks, licensed tools, special hardware | limited on hosted; full on self-hosted runners | full when self-managed |
Pricing and quotas change: check GitHub Actions billing and GitLab compute minutes before deciding. Plugin count: 2,116 in the update centre on 3 October 2026 (plugins.jenkins.io).
New to GitHub Actions? Introduction to GitHub Actions is the hands-on guide: workflows, security, debugging and protecting main, every example run for real.
Mixing them is common: GitHub Actions for fast PR checks, Jenkins for the hardware-in-the-loop or licence-bound regressions that can't run on hosted runners. The hardware and simulation case is the subject of SimEng 07: Jenkins for Simulation Teams.
# needs a supported Java (this deck: JDK 21)
java -jar jenkins.war --httpListenAddress=127.0.0.1 --httpPort=8080
# first start prints an initial admin password, also in
# $JENKINS_HOME/secrets/initialAdminPassword
docker run -p 8080:8080 -p 50000:50000 \
-v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk21
Port 50000 is for inbound agents. Docs: WAR file, Docker, Java support policy. Image tags change; check Docker Hub.
This deck's Jenkins skips the wizard (-Djenkins.install.runSetupWizard=false) and sets itself up from a Groovy script in init.groovy.d/: an admin user, an API token, the jobs.
Jenkinsfile and switch to "Pipeline script from SCM"env in a parameter default ran: Intro_gotchas #1–2parameters {
string(name: 'TOOL_ROOT', defaultValue: "${env.HOME}/tools", description: 'Default built from env.HOME')
string(name: 'MODE', defaultValue: 'quick', description: 'A plain default')
}
environment {
TOOL_FROM_PARAM = "${params.TOOL_ROOT}"
PATH = "${params.TOOL_ROOT}/bin:${env.PATH}"
}
shell: TOOL_FROM_PARAM = null/tools
shell: PATH starts null/tools/bin
parameters is evaluated before the agent exists, so env.HOME is null there and the default becomes the text null/tools. The bad value then flows through environment into sh. (In a step, env.HOME was fine.)
sh '''
TOOL_ROOT="${TOOL_ROOT:-$HOME/tools}"
export PATH="$TOOL_ROOT/bin:$PATH"
echo "shell: PATH starts ${PATH%%:*}"
'''
shell: PATH starts /home/brendan/tools/bin
pkg @ git+… whose version didn't change, so you test old code. Force-reinstall what's under testbuildWithParameters. A plain POST …/build to one returned HTTP 400 here and queued nothingproperties()). A ?: fallback is harmlessThe stale-report and stale-dependency traps hit real builds in SimEng 07, which documents them.
withCredentials, single-quoted shpost { always }; archive and fingerprint artifactsThe Jenkins User Handbook · Pipeline syntax · Pipeline steps reference · Securing Jenkins · Pipeline best practices