09 May 2021 — Comments
In the last couple of days I have been playing around with the idea of combining GitHub Actions and GitHub Pages to be able to dynamically build preview environments every time a pull request is opened against a repository hosting a client-side web app.
The idea is that you can quickly preview the changes made by some contributor, without having to clone their branch and running everything locally, something specially useful for open source projects.
In this article I will explain how to set everything up, and what are some considerations and problems you may face during the process.
The first thing you need to do is enable your project to publish pages on some branch. I recommend creating a new branch for this, let's say preview-env
, and then enabling publishing on it.
In order to do that go to your repository and then Settings -> Pages, look for the Source section, and you should see something like this:
Click in the dropdown and select your new branch, preview-env
. Then click Save.
After saving you will see a message at the top of this section telling you what's the URL in which the pages are published. It always follows the same pattern. If your organization is my-org
and your repository is my-repo
, the url will be https://my-org.github.io/my-repo/
.
Now we need to configure a workflow that will "deploy" the preview environment for the branch every time a new pull request is created or updated:
Let's imagine we have a React web app that is built with node.js. The configuration could look like this.
# .github/workflows/deploy-preview.yml
name: Deploy preview
on:
pull_request: null
jobs:
deploy:
runs-on: ubuntu-20.04
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use node.js 14.15
uses: actions/setup-node@v1
with:
node-version: 14.15
- name: Generate slug
id: generate_slug
run: echo "##[set-output name=slug;]$(echo ${GITHUB_HEAD_REF#refs/heads/} | sed -r 's/[~\^]+//g' | sed -r 's/[^a-zA-Z0-9]+/-/g' | sed -r 's/^-+\|-+$//g' | tr A-Z a-z)"
- name: Build
run: npm ci && npm run build
- name: Deploy
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
branch: preview-env
folder: build
target-folder: ${{ steps.generate_slug.outputs.slug }}
- name: Publish env
uses: marocchino/sticky-pull-request-comment@v2
with:
header: Preview environment
message: |
## Preview environment
https://my-org.github.io/my-repo/${{ steps.generate_slug.outputs.slug }}/
If you are not familiar with GitHub Actions yet, take a look at the official documentation.
This workflow will follow the next steps:
preview-env
(remember we enabled GitHub pages for this branch).The steps may change depending on your needs and how you build your project. For example, I have an extra step to update a property in the package.json
that is then used to allow serving the app in a sub-path instead of the root of the domain.
You may also not build your site with node.js, as you could be using any of the multiple existing static site generators out there, but you get the idea.
Finally, you have probably noticed the config includes continue-on-error: true
. This will allow the pull request to be merged even if this process fails for some reason.
While the configuration above will work perfectly when creating pull requests inside the same repository, there are some limitations when the pull request comes from a fork (which is the usual in OSS projects).
For security reasons, the GITHUB_TOKEN
injected in those cases doesn't have write permissions, which will make the "deploy" step fail.
Also, you cannot define your own token with more permissions and inject it as a secret, as the secrets are not exposed to workflows running on forks.
However, with a couple small config changes, we can work around these limitations:
The first thing we need to do is use the pull_request_target
event instead of the pull_request
one.
on:
- pull_request: null
+ pull_request_target: null
According to the documentation, this is equivalent to the pull_request
event, but it runs in the scope of the base repository.
Thanks to this, the GITHUB_TOKEN
will come with write permissions, and we will have access to secrets if needed.
The second thing we are going to change is that we now need to specify the repository to checkout.
In order to do that, we need to add two inputs to the "Checkout code" step, like this:
steps:
- name: Checkout code
uses: actions/checkout@v2
+ with:
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ ref: ${{ github.event.pull_request.head.ref }}
This will ensure we checkout from the fork repository, instead of the base one, which is the default, as it's the one where the pull_request_target
event is scoped to.
And that's it. With these two changes you can now deploy preview environments for pull requests coming from forks.
IMPORTANT! The limitations of the
pull_request
event are there for a reason. People could potentially get access to your secrets or manipulate your code. Proceed at your own risk.
Other than the limitations for forks, there are a few other minor things to take into consideration.