The complete workflow
Here's a production-ready GitHub Actions workflow that handles the full CI/CD pipeline for a Node.js app.
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
push: true
tags: registry.example.com/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to production
run: |
curl -X POST ${{ secrets.DEPLOY_WEBHOOK_URL }} -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" -d '{"image": "registry.example.com/myapp:${{ github.sha }}"}'Key elements explained
Matrix testing — Run tests against Node 20 and 22 simultaneously. Catch compatibility issues before they reach production.
cache: 'npm' — Caches the npm module cache between runs. Cuts install time from ~60s to ~5s on cache hit.
docker/build-push-action with GHA cache — Docker layer caching via GitHub Actions cache. Unchanged layers are reused. A change to src/ only rebuilds from the COPY layer, not from npm install.
needs: test — The deploy job only runs if the test job passes. Never deploy without green tests.
if: github.ref == 'refs/heads/main' — Only deploy on pushes to main. PRs run tests but don't deploy.
Managing secrets
Store all sensitive values in GitHub repository secrets (Settings → Secrets → Actions). Reference them with ${{ secrets.MY_SECRET }}. Never hardcode credentials in workflows.
For environment-specific secrets (staging vs production), use GitHub Environments with separate secret sets and required reviewer approvals for production.
Preview deployments for PRs
preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Deploy preview
id: deploy
run: |
URL=$(deploy-preview --branch ${{ github.head_ref }})
echo "url=$URL" >> $GITHUB_OUTPUT
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed to ${{ steps.deploy.outputs.url }}'
})Workflow runtime targets
A well-optimized CI pipeline should complete in under 5 minutes. If yours takes longer, investigate: npm install without caching, running tests serially instead of parallel, or building Docker images without layer caching are the usual culprits.