Build searchable docs #82
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |