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