CI/CD Pipelines with GitHub Actions for Node.js
Continuous Integration and Continuous Deployment (CI/CD) is no longer a luxury; it's a necessity. GitHub Actions has revolutionized this space by providing powerful, native automation directly within your repository. Let's look at a production-grade workflow for a Node.js project.
1. The Continuous Integration (CI) Workflow
The goal of CI is to prove that new code integrates cleanly with the main branch. We want to run
linting, type-checking (if using TypeScript), and unit tests on every Pull Request.
name: Node.js CI
on:
pull_request:
branches: [ "main" ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm' # CRITICAL: Caches ~/.npm to speed up npm ci
- name: Install dependencies
run: npm ci # Use ci instead of install for reproducible builds
- name: Run linter
run: npm run lint
- name: Run tests
run: npm test
Why npm ci?
Never use npm install in a CI pipeline. It can update your package-lock.json
unexpectedly. npm ci reads strictly from the lockfile, ensuring deterministic, reproducible
builds.
2. Database Services in CI
If your integration tests require a real database, you can spin up ephemeral Docker containers directly
within the GitHub Action runner using services.
jobs:
test-with-db:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: password
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
# ... steps ...
3. The Continuous Deployment (CD) Workflow
Once code is merged to main, we want to automatically deploy it. A common pattern is building a
Docker image and pushing it to a registry (like AWS ECR or GitHub Container Registry), then triggering a
server to pull the new image.
name: Deploy to Production
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest
needs: build-and-test # Ensure tests pass first!
steps:
- uses: actions/checkout@v4
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: myorg/myapp:latest,myorg/myapp:${{ github.sha }}
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PROD_SERVER_IP }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull myorg/myapp:latest
docker-compose -f docker-compose.prod.yml up -d
Conclusion
GitHub Actions removes the need to maintain separate Jenkins servers. By defining your CI/CD pipelines as code within your repository, leveraging caching, and utilizing built-in marketplace actions, you can ensure that your deployments are fast, automated, and strictly validated.