How to implement a CI/CD pipeline for a WordPress plugin with GitHub Actions?

You want to set up a CI/CD pipeline for your WordPress plugin using GitHub Actions? Great idea! It totally automates the testing and deployment process, saving you a ton of time and reducing human error. In a nutshell, we’re going to create some GitHub Actions workflows that automatically run tests whenever you push code, and then, if everything looks good, deploy your plugin to the WordPress.org SVN repository.

Understanding the \”Why\” Behind CI/CD for WordPress Plugins

Before we dive into the “how,” let’s quickly touch on why this is such a good idea. Building a WordPress plugin can be a lot of fun, but managing its updates and testing can get tedious, especially as it grows.

What is CI/CD, Anyway?

CI/CD stands for Continuous Integration and Continuous Delivery (or Deployment).

Continuous Integration (CI)

This part is all about frequently merging code changes into a central repository. Think of it like a safety net. Every time you (or your team) make a change, an automated system kicks in to build the project and run a series of tests. If something breaks, you know about it almost immediately, making it much easier to pinpoint and fix the issue. For a WordPress plugin, this often means running unit tests, linting, and perhaps some static analysis.

Continuous Delivery/Deployment (CD)

Once your code passes all the CI checks, the CD part takes over.

Continuous Delivery

This means that your code is always in a deployable state. It’s ready to go live at any moment, but the final push to production (in our case, the WordPress.org plugin directory) is still a manual step.

Continuous Deployment

This is the fully automated version. If all tests pass, the new version of your plugin is automatically deployed without any manual intervention. For plugin developers on WordPress.org, this usually means pushing to the SVN repository. We’ll be focusing on this more automated approach for deployments.

Benefits for WordPress Plugin Developers

Automating your CI/CD offers several tangible benefits:

Catch Bugs Early

The sooner you find a bug, the easier and cheaper it is to fix. CI ensures that even small changes are tested, preventing issues from spiraling out of control.

Faster Release Cycles

With automated testing and deployment, you can release updates more frequently and consistently. This keeps your users happy and helps you respond faster to feedback or reported issues.

Improved Code Quality

Automated linters and static analysis tools can enforce coding standards, leading to cleaner, more maintainable code.

Reduced Manual Errors

Let’s be honest, we all make mistakes. Automating the deployment process eliminates the chances of human error during manual uploads, like forgetting to include a critical file or zipping the wrong directory.

Peace of Mind

Knowing that your plugin is automatically tested and deployed reliably allows you to focus more on developing new features rather than worrying about the release process.

For those looking to deepen their understanding of continuous integration and continuous deployment in the context of WordPress development, a related article that offers valuable insights is available at The Sheryar Blog. This resource provides additional tips and best practices for implementing CI/CD pipelines effectively, ensuring that your WordPress plugins are not only robust but also seamlessly integrated with modern development workflows.

Setting Up Your Repository and Initial Structure

Before we write any YAML for GitHub Actions, let’s make sure your plugin’s repository is ready.

Your Plugin’s Folder Structure

A typical WordPress plugin structure looks something like this:

  • my-awesome-plugin/
  • my-awesome-plugin.php (main plugin file)
  • includes/
  • admin/
  • public/
  • assets/
  • languages/
  • readme.txt (plugin readme for WordPress.org)
  • readme.md (for GitHub)
  • composer.json (if you use Composer)
  • phpunit.xml.dist (for PHPUnit configuration)
  • tests/ (where your tests live)

For our CI/CD pipeline, we’ll need a .github/workflows/ directory at the root of your repository. This is where all your GitHub Actions workflow files will reside.

“`

my-awesome-plugin/

├── .github/

│ └── workflows/

│ ├── ci.yml

│ └── deploy.yml

├── my-awesome-plugin.php

├── … (your plugin files)

├── readme.txt

└── tests/

└── Unit/

└── ExampleTest.php

“`

GitHub Repository Setup

Make sure your plugin’s code is pushed to a GitHub repository. If it’s not already, create a new one and push your code. This is a fundamental requirement for using GitHub Actions.

