Pipeline Architecture¶
Every project repo contains a Jenkinsfile at its root. Jenkins discovers these automatically via the GitHub Organization Folder job and runs them on every push and pull request.
Where things live¶
jenkins-config/ ← you are here
├── casc.yml ← Docker agent template, credentials, shared library registration
├── Dockerfile ← Jenkins controller image with all plugins baked in
└── docker-compose.yml ← Runs the controller + mounts Docker socket
jenkins-shared-library/ ← separate repo
└── vars/
├── runPytest.groovy ← shared Test + Archive stage logic
├── deployMkdocs.groovy ← shared Deploy Docs stage logic
└── generateChangelog.groovy ← shared Update Changelog stage logic
<any-project-repo>/
├── Jenkinsfile ← calls shared library steps; defines what runs
├── cliff.toml ← git-cliff config for changelog generation
└── src/ tests/ etc.
Build agent¶
Each build runs inside a fresh Docker container spun up on demand and discarded after the build. The Jenkins controller never runs build code — it only orchestrates. The Docker socket mount (/var/run/docker.sock) allows the controller to launch agent containers via Docker Desktop.
Agent selection¶
There are three ways a project declares its build environment, in order of preference:
| Scenario | Agent declaration | casc.yml change? |
|---|---|---|
| Single language, standard runtime | agent { label 'python-3.14' } |
Never |
| Multi-language, different stages | agent none + per-stage labels |
Never |
| Multi-language, same stage | agent { dockerfile { filename 'Dockerfile.ci' } } |
Never |
| New commonly-used language | Add template to casc.yml |
Yes — one time |
Pre-defined labels (configured in casc.yml): python-3.14, node-20, java-21, go-1.22, dotnet-8, ruby-3.3
Dockerfile.ci — for projects that need runtimes not covered by a single label, or need a custom combination. Placed in the project repo root, built and cached by Docker on first run:
# Example: Python backend + Node frontend in one image
FROM python:3.14
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs
Auto-detection via shared library¶
When using the shared library, detectAgent() selects the agent automatically based on project files — no agent declaration needed in the Jenkinsfile:
| File present | Agent used |
|---|---|
Dockerfile.ci |
dockerfile { filename 'Dockerfile.ci' } |
pyproject.toml / setup.py |
label 'python-3.14' |
package.json |
label 'node-20' |
pom.xml / build.gradle |
label 'java-21' |
go.mod |
label 'go-1.22' |
*.csproj / *.sln |
label 'dotnet-8' |
Gemfile |
label 'ruby-3.3' |
Stages¶
Install¶
Lives in: project Jenkinsfile (or runPytest shared step)
Agent: python:3.14 container
What it does: pip install -e ".[dev]" — installs the project and its dev dependencies into the ephemeral agent
Test¶
Lives in: project Jenkinsfile (or runPytest shared step)
Agent: python:3.14 container
What it does: Runs pytest, captures test-results.xml and test-output.txt
Failure handling: catchError(buildResult: 'UNSTABLE') — build continues even if tests fail so artifacts are still archived
Post-stage: JUnit plugin publishes test-results.xml as a trend graph in Jenkins
Key environment variables injected:
| Variable | Value | Purpose |
|---|---|---|
CI |
true |
Suppresses local archive creation in conftest.py |
VERSION |
v${BUILD_NUMBER} |
Stamped into test-output.txt and test-results.xml |
Rename¶
Lives in: project Jenkinsfile
Agent: python:3.14 container
Condition: only runs if tmp-test-files/ is non-empty
What it does: Runs python -m filename_ingest tmp-test-files draft to apply filename transformations to test-generated files
Archive Artifacts¶
Lives in: project Jenkinsfile (or runPytest shared step)
Agent: python:3.14 container
Condition: only runs if tmp-test-files/ exists
What it does: Archives tmp-test-files/**, test-output.txt, test-results.xml into Jenkins build storage
Retention: 30 days (configured via "Discard Old Builds" on the job)
Update Changelog¶
Lives in: project Jenkinsfile (or generateChangelog shared step)
Agent: python:3.14 container (uses Docker socket to pull orhunp/git-cliff)
Condition: main branch only
What it does:
1. Runs git-cliff via Docker to regenerate CHANGELOG.md from Conventional Commits history
2. Commits CHANGELOG.md back to main with [skip ci] in the message to prevent a build loop
3. Skips the commit if CHANGELOG.md has no changes
cliff.toml in each project repo controls the changelog format and filters out noise commits.
Deploy Docs¶
Lives in: project Jenkinsfile (or deployMkdocs shared step)
Agent: python:3.14 container
Condition: main branch only
What it does:
1. pip install -e ".[docs]"
2. Sets git user config
3. Rewrites the remote URL with a Jenkins CI App token via withCredentials([gitHubApp(...)])
4. Runs mkdocs gh-deploy --force to push built docs to the gh-pages branch
Full Jenkinsfile (initial — before shared library)¶
pipeline {
agent {
docker { image 'python:3.14' }
}
environment {
TMP_FILES_DIR = 'tmp-test-files'
VERSION = "v${BUILD_NUMBER}"
CI = 'true'
}
stages {
stage('Install') {
steps {
sh 'pip install -e ".[dev]"'
}
}
stage('Test') {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
sh 'pytest'
}
}
post {
always { junit 'test-results.xml' }
}
}
stage('Rename') {
when {
expression {
return sh(script: 'ls tmp-test-files 2>/dev/null | wc -l',
returnStdout: true).trim() != '0'
}
}
steps {
sh 'python -m filename_ingest tmp-test-files draft'
}
}
stage('Archive Artifacts') {
when {
expression { fileExists('tmp-test-files') }
}
steps {
archiveArtifacts artifacts: 'tmp-test-files/**, test-output.txt, test-results.xml',
fingerprint: true
}
}
stage('Update Changelog') {
when { branch 'main' }
steps {
sh 'docker run --rm -v $(pwd):/app orhunp/git-cliff:latest --output CHANGELOG.md'
withCredentials([gitHubApp(credentialsId: 'jenkins-ci-app', variable: 'GH_TOKEN')]) {
sh '''
git config user.email "BrettT@aptora.com"
git config user.name "Jenkins"
git remote set-url origin https://x-access-token:${GH_TOKEN}@github.com/<your-github-username>/<repo>.git
if ! git diff --quiet CHANGELOG.md; then
git add CHANGELOG.md
git commit -m "chore: update changelog [skip ci]"
git push origin main
fi
'''
}
}
}
stage('Deploy Docs') {
when { branch 'main' }
steps {
sh 'pip install -e ".[docs]"'
withCredentials([gitHubApp(credentialsId: 'jenkins-ci-app', variable: 'GH_TOKEN')]) {
sh '''
git config user.email "BrettT@aptora.com"
git config user.name "Jenkins"
git remote set-url origin https://x-access-token:${GH_TOKEN}@github.com/<your-github-username>/<repo>.git
mkdocs gh-deploy --force
'''
}
}
}
}
post {
always { cleanWs() }
}
}
Thin Jenkinsfile (after shared library is set up)¶
@Library('shared') _
pipeline {
agent { docker { image 'python:3.14' } }
stages {
stage('Test') { steps { runPytest() } }
stage('Update Changelog') { when { branch 'main' }
steps { generateChangelog() } }
stage('Deploy Docs') { when { branch 'main' }
steps { deployMkdocs() } }
}
}
Trigger flow¶
Push / PR to GitHub
↓
Jenkins CI App sends webhook to JENKINS_URL/github-webhook/
↓
Jenkins GitHub Branch Source plugin receives it
↓
Matching job triggered (branch or PR build)
↓
python:3.14 agent container spun up
↓
Stages run → results posted back to GitHub commit status
↓
Agent container destroyed, workspace cleaned