Learning how to use GitHub Actions means writing YAML files that automatically run your tests, builds, and deployments whenever something happens in your repository—a push, a pull request, a release, or a schedule. This guide covers the core concepts, walks through a complete working CI workflow line by line, and shows the building blocks—matrix builds, secrets, caching—plus the cost and security tradeoffs that trip people up. By the end you'll be able to automate your own pipeline from scratch.
What GitHub Actions is and its core concepts
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform built directly into GitHub. CI/CD means automatically testing every change and, optionally, shipping it—no separate service, no extra account. Your automation lives as YAML files in the .github/workflows/ directory of your repo, version-controlled alongside your code.
Five terms make up the entire vocabulary, and getting them straight makes everything else click:
- Workflow — an automated process defined in one YAML file. A repo can have many.
- Event — the trigger that starts a workflow: a
push, apull_request, aschedule, a manualworkflow_dispatch, and dozens more. - Job — a set of steps that run together on one machine. Jobs run in parallel by default; you can make one wait for another with
needs. - Step — a single task in a job, either a shell command (
run) or a prepackaged action (uses). - Action — a reusable unit of code published by GitHub or the community, like
actions/checkoutto clone your repo. Runner — the virtual machine that executes a job, such asubuntu-latest.
That hierarchy—a workflow triggered by an event, containing jobs, made of steps, some of which call actions, all running on a runner—is the whole mental model.
Writing your first workflow
The best way to learn is to read a real one. Create .github/workflows/ci.yml, commit it, and GitHub runs it automatically. Here's a complete continuous-integration workflow for a Node.js project that runs on every push to main and every pull request:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest # GitHub-hosted Ubuntu 24.04 VM
steps:
- name: Check out the code
uses: actions/checkout@v6 # clone the repo into the runner
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm' # cache dependencies between runs
- name: Install dependencies
run: npm ci # clean, reproducible install
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
Reading top to bottom: the workflow is named CI; the on block declares it fires on pushes and PRs targeting main; the single test job runs on a GitHub-hosted Ubuntu 24.04 machine; and its steps check out the code, install Node 22, install dependencies with npm ci, then lint and test. If any step exits with a non-zero code, the job fails and—if you've enabled it—blocks the pull request from merging. That blocking behavior is what turns a workflow into a real quality gate, the practice covered in our guide to Git workflow best practices.
One important habit visible above: actions are pinned to a major version like @v6. These versions increment regularly—actions/checkout is on v6, setup-node on v4, setup-python on v5 at the time of writing—so check each action's repository for the current major and pin to it deliberately rather than chasing @latest.
The building blocks that make workflows powerful
A passing test suite is the start. A few features turn a basic workflow into a robust pipeline.
Matrix builds
A matrix runs the same job across multiple combinations of variables—operating systems, language versions, dependencies—in parallel. It's the fastest way to catch compatibility bugs:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: ['20', '22']
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
This single block spins up six parallel jobs (three operating systems times two Node versions). Be aware of the cost implications, which the next section covers—Windows and macOS minutes are far more expensive than Linux.
Secrets and variables
Never hardcode credentials in a workflow file; anyone with read access can see it. Store sensitive values as encrypted secrets under Settings → Secrets and variables → Actions, then reference them through the secrets context. Non-sensitive config goes in vars:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
REGION: ${{ vars.AWS_REGION }}
run: ./deploy.sh
Secrets are masked in logs automatically, but treat that as a safety net, not a license to print them.
Caching and artifacts
Re-downloading dependencies on every run wastes minutes. The cache feature (and the built-in cache option in setup actions) restores them between runs, often cutting build time substantially. Artifacts, by contrast, persist files produced by a run—test reports, compiled binaries, coverage data—so later jobs or you can download them:
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
Caching saves inputs to speed jobs up; artifacts save outputs to inspect or pass along. Don't confuse the two.
From CI to CD and beyond
Once tests pass reliably, the natural next step is continuous delivery—deploying automatically. Add a deploy job that depends on tests succeeding and runs only on the main branch:
deploy:
needs: test # wait for the test job to pass
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # enables approvals and protections
steps:
- uses: actions/checkout@v6
- run: ./deploy.sh
The environment key unlocks deployment protections like required reviewers and wait timers, so a human can approve production releases. This is the foundation of a full pipeline—our CI/CD pipeline setup guide goes deeper on structuring multi-stage deployments.
Two patterns are worth building in early. First, decouple deploying code from releasing features by wiring feature flags into your release process, so you can ship dark and turn features on without another deploy. Second, treat schema changes as a deliberate, reversible pipeline step rather than a manual scramble; running your migrations through the pipeline with one of the database migration tools worth comparing keeps deploys safe and repeatable. For shared logic across many repos, factor common steps into reusable workflows (called with uses: at the job level) so you maintain your pipeline in one place.
Costs, performance, and security
GitHub Actions is generous but not unlimited, and a few defaults are worth tightening.
Cost. Workflows on public repositories are free, and the free plan includes 2,000 minutes per month for private repositories (with more on paid plans). The catch is the per-OS multiplier: Linux minutes count as 1×, but Windows minutes count double (2×) and macOS minutes count 10×. A macOS matrix job burns your allowance ten times faster than the equivalent Linux job, so reserve expensive runners for builds that genuinely need them.
Performance. Cache dependencies, run independent jobs in parallel, and add a concurrency group to cancel superseded runs when you push twice in quick succession:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Security. Three habits matter most. Pin third-party actions to a specific version—and for untrusted ones, to a full commit SHA—so a compromised update can't silently run in your pipeline. Restrict the automatically provided GITHUB_TOKEN to least privilege with a permissions: block rather than its broad defaults. And be cautious with workflows triggered by pull requests from forks, which should never expose your secrets to untrusted code. Also pin your runner OS (for example ubuntu-24.04 instead of ubuntu-latest) on critical pipelines, since the -latest label migrates to new OS versions over a one-to-two-month window and can change behavior under you.
Common mistakes to avoid
Hardcoding secrets in the YAML. Workflow files are visible to anyone with repo access. Always use encrypted secrets.
Ignoring the macOS multiplier. Teams blow through their minutes running everything on macOS out of habit. Default to Linux and use other runners only when required.
Using @latest or floating tags for actions. An unpinned action can change—or be compromised—without warning. Pin to a major version, or a SHA for anything untrusted.
No caching. Re-installing dependencies every run wastes minutes and slows feedback. Cache them.
Giant monolithic workflows. One sprawling job that does everything is slow and hard to debug. Split into focused jobs and parallelize.
Leaving GITHUB_TOKEN over-permissioned. The default token permissions are broader than most workflows need. Scope them down explicitly.
Frequently asked questions
Is GitHub Actions free to use? Yes for public repositories. Private repositories on the free plan get 2,000 included minutes per month, with larger allowances on paid plans. Remember that Windows runners consume minutes at 2× and macOS at 10× the Linux rate.
Where do GitHub Actions workflows live?
In the .github/workflows/ directory at the root of your repository, as .yml or .yaml files. GitHub automatically detects and runs them based on the events declared in each file's on block.
What's the difference between a job and a step?
A job is a group of steps that run together on a single runner; jobs run in parallel unless you chain them with needs. A step is a single task within a job—either a shell command or a reusable action.
How do I store secrets in GitHub Actions?
Add them under Settings → Secrets and variables → Actions, then reference them with the secrets context, like ${{ secrets.API_KEY }}. They're encrypted at rest and masked in logs. Never commit credentials directly into a workflow file.
Can GitHub Actions deploy my application, not just test it?
Yes. Add a deploy job that runs after tests pass, gate it to your main branch, and use an environment to require approvals. Combined with reusable workflows and feature flags, Actions can run your entire build-test-deploy pipeline.
The takeaway
Knowing how to use GitHub Actions starts with one small file: a workflow that checks out your code and runs your tests on every push. From there you layer on matrix builds, caching, secrets, and a gated deploy job until the whole build-test-ship loop is automated. Your next step is to drop the CI workflow above into .github/workflows/ci.yml, adjust the install and test commands for your stack, and push—you'll have working continuous integration within minutes, and a foundation you can grow into a full pipeline.