Your First GitHub Actions AI workflow

Build your first CI/CD pipeline with GitHub Actions and automate Python testing with pytest!

Introduction

This project has been archived

We're no longer updating this project - we recommend checking out our active projects instead!

If you've already completed this project, don't worry! Your documentation is still available for you to check out and share.

If you have any questions, leave a question in our Discord and we'll get back to you ASAP!

โšก๏ธ 30 Second Summary

Every time you push code, you're rolling the dice. Did you break something? Will it work on someone else's machine? CI/CD pipelines can eliminate the guesswork by automatically testing your code before it reaches production.

In today's project, you'll build a CI pipeline with GitHub Actions that automatically runs tests on every push. By the end, you'll have a safety net that catches bugs before they become problems.

Get ready to...
  • ๐Ÿ Set up a Python development environment
  • โœ๏ธ Write functions and unit tests with pytest
  • ๐Ÿ”„ Build a CI pipeline that runs on every push
  • โœ… Watch your tests pass (and fail!) automatically

Want a complete demo of how to do this project, from start to finish? Check out our ๐ŸŽฌ walkthrough with Kahu ๐ŸŽฌ

Not sure if this project is right for you? Check if it matches your goals

If you're up for a bit of a challenge, quiz yourself on the key concepts up ahead in this project.

This project is part of a series:

  1. Part 1: You are here!
  2. Part 2: Review GitHub Pull Requests with Gemini
  3. Part 3: Increase Test Coverage with AI
  4. Part 4: Coming Soon.

Set Up Your Python Project

Every professional Python project starts with a proper development environment. Companies like Google, Netflix, and Spotify all use virtual environments and dependency management to keep their projects isolated and reproducible - so when a new developer joins, they can get running in minutes.

In this step, you're going to:

  • Fork and clone the starter repository.
  • Create a Python virtual environment and install dependencies.
  • Explore the project structure.

Setup the starter repo

You'll need a GitHub account to fork the starter repository.

โœ… I have a GitHub account

You're all set! Continue to the next step below.

๐Ÿ†• I need to create one

  • Head to github.com and click Sign up.
  • Fill in the form with your email, password, and username.
  • Click Create account.

(Or You can also sign up with Google, Apple!)

  • Verify your email address and come back here.

Great, now you have a GitHub account! Let's move on to forking the starter repo below.

  • Click the green Create fork button.
  • Click the green <> Code button.
  • Click the copy icon.

โœ… I have Cursor

Open Cursor from your Applications (Mac) or Start menu (Windows).

๐Ÿ†• I need to install it

  • Download the installer for your operating system (macOS, Windows, or Linux).

๐ŸŽ macOS

  • Open the downloaded .dmg file.
  • Drag Cursor into your Applications folder.
  • Open Cursor from Applications.

๐Ÿ–ผ๏ธ Windows

  • Open the downloaded .exe file.
  • Click through the setup wizard (the defaults are fine).
  • Click Finish to launch Cursor.

You should see the Welcome to Cursor screen:

Great, now you have Cursor installed! Let's clone the starter repo.

  • Press Ctrl/Cmd + Shift + P
  • Select Git: Clone
  • Paste the forked repository URL.
  • Press Enter
  • Select a folder to save your project (e.g. Desktop).
  • Click Select as Repository Destination.
  • Click Open when Cursor asks if you'd like to open the cloned repository.

โœ… It's working

Great! Your repository is cloned and ready. Let's move on.

โŒ Git not found

That's totally okay! This just means you don't have Git installed on your computer.

  • Head over to git-scm.com/downloads.
  • Download and run the installer for your operating system.
  • Once installed, restart your IDE and try again.

Still having issues? Troubleshoot here or share your error with the NextWork community!

Create a Python venv

What's a virtual environment?

A virtual environment is an isolated space for your Python project. It keeps your project's dependencies separate from other projects on your computer, so they don't interfere with each other.

Let's create a Python virtual environment to isolate your project dependencies.

  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Enter:
python -m venv venv

โœ… It's working

Great! Let's move on to activating your virtual environment.

