GitHub Actions Basics
A complete GitHub Actions workflow that checks out code, installs dependencies, and runs tests on every push and PR.
What GitHub Actions is
GitHub Actions is GitHub's built-in CI/CD engine. You define workflows as YAML files committed to your repository at .github/workflows/*.yml, and GitHub runs them automatically whenever the events you configure occur — a push, a pull request, a schedule, or a manual trigger.
A complete example workflow
Here is a full, working workflow that checks out a Node.js project, installs its dependencies, and runs its test suite on every push and pull request:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Drop this file in .github/workflows/ci.yml, commit it, and push — GitHub picks it up automatically. No separate CI server to install or maintain.
Breaking it down
on — triggers
The on key defines which events start the workflow:
on:
push:
branches: [main]
pull_request:
branches: [main]
This runs the workflow on every push to main, and on every pull request targeting main (from any branch, including forks). Other common triggers:
on:
schedule:
- cron: '0 3 * * *' # every day at 03:00 UTC
workflow_dispatch: {} # adds a "Run workflow" button in the GitHub UI
jobs — units of work
A workflow is made of one or more jobs. Each job runs on a fresh virtual machine (a "runner") specified by runs-on, and by default, jobs in the same workflow run in parallel unless you tell one to wait on another with needs:
jobs:
test:
runs-on: ubuntu-latest
# ...
lint:
runs-on: ubuntu-latest
# ...
build:
needs: [test, lint] # waits for both to succeed first
runs-on: ubuntu-latest
# ...
steps — the sequence within a job
Each job runs a list of steps in order, top to bottom. A step either:
- runs a reusable action with
uses:(e.g.actions/checkout@v4— a packaged, versioned unit someone else wrote), or - runs a raw shell command with
run:(executed on the runner's shell —bashby default on Linux runners).
steps:
- name: Checkout code
uses: actions/checkout@v4 # clones your repo onto the runner
- name: Set up Node.js
uses: actions/setup-node@v4 # installs the requested Node.js version
with:
node-version: '20'
- name: Install dependencies
run: npm ci # a raw shell command
- name: Run tests
run: npm test
actions/checkout@v4 deserves special mention: a fresh runner starts with an empty filesystem — without this step as the first one, there is no repository code to build or test at all.
Pinning versions
Always pin actions to a version (@v4, or better, a full commit SHA for supply-chain security) rather than @main — an unpinned action can change behavior under you without warning.
Common mistakes
- Forgetting
actions/checkoutas the first step — every later step then runs against an empty directory. - Using
npm installinstead ofnpm ciin CI —npm ciinstalls exactly what's inpackage-lock.jsonand fails loudly on a mismatch, which is what you want in an automated pipeline;npm installcan silently update the lockfile. - Not scoping
on.push.branches— without it, the workflow fires on pushes to every branch, which is often wasteful and slows down feedback on unrelated work. - Assuming jobs run in sequence by default — they run in parallel unless you add
needs.