Every test built so far in this course only protects you if it actually runs before a change merges, not whenever someone happens to remember to run it locally. This lesson wires the deterministic PHPUnit suite from Lessons 3 and 4 into GitHub Actions, against a real MySQL service container rather than a mocked database, and sets a branch protection rule so a red test blocks the merge, not just embarrasses it after the fact. It also wires in the eval suite from Lesson 5, deliberately on a different schedule, for the reasons that lesson already laid out.
The Composer-based PHPUnit setup from Lesson 2, and at least the test files from Lessons 3, 4, and 6 passing locally. A GitHub repository with Actions enabled and permission to add a branch protection rule.
Step 1: The PHPUnit workflow, against a real database service container
wp-phpunit/wp-phpunit, from Lesson 2, still needs a real MySQL database to create
its test tables against, no dependency swap changes that. GitHub Actions’ service
containers give you one for free, started and torn down automatically for each job
run, no Docker-in-Docker layer like a full wp-env setup needs:
# File: .github/workflows/test.yml
name: Test
on:
push:
branches: [main]
pull_request:
jobs:
phpunit:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['8.1', '8.3']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer
- name: Install dependencies
run: composer install --no-progress --prefer-dist
- name: Wait for MySQL to accept connections
run: |
until mysqladmin ping -h127.0.0.1 -P3306 -uroot -proot --silent; do
sleep 1
done
- name: Run PHPUnit
run: composer test
The options block on the mysql service isn’t decoration, it’s what makes the
mysqladmin ping wait step reliable, the health check gives Actions its own signal
that the container is actually ready to accept connections, not merely started.
Step 2: Why a service container instead of wp-env for this pipeline
Course 8’s CI/CD lesson runs its suite through wp-env inside GitHub Actions, a full
containerized WordPress environment, which is a reasonable, real approach and works
well when your local development already standardizes on wp-env. This pipeline takes
a lighter route on purpose, pairing directly with the Composer-based
wp-phpunit/wp-phpunit setup from Lesson 2: no Docker layer to start and wait on
beyond the one MySQL container, and the exact same composer test command your local
machine runs, in CI, with no translation between the two.
If your team already runs wp-env for local development, Course 8’s approach keeps
local and CI environments identical, which is a real, legitimate reason to prefer it.
This lesson’s approach optimizes for CI speed and for matching the Composer-native
setup this course builds in Lesson 2. Pick the one that matches how your team already
develops locally, running both is unnecessary duplication.
Step 3: Matrix across PHP and WordPress versions
Keep the matrix as small as your plugin’s actual support window, every additional combination multiplies total CI time, and a matrix testing versions nobody runs in production is cost without signal.
Step 4: Block the merge, don’t just report the result
A workflow that runs but doesn’t gate anything is a suggestion, not a safeguard. In
your repository’s branch protection settings (Settings, then Branches, then a
protection rule on your default branch), require the phpunit job as a required
status check before a pull request can merge. Once that’s set, a red test from Lesson
3’s permission suite or Lesson 4’s fake-summarizer suite makes the merge button
unavailable, not just unattractive.
GitHub Actions matches a required status check by the job’s name as it appears in the
workflow run, phpunit in this workflow. Rename the job, or add a matrix without
updating the required check to match a specific matrix leg, and pull requests can get
stuck unable to merge even though the tests are actually passing, since the check
GitHub is waiting for no longer appears under the old name. Confirm the exact check
name in a real, recent workflow run before setting the branch protection rule.
Step 5: A separate, non-blocking job for the eval suite
Lesson 5’s eval suite makes real API calls and has expected run-to-run variance, so it does not belong in the workflow that gates every pull request. Give it its own workflow, on a schedule, informational rather than merge-blocking:
# File: .github/workflows/evals.yml
name: Evals
on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
jobs:
eval:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Run eval suite
run: wp eval-file tests/evals/tagging-eval.php
continue-on-error: true
continue-on-error: true here is deliberate, the job still runs and its output is
still visible in the Actions log every morning, but a below-threshold eval score
doesn’t block a merge the way a PHPUnit failure does. workflow_dispatch lets anyone
trigger it manually right before a prompt or provider change ships, exactly the moment
Lesson 5 recommended actually reading its output closely.
Test it: push a deliberate regression and watch the pipeline catch it
Open a pull request that comments out the capability check in a permission_callback,
the same deliberate break Lesson 3 used locally. Confirm the phpunit job goes red in
the pull request’s checks, and, with the branch protection rule from Step 4 in place,
confirm the merge button is genuinely disabled, not just showing a warning. That’s the
whole pipeline working end to end, a real regression, caught before a human or an
agent ever sees it in production.
Recap
A real CI/CD pipeline for ability testing runs the deterministic PHPUnit suite from
Lessons 3 and 4 against a real MySQL service container in GitHub Actions, using the
same composer test command a developer runs locally, then blocks merges on a failing
required status check. The eval suite from Lesson 5 gets its own separate, scheduled
workflow, informational rather than merge-blocking, since its cost and variance
profile is entirely different from an exact-assertion test suite. Course 8’s wp-env
based pipeline is a legitimate alternative, pick whichever matches how your team
already develops locally.
Resources & further reading
- GitHub Actions service containers, GitHub Docs
- About protected branches, GitHub Docs
- wp-phpunit/wp-phpunit documentation, GitHub