Don’t hesitate to contact us if you have any feedback.

Automatic WordPress release with GitHub Actions

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, and wp-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.


Comments

Leave a Reply

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