Skip to main content

GitHub Actions: CI/CD Workflow Configuration for JavaScript Applications

GitHub Actions automates your development workflow directly within your GitHub repository. Instead of relying on external services, you can build, test, and deploy your applications using workflows that trigger on code changes, pull requests, or scheduled intervals.

Understanding GitHub Actions: Complete Beginner's Guide

What you'll learn:

  • What GitHub Actions is and why developers use it
  • How GitHub Actions works (with simple analogies)
  • Core concepts: workflows, jobs, steps, and runners
  • How to create your first CI/CD workflow
  • Real-world examples for JavaScript applications

Prerequisites:

  • GitHub account and basic Git knowledge
  • Basic understanding of CI/CD concepts (see our CI/CD guide)
  • JavaScript/Node.js development experience

What is GitHub Actions? GitHub Actions is like having a personal assistant for your code that lives inside GitHub. Every time you push code, create a pull request, or perform other actions, this "assistant" can automatically run tasks like testing your code, building your application, or deploying it to a server.

Real-world analogy: Think of GitHub Actions like a smart home automation system. Just as you can set rules like "when someone rings the doorbell (trigger), turn on the porch light and send a notification (actions)," GitHub Actions lets you set rules like "when code is pushed (trigger), run tests and deploy if they pass (actions)."

Why use GitHub Actions instead of manual processes?

Manual process problems:

  • Forgetting to run tests before merging code
  • Inconsistent deployment steps between team members
  • Time-consuming repetitive tasks
  • Human errors in production deployments
  • No standardized process across projects

GitHub Actions solutions:

  • Automatic testing on every code change
  • Standardized, repeatable processes
  • Fast, reliable deployments
  • Built into GitHub (no external services needed)
  • Free for public repositories, affordable for private ones

Understanding GitHub Actions Fundamentals

Core Concepts: Breaking Down GitHub Actions

Understanding GitHub Actions requires learning four key concepts. Let's explore each one with simple explanations and examples:

1. Workflows: Your Automation Instructions

What are workflows? A workflow is like a recipe that tells GitHub Actions exactly what to do when certain events happen. Just as a cooking recipe has ingredients and step-by-step instructions, a workflow has triggers and a list of tasks to complete.

How workflows work:

  • Written in YAML format (a simple text format for configuration)
  • Stored in .github/workflows/ directory in your repository
  • Each workflow file represents one automated process
  • Can have multiple workflows for different purposes (testing, deployment, etc.)

Example workflow purposes:

  • "When someone creates a pull request, run all tests"
  • "When code is pushed to main branch, deploy to production"
  • "Every day at midnight, run security scans"

What are jobs? Jobs are like different workstations in a factory. Each job focuses on one main purpose and contains multiple related tasks. Jobs can run at the same time (parallel) or one after another (sequential).

How jobs work:

  • Each job runs on its own virtual machine (called a "runner")
  • Jobs run independently unless you specify dependencies
  • Common job types: "test", "build", "deploy"
  • You can have multiple jobs in one workflow

Example job structure:

jobs:
test:# Job name:
run tests
# ... test-related steps

build: # Job name: build application
needs: test # Wait for test job to complete first
# ... build-related steps

deploy: # Job name: deploy to server
needs: build # Wait for build job to complete first
# ... deployment steps

3. Steps: Individual Actions

What are steps? Steps are the smallest units of work—individual commands or actions that execute one at a time within a job. Think of them like individual instructions in a recipe: "preheat oven," "mix ingredients," "bake for 30 minutes."

Types of steps:

  • Run commands: Execute shell commands (like npm install, npm test)
  • Use actions: Use pre-built actions from the GitHub marketplace
  • Set up tools: Install Node.js, Python, or other development tools

Example steps in a test job:

steps:
- name: Get the code
uses: actions/checkout@v4 # Pre-built action to download your code

- name: Set up Node.js
uses: actions/setup-node@v4 # Pre-built action to install Node.js
with:
node-version: "18"

- name: Install dependencies
run: npm ci # Shell command to install packages

- name: Run tests
run: npm test # Shell command to run tests

4. Runners: Virtual Computers

What are runners? Runners are virtual computers (like cloud servers) that GitHub provides to execute your workflows. Think of them as temporary, clean computers that start fresh for each job.

Types of runners:

  • GitHub-hosted: Free virtual machines provided by GitHub
    • Ubuntu Linux (most common for web development)
    • Windows (for Windows-specific applications)
    • macOS (for iOS/Mac applications)
  • Self-hosted: Your own computers/servers (for special requirements)

