Git & Command Line · Lesson 12 of 12
CI with GitHub Actions
Set up continuous integration with GitHub Actions: write workflow YAML that runs tests on every push and pull request, with caching, matrices and secrets.
- Intermediate
- 17 min read
- 4 objectives
Before this lessonLesson 11: Cherry-pick, Bisect and Reflog
What you will learn
- Explain what CI is and why teams use it
- Write a workflow with triggers, jobs and steps
- Use a matrix, caching and secrets
- Require passing checks before merging
Your Progress
0 of 12 lessons 0%
- Lessons0 / 12
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
"It works on my machine" is the oldest excuse in software. Continuous integration (CI) fixes it: every time someone pushes or opens a pull request, a fresh machine checks out the code, installs dependencies and runs the tests. If anything fails, the pull request shows a red cross before a human even starts reviewing.
GitHub Actions is GitHub's built-in CI. It is free for public repositories and includes a monthly allowance of minutes for private ones. Everything is configured with YAML files inside your repo, so CI changes go through pull requests like any other code.
The vocabulary
- Workflow: a YAML file in
.github/workflows/. A repo can have many. - Event (
on): what triggers it, such aspush,pull_request, a schedule or a manual button. - Job: a set of steps that runs on one fresh virtual machine (a runner). Jobs run in parallel unless you chain them.
- Step: either a shell command (
run) or a reusable action (uses). - Action: a packaged step from the Marketplace, like
actions/checkout.
Your first workflow
Create .github/workflows/ci.yml in a Node.js project:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm testRead it top to bottom: run on pushes to main and on every pull request; start an Ubuntu machine; check out the repo (the runner starts empty); install Node 22 with the npm cache turned on; install exact dependencies from the lock file with npm ci; lint; test. Each run step fails the job if its command exits non-zero, exactly the exit-code rule from the shell lesson.
mkdir -p .github/workflows
# save the YAML above as .github/workflows/ci.yml
git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push
gh run list --limit 3 # see runs from the terminal
gh run watch # follow the latest run liveA Python version
The shape is the same for any language; only the setup step changes:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
- run: pip install -r requirements.txt
- run: pytest -qQuote version numbers like "3.10": unquoted, YAML reads 3.10 as the number 3.1.
Matrix builds
Libraries often need to work on several versions or operating systems. A matrix runs the same job once per combination, in parallel:
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: [20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm testThat is four jobs. ${{ ... }} is an expression; here it reads values from the matrix. fail-fast: false lets the other combinations finish even if one fails, so you see the full picture.
Secrets and permissions
Never put API keys in a workflow file. Add them under the repo's Settings, Secrets and variables, Actions (or gh secret set DEPLOY_TOKEN), then reference them. GitHub masks secret values in logs:
permissions:
contents: read # least privilege for the built-in GITHUB_TOKEN
jobs:
deploy:
needs: test # run only after the test job passes
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}Other useful triggers
on:
push:
tags: ["v*"] # release when a v1.2.0-style tag is pushed
schedule:
- cron: "0 3 * * 1" # every Monday 03:00 UTC
workflow_dispatch: # adds a 'Run workflow' buttonCombined with the tags lesson, a tag-triggered job can build your app and run gh release create ${{ github.ref_name }} --generate-notes (with GH_TOKEN: ${{ github.token }} and permissions: contents: write), so releasing becomes "push a tag".
Make CI a gate: branch protection
CI only helps if red builds cannot be merged. In the repo settings, add a branch protection rule or ruleset for main and enable Require status checks to pass, selecting your test job. Now the merge button stays disabled until CI is green. Add a status badge to your README so everyone sees the health of main:
When a run fails, open it in the Actions tab (or gh run view --log-failed), find the red step, and reproduce the same command locally. Because the workflow is just shell commands, anything CI does you can run yourself.
Recap
- CI runs your checks on a clean machine for every push and pull request.
- Workflows live in
.github/workflows/*.yml: events, jobs, steps. - Use setup actions with caching,
npm cior pinned requirements, and matrices for multiple versions. - Keep secrets in GitHub Secrets and give
GITHUB_TOKENminimal permissions. - Protect
mainso only green pull requests can merge.
# Write your solution here
Finished reading? Mark this lesson complete to track your progress.
