This guide will explain how to set up a GitHub Actions workflow for deploying a WordPress site, making use of server structure and deployment automation. The workflow automates the process of building, packaging, uploading, and activating a WordPress release on a server. Let’s break down each step and see how the script works.
Preliminary steps for setting up WordPress deployment
Before you implement the GitHub Actions workflow for deploying your WordPress site, there are several key preparatory steps you need to undertake:
Check web host
Check that your host allows deployment via github actions. If not you can stop this tutorial.
Set up GitHub secrets
To securely manage sensitive information like server credentials, you should use GitHub Secrets. This ensures that your deployment script can access necessary credentials without exposing them in your codebase. For a detailed guide on setting up GitHub Secrets, refer to this tutorial.
The secrets needed for this workflow are:
- HOST
- USERNAME
- PASSWORD
- PORT
- PATH
Edit theme name and backup folder
Within the workflow, be sure to customize the `THEME_NAME` environment variables and `your-backup-theme` folder name, to match your theme’s name and the backup directory for your base theme, respectively. This is necessary to ensure that the workflow correctly targets your theme and manages the backup accordingly.
Repository structure assumption
This deployment workflow assumes that your entire WordPress installation is tracked in your Git repository, with custom development work primarily occurring within the theme directory. This approach allows for comprehensive version control and streamlined deployment updates.
By taking these steps, you prepare your environment for a smooth and efficient deployment process, leveraging GitHub Actions to automate and manage your WordPress site.
Create the server directory structure
Ensure that your server is set up with the following directory structure to accommodate the deployment process effectively:
.
โโโ your server/
โโโ artifacts/
โโโ www/
โ โโโ releases/
โ โ โโโ static/
โ โ โโโ .htaccess
โ โ โโโ wp-config.php
โ โ โโโ languages/
โ โ โโโ uploads/
โ โ โโโ themes/your-backup-theme
โ โโโ current(symlink to latest release)/
โโโ .htaccess
Step 1: Triggering the workflow
The workflow is configured to trigger on a push to the `release/deploy` branch or manually through the GitHub interface.
name: Deploy WordPress
on:
push:
branches: ['release/deploy']
workflow_dispatch:
This setup ensures that deployments are only initiated intentionally, either by pushing to a specific branch or manually triggering the workflow.
Step 2: Building the deployment artifact
The build job compiles assets and packages for the WordPress website, preparing it for deployment. This part of the script checks out the repository and sets up a Node.js environment, allowing for asset compilation.
The workflow packages the WordPress site, excluding unnecessary files.
This step involves excluding non-essential files and creating a compressed archive of the WordPress site, which is then uploaded as an artifact.
build:
name: 'Build theme/plugin & zip WordPress'
runs-on: ubuntu-latest
env:
THEME_NAME: 'your-theme'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Compile CSS and Javascript
# build your theme/plugin here
run: |
cd wp-content/themes/${THEME_NAME}
- name: Create deployment artifact
env:
GITHUB_SHA: ${{ github.sha }}
run: |
excluded_theme_files=(
"wp-content/themes/${THEME_NAME}/src"
"wp-content/themes/${THEME_NAME}/package.json"
"wp-content/themes/${THEME_NAME}/package-lock.json"
"wp-content/themes/${THEME_NAME}/composer.json"
"wp-content/themes/${THEME_NAME}/composer.lock"
"wp-content/themes/${THEME_NAME}/README.md"
"wp-content/themes/${THEME_NAME}/.gitignore"
"wp-content/themes/${THEME_NAME}/vendor"
"wp-content/themes/${THEME_NAME}/node_modules"
"wp-content/themes/${THEME_NAME}/.editorconfig"
"wp-content/themes/${THEME_NAME}/.php-cs-fixer.php"
)
excluded_root_files=(
*.git
.vscode
.husky
node_modules
scripts
vendor
package.json
package-lock.json
composer.json
composer.lock
wp-config.php
README.md
)
excluded_args=""
for file in "${excluded_theme_files[@]}" "${excluded_root_files[@]}"; do
excluded_args+=" --exclude=$file"
done
tar -czf "${GITHUB_SHA}".tar.gz --anchored $excluded_args *
- name: Store artifact for distribution
uses: actions/upload-artifact@v3
with:
name: app-build
path: ${{ github.sha }}.tar.gz
Step 3: Preparing the release on the server
The prepared artifact is downloaded and extracted on the server, readying it for activation.
The workflow transfers the deployment package to the server and extracts it into a designated release directory.
prepare-release-on-servers:
name: 'Prepare release'
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v3
with:
name: app-build
- name: Upload
uses: appleboy/scp-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
source: ${{ github.sha }}.tar.gz
target: ${{ secrets.PATH }}/artifacts
- name: Extract archive and create directories
uses: appleboy/ssh-action@master
env:
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
envs: GITHUB_SHA
script: |
mkdir -p "${{ secrets.PATH }}/www/releases/${GITHUB_SHA}"
tar xzf ${{ secrets.PATH}}/artifacts/${GITHUB_SHA}.tar.gz -C "${{ secrets.PATH}}/www/releases/${GITHUB_SHA}"
Step 4: Activating the release
The server configuration is updated to point to the new release, and required symbolic links are created.
Symbolic links are created to ensure that static assets and configuration files remain consistent across releases.
activate-release:
name: 'Activate release'
runs-on: ubuntu-latest
needs: [prepare-release-on-servers, run-before-hooks]
steps:
- name: Activate release
uses: appleboy/ssh-action@master
env:
GITHUB_SHA: ${{ github.sha }}
BASE_PATH: ${{ secrets.PATH }}
RELEASE_PATH: ./releases/${{ github.sha }}
ACTIVE_RELEASE_PATH: ${{ secrets.PATH }}/www/current
ACTIVE_WP_CONTENT_PATH: ${{ secrets.PATH }}/www/releases/${{ github.sha }}/wp-content
UPLOADS_PATH: ../../static/uploads/
LANG_PATH: ../../static/languages/
BACKUP_THEME_PATH: ../../../static/themes/your-backup-theme/
HTACCESS_PATH: ../static/.htaccess
WP_CONFIG_PATH: ../static/wp-config.php
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME}}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT}}
envs: GITHUB_SHA,BASE_PATH,RELEASE_PATH,ACTIVE_RELEASE_PATH,ACTIVE_WP_CONTENT_PATH,UPLOADS_PATH,LANG_PATH,BACKUP_THEME_PATH,HTACCESS_PATH,WP_CONFIG_PATH
script: |
cd $BASE_PATH/www
ln -s -n -f $RELEASE_PATH $ACTIVE_RELEASE_PATH
cd $ACTIVE_WP_CONTENT_PATH
ln -s -n -f $UPLOADS_PATH ./uploads
ln -s -n -f $LANG_PATH ./languages
cd $ACTIVE_WP_CONTENT_PATH/themes
ln -s -n -f $BACKUP_THEME_PATH ./your-backup-theme
cd $ACTIVE_RELEASE_PATH
ln -s -n -f $HTACCESS_PATH .htaccess
ln -s -n -f $WP_CONFIG_PATH wp-config.php
Step 5: Clean up
Old releases and artifacts are removed, keeping the server clean and efficient.
This process ensures that only the two most recent releases and artifacts are retained, preventing the accumulation of unnecessary files on the server.
clean-up:
name: 'Clean up'
runs-on: ubuntu-latest
needs: [prepare-release-on-servers, run-before-hooks, activate-release, run-after-hooks]
steps:
- name: Clean old releases and artifacts
uses: appleboy/ssh-action@master
env:
RELEASES_PATH: ${{ secrets.PATH }}/www/releases
ARTIFACTS_PATH: ${{ secrets.PATH }}/artifacts
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME}}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT}}
envs: RELEASES_PATH,ARTIFACTS_PATH
script: |
cd $RELEASES_PATH && ls -d -t -1 */ | grep -v "static" | tail -n +3 | xargs rm -rf
cd $ARTIFACTS_PATH && ls -t -1 | tail -n +3 | xargs rm -rf
Complete workflow script
Here’s the full GitHub Actions workflow script that automates the deployment process for a WordPress site, integrating the steps discussed above:
name: Deploy WordPress
on:
push:
branches: ['release/deploy']
workflow_dispatch:
jobs:
build:
name: 'Build theme/plugin & zip WordPress'
runs-on: ubuntu-latest
env:
THEME_NAME: 'your-theme'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Compile CSS and Javascript
# build your theme/plugin here
run: |
cd wp-content/themes/${THEME_NAME}
- name: Create deployment artifact
env:
GITHUB_SHA: ${{ github.sha }}
# zip the whole WordPress without useless files (this list is non-exhaustive)
run: |
excluded_theme_files=(
"wp-content/themes/${THEME_NAME}/src"
"wp-content/themes/${THEME_NAME}/package.json"
"wp-content/themes/${THEME_NAME}/package-lock.json"
"wp-content/themes/${THEME_NAME}/composer.json"
"wp-content/themes/${THEME_NAME}/composer.lock"
"wp-content/themes/${THEME_NAME}/README.md"
"wp-content/themes/${THEME_NAME}/.gitignore"
"wp-content/themes/${THEME_NAME}/vendor"
"wp-content/themes/${THEME_NAME}/node_modules"
"wp-content/themes/${THEME_NAME}/.editorconfig"
"wp-content/themes/${THEME_NAME}/.php-cs-fixer.php"
)
excluded_root_files=(
*.git
.vscode
.husky
node_modules
scripts
vendor
package.json
package-lock.json
composer.json
composer.lock
wp-config.php
README.md
)
excluded_args=""
for file in "${excluded_theme_files[@]}" "${excluded_root_files[@]}"; do
excluded_args+=" --exclude=$file"
done
tar -czf "${GITHUB_SHA}".tar.gz --anchored $excluded_args *
- name: Store artifact for distribution
uses: actions/upload-artifact@v3
with:
name: app-build
path: ${{ github.sha }}.tar.gz
# Unzip release on the server
prepare-release-on-servers:
name: 'Prepare release'
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v3
with:
name: app-build
- name: Upload
uses: appleboy/scp-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
source: ${{ github.sha }}.tar.gz
target: ${{ secrets.PATH }}/artifacts
- name: Extract archive and create directories
uses: appleboy/ssh-action@master
env:
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
envs: GITHUB_SHA
script: |
mkdir -p "${{ secrets.PATH }}/www/releases/${GITHUB_SHA}"
tar xzf ${{ secrets.PATH}}/artifacts/${GITHUB_SHA}.tar.gz -C "${{ secrets.PATH}}/www/releases/${GITHUB_SHA}"
# Not really useful but it's good to have it just in case
run-before-hooks:
name: 'Before hook'
runs-on: ubuntu-latest
needs: [prepare-release-on-servers]
steps:
- name: Run before hooks
uses: appleboy/ssh-action@master
env:
GITHUB_SHA: ${{ github.sha }}
RELEASE_PATH: ${{ secrets.PATH }}/www/releases/${{ github.sha }}
ACTIVE_RELEASE_PATH: ${{ secrets.PATH }}/www/current
BASE_PATH: ${{ secrets.PATH }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME}}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT}}
envs: GITHUB_SHA,RELEASE_PATH,ACTIVE_RELEASE_PATH,BASE_PATH
script: |
echo before activating release
# Link the server's pointer to the new release folder
# Link the uploads folder with the new release folder
# Link the languages folder with the new release folder
# Link the base theme folder with the new release folder
# Link the htaccess with the new release folder
# Link the wp-config with the new release folder
activate-release:
name: 'Activate release'
runs-on: ubuntu-latest
needs: [prepare-release-on-servers, run-before-hooks]
steps:
- name: Activate release
uses: appleboy/ssh-action@master
env:
GITHUB_SHA: ${{ github.sha }}
BASE_PATH: ${{ secrets.PATH }}
RELEASE_PATH: ./releases/${{ github.sha }}
ACTIVE_RELEASE_PATH: ${{ secrets.PATH }}/www/current
ACTIVE_WP_CONTENT_PATH: ${{ secrets.PATH }}/www/releases/${{ github.sha }}/wp-content
UPLOADS_PATH: ../../static/uploads/
LANG_PATH: ../../static/languages/
BACKUP_THEME_PATH: ../../../static/themes/twentytwentythree/
HTACCESS_PATH: ../static/.htaccess
WP_CONFIG_PATH: ../static/wp-config.php
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME}}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT}}
envs: GITHUB_SHA,BASE_PATH,RELEASE_PATH,ACTIVE_RELEASE_PATH,ACTIVE_WP_CONTENT_PATH,UPLOADS_PATH,LANG_PATH,BACKUP_THEME_PATH,HTACCESS_PATH,WP_CONFIG_PATH
script: |
cd $BASE_PATH/www
ln -s -n -f $RELEASE_PATH $ACTIVE_RELEASE_PATH
cd $ACTIVE_WP_CONTENT_PATH
ln -s -n -f $UPLOADS_PATH ./uploads
ln -s -n -f $LANG_PATH ./languages
cd $ACTIVE_WP_CONTENT_PATH/themes
ln -s -n -f $BACKUP_THEME_PATH ./twentytwentythree
cd $ACTIVE_RELEASE_PATH
ln -s -n -f $HTACCESS_PATH .htaccess
ln -s -n -f $WP_CONFIG_PATH wp-config.php
# Not really useful but it's good to have it just in case
run-after-hooks:
name: 'After hook'
runs-on: ubuntu-latest
needs: [prepare-release-on-servers, run-before-hooks, activate-release]
steps:
- name: Run after hooks
uses: appleboy/ssh-action@master
env:
GITHUB_SHA: ${{ github.sha }}
RELEASE_PATH: ${{ secrets.PATH }}/www/releases/${{ github.sha }}
ACTIVE_RELEASE_PATH: ${{ secrets.PATH }}/www/current
BASE_PATH: ${{ secrets.PATH }}
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME}}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT}}
envs: GITHUB_SHA,RELEASE_PATH,ACTIVE_RELEASE_PATH,BASE_PATH
script: |
echo after activating release
# Keep only the last two releases & artifacts (by date)
# In the /releases/ folder, delete only folders that are not 'static'
clean-up:
name: 'Clean up'
runs-on: ubuntu-latest
needs: [prepare-release-on-servers, run-before-hooks, activate-release, run-after-hooks]
steps:
- name: Clean old releases and artifacts
uses: appleboy/ssh-action@master
env:
RELEASES_PATH: ${{ secrets.PATH }}/www/releases
ARTIFACTS_PATH: ${{ secrets.PATH }}/artifacts
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME}}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT}}
envs: RELEASES_PATH,ARTIFACTS_PATH
script: |
cd $RELEASES_PATH && ls -d -t -1 */ | grep -v "static" | tail -n +3 | xargs rm -rf
cd $ARTIFACTS_PATH && ls -t -1 | tail -n +3 | xargs rm -rf
Understanding the final server structure
The server file structure plays a crucial role in how the deployment process is managed and executed. Here’s a brief overview of the structure:
.
โโโ your server/
โโโ artifacts/
โ โโโ artifact1.tar.gz
โ โโโ artifact2.tar.gz
โโโ www/
โ โโโ releases/
โ โ โโโ artifact1/
โ โ โ โโโ wp-config.php(symlink to static files)/
โ โ โ โโโ .htaccess(symlink to static files)/
โ โ โ โโโ wp-content/
โ โ โ โ โโโ uploads(symlink to static files)/
โ โ โ โ โโโ languages(symlink to static files)/
โ โ โ โ โโโ other-folders.../
โ โ โ โโโ wp-includes/
โ โ โ โโโ wp-admin/
โ โ โ โโโ other-files.php...
โ โ โโโ artifact2/
โ โ โ โโโ same structure as above
โ โ โโโ static/
โ โ โโโ .htaccess
โ โ โโโ wp-config.php
โ โ โโโ languages/
โ โ โโโ uploads/
โ โ โโโ themes/twentytwentythree
โ โโโ current(symlink to latest release)/
โโโ .htaccess
- Artifacts Directory: Stores compressed deployment archives. The workflow manages these artifacts to ensure that only the latest versions are retained, preventing storage bloat.
- Releases Directory: Contains unpacked versions of each release. Each release directory includes symbolic links to static files, ensuring consistency across deployments.
- Static Directory: Houses files that remain constant across deployments, such as uploads, translation files, fallback themes,
.htaccess, andwp-config.php. - Current Directory: A symlink that points to the latest active release, enabling seamless transitions between different versions of the site.
Note to Host:
Please ensure that the server’s host configuration is set to point to the current/ directory within the www/ folder instead of the root directory. This setup allows the server to always serve the latest active release of the WordPress site. Adjusting the document root in your server’s configuration (e.g., Apache’s DocumentRoot or Nginx’s root directive) to your-server/www/current will facilitate this arrangement.
Final thoughts
This deployment script, in conjunction with the structured server setup, provides an efficient and reliable method for deploying WordPress sites. By automating the build, package, upload, and activation processes, development teams can ensure that their sites are deployed consistently, with minimal manual intervention. This approach not only saves time but also reduces the potential for errors, making it an essential tool for modern WordPress development workflows.

Leave a Reply