Why GitHub Actions?
GitHub Actions is deeply integrated into the most popular code hosting platform, requires zero infrastructure setup, and uses a YAML-based configuration that lives directly in your repository. For teams already on GitHub, it removes the need for a separate CI server entirely.
This guide walks through building a production-ready pipeline that runs tests, checks code quality, and deploys to a target environment on every push to main.
Core Concepts to Know First
- Workflow: A YAML file in
.github/workflows/that defines your automation. - Trigger (on): The event that starts the workflow — push, pull request, schedule, etc.
- Job: A set of steps that run on the same runner (virtual machine).
- Step: A single task — either a shell command or a pre-built Action.
- Runner: The execution environment (ubuntu-latest, windows-latest, macos-latest, or self-hosted).
- Action: A reusable unit of automation from the GitHub Marketplace or your own repo.
A Basic CI Workflow
Here's a starting point for a Node.js project that installs dependencies, lints, and runs tests:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
Key decisions in this workflow:
npm ciis used instead ofnpm install— it respects the lock file and is faster in CI.- The
cache: 'npm'option on the setup action caches the npm store, speeding up subsequent runs. - Triggers on both push to main and pull requests, giving you feedback before merging.
Adding a Build and Deploy Stage
Extend the workflow with a deployment job that depends on the test job passing:
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Deploy to server
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: ./scripts/deploy.sh
The needs: test directive ensures the deploy job only runs if tests pass. The if: github.ref == 'refs/heads/main' guard prevents deployments from pull request builds.
Managing Secrets Securely
Never hardcode credentials in your workflow files. Use GitHub's encrypted secrets:
- Go to your repository → Settings → Secrets and variables → Actions.
- Add secrets like
DEPLOY_KEY,AWS_ACCESS_KEY_ID, orDATABASE_URL. - Reference them in workflows as
${{ secrets.SECRET_NAME }}.
Secrets are masked in log output automatically and are never exposed to pull requests from forked repositories by default.
Matrix Builds for Multi-Version Testing
Test against multiple Node.js versions with a matrix strategy:
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
Best Practices Summary
- Pin Action versions to a specific SHA or tag (e.g.,
actions/checkout@v4) to avoid supply chain risks. - Use
npm cior equivalent freeze commands — never bare installs in CI. - Keep job scopes small — separate lint, test, and deploy into distinct jobs.
- Cache dependencies aggressively to reduce build times.
- Use environment protection rules for production deployments to require manual approval.
- Store all secrets in GitHub Secrets, never in code or workflow files directly.
GitHub Actions' free tier covers generous minutes for public repositories, and its tight integration with pull requests, branch protection rules, and deployment environments makes it a compelling choice for teams of any size.