What happens with runners:

  1. GitHub starts a fresh virtual machine
  2. Your job's steps execute on this machine
  3. When the job finishes, the virtual machine is destroyed
  4. Each job gets its own clean environment
  • Can run commands or use pre-built actions
  • Share the same virtual environment within a job

Actions: Reusable units of code

  • Created by GitHub community or custom-built
  • Simplify common tasks like Node.js setup, deployment

Workflow File Structure

name: Descriptive Workflow Name
on: [trigger events]
jobs:
job-name:
runs-on: runner-type
steps:
- name: Step description
uses: action-name@version
- name: Run commands
run: command-to-execute

Setting Up Continuous Integration (CI)

Basic CI Workflow for Node.js Applications

Create .github/workflows/ci.yml:

name: Continuous Integration

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18, 20]

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

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build application
run: npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-files-node${{ matrix.node-version }}
path: dist/

Key features:

  • Matrix strategy: Tests across multiple Node.js versions
  • Caching: Speeds up subsequent runs by caching node_modules
  • Artifact upload: Saves build output for later use or debugging

Advanced CI with Code Quality Checks

name: Comprehensive CI

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
quality:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Lint code
run: npm run lint

- name: Check formatting
run: npm run format:check

- name: Type checking
run: npm run type-check
if: hashFiles('tsconfig.json') != ''

- name: Security audit
run: npm audit --audit-level=moderate

- name: Run tests with coverage
run: npm run test:coverage

- name: Upload coverage reports
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
fail_ci_if_error: true

build:
needs: quality
runs-on: ubuntu-latest

strategy:
matrix:
build-env: [production, staging]

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build for ${{ matrix.build-env }}
run: npm run build:${{ matrix.build-env }}
env:
NODE_ENV: ${{ matrix.build-env }}

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.build-env }}-build
path: dist/
retention-days: 7

Package Manager Specific Configurations

For Yarn Projects

- name: Setup Node.js with Yarn
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "yarn"

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build project
run: yarn build

For pnpm Projects

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build project
run: pnpm build

Setting Up Continuous Deployment (CD)

Basic CD Workflow to VPS

Create .github/workflows/deploy.yml:

name: Deploy to Production

on:
push:
branches: [main]
workflow_dispatch: # Allow manual triggers

jobs:
deploy:
runs-on: ubuntu-latest

environment: production # Use GitHub environments for protection

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build for production
run: npm run build
env:
NODE_ENV: production

- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

- name: Deploy to server
run: |
scp -r ./dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/html/
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo systemctl reload nginx"

- name: Verify deployment
run: |
sleep 10
curl -f ${{ secrets.APP_URL }} || exit 1

Advanced CD with Blue-Green Deployment

name: Blue-Green Deployment

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest

environment: production

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install and build
run: |
npm ci
npm run build

- name: Deploy to staging slot
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Deploy to staging directory
scp -r ./dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/staging/

- name: Run health checks on staging
run: |
# Wait for deployment to settle
sleep 15

# Check staging endpoint
if curl -f ${{ secrets.STAGING_URL }}/health; then
echo "✅ Health check passed"
else
echo "❌ Health check failed"
exit 1
fi

- name: Switch to production
run: |
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} '
# Backup current production
sudo cp -r /var/www/html /var/www/backup-$(date +%Y%m%d-%H%M%S)

