Module 06CI/CD~1 hour

CI Pipelines in Practice with GitHub Actions

Learn to read, write, and debug GitHub Actions workflows — the industry-standard CI system built into GitHub.

Covers:MLO2MLO4MLO5
58

What GitHub Actions is

GitHub Actions is a CI/CD system that runs inside GitHub. When events happen in your repository — a push, a pull request, a scheduled timer — GitHub Actions triggers automated workflows defined in YAML files you commit alongside your code.

The key distinction from separate CI services like Jenkins or CircleCI is that GitHub Actions is integrated: the configuration lives in the repository, the workflows trigger on repository events, and the results appear directly on pull requests. No separate service to sign up for, no webhooks to configure.

GitHub provides hosted runners — virtual machines that execute your workflows. You get 2,000 free minutes per month on the free plan; most student projects use a small fraction of this.

59

Anatomy of a workflow file

Workflow files live in .github/workflows/ and must have a .yml or .yaml extension. Here is a complete, annotated example:

.github/workflows/ci.yml — annotated
# Human-readable name shown in the Actions tab
name: CI

# When to run this workflow
on:
  push:
    # Only trigger on these branches
    branches: [main, develop]
  pull_request:
    # Trigger on PRs targeting main
    branches: [main]

# A workflow contains one or more jobs
jobs:
  # Job name — choose something descriptive
  test:
    # The OS and version for the runner VM
    runs-on: ubuntu-latest

    # Each job has a sequence of steps
    steps:
      # Check out the repository code onto the runner
      - name: Checkout code
        uses: actions/checkout@v4

      # Set up the Python version
      - name: Set up Python 3.11
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      # Install dependencies
      - name: Install dependencies
        run: pip install -r requirements.txt

      # Run the test suite
      - name: Run tests
        run: pytest -v
60

Triggers in depth

The on: key controls when the workflow runs. Understanding triggers lets you build efficient pipelines that run only when needed.

.github/workflows — trigger examples
# Push to any branch
on:
 push

# Push or PR on specific branches
on:
  push:
    branches: [main, 'feature/**']
  pull_request:
    branches: [main]

# Scheduled (cron syntax)
on:
  schedule:
    # Run at 09:00 UTC every weekday
    - cron: '0 9 * * 1-5'

# Manual trigger (adds 'Run workflow' button)
on:
 workflow_dispatch

# On new GitHub release
on:
 release:
    types: [published]
61

Jobs and runners

A workflow can contain multiple jobs. By default, jobs run in parallel. You can declare dependencies to run them sequentially:

.github/workflows — multiple jobs
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install flake8 && flake8 .

  test:
    # This job waits for 'lint' to succeed before starting
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: pytest

GitHub provides runners for ubuntu-latest, windows-latest, and macos-latest. Ubuntu is most commonly used for server-side code.

62

Steps: run vs uses

Every step does one of two things:

run:
Executes a shell command directly on the runner. This is where your scripts and tool invocations go.
uses:
Runs a pre-built Action from the GitHub Marketplace. Actions are reusable workflow components published as GitHub repositories.
.github/workflows — run and uses examples
# run: executes bash commands
- name: Build application
  run: |
    echo 'Starting build'
    python3 -m build
    echo 'Build complete'

# uses: runs a published Action
- name: Upload test report
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: test-results.xml
ℹ Pinning action versions

Always specify a version tag for actions: actions/checkout@v4 not actions/checkout. Without a tag, the action uses the latest version, which may break when the action is updated. The @v4 tag is stable — it only receives non-breaking updates.

63

Secrets and environment variables

Your application often needs configuration — API keys, database URLs, feature flags. These should never be hard-coded in the workflow file (which is in version control). GitHub provides two mechanisms:

Environment variables

.github/workflows — environment variables
jobs:
  test:
    # Job-level env vars
    env:
      APP_ENV: test
      LOG_LEVEL: debug
    steps:
      - name: Run tests
        # Step-level env var (overrides job-level)
        env:
          DATABASE_URL: sqlite:///:memory:
        run: pytest

Secrets

Secrets are encrypted values stored in GitHub (Settings → Secrets → Actions). They are never written to logs.

.github/workflows — using secrets
      - name: Deploy
        env:
          # Secrets are referenced with ${{ secrets.NAME }}
          DEPLOY_API_KEY: ${{ secrets.DEPLOY_API_KEY }}
        run: ./deploy.sh
