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 as push, 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 test

Read 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 live

A 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 -q

Quote 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 test

That 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' button

Combined 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:

![CI](https://github.com/stackcone/demo/actions/workflows/ci.yml/badge.svg)

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 ci or pinned requirements, and matrices for multiple versions.
  • Keep secrets in GitHub Secrets and give GITHUB_TOKEN minimal permissions.
  • Protect main so only green pull requests can merge.
# Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Last lessonFinish Git & Command LineMark this lesson complete and pick your next course.