โŒ Python not found

If Python isn't recognized, common issues include:

  • "python not found" or "python: command not found": Python isn't installed or isn't in your system PATH. Download it from python.org and make sure to check "Add to PATH" during installation. Then restart Cursor.
  • "python3" works but "python" doesn't: On some systems, try python3 -m venv venv instead.

Get help with Python setup or share your error with the NextWork community!

What does this command do?

python -m venv venv creates a new virtual environment in a folder called venv. The -m venv tells Python to run the built-in venv module.

  • Run this command in terminal to activate your virtual environment:

๐ŸŽ macOS/Linux

source venv/bin/activate

๐Ÿ–ผ๏ธ Windows

venv\Scripts\activate

โœ… I see (venv)

You're all set! The (venv) at the beginning of your terminal prompt means your virtual environment is active.

โš ๏ธ Activation failed

If you're having trouble activating the virtual environment, common issues include:

  • "Running scripts is disabled" - This is a Windows PowerShell security setting. Open PowerShell as Administrator and run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser. Then try the activation command again.
  • "No such file or directory" - Make sure the python -m venv venv command completed successfully first. Check that a venv folder exists in your project.
  • "Command not found" or "python not recognized" - Python isn't installed or isn't in your system PATH. Download it from python.org and make sure to check "Add to PATH" during installation.

Get help with this error or share your error with the NextWork community!

What does this command do?

This activates your virtual environment. Once activated, any Python packages you install will only be available in this project, not system-wide.

You should see (venv) appear at the beginning of your terminal prompt, indicating the virtual environment is active.

Project dependencies

Now let's install the project dependencies.

  • Run this command in terminal:
pip install -r requirements.txt

This installs pytest for testing and build for packaging.

What are these tools?

- pytest: A testing framework that makes it easy to write and run tests. - build: A tool that packages your Python code for distribution.

  • Run this command in terminal to list the files:

๐ŸŽ macOS/Linux

ls -la

๐Ÿ–ผ๏ธ Windows

dir

You should see these key files and folders:

  • app.py: Contains utility functions like add(), is_even(), and reverse_string()
  • tests/: Directory for your test files
  • requirements.txt: Lists the project dependencies
  • pyproject.toml: Python project configuration
  • .github: Where your CI workflow will live

You'll see three simple functions already defined. In the next step, you'll add your own function and write tests for it.

Step #1 Complete

Nice work getting your Python Environment setup. Now let's write some code.

Write Code and Tests

Python environment? Set up โœ…

Now it's time to write code and prove it works. At companies like Microsoft and Meta, developers don't just write code - they write tests that verify their code behaves correctly. This practice, called test-driven development, catches bugs before they ever reach users.

In this step, you're going to:

  • Write a Python function with type hints.
  • Write automated tests using pytest.
  • Run your tests locally to verify everything works.

Why test your code?

Tests are your safety net. Without them, every code change is a gamble - you might accidentally break something and not know until a user complains. With tests, you get instant feedback: green means safe, red means fix it.

Adding Code

  • Open app.py in Cursor.

You'll see some existing functions already defined (like add, is_even, reverse_string). Now you're going to add your own!

What is a Python function?

A function is a reusable block of code that performs a specific task. Here's the anatomy of a Python function:

