By Alberuni Azad ·

Automate WordPress Theme Deployments with GitHub Actions and SSH

Timeline showing how weeks pass between making changes and opening the repository, leaving you unsure what's actually deployed on production

There’s a workflow pattern that’s common across WordPress development: you keep a local copy of the site, build and test everything locally, and when things look good, you upload the updated theme to the live server via FTP or cPanel.

It works. Until it doesn’t.

The problem shows up weeks later, when you open the repository and notice modified or untracked files. It’s easy to forget that some sites go untouched for weeks at a time, and that gap is long enough to lose track of what actually made it to production.

That’s when the confusion started.

Then I’d ask myself:

Are these changes already on production, or did I forget to deploy them?

Sometimes I’d end up downloading the live theme just to compare files manually.

Not because Git was broken. But because GitHub wasn’t my source of truth anymore. The production server was.

In this guide I’ll walk you through the exact setup I use to deploy a WordPress theme automatically, every time, straight from GitHub.


The Problem with Manual Uploads

The problem wasn’t FTP.

The problem wasn’t Git.

The real problem was the workflow.

It looked something like this:

Local changes

Upload via FTP

Production server

(Maybe commit later...)

Sometimes I’d remember to commit after deploying.

Sometimes I wouldn’t. Switching between multiple projects made it easy to move on and forget to come back.

Months later, Git could no longer answer a simple question:

What’s currently running on production?

When Git can’t answer that question, it stops being the source of truth.


The Approach

Diagram showing the five-step deployment flow: local development, GitHub repository, GitHub release, GitHub Actions, and finally the production server

One simple rule replaces the entire old workflow:

Never deploy directly to the server. Every deployment starts from GitHub.

Now every change follows exactly the same path.

  1. Make changes locally
  2. Commit
  3. Push
  4. Create a GitHub Release
  5. GitHub Actions deploys automatically

No FTP. No cPanel uploads. No “I’ll commit later.”


What You’ll Need

  • A WordPress theme stored in a GitHub repository
  • SSH access to your production server
  • A GitHub account

Step 1: Create the Workflow File

Create this file inside your repository. It must be on your default branch (main or master) for GitHub Actions to pick it up.

.github/workflows/deploy.yml
name: Deploy Theme to Production

on:
  release:
    types: [published]

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          echo "Host YOUR_SERVER_IP
            HostName YOUR_SERVER_IP
            Port YOUR_SSH_PORT
            User YOUR_SSH_USER
            IdentityFile ~/.ssh/deploy_key
            StrictHostKeyChecking no" > ~/.ssh/config

      - name: Create theme zip
        run: |
          THEME_NAME="your-theme-folder-name"
          cd "$GITHUB_WORKSPACE/.."
          zip -r /tmp/${THEME_NAME}-deploy.zip "$THEME_NAME" \
            -x "*.git*" \
            -x "*node_modules*" \
            -x "*docs/*" \
            -x "*.md" \
            -x "*.DS_Store" \
            -x "*/.claude/*" \
            -x "*/bin/*" \
            -x "*/bin/**" \
            > /dev/null

      - name: Upload to server
        run: |
          scp /tmp/your-theme-folder-name-deploy.zip \
            your-user@YOUR_SERVER_IP:~/public_html/wp-content/themes/

      - name: Extract on server
        run: |
          ssh your-user@YOUR_SERVER_IP << 'EOF'
          cd ~/public_html/wp-content/themes/
          unzip -o your-theme-folder-name-deploy.zip > /dev/null
          rm your-theme-folder-name-deploy.zip
          echo "Deployed successfully"
          EOF

Once a release is published, the workflow:

  1. Checks out the latest code
  2. Creates a production ZIP
  3. Excludes development files
  4. Uploads the ZIP via SSH
  5. Extracts it on the server
  6. Removes temporary files

The .git folder is excluded from the ZIP and never lands on the server.


Step 2: Add Your SSH Key to GitHub

First, get your private key from your local machine:

cat ~/.ssh/id_rsa

Then add it to your repository:

Settings
→ Secrets and variables
→ Actions
→ New repository secret

Secret name:

SSH_PRIVATE_KEY

Value: the full contents of ~/.ssh/id_rsa, including the header and footer lines.

Once this is configured, you never need to touch it again.


Step 3: Push and Create a Release

After making your theme changes:

git add .
git commit -m "Updated hero section"
git push origin main

Then create a GitHub Release using the CLI:

gh release create v1.2.0 \
  --title "v1.2.0" \
  --notes "Updated hero section"

Or create the release from the GitHub web interface.

That’s all. GitHub Actions handles the deployment automatically.


Step 4: Verify the Deployment

Even with automation, it’s worth confirming the deployment actually reached the server.

Before creating the release, add a traceable line to a log file:

echo "= v1.2.0 deployed =" >> changes-log.txt

After deployment, check it landed:

ssh your-user@your-server \
"head -3 ~/public_html/wp-content/themes/your-theme/changes-log.txt"

A quick sanity check that confirms the latest release is live.


Notes for Shared Hosting

A few things worth knowing if you’re on a shared host:

  • rsync is often blocked. The zip and SCP approach above works on any host with SSH access.
  • Git does not need to be installed on the server. The GitHub Actions runner handles everything and only uploads the final ZIP.
  • The .git folder is excluded from the ZIP, so it never lands on the server.
  • If a push fails with a 400 error, increase the HTTP buffer size:
git config http.postBuffer 524288000

Before vs After

Side-by-side comparison showing the painful and unreliable manual upload workflow on the left versus the simple and reliable GitHub Actions automated workflow on the right

The biggest improvement isn’t saving a few clicks.

It’s eliminating uncertainty.

Before:

  • Production could contain changes that Git didn’t.
  • Manual uploads were easy to forget.
  • I wasn’t always sure what was live.

After:

  • Every deployment starts from Git.
  • Every production release has a commit history.
  • GitHub always reflects exactly what’s running on the server.

Final Thoughts

This is one of those small workflow improvements that quietly saves time over the long run.

My website only gets updated every few weeks, which makes it surprisingly easy to forget what happened during the last deployment.

By making GitHub the only deployment path, I removed that uncertainty completely.

Now every deployment is reproducible.

Every release has a history.

Every production change can be traced back to a Git commit.

If you’re still uploading themes manually, I’d recommend giving GitHub Actions a try. The initial setup takes a little time, but afterward deploying becomes as simple as creating a release and you’ll always know exactly what’s running in production.


Want a smoother WordPress workflow?

Deployment is one piece of a healthy WordPress setup. If you want help wiring up CI/CD, cleaning up a fragile deploy process, or hardening a production site, book a free discovery call.

Or browse my WordPress and WooCommerce services to see how I can help.