# Switch staging to production
sudo rm -rf /var/www/html/*
sudo cp -r /var/www/staging/* /var/www/html/

# Reload nginx
sudo systemctl reload nginx
'

- name: Verify production deployment
run: |
sleep 10
if curl -f ${{ secrets.APP_URL }}/health; then
echo "✅ Production deployment successful"
else
echo "❌ Production deployment failed, rolling back"
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} '
# Find latest backup
BACKUP=$(ls -t /var/www/backup-* | head -1)

# Restore backup
sudo rm -rf /var/www/html/*
sudo cp -r $BACKUP/* /var/www/html/
sudo systemctl reload nginx
'
exit 1
fi

Environment-Specific Workflows

Multi-Environment Deployment Pipeline

name: Multi-Environment Pipeline

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- run: npm ci
- run: npm test

deploy-staging:
needs: test
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging

steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Build for staging
run: |
npm ci
npm run build:staging
env:
VITE_API_URL: ${{ secrets.STAGING_API_URL }}
VITE_APP_ENV: staging

- name: Deploy to staging
run: |
# Deploy to staging server
echo "Deploying to staging environment"
# ... deployment steps

deploy-production:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production

steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Build for production
run: |
npm ci
npm run build:production
env:
VITE_API_URL: ${{ secrets.PRODUCTION_API_URL }}
VITE_APP_ENV: production

- name: Deploy to production
run: |
# Deploy to production server
echo "Deploying to production environment"
# ... deployment steps

Required GitHub Secrets Configuration

Basic Secrets Setup

Navigate to Repository SettingsSecrets and variablesActions

Required secrets for VPS deployment:

Secret NameDescriptionExample Value
SSH_PRIVATE_KEYPrivate SSH key content-----BEGIN RSA PRIVATE KEY-----\n...
SSH_USERSSH username for serverubuntu, deploy, root
SSH_HOSTServer IP address or hostname192.168.1.100, server.example.com
APP_URLApplication URL for health checkshttps://myapp.com, http://192.168.1.100

Environment-specific secrets:

Secret NameDescriptionEnvironment
STAGING_API_URLAPI endpoint for stagingStaging
PRODUCTION_API_URLAPI endpoint for productionProduction
STAGING_URLStaging application URLStaging
DATABASE_URLDatabase connection stringProduction

Generating SSH Keys

What are SSH Keys? SSH keys are like a digital lock and key system. Instead of using passwords, your computer uses a "private key" (kept secret) and a "public key" (shared with the server) to authenticate securely. This is much more secure than passwords.

On your local machine:

# Generate SSH key pair
# -t rsa = use RSA encryption
# -b 4096 = use 4096-bit key (very secure)
# -C = comment to identify this key
# -f = filename to save the key
ssh-keygen -t rsa -b 4096 -C "github-actions@yourapp.com" -f ~/.ssh/github_actions

# Copy public key to server
ssh-copy-id -i ~/.ssh/github_actions.pub user@your-server.com

# Display private key for GitHub secret
cat ~/.ssh/github_actions

Adding to GitHub:

  1. Copy the entire private key content (including -----BEGIN and -----END lines)
  2. Go to repository SettingsSecrets and variablesActions
  3. Click New repository secret
  4. Name: SSH_PRIVATE_KEY
  5. Value: Paste the private key content

Framework-Specific Configurations

React with Vite

name: React Vite CI/CD

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Run linting
run: npm run lint

- name: Run tests
run: npm run test

- name: Build application
run: npm run build
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
VITE_APP_TITLE: ${{ secrets.VITE_APP_TITLE }}

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

deploy:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest

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

- name: Deploy to server
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

scp -r ./dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/html/
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo systemctl reload nginx"

Vue.js with Nuxt

name: Nuxt.js Deployment

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build Nuxt application
run: npm run build
env:
NITRO_PRESET: node-server
NUXT_API_BASE_URL: ${{ secrets.API_BASE_URL }}

- name: Create deployment package
run: |
mkdir deployment
cp -r .output deployment/
cp package.json deployment/
cp ecosystem.config.js deployment/

- name: Deploy to server
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Upload deployment package
scp -r deployment/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/nuxt-app/

# Restart application with PM2
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
cd /var/www/nuxt-app
pm2 reload ecosystem.config.js --env production
"

Workflow Optimization Strategies

Caching Dependencies

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

- name: Cache build output
uses: actions/cache@v3
with:
path: dist
key: ${{ runner.os }}-build-${{ github.sha }}

Conditional Execution

- name: Run tests
run: npm test
if: hashFiles('**/*.test.js') != ''

- name: Type checking
run: npm run type-check
if: hashFiles('tsconfig.json') != ''

- name: Deploy only on main branch
run: npm run deploy
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

Matrix Builds for Multiple Configurations

strategy:
matrix:
node-version: [18, 20]
build-target: [production, staging]
exclude:
- node-version: 18
build-target: staging

steps:
- name: Build for ${{ matrix.build-target }}
run: npm run build:${{ matrix.build-target }}

Multi-Application Workflows

Smart Path Detection for Multiple Apps

When you have multiple applications in one repository, you want to deploy only the changed ones. Think of it like a smart home system that only turns on lights in rooms where motion is detected - GitHub Actions can detect which applications changed and deploy only those.

Setting Up Path-Based Detection

First, install the path-filter action to detect changes:

