Deploying to GitHub Pages using Circle CI 2.0
GitHub Pages is GitHub’s free service for hosting static websites (like this blog) and assets. Its usage is extremly straight forward but requires little repetitive chunks of manual work each time you want to update something (unless you use their built-in jekyll tool). The number of smallish and static side projects I maintain recently reached critical mass, so I looked into using Circle CI for automating the deployment to GitHub pages.
Publishing sources
GitHub pages lets you specify different locations where it can serve the assets from:
- The root of your repository on the master branch
- A
/docs
folder on the master branch - The root of a dedicated branch called
gh-pages
Most people, including me, use the gh-pages
option and set the branch up so that it is a detached orphan branch. This lets you nicely separate your sources, kept on master
and other branches, and the rendered output which is being isolated on gh-pages
.
When setting up Continuous Deployment in this scenario a push to master
should therefore trigger the build and replace the last revision of gh-pages
with the build content so that GitHub pages knows it should deploy the updates.
Granting repository access to Circle CI
This of course means Circle CI needs to be be able to push to your repository. Circle CI has automated this process and lets you automatically create a User Key using any account that has access to the repository. This key is kept semi-secret - only the fingerprint will be visible in the UI. It is available to your deployment script though, so make sure you do not run untrusted or unknown code when deploying. If you need fine grained access control it is probably a good idea to create a dedicated GitHub user (“Me-DeployBot”) for this, so you can grant access only where needed.
In addition to that your deployment script will need to know about the username and email that is associated with your User Key. Those are best stored in a GH_NAME
and GH_EMAIL
environment variable that you can define either in your config.yml
or in the UI of Circle CI. This lets you use the following in your deployment script:
git config --global user.email $GH_EMAIL
git config --global user.name $GH_NAME
Creating a second clone of your repository
The next thing to do is to create a clone-in-clone of the repository you are working with so that you can check out its gh-pages
branch. The example uses a folder called out
to contain this copy, but you can call it anything you like.
git clone $CIRCLE_REPOSITORY_URL out
cd out
git checkout $TARGET_BRANCH || git checkout --orphan $TARGET_BRANCH
git rm -rf .
The last command makes sure you can do a fresh build in case the script has checked out a new orphan branch from master
. In this case it would still contain an unwanted copy of all the tracked files.
Building into your target folder
Assuming that make
builds your project (this can be any command that fits your project, npm
scripts, rake
, gulp
, a bash script, anything) into a folder called build
you can now do:
make
cp -a build/. out/.
mkdir -p out/.circleci && cp -a .circleci/. out/.circleci/.
There is an important detail hidden in the last line. If Circle CI is configured to watch your repository, it will also try to build your gh-pages
branch. Usually you would ignore such a branch in your config.yml
s branches.ignore
section, yet on gh-pages
there will probably be no config file yet (we deleted everything before), so Circle CI will not find any configuration and try to build your project using inferred defaults. Copying your .circleci
folder will fix this issue for you as your build now knows which branches to ignore. You can ignore a branch in your config.yml
like this:
branches:
ignore:
- gh-pages
Pushing the build to your repository
Pushing the sources is now as simple as:
cd out
git add -A
git commit -m "Automated deployment to GitHub Pages: ${CIRCLE_SHA1}" --allow-empty
git push origin $TARGET_BRANCH
The commit message will refer to the latest commit on your master
branch (which I find to be quite handy), but you can adopt any scheme you wish.
GitHub Pages will now build your site and deploy the updates. ZOMFG.
A full configuration and script example
The config.yml
that is currently building and deploying this blog looks like:
version: 2
jobs:
build:
branches:
ignore:
- gh-pages
docker:
- image: circleci/node:8
working_directory: ~/repo
environment:
- SOURCE_BRANCH: master
- TARGET_BRANCH: gh-pages
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-
- v1-dependencies-
- run:
name: Install dependencies
command: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-
- run:
name: Run tests
command: npm test
- deploy:
name: Deploy
command: |
if [ $CIRCLE_BRANCH == $SOURCE_BRANCH ]; then
git config --global user.email $GH_EMAIL
git config --global user.name $GH_NAME
git clone $CIRCLE_REPOSITORY_URL out
cd out
git checkout $TARGET_BRANCH || git checkout --orphan $TARGET_BRANCH
git rm -rf .
cd ..
npm run build
cp -a build/. out/.
mkdir -p out/.circleci && cp -a .circleci/. out/.circleci/.
cd out
git add -A
git commit -m "Automated deployment to GitHub Pages: ${CIRCLE_SHA1}" --allow-empty
git push origin $TARGET_BRANCH
fi
Happy building!
On a side note, this config file is intentionally missing two possible additions:
- Put the deployment script into a separate
deploy.sh
script - Use Circle CI workflows to have a cleaner way of running the script only on the
master
branch.