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"
2. Jobs: Groups of Related Tasks
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:
- GitHub starts a fresh virtual machine
- Your job's steps execute on this machine
- When the job finishes, the virtual machine is destroyed
- 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 Settings → Secrets and variables → Actions
Required secrets for VPS deployment:
Secret Name | Description | Example Value |
---|---|---|
SSH_PRIVATE_KEY | Private SSH key content | -----BEGIN RSA PRIVATE KEY-----\n... |
SSH_USER | SSH username for server | ubuntu , deploy , root |
SSH_HOST | Server IP address or hostname | 192.168.1.100 , server.example.com |
APP_URL | Application URL for health checks | https://myapp.com , http://192.168.1.100 |
Environment-specific secrets:
Secret Name | Description | Environment |
---|---|---|
STAGING_API_URL | API endpoint for staging | Staging |
PRODUCTION_API_URL | API endpoint for production | Production |
STAGING_URL | Staging application URL | Staging |
DATABASE_URL | Database connection string | Production |
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:
- Copy the entire private key content (including
-----BEGIN
and-----END
lines) - Go to repository Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
SSH_PRIVATE_KEY
- 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.