name: Multi-App Deployment

on:
push:
branches: [main]

jobs:
changes:
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
admin: ${{ steps.changes.outputs.admin }}
api: ${{ steps.changes.outputs.api }}
steps:
- uses: actions/checkout@v3

# This is like having security cameras for each room
# It watches for changes and reports which "rooms" (apps) have activity
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'apps/frontend/**'
- 'shared/components/**'
admin:
- 'apps/admin/**'
- 'shared/utils/**'
api:
- 'apps/api/**'
- 'shared/types/**'

Complete Multi-App Workflow

name: Multi-Application CI/CD

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
# Step 1: Detect which applications changed
detect-changes:
name: Detect Changed Applications
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
admin: ${{ steps.changes.outputs.admin }}
dashboard: ${{ steps.changes.outputs.dashboard }}
api: ${{ steps.changes.outputs.api }}
tools: ${{ steps.changes.outputs.tools }}
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Detect changes
uses: dorny/paths-filter@v2
id: changes
with:
# Think of this as a motion detector for each room in your house
# Each filter watches specific directories for changes
filters: |
frontend:
- 'apps/frontend/**'
- 'packages/shared-ui/**'
- 'packages/api-client/**'
admin:
- 'apps/admin/**'
- 'packages/admin-components/**'
- 'packages/shared-utils/**'
dashboard:
- 'apps/dashboard/**'
- 'packages/charts/**'
- 'packages/data-utils/**'
api:
- 'apps/api/**'
- 'packages/database/**'
- 'packages/auth/**'
tools:
- 'apps/tools/**'
- 'packages/cli/**'

# Step 2: Build and test only changed applications
build-frontend:
name: Build Frontend Application
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.frontend == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
cache-dependency-path: apps/frontend/package-lock.json

- name: Install dependencies
run: |
cd apps/frontend
npm ci

- name: Run tests
run: |
cd apps/frontend
npm test -- --coverage --watchAll=false

- name: Build application
run: |
cd apps/frontend
npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: frontend-build
path: apps/frontend/dist

build-admin:
name: Build Admin Panel
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.admin == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
cache-dependency-path: apps/admin/package-lock.json

- name: Install dependencies
run: |
cd apps/admin
npm ci

- name: Build admin panel
run: |
cd apps/admin
npm run build

- name: Upload admin build
uses: actions/upload-artifact@v3
with:
name: admin-build
path: apps/admin/build

build-api:
name: Build API Server
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.api == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
cache-dependency-path: apps/api/package-lock.json

- name: Install dependencies
run: |
cd apps/api
npm ci

- name: Run API tests
run: |
cd apps/api
npm test

- name: Build API
run: |
cd apps/api
npm run build

# Step 3: Deploy only changed applications
deploy-frontend:
name: Deploy Frontend
runs-on: ubuntu-latest
needs: [detect-changes, build-frontend]
if: needs.detect-changes.outputs.frontend == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Download frontend build
uses: actions/download-artifact@v3
with:
name: frontend-build
path: ./frontend-build

- name: Deploy to VPS
run: |
# Setup SSH
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Deploy frontend (path-based: example.com/)
scp -r frontend-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/html/

# Reload nginx if needed
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo nginx -s reload"

deploy-admin:
name: Deploy Admin Panel
runs-on: ubuntu-latest
needs: [detect-changes, build-admin]
if: needs.detect-changes.outputs.admin == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Download admin build
uses: actions/download-artifact@v3
with:
name: admin-build
path: ./admin-build

- name: Deploy Admin Panel
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Deploy admin panel (path-based: example.com/admin/)
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "mkdir -p /var/www/html/admin"
scp -r admin-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/html/admin/

deploy-api:
name: Deploy API Server
runs-on: ubuntu-latest
needs: [detect-changes, build-api]
if: needs.detect-changes.outputs.api == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Deploy API Server
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Upload API code
rsync -avz --delete apps/api/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/api/

# Restart API with PM2
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
cd /var/www/api
npm ci --production
pm2 reload ecosystem.config.js --env production
"

Matrix Strategy for Multiple Environments

name: Multi-Environment Deployment

on:
push:
branches: [main, staging, develop]

jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- environment: production
branch: main
host: ${{ secrets.PROD_SSH_HOST }}
user: ${{ secrets.PROD_SSH_USER }}
key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}

