Deploy previews of branches to gh-pages
I love GitHub Pages for it’s simplicity.
I use it for simple websites and blogs (like this page). It works great for style-guides and component libraries that I build for my clients.
And it’s also great for single page applications (SPAs).
No additional services needed, just works, awesome!
The Problem
Normally only one Source (branch
or directory
) is deployed to production
.
But when you work with a team you might want to use feature branches for work in progress or spikes.
Wouldn’t it be great when these branches would also be deployed as a preview so that I can show my WIP to colleagues and get feedback?
Spoiler: The answer is “Yes of course!”
TL;DR
See example-ghpage-feature-preview repo for the complete setup.
What we’ll build
In this article I’ll walk you through the setup of…
- A GitHub repository
- with GitHub Actions
- running a build-step (jekyll in this case) (could be anything generating a deployable page)
- deploying it’s production branch to
[GITHUB_USER].github.io/[PROJECT_NAME]
(Can be changed with CNAME) - creating previews for other branch on
[GITHUB_USER].github.io/[PROJECT_NAME]/preview/[branchname]
- cleaning up previews of deleted or merged branches
I assume you’re familiar with git, GitHub and have a brief understanding of building software in continuous integration environments.
Whenever you see a [YOUR_...]
notation, that’s a placeholder that you should
replace including the []
braces.
This example uses main
as the production branch and gh-pages
for GitHub Pages. It’s not required to use these branch names.
Create the project locally and push it to GitHub
- Create a new jekyll project:
gem install bundler jekyll jekyll new [YOUR_PROJECT_NAME] cd [YOUR_PROJECT_NAME] git init git checkout -b main git add . git commit -m'initial commit'
- Create a new empty repository on github
- Copy the remote url
- Push the initial project to Github:
git remote add origin [YOUR_REMOTE_URL] git push origin main -u
- Also create a new empty branch that we’ll use for github pages
git checkout --orphan gh-pages git rm --cached -r . git commit --allow-empty -m'init' git clean -df git push origin gh-pages git checkout main
- Enable GitHub pages for the
/ (root)
ofgh-pages
branch under theSettings
of your repository
Create a github workflow to build the sites artifacts in CI
Sidetrack: why a custom build?
You might now think: Aren’t we creating a jekyll page? Why should we build it ourself instead of using the builtin jekyll that GitHub is using for pages?
- In this article jekyll serves as a real-life placeholder for hugo, Next.js, or any other static page generator.
- Even with jekyll, since we’ll be hosting multiple versions of the page we need to build each one and move it to the right place on the
gh-pages
branch - In fact we’ll disable the default jekyll build
</Sidetrack>
Add a .github/workflows/deploy.yml
file to your project
name: deploy
on: push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-ruby@v1
with:
ruby-version: "2.x"
- name: bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: jekyll build
run: |
bundle exec jekyll build
touch _site/.nojekyll
Once this is committed and pushed to github you should see a deploy workflow under
the Actions
tab of your repository. Currently it will only build the jekyll page
and do nothing with it…
Update gh-pages branch with our build artifacts
jekyll builds the page to a _site
folder. This is the folder we
want to move to gh-pages
branch.
Add the following lines to .github/workflows/deploy.yml
# ... config to build your page to _site
- uses: actions/setup-node@v1
with:
node-version: 12.x
- name: deploy gh-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DEST=.
git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/[YOUR_GITHUB_USER]/[YOUR_REPO_NAME].git
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
npx gh-pages\
--branch gh-pages\
--dist _site\
--dest $DEST\
--add\
--dotfiles
Make sure to update
[YOUR_GITHUB_USER]
and[YOUR_REPO_NAME]
to fit your setup. You can also use your email and user instead of thegithub-actions[bot]
Once this is running, it will put the latest build to gh-pages
branch.
And you should be able to visit the deployed page to see your site.
Create previews for feature branches
At this point each commit will update your page. Also WIP-features on a unmerged branch. Leading to some sort of race-condition.
In order to solve this we will put the artifacts of all branches except main
to a preview/[BRANCHNAME]
subfolder.
For this to work we need to update the build step to make the pages aware of their
new, nested location. This will cause internal links to point to /preview/[branchname]/link
instead of back to the production deploy of main
.
- name: jekyll build
run: |
BRANCH=${GITHUB_HEAD_REF##*/}
PROD_URL=[YOUR_DEFAULT_BASE_URL]
BASE_URL=$([ "$BRANCH" == "main" ] && echo $PROD_URL || echo "$PROD_URL/preview/$BRANCH")
bundle exec jekyll build --baseurl $BASE_URL
touch _site/.nojekyll
Next we configure the gh-pages deploy to move the _site
folder to different destinations
based on the current branch.
- name: deploy gh-pages
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BRANCH=${GITHUB_HEAD_REF##*/}
DEST=$([ "$BRANCH" == "main" ] && echo "." || echo "preview/$BRANCH")
# git remote set-url ...
Now when you branch of from main
, call it test
, make some changes and push the test
branch to GitHub, it will be deployed under [GITHUB_USER].github.io/[PROJECT_NAME]/preview/test
Clean up previews of merged or deleted branches
Now that this is working we might want to clean up old previews once the branch has been deleted or merged to production.
For this we’ll add a custom beforeAdd
script to gh-pages that checks our existing preview folders and removes those
that do not have a unmerged remote branch counterpart.
Replace the npx gh-pages\ ...
call with the following:
npm i cleanup-gh-pages-previews
npx gh-pages\
--branch gh-pages\
--beforeAdd cleanup-gh-pages-previews\
--dist _site\
--dest $DEST\
--add\
--dotfiles
Warning: cleanup-gh-pages-previews
is a highly specific implementation
tailored for setups like this one. See implementation to check if it fits your needs.
Bonus: Automatically add preview-links to pull requests
Now that everything works, wouldn’t it be great to automatically add a link to Pull Requests?
Memo to self: less rhetorical questions
We need to change the action so that it also runs on pull-requests to the main
branch otherwise the PR-id can not be found.
Also in order to not have duplicated runs we should also limit the builds on push to only main
.
name: deploy
# on: push
on:
push:
branches:
- main
pull_request:
branches:
- main
# rest of the config ...
Now we can add another step at the bottom of the config
- name: decorate PR
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npx decorate-gh-pr -r -c "<a href=\"[YOUR_GITHUB_USER].github.io/[YOUR_PROJECT_NAME]/preview/${GITHUB_HEAD_REF##*/}\"><img src=\"https://img.shields.io/badge/published-gh--pages-green\" alt=\"published to gh-pages\" /></a><hr />"
And that’s it!
See deploy.yml
of my example repository
for the optimized version that includes caching of dependencies.
Was this post valuable for you?
Cool! Here is how you can give back if you want to: (only pick a few 😉)
- Fix typos or improve my phrasing 💖
- Share the article to people that might also like it
- Follow me on Twitter
- Sponsor me on GitHub
- Hire me to build or improve your continuous deployment or recommend me
- Spread love, peace and sanity
- Act to save our planet
- Be inclusive to everyone but the intolerant (because the paradox of tolerance is a thing).