Set Up CI/CD with GitHub Actions
Configure a continuous integration pipeline with GitHub Actions that runs linting, tests, and builds on every pull request, preventing broken code from merging into main.
Objective
Create a GitHub Actions workflow that lints, type-checks, and tests the codebase on every push and pull request, blocking merges when checks fail.
Environment
- GitHub repository with Actions enabled
- Node.js 20+ project with pnpm
- package.json with scripts: lint, test, build
Steps
1. Create workflow directory
Create .github/workflows/ directory in your repository root
Expected state: Directory .github/workflows/ exists (create with mkdir -p if needed)
2. Write CI workflow YAML
Create .github/workflows/ci.yml with configuration for Node.js setup, dependency installation, and test execution
Expected state: File .github/workflows/ci.yml contains valid YAML with push/PR triggers, Node 20 matrix, and lint/test/build steps
3. Configure branch protection rules
In GitHub repository settings → Branches → Add rule for 'main', enable 'Require status checks to pass' and select the CI workflow
Expected state: Branch protection rule active: PRs cannot merge until CI passes
4. Test the workflow
Push a commit that breaks a test to verify the workflow catches it
Expected state: GitHub Actions shows red X on the commit; PR cannot be merged
5. Test the passing case
Push a valid commit and verify all checks pass
Expected state: GitHub Actions shows green checkmark; PR merge is allowed
Decision points
Should tests run in parallel or sequential?
- Parallel (faster feedback)
- Sequential (lower resource usage)
Parallel — use a matrix strategy to run lint, test, and build in separate jobs for faster CI cycles.
Should the workflow use pnpm or npm?
- pnpm (faster, deterministic)
- npm (built-in, no setup)
Use pnpm if the project uses it. Add 'pnpm-action/setup' step. Otherwise default to npm ci.
Expected outcome
Every PR triggers lint + typecheck + test. Broken code is blocked from merging. Main branch stays green.
Verification
Open a PR with an intentional lint error. Verify GitHub Actions shows failure and the merge button is greyed out.
Cleanup
Remove the test branch. The CI workflow stays in place for all future PRs.
Why Use GitHub Actions for CI
CI (Continuous Integration) ensures every code change passes quality checks before merging. GitHub Actions runs workflows directly in your repository, free for public repos with generous limits for private ones.
The Workflow File
Create .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm test -- --run
build:
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
Key Concepts
Caching for Speed
The cache: 'pnpm' key tells setup-node to cache the ~/.pnpm-store directory between runs. First run: ~60s. Subsequent runs: ~10s.
Job Dependencies with needs
build:
needs: [lint, typecheck, test] # build only runs if all 3 pass
If lint or test fails, build is skipped. This saves minutes of compute.
Matrix Strategy for Multiple Node Versions
strategy:
matrix:
node-version: [18, 20, 22]
Runs the same job against each Node version in parallel.
Branch Protection Setup
- Go to: Repository → Settings → Branches → Add rule
- Branch name pattern:
main - Check “Require status checks to pass before merging”
- Search and select:
lint,typecheck,test,build - Save changes
Debugging Failed Workflows
- Click the failed job in the Actions tab to see the step-by-step log
- Use
continue-on-error: trueon non-critical steps during debugging - Add
ACTIONS_STEP_DEBUG: trueas a repository secret for verbose logs
Security Best Practices
- Never use
${{ secrets.* }}in echo statements (can leak via logs) - Pin action versions to SHA commit hashes for supply chain security
- Use
pull_request_targetinstead ofpull_requestonly if absolutely necessary (security risk)