all posts
2026-01-22·8 min read

GitHub: CI/CD Deployment Workflow for minimising downtime

APICi/CDGitHubWP Engine

This document outlines the CI/CD pipeline established for managing the WP Engine sites. The workflow allows developers to work locally in VS Code, push changes to GitHub to automatically update the Staging environment, and manually trigger a flexible “Copy” process to copy changes to Production using the WP Engine API.

In this use case, due to the sensitivity of the production site and the need to minimise impact, we can’t push directly from GitHub to the production environment.

1. Workflow Overview

The process is split into two distinct workflows:

  1. Automated Staging Deployment: Code pushed to the main branch is immediately deployed to the Staging environment (example: apitestcopystg) via Git.
  2. Production Copy (API Copy): A manual “Click-to-Run” action in GitHub that triggers the WP Engine API to copy the Staging environment to Production. This allows granular control over files, databases, and specific tables.

Visual Flow: Development to Staging (Automated)

Visual Flow: Copy to Production (Manual API)


2. Zero-Downtime Copy Configuration (Feature Flag)

Important: To minimise downtime during the copy from Staging to Production, WP Engine can enable a specific feature flag.

⚠️ Critical Caveat: Data Loss Risk. While this feature keeps the Production site online during the copy, it introduces a risk of data loss.

  • The Risk: If any data is written to the Production site (e.g., new orders, comments, user registrations) while the copy is in progress, that data will be lost at the end of the copy because it gets overwritten by the source data from Staging.
  • Recommendation: It is highly advised to place the Production site into Content Freeze state during the deployment to prevent data writes.

Enabling the Feature Flag: The feature, called Deploy Directory, copies the site to a separate directory and then swaps that directory with the current production one once the copy is complete. This feature is disabled by default. To enable it, the WP Engine team must turn it on for you

Please confirm with WP Engine support that this flag has been enabled for your specific install if you intend to use it.


3. Prerequisites & Configuration

This section covers the setup required for Secrets, SSH keys, and Git configuration for Smart Plugin Manager (SPM).

A. Secrets Setup

Configure the following in GitHub under Settings > Secrets and variables > Actions.

Secret NameDescription
WPE_SSHG_KEY_PRIVATESSH Private Key for Staging git push.
WPE_API_USER_IDWP Engine API User ID (UUID).
WPE_API_PASSWORDWP Engine API Password.
WPE_SOURCE_ENV_IDUUID of the Staging environment.
WPE_DEST_ENV_IDUUID of the Production environment.
NOTIFICATION_EMAILDefault email(s) for completion alerts.

B. SSH Key Generation Guide

To allow GitHub Actions to push code to WP Engine, you must generate an SSH key pair.

  1. Generate Keys Locally:

Open your terminal (Mac/Linux) or Git Bash (Windows) and run:

ssh-keygen -t rsa -b 4096 -m PEM -f wpengine_key -C "git-deploy"

  • Press Enter to accept defaults (leave passphrase empty).
  • This creates two files: wpengine_key (Private) and wpengine_key.pub (Public).
  1. Add Public Key to WP Engine:
    1. Log in to the User Portal (my.wpengine.com).
    2. Click on your Name (top right) > Profile > SSH Keys.
    3. Click Add SSH Key.
    4. Open wpengine_key.pub on your computer, copy the contents, and paste it into the “Public Key” field.
    5. Click Add SSH Key.
  2. Add Private Key to GitHub:
    1. Open the wpengine_key file (the one without an extension) and copy the entire block including -----BEGIN RSA PRIVATE KEY-----.
    2. Go to your GitHub Repo > Settings > Secrets and variables > Actions.
    3. Create a New Repository Secret.
    4. Name: WPE_SSHG_KEY_PRIVATE.
    5. Value: Paste the private key content.

Read More: WP Engine SSH Key Documentation

C. Smart Plugin Manager & .gitignore

Smart Plugin Manager (SPM) Setup: WP Engine’s Smart Plugin Manager (SPM) handles plugin updates automatically. Therefore, you must not track or push plugin files via Git. If you push outdated plugin files from the repository, they will overwrite the updated versions on the server, breaking the SPM workflow.

Ensure your root .gitignore file includes the following rules to exclude plugins while keeping themes and custom code:

# WordPress defaults
wp-config.php
wp-content/uploads/
wp-content/cache/

# Ignore ALL plugins (Managed by SPM)
wp-content/plugins/

# Optional: Whitelist a specific custom plugin if you are developing one
# !wp-content/plugins/my-custom-plugin/

Read More: Using Git with WP Engine


4. Workflow Details

Part A: Development & Staging (Automated)

  • Trigger: git push to main branch.
  • Action: Deploys code changes to the Staging environment.
  • Mechanism: Uses the official wpengine/github-action-wpe-site-deploy to sync the repository content to the WP Engine git endpoint.

Developer Process:

  1. Developer makes changes locally in VS Code.
  2. Developer commits changes: git commit -m "Updated header styles".
  3. Developer pushes to GitHub: git push origin main.
  4. Result: GitHub Actions automatically syncs these files to the Staging site. The developer can view the changes on the Staging URL immediately after the action completes.

File 1: .github/workflows/deploy-staging.yml

Handles the automatic code deployment to Staging.

name: Deploy to Staging (apitestcopystg)
on:
  push:
    branches:
      - main # Adjust if your branch is named 'master'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0 # Required for WPE deploy action to work correctly

      - name: Deploy to WP Engine Staging
        uses: wpengine/github-action-wpe-site-deploy@v3
        with:
          # The name of the WP Engine environment (e.g., apitestcopystg)
          WPE_ENV: apitestcopystg
          # The SSH Private Key stored in GitHub Secrets
          WPE_SSHG_KEY_PRIVATE: ${{ secrets.WPE_SSHG_KEY_PRIVATE }}

Part B: Production Promotion (Manual API Copy)

  • Trigger: Manual workflow_dispatch button in GitHub Actions tab.
  • Action: Triggers the WP Engine install_copy API endpoint.
  • Mechanism: Sends a JSON payload to WP Engine API command instructing it to copy data from Staging (Source) to Production (Destination).

Capabilities: This workflow is flexible. You can choose to copy the entire environment or specific components.

Available Options (Inputs):

  1. Copy File System?(Checkbox)
    • If checked: Overwrites production files with staging files.
    • If unchecked: Files are ignored.
  2. Copy Database?(Checkbox)
    • If checked: Copies the database.
    • If unchecked: Database is ignored.
  3. Specific DB Tables(Text Field)
    • Leave Empty: Copies the entire database (if “Copy Database” is checked).
    • Specific Tables: Enter a comma-separated list (e.g., wp_posts, wp_options) to copy only those tables.
  4. Notification Emails(Text Field)
    • Leave Empty: Uses the default email stored in NOTIFICATION_EMAIL secret.
    • Enter Emails: Enter comma-separated emails to override the default (e.g., dev@agency.com, client@company.com).

File 2: .github/workflows/copy-to-prod.yml

Handles the manual API copy request to Production.

name: Copy Staging to Production (Copy)
on:
  workflow_dispatch:
    inputs:
      include_files:
        description: 'Copy File System?'
        required: true
        default: true
        type: boolean
      include_db:
        description: 'Copy Database?'
        required: true
        default: true
        type: boolean
      specific_tables:
        description: 'Specific DB Tables (comma-separated, leave empty for all)'
        required: false
        type: string
      notification_emails:
        description: 'Emails (leave empty to use default from Secrets)'
        required: false
        default: ''
        type: string