WordPress.org SVN Credentials

For deploying to WordPress.org, you’ll need the following:

Your WordPress.org Username

This is the username you use to log into WordPress.org.

An Application Password

It’s highly recommended not to use your main WordPress.org password directly in your CI/CD pipeline. Instead, WordPress.org allows you to generate application-specific passwords. Go to your WordPress.org profile, then “Edit Profile,” and scroll down to the “Application Passwords” section to generate one. Copy this password; you’ll use it as a secret in GitHub.

Your Plugin’s SVN URL

This will typically be https://plugins.svn.wordpress.org/your-plugin-slug/. You can find this on your plugin’s page on WordPress.org.

Implementing Continuous Integration (CI) with GitHub Actions

The CI part of our pipeline will run automated tests whenever new code is pushed. This ensures that any changes haven’t introduced regressions.

Creating the CI Workflow File (.github/workflows/ci.yml)

Let’s create a new file at .github/workflows/ci.yml.

“`yaml

name: CI for WordPress Plugin

on:

push:

branches:

  • main
  • master

pull_request:

branches:

  • main
  • master

jobs:

build-and-test:

runs-on: ubuntu-latest

steps:

  • name: Checkout code

uses: actions/checkout@v4

  • name: Setup PHP

uses: shivammathur/setup-php@v2

with:

php-version: ‘8.1’ # Adjust to your plugin’s minimum PHP requirement or test matrix

extensions: mbstring, iconv, json, dom, xml, simplexml, curl, zip

ini-values: post_max_size=256M, upload_max_filesize=256M

coverage: none # Can be xdebug or pcov if you want code coverage

  • name: Install Composer dependencies

run: composer install –prefer-dist –no-progress –no-interaction –ansi

  • name: Set up WordPress Test Environment (for PHPUnit)

run: |

bash ./bin/install-wp-tests.sh wordpress_test root ” localhost latest

Note: ‘bin/install-wp-tests.sh’ needs to be present in your repo.

This script sets up a temporary WordPress installation for running tests.

env:

WP_TESTS_DIR: /tmp/wordpress-tests-lib

  • name: Run PHPUnit tests

run: vendor/bin/phpunit

  • name: Run PHPStan (optional, for static analysis)

run: vendor/bin/phpstan analyse –memory-limit=2G

  • name: Run PHPCS (optional, for coding standards)

run: vendor/bin/phpcs –standard=WordPress .

“`

Explaining the CI Workflow

Let’s break down what each part of this YAML file does.

name: CI for WordPress Plugin

This is just a descriptive name for your workflow. It’ll show up in the GitHub Actions UI.

on:

This defines when the workflow should run.

  • push:: The workflow will trigger whenever code is pushed to the main or master branches.
  • pull_request:: It will also trigger when a pull request is opened or updated targeting the main or master branches. This is crucial for getting early feedback before merging changes.
jobs: build-and-test:

A workflow can have one or more jobs. This one is named build-and-test.

  • runs-on: ubuntu-latest: This specifies the operating system environment where your job will run. ubuntu-latest is a common and robust choice.
steps:

Here’s where the actual work happens, sequence by sequence.

Checkout code (using actions/checkout@v4)

This action checks out your repository’s code into the runner’s environment, making it available for subsequent steps. It’s almost always the first step in any GitHub Actions workflow.

Setup PHP (using shivammathur/setup-php@v2)

This action from shivammathur is incredibly useful for setting up a specific PHP version and common PHP extensions.

  • php-version: '8.1': Adjust this to the PHP version your plugin primarily supports or requires. You might even set up a matrix build to test against multiple PHP versions.
  • extensions: ...: Lists common PHP extensions needed for WordPress and Composer.
  • ini-values: ...: Sets specific php.ini values.
  • coverage: none: If you’re not generating code coverage reports, set this to none.
Install Composer dependencies

If your plugin uses Composer for managing dependencies, this step will install them. The flags --prefer-dist, --no-progress, --no-interaction, and --ansi are good practices for CI environments.

Set up WordPress Test Environment (for PHPUnit)