64

Caching dependencies

Installing Python packages on every pipeline run takes 20–60 seconds. Caching stores the installed packages and reuses them if requirements.txt has not changed.

.github/workflows — caching pip
      - name: Cache pip packages
        uses: actions/cache@v4
        with:
          # Cache key: unique per OS and requirements file hash
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          path: ~/.cache/pip

      - name: Install dependencies
        run: pip install -r requirements.txt

If requirements.txt has not changed, the cache hit saves the install step entirely. If it has changed, the new packages are installed and the cache is updated.

65

Debugging a failing pipeline

Failing pipelines are normal. The process for fixing them is systematic:

  1. Click the failed run in the Actions tab. Look for the red ✗ icon.
  2. Identify the failing step. Each step has its own expandable log.
  3. Read from the first error. Error messages propagate — the first error is the root cause.
  4. Look for file and line numbers. Python errors always include these.
  5. Reproduce locally. Run the exact command from the failing step.
  6. Fix and push. The pipeline re-runs automatically.
.github/workflows — adding debug output
# Print debug information when a step fails
      - name: Debug environment
        # if: failure() means this only runs if a previous step failed
        if: failure()
        run: |
          python3 --version
          pip list
          echo "PATH: $PATH"
          ls -la
66

Worked pipeline walkthrough

Let us trace the complete execution of the annotated workflow above:

  1. Trigger: A developer pushes to the main branch. GitHub detects the push and starts the workflow.
  2. Runner allocation: GitHub provisions a fresh Ubuntu VM. Every run starts with a clean environment.
  3. Checkout: actions/checkout@v4 clones the repository onto the runner at the pushed commit.
  4. Python setup: actions/setup-python@v5 installs Python 3.11 and adds it to PATH.
  5. Install: pip install -r requirements.txt installs all declared dependencies.
  6. Tests: pytest -v runs all tests. Each test is reported. If any fail, the step exits with code 1.
  7. Result: If all steps exited with code 0, the workflow shows green. GitHub marks the commit with a tick. If any step failed, the workflow shows red and the commit gets a cross. Any open PR targeting this branch cannot be merged until the checks pass.
67

Key terms

GitHub Actions
CI/CD system built into GitHub. Configured with YAML workflow files.
.github/workflows/
The directory where GitHub looks for workflow files.
workflow
A YAML file defining when and how automation runs.
trigger (on:)
The event that causes a workflow to run.
job
A set of steps that runs on a single runner machine.
step
A single task within a job — either a run: command or a uses: action.
runner
The virtual machine that executes a job. GitHub provides hosted runners.
action
A reusable workflow component published on GitHub Marketplace.
secrets
Encrypted values stored in GitHub, referenced in workflows as ${{ secrets.NAME }}.
cache
Storing build outputs (like installed packages) to speed up future runs.
needs:
Declares that a job depends on another job completing successfully first.
68

Exercises

✎ Lab exercises — approximately 55 minutes

Part A: First pipeline

  1. Fork the provided Python starter repository.
  2. Create the directory .github/workflows/ and add a file called ci.yml.
  3. Write a workflow that: triggers on push and pull_request, uses ubuntu-latest, sets up Python 3.11, installs requirements.txt, runs pytest.
  4. Push the file and check the Actions tab. Did it pass?
  5. Deliberately break a test. Push and observe the failure in the pipeline.
  6. Fix the test and push again.

Part B: Adding a lint step

  1. Add flake8 to your requirements.txt and to the workflow as a step before tests.
  2. Introduce a style error (import os at the top but never use it). Push and observe flake8 catching it.
  3. Fix the error and confirm the pipeline passes.

Part C: Environment variables

  1. Add an environment variable APP_ENV=test to the job level of the workflow.
  2. Modify a test to check that the APP_ENV variable is set (use os.getenv('APP_ENV')).
  3. Push and confirm the test passes in CI.

Part D: Caching

  1. Add the pip caching step to your workflow (before the install step).
  2. Push twice. Check the second run's log — does the cache step show a hit?
  3. Measure the time difference between a run with and without caching.

Extension: Status badge

Add a GitHub Actions status badge to your README.md. The badge URL is: https://github.com/YOUR-USERNAME/YOUR-REPO/actions/workflows/ci.yml/badge.svg.