- environment: staging
branch: staging
host: ${{ secrets.STAGING_SSH_HOST }}
user: ${{ secrets.STAGING_SSH_USER }}
key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}

- environment: development
branch: develop
host: ${{ secrets.DEV_SSH_HOST }}
user: ${{ secrets.DEV_SSH_USER }}
key: ${{ secrets.DEV_SSH_PRIVATE_KEY }}

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

- name: Deploy to ${{ matrix.environment }}
if: github.ref == format('refs/heads/{0}', matrix.branch)
run: |
echo "Deploying to ${{ matrix.environment }} environment"
# Your deployment commands here using matrix variables

Monitoring and Notifications

Slack Notifications

- name: Notify Slack on success
if: success()
uses: 8398a7/action-slack@v3
with:
status: success
text: "✅ Deployment successful!"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

- name: Notify Slack on failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "❌ Deployment failed!"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Email Notifications

- name: Send email notification
if: always()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 587
username: ${{ secrets.EMAIL_USERNAME }}
password: ${{ secrets.EMAIL_PASSWORD }}
subject: "Deployment Status: ${{ job.status }}"
body: |
Deployment for ${{ github.repository }} has ${{ job.status }}.

Commit: ${{ github.sha }}
Branch: ${{ github.ref }}
Author: ${{ github.actor }}
to: team@yourcompany.com

Adding New Applications to Multi-App Workflows

Step-by-Step: Adding a New App to Existing Setup

When you need to add a new application to your existing multi-app deployment, follow this systematic approach. Think of it like adding a new restaurant to your food court—you need to update the directory, add signage, and train staff on the new location.

1. Update Path Detection Configuration

First, add your new application to the paths-filter configuration:

# In your existing workflow file (.github/workflows/multi-app-deploy.yml)
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'apps/frontend/**'
- 'packages/shared-ui/**'
admin:
- 'apps/admin/**'
- 'packages/admin-components/**'
dashboard:
- 'apps/dashboard/**'
- 'packages/charts/**'
# NEW APPLICATION: Add your new app here
support-portal:
- 'apps/support-portal/**'
- 'packages/support-components/**'
client-portal:
- 'apps/client-portal/**'
- 'packages/client-utils/**'

2. Add Build Job for New Application

Create a build job specifically for your new application:

# Add this job to your existing workflow
build-support-portal:
name: Build Support Portal
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.support-portal == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
cache-dependency-path: apps/support-portal/package-lock.json

- name: Install dependencies
run: |
cd apps/support-portal
npm ci

- name: Run tests
run: |
cd apps/support-portal
npm test -- --coverage --watchAll=false

- name: Build application
run: |
cd apps/support-portal
# Configure build for your deployment strategy
npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: support-portal-build
path: apps/support-portal/dist

build-client-portal:
name: Build Client Portal
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.client-portal == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
cache-dependency-path: apps/client-portal/package-lock.json

- name: Install dependencies
run: |
cd apps/client-portal
npm ci

- name: Build application
run: |
cd apps/client-portal
npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: client-portal-build
path: apps/client-portal/dist

3. Add Deployment Job for New Application

For Port-Based Strategy (e.g., port 9000):

deploy-support-portal:
name: Deploy Support Portal
runs-on: ubuntu-latest
needs: [detect-changes, build-support-portal]
if: needs.detect-changes.outputs.support-portal == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Download support portal build
uses: actions/download-artifact@v3
with:
name: support-portal-build
path: ./support-portal-build

- name: Deploy Support Portal (Port-based)
run: |
# SSH setup
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Deploy to specific directory (port 9000 in Nginx config)
scp -r support-portal-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/support-portal/

# Reload nginx to apply any new configurations
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo systemctl reload nginx"

For Subdomain Strategy (support.yourdomain.com):

deploy-support-portal:
name: Deploy Support Portal
runs-on: ubuntu-latest
needs: [detect-changes, build-support-portal]
if: needs.detect-changes.outputs.support-portal == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Download support portal build
uses: actions/download-artifact@v3
with:
name: support-portal-build
path: ./support-portal-build

- name: Deploy Support Portal (Subdomain)
run: |
# SSH setup
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Deploy to subdomain directory
scp -r support-portal-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/support-portal/

# The subdomain Nginx config should already exist and point to this directory
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo systemctl reload nginx"

For Path-Based Strategy (yourdomain.com/support/):