This is critical for running integration tests against a real WordPress environment.

  • bash ./bin/install-wp-tests.sh ...: This command relies on a script, typically install-wp-tests.sh, which is commonly used in WordPress development. You’ll need to place this script in your bin/ directory within your repository. This script downloads and sets up a fresh WordPress installation and the WordPress PHPUnit test suite. You’ll need to create this bin folder and place the script there or adjust the path. You can find this script in the official WordPress Core development repository, or many WordPress frameworks provide it. A simplified version is often sufficient.
  • env:: Sets environment variables specifically for this step. WP_TESTS_DIR tells the script where to install temporary WordPress files.
Run PHPUnit tests

This executes your PHPUnit test suite. You’ll need to have phpunit installed via Composer (usually composer require --dev phpunit/phpunit). Your phpunit.xml.dist file should be configured to use the WordPress test environment.

Run PHPStan (optional, for static analysis)

PHPStan is a powerful static analysis tool that can find bugs in your code without even running it. You’d typically install phpstan/phpstan via Composer.

Run PHPCS (optional, for coding standards)

PHP_CodeSniffer (PHPCS) ensures your code adheres to specific coding standards (like the WordPress Coding Standards). Install wp-coding-standards/wpcs and squizlabs/php_codesniffer via Composer.

Implementing Continuous Deployment (CD) with GitHub Actions

Once your CI checks pass, the deployment job takes over to push your plugin to WordPress.org. This typically triggers only on specific events, like a tag push.

Creating the CD Workflow File (.github/workflows/deploy.yml)

Let’s create another file at .github/workflows/deploy.yml.

“`yaml

name: Deploy to WordPress.org

on:

push:

tags:

  • ‘[0-9]+.[0-9]+.[0-9]+*’ # Triggers on tags like 1.0.0, 1.0.0-beta, etc.

jobs:

deploy:

runs-on: ubuntu-latest

steps:

  • name: Checkout code

uses: actions/checkout@v4

  • name: Prepare Plugin for Deployment

run: |

mkdir -p ./.tmp_plugin_deploy

cp -r ./. ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}

Remove .git, .github, and other non-plugin files

rm -rf ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/.git

rm -rf ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/.github

rm -rf ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/.gitignore

rm -rf ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/tests

rm -rf ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/vendor # If not distributing vendor dir

rm -f ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/composer.json

rm -f ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/composer.lock

Copy main plugin files to ‘trunk’

cp -r ./. ./.tmp_plugin_deploy/trunk/

rm -rf ./.tmp_plugin_deploy/trunk/.git

rm -rf ./.tmp_plugin_deploy/trunk/.github

rm -rf ./.tmp_plugin_deploy/trunk/.gitignore

rm -rf ./.tmp_plugin_deploy/trunk/tests

rm -rf ./.tmp_plugin_deploy/trunk/vendor # If not distributing vendor dir

rm -f ./.tmp_plugin_deploy/trunk/composer.json

rm -f ./.tmp_plugin_deploy/trunk/composer.lock

  • name: Deploy to WordPress.org

uses: 10up/action-wordpress-plugin-deploy@stable

id: deploy

with:

svn-username: ${{ secrets.WORDPRESS_USERNAME }}

svn-password: ${{ secrets.WORDPRESS_PASSWORD }}

This assumes your plugin slug your-plugin-slug is directly in the svn url

e.g. plugins.svn.wordpress.org/your-plugin-slug/

“`

Explaining the CD Workflow

This workflow is simpler, but it has some crucial parts.

name: Deploy to WordPress.org

Again, a descriptive name for the workflow.

on:

This defines when the deployment should happen.

  • push::
  • tags: ['[0-9]+.[0-9]+.[0-9]+*']: This is a regular expression that tells GitHub to trigger this workflow only when a Git tag matching a semantic versioning pattern is pushed (e.g., 1.0.0, 1.0.1-beta, 2.5.3). This is a common and safe way to initiate deployments, as you manually control when a new version is “released” by creating and pushing a tag.
jobs: deploy:

