Repository navigation
189 lines (179 loc) · 9.3 KB
/
Copy pathdocs.yml
File metadata and controls
189 lines (179 loc) · 9.3 KB
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
name: Docs
on:
workflow_call:
inputs:
build-command:
description: Command that builds the HTML site into html-dir at $BASE_URL.
required: true
type: string
html-dir:
description: Directory the build writes the site to (packed as docs.zip's single root dir, html/).
required: false
default: docs/_build/html
type: string
base-path:
description: >-
URL path the SITE is served at, without the version segment. Empty (the
default) means /<repo>, which is where a project Pages site lives
(https://ORG.github.io/REPO/). Set it to "/" for a custom domain (CNAME) or an
ORG.github.io repo — both serve from the root, where a /<repo> prefix would
404 every root-absolute asset.
required: false
default: ""
type: string
# REUSABLE docs build — the UNPRIVILEGED half, shared across MyST projects. Builds
# the docs at the versioned BASE_URL and uploads this build's `docs` artifact
# (docs.zip, a single html/ root dir). It runs for every event — PRs (including forks),
# pushes to the default branch, and tags — but NEVER publishes. publish.yml (the
# privileged half) picks the artifact up once the run completes.
#
# This workflow owns one half of the build/publish contract: the docs.zip
# single-root-dir shape and the `docs` artifact NAME (publish.yml gathers cross-run
# artifacts by that name via the artifacts API, never by workflow filename). The
# consumer supplies only HOW to turn sources into HTML, via `build-command`.
#
# Environment given to build-command: Node/npx (preinstalled on the runner) and uv
# (always installed; honours a committed .python-version). So build-command can be
# `make docs`, `npx … myst build`, `tox -e docs`, `uv run …`, etc. Fold any
# project-specific setup (cp CONFIG, npm ci, apt deps) behind that one command.
#
# contents:read at the workflow level keeps this (untrusted-input) build incapable
# of holding a write token — the elevated permissions live only in publish.yml.
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
# Generous: build-command is the consumer's, and a docs build with a Python env to
# solve can legitimately take a while. It only has to beat the 6-hour default, so a
# hung build fails within the hour instead of holding a runner all day.
timeout-minutes: 30
steps:
# fetch-depth: 0 brings the full history *and tags* — a build-command may
# derive a version from `git describe` (this repo's docs do, to pin the
# tutorial's snippets at the latest release), and the default shallow clone
# has no tags. Tags are cheap; fetch them unconditionally so any consumer's
# build-command can rely on them.
- uses: actions/checkout@v7
with:
fetch-depth: 0
# uv is installed unconditionally (Node/npx is already on the runner) so every
# docs build has the same baseline env regardless of build-command. With no
# python-version input it only puts uv on PATH, honouring a committed
# .python-version if the project has one — a no-op for npx-only repos.
- name: Install uv
uses: astral-sh/setup-uv@v10.0.1 # full version: setup-uv uses immutable releases (no floating @v8)
with:
# No project deps to cache here: build-command owns dependency install
# (npx/make/tox/uv) and there are no requirements/pyproject/uv.lock files to
# key the cache on — which otherwise emits a "cache will never get
# invalidated" warning every build. uv's own download is small, so skip it.
enable-cache: false
# Version name = the site sub-dir this build is served at, and the BASE_URL it
# must be built with: pr-<n> for PRs, otherwise the ref name (the default branch,
# or a tag without `/`).
#
# This is the OTHER half of the contract, and it is not passed anywhere: the
# gather independently re-derives the same name for each source it stages (a
# release's tag, a PR's number, the repo's default_branch) and unpacks the zip at
# site/<that name>. Both sides compute the same string from the same facts, so
# this rule and assemble.mjs's dest rule must change together.
#
# The name is VALIDATED, not assumed clean. In THIS repo it is clean by
# construction (`tags: ['*']` never builds a `/`-tag), but this is a reusable
# workflow: a consumer triggering it on `push: branches: ['**']` would let anyone
# who can push a branch choose the string — and git accepts `$( )`, backticks and
# quotes in a ref name. Hence both halves here: the ref comes through `env:` so
# the shell never parses it as script text, and the character set is then checked
# so it stays a safe URL path segment and site dir.
- name: Compute version name and BASE_URL
id: ver
env:
EVENT: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REF_NAME: ${{ github.ref_name }}
BASE_PATH: ${{ inputs.base-path }}
REPO_NAME: ${{ github.event.repository.name }}
run: |
set -euo pipefail
if [ "$EVENT" = pull_request ]; then
name="pr-$PR_NUMBER"
elif [ "$EVENT" = merge_group ]; then
# merge_group's ref_name is GitHub's internal
# gh-readonly-queue/<base-branch>/pr-<n>-<sha> — it contains '/' (and, via
# the base branch, whatever else a branch name allows) so it fails the check
# below outright. This build is never gathered: the site-side gather only
# reads the default branch's HEAD artifact, open PRs' artifacts and release
# zips, never a merge queue's temporary branch, so the name just needs to be
# a valid path segment, not a parse of GitHub's ref.
name="merge-queue"
else
name="$REF_NAME"
fi
case "$name" in
"" | "." | "..") ok=false ;;
*[!A-Za-z0-9._-]*) ok=false ;;
*) ok=true ;;
esac
if [ "$ok" != true ]; then
echo "::error::version name '$name' is not usable as a URL path segment \
— it must be non-empty and contain only A-Z a-z 0-9 . _ - (a '/' in a branch \
or tag name is the usual cause). Build this ref under a name that is, or \
exclude it from the workflow's triggers."
exit 1
fi
echo "version-name=$name" >> "$GITHUB_OUTPUT"
# BASE_URL must match the versioned sub-path the site is served at, or the
# build's root-absolute assets 404. The site half of it is base-path, which
# defaults to /<repo> — right for a project Pages site, wrong for a custom
# domain or an ORG.github.io repo, which serve at the root. Normalise to
# exactly one leading slash and no trailing one, so "/", "/repo" and "repo/"
# all compose the same way.
base="${BASE_PATH:-/$REPO_NAME}"
base="/${base#/}"
base="${base%/}"
echo "BASE_URL=$base/$name" >> "$GITHUB_ENV"
echo "version name: $name BASE_URL: $base/$name"
# BASE_URL arrives via GITHUB_ENV from the step above (it needs base-path's
# normalisation, which an `env:` expression cannot do).
# VERSION_NAME is the bare version token, exported for builds
# that need it directly — e.g. a Sphinx conf.py setting pydata-sphinx-theme's
# switcher `version_match` — without parsing it back out of BASE_URL.
# build-command comes through an env var (not interpolated into the script)
# and is run with `eval` so compound commands work.
- name: Build docs
env:
VERSION_NAME: ${{ steps.ver.outputs.version-name }}
BUILD_COMMAND: ${{ inputs.build-command }}
run: |
set -euo pipefail
eval "$BUILD_COMMAND"
# Pack the site as docs.zip with a single root directory — the durable contract
# both publish.yml's gather and release.yml's asset rely on. Stage into html/
# first so any html-dir name yields the same root. The extract side keys on there
# being exactly ONE root dir, not on its name, but everything this workflow
# produces is html/-rooted.
- name: Pack docs.zip (single html/ root)
env:
HTML_DIR: ${{ inputs.html-dir }}
run: |
set -euo pipefail
staging="$RUNNER_TEMP/pack"
rm -rf "$staging"
mkdir -p "$staging/html"
cp -a "$HTML_DIR/." "$staging/html/"
( cd "$staging" && zip -rq "$RUNNER_TEMP/docs.zip" html )
# compression-level 0: docs.zip is already compressed.
#
# Skipped on merge_group: that build's version name is the fixed literal
# "merge-queue" (see the step above), and the site-side gather never looks for
# it — only the default branch's HEAD artifact, open PRs' artifacts and release
# zips are gathered. Uploading it anyway would be pure storage churn against the
# repo's Actions quota, once per queue entry.
- name: Upload docs artifact
if: github.event_name != 'merge_group'
uses: actions/upload-artifact@v7
with:
name: docs
path: ${{ runner.temp }}/docs.zip
compression-level: 0