An improved GitHub deployment workflow

A couple of days ago I blogged about how to deploy SvelteKit websites to a digital ocean droplet. Near the end of that post I offered a github workflow file but noted that it was basic and could be improved. Well here it is...

name: Build & Deploy
on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout main branch
        uses: actions/checkout@v4

      - name: Install SSH Key
        uses: shimataro/ssh-key-action@v2
        with:
          key: ${{ secrets.SSH_KEY }}
          known_hosts: "unnecessary"

      - name: Adding Known Hosts
        run: ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy with rsync
        run: rsync -avz --delete . ${{ secrets.SSH_USERNAME }}@${{ secrets.SSH_HOST }}:website/${GITHUB_SHA}

      - name: Build site
        uses: appleboy/ssh-action@master
        env:
          GITHUB_SHA: ${GITHUB_SHA}
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          envs: GITHUB_SHA
          script: |
            cd ~/website/${GITHUB_SHA}
            nvm use || nvm install $(cat .nvmrc)
            npm install
            npm run build
            cd ~/website
            rm ~/website/current 2> /dev/null
            ln -s ~/website/${GITHUB_SHA} ~/website/current
            cd ~/website/current
            pm2 reload website || pm2 start server.js --name website
            cd ~/website
            ls -dt */ | tail -n +3 | xargs rm -rf

The improvements come as follows:

Line 30: Instead of copying the files dfirectly into the "website" folder, we now copy them into a folder inside of the website folder that is named with the commit hash id e.g. "website/9087191712783b40dc975c2f646d7c377ca98b51"

Lines 34, 35, and 40: We pass the GITHUB_SHA environment variable down through the action so that we can use it in our script on the server.

Lines 42 - 45: we move into this new "website/9087191712783b40dc975c2f646d7c377ca98b51" folder and do our build inside there instead of directly inside the website folder.

Line 47: we remove the "current" symbolic link and fail silently if it couldn't be removed due to it not existing (which, will be the case the first time).

Line 48: we create a symbolic link from the "website/9087191712783b40dc975c2f646d7c377ca98b51" folder to "website/current".

Lines 49 and 50: we then move into the "current" folder (which is really just the "website/9087191712783b40dc975c2f646d7c377ca98b51" folder) and then reload pm2 so that it will pick up the new files.

Lines 51-52: We then finish off by moving into the "website" directory, listing the files in time order and removing anything that is not in the most recent "3" files. This should leave behind the "current" symbolic link, the folder that has the latest files in it and a folder with the previous version in, just in case you need to rollback quickly by deleting the symbolic link and adding a new one to the old folder.

You might be wondering why I deleted the symbolic link and then made a new one instead of just using ln -sf to overwrite the existing one. I did it this way to ensure the created at timestamp is "now" otherwise the "current" folder might end up being deleted by that final line which would result in the website going offline.

James