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:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm testname: shown in the Actions tab.on: the events that trigger the workflow.jobs: one or more jobs; each runs on its own runner chosen byruns-on.steps: run in order.usesruns a ready-made action;runruns a shell command.
Triggers
| Trigger | Runs when |
|---|---|
push | Commits are pushed (filter with branches: [main]) |
pull_request | A pull request is opened or updated |
schedule | On a timetable, using cron syntax |
workflow_dispatch | Someone clicks Run workflow |
release | A 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:
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.
onsets the triggers;jobscontainstepsthat eitherusesan action orruna 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.
Running tests on every pull request
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 testCI / 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 passedLine-by-line explanation
- Lines 2 to 5Run on every pull request and on pushes to main.
- Line 10Download the repository onto the runner.
- Lines 11 to 13Install Node.js version 20.
- Lines 14 to 15Install exact dependencies, then run the tests.
Testing on several versions
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 pytesttest (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 37sLine-by-line explanation
- Line 3The operating system comes from the matrix.
- Lines 5 to 7Two systems times two versions gives four jobs, run in parallel.
- 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.
A scheduled job with a secret
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 }}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
- Line 4Cron fields are minute, hour, day, month and weekday.
- Line 5Adds a Run workflow button.
- Lines 12 to 13The secret is passed to the script as an environment variable.
Common GitHub Actions Mistakes
Tabs or wrong indentation
jobs:
test:
runs-on: ubuntu-latestjobs:
test:
runs-on: ubuntu-latestForgetting to check out the code
steps:
- run: npm test # the runner is emptysteps:
- uses: actions/checkout@v4
- run: npm testSecrets in the workflow file
env:
API_KEY: my-real-api-key-123env:
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.
Complete the folder where workflow files live.
.github//ci.yml
What does on: pull_request do?
What's the difference between uses and run?
A matrix lists 3 Node versions and 2 operating systems. How many jobs run?
How should a workflow use an API key?
GitHub Actions Quiz
5 questions to check what you learned.
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
What does
runs-on: ubuntu-latestchoose?- The language version
- The branch to deploy
- The type of machine (runner) the job runs on
- The workflow name
Which trigger adds a "Run workflow" button?
- workflow_dispatch
- push
- schedule
- release
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
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