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 main stays 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:

YAML .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-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

KeyMeaning
nameThe name shown in the Actions tab and on pull requests
onThe events that trigger it: pushes, pull requests, tags, a schedule, a manual button…
jobsOne or more jobs; they run in parallel unless one needs another
runs-onThe kind of virtual machine (a runner): Ubuntu, Windows or macOS
stepsRun in order. uses runs a published action; run runs shell commands
actions/checkoutClones 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:

YAML .github/workflows/tests.yml
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:

YAML .github/workflows/release.yml
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.