Module 1: Introduction to CI/CD and GitHub Actions

6. Your First Workflow Step by Step

Overview

It's time to go from theory to practice. In this capsule you're going to create your first GitHub Actions workflow from scratch, push it to your repo, and watch it run automatically on GitHub. The workflow is intentionally simple — the simplicity is the point. You need to experience the complete cycle (push → something happens automatically → visible result) before adding complexity.

By the end of this capsule, you'll have seen your code run on a GitHub machine without manual intervention. That moment — seeing the green check appear automatically — is the turning point. From there, everything you build in this guide is adding capabilities on top of this base.


Step 1: Check your project

Before creating the workflow, make sure you have a project with this minimal structure:

my-ai-project/
├── src/
│   └── main.py
├── tests/
│   └── test_main.py
├── requirements.txt
└── README.md

If you don't have a project, create it:

mkdir my-ai-project && cd my-ai-project
git init

mkdir -p src tests

cat > src/main.py << 'PYEOF'
"""Main module of the AI project."""


def greet(name: str) -> str:
    """Generate a personalized greeting."""
    return f"Hello, {name}! Welcome to CI/CD for AI."


def calculate_cost(tokens: int, price_per_1k: float = 0.002) -> float:
    """Calculate the estimated cost of an LLM call."""
    return (tokens / 1000) * price_per_1k


if __name__ == "__main__":
    print(greet("AI Engineer"))
    cost = calculate_cost(1500)
    print(f"Estimated cost: ${cost:.4f}")
PYEOF

cat > tests/test_main.py << 'PYEOF'
"""Tests for the main module."""
from src.main import greet, calculate_cost


def test_greet():
    result = greet("World")
    assert result == "Hello, World! Welcome to CI/CD for AI."


def test_greet_empty_name():
    result = greet("")
    assert "Hello, " in result


def test_calculate_cost():
    cost = calculate_cost(1000, 0.002)
    assert cost == 0.002


def test_calculate_cost_large():
    cost = calculate_cost(10000, 0.002)
    assert cost == 0.02
PYEOF

cat > requirements.txt << 'PYEOF'
pytest>=8.0
PYEOF

echo "# My AI Project" > README.md

Check that it works locally:

pip install -r requirements.txt
python src/main.py

Expected output:

Hello, AI Engineer! Welcome to CI/CD for AI.
Estimated cost: $0.0030
pytest tests/ -v

Expected output:

tests/test_main.py::test_greet PASSED
tests/test_main.py::test_greet_empty_name PASSED
tests/test_main.py::test_calculate_cost PASSED
tests/test_main.py::test_calculate_cost_large PASSED

========================= 4 passed in 0.01s =========================

Step 2: Create the workflows directory

mkdir -p .github/workflows

This is the path GitHub Actions looks for automatically. If the directory doesn't exist with this exact name (.github/workflows/), GitHub won't find your workflows.


Step 3: Write your first workflow

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

name: CI Pipeline

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

jobs:
  check:
    name: Basic CI Check
    runs-on: ubuntu-latest

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

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Show Python version
        run: python --version

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

      - name: Run script
        run: python src/main.py

      - name: Run tests
        run: pytest tests/ -v

Line-by-line breakdown

name: CI Pipeline

Name of the workflow. It shows up in GitHub's Actions tab.

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

Three triggers: push to main, PRs toward main, and manual trigger.

jobs:
  check:
    name: Basic CI Check
    runs-on: ubuntu-latest

A single job called check, with the display name "Basic CI Check", running on Ubuntu.

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

First step: downloads your code onto the runner. Without this, the runner has no access to your files.

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

Second step: sets up Python 3.12 on the runner. Even though Ubuntu comes with Python, this action gives you control over the exact version.

      - name: Show Python version
        run: python --version

Third step: verification. It shows you which Python version was set up. Useful for debugging.

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

Fourth step: installs the project's dependencies.

      - name: Run script
        run: python src/main.py

Fifth step: runs your main script. It verifies that the code runs without errors.

      - name: Run tests
        run: pytest tests/ -v

Sixth step: runs the tests. If any of them fails, the workflow fails.


Step 4: Commit and push

# Add all the files
git add .

# Commit
git commit -m "Add CI workflow with GitHub Actions"

# If you don't have a remote configured:
# git remote add origin https://github.com/YOUR_USERNAME/my-ai-project.git

# Push
git push origin main

If your branch is called master instead of main, adjust:

git push origin master

And change the workflow: branches: [master].


Step 5: Check on GitHub

5.1 Navigate to the Actions tab

  1. Go to your repository on GitHub
  2. Click the Actions tab (between "Pull requests" and "Projects")
  3. You'll see your workflow run show up

5.2 What you'll see

CI Pipeline
  ✅ Basic CI Check
    ✅ Set up job          (GitHub prepares the runner)
    ✅ Checkout code       (downloads your code)
    ✅ Setup Python        (installs Python 3.12)
    ✅ Show Python version (shows the version)
    ✅ Install dependencies (pip install)
    ✅ Run script          (runs main.py)
    ✅ Run tests           (runs pytest)
    ✅ Post Checkout code  (cleanup)
    ✅ Complete job         (final cleanup)

Each step can be expanded to see its detailed output.

5.3 The checks on your commit

On the commit's page, you'll see a green ✅ (or a red ❌ if it failed) next to the commit message. This is the status check that tells you whether CI passed.


Step 6: Experiment with workflow_dispatch

Since you included workflow_dispatch, you can run the workflow manually:

  1. Go to Actions in your repo
  2. Select "CI Pipeline" in the left sidebar
  3. Click "Run workflow"
  4. Select the branch (main)
  5. Click "Run workflow" (green button)

You'll see a new workflow run show up. This is invaluable for testing: you don't need to make empty commits to test changes to the workflow.


Step 7: Cause a failure on purpose

To understand what a failure looks like, let's break something intentionally:

# Modify tests/test_main.py — add a test that fails:
def test_intentional_failure():
    assert 1 == 2, "This test fails intentionally"
git add tests/test_main.py
git commit -m "Add intentional failing test"
git push origin main

What you'll see in Actions

CI Pipeline
  ❌ Basic CI Check
    ✅ Set up job
    ✅ Checkout code
    ✅ Setup Python
    ✅ Show Python version
    ✅ Install dependencies
    ✅ Run script
    ❌ Run tests           ← It failed here

When you expand "Run tests" you'll see:

FAILED tests/test_main.py::test_intentional_failure
  AssertionError: This test fails intentionally
  assert 1 == 2

Fix the failure

# Remove or comment out the failing test
# def test_intentional_failure():
#     assert 1 == 2, "This test fails intentionally"
git add tests/test_main.py
git commit -m "Remove intentional failing test"
git push origin main

You'll see a new workflow run with a ✅ — the pipeline passes again.


Immediate improvements to the basic workflow

Improvement 1: Add a timeout

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 10          # Cancels if it takes more than 10 min

Without a timeout, a job can run for up to 6 hours (GitHub's default limit). With AI apps that call external APIs, a hung test can waste all your free minutes.

Improvement 2: Show a summary

      - name: Run tests
        run: pytest tests/ -v --tb=short

--tb=short shows short tracebacks in case of failure. Easier to read in the Actions UI than --tb=long.

Improvement 3: Add a status badge

GitHub generates an automatic badge for your workflow. Add this to your README:

![CI](https://github.com/YOUR_USERNAME/my-ai-project/actions/workflows/ci.yml/badge.svg)

This shows a badge with the pipeline's current status directly in your README.

Improvement 4: Pin action versions

# ✅ Good practice: specific version
- uses: actions/checkout@v4
- uses: actions/setup-python@v5

# ⚠️ Avoid: latest or no version
- uses: actions/checkout@main     # Can change without warning

The major versions (v4, v5) are stable. Always use pinned versions.


Troubleshooting

"The workflow doesn't show up in Actions"

  1. Is the file in .github/workflows/ci.yml? (with the dot in .github)
  2. Is the YAML valid? Validate with yamllint or an editor with a YAML extension
  3. Did you push the file? Check with git log that the commit is on the remote

"Error: Process completed with exit code 1"

A step returned exit code 1 (error). Expand the step in the Actions UI to see the full error message.

Common causes:

  • pip install failed: dependency not found or incompatible version
  • pytest found a test failure
  • python script.py has a runtime error

"Error: Unable to resolve action"

Error: Unable to resolve action `actions/checkout@v4`

The action's name is misspelled or the version doesn't exist. Check the exact name in the marketplace.

"Error: The workflow is not valid"

GitHub validated the YAML but found an error in the workflow's structure. The most common causes:

  • A job doesn't have runs-on
  • A step has neither run nor uses
  • An indentation error that GitHub reads as invalid structure

"Tests pass locally but fail in CI"

Differences between your local environment and the runner:

  • Different Python version: Specify the exact version with setup-python
  • Missing dependencies: Make sure requirements.txt has all the dependencies
  • Relative paths: Your project may use paths that work from your directory but not from the runner's root
  • Environment variables: If your code depends on env vars that you have locally but not in CI

Exercises

Exercise 1: Modify the workflow

Add a step that shows the runner's current date and time:

See solution
      - name: Show current time
        run: date

Or with more detail:

      - name: Show current time
        run: |
          echo "Current UTC time: $(date -u)"
          echo "Runner OS: $(uname -a)"

Exercise 2: Add a lint job

Add a second job called lint that installs ruff and runs ruff check src/:

See solution
jobs:
  lint:
    name: Code Linting
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install ruff
        run: pip install ruff
      - name: Run linting
        run: ruff check src/

  check:
    name: Basic CI Check
    runs-on: ubuntu-latest
    steps:
      # ... (the existing steps)

lint and check will run in parallel because there's no needs between them.

Exercise 3: Make jobs sequential

Modify the workflow so the tests only run if the lint passes:

See solution
jobs:
  lint:
    name: Code Linting
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install ruff
      - run: ruff check src/

  test:
    name: Run Tests
    needs: lint                    # Only runs if lint passed
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v

Exercise 4: Add workflow_dispatch with an input

Modify the trigger to accept a verbose input that makes pytest use -vvv instead of -v:

See solution
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      verbose:
        description: "Run tests with extra verbosity"
        type: boolean
        default: false

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - name: Run tests
        run: |
          if [ "${{ inputs.verbose }}" == "true" ]; then
            pytest tests/ -vvv --tb=long
          else
            pytest tests/ -v --tb=short
          fi

When you run it manually, you can tick the "verbose" checkbox to see detailed output.


Summary

  • Your first workflow has 6 steps: checkout → setup python → show version → install deps → run script → run tests
  • The complete cycle: push → GitHub detects the workflow → the runner executes → visible result
  • workflow_dispatch lets you run manually without empty commits
  • Causing a failure teaches you what errors look like and how to debug
  • Immediate improvements: timeout, --tb=short, status badge, pin versions
  • Every step has a descriptive name to make debugging easier

Additional resources

  1. Quickstart for GitHub Actions - Official quick-start guide
  2. actions/checkout - Documentation for the checkout action
  3. actions/setup-python - Documentation for setup-python
  4. Adding a workflow status badge - How to add badges
  5. pytest Command Line Options - pytest options
  6. GitHub Actions Starter Workflows - Workflow templates for different languages