deploy-support-portal:
name: Deploy Support Portal
runs-on: ubuntu-latest
needs: [detect-changes, build-support-portal]
if: needs.detect-changes.outputs.support-portal == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Download support portal build
uses: actions/download-artifact@v3
with:
name: support-portal-build
path: ./support-portal-build

- name: Deploy Support Portal (Path-based)
run: |
# SSH setup
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Create directory for path-based routing
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "mkdir -p /var/www/support-portal"

# Deploy to path-based directory
scp -r support-portal-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/support-portal/

# Reload nginx (path should be configured in main nginx config)
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo systemctl reload nginx"

4. Update detect-changes Job Outputs

Don't forget to add outputs for your new applications:

detect-changes:
name: Detect Changed Applications
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
admin: ${{ steps.changes.outputs.admin }}
dashboard: ${{ steps.changes.outputs.dashboard }}
api: ${{ steps.changes.outputs.api }}
tools: ${{ steps.changes.outputs.tools }}
# ADD YOUR NEW APPS HERE
support-portal: ${{ steps.changes.outputs.support-portal }}
client-portal: ${{ steps.changes.outputs.client-portal }}
steps:
# ... existing steps

5. Add Health Checks for New Applications

Include health checks for your new applications:

- name: Health Check New Applications
run: |
# Test existing applications
curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}:80 > /dev/null
curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}:8080 > /dev/null

# ADD HEALTH CHECKS FOR NEW APPS
# For port-based
curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}:9000 > /dev/null # Support portal
curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}:9001 > /dev/null # Client portal

# For subdomain-based (replace with your domain)
# curl -f -s --max-time 30 https://support.yourdomain.com > /dev/null
# curl -f -s --max-time 30 https://client.yourdomain.com > /dev/null

# For path-based
# curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}/support/ > /dev/null
# curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}/client/ > /dev/null

echo "✅ All applications health checks passed!"

Complete Example: Adding a "Client Portal" Application

Here's a complete example of adding a client portal to an existing multi-app setup:

name: Multi-Application CI/CD with Client Portal

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
detect-changes:
name: Detect Changed Applications
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
admin: ${{ steps.changes.outputs.admin }}
api: ${{ steps.changes.outputs.api }}
client-portal: ${{ steps.changes.outputs.client-portal }} # NEW
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Detect changes
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
frontend:
- 'apps/frontend/**'
admin:
- 'apps/admin/**'
api:
- 'apps/api/**'
client-portal: # NEW APPLICATION
- 'apps/client-portal/**'
- 'packages/client-utils/**'

# Existing build jobs...

build-client-portal: # NEW BUILD JOB
name: Build Client Portal
runs-on: ubuntu-latest
needs: detect-changes
if: needs.detect-changes.outputs.client-portal == 'true'
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
cache: "npm"
cache-dependency-path: apps/client-portal/package-lock.json

- name: Install dependencies
run: |
cd apps/client-portal
npm ci

- name: Run tests
run: |
cd apps/client-portal
npm test -- --coverage --watchAll=false

- name: Build application
run: |
cd apps/client-portal
npm run build

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: client-portal-build
path: apps/client-portal/dist

# Existing deployment jobs...

deploy-client-portal: # NEW DEPLOYMENT JOB
name: Deploy Client Portal
runs-on: ubuntu-latest
needs: [detect-changes, build-client-portal]
if: needs.detect-changes.outputs.client-portal == 'true' && github.ref == 'refs/heads/main'
steps:
- name: Download client portal build
uses: actions/download-artifact@v3
with:
name: client-portal-build
path: ./client-portal-build

- name: Deploy Client Portal
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

# Deploy to port 9001 (configure Nginx accordingly)
scp -r client-portal-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/client-portal/

# Reload nginx
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "sudo systemctl reload nginx"

- name: Health Check Client Portal
run: |
sleep 30 # Wait for deployment to complete
curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}:9001
echo "✅ Client Portal deployed successfully!"

Managing Secrets for Multiple Applications

When adding new applications, you might need additional secrets:

Repository Secrets Structure:

# Shared secrets (used by all apps)
SSH_PRIVATE_KEY
SSH_USER
SSH_HOST

# Application-specific secrets (if needed)
CLIENT_PORTAL_API_KEY
SUPPORT_PORTAL_DATABASE_URL
ADMIN_PANEL_SECRET_KEY

# Environment-specific secrets
PROD_SSH_HOST
STAGING_SSH_HOST
DEV_SSH_HOST

