skip to content
blog.metters.dev
Table of Contents

Prerequisites

  • The repository must be stored on GitHub to make use of GitHub Action workflows
  • Some of the steps in the workflow are specific to my site/project, e.g., the node version, the package manager (pnpm), …

The process is actually nearly identical to what is documented in this previous post. Instead of the magic happening on your laptop/pc, it happens on a GitHub runner. To protect your private key or other sensitive information from being leaked accidentally, you need to store them manually as secrets on GitHub.

Creating the workflow file - step by step

⬇️ Scroll to the full content of the workflow at the end of this post.

  1. Create .github/workflows/deploy.yml in your project
    name: 'Deploy artefact'
  2. Add two triggers to the workflow: manual and on each change on your main branch:
    name: 'Deploy artefact'
    on:
    workflow_dispatch:
    push:
    branches: [ main ]
  3. Add a job and name it, e.g. deploy. This job runs on Ubuntu and consists of several steps.
    name: 'Deploy artefact'
    on:
    workflow_dispatch:
    push:
    branches: [ main ]
    jobs:
    deploy:
    runs-on: ubuntu-latest
    steps:
  4. Add a step to check out the repository code into your runner. It needs access to the source code, so it can build the artefact and deploy it.
    jobs:
    deploy:
    runs-on: ubuntu-latest
    steps:
    - name: 'Check out repository'
    uses: actions/checkout@v4
  5. Add a step to install node, so the runner can execute JavaScript tooling
    - name: 'Install node'
    uses: actions/setup-node@v4
    with:
    node-version: 18
    registry-url: 'https://npm.pkg.github.com'
  6. Add a step to install pnpm. The runner uses it to run the package manager, e.g. to install dependencies.
    - name: 'Install pnpm'
    uses: pnpm/action-setup@v4
    with:
    version: 8.6.1
    run_install: false
  7. Add steps to install all dependencies and then build via shell command
    - name: 'Install dependencies'
    run: pnpm install
    - name: 'Build artefact'
    run: pnpm build
  8. Add a step to read secrets and variables and then execute deployment via rsync
    - name: 'Prepare secrets for ssh connection and execute deployment'
    env:
    DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
    SSH_PASS: ${{ secrets.SSH_PASS }}
    USER: ${{ vars.REMOTE_USER }}
    HOST: ${{ vars.REMOTE_HOST }}
    DIR: ${{ vars.REMOTE_DIR }}
    run: |
    mkdir -p ~/.ssh
    echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
    chmod 600 ~/.ssh/id_ed25519
    ssh-keyscan -t ed25519, "$HOST" >> ~/.ssh/known_hosts
    eval "$(ssh-agent -s)"
    echo "$SSH_PASS" | ssh-add ~/.ssh/id_ed25519
    rsync -avzr -e 'ssh -o StrictHostKeyChecking=no -v' --delete ./dist/ "${USER}"@"${HOST}":/var/www/html/"${DIR}"

Analysing ssh connection setup and deployment

This last step is the most complex one, so let’s break down what’s going on. Assign the values of the repository variables/secrets to the variables in your workflow:

Variable assignment
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
SSH_PASS: ${{ secrets.SSH_PASS }}
USER: ${{ vars.REMOTE_USER }}
HOST: ${{ vars.REMOTE_HOST }}
DIR: ${{ vars.REMOTE_DIR }}

Next, store the ssh keys on the runner (which is also an Ubuntu machine). Therefore, use terminal commands to create a folder .ssh and then write the private ssh key in a file, e.g. named id_ed25519. Make sure, only the owner (the user executing these commands) may read/write the file.

Terminal window
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519

Add the ssh key to the known_hosts file to verify the host’s ($HOST) identity:

Terminal window
ssh-keyscan -t ed25519, "$HOST" >> ~/.ssh/known_hosts

Before an ssh connection can be opened, the ssh agent needs to be started. Then, add the private key together with its corresponding passphrase to the ssh agent:

Terminal window
eval "$(ssh-agent -s)"
echo "$SSH_PASS" | ssh-add ~/.ssh/id_ed25519

Finally, use rsync to deploy the artefact to the configured destination.

  • rsync -avzr transfers files recursively but also compresses them
  • -e 'ssh -o StrictHostKeyChecking=no -v' is important to force the ssh agent to accept new hosts without prompt!
  • --delete to delete old files
  • ./dist/ is the source directory, containing the artefact
  • "${USER}"@"${HOST}":/var/www/html/"${DIR}" is the destination of the deployment - the full path to the directory on your vps, which hosts the artefact
Terminal window
rsync -avzr -e 'ssh -o StrictHostKeyChecking=no -v' --delete ./dist/ "${USER}"@"${HOST}":/var/www/html/"${DIR}"

