GitHub Actions

Automate testing, checks and deployments with GitHub Actions workflows written in YAML.

Lesson 15 of 30Advanced17 min read

Every time someone pushes code, a few things should happen: the tests should run, the code style should be checked and, if everything passes on main, the site should be deployed. Doing that by hand is slow and easy to forget. GitHub Actions does it automatically.

Actions runs workflows: small YAML files in your repository that say "when this happens, run these steps". GitHub provides the computers, called runners, so your commands run on a fresh Linux, Windows or macOS machine in the cloud. Results appear on the pull request as green ticks or red crosses, which is why this practice is called continuous integration (CI).

In this lesson you'll read and write a workflow, understand triggers, jobs and steps, use ready-made actions, store secrets safely, test on several versions at once, and deploy a site. You already met Actions in the GitHub Pages lesson: every Pages deployment is a workflow run.

Anatomy of a workflow

A workflow is a .yml file in .github/workflows/. A minimal one:

YAML
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
  • name: shown in the Actions tab.
  • on: the events that trigger the workflow.
  • jobs: one or more jobs; each runs on its own runner chosen by runs-on.
  • steps: run in order. uses runs a ready-made action; run runs a shell command.

Triggers

TriggerRuns when
pushCommits are pushed (filter with branches: [main])
pull_requestA pull request is opened or updated
scheduleOn a timetable, using cron syntax
workflow_dispatchSomeone clicks Run workflow
releaseA release is published

YAML is sensitive to indentation: use spaces, never tabs, and keep children indented two spaces under their parent. Most workflow errors are an indent in the wrong place.

Actions, secrets and variables

Actions are reusable steps shared on the GitHub Marketplace: actions/checkout downloads your repository, and actions/setup-node or actions/setup-python installs a language version. Pin them to a version, such as @v4.

Never write passwords or API keys in a workflow. Add them under Settings, Secrets and variables, Actions, then use them as ${{ secrets.DEPLOY_TOKEN }}. GitHub hides secret values in logs. Each run also gets an automatic GITHUB_TOKEN with limited permissions for working with the repository itself.

Matrix builds and status checks

A matrix runs the same job with different settings, for example testing on three Node.js versions in parallel:

YAML
strategy:
  matrix:
    node: [18, 20, 22]

Each workflow job appears as a status check on pull requests. With branch protection, you can make checks required, so a pull request can't merge until the tests pass. Combined with code review, that keeps main working.

GitHub Actions Syntax

Workflow structure

name: <Workflow name>
on:
  push:
    branches: [main]
  pull_request:
jobs:
  <job-id>:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: <Step name>
        run: <shell command>
        env:
          API_KEY: ${{ secrets.API_KEY }}

GitHub Actions Summary

  • GitHub Actions runs workflows, YAML files in .github/workflows, on GitHub-hosted runners.
  • on sets the triggers; jobs contain steps that either uses an action or run a command.
  • Store sensitive values as secrets and use them with ${{ secrets.NAME }}.
  • A matrix runs one job across several versions or operating systems.
  • Workflow results appear as status checks, which branch protection can require before merging.

GitHub Actions Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Running tests on every pull request

YAML
name: CI
on:
  pull_request:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
Output
CI / test (pull_request)
  ✓ Set up job
  ✓ Run actions/checkout@v4
  ✓ Run actions/setup-node@v4
  ✓ Run npm ci
  ✓ Run npm test          24 tests passed
All checks have passed
Line-by-line explanation
  1. Lines 2 to 5Run on every pull request and on pushes to main.
  2. Line 10Download the repository onto the runner.
  3. Lines 11 to 13Install Node.js version 20.
  4. Lines 14 to 15Install exact dependencies, then run the tests.
Example 2

Testing on several versions