Using Secrets in New Application Deployment:

- name: Deploy with App-Specific Configuration
run: |
# Deploy files
scp -r client-portal-build/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/client-portal/

# Create environment configuration
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
echo 'REACT_APP_API_KEY=${{ secrets.CLIENT_PORTAL_API_KEY }}' > /var/www/client-portal/.env
echo 'REACT_APP_API_URL=${{ secrets.CLIENT_PORTAL_API_URL }}' >> /var/www/client-portal/.env
"

This systematic approach ensures that adding new applications to your multi-app setup is consistent, maintainable, and doesn't break existing functionality.

Health Checks and Deployment Verification

Post-Deployment Health Checks

After deploying applications, it's crucial to verify they're working correctly. Think of this like a quality inspector checking products as they come off an assembly line—you want to catch issues before users do.

Basic Health Check Implementation

- name: Health Check After Deployment
run: |
# Wait for application to start
sleep 30

# Test application endpoints
curl -f -s --retry 3 --retry-delay 10 http://${{ secrets.SSH_HOST }}/health

# Test specific application routes
curl -f -s http://${{ secrets.SSH_HOST }}/ | grep "Welcome"
curl -f -s http://${{ secrets.SSH_HOST }}:8080/admin | grep "Admin"

echo "✅ All health checks passed!"

Advanced Multi-Application Health Monitoring

name: Comprehensive Health Check

on:
schedule:
- cron: "*/5 * * * *" # Every 5 minutes
workflow_dispatch:

jobs:
health-check:
runs-on: ubuntu-latest
steps:
- name: Multi-App Health Check
run: |
# Define applications to check
declare -A apps=(
["Main Site"]="http://${{ secrets.SSH_HOST }}:80"
["Admin Panel"]="http://${{ secrets.SSH_HOST }}:8080"
["Dashboard"]="http://${{ secrets.SSH_HOST }}:3000"
["Tools"]="http://${{ secrets.SSH_HOST }}:8090"
)

failed_checks=0

echo "🔍 Starting health check at $(date)"
echo "================================"

for app_name in "${!apps[@]}"; do
url="${apps[$app_name]}"
echo "Checking: $app_name at $url"

if curl -f -s --max-time 30 "$url" > /dev/null; then
echo "✅ $app_name - Healthy"
else
echo "❌ $app_name - Failed"
failed_checks=$((failed_checks + 1))
fi
done

if [ $failed_checks -gt 0 ]; then
echo "💥 $failed_checks application(s) failed health checks"
exit 1
else
echo "🎉 All applications are healthy!"
fi

- name: Notify Team on Failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: failure
text: "🚨 Health check failed! One or more applications are down."
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Database and API Health Checks

- name: Database Health Check
run: |
# Check database connectivity
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
# Test database connection
mysqladmin ping -h localhost -u app_user -p'${{ secrets.DB_PASSWORD }}'

# Check API endpoints
curl -f -s http://localhost:3001/api/health | jq '.status'

# Verify critical API endpoints
curl -f -s -H 'Authorization: Bearer test-token' http://localhost:3001/api/users/count
"