Unfortunately, the preparation of the ssh connection and the actual deployment cannot be split up into two separate steps. Each GitHub Actions step runs in its own (non-interactive) shell process, so environment variables and the ssh agent won’t be available any more in a later step.

Adding the variables and secrets to GitHub

Merging the workflow above into your main branch will automatically trigger a run on each change. You may also manually trigger the workflow on any branch of your choice that contains the workflow file. However, you must add the values to each variable/secret, otherwise the run will fail.

Store two secrets and three variables in GitHub:

  1. DEPLOY_KEY: the private ssh key necessary to access the vps via ssh
  2. SSH_PASS: the corresponding passphrase for the private key
  3. USER: name of the user the private ssh key belongs to
  4. HOST: ip address of the vps
  5. DIR: name of the directory on the vps that contains the artefact

You may add environment or repository variables. With the workflow file set up as described above, you’ll need to add your key value pairs as repository variables/secrets.

Screenshots on how to add secrets/variables in GitHub

secrets ➡️ Navigate to https://github.com/{YOUR_USERNAME}/{YOUR_REPOSITORY_NAME}/settings/secrets/actions to add secrets

variables ➡️ Navigate to https://github.com/{YOUR_USERNAME}/{YOUR_REPOSITORY_NAME}/settings/variables/actions to add variables

If you decide to use environment variables, define the environment in your workflow file:

Define the environment, e.g. PROD
jobs:
deploy:
runs-on: ubuntu-latest
environment: PROD
steps:

The destination of your deployment is the actual directory that contains the static site (the artefact). In the following snippet, I connected to my vps via ssh, navigated into /var/www/html/metters.dev, and then listed the contents:

Location of the deployed artefact on your vps
metters@ubuntu-vps:/var/www/html/metters.dev$ ll
total 244
drwxr-xr-x 11 metters root 4096 Sep 7 14:08 ./
drwxr-xr-x 9 metters root 4096 Aug 10 13:01 ../
drwxr-xr-x 2 metters root 4096 Sep 7 14:08 _astro/
-rw-r--r-- 1 metters root 31209 Sep 7 14:08 index.html
drwxr-xr-x 43 metters root 4096 Sep 7 14:08 posts/
...

Troubleshooting

Why can I not find my newly created workflow to manually trigger it?

An ‘instance’ of the workflow must first exist on the main branch of your repository. After you have merged your workflow into your main branch, it will show up in Actions

But I don’t want to test my workflow on main

  1. Add a ‘dummy’ workflow on your main branch. For a minimal run:
    • Add a manual trigger
    • Add a single step, e.g. let it print out “Hello, world!” via echo
  2. Create a feature branch from main
  3. Develop and test your workflow on that feature branch. You might want to update the destination of the deployment, to avoid deploying an artefact that’s under development on PROD.

The step to install the dependencies or build the artefact fails

  • Read the error logs of the failed run
    • Are you using the correct package manager?
    • Is its version correct?

The deployment step fails

  • Check whether you have properly stored your repository variables and secrets
  • Check whether the user that technically executes the deployment (the owner of the private key) exists on your vps.
  • Check whether that user has ownership of the directories on your vps
  • Check the name of the folder containing the build artefact and update the name in your deployment step accordingly. Common names are dist, build, target, …
  • Read the error logs of the failed run

Full workflow ‘Deploy artefact’

⬆️ Back to the top

name: 'Deploy artefact'
on:
workflow_dispatch:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: 'Check out repository'
uses: actions/checkout@v4
- name: 'Install node'
uses: actions/setup-node@v4
with:
node-version: 18
registry-url: 'https://npm.pkg.github.com'
- name: 'Install pnpm'
uses: pnpm/action-setup@v4
with:
version: 8.6.1
run_install: false
- name: 'Install dependencies'
run: pnpm install
- name: 'Build artefact'
run: pnpm build
- name: 'Prepare secrets for ssh connection and execute deployment'
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
SSH_PASS: ${{ secrets.SSH_PASS }}
USER: ${{ vars.REMOTE_USER }}
HOST: ${{ secrets.REMOTE_HOST }}
DIR: ${{ vars.REMOTE_DIR }}
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -t ed25519, "$HOST" >> ~/.ssh/known_hosts
eval "$(ssh-agent -s)"
echo "$SSH_PASS" | ssh-add ~/.ssh/id_ed25519
rsync -avzr -e 'ssh -o StrictHostKeyChecking=no -v' --delete ./dist/ "${USER}"@"${HOST}":/var/www/html/"${DIR}"

Sidenote

If you store some text as a secret and also a variable with identical content, the variable will also be redacted from the logs. I randomly set up a folder name as secret, but it was the same content as the domain. When I analysed my log output the values were redacted which was confusing at first.