GitHub Actions basics: a practical guide for developers
If you're running manual test suites before merging code or remembering to deploy builds to staging, you're spending time that machines should handle for you. GitHub Actions lets you automate exactly those tasks—tests, builds, deployments—right from your repository, and it runs for free on public repos (with generous minutes on private ones). In this guide, we'll walk through setting up your first workflows, parallelizing builds, and managing secrets safely.
What are workflows, and why they live in .github/workflows
A GitHub Actions workflow is an automated process triggered by events in your repository. It's defined in a YAML file stored in the .github/workflows directory at the root of your project. When you push code, open a pull request, or manually trigger a workflow, GitHub spins up a runner (a virtual machine) and executes your steps in sequence.
The file structure looks like this:
your-repo/
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── deploy.yml
├── src/
├── tests/
└── package.jsonEach YAML file is one workflow. You can have multiple workflows—one for CI, one for deployment, one for security scanning. GitHub treats them independently, so they run in parallel if triggered by the same event.
The key sections in a workflow file are:
- name: What GitHub displays in the UI.
- on: The event(s) that trigger the workflow.
- jobs: The tasks to run, each with steps.
- runs-on: The operating system for the runner.
- steps: Individual commands or actions executed in order.
Workflows are version-controlled like any other code, so your entire team sees what's automated and can propose changes via pull request.
Building a CI workflow: tests on every push and PR
The most common workflow is continuous integration (CI): run your test suite automatically whenever code enters the repository. This catches bugs before they reach main or production branches.
Here's a practical CI workflow for a Node.js project:
name: CI
on: [push, 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 && npm testLet's break down what happens:
- on: [push, pull_request] — The workflow triggers on any push to any branch and on every pull request opened or updated.
- runs-on: ubuntu-latest — GitHub provisions a fresh Ubuntu runner. You can also use windows-latest or macos-latest, or a self-hosted runner for more control.
- actions/checkout@v4 — Clones your repository code into the runner's workspace. Always the first step.
- actions/setup-node@v4 — Installs Node.js (version 22) and sets up npm caching. Caching npm dependencies cuts workflow time from minutes to seconds on repeat runs.
- npm ci && npm test — Installs dependencies with ci (clean install, preferred in CI) and runs your test script. If tests fail, the job fails and GitHub blocks the PR merge (if branch protection is enabled).
After this workflow runs, you'll see a green checkmark or red X next to your commit in the repository. On PRs, the checks appear below the description, making it clear whether the code is safe to merge.
For a production React or Next.js project, you might add a linting step:
name: CI
on: [push, 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 run build
- run: npm testThis ensures code style, builds correctly, and passes tests—three layers of validation before any human review.
Matrix builds: testing across versions in parallel
Sometimes you need to verify your code works on multiple versions of a runtime or dependency. Writing separate jobs for Node 18, 20, and 22 is repetitive. Matrix builds solve this by running the same job configuration with different variables in parallel.
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci && npm testGitHub will spawn three jobs in parallel, each with a different Node version. The matrix syntax ${{ matrix.node-version }} injects the value. If any job fails, the overall workflow fails.
Matrices are also useful for testing against multiple databases or cloud regions. For example:
strategy:
matrix:
db: [postgres, mysql, sqlite]
os: [ubuntu-latest, windows-latest]This creates six combinations (3 databases × 2 operating systems), all running in parallel. It's a powerful way to catch environment-specific bugs early.
Managing secrets: tokens, keys, and credentials
Your workflows often need to authenticate with external services: deploying to AWS, publishing to npm, posting notifications to Slack. Hardcoding credentials in your YAML file is a security disaster. Instead, use GitHub Secrets.
To add a secret:
- Go to your repository settings.
- Click "Secrets and variables" → "Actions."
- Click "New repository secret."
- Enter a name (e.g.,
NPM_TOKEN) and the value (your actual token).
Once created, reference it in your workflow with ${{ secrets.NPM_TOKEN }}:
name: Publish
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}GitHub masks secrets in logs, so your token never appears in workflow output, even if a command echoes it. For organization-wide secrets, store them at the organization level so all repositories can access them. For environment-specific secrets (e.g., production API keys only for deployments to main), use environment secrets, which override repository secrets when deploying to that environment.
Common mistakes
Using push and pull_request without branch filters. By default, workflows run on all branches. For deployment workflows, filter to main or production only using branches: [main] under the on trigger. Otherwise, you'll deploy every feature branch.
Not caching dependencies. Without caching, npm install takes 30 seconds every run. With caching, it takes 3 seconds. Use the cache: npm option in setup-node or configure caching explicitly.
Forgetting to checkout code. The first step should always be actions/checkout@v4. Without it, your steps run in an empty workspace.
Committing secrets to the repository. Even if you delete a secret later, it's still in git history. Use GitHub Secrets for anything sensitive, and use a tool like git-secrets or Gitleaks locally to prevent accidental commits.
Assuming free tier limits. Public repositories get unlimited Actions minutes. Private repositories get 2,000 minutes per month. If you have heavy workflows, consider self-hosted runners or upgrading your plan.
Key takeaways
- Workflows are YAML files in
.github/workflowsthat automate tasks on repository events. They're version-controlled and collaborate-friendly. - A basic CI workflow runs tests on every push and PR, providing fast feedback to developers and preventing broken code from merging.
- Matrix builds test across multiple versions or configurations in parallel, catching environment-specific bugs without duplicating job definitions.
- GitHub Secrets securely store tokens and credentials; reference them with
${{ secrets.NAME }}and never hardcode sensitive data in YAML. - Start simple with a single test job, then add matrix builds, linting, and deployments as your project grows.






Comments
Loading comments…