jobs:
  trigger-copy:
    runs-on: ubuntu-latest
    steps:
      - name: Call WP Engine Copy API
        env:
          WPE_USER: ${{ secrets.WPE_API_USER_ID }}
          WPE_PASS: ${{ secrets.WPE_API_PASSWORD }}
          # Pulling Environment IDs from Secrets
          SOURCE_ID: ${{ secrets.WPE_SOURCE_ENV_ID }}
          DEST_ID: ${{ secrets.WPE_DEST_ENV_ID }}
          # Pulling Default Email from Secrets
          DEFAULT_EMAILS: ${{ secrets.NOTIFICATION_EMAIL }}
          # Inputs from the UI
          INPUT_EMAILS: ${{ inputs.notification_emails }}
          INPUT_FILES: ${{ inputs.include_files }}
          INPUT_DB: ${{ inputs.include_db }}
          INPUT_TABLES: ${{ inputs.specific_tables }}
        run: |
          echo "Preparing WP Engine Copy..."

          # 1. Determine Email List
          # If input is empty, use the secret. Otherwise, use the input.
          if [[ -z "${INPUT_EMAILS// }" ]]; then
            FINAL_EMAIL_STRING="$DEFAULT_EMAILS"
            echo "No input provided. Using DEFAULT email from Secrets."
          else
            FINAL_EMAIL_STRING="$INPUT_EMAILS"
            echo "Using CUSTOM email from Input."
          fi

          # Process Emails into JSON Array
          EMAIL_ARRAY=$(jq -n --arg emails "$FINAL_EMAIL_STRING" '$emails | split(",") | map(sub("^\\s+|\\s+$"; ""))')

          # 2. Process Tables (String -> JSON Array OR null)
          if [[ -z "${INPUT_TABLES// }" ]]; then
            TABLE_ARRAY="null"
            echo "Copying ALL tables (no specific tables selected)."
          else
            TABLE_ARRAY=$(jq -n --arg tables "$INPUT_TABLES" '$tables | split(",") | map(sub("^\\s+|\\s+$"; "")) | select(length > 0)')
            echo "Copying specific tables: $TABLE_ARRAY"
          fi

          # 3. Construct Payload
          JSON_PAYLOAD=$(jq -n \
            --arg src "$SOURCE_ID" \
            --arg dest "$DEST_ID" \
            --argjson emails "$EMAIL_ARRAY" \
            --argjson files "$INPUT_FILES" \
            --argjson db "$INPUT_DB" \
            --argjson tables "$TABLE_ARRAY" \
            '{
              source_environment_id: $src,
              destination_environment_id: $dest,
              notification_emails: $emails,
              custom_options: (
                { include_files: $files, include_db: $db } +
                if ($tables != null) then { db_tables: $tables } else {} end
              )
            }')
          echo "Generated Payload: $JSON_PAYLOAD"

          # 4. Send Request
          curl -X POST "https://api.wpengineapi.com/v1/install_copy" \
            -u "$WPE_USER:$WPE_PASS" \
            -H "Content-Type: application/json" \
            -d "$JSON_PAYLOAD"

5. Step-by-Step Usage Guide

Step 1: Push Code to Staging

  1. Open your project in VS Code.
  2. Edit your theme/plugin files.
  3. Commit and push your changes to the main branch.git add . git commit -m "Fix layout bug" git push origin main
  4. Go to the Actions tab in GitHub to see the “Deploy to Staging” workflow running.
  5. Verify your changes on the Staging URL.

Step 2: Copy to Production

When the Staging site is verified and ready for launch:

  1. Safety Check: Verify the Production site is in content freeze, and no one is working on it to prevent data loss.
  2. Navigate to the Actions tab in the GitHub repository.
  3. On the left sidebar, click Copy Staging to Production (Copy).
  4. Click the Run workflow dropdown button on the right.
  5. Configure your deployment:
    • Files: Check if you updated code/assets.
    • Database: Check if you need to sync content/settings.
    • Specific Tables: If you only want to sync blog posts but keep plugin settings intact, type wp_posts, wp_postmeta.
    • Emails: Leave blank to notify the default admin, or add your own email.
  6. Click the green Run workflow button.
  7. WP Engine will begin the environment copy process. You will receive an email upon completion.

6. Technical Payload Reference

For troubleshooting, the GitHub Action constructs a JSON payload dynamically based on your inputs.

Example: Full Copy (Default)

{
  "source_environment_id": "eda55...",
  "destination_environment_id": "3d208...",
  "notification_emails": ["default@example.com"],
  "custom_options": {
    "include_files": true,
    "include_db": true
  }
}

Example: Partial Copy (Specific Tables) Input: Include DB = True, Specific Tables = “wp_options, wp_users”

{
  "source_environment_id": "eda55...",
  "destination_environment_id": "3d208...",
  "custom_options": {
    "include_files": true,
    "include_db": true,
    "db_tables": ["wp_options", "wp_users"]
  }
}

Found this useful?

There's more where that came from — explore the rest of the documentation, experiments, and production work.

Back to the index