Our singular job for deployment.

  • runs-on: ubuntu-latest: Same as before.
steps:
Checkout code (using actions/checkout@v4)

Gets your plugin’s code.

Prepare Plugin for Deployment

This manual step is crucial if you want to control exactly what gets deployed to WordPress.org SVN.

  • mkdir -p ./.tmp_plugin_deploy: Creates a temporary directory to build the deployment package.
  • cp -r ./. ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}: Copies your entire repository content into a tags/YOUR_VERSION_NUMBER directory. GITHUB_REF_NAME will be the name of your pushed tag (e.g., 1.0.0).
  • rm -rf ...: These commands remove files and directories that you don’t want to be part of your deployed plugin. This typically includes .git, .github (your workflow files!), tests/, composer.json, composer.lock, and potentially your vendor/ directory if you’re not distributing compiled assets or are using a different approach for dependencies. Customize this list carefully! Make sure you’re not removing anything vital for your plugin.
  • cp -r ./. ./.tmp_plugin_deploy/trunk/: Similarly, copies your entire repository to the trunk/ directory. The trunk directory in SVN is where the development version of your plugin lives. This ensures that the latest changes are available for users who install the development version.
  • rm -rf ./.tmp_plugin_deploy/trunk/...: Again, clean up the trunk directory of non-plugin files.

Important Note on vendor/ directory: If your plugin has Composer dependencies, you have a few choices:

  1. Include vendor/: Copy vendor/ into your deploy package. This makes your plugin self-contained. You’ll need to omit rm -rf ./.tmp_plugin_deploy/tags/${GITHUB_REF_NAME}/vendor and similar for trunk.
  2. Autoloading within a single file: Merge your Composer dependencies into a single PHP file using tools like humbug/php-scoper or a custom build script. This is more advanced but avoids conflicts with other plugins.
  3. Reliance on a host: This is generally not recommended for WordPress.org plugins as user environments might not have the correct dependencies.

For most cases, including a vendor/ directory where the dependencies are prefix-namespaced (using a tool like php-scoper) is the safest bet to avoid conflicts with other plugins or themes that might use the same libraries. However, if your plugin is simple and only uses a few common libraries, including the vendor directory without scoping might be acceptable. Make sure to remove it from the rm -rf list if you choose to deploy it.

Deploy to WordPress.org (using 10up/action-wordpress-plugin-deploy@stable)

