Versioned Docs Pages

How to deploy versioned documentation site to GitHub Pages using GitHub Actions

Note: you can find the source code for this blog post in the repository bahmutov/deploy-gh-example and the deployed site at https://glebbahmutov.com/deploy-gh-example.

The app

Let's say we want to host static documentation pages for our software at GitHub Pages. We could prepare the site and generate the HTML pages in the public folder. For simplicity I will have a single public/index.html file. At first, the software is at version 1.0.0, and the HTML file contains:

public/index.html
1
2
3
4
<body>
<h1>My awesome [email protected]</h1>
<p>To use this awesome lib ...</p>
</body>

In the future we will update the documentation and will release version 2.0.0 that we will want to deploy alongside the 1.0.0 pages.

The deploy

Let's deploy this site to GitHub Pages using GitHub Actions.

./github/workflows/deploy.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: deploy
# only deploy site from master branch
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
persist-credentials: false

# you probably would do build and test steps here

# https://github.com/marketplace/actions/github-pages-action
- name: Deploy 🚀
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# deploy the current public folder
# to <base url>/x.y.z version subfolder
publish_dir: ./public
destination_dir: 1.0.0

To deploy, I am using peaceiris/actions-gh-pages GH Action. After I push the code, the workflow runs once and before I see the deployed site, I need to enable the GitHub Pages from the repository's settings page.

Enable GitHub Pages before you can see the site

Now you can see the live site (I am using my custom domain) at https://glebbahmutov.com/deploy-gh-example/1.0.0/.

The deployed site v1.0.0

You can only access the /1.0.0 path, since there is no root index page.

The second version

Let's pretend that we have released the new version of the software, and thus have updated the documentation to v2.0.0. We simply change the public/index.html to reflect the documentation.

public/index.html
1
2
3
4
<body>
<h1>My awesome [email protected]</h1>
<p>Much much better library ...</p>
</body>

We need to bump the destination directory in the workflow file

1
2
- destination_dir: 2.0.0
+ destination_dir: 2.0.0

Push the code, the deploy runs, and now you can access both /1.0.0 and /2.0.0. A good trick to see the built and deployed site is to look at the gh-pages branch of the repository. It shows documentation for both versions is present.

Both 1.0.0 and 2.0.0 folders are deployed

The redirect

Not having something at the root of the documentation site is bad. I think a nice solution is to redirect the root URL to the latest documentation. For GitHub Pages we can redirect by using a META tag. Let's create an HTML file like this:

redirect/index.html
1
2
3
<head>
<meta http-equiv="Refresh" content="0; url='/deploy-gh-example/2.0.0'" />
</head>

Note the destination URL; it includes the path to the site. During the workflow, after we deploy the 2.0.0 subfolder, we can deploy again, but just bring the redirect folder and place it at the root.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# https://github.com/marketplace/actions/github-pages-action
- name: Deploy 🚀
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# deploy the current public folder
# to <base url>/x.y.z version subfolder
publish_dir: ./public
destination_dir: 2.0.0

- name: Deploy redirect 🚀
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./redirect
keep_files: true

Let the deploy run and observe the result in gh-pages branch.

Redirect HTML page at the root

The redirect in action

Redirect in action

Deleting versions

Every deploy (potentially) adds more and more subfolders in the gh-pages. After a while the static site might have lots of old obsolete version folders. To remove them we can simply check out the gh-pages branch, remove the old folder and push the commit back to GitHub.

Bonus 1: Set the version dynamically

When deploying subfolders it helps to set the version string dynamically and not hardcode it in the workflow file. You can set the subfolder name using the output parameter. Just create a new step where you use the ::set-output commnad. For example:

1
2
3
- name: Set Cypress version 🎁
id: cypress
run: echo "::set-output name=version::5.6.0"

From the later steps you can refer to the output by using the step's ID + output name. For example to print

1
2
- name: Print Cypress version 🖨
run: echo "Cypress version is ${{ steps.cypress.outputs.version }}"

Or you can use it to form other commands

1
2
- name: Export Markdown specs
run: node ./md-to-js http://localhost:5000/cypress-examples/${{ steps.cypress.outputs.version }}

Find the above code in actions in bahmutov/cypress-examples.