Background Gradient for Hero Section

Automate Your WordPress Plugin Workflow: Build and Deploy Like a Pro

Releasing a WordPress plugin to the world should feel like a milestone, but for most developers it turns into a series of painful, manual steps. You zip the files, upload them to WordPress.org or your own site, run tests, realise you forgot to update the version number, do it all again. Repeat for every fix and feature.

In this guide, I’ll walk you through how I automated the full build and release pipeline for my own WordPress plugins using GitHub Actions, WP-CLI, and a few battle-tested conventions. By the end, you’ll have a workflow that handles versioning, building assets, running tests, and publishing a release automatically every time you push a new tag.

Why Automate Your WordPress Plugin Workflow?

Manual deployment is error-prone. I once shipped a plugin update that had the wrong version string in the main file header, which broke the update notification on WordPress.org. That kind of mistake is embarrassing and avoidable.

Automating your workflow gives you:

  • Consistent, repeatable builds every time
  • Automatic version bumps tied to Git tags
  • Test runs before any code ships
  • Clean release artefacts with no dev files included
  • Deployment to WordPress.org SVN or your own update server without touching the terminal

The Stack I Use

  • GitHub Actions for CI/CD
  • Composer for PHP dependencies
  • npm for JavaScript and CSS assets
  • PHPUnit for unit tests
  • PHPCS + WPCS for code standards
  • WP-CLI for WordPress-specific tasks
  • 10up/action-wordpress-plugin-deploy for SVN deployment

Step 1: Structure Your Repository Correctly

Before any automation makes sense, your repository needs a clean structure. Here is the layout I use:

my-plugin/
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── deploy.yml
├── assets/            # WordPress.org banner and screenshot images
├── src/               # PHP source files
├── tests/             # PHPUnit tests
├── vendor/            # Composer packages (gitignored)
├── node_modules/      # npm packages (gitignored)
├── my-plugin.php      # Main plugin file
├── readme.txt         # WordPress.org readme
├── composer.json
├── package.json
└── phpcs.xml

Keep vendor/ and node_modules/ out of version control. Your build step will install them fresh every run.

Step 2: Set Up the CI Workflow

The CI workflow runs on every push and pull request. Its job is to catch problems before they reach production.

# .github/workflows/ci.yml
name: CI

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

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

    steps:
      - uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          tools: composer, cs2pr

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Run PHPCS
        run: vendor/bin/phpcs --report=checkstyle | cs2pr

      - name: Run PHPUnit
        run: vendor/bin/phpunit --testdox

The cs2pr tool converts PHPCS output into inline pull request annotations, which makes reviewing code issues much faster.

Step 3: Automate Version Bumping

Version numbers appear in at least three places in a typical WordPress plugin:

  • The Version: header in the main plugin file
  • The define( 'MY_PLUGIN_VERSION', ... ) constant
  • The Stable tag: line in readme.txt

I handle this with a small shell script that reads the tag name from the GitHub Actions context and replaces the version strings before building.

#!/bin/bash
# scripts/bump-version.sh
VERSION=$1

sed -i "s/Version: .*/Version: $VERSION/" my-plugin.php
sed -i "s/define( 'MY_PLUGIN_VERSION', '.*' )/define( 'MY_PLUGIN_VERSION', '$VERSION' )/" my-plugin.php
sed -i "s/Stable tag: .*/Stable tag: $VERSION/" readme.txt

echo "Bumped to $VERSION"

This script runs as part of the deploy workflow, before the plugin files are packaged.

Step 4: Build the Release Artefact

The release artefact is the zip file that gets distributed. It should contain only production files, not tests, dot files, or development config.

I use a .distignore file to control what gets excluded:

# .distignore
.git
.github
.gitignore
tests/
node_modules/
*.test.js
composer.json
composer.lock
package.json
package-lock.json
phpcs.xml
phpunit.xml
CHANGELOG.md
*.md

Then in the deploy workflow I copy everything except the ignored files into a clean build directory and zip it:

- name: Build plugin
  run: |
    rsync -av --exclude-from='.distignore' . build/my-plugin/
    cd build
    zip -r my-plugin.zip my-plugin/

Step 5: The Full Deploy Workflow

The deploy workflow fires when you push a tag that matches v*.*.*. It bumps the version, builds the artefact, creates a GitHub Release, and deploys to WordPress.org SVN.

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          tools: composer

      - name: Install dependencies
        run: composer install --prefer-dist --no-dev --no-progress

      - name: Get version from tag
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Bump version strings
        run: bash scripts/bump-version.sh ${{ steps.version.outputs.VERSION }}

      - name: Build plugin zip
        run: |
          rsync -av --exclude-from='.distignore' . build/my-plugin/
          cd build && zip -r my-plugin.zip my-plugin/

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: build/my-plugin.zip
          generate_release_notes: true

      - name: Deploy to WordPress.org
        uses: 10up/action-wordpress-plugin-deploy@stable
        env:
          SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
          SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
          SLUG: my-plugin

To use the SVN deploy step, add your WordPress.org username and password as repository secrets under Settings > Secrets and variables > Actions.

Step 6: Trigger a Release

Once everything is in place, releasing a new version is a two-command operation:

git tag v1.2.0
git push origin v1.2.0

GitHub Actions picks up the tag, runs the full deploy workflow, creates the release on GitHub, and pushes the new version to WordPress.org. You do not touch the terminal again.

Bonus: Running Tests Against Multiple PHP Versions

WordPress supports a wide range of PHP versions. Use a matrix strategy in your CI workflow to test against all of them:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: [ '7.4', '8.0', '8.1', '8.2' ]

    steps:
      - uses: actions/checkout@v4

      - name: Set up PHP ${{ matrix.php }}
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}

      - name: Install and test
        run: |
          composer install --prefer-dist --no-progress
          vendor/bin/phpunit

Frequently Asked Questions

No. GitHub Actions is free for public repositories and includes a generous free tier for private repositories (2000 minutes per month on the free plan).

Yes. Simply remove the SVN deploy step and upload the zip to your own update server, a private S3 bucket, or a licensing platform like Freemius instead.

Add a Node.js setup step before the build and run your npm script. For example: npm ci && npm run build. The compiled assets will be included in the release zip automatically.

GitHub Actions logs every step. Check the Actions tab in your repository, find the failed run, and expand the failing step to see the exact error. Most failures are missing secrets, incorrect file paths, or SVN authentication issues.

Conclusion

A well-automated plugin workflow pays for the setup time on the very first release. You stop worrying about manual steps, version string mismatches, and forgetting to run tests before shipping. The pipeline handles all of that for you.

Start with the CI workflow, get your tests passing, then layer on the deploy workflow when you are ready. Tag a release, watch the Actions tab, and let the automation do the heavy lifting.

If you need help building or maintaining a custom WordPress plugin, take a look at my WordPress plugin development services.

Mehul Gohil
Mehul Gohil

Mehul Gohil is a Full Stack WordPress developer and an active member of the local WordPress community. For the last 13+ years, he has been developing custom WordPress plugins, custom WordPress themes, third-party API integrations, performance optimization, and custom WordPress websites tailored to the client's business needs and goals.

Articles: 144

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Mehul Gohil

Subscribe now to keep reading and get access to the full archive.

Continue reading