Skip to content

Commit 8378b88

Browse files
committed
ci: update the books via a GitHub workflow
With this commit, the SHAs corresponding to the various repositories containing the ProGit Book and its translations are stored in a sparse-checkout'able directory. This information is then used by a scheduled workflow to determine what needs to be updated (if anything) and then performing that task. When GitHub workflows push new changes, they cannot trigger other GitHub workflows (to avoid infinite loops). Therefore, this new GitHub workflow not only synchronizes the books, but also builds the site and deploys it. Note: The code to build the site and to deploy it is provided in a custom Action, to make it reusable. It will come in handy over the next commits, where other GitHub workflows are added that likewise need to synchronize changes that desire a site rebuild & deployment. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent c8657dc commit 8378b88

File tree

3 files changed

+255
-0
lines changed

3 files changed

+255
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: 'Run Hugo/Pagefind and deploy to GitHub Pages'
2+
description: 'Runs Hugo and Pagefind and then deploys the result to GitHub Pages.'
3+
# This composite Action requires the following things in the calling workflow:
4+
#
5+
# permissions:
6+
# contents: write # to push changes (if any)
7+
# pages: write # to deploy to GitHub Pages
8+
# id-token: write # to verify that the deployment source is legit
9+
# environment:
10+
# name: github-pages
11+
# url: ${{ steps.<id-of-deployment-step>.outputs.url }}
12+
outputs:
13+
url:
14+
description: The URL to which the site was deployed
15+
value: ${{ steps.deploy.outputs.page_url }}
16+
runs:
17+
using: "composite"
18+
steps:
19+
- name: push changes
20+
shell: bash
21+
run: git push origin HEAD
22+
23+
- name: un-sparse worktree to prepare for deployment
24+
shell: bash
25+
run: git sparse-checkout disable
26+
27+
- name: setup GitHub Pages
28+
id: pages
29+
uses: actions/configure-pages@v5
30+
31+
- name: configure Hugo and Pagefind version
32+
shell: bash
33+
run: |
34+
set -x &&
35+
echo "HUGO_VERSION=$(sed -n 's/^ *hugo_version: *//p' <hugo.yml)" >>$GITHUB_ENV
36+
echo "PAGEFIND_VERSION=$(sed -n 's/^ *pagefind_version: *//p' <hugo.yml)" >>$GITHUB_ENV
37+
38+
- name: install Hugo ${{ env.HUGO_VERSION }}
39+
shell: bash
40+
run: |
41+
set -x &&
42+
curl -Lo /tmp/hugo.deb https://github.com/gohugoio/hugo/releases/download/v$HUGO_VERSION/hugo_extended_${HUGO_VERSION}_linux-amd64.deb &&
43+
sudo dpkg -i /tmp/hugo.deb
44+
45+
- name: run Hugo to build the pages
46+
env:
47+
HUGO_RELATIVEURLS: false
48+
shell: bash
49+
run: hugo config && hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
50+
51+
- name: run Pagefind ${{ env.PAGEFIND_VERSION }} to build the search index
52+
shell: bash
53+
run: npx -y pagefind@${{ env.PAGEFIND_VERSION }} --site public
54+
55+
- name: upload GitHub Pages artifact
56+
uses: actions/upload-pages-artifact@v3
57+
with:
58+
path: ./public
59+
60+
- name: deploy
61+
id: deploy
62+
uses: actions/deploy-pages@v4

