Automating Checks with GitHub Actions
Continuous integration (CI) means automatically building and testing the code every time someone pushes or opens a pull request. On GitHub this is done with GitHub Actions: workflow files, committed to the repository, that tell GitHub what to run and when.
Why automate?
- Every change is tested the same way, on a clean machine — no "works on my computer".
- Reviewers see a green tick or red cross on the pull request before reading a line.
- Branch protection can require the checks to pass before merging (lesson 36), so
mainstays working. - The same system can deploy the site, publish packages or create releases when you push a tag.
A first workflow
Workflows are YAML files in .github/workflows/. This one runs a Node.js project's tests on every push to main and on every pull request:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm test
Commit it and push. Open the repository's Actions tab to watch it run, and every pull request will show the result.
Reading the workflow
| Key | Meaning |
|---|---|
name | The name shown in the Actions tab and on pull requests |
on | The events that trigger it: pushes, pull requests, tags, a schedule, a manual button… |
jobs | One or more jobs; they run in parallel unless one needs another |
runs-on | The kind of virtual machine (a runner): Ubuntu, Windows or macOS |
steps | Run in order. uses runs a published action; run runs shell commands |
actions/checkout | Clones your repository onto the runner — almost every workflow starts with it |
A Python version, tested on several versions
A matrix runs the same job with different settings — here three Python versions in parallel:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- run: pytest
Running on a tag: releases
Workflows can react to tags (lesson 39). This one creates a GitHub release whenever a tag like v1.2.0 is pushed:
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: gh release create "$GITHUB_REF_NAME" --generate-notes
env:
GH_TOKEN: ${{ github.token }}
Secrets
Deployment keys and API tokens must never be committed. Store them under Settings → Secrets and variables → Actions, and refer to them as ${{ secrets.NAME }}; GitHub hides their values in logs. Give each workflow only the permissions it needs, as the release example does.
Tips
- Keep CI fast. Cache dependencies (like
cache: npm) and split slow jobs, so feedback arrives in minutes. - Pin action versions — at least to a major version like
@v5; security-conscious projects pin to a full commit hash. - Make it match local commands. If CI runs
npm test, developers can run exactly the same thing before pushing. - Read failures from the log. Click the red cross on a pull request to see which step failed and its output.
Other CI services
GitLab CI/CD (.gitlab-ci.yml), Bitbucket Pipelines, Azure Pipelines, CircleCI and Jenkins follow the same idea: a file in the repository describes jobs that run on each push. The details differ, but the concepts — triggers, jobs, steps, runners, secrets — carry over.
Try it yourself
Add a workflow to one of your repositories that runs on every push and prints the Git log with run: git log --oneline -5 (tip: actions/checkout fetches only one commit by default; add with: fetch-depth: 0 for full history). Open a pull request and watch the check appear.