What GitHub Actions Is
GitHub Actions is GitHub's built-in CI/CD platform. You describe automation as workflows — YAML files stored in your repo — that run in response to repository events like a push, a pull request, or a schedule. GitHub runs them on hosted virtual machines called runners.
Where Workflows Live
Workflow files go in .github/workflows/ and end in .yml. Each file is one workflow. A minimal example:
# .github/workflows/ci.yml
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Hello from GitHub Actions"
Mental model
A workflow contains jobs. Each job runs on its own fresh runner and contains ordered steps. A step either runs a shell command (run) or calls a reusable action (uses).
Events and Triggers
The on key declares what starts a workflow. Common triggers:
| Event | Fires when |
|---|---|
push | Commits are pushed to a branch or tag |
pull_request | A PR is opened, updated, or reopened |
schedule | On a cron timetable (UTC) |
workflow_dispatch | Manually, from the Actions tab (can take inputs) |
on:
push:
branches: [main]
paths: ["src/**"] # only when src changes
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * *" # daily at 06:00 UTC
workflow_dispatch:
inputs:
environment:
description: "Deploy target"
default: staging
Jobs, Steps, and Runners
Jobs run in parallel by default. Use needs to make one job wait for another. Each job specifies its runner via runs-on.
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 test
deploy:
needs: test # waits for test to succeed
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
GitHub-hosted runner labels include ubuntu-latest, windows-latest, and macos-latest. For custom hardware or private networks you register your own self-hosted runners.
uses vs run
| Keyword | Purpose |
|---|---|
run | Executes shell commands on the runner. |
uses | Runs a prebuilt action from the Marketplace or a repo, passing inputs via with. |
Pin your actions
Reference actions by a full version tag such as actions/checkout@v4, or for maximum supply-chain safety pin to a full commit SHA. Never rely on a mutable branch like @main for third-party actions.
Matrix Strategy
A matrix runs the same job across many combinations of values — great for testing multiple language versions or operating systems.
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
node: ["20", "22"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
Secrets and Environments
Never hard-code credentials. Store them as repository or organization secrets and read them via the secrets context. Environments add protection rules — required reviewers, wait timers, and environment-scoped secrets.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # requires approval if configured
steps:
- run: ./deploy.sh
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
GITHUB_TOKEN and Permissions
Every run gets an automatic GITHUB_TOKEN secret for authenticating to the GitHub API. Follow least privilege by declaring permissions explicitly — the modern default is read-only.
permissions:
contents: read
pull-requests: write # e.g. to comment on a PR
steps:
- run: gh pr comment $PR --body "Build passed"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR: ${{ github.event.number }}
Caching Dependencies
Caching speeds up builds by restoring dependency directories between runs. Many setup actions cache automatically, but actions/cache gives full control.
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
Artifacts
Artifacts persist files (build output, test reports, coverage) after a run so you can download them or pass them between jobs.
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# in a later job:
- uses: actions/download-artifact@v4
with:
name: dist
Reusable Workflows
Avoid copy-pasting pipelines across repos by defining a workflow that others call with workflow_call.
# .github/workflows/reusable-deploy.yml
on:
workflow_call:
inputs:
env: { required: true, type: string }
secrets:
token: { required: true }
# caller
jobs:
call:
uses: my-org/repo/.github/workflows/reusable-deploy.yml@v1
with:
env: staging
secrets:
token: ${{ secrets.DEPLOY_TOKEN }}
Practice Exercises
- Create a workflow that runs on every
pushandpull_requesttomain, checks out the code, and runs your test suite. - Add a
matrixthat tests against Node 20 and 22 on both Ubuntu and Windows. - Cache your package manager's download directory with
actions/cacheand confirm a cache hit on the second run. - Build the project and upload the
dist/folder as an artifact, then download it in a dependent job thatneedsthe build. - Add a manual
workflow_dispatchdeploy job that targets aproductionenvironment protected by a required reviewer, reading a secret token. - Set top-level
permissionsto read-only and grant only the scopes a job actually needs.