WordPress CI/CD with GitHub Actions: A Professional Deployment Pipeline
How to build a WordPress CI/CD pipeline with Git and GitHub Actions: tests on every pull request, staging on merge, approved production deploys and rollbacks.
WordPress CI/CD means your site’s code lives in Git, every change is tested automatically, and deployments happen through a script instead of someone editing files on the live server. With GitHub Actions you can run tests on each pull request, deploy to staging when code is merged, and deploy to production only after a person approves it. This guide walks through the full setup we use, from what belongs in the repository to how to roll back a bad release.
Why “edit on production” creates technical debt
Many WordPress sites are still changed the old way. Someone opens the theme editor, uploads a file over FTP, or installs a plugin straight on the live site. It works until it doesn’t. Then nobody knows what changed, when, or why.
That missing history is technical debt. It shows up as:
- Code that exists only on one server, with no copy anywhere else.
- Changes nobody can review, test or undo.
- Fear of updates, because past updates broke things in ways no one could trace.
- A site that only one person understands.
A deployment pipeline fixes this at the root. Every change has an author, a reason and a review. Every release can be repeated or reversed. In our guide on how to measure WordPress technical debt, “untested custom code” is one of the six metrics we score. A pipeline is the most direct way to push that number down and keep it down.
Step 1: Put the code in Git, and only the code
Git is a version control system. It records every change to your files, who made it and why. The first decision is what goes into the repository.
What belongs in the repository
| In Git | Not in Git |
|---|---|
| Your custom theme and child theme | wp-content/uploads (media library files) |
| Custom plugins and must-use plugins | wp-config.php with real passwords and keys |
composer.json and composer.lock | vendor/ and node_modules/ folders |
package.json and package-lock.json | Built CSS and JavaScript files |
| Test files and the CI workflow | Database dumps and backups |
| Server and build configuration templates | Cache folders and log files |
Uploads are content, not code. They grow every day and belong to production. Database dumps often contain customer data and should never sit in a repository.
Keep secrets out of wp-config.php
Your wp-config.php holds database passwords and the security salts. Do not commit it with real values. A common pattern is to commit a version that reads values from environment variables, or from a small file that exists only on each server. Each environment (local, staging, production) then gets its own credentials. If a secret ever lands in Git history, treat it as leaked and change it. Deleting the file in a later commit does not remove it from the history.
Manage plugins and build steps with Composer and npm
Composer is the dependency manager for PHP. Many teams use it to install WordPress core, public plugins from the WordPress.org directory and premium plugins at exact versions. The composer.lock file pins those versions, so staging and production get exactly what you tested.
npm does the same for front-end tools. It installs the packages that compile your theme’s CSS and JavaScript. Commit the lock files, not the output. The pipeline runs composer install and npm run build itself, so what reaches the server is always built from reviewed source.
Step 2: Give every developer a local environment
Nobody should need the live site to work on the code. Two common options:
- wp-env: the official WordPress tool that starts a local site in Docker with one command. It also includes a separate test site, which is handy for automated tests. See the wp-env documentation on developer.wordpress.org.
- Custom Docker Compose: more setup, but you can match production closely, including the same PHP version, web server and object cache.
Whichever you choose, put the configuration in the repository. A new developer should be able to clone the repo, run one or two commands and have a working copy. We work in Docker daily and often run hundreds of throwaway WordPress test sites in automated environments. Being able to throw a site away and rebuild it in minutes is what makes this work.
Step 3: Set up staging and production environments
You need at least two servers or hosting environments:
- Staging: a private copy of the site that runs the next release before customers see it. It should match production in PHP version, database version and server setup.
- Production: the live site.
Lock down staging so search engines cannot index it, and make sure it cannot send real emails or charge real cards. Our article on safe WordPress updates with staging and automated tests covers how to keep staging close to production.
Step 4: Test every pull request with GitHub Actions
A pull request is a proposed change that others can review before it is merged into the main branch. GitHub Actions can run your checks on every pull request and block the merge if anything fails. A good set of checks for WordPress:
- Linters: PHP_CodeSniffer with the WordPress Coding Standards, plus ESLint and Stylelint for front-end code. These catch style problems and many common bugs.
- PHPUnit: unit and integration tests for your custom PHP, such as plugin logic, form handling and custom endpoints.
- Playwright: end-to-end tests that drive a real browser through key paths like login, checkout and contact forms.
wp-env works well inside GitHub Actions, because the hosted Ubuntu runners include Docker. We cover how to write these tests in automated testing for WordPress with Playwright and PHPUnit.
In your repository settings, turn on branch protection for main. Require the test job to pass and require at least one review before merging. This one setting ends most “quick fixes” that skip review.
Step 5: Deploy to staging on merge and to production on approval
Once a pull request is merged into main, the pipeline deploys the build to staging automatically. Production waits for a person.
GitHub Environments make this simple. You create two environments in the repository settings, staging and production. For production you add required reviewers. When a job targets that environment, it pauses until one of the reviewers approves it. You can also restrict which branches are allowed to deploy to each environment. The GitHub documentation on managing environments explains each option. Availability for private repositories depends on your GitHub plan.
An example workflow
Here is a short, generic workflow. It assumes your package.json has test:php and test:e2e scripts that start wp-env and run PHPUnit and Playwright, and that a bin/deploy.sh script copies the built files to the server over SSH.
name: CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
- uses: actions/setup-node@v7
with:
node-version: 24
- run: composer install --no-interaction --prefer-dist
- run: npm ci
- run: composer run lint
- run: npm run lint
- run: npm run build
- run: npm run test:php
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
deploy-staging:
if: github.event_name == 'push'
needs: test
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v7
- run: ./bin/deploy.sh
env:
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://www.example.com
steps:
- uses: actions/checkout@v7
- run: ./bin/deploy.sh
env:
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
The deploy.sh script would run composer install --no-dev and npm run build, then copy the result to the server. Because both deploy jobs use the same script with different environment values, staging is a true rehearsal for production. Check the current major versions of each action before you copy this, since they change over time.
Step 6: Manage secrets properly
Secrets are passwords, API keys and SSH keys. A few rules:
- Store them as environment secrets in GitHub, not in the repository. Environment secrets are only given to jobs that use that environment. With required reviewers, production secrets are not released until someone approves.
- Use a separate SSH key for each environment, limited to a deploy user that can only write to the site folder.
- Keep non-secret settings, like host names, as environment variables (
vars) so they are easy to read and change. - Rotate keys when someone leaves the team, and review who can approve production deploys.
Leaked keys and stale accounts are part of the security exposure we measure in every audit. A pipeline gives you one place to manage them instead of a dozen FTP logins nobody remembers creating.
Step 7: Code flows up, content flows down
This is the rule that keeps WordPress pipelines sane:
- Code flows up: local, then staging, then production, always through Git and the pipeline.
- Content flows down: the database and uploads are copied from production to staging or local, never the other way.
Production is where editors write posts, customers place orders and forms collect entries. If you push a staging database to production, you overwrite all of that. When you copy production data down, use WP-CLI’s search-replace command to update URLs, and scrub or anonymize personal data before it reaches a developer laptop.
What about settings that live in the database, such as plugin options or a new page template? Make them part of the code. Write a small migration that runs with WP-CLI during deployment, or register settings in code. Then the change is tested and repeatable, not clicked by hand on each server.
Step 8: Plan your rollbacks
Every pipeline needs a fast way back. Two common approaches:
- Release folders with a symlink: each deploy goes into a new, dated folder, and a
currentsymlink points to the live one. Rolling back means pointing the symlink at the previous folder. It takes seconds. Tools like Deployer work this way. - Redeploy a previous tag: tag each release in Git. To roll back, run the production deploy for the last good tag.
Code rollbacks are the easy part. Database changes are harder to reverse, so take a backup right before any release that changes the database, and write migrations that can be undone where possible. Test the restore process, not just the backup.
What you get from a pipeline
After this setup, a typical change looks like this: a developer opens a pull request, automated tests run, a teammate reviews it, it merges, staging updates on its own, someone checks staging, and a reviewer approves production. Every step is recorded.
The results are practical. Updates stop being scary. New developers can start quickly. Problems are found in pull requests instead of by customers. And the risky habit of editing production directly simply has no place to happen.
Getting help with your WordPress pipeline
If your site still changes through FTP and the admin dashboard, we can move it into Git, set up local and staging environments, and build a GitHub Actions pipeline with tests and approved deploys. Our server management service covers the hosting side, and a WordPress technical debt audit shows where to start. Contact us to talk about your setup.
Frequently asked questions
- What does CI/CD mean for a WordPress site?
- CI (continuous integration) means every code change is tested automatically before it is merged. CD (continuous delivery) means tested code is deployed to staging and production by a repeatable script instead of by hand over FTP or the admin dashboard.
- Should the WordPress uploads folder be in Git?
- No. Uploads are content, not code. They change every day, they can be very large, and they belong to production. Keep them out of the repository and back them up separately, or store them in object storage.
- Can GitHub Actions require approval before deploying to production?
- Yes. GitHub Environments let you add required reviewers to a production environment. The deploy job pauses until one of the listed reviewers approves it, and the environment's secrets are only released to the job after approval.
- Do I still need backups if I have a deployment pipeline?
- Yes. A pipeline makes code rollbacks easy, but it does not protect your database or uploaded media. Keep regular, tested backups of both, and take one right before any deployment that changes the database.