Using GitHub Actions for Pull Request Staging Environments, Part 4: Automated Deploy and Teardown
• CommentsAutomating Deploy
We currently have slash commands /deploy
and /teardown
that are converted into repository_dispatch
(deploy-command
/ teardown-command
) events, and those repository_dispatch
events deploy or tear down a staging environment specific to a pull request. That’s pretty cool.
But what I’d really like is to deploy a PR’s staging environment as soon as the PR is created, and automatically tear it down when the PR is closed. That would be really cool.
Security (Again)
First, though, I do have to talk about security. Yes, again.
For security reasons, all secrets are unavailable when automating a pull request from a fork of a repository. This makes sense; you don’t want someone to create a fork, change the actions, and be able to retrieve your secrets by opening a PR against your repository.
Currently, this is a hard and fast rule. But there are discussions about loosening up these rules a bit:
Personally, I think it makes sense to always have pull request events run using the actions of the base branch, not the head branch. So cross-repository pull requests will always run the actions in the origin repository, not the fork repository.
However, for now, the result of this security restriction is that we can only automate local pull requests (from one branch to another in the same repository). The solution we’re using here will not work for cross-repository PRs (i.e., a PR from a forked repository).
Dispatch when a Pull Request is Opened
We already have an action that handles repository_dispatch
events of type deploy-command
, so we can just use a pull request event as a “trigger”, dispatching the same kind of command:
# Queues a deploy command for every local PR.
name: Local PR Opened/Updated
# By default, this is run when a PR is opened or synchronized.
on:
pull_request:
jobs:
dispatch-deploy-command:
if: github.repository == github.event.pull_request.head.repo.full_name # Only try to deploy local PRs
runs-on: ubuntu-latest
steps:
- name: Dispatch /deploy Command
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.DISPATCH_TOKEN }} # Same security issues as before, unfortunately
event-type: deploy-command # Send the deploy-command type for the repository_dispatch event
client-payload: '{"pull_request": ${{ toJson(github.event.pull_request) }}}' # Pass along the pull request details
The only tricky part here is the last line: our deploy-command.yml
handler expects to be able to use client_payload.pull_request.number
and client_payload.pull_request.head.sha
. client_payload.pull_request
is populated automatically by slash-command-dispatch
, but here we need to fill it out ourselves. Fortunately, the pull_request
data is already provided to us as part of the pull_request
event, so we just need to copy it over.
Dispatch when a Pull Request is Closed
Similarly, we have an action that handles repository_dispatch
events of type teardown-command
, so we can use a pull request close event to dispatch the same command:
# Queues a teardown command for every local PR closed.
name: Local PR Closed
# Only listen for close events.
on:
pull_request:
types: [ closed ]
jobs:
dispatch-teardown-command:
if: github.repository == github.event.pull_request.head.repo.full_name # Only try to tear down local PRs
runs-on: ubuntu-latest
steps:
- name: Dispatch /teardown Command
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.DISPATCH_TOKEN }} # Same security issues as before, unfortunately
event-type: teardown-command # Send the deploy-command type for the repository_dispatch event
client-payload: '{"pull_request": ${{ toJson(github.event.pull_request) }}}' # Pass along the pull request details
Done
At this point, we’re as automated as we can be (safely). All PRs support /deploy
and /teardown
commands, managing a per-PR staging environment. In addition, local PRs get their environments deployed and torn down automatically.
Hopefully in the future, we can fully automate PRs from forks. That would be especially helpful for open-source projects. In the meantime, /deploy
and /teardown
are still pretty cool.
As a final reminder, the staging environments in this example were deliberately simple: building a static site and deploying that static site. This simple example is fine for a lot of front-end projects. But staging environments can also include back-end code. There’s no reason you can’t define “deploy” to mean “deploy the front end to Surge and deploy the backend to a new Azure resource group” or something like that. So dream big!
Enjoy GitHub Actions!