- name: Performance Health Check
run: |
# Check response times
response_time=$(curl -o /dev/null -s -w '%{time_total}\n' http://${{ secrets.SSH_HOST }})

if (( $(echo "$response_time > 5.0" | bc -l) )); then
echo "⚠️ Slow response time: ${response_time}s"
exit 1
else
echo "✅ Response time OK: ${response_time}s"
fi

Rollback Strategies

Automatic Rollback on Failed Health Checks

name: Deploy with Rollback

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Backup Current Version
run: |
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
# Create backup of current deployment
sudo cp -r /var/www/my-app /var/www/my-app-backup-$(date +%Y%m%d-%H%M%S)

# Keep only last 5 backups
cd /var/www && sudo ls -t my-app-backup-* | tail -n +6 | xargs -r sudo rm -rf
"

- name: Deploy New Version
run: |
# Deploy new version
scp -r dist/* ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/my-app/

- name: Health Check New Deployment
id: health_check
run: |
sleep 30 # Wait for application to start

# Test critical endpoints
if curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}/ > /dev/null && \
curl -f -s --max-time 30 http://${{ secrets.SSH_HOST }}/api/status > /dev/null; then
echo "✅ Health check passed"
echo "success=true" >> $GITHUB_OUTPUT
else
echo "❌ Health check failed"
echo "success=false" >> $GITHUB_OUTPUT
fi

- name: Rollback on Failure
if: steps.health_check.outputs.success != 'true'
run: |
echo "🔄 Rolling back due to failed health checks..."

ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
# Find most recent backup
latest_backup=\$(ls -t /var/www/my-app-backup-* | head -n1)

# Restore from backup
sudo rm -rf /var/www/my-app
sudo cp -r \$latest_backup /var/www/my-app

# Reload nginx
sudo systemctl reload nginx

echo 'Rollback completed to: '\$latest_backup
"

# Notify team of rollback
curl -X POST -H 'Content-Type: application/json' \
-d '{"text":"🔄 Deployment rolled back due to health check failures"}' \
${{ secrets.SLACK_WEBHOOK }}

exit 1

- name: Cleanup Old Deployment on Success
if: steps.health_check.outputs.success == 'true'
run: |
echo "✅ Deployment successful, cleaning up old backups..."
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
# Keep only the 3 most recent backups
cd /var/www && sudo ls -t my-app-backup-* | tail -n +4 | xargs -r sudo rm -rf
"

Deployment Monitoring and Analytics

Track Deployment Metrics

- name: Record Deployment Metrics
run: |
# Record deployment start time
echo "DEPLOY_START=$(date +%s)" >> $GITHUB_ENV

# Log deployment details
echo "📊 Deployment Metrics"
echo "Repository: ${{ github.repository }}"
echo "Commit: ${{ github.sha }}"
echo "Author: ${{ github.actor }}"
echo "Branch: ${{ github.ref_name }}"
echo "Workflow: ${{ github.workflow }}"

- name: Calculate Deployment Duration
if: always()
run: |
deploy_end=$(date +%s)
duration=$((deploy_end - DEPLOY_START))

echo "⏱️ Deployment took: ${duration} seconds"

# Send metrics to monitoring system (example)
curl -X POST "https://your-monitoring-api.com/metrics" \
-H "Content-Type: application/json" \
-d "{
\"metric\": \"deployment_duration\",
\"value\": $duration,
\"tags\": {
\"repository\": \"${{ github.repository }}\",
\"branch\": \"${{ github.ref_name }}\",
\"status\": \"${{ job.status }}\"
}
}"

Deployment Status Dashboard

- name: Update Deployment Status
if: always()
run: |
# Prepare status data
if [ "${{ job.status }}" == "success" ]; then
status="✅ SUCCESS"
color="good"
else
status="❌ FAILED"
color="danger"
fi

# Send to Slack with rich formatting
curl -X POST -H 'Content-Type: application/json' \
-d "{
\"attachments\": [{
\"color\": \"$color\",
\"title\": \"Deployment $status\",
\"fields\": [
{\"title\": \"Repository\", \"value\": \"${{ github.repository }}\", \"short\": true},
{\"title\": \"Branch\", \"value\": \"${{ github.ref_name }}\", \"short\": true},
{\"title\": \"Commit\", \"value\": \"${{ github.sha }}\", \"short\": true},
{\"title\": \"Author\", \"value\": \"${{ github.actor }}\", \"short\": true}
],
\"footer\": \"GitHub Actions\",
\"ts\": $(date +%s)
}]
}" \
${{ secrets.SLACK_WEBHOOK }}

- name: Update GitHub Deployment Status
uses: bobheadxi/deployments@v1
if: always()
with:
step: finish
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env_url: "http://${{ secrets.SSH_HOST }}"

Best Practices

Security

  • Never commit secrets to repository
  • Use environment protection rules for production deployments
  • Rotate SSH keys regularly
  • Limit secret access to necessary workflows only
  • Use least privilege principle for server access

Performance

  • Cache dependencies to speed up builds
  • Use matrix builds judiciously to avoid resource waste
  • Optimize Docker layers if using containers
  • Parallel job execution where possible

Reliability

  • Include health checks after deployment
  • Implement rollback strategies for failed deployments
  • Use environment-specific configurations
  • Test workflows with workflow_dispatch trigger
  • Monitor workflow run times and optimize slow steps

Maintenance

  • Keep actions up to date (use Dependabot)
  • Document workflow purpose in README
  • Use descriptive job and step names
  • Regular workflow review and optimization

GitHub Actions provides powerful automation capabilities that scale from simple CI checks to complex deployment pipelines. Start with basic workflows and gradually add sophistication as your needs grow and your confidence with the platform increases.