.github/workflows/update-book.yml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
name: Update Progit Book
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
# check daily for updates
7+
- cron: '29 4 * * *'
8+
9+
jobs:
10+
check-for-updates:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
with:
15+
sparse-checkout: |
16+
external/book/sync
17+
script
18+
- uses: actions/github-script@v7
19+
id: get-pending
20+
with:
21+
script: |
22+
const { getPendingBookUpdates } = require('./script/ci-helper.js')
23+
24+
const pending = await getPendingBookUpdates(github)
25+
// an empty matrix is invalid and makes the workflow run fail, unfortunately
26+
return pending.length ? pending : ['']
27+
- name: ruby setup
28+
# Technically, we do not need Ruby in this job. But we do want to cache
29+
# Ruby & The Gems for use in the matrix in the next job.
30+
if: steps.get-pending.outputs.result != '[""]'
31+
uses: ruby/setup-ruby@v1
32+
with:
33+
bundler-cache: true
34+
outputs:
35+
matrix: ${{ steps.get-pending.outputs.result }}
36+
update-book:
37+
needs: check-for-updates
38+
if: needs.check-for-updates.outputs.matrix != '[""]'
39+
runs-on: ubuntu-latest
40+
strategy:
41+
matrix:
42+
language: ${{ fromJson(needs.check-for-updates.outputs.matrix) }}
43+
fail-fast: false
44+
steps:
45+
- uses: actions/checkout@v4
46+
with:
47+
sparse-checkout: |
48+
script
49+
external/book/sync
50+
external/book/data
51+
external/book/content/book/${{ matrix.language.lang }}
52+
external/book/static/book/${{ matrix.language.lang }}
53+
- name: clone ${{ matrix.language.repository }}
54+
run: |
55+
printf '%s\n' /progit-clone/ /vendor >>.git/info/exclude &&
56+
57+
# Clone the book's sources
58+
git clone --depth 1 --single-branch \
59+
https://github.com/${{ matrix.language.repository }} progit-clone
60+
- name: ruby setup
61+
uses: ruby/setup-ruby@v1
62+
with:
63+
bundler-cache: true
64+
- name: update book/${{ matrix.language.lang }}
65+
run: |
66+
# this seems to be needed to let `bundle exec` see `vendor/bundle/`
67+
{ bundle check || bundle install --frozen; } &&
68+
69+
# generate the HTML
70+
bundle exec ruby ./script/update-book2.rb ${{ matrix.language.lang }} progit-clone
71+
- name: commit changes
72+
run: |
73+
# record the commit hash
74+
mkdir -p external/book/sync &&
75+
git -C progit-clone rev-parse HEAD >external/book/sync/book-${{ matrix.language.lang }}.sha &&
76+
77+
# commit it all
78+
git add -A external/book &&
79+
git -c user.name=${{ github.actor }} \
80+
-c user.email=${{ github.actor }}@noreply.github.com \
81+
commit -m 'book: update ${{ matrix.language.lang }}' \
82+
-m 'Updated via the `update-book.yml` GitHub workflow.'
83+
- name: verify that there are no uncommitted changes
84+
run: |
85+
git update-index --refresh &&
86+
if test -n "$(git diff HEAD)$(git ls-files --exclude-standard --other)"
87+
then
88+
echo '::error::there are uncommitted changes!' >&2
89+
git status >&2
90+
exit 1
91+
fi
92+
- name: generate the bundle
93+
run: |
94+
git branch -m book-${{ matrix.language.lang }}
95+
git bundle create ${{ matrix.language.lang }}.bundle refs/remotes/origin/${{ github.ref_name }}..book-${{ matrix.language.lang }}
96+
- uses: actions/upload-artifact@v4
97+
with:
98+
name: bundle-${{ matrix.language.lang }}
99+
path: ${{ matrix.language.lang }}.bundle
100+
push-updates:
101+
needs: [check-for-updates, update-book]
102+
if: needs.check-for-updates.outputs.matrix != '[""]'
103+
permissions:
104+
contents: write # to push changes (if any)
105+
pages: write # to deploy to GitHub Pages
106+
id-token: write # to verify that the deployment source is legit
107+
environment:
108+
name: github-pages
109+
url: ${{ steps.deploy.outputs.url }}
110+
runs-on: ubuntu-latest
111+
steps:
112+
- uses: actions/checkout@v4
113+
- uses: actions/download-artifact@v4
114+
- name: apply updates
115+
id: apply
116+
run: |
117+
for lang in $(echo '${{ needs.check-for-updates.outputs.matrix }}' |
118+
sed -n 's/\[\?{[^}]*"lang":"\([^"]*\)[^}]*},\?\]\?/\1 /gp')
119+
do
120+
git -c core.editor=: \
121+
-c user.name=${{ github.actor }} \
122+
-c user.email=${{ github.actor }}@noreply.github.com \
123+
pull --no-rebase bundle-$lang/$lang.bundle book-$lang ||
124+
exit 1
125+
done
126+
- name: deploy to GitHub Pages
127+
id: deploy
128+
uses: ./.github/actions/deploy-to-github-pages

script/ci-helper.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const fs = require('fs')
2+
3+
const getFileContents = async (path) => {
4+
return (await fs.promises.readFile(path)).toString('utf-8').trim()
5+
}
6+
7+
const getAllBooks = async () => {
8+
const book_rb = await getFileContents("script/book.rb");
9+
const begin = book_rb.indexOf('@@all_books = {')
10+
const end = book_rb.indexOf('}', begin + 1)
11+
if (begin < 0 || end < 0) throw new Error(`Could not find @@all_books in:\n${book_rb}`)
12+
return book_rb
13+
.substring(begin, end)
14+
.split('\n')
15+
.reduce((allBooks, line) => {
16+
const match = line.match(/"([^"]+)" => "([^"]+)"/)
17+
if (match) allBooks[match[1]] = match[2]
18+
return allBooks
19+
}, {})
20+
}
21+
22+
const getPendingBookUpdates = async (octokit) => {
23+
const books = await getAllBooks()
24+
const result = []
25+
for (const lang of Object.keys(books)) {
26+
try {
27+
const localSha = await getFileContents(`external/book/sync/book-${lang}.sha`)
28+
29+
const [owner, repo] = books[lang].split('/')
30+
const { data: { default_branch: remoteDefaultBranch } } =
31+
await octokit.rest.repos.get({
32+
owner,
33+
repo
34+
})
35+
const { data: { object: { sha: remoteSha } } } =
36+
await octokit.rest.git.getRef({
37+
owner,
38+
repo,
39+
ref: `heads/${remoteDefaultBranch}`
40+
})
41+
42+
if (localSha === remoteSha) continue
43+
} catch (e) {
44+
// It's okay for the `.sha` file not to exist yet.`
45+
if (e.code !== 'ENOENT') throw e
46+
}
47+
result.push({
48+
lang,
49+
repository: books[lang]
50+
})
51+
}
52+
return result
53+
}
54+
55+
// for testing locally, needs `npm install @octokit/rest` to work
56+
if (require.main === module) {
57+
(async () => {
58+
const { Octokit } = require('@octokit/rest')
59+
console.log(await getPendingBookUpdates(new Octokit()))
60+
})().catch(console.log)
61+
}
62+
63+
module.exports = {
64+
getPendingBookUpdates
65+
}

0 commit comments

Comments
 (0)