GitHub Pages provides hosting for static files by serving a branch (e.g. gh-pages) of the respective repository. GitHub Actions can be used to automate deployments, avoiding the hassle of having to update that branch manually when the main branch (typically master) changes.

The idea of using other people’s actions made me slightly uncomfortable, due to mild security concerns (handing full repo access to some unknown party) and the complexity of excessive abstraction (who wants to read documentation when they can write code instead…. ). So I set out to automate this myself – thinking it should be straightforward:

#!/usr/bin/env bash

set -eu

repo_uri="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
remote_name="origin"
main_branch="master"
target_branch="gh-pages"
build_dir="dist"

cd "$GITHUB_WORKSPACE"

git config user.name "$GITHUB_ACTOR"
git config user.email "${GITHUB_ACTOR}@bots.github.com"

git checkout "$target_branch"
git rebase "${remote_name}/${main_branch}"

./bin/build "$build_dir"
git add "$build_dir"

git commit -m "updated GitHub Pages"
if [ $? -ne 0 ]; then
    echo "nothing to commit"
    exit 0
fi

git remote set-url "$remote_name" "$repo_uri" # includes access token
git push --force-with-lease "$remote_name" "$target_branch"

Here we run ./bin/build (a placeholder for make, npm start or similar) to generate build artifacts in the dist directory and then commit them to the gh-pages branch. The script relies on various environment variables.

We can make GitHub Actions execute that script (./bin/update-gh-pages) by creating a workflow description (e.g. .github/workflows/pages.yml):

name: GitHub Pages

on:
    push:
        branches:
        - master

jobs:
    build:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v1
        - run: ./bin/update-gh-pages
          env:
              GITHUB_TOKEN: ${{ secrets.github_token }}

Note that we pass in GITHUB_TOKEN, which is used in the script’s repo URI to provide read/write access.

However, turns out that repo updates using GITHUB_TOKEN do not trigger GitHub Pages builds. (Which was not at all obvious… )

So we need to generate a personal access token, add it to respective repo’s secrets (via Settings → Secrets; named DEPLOY_TOKEN here) and use that instead of GITHUB_TOKEN:

env:
    GITHUB_TOKEN: ${{ secrets.github_token }}
    DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
repo_uri="https://x-access-token:${DEPLOY_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"

With those adjustments, our automated repo updates finally result in GitHub Pages being published as well.

PS: In my case, the repo in question is a Node-based application, so the workflow file includes a few additional steps:

- uses: actions/setup-node@v1
  with:
      node-version: 12
- uses: actions/cache@v1
  with:
      path: ~/.npm
      key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
      restore-keys: |
          ${{ runner.os }}-build-${{ env.cache-name }}-
          ${{ runner.os }}-build-
          ${{ runner.os }}-
- run: npm install-ci-test