Photo of David Winter

david winter

Post-deployment triggers for PaaS providers

For really simple projects, PaaS providers such as Fly.io and render.com offer a pain-free way to get an app or static site up and running in minutes. Sometimes you may want to trigger some follow up actions after a deployment, for example, a Slack notification to notify your team that a new release is live, or some smoke tests.

The above two providers are quite different in how they handle deployments. Fly.io lets you run a command to deploy from your local machine. With render.com, it integrates with your Git provider and monitors pushes to the repository, and then initiates a deployment itself.

But with either mechanism, you want to have confidence when your change has been successfully deployed.

A number of factors might cause a delay before the change is publically visible. For example, caching by the hosting provider, or even your own caching layer, might mean that if they flag it is having been deployed, it might take a further few seconds or minutes before that is reflected across their CDN (something I’ve certainly noticed with render.com).

The examples below will offer some more concrete certainty based on Git commit hashes. Exposing and then testing your site against an expected Git hash being available is the key to this method.

Exposing the Git hash - static vs dynamic sites

A simple example for a dynamic site is to offer a version check endpoint at a known URL route. Here you allow your deployment pipeline to pass in a commit hash based on the commit that is being deployed, and the app will check this against it’s known deployed commit hash and return either:

  • a 404 status indicating that version is not yet deployed (not found)
  • a 204 success status, yet we don’t need to provide any content in the response

render.com offers an environment variable RENDER_GIT_COMMIT for the running application, and so an example looks like this for a Ruby based Sinatra app:

get '/version-check/:commit' do
    params[:commit] == ENV['RENDER_GIT_COMMIT'] ? 204 : 404
end

For a static site that is built with a tool such as Hugo, then you need to approach this a slightly different way. When the site is built, generate a static file named with the Git commit hash. We will then be able to check if the file exists, which will return either a 404, or 200 by the web server:

mkdir -p static/version-check
echo "ok" > "static/version-check/${GITHUB_SHA}"

With Hugo, files within the static directory are served as they are, unmanipulated, at the root of the project.

Both static and dynamic projects will use the URL format of:

/version-check/COMMIT_HASH

For example:

/version-check/df5222b2ac95769803a54cc44ef9698aff65be34

Testing for the presence of the correct commit

Once the PaaS solution has begun, or indicated the deployment is complete, you need to check for the presence of the latest commit hash. With GitHub Actions, you can use ${GITHUB_SHA} to determine this.

You can use the Node.js tool wait-on or curl to retry the URL until it receives a 2xx (successful) status. The curl option might be more appropriate if you don’t have or want to setup Node.js within your deployment pipeline.

For wait-on, you can use this in your pipeline run steps:

npx wait-on --timeout 600000 "https://yourapp.com/version-check/${GITHUB_SHA}"

This will wait 600,000 miliseconds, or 10 minutes, before exiting in failure.

And for curl, you can use:

curl --head --retry 600 --retry-all-errors --fail --retry-delay 1 "https://yourapp.com/version-check/${GITHUB_SHA}"

After these commands have run successfully, then you’re free to run any subsequent commands that you want to trigger.

GitHub Actions full examples

For my blog, I use something similar to the below for a Hugo build handled on Fly.io:

name: deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        with:
          # Hugo uses submodules for themes:
          submodules: 'recursive'
          # I use `enableGitInfo` for Hugo:
          fetch-depth: 0

      - name: create version check file
        run: |
          mkdir -p static/version-check \
            && echo "ok" > "static/version-check/${GITHUB_SHA}"          

      - uses: superfly/flyctl-actions/setup-flyctl@master

      - name: deploy to fly
        run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

      - uses: actions/setup-node@v3
        with:
          node-version: 18

      - name: wait for deployment to be active
        run: npx wait-on --timeout 600000 "https://davidwinter.dev/version-check/${GITHUB_SHA}"
        id: deploy_wait
        continue-on-error: true

      - name: send Telegram success message
        if: steps.deploy_wait.outcome == 'success'
        uses: appleboy/telegram-action@v0.1.1
        with:
          to: ${{ secrets.TELEGRAM_CHAT_ID }}
          token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          format: markdown
          message: |
            🎉 davidwinter.dev deployed!

            Git SHA: `${{ github.sha }}`

            https://davidwinter.dev            

      - name: send Telegram fail message
        if: steps.deploy_wait.outcome != 'success'
        uses: appleboy/telegram-action@v0.1.1
        with:
          to: ${{ secrets.TELEGRAM_CHAT_ID }}
          token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          format: markdown
          message: |
            🚨 davidwinter.dev failed to deploy

            Git SHA: `${{ github.sha }}`            

And for a dynamic site deployed to render.com, you can see a small example application written in Ruby with Sinatra here: https://github.com/davidwinter/render-post-deploy