Actions is CI/CD that runs whenever you put a workflow YAML file in your repository. There’s no server to stand up, so it’s painless even for personal projects.

A first workflow

.github/workflows/ci.yml:

  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
  

Push and it runs. Follow along from the terminal with gh run watch.

Core concepts

TermMeaning
workflowOne YAML file defining triggers and jobs
jobRuns on its own runner; jobs are parallel by default
stepA sequential unit inside a job
actionA reusable step (uses:)
runnerThe execution environment (ubuntu-latest, etc.)

Common patterns

Testing several versions (matrix)

  strategy:
  matrix:
    node: [20, 22]
    os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
  

Caching dependencies

  - uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ hashFiles('requirements.txt') }}
  

setup-node, setup-python, and friends handle this more simply via cache:.

Conditional steps

  - name: Deploy
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  run: ./deploy.sh
  

Job dependencies

  jobs:
  build: { ... }
  deploy:
    needs: build
    runs-on: ubuntu-latest
  

Secrets

Register them under Settings → Secrets and variables → Actions, then use them:

  - run: ./deploy.sh
  env:
    API_TOKEN: ${{ secrets.API_TOKEN }}
  

Secrets are masked in logs automatically, but a script that transforms and prints the value can still leak it. Don’t echo them.

Least privilege

Declaring token permissions per workflow is the safer default.

  permissions:
  contents: read
  pull-requests: write
  

Debugging

ProblemCheck
The workflow never runsFile location (.github/workflows/), branch, YAML syntax
Passes locally, fails in CIMissing tooling, environment variables, filename casing
Cache never hitsWhether the key includes a lockfile hash
Permission errorsThe permissions: block and token scopes
  gh run list --limit 5
gh run view <id> --log-failed
  

To run a workflow locally, use act.

  brew install act
act -j test
  

Watch the cost

Public repositories are free, but private ones are billed by minute. macOS runners cost around ten times a Linux one, so use them only when you must. Cancelling superseded runs with concurrency cuts waste.

  concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
  

Next

To manage the infrastructure itself as code → Terraform

Last updated 19 Aug 2026, 00:00 UTC. history