Skip to content
Commitpedia
Log in Join & edit

Your first GitHub Actions workflow: run tests on every push

Build a working GitHub Actions pipeline from nothing: a small Python project with a test, a workflow file that runs pytest on every push and pull request, a deliberate failure to see the red cross, and a matrix.

Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?

Edit history of Your first GitHub Actions workflow: run tests on every push: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
4 min

What you will build

A GitHub repository with a tiny Python module, one pytest test, and a workflow that installs dependencies and runs the tests on every push to main and on every pull request. You will watch it pass, break it on purpose to see how a failure looks, fix it, and finally extend it to test on two operating systems and two Python versions at once. The same shape works for any language; only the install and test commands change.

Before you start

  • A GitHub account and git installed locally, with push access over SSH or HTTPS.
  • Python 3 installed locally so you can run the test yourself before CI does.
  • About 20 minutes. GitHub Actions is free for public repositories and has a free monthly allowance for private ones.

Step 1: create the project

mkdir ci-demo && cd ci-demo
git init -b main
mkdir calc tests
printf 'def add(a, b):\n    return a + b\n' > calc/__init__.py
printf 'from calc import add\n\n\ndef test_add():\n    assert add(2, 3) == 5\n' > tests/test_calc.py
echo 'pytest==8.4.1' > requirements.txt
python3 -m venv .venv && . .venv/bin/activate
python -m pip install -r requirements.txt
python -m pytest -v

The last command should print 1 passed. Add a .gitignore with .venv/ and __pycache__/ so the virtual environment is never committed.

Step 2: write the workflow

GitHub looks for workflows in .github/workflows/. Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
          cache: pip

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

      - name: Run tests
        run: python -m pytest -v
  • on lists the events that start the workflow: pushes to main and any pull request.
  • jobs.test is one job; runs-on: ubuntu-latest asks for a fresh Ubuntu virtual machine from GitHub.
  • Each uses step runs a reusable action: actions/checkout clones your repository into the runner, actions/setup-python installs the requested Python and, with cache: pip, caches downloaded packages between runs.
  • Each run step is a shell command, exactly what you typed locally. If any step exits non-zero, the job fails and later steps are skipped.

Step 3: push it to GitHub

Create an empty repository on GitHub named ci-demo (no README, so the first push is clean), then:

git add .
git commit -m "Add calc module, test and CI workflow"
git remote add origin [email protected]:YOUR-USER/ci-demo.git
git push -u origin main

Open the repository in the browser and click the Actions tab. A run named after your commit message appears within seconds, first with a yellow dot, then a green check. Click it, then the test job, to see every step’s output, including the pytest line 1 passed.

Step 4: break it on purpose

Seeing a failure once, in a safe setting, makes the real ones much less alarming. Change the expected value in the test and push on a branch:

git switch -c break-the-build
sed -i.bak 's/== 5/== 6/' tests/test_calc.py && rm tests/test_calc.py.bak
git commit -am "Break the test on purpose"
git push -u origin break-the-build

Open a pull request from break-the-build to main. At the bottom of the pull request the check CI / test turns red; Details leads to the log where pytest shows assert 5 == 6. Fix the test (change 6 back to 5), commit and push again; the check re-runs on its own and turns green. Then merge the pull request from the web interface, or close it and delete the branch.

Step 5: test on more than one platform

Replace the job with a matrix so the same steps run for each combination of operating system and Python version. Edit .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
        python: ["3.12", "3.13"]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
          cache: pip
      - run: python -m pip install -r requirements.txt
      - run: python -m pytest -v

fail-fast: false lets all four combinations finish even if one fails, so you see the full picture. Commit and push; the Actions tab now shows four jobs named test (ubuntu-latest, 3.12) and so on, running in parallel.

Check that it works

  • The Actions tab shows a green run for your latest push on main, with four matrix jobs.
  • The commit list on the repository front page shows a green check next to the newest commit.
  • Opening a pull request shows the CI checks and blocks the merge button while they run.
  • Optional: in Settings > Branches add a protection rule for main that requires the test checks to pass before merging.

Clean up

Nothing runs unless you push, so an idle repository costs nothing. If you do not want to keep it, delete the repository under Settings > General > Delete this repository, and remove the local folder with rm -rf ci-demo.

Next steps

  • Add a linting step before the tests, for example pip install ruff && ruff check ., so style problems fail fast.
  • Read the GitHub Actions cheat sheet for triggers, secrets, caching and job dependencies.
  • Run the same hooks locally before every commit with pre-commit, so CI rarely has to tell you about trailing whitespace.
  • Try the workflow locally without pushing using nektos/act.

Official documentation

Did this work for you?

No votes yet. Be the first.

If it did not work, the report form below tells the editors what went wrong.

See also

  • 0039 A minimal GitHub Actions workflow that runs your tests Add one YAML file under .github/workflows and GitHub runs your tests on every push and pull request. Here is a small, explained… Intermediate · Step by step · Any system Article GitHub Actions 15 Oct 2026
  • 0256 Manage infrastructure as code with Terraform: from first file to plans on pull requests Learn the full Terraform workflow without a cloud account: variables, a module, outputs, state, plan, apply and destroy, a backend for shared… Beginner · Guided project · Linux, macOS, Windows Article GitHub Actions · Terraform 15 Oct 2026
  • 0313 What is CI/CD? CI/CD is the habit of automatically testing every code change and, once it passes, deploying it without manual steps. Beginner · Explains an idea · Any system Article Pipelines, runners & hooks 15 Oct 2026
  • 0378 Continuous integration explained What continuous integration means, why small frequent merges plus an automated build on every change prevent integration pain, what a good pipeline… Beginner · Explains an idea · Any system Article Pipelines, runners & hooks 15 Oct 2026
  • 0380 GitHub Actions cheat sheet: triggers, matrix, secrets, caching The GitHub Actions syntax you reach for most, in copy-ready blocks: event triggers and filters, build matrices, secrets and permissions, dependency caching… Intermediate · Cheat sheet · Any system Article GitHub Actions 15 Oct 2026
  • 0381 pre-commit hooks: catch mistakes before they are committed Install the pre-commit framework, configure hooks for whitespace, YAML and JSON syntax, large files, leaked private keys, Python linting and shellcheck, run… Beginner · Step by step · Linux, macOS, Windows Article Pipelines, runners & hooks · Setup 15 Oct 2026

Repos in the wild

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Dominik +5,819 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?