I use Heroku and its Review Apps functionality to deploy every Pull Request of a web application project. This way any changes can be reviewed before merging and deploying to production (or staging) version.
Heroku automatically creates a deployment in my GitHub repository for each Review App and updates its state whenever the deployment succeeds or fails. However, when the deployment fails, the corresponding Pull Request can still be merged!
In this blog post, we will see how to create a GitHub Action that automatically translates a deployment status into a status check for the corresponding PR, so you can for example forbid it from being merging using required status checks.
Existing solutions
There are already some GitHub Actions that try to solve this problem. For example, Heroku Review App Deployment Status. However, they use polling HTTP requests to wait until the deployment succeeds or fails. And that costs you Action minutes. My complex web project takes approx. 10 minutes to build in Heroku, so this solution would be unfeasible.
The aforementioned Action’s authors propose a solution where you need to change your Heroku deployment script so that it creates a status check in your GitHub repository when it finishes. However, that seems overly complicated and I propose alternative easier approach here.
My solution
We can use the deployment_status
trigger to execute a GitHub Action only after the Heroku deployment finishes.
Here is the code (place it for example into .github/workflows/heroku.yml
):
name: Check Deployment Status
on: deployment_status
jobs:
deployment-status:
runs-on: ubuntu-latest
# Continue only if some definitive status has been reported.
if: github.event.deployment_status.state != 'pending'
steps:
# Forward deployment's status to the deployed commit.
- uses: octokit/request-action@v2.0.23
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
route: POST /repos/:repository/statuses/${{ github.event.deployment.sha }}
repository: ${{ github.repository }}
state: ${{ github.event.deployment_status.state }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
description: >
${{ format('Heroku deployment is {0}', github.event.deployment_status.state) }}
context: deployment-status
It simply forwards the status of a Heroku deployment (success
or failure
) into status of the Pull Request’s last commit.
And it only runs 20 seconds on average, that is a huge improvement over 10 minutes, right?
Bonus: Health check
As you might have noticed from the screenshot in the introduction, I also use separate health-check step to verify that the web application actually runs successfully after deployment.
Its code is below (you can simply append it to the same file, e.g., .github/workflows/heroku.yml
):
health-check:
runs-on: ubuntu-latest
# Run health check only if deployment succeeds.
if: github.event.deployment_status.state == 'success'
# Check that the deployed app returns successful HTTP response.
steps:
- id: health_check
uses: jtalk/url-health-check-action@v1.3
with:
url: ${{ github.event.deployment.payload.web_url }}
follow-redirect: true
max-attempts: 4
retry-delay: 30s
continue-on-error: true
# Set appropriate status to the deployed commit.
- uses: octokit/request-action@v2.0.23
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
route: POST /repos/:repository/statuses/${{ github.event.deployment.sha }}
repository: ${{ github.repository }}
state: ${{ steps.health_check.outcome }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
description: >
${{ format('Site health is {0}', steps.health_check.outcome) }}
context: health-check
Limitations
Note that the presented solution is not limited to Heroku deployments, it should work with any service that publishes deployment statuses to GitHub. However, the solution has one limitation (that I am aware of) which I will try to address in some future post. Namely, commit hashes are not checked, so if for example deployment succeeds but Heroku Release Phase fails, health check succeeds anyway (with previously deployed version of the branch if it exists).
Let me know what you think and feel free to ask questions about this post in comments below.