YAML
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        python: ["3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
      - run: python -m pytest
Output
test (ubuntu-latest, 3.11)   ✓ 1m 02s
test (ubuntu-latest, 3.12)   ✓ 58s
test (windows-latest, 3.11)  ✓ 1m 41s
test (windows-latest, 3.12)  ✗ 1m 37s
Line-by-line explanation
  1. Line 3The operating system comes from the matrix.
  2. Lines 5 to 7Two systems times two versions gives four jobs, run in parallel.
  3. Line 12Each job uses its own Python version. The output shows the code fails only on Windows with Python 3.12, information you'd rarely find by testing on one machine.
Example 3

A scheduled job with a secret

YAML
name: Nightly link check
on:
  schedule:
    - cron: "0 3 * * *"
  workflow_dispatch:
jobs:
  links:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/check-links.sh
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
Output
Runs every day at 03:00 UTC, and can also be started
by hand from the Actions tab. The webhook address is
shown as *** in the logs.
Line-by-line explanation
  1. Line 4Cron fields are minute, hour, day, month and weekday.
  2. Line 5Adds a Run workflow button.
  3. Lines 12 to 13The secret is passed to the script as an environment variable.

Common GitHub Actions Mistakes

Tabs or wrong indentation

Don't
jobs:
test:
  runs-on: ubuntu-latest
Do
jobs:
  test:
    runs-on: ubuntu-latest

Forgetting to check out the code

Don't
steps:
  - run: npm test       # the runner is empty
Do
steps:
  - uses: actions/checkout@v4
  - run: npm test

Secrets in the workflow file

Don't
env:
  API_KEY: my-real-api-key-123
Do
env:
  API_KEY: ${{ secrets.API_KEY }}

Anything in the file is visible to everyone who can read the repository.

GitHub Actions in Real Projects

Actions in real projects

Almost every active open-source repository runs tests on pull requests with Actions, and contributors see the results before a maintainer even looks. Teams use workflows to deploy websites, publish packages when a release is created, label new issues, check links nightly and build mobile apps. Public repositories get generous runner time on GitHub-hosted machines, and teams can also connect their own self-hosted runners.

GitHub Actions FAQ

Are GitHub Actions free?

For public repositories, standard GitHub-hosted runners are free. Private repositories get a monthly allowance of free minutes that depends on your plan, and usage above that is billed. You can see your usage in your account's billing settings.

How do I run a job only on the main branch?

Add a condition to the job: if: github.ref == 'refs/heads/main'. Or limit the whole workflow with a branch filter under the trigger, such as push: with branches: [main]. The job condition is useful when other jobs in the same workflow should still run on every branch.

Why didn't my workflow run?

Check that the file is in .github/workflows/, ends in .yml or .yaml and has valid YAML. Then check the trigger: a branch or path filter may exclude your change, and a workflow_dispatch workflow only appears once the file is on the default branch. The Actions tab shows an error for files that can't be parsed.

How do I cache dependencies in GitHub Actions?

Many setup actions have caching built in, for example actions/setup-node with cache: npm or actions/setup-python with cache: pip. For anything else, use actions/cache with a key based on your lock file, so the cache is rebuilt only when dependencies change.

Can I test GitHub Actions workflows on my computer?

Not with GitHub itself, but the open-source tool act runs many workflows locally using Docker. It doesn't match GitHub's runners exactly, so treat it as a quick check. Otherwise, push to a test branch and use workflow_dispatch to rerun as you fix things.

GitHub Actions Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the folder where workflow files live.

.github//ci.yml
Multiple choiceeasy

What does on: pull_request do?

Multiple choicemedium

What's the difference between uses and run?

Predict the outputmedium

A matrix lists 3 Node versions and 2 operating systems. How many jobs run?

Multiple choicehard

How should a workflow use an API key?

GitHub Actions Quiz

5 questions to check what you learned.

  1. What is continuous integration?

    • Merging every branch at once
    • Automatically building and testing code every time changes are pushed
    • Working without branches
    • Writing code in the browser
  2. What does runs-on: ubuntu-latest choose?

    • The language version
    • The branch to deploy
    • The type of machine (runner) the job runs on
    • The workflow name
  3. Which trigger adds a "Run workflow" button?

    • workflow_dispatch
    • push
    • schedule
    • release
  4. How can you stop a pull request merging while its tests fail?

    • Delete the workflow
    • Add a label
    • Use a matrix
    • Make the workflow's check required in branch protection rules
  5. A workflow fails with "npm: package.json not found". What's the likely cause?

    • The runner is too slow
    • Node.js doesn't run on Ubuntu
    • The workflow didn't run actions/checkout, so the repository isn't on the runner
    • Secrets are missing