This is the magic action that handles the actual SVN commit.

  • uses: 10up/action-wordpress-plugin-deploy@stable: This is a popular and well-maintained action specifically for deploying WordPress plugins to WordPress.org SVN.
  • id: deploy: An optional ID for the step.
  • with::
  • svn-username: ${{ secrets.WORDPRESS_USERNAME }}: Your WordPress.org username. This uses a GitHub Secret for security.
  • svn-password: ${{ secrets.WORDPRESS_PASSWORD }}: Your application password for WordPress.org. This also uses a GitHub Secret.
  • plugin-slug: your-plugin-slug: Crucial! This should be the slug of your plugin as it appears in the WordPress.org plugin directory URL (e.g., if your plugin is at https://wordpress.org/plugins/my-awesome-plugin/, the slug is my-awesome-plugin).

If you’re looking to enhance your development workflow, implementing a CI/CD pipeline for a WordPress plugin with GitHub Actions can be a game changer. This approach not only automates testing and deployment but also ensures that your code remains robust and reliable. For those interested in further optimizing their server management alongside their development processes, you might find it helpful to read about the migration process in this article on migrating to another server. Understanding how to efficiently manage your server can complement your CI/CD efforts and lead to a more seamless development experience.

Managing Secrets for Secure Deployment

You’ve noticed secrets.WORDPRESS_USERNAME and secrets.WORDPRESS_PASSWORD in the deployment workflow. This is how you keep sensitive information out of your public code.

How to Add GitHub Secrets

  1. Go to your GitHub repository.
  2. Click on “Settings”.
  3. In the left sidebar, click “Secrets and variables” > “Actions”.
  4. Click on “New repository secret”.
  5. Add two secrets:
  • WORDPRESS_USERNAME: Enter your WordPress.org username.
  • WORDPRESS_PASSWORD: Enter the application password you generated for WordPress.org.

GitHub automatically encrypts these secrets, and they are only exposed to the jobs that need them during a workflow run. They will not be printed in logs.

Testing Your Pipeline

Once you have your ci.yml and deploy.yml files in place, along with your GitHub Secrets, it’s time to test.

Testing CI

  1. Push a change to main or master: Make a small, innocuous change (e.g., add a comment to your main plugin file) and push it to your main branch.
  2. Check GitHub Actions: Go to your repository on GitHub, click the “Actions” tab. You should see a workflow run triggered by your push. Click on it to see the status of each job and step.
  3. Create a Pull Request: Try creating a pull request from a feature branch to main. The CI workflow should automatically run on the PR.

Testing CD

  1. Ensure CI passes: Make sure your ci.yml workflow runs successfully on your main branch with the latest code. You don’t want to deploy broken code.
  2. Create and Push a Git Tag (e.g., 1.0.0):

“`bash

git checkout main # or master

git pull origin main

git tag 1.0.0 # Use your actual version number

git push origin 1.0.0

“`

  1. Check GitHub Actions again: A new workflow run for Deploy to WordPress.org should appear in the Actions tab. Monitor its progress.
  2. Verify on WordPress.org: If successful, your plugin’s version should be updated on WordPress.org, and a new tag (e.g., 1.0.0) should appear in the “Advanced View” section of your plugin page.

Fine-Tuning and Further Enhancements

This setup provides a solid foundation, but there’s always room for improvement.

Build Matrix for PHP Versions

You can test your plugin against multiple PHP versions by using a build matrix in your ci.yml.

“`yaml

jobs:

build-and-test:

runs-on: ubuntu-latest

strategy:

matrix:

php-version: [‘7.4’, ‘8.0’, ‘8.1’, ‘8.2’] # Test against multiple PHP versions

steps:

  • name: Checkout code

uses: actions/checkout@v4

  • name: Setup PHP ${{ matrix.php-version }}

uses: shivammathur/setup-php@v2

with:

php-version: ${{ matrix.php-version }}

extensions: …

coverage: none

… rest of your testing steps …

“`

This will create separate job runs for each specified PHP version, giving you confidence that your plugin works across different environments.

Composer Install/Update Considerations

For the CI workflow, composer install is typically preferred as it installs dependencies based on composer.lock, ensuring reproducible builds. For the deployment step, you might consider running composer install --no-dev --optimize-autoloader if you are including the vendor directory and want the most compact production-ready dependencies.

Linting and Formatting

Integrate tools like php-cs-fixer or prettier to automatically format your code and enforce consistency. You can add steps to your CI workflow to check formatting or even automatically fix it in a separate branch (though automatically fixing on main is less common for CI).

Asset Compilation (e.g., SCSS to CSS, JavaScript bundling)

If your plugin uses modern frontend build tools (webpack, Gulp, Grunt), you’ll need to add steps to your package.json and then to your ci.yml or deploy.yml to compile these assets.

“`yaml

  • name: Install Node.js

uses: actions/setup-node@v4

with:

node-version: ’18’ # Or your desired Node.js version

  • name: Install NPM dependencies

run: npm ci

  • name: Build assets

run: npm run build

“`

The npm run build command would then compile your assets. You would need to ensure these compiled assets are included in your deployment package.

Notifications

Integrate with Slack, Discord, or email to get notifications about successful or failed deployments. There are GitHub Actions for these, such as rtCamp/action-slack-notify@v2.

Release Notes Generation

Automate the generation of your changelog.txt based on your Git commit messages or specific tag comments. This ties in with your readme.txt updates for WordPress.org.

Having a robust CI/CD pipeline for your WordPress plugin isn’t just a nice-to-have; it’s a game-changer for maintainability, reliability, and faster development. While it takes a bit of upfront setup, the time saved and the confidence gained in your release process are well worth the effort. Happy coding!