CI Pipelines for a Python App

A 1-hour hands-on session: GitHub Actions and Jenkins, end to end.

Contents
  1. Session plan
  2. Prerequisites
  3. Part 1: Sample Python app
  4. Part 2: GitHub Actions
  5. Part 3: Jenkins
  6. Part 4: Trigger, observe, and break things
  7. Part 5: Recap and next steps
  8. Troubleshooting

Session plan

TimeSegment
0:00 to 0:05Prerequisites check and repo setup
0:05 to 0:15Build the sample Python app and tests
0:15 to 0:25GitHub Actions workflow
0:25 to 0:45Jenkins setup and Jenkinsfile
0:45 to 0:55Trigger both pipelines, break a test, observe failures
0:55 to 1:00Recap and next steps

Prerequisites

Verify the basics:

python3 --version
git --version
docker --version

Part 1: Sample Python app (0:05 to 0:15)

Create the project structure:

mkdir python-ci-demo && cd python-ci-demo
git init -b main
mkdir -p app tests
touch app/__init__.py tests/__init__.py
app/calculator.py
def add(a: float, b: float) -> float:
    return a + b


def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
app/main.py
from app.calculator import add, divide


def run() -> None:
    print(f"2 + 3 = {add(2, 3)}")
    print(f"10 / 4 = {divide(10, 4)}")


if __name__ == "__main__":
    run()
tests/test_calculator.py
import pytest
from app.calculator import add, divide


def test_add():
    assert add(2, 3) == 5


def test_divide():
    assert divide(10, 2) == 5


def test_divide_by_zero_raises():
    with pytest.raises(ValueError):
        divide(1, 0)
requirements.txt
pytest==8.3.3
pytest-cov==5.0.0
flake8==7.1.1
pytest.ini (lets tests import the app package from the project root)
[pytest]
pythonpath = .
testpaths = tests
.gitignore
.venv/
__pycache__/
.pytest_cache/
.coverage
coverage.xml
reports/
*.tar.gz

Run locally to confirm everything works before automating it:

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt
flake8 app tests
pytest -v

Commit and push:

git add . && git commit -m "Add sample app and tests"
git remote add origin https://github.com/<your-user>/python-ci-demo.git
git push -u origin main

Part 2: GitHub Actions (0:15 to 0:25)

Create the workflow file at .github/workflows/ci.yml:

name: Python CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.11", "3.12"]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Lint with flake8
        run: flake8 app tests

      - name: Run tests with coverage
        run: pytest --junitxml=reports/junit.xml --cov=app --cov-report=term-missing

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: junit-results-py${{ matrix.python-version }}
          path: reports/junit.xml

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Package application
        run: tar czf app-${{ github.sha }}.tar.gz app requirements.txt

      - name: Upload package
        uses: actions/upload-artifact@v4
        with:
          name: app-package
          path: app-${{ github.sha }}.tar.gz

Commit and push, then check the Actions tab in GitHub. The matrix produces two test jobs, followed by the build job after both pass.


Part 3: Jenkins (0:25 to 0:45)

3.1 Run Jenkins

docker run -d --name jenkins \
  -p 8080:8080 -p 50000:50000 \
  -v jenkins_home:/var/jenkins_home \
  jenkins/jenkins:lts-jdk17

Get the initial admin password and open the UI:

docker logs jenkins 2>&1 | grep -A 2 "Please use the following password"
# or
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

Go to http://localhost:8080, paste the password, choose Install suggested plugins, and create an admin user.

3.2 Install Python in the Jenkins container

The LTS image may not include Python 3 with venv support, so install it:

docker exec -u root jenkins bash -c "apt-get update && apt-get install -y python3 python3-venv python3-pip git"

Verify:

docker exec jenkins python3 --version

3.3 Add the Jenkinsfile to the repository

Create Jenkinsfile in the repo root:

pipeline {
    agent any

    options {
        timestamps()
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Setup') {
            steps {
                sh '''
                    python3 -m venv .venv
                    . .venv/bin/activate
                    pip install --upgrade pip
                    pip install -r requirements.txt
                '''
            }
        }

        stage('Lint') {
            steps {
                sh '. .venv/bin/activate && flake8 app tests'
            }
        }

        stage('Test') {
            steps {
                sh '''
                    . .venv/bin/activate
                    mkdir -p reports
                    pytest --junitxml=reports/junit.xml --cov=app --cov-report=term-missing
                '''
            }
            post {
                always {
                    junit 'reports/junit.xml'
                }
            }
        }

        stage('Package') {
            steps {
                sh 'tar czf app-${BUILD_NUMBER}.tar.gz app requirements.txt'
                archiveArtifacts artifacts: 'app-*.tar.gz', fingerprint: true
            }
        }
    }

    post {
        always {
            cleanWs()
        }
    }
}

Commit and push the Jenkinsfile:

git add Jenkinsfile && git commit -m "Add Jenkinsfile" && git push

3.4 Create the Jenkins pipeline job

  1. On the Jenkins dashboard, click New Item.
  2. Name it python-ci-demo, select Pipeline, and click OK.
  3. Under Build Triggers, check Poll SCM and enter H/5 * * * *. This checks for new commits every five minutes. A GitHub webhook is the better option for production, but it requires a publicly reachable Jenkins URL.
  4. Under Pipeline, set Definition to Pipeline script from SCM.
  5. Set SCM to Git, enter your repository URL (https://github.com/<your-user>/python-ci-demo.git), and set Branch Specifier to */main.
  6. Keep Script Path as Jenkinsfile and click Save.
  7. Click Build Now and open the console output to follow the run.

Part 4: Trigger, observe, and break things (0:45 to 0:55)

This is the most useful part of the session. Introduce a failure on purpose and watch both systems catch it.

Create a feature branch with a broken test:

git checkout -b feature/break-test

Edit tests/test_calculator.py and change assert add(2, 3) == 5 to assert add(2, 3) == 6. Then add a lint violation, such as an unused import, to app/main.py:

import os  # unused

Commit, push, and open a pull request:

git add . && git commit -m "Introduce failures" && git push -u origin feature/break-test

Observe:

Fix both problems, push again, and confirm both pipelines go green. Then merge the PR into main.


Part 5: Recap and next steps (0:55 to 1:00)

Key concepts covered:

Suggested next steps:

  1. Add a Docker build stage and push the image to a registry such as Azure Container Registry or Docker Hub.
  2. Store secrets in GitHub Secrets and Jenkins Credentials, not in the repository.
  3. Add a deployment stage gated on the main branch, with a manual approval step in Jenkins.
  4. Replace Poll SCM with a GitHub webhook, using ngrok for local testing.
  5. Use a Jenkins agent with the Docker Pipeline plugin so each build runs in a clean python:3.12-slim container.

Troubleshooting quick reference

SymptomLikely cause and fix
ModuleNotFoundError: app in pytestConfirm pytest.ini contains pythonpath = .
python3: command not found in JenkinsRe-run the apt-get install step in section 3.2
venv creation fails in JenkinsInstall python3-venv for the matching Python version
JUnit report not shown in JenkinsConfirm the JUnit plugin is installed and the path is reports/junit.xml
Jenkins can't clone the repoUse a public repo, or add a GitHub credential under Manage Jenkins, then Credentials
Workflow doesn't triggerCheck the branches: filter and confirm the file is in .github/workflows/