Skip to main content

Building a Complete CI/CD Pipeline with GitHub Actions

In this guide, we'll create a robust CI/CD pipeline that includes testing, building, and deployment. We'll use a Node.js project as an example, but the concepts apply to any language or framework.

Step 1: Create Project Structure

Before setting up GitHub Actions, organize your project with a clear structure. Here's a typical layout for a Node.js application:

nodejs-app/
├── .github/
│ └── workflows/
│ └── ci-cd.yml # Our main workflow file
├── src/
│ └── index.js # Application code
├── test/
│ └── index.test.js # Test files
├── package.json # Project dependencies
└── README.md # Project documentation

Step 2: Create Workflow File

Now create a workflow file that automates testing, building, and deployment. This workflow will run tests first, then build the application if tests pass, and finally deploy to production only when code is merged to the main branch.

Create a new file at .github/workflows/ci-cd.yml with the following configuration:

name: CI/CD Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch: # Allows manual triggering

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test
env:
NODE_ENV: test

build:
name: Build Artifacts
needs: test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'

- name: Install dependencies
run: npm ci

- name: Build application
run: npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: |
dist/
package.json
package-lock.json

deploy:
name: Deploy to Production
needs: build
if: github.ref == 'refs/heads/main' # Only run on main branch
runs-on: ubuntu-latest

steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-artifacts
path: ./dist

- name: Deploy to Production
run: | npm run deploy
echo "Deploying to production..."
# This could be deploying to Vercel, AWS, etc.

Step 3. Setting Up Environment Variables

For sensitive data like API keys or deployment tokens:

  • Go to your GitHub repository
  • Click on "Settings" > "Secrets and variables" > "Actions"
  • Click "New repository secret"
  • Add secrets like DEPLOY_TOKEN, AWS_ACCESS_KEY_ID, etc.

Use them in your workflow:

- name: Deploy to Production
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "Deploying with token: $DEPLOY_TOKEN"
# Add Your deployment commands here

Step 4. Add a Status Badge

Add this to your README.md to show the build status:

![CI/CD Status](https://github.com/username/repository-name/actions/workflows/ci-cd.yml/badge.svg)

Step 5. Advanced Features

Caching Dependencies

Speed up your builds by caching dependencies, add the following content to your yaml file:

- name: Cache node modules
uses: actions/cache@v3
id: cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

Matrix Testing

Matrix builds allow you to test your application across multiple operating systems, programming language versions, or configurations in parallel. This ensures compatibility across different environments.

This example tests on 3 operating systems and 3 Node.js versions (9 total combinations):

jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16.x, 18.x, 20.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

Scheduled Workflows

Scheduled workflows run automatically at specified times using cron syntax. Perfect for nightly builds, database backups, or periodic reports.

This example runs the workflow daily at midnight UTC:

on:
schedule:
# Run at 00:00 UTC every day
- cron: '0 0 * * *'

Tip: Use crontab.guru to generate and validate cron expressions.

Step 6: Real-World Example - Deploying to Vercel

Let's deploy a Next.js application to Vercel automatically when code is pushed to the main branch.

First, obtain your Vercel token from Vercel Account Settings and add it as a secret named VERCEL_TOKEN.

Add this deployment job to your workflow:

- name: Install Vercel CLI
run: npm install --global vercel@latest

- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}

- name: Deploy Project Artifacts to Vercel
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod

Security Best Practices

Security is critical when automating workflows. Follow these practices to protect your code, credentials, and deployments.

1. Protect Secrets and Credentials

Never hard-code sensitive information in your workflow files. Always use GitHub secrets.

Bad Practice:

- name: Deploy
env:
API_KEY: sk-1234567890abcdef # Never do this!
run: deploy-app

Good Practice:

- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
run: deploy-app

2. Limit Token Permissions

Use fine-grained permissions for the GITHUB_TOKEN to follow the principle of least privilege.

permissions:
contents: read # Read repository contents
pull-requests: write # Comment on pull requests
issues: write # Create/update issues

Avoid using:

permissions: write-all # Too permissive!