Skip to content

Build searchable docs #114

Build searchable docs

Build searchable docs #114

Workflow file for this run

name: Build searchable docs
# Pulls the HTML docs that LinuxCNC CI already publishes, adds pagefind search,
# and uploads the result as an artifact. Runs every 6 hours (skipping when the
# source docs are unchanged), on every push to master, and on demand.
on:
workflow_dispatch:
push:
branches: [master]
schedule:
- cron: "43 */6 * * *" # 03:43, 09:43, 15:43, 21:43 UTC
permissions:
contents: read
actions: read # the check job reads this repo's own state artifact
jobs:
check:
# Decide whether the source docs changed since our last build, and hand the
# source artifact location to the build job. Skipping here also keeps the
# webserver idle: no new artifact, nothing to republish.
runs-on: ubuntu-24.04
outputs:
build: ${{ steps.guard.outputs.build }}
source_url: ${{ steps.guard.outputs.source_url }}
docs_sha: ${{ steps.guard.outputs.docs_sha }}
steps:
- name: Compare source docs tree against last build
id: guard
env:
SOURCE_TOKEN: ${{ secrets.SOURCE_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT: ${{ github.event_name }}
run: |
set -eu
# Newest non-expired linuxcnc-doc.tar.gz built on master by the
# trusted repo (head_repository_id, so a fork PR whose branch happens
# to be named master cannot inject docs).
src=$(curl -fsSL -H "Authorization: Bearer $SOURCE_TOKEN" \
"https://api.github.com/repos/LinuxCNC/linuxcnc/actions/artifacts?per_page=100&name=linuxcnc-doc.tar.gz" \
| python3 -c '
import json, sys
for a in json.load(sys.stdin)["artifacts"]:
w = a.get("workflow_run") or {}
if a.get("expired"): continue
if w.get("head_branch") != "master": continue
if int(w.get("head_repository_id") or 0) != 3662905: continue
print(a["archive_download_url"])
print(w.get("head_sha") or "")
break
else:
sys.exit("no usable linuxcnc-doc.tar.gz artifact for master")
')
url=$(echo "$src" | sed -n 1p)
head_sha=$(echo "$src" | sed -n 2p)
echo "source_url=$url" >> "$GITHUB_OUTPUT"
# The artifact digest churns on every master CI run (timestamps in
# the tar), so it cannot tell docs changes from code-only merges.
# Gate on the docs/ git tree sha of the commit that produced the
# artifact instead: it only moves when docs content actually changes.
tree_sha=$(curl -fsSL -H "Authorization: Bearer $SOURCE_TOKEN" \
"https://api.github.com/repos/LinuxCNC/linuxcnc/commits/$head_sha" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["commit"]["tree"]["sha"])')
docs_sha=$(curl -fsSL -H "Authorization: Bearer $SOURCE_TOKEN" \
"https://api.github.com/repos/LinuxCNC/linuxcnc/git/trees/$tree_sha" \
| python3 -c '
import json, sys
for e in json.load(sys.stdin)["tree"]:
if e["path"] == "docs":
print(e["sha"])
break
else:
sys.exit("no docs/ entry in master tree")
')
echo "docs_sha=$docs_sha" >> "$GITHUB_OUTPUT"
# Forced builds: injector changes (push) or a human (dispatch)
# always rebuild, even if the source docs are unchanged.
if [ "$EVENT" != schedule ]; then
echo "build=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# Scheduled run: rebuild only if the docs tree sha differs from the
# one our last build consumed (stored as a tiny raw artifact; with
# archive:false the artifact is named after the file).
state=$(gh api "repos/${{ github.repository }}/actions/artifacts?per_page=1&name=doc-search-state.txt" \
--jq '.artifacts[0].archive_download_url // empty')
if [ -n "$state" ]; then
last_docs_sha=$(gh api "$state")
else
last_docs_sha=
fi
if [ -n "$docs_sha" ] && [ "$docs_sha" = "$last_docs_sha" ]; then
echo "source docs unchanged ($docs_sha); skipping build"
echo "build=false" >> "$GITHUB_OUTPUT"
else
echo "build=true" >> "$GITHUB_OUTPUT"
fi
build:
needs: check
if: needs.check.outputs.build == 'true'
runs-on: ubuntu-24.04
steps:
- name: Checkout this repo
uses: actions/checkout@v6
- name: Download LinuxCNC HTML docs
# The linuxcnc-doc.tar.gz artifact is uploaded with archive:false, so it
# comes back as the raw tar.gz (not a zip); fetch it straight from the
# API. SOURCE_TOKEN is a classic PAT with the public_repo scope (the
# default GITHUB_TOKEN cannot read another repo's artifacts).
env:
TOKEN: ${{ secrets.SOURCE_TOKEN }}
URL: ${{ needs.check.outputs.source_url }}
run: |
set -eux
curl -fsSL -H "Authorization: Bearer $TOKEN" -o docs.tar.gz "$URL"
tar -xzf docs.tar.gz # produces ./html
test -d html
- name: Add search
run: ./scripts/build.sh html
- name: Package searchable docs
run: tar -czf linuxcnc-doc-search.tar.gz html # tarball contains html/
- name: Upload searchable docs
uses: actions/upload-artifact@v7
with:
# archive:false keeps it a raw tar.gz (not zip-wrapped), matching the
# upstream linuxcnc-doc.tar.gz so the deploy fetches both the same way.
name: linuxcnc-doc-search.tar.gz
archive: false
path: linuxcnc-doc-search.tar.gz
if-no-files-found: error
- name: Record the source docs tree sha we built from
env:
DOCS_SHA: ${{ needs.check.outputs.docs_sha }}
run: echo "$DOCS_SHA" > doc-search-state.txt
- name: Upload build state
uses: actions/upload-artifact@v7
with:
# Tiny raw text file; the check job reads it back to decide whether a
# scheduled run has anything to do.
name: doc-search-state
archive: false
path: doc-search-state.txt
if-no-files-found: error