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
- Go to your repository on GitHub
- Click the Actions tab (between "Pull requests" and "Projects")
- 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:
- Go to Actions in your repo
- Select "CI Pipeline" in the left sidebar
- Click "Run workflow"
- Select the branch (
main) - 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:

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"
- Is the file in
.github/workflows/ci.yml? (with the dot in.github) - Is the YAML valid? Validate with yamllint or an editor with a YAML extension
- Did you push the file? Check with
git logthat 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 installfailed: dependency not found or incompatible versionpytestfound a test failurepython script.pyhas 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
runnoruses - 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.txthas 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
- Quickstart for GitHub Actions - Official quick-start guide
- actions/checkout - Documentation for the checkout action
- actions/setup-python - Documentation for setup-python
- Adding a workflow status badge - How to add badges
- pytest Command Line Options - pytest options
- GitHub Actions Starter Workflows - Workflow templates for different languages