Jenkins CI/CD (3/11): First Jenkins Job With a Jenkinsfile
Summary: You write a declarative Jenkinsfile that creates a Python virtual environment, installs your package, and runs pytest. Then you create a Pipeline job in Jenkins, point it at your Git repository, and hit Build Now. By the end you have your first green build — proof that Jenkins can clone your code and run your tests without human intervention.
Example Values Used in This Tutorial
| Key | Value |
|---|---|
| Jenkins URL | http://localhost:8080 |
| Repository URL | <YOUR_REPO_URL> |
| Jenkinsfile path | Jenkinsfile |
| Python venv (CI) | .venv |
| Agent | any |
| Pipeline type | Declarative |
0. Prerequisites
- A running Jenkins controller at
http://localhost:8080(Part 1). - The
hellociPython package with eight passing unit tests committed to a Git repository (Part 2). - Python
3.12installed on the Jenkins agent (the machine where Jenkins runs builds). - Git installed on the Jenkins agent.
Note: If you followed Part 1 using a local Jenkins install, the agent is your own machine. The
anyagent label tells Jenkins to use whatever executor is available.
1. What Is a Jenkinsfile?
A Jenkinsfile is a text file that lives in the root of your repository and defines your entire build pipeline as code. Instead of clicking through the Jenkins web UI to configure build steps, you write them in a Groovy-based DSL and commit the file alongside your source code.
This approach has three immediate benefits:
- Version control — your pipeline definition lives in Git, so changes are tracked, reviewable, and reversible.
- Portability — anyone who clones the repo gets the pipeline definition for free.
- Reproducibility — the pipeline runs the same way every time, on any Jenkins controller that can reach the repo.
Jenkins supports two pipeline syntaxes: Scripted and Declarative. This series uses Declarative syntax because it is structured, opinionated, and easier to read. Declarative pipelines start with the pipeline block and enforce a fixed hierarchy: pipeline > stages > stage > steps.
2. Write the Jenkinsfile
Create a file called Jenkinsfile in the root of your helloci repository. This is the complete file — paste it exactly as shown.
pipeline {
agent any
stages {
stage('Setup Python') {
steps {
sh 'python3 -m venv .venv'
sh '.venv/bin/pip install --upgrade pip'
}
}
stage('Install Dependencies') {
steps {
sh '.venv/bin/pip install -e ".[test]"'
}
}
stage('Run Unit Tests') {
steps {
sh '.venv/bin/pytest tests/'
}
}
}
}Code language: Groovy (groovy)
Save the file. Before committing, take a moment to understand what each block does.
3. Anatomy of the Jenkinsfile
Every declarative pipeline follows the same structure. Here is what each keyword means.
3.1 pipeline
pipeline {
...
}Code language: Groovy (groovy)
The pipeline block is the top-level container. Everything else lives inside it. A declarative Jenkinsfile must have exactly one pipeline block, and it must be the outermost element.
3.2 agent any
agent anyCode language: Groovy (groovy)
The agent directive tells Jenkins where to run the pipeline. any means “pick any available executor.” If you only have the built-in node (the Jenkins controller itself), that is where it runs. Later in the series you will use Docker-based agents, but any is the right starting point.
3.3 stages and stage
stages {
stage('Setup Python') {
steps { ... }
}
stage('Install Dependencies') {
steps { ... }
}
stage('Run Unit Tests') {
steps { ... }
}
}Code language: Groovy (groovy)
The stages block contains one or more stage blocks. Each stage represents a logical phase of the build. Jenkins displays stages as columns in the pipeline visualization, so naming them clearly matters. The stages run in order, top to bottom. If any stage fails, the pipeline stops.
3.4 steps and sh
steps {
sh 'python3 -m venv .venv'
sh '.venv/bin/pip install --upgrade pip'
}Code language: Groovy (groovy)
The steps block contains the actual commands. sh runs a shell command on the agent. Each sh call is a separate shell invocation. Notice that the venv is created with python3 -m venv and then all subsequent commands use .venv/bin/pip and .venv/bin/pytest — explicit paths to the venv binaries. This avoids the need to source activate inside the pipeline, which can be unreliable across different shell environments.
Tip: Always use the full path to venv binaries in CI (
.venv/bin/pip,.venv/bin/pytest). Thesource .venv/bin/activatetrick depends on shell state that may not persist betweenshsteps.
4. Create the Pipeline Job in Jenkins
Open Jenkins at http://localhost:8080 and follow these steps to create the job.
4.1 Create a New Item
- Click New Item in the left sidebar.
- Enter the name
hellociin the item name field. - Select Pipeline as the project type.
- Click OK.
Jenkins creates the job and opens the configuration page.
4.2 Configure Pipeline from SCM
Scroll down to the Pipeline section at the bottom of the configuration page. You need to change two settings.
- In the Definition dropdown, select Pipeline script from SCM.
- In the SCM dropdown, select Git.
- In Repository URL, enter the URL of your
hellociGit repo.
If you are using a local bare repo on the same machine as Jenkins, the URL looks like this:
/home/user/projects/hellociCode language: Shell Session (shell)
If your repo is on GitHub, GitLab, or another remote host, the URL looks like this:
https://github.com/youruser/helloci.gitCode language: Shell Session (shell)
- Leave Credentials as
- none -for a public repo. For a private repo, add credentials through Jenkins > Manage Jenkins > Credentials first. - In Branch Specifier, leave the default
*/main(or change it to*/masterif that is your default branch name). - In Script Path, leave the default value
Jenkinsfile. This tells Jenkins which file to read from the repo root.
Click Save.
Note: The “Pipeline script from SCM” option is the correct choice for real projects. The alternative — “Pipeline script” — lets you paste Groovy directly into the Jenkins UI, but that defeats the purpose of pipeline-as-code. Always keep your Jenkinsfile in the repo.
5. Run the First Build
Click Build Now in the left sidebar.
Jenkins starts a build. You can watch progress in two places:
- Build History — the list in the bottom-left corner shows build numbers with colored indicators (blue for success, red for failure).
- Stage View — if the Pipeline Stage View plugin is installed, you see a visual representation of stages with timing information.
Wait for the build to finish. If everything is configured correctly, the build indicator turns blue. That is your first green build.
6. Read the Console Output
Click on the build number (e.g., #1) in the Build History, then click Console Output in the left sidebar.
The console output shows exactly what Jenkins did, step by step. Here is what to look for in a successful run:
Started by user admin
Obtained Jenkinsfile from git <YOUR_REPO_URL>Code language: Shell Session (shell)
This confirms Jenkins cloned the repo and found the Jenkinsfile.
[Pipeline] stage (Setup Python)
[Pipeline] sh
+ python3 -m venv .venv
[Pipeline] sh
+ .venv/bin/pip install --upgrade pipCode language: Shell Session (shell)
This confirms the venv was created and pip was upgraded.
[Pipeline] stage (Install Dependencies)
[Pipeline] sh
+ .venv/bin/pip install -e .[test]Code language: Shell Session (shell)
This confirms the package and test dependencies were installed.
[Pipeline] stage (Run Unit Tests)
[Pipeline] sh
+ .venv/bin/pytest tests/
========================= test session starts ==========================
collected 8 items
tests/test_greet.py ........ [100%]
========================== 8 passed in 0.42s ===========================Code language: Shell Session (shell)
This confirms all eight tests passed. The pytest output in Jenkins is identical to what you saw locally in Part 2.
Finished: SUCCESSCode language: Shell Session (shell)
The final line tells you the build result. SUCCESS means every stage completed without a non-zero exit code.
Tip: Bookmark the console output page. When a build fails, this is the first place to look. Read from the bottom up — the failure is usually near the end.
7. When It Fails — Common First-Run Issues
Your first build might not be green. That is normal. Here are the most common failures and how to fix them.
7.1 python3: not found
+ python3 -m venv .venv
python3: not foundCode language: Shell Session (shell)
Python is not installed on the Jenkins agent, or it is not on the PATH. Fix this by installing Python on the machine where Jenkins runs builds.
sudo apt update && sudo apt install -y python3 python3-venv python3-pipCode language: Shell Session (shell)
After installing, restart the Jenkins agent or controller and rebuild.
7.2 Permission denied
+ python3 -m venv .venv
Error: [Errno 13] Permission denied: '.venv'Code language: Shell Session (shell)
The Jenkins workspace directory does not have write permissions for the Jenkins user. Check who owns the workspace.
ls -la /var/lib/jenkins/workspace/helloci/Code language: Shell Session (shell)
The workspace should be owned by the jenkins user. If not, fix it:
sudo chown -R jenkins:jenkins /var/lib/jenkins/workspace/Code language: Shell Session (shell)
7.3 Repository not found or access denied
ERROR: Error cloning remote repo '<YOUR_REPO_URL>'Code language: Shell Session (shell)
Jenkins cannot reach the Git repository. Check these things:
- If using a local path, verify the path exists and the
jenkinsuser can read it. - If using a remote URL, verify the URL is correct and the repo is accessible.
- For private repos, configure credentials in Manage Jenkins > Credentials.
7.4 ensurepip is not available
Error: Command '[...python3', '-m', 'ensurepip', ...]' returned non-zero exit status 1.Code language: Shell Session (shell)
The python3-venv package is not installed. On Debian/Ubuntu:
sudo apt install -y python3-venvCode language: Shell Session (shell)
7.5 ModuleNotFoundError: No module named ‘helloci’
ModuleNotFoundError: No module named 'helloci'Code language: Shell Session (shell)
The pip install -e ".[test]" step failed silently or the install happened in a different venv than the one pytest is using. Verify the Jenkinsfile uses .venv/bin/pip and .venv/bin/pytest — not bare pip or pytest, which might point to the system Python.
Warning: Every
shstep in a declarative pipeline runs in a new shell. Environment variables andsource activatefrom a previousshstep do not carry over. Always use absolute or relative paths to venv binaries.
8. Commit the Jenkinsfile to the Repo
Now that you have confirmed the pipeline works (or debugged it until it does), commit the Jenkinsfile.
cd ~/projects/helloci
git add Jenkinsfile
git statusCode language: Shell Session (shell)
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: JenkinsfileCode language: Shell Session (shell)
git commit -m "Add Jenkinsfile: venv, install, pytest pipeline"Code language: Shell Session (shell)
[main b4e5f6g] Add Jenkinsfile: venv, install, pytest pipeline
1 file changed, 19 insertions(+)
create mode 100644 JenkinsfileCode language: Shell Session (shell)
If your Jenkins job is pointed at a remote repository, push the commit:
git push origin mainCode language: Shell Session (shell)
Then go back to Jenkins and click Build Now again to confirm the committed Jenkinsfile produces a green build from the repository.
9. Verify the Green Build
After the build completes, confirm three things:
- Build status is SUCCESS — the build indicator in Build History is blue.
- All eight tests passed — the console output shows
8 passed. - All three stages completed — the Stage View (if available) shows green for Setup Python, Install Dependencies, and Run Unit Tests.
If all three check out, your pipeline is working. Jenkins is now doing exactly what you were doing locally: creating a venv, installing the package, and running pytest. The difference is that Jenkins does it automatically every time you trigger a build.
| What you typed locally | What Jenkins does |
|---|---|
python3 -m venv .venv | sh 'python3 -m venv .venv' |
source .venv/bin/activate | (not needed — uses full paths) |
pip install --upgrade pip | sh '.venv/bin/pip install --upgrade pip' |
pip install -e ".[test]" | sh '.venv/bin/pip install -e ".[test]"' |
pytest tests/ | sh '.venv/bin/pytest tests/' |
The mapping is nearly one-to-one. CI is not magic. It is your local workflow, scripted and automated.
Summary
You wrote a declarative Jenkinsfile with three stages — Setup Python, Install Dependencies, and Run Unit Tests — and got your first green build in Jenkins.
- The
Jenkinsfilelives in the repo root and defines the pipeline as code. pipeline,agent any,stages,stage, andstepsare the five building blocks of every declarative pipeline.shruns shell commands on the agent. Eachshstep is a separate shell invocation, so always use explicit paths to venv binaries.- “Pipeline script from SCM” tells Jenkins to read the Jenkinsfile from the Git repo, keeping your pipeline definition version-controlled.
- Common first-run failures (Python not found, permission denied, repo access) all have straightforward fixes.
- The Jenkinsfile is committed to the repo so the pipeline definition travels with the code.
Your CI pipeline now runs tests automatically, but the only way to see results is to scroll through console logs. In Part 4 you will add JUnit XML reporting so Jenkins can parse test results and give you dashboards, trend graphs, and per-test failure details.
Jenkins CI/CD — All Parts
- 1 Jenkins CI/CD (1/11): Prerequisites and Mental Model
- 2 Jenkins CI/CD (2/11): Create the Minimal Repo and Prove Local Tests Run
- 3 Jenkins CI/CD (3/11): First Jenkins Job With a Jenkinsfile You are here
- 4 Jenkins CI/CD (4/11): Test Reporting With JUnit Results in Jenkins
- 5 Jenkins CI/CD (5/11): Fast-Fail Static Checks With a Lint Stage
- 6 Jenkins CI/CD (6/11): Integration Tests With Docker Compose
- 7 Jenkins CI/CD (7/11): Artifacts and Debugging Failed Builds
- 8 Jenkins CI/CD (8/11): Workspace Cleanup, Timeouts, and Retries
- 9 Jenkins CI/CD (9/11): Concurrency and Port Conflicts
- 10 Jenkins CI/CD (10/11): Release Pipeline — Build Artifacts on Tags
- 11 Jenkins CI/CD (11/11): Publish to PyPI Securely