def add(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b
  • def: The keyword that says "I'm defining a function"
  • add: The function's name (you choose this)
  • (a: int, b: int): Parameters the inputs your function accepts. a and b are variables that hold whatever values are passed in
  • -> int: Says this function returns an integer
  • """...""": A docstring that describes what the function does
  • return a + b: Sends back the result

Indentation matters! In Python, the code inside a function MUST be indented (shifted right by 4 spaces or 1 tab). This is how Python knows which lines belong to the function.

  • Add a new multiply function at the bottom of the file.

Your function should:

  • Take two integer parameters
  • Return their product

I'll give it a go! ๐Ÿ’ช

Nice! Try writing the multiply function yourself. When you're ready, check your work in the other tab.

๐Ÿ’ก Need a hint?

Here's the code you need to add:

def multiply(a: int, b: int) -> int:
    """Multiply two numbers together."""
    return a * b

Note

  • def multiply(a: int, b: int) -> int:: Defines a function called multiply that takes two integers and returns an integer
  • """Multiply two numbers together.""": A docstring that describes what the function does
  • return a * b: Multiplies the two numbers together and sends the result back

What does this code do?

What are type hints?

The : int after each parameter and -> int after the function name are type hints. int stands for "integer" - a whole number like 1, 42, or -7. These hints tell other developers (and tools like linters) what types of values the function expects and returns. They don't enforce anything at runtime - they're just helpful documentation.

  • Save the file. (Ctrl/Cmd + S)
  • Open tests/test_app.py to see the existing tests.

What is a Python class?

A class is a way to group related functions together. In testing, we use classes to organize tests for the same feature:

class TestAdd:
    def test_add_positive(self):
        assert add(2, 3) == 5
  • class TestAdd:: Creates a group called "TestAdd"
  • def test_add_positive(self):: A method (a function inside a class). The self parameter is required for every method in a class. Python uses it to refer to the class instance
  • assert: Checks if something is true. If add(2, 3) equals 5, the test passes. If not, the test fails

Notice the double indentation: the method is indented inside the class, and the method's code is indented inside the method.

  • Add a new test class for your multiply function at the bottom of the file.

Your test class should include tests for:

  • Multiplying two positive numbers
  • Multiplying by zero
  • Multiplying negative numbers

I'll give it a go! ๐Ÿ’ช

Nice! Try writing the TestMultiply class yourself. Look at the existing tests in the file for inspiration. When you're ready, check your work in the other tab.

๐Ÿ’ก Need a hint?

Here's the code you need to add:

class TestMultiply:
    """Tests for the multiply function."""

    def test_multiply_positive_numbers(self):
        """Test multiplying two positive numbers."""
        assert multiply(3, 4) == 12

    def test_multiply_by_zero(self):
        """Test multiplying by zero."""
        assert multiply(5, 0) == 0

    def test_multiply_negative_numbers(self):
        """Test multiplying negative numbers."""
        assert multiply(-2, 3) == -6

Note

  • class TestMultiply:: Groups all the multiply tests together in one class
  • def test_multiply_positive_numbers(self):: A test method that checks if 3 * 4 gives 12
  • def test_multiply_by_zero(self):: Checks that multiplying by zero gives 0
  • def test_multiply_negative_numbers(self):: Checks that -2 * 3 gives -6
  • assert multiply(3, 4) == 12: If the result isn't 12, the test fails

What does this code do?

What's an assertion?

An assertion (assert statement) checks if something is true. If multiply(3, 4) equals 12, the test passes. If not, the test fails and tells you something's wrong with your code.

Now let's update the import statement at the top of the test_app.py file.

  • Add: multiply.
  • Save the file. (Ctrl/Cmd + S)
  • Re-open Cursor's integrated terminal (Ctrl/Cmd + `)
  • Run the following command:
pytest -v

What does the -v flag do?

The -v flag stands for "verbose". It makes pytest show each test name and its result individually, instead of just showing dots for passes and F's for failures.

What do you see in your terminal?

โœ… All tests passing

Nice work! You should see green checkmarks for all your tests.

โš ๏ธ Tests failing

If your tests are failing:

  • "ImportError: cannot import name 'multiply'" - Make sure you added multiply to the import at the top of test_app.py: from app import add, is_even, reverse_string, multiply
  • "NameError: name 'multiply' is not defined" - The multiply function is missing from app.py. Make sure you saved the file after adding it.
  • "AssertionError" - Your multiply function isn't returning the right values. Check that it uses return a * b (not a + b!)
  • "IndentationError" - Python is strict about spaces. Make sure the code inside your function is indented by 4 spaces.

Get help with failing tests or share your error with the NextWork community!

Step #2 Complete

Get ready to automate these checks with a GitHub Actions workflow.

Build Your CI Pipeline

Code? Written โœ…

Tests? Passing โœ…

Here comes the magic. Teams at GitHub, Shopify, and Stripe don't manually run tests before every deployment - they use CI/CD pipelines to automate it. You're about to build the same thing with GitHub Actions: every time you push code, GitHub will automatically spin up a server, install your dependencies, and run your tests.

In this step, you're going to:

  • Create a GitHub Actions workflow file that runs on every push.
  • Break a test intentionally and watch the pipeline catch it.
  • Fix the test and verify the pipeline passes.

Create the workflow file

Now we're going to create a new file for our workflow.

  • Open the workflows folder inside of the .github directory
  • Right click on the workflows folder in the explorer menu
  • Select "New file..."
  • Name the file: ci.yml (YAML format)
  • Enter the following workflow configuration:

Explain this workflow

name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest

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

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

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

      - name: Run tests
        run: pytest -v

Breaking down the workflow

  • on: Triggers the workflow on push or pull request to main.
  • runs-on: ubuntu-latest: Runs your code on a fresh Linux server provided by GitHub.
  • steps: The sequence of commands to run: checkout code, setup Python, install dependencies, and test.
  • Save the file. (Ctrl/Cmd + S)

Watch the workflow run

  • Open your repository on GitHub.
  • Click the Actions tab.

GitHub will ask you to enable GitHub Actions for a forked repo,

  • Select Enable

Push to GitHub

  • Open the integrated Cursor terminal (Ctrl/Cmd + `)
  • Run these commands to commit and push your changes:
git add .
git commit -m "Add multiply function, tests, and CI workflow"
git push origin main

What do these commands do?

  • git add .: Stages all your changed files for commit
  • git commit -m "...": Saves your changes with a descriptive message
  • git push origin main: Uploads your commits to GitHub

โœ… Push successful

Great! Your code is now on GitHub. Let's check the Actions tab.

โŒ Push failed

If your push failed, common issues include:

  • "Permission denied" or "Authentication failed": You need to set up GitHub authentication. Go to github.com/settings/tokens and generate a Personal Access Token (classic) with repo scope. When Git prompts for a password, use this token instead of your GitHub password.
  • "Remote rejected" or "failed to push": Make sure you're pushing to your forked repo, not the original. Check with git remote -v.

Ask for help or share your error with the NextWork community!

  • Navigate back to your GitHub Actions tab.

Have you used GitHub Actions on this account before?

โœ… Yes, I have

You're all set - GitHub Actions is already enabled on your account.

๐Ÿ†• No, this is my first time

For forked repos, GitHub requires you to manually enable Actions.

  • Click the green "I understand my workflows, go ahead and enable them" button on the Actions tab.

Once enabled, your workflow will start running automatically on your next push. If you already pushed, re-push with:

git commit --allow-empty -m "Trigger workflow"
git push origin main

What do you see in the Actions tab?

โœ… Workflow running

You should see your workflow in progress with a yellow circle, then complete with a green checkmark.

What just happened?

GitHub detected your push, found ci.yml in .github/workflows/, and automatically started running the steps you defined. It spun up a fresh Linux server, installed Python, ran your tests, and reported the results - all in about a minute.

โš ๏ธ Not seeing the workflow

If your workflow isn't appearing in the Actions tab:

  • No new workflow run appearing - Make sure you pushed to main (not a different branch). Check with git branch to see your current branch.
  • "Workflow file is invalid" - YAML is sensitive to indentation. Every level must use exactly 2 spaces (not tabs). Check your ci.yml formatting carefully.
  • Workflow appears but is greyed out - Click on it to see if there's a permissions prompt. Forked repos sometimes need manual approval.

Get help with workflow issues or share your error with the NextWork community!

Break a test

Breaking things on purpose!

We're intentionally making a test fail so you can see how CI catches bugs. This is exactly what CI is for - catching mistakes before they reach production!

Let's see what happens when a test fails.

  • Open tests/test_app.py in Cursor.
  • Edit one of the test assertions to be incorrect:
def test_multiple(self):
    """Test multiplying two positive numbers."""
    assert multiply(9, 9) == 18  # This is wrong!
  • Save the file. (Ctrl/Cmd + S)
  • Commit and push the change:
git add .
git commit -m "Break a test"
git push origin main
  • Go back to the Actions tab on GitHub.

You should see the workflow fail with a red X.

  • Click on the failed run to see what went wrong.
  • Click on the failed build section.

You can see exactly which test failed and why - the assertion 81 != 18 tells you the expected vs actual value.

Fix the test

  • Open tests/test_app.py in Cursor.
  • Fix the assertion back to the correct value:
def test_multiple(self):
    """Test multiplying two positive numbers."""
    assert multiply(9, 9) == 81
  • Save the file. (Ctrl/Cmd + S)
  • Commit and push the fix:
git add .
git commit -m "Fix the test"
git push origin main
  • Go back to the Actions tab on GitHub.

You should see the workflow pass with a green checkmark. Your fix worked!

  • Click on the successful run to see the details.

Every step completed successfully - checkout, Python setup, dependency installation, and most importantly, your tests all passed.

Step #3 Complete

You now have a working CI pipeline that automatically runs tests on every push to main.

Secret mission

Welcome to your secret mission!

Your CI pipeline runs tests, but what if it could also package your code for distribution? That's exactly what you'll build here. You'll add a build step that turns your Python project into an installable package, then upload it as a GitHub artifact that anyone on your team can download.

Package Your Code

Clean Up Your Resources

Clean Up Your Resources

Nice work completing this project! Let's clean up the resources we created.

Resources to clean up:

  • Deactivate your Python virtual environment
  • Delete the forked repository (optional)
  • Delete the local project folder (optional)

Deactivate Virtual Environment

First, deactivate your virtual environment:

deactivate

You should see the (venv) prefix disappear from your terminal prompt.

What About Your GitHub Resources?

You have options for the forked repository you created:

๐Ÿ’พ Keep the repository

If you want to keep practicing with CI/CD, keep the repository!

Your pipeline is ready to run anytime - just push changes to see GitHub Actions in action.

Ideas for practice:

  • Add more test cases to test_app.py
  • Add new functions to app.py
  • Try adding code coverage reporting to your workflow
  • Experiment with matrix builds to test multiple Python versions

๐Ÿ—‘๏ธ Delete the repository

If you're completely done with the repository:

  • Open your GitHub repositories
  • Find your ai-cicd-github fork
  • Click on it, then go to Settings
  • Scroll to the bottom and click Delete this repository
  • Follow the confirmation prompts

Your forked repository is now removed.

Delete Project Files (Optional)

๐Ÿ’พ Keep the files

No action needed! Your project files will stay on your computer for future reference.

๐Ÿ—‘๏ธ Delete the files

To remove the local project folder:

  • Close Cursor (make sure no files from this project are open)
  • Navigate to where you saved the project (e.g. Desktop)
  • Delete the ai-cicd-github folder

That's a wrap!

That's a wrap!

Congrats on completing this project! You've built your first CI/CD pipeline with GitHub Actions.

What you learned:

  • ๐Ÿ Setting up a Python project with virtual environments
  • ๐Ÿงช Writing tests with pytest
  • ๐Ÿ”„ GitHub Actions workflow configuration
  • โšก Automated CI/CD pipelines that run on every push
  • ๐Ÿ“ฆ Building and uploading artifacts (Secret Mission)

Note

If you're EVER stuck - ask the NextWork community. Students like you are already asking questions about this project.

These skills are valuable for:

  • Catching bugs before they reach production
  • Maintaining consistent code quality across teams
  • Automating repetitive testing and build tasks
  • Building confidence in your deployments

p.s. Does it say "Still tasks to complete!" at the bottom of the screen?

This means you still have screenshots left to upload, or questions left to answer!

  1. Press Ctrl+F (Windows) or Command+F (Mac) on your keyboard.
  2. Search for the text Return to later.
  3. Jump straight to your incomplete tasks!
  4. ๐Ÿ™‹โ€โ™€๏ธ Still stuck? Ask the community!