Automatically build and deploy projects with GitHub Actions

In an effort to consolidate and save a little bit of money I’ve recently moved all my projects off DeployHQ and moved them to a free alternative: GitHub Actions.

I work almost exclusively on WordPress plugins and themes that use a combination of Composer and Node scripts, and I want to automatically run these scripts and deploy the built files to my server over SSH. In this post, I’ll share how I accomplished this with GitHub Actions in just a few lines of code.

In your GitHub repository, create a new file in the .github/workflows folder called deploy.yml with the following content:

name: Deploy
on:
    push:
        branches:
            - main
jobs:
    build:
        runs-on: ubuntu-latest
        steps:
            - name: Checkout code
              uses: actions/checkout@v3
            - name: Setup PHP
              uses: shivammathur/setup-php@v2
              with:
                  php-version: '8.3'
                  coverage: none
            - name: Install Composer dependencies
              run: composer install --no-dev
            - name: Setup Node.js
              uses: actions/setup-node@v3
              with:
                  node-version: '18'
            - name: Install npm dependencies
              run: npm install
            - name: Build
              run: npm run build
            - name: Deploy to server via SSH
              env:
                  SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
                  SERVER_USERNAME: ${{ secrets.SSH_USER }}
                  SERVER_HOST: ${{ secrets.SSH_SERVER }}
                  DEPLOY_DIR: ${{ secrets.SSH_PATH }}
              run: |
                  mkdir -p ~/.ssh
                  echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
                  chmod 600 ~/.ssh/id_rsa
                  ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts

                  rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no" --exclude-from='.distignore' ./ $SERVER_USERNAME@$SERVER_HOST:$DEPLOY_DIR

If the .github/workflows folder doesn’t already exist you can create it yourself by running this command in the root of your repository:

mkdir -p .github/workflows

This action takes a few different steps in order, whenever a new commit is pushed to main:

  1. Check out the current repository
  2. Install PHP (you can replace 8.3 with your desired PHP version)
  3. Install Composer dependencies (uses --no-dev which ensures no dev dependencies get installed)
  4. Install npm
  5. Run the npm build script (replace npm run build with your own build script if necessary)
  6. Use rsync to copy files to your remote server over SSH

To make this work, you need to set a few secrets in the repository settings.

Screenshot of the GitHub repository settings showing a list of secrets.
Go to your repository settings, click “Actions” under “Secrets and variables” and click “New repository secret” to add a new secret to the repository.

Set the following secrets:

SSH_SERVERthe IP address of your server
SSH_PATHthe path to upload the files to (.e.g ~/files/wp-content/plugins/my-plugin/)
SSH_USERthe username you use to SSH into your server
SSH_KEYthe private key you use to connect to your server

rsync only supports keys in PEM format, which you can generate using this command:

 ssh-keygen -m PEM -t rsa -b 4096

This generates a public/private key pair. The private key, which starts with -----BEGIN RSA PRIVATE KEY----, needs to be set as the SSH_KEY repository secret, and the public key needs to be added to the ~/.ssh/authorized_keys file on your server in order to connect.

The last rsync step looks a little complicated, but is actually fairly straightforward. It creates a new ~/.ssh folder (where your SSH keys are stored), copies the private key to this folder, gives it the correct permissions and connects to your server to get the known hosts.

It then copies the files over using rsync, while ignoring any files that are present in the .distignore file in your repository root. This is super useful to prevent copying over files you don’t want or need. Here’s an example:

bin
node_modules
src
.*
composer.json
composer.lock
license.txt
README.md
package.json
package-lock.json
webpack.config.js

Add the .distignore file to your own repository root and choose which files and folders to ignore— these will not be copied over when the action is run.

.* means “any file or folder that starts with a .“, so it automatically ignores all dotfiles such as .editorconfig as well as hidden folders like .github and .vscode.

If you want to include specific dotfiles (for example .env), you can add this on the line below: !.env

Hopefully this helps you deploy your own projects using GitHub Actions! If you want to learn more about actions and see what else is possible I encourage you to check out the official docs.

Written by Daniel Post

Hi! I’m Daniel Post, a freelance full-stack WordPress developer from the Netherlands. This is my personal website, where I share articles and guides related to WordPress.

I am also available for hire, so if you’re looking for a developer for your next project feel free to get in touch!


Comments

Join the conversation on Bluesky

  1. Emir OGUZ
    Emir OGUZ @ranork.bsky.social

    That’s awesome! GitHub Actions is so powerful. Congrats on the move! Maybe we could collaborate on a post sometime?

    April 12, 2025
  2. DeployHQ
    DeployHQ @deployhq.com

    Good to hear you’ve found a cost-effective solution! If you ever need help with more advanced deployment workflows, especially zero-downtime, we’re here to help 🙂

    April 12, 2025
    1. Daniel Post
      Daniel Post @danielpost.com

      Thank you! I’ve had nothing but good experiences with your service—it was just overkill for what I need. Would definitely use it for anything more complex or mission-critical.

      April 12, 2025

Leave a Reply

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