@@ -3,82 +3,255 @@ name: build-book
33on :
44 push :
55 branches :
6- - " main"
6+ - main
7+ - " releases/v*"
8+ paths-ignore :
9+ - " **/*.md"
10+ - " .github/ISSUE_TEMPLATE/**"
711 pull_request :
812 branches :
9- - " main"
10- - " release/**"
13+ - main
14+ - " releases/**"
15+ paths :
16+ - " .github/workflows/docs.yml"
17+ - " .github/docs-versions.yml"
18+ - " build_scripts/inject_version_picker.py"
19+ - " build_scripts/version_picker_assets/**"
20+ - " doc/**"
21+ - " build_scripts/pydoc2json.py"
22+ - " build_scripts/gen_api_md.py"
23+ - " pyrit/**"
1124 workflow_dispatch :
1225
13- # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
26+ # Permissions for the GH Pages deployment.
1427permissions :
1528 contents : read
1629 pages : write
1730 id-token : write
1831
19- # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
20- # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
32+ # One deploy at a time. Don't cancel in-progress production deploys.
2133concurrency :
22- group : " pages"
34+ group : pages
2335 cancel-in-progress : false
2436
2537env :
26- # BASE_URL determines the website path prefix, including CSS & JS assets
27- BASE_URL : /${{ github.event.repository.name }}
38+ # All versioned URLs live under /<repo-name>/<slug>/, e.g. /PyRIT/0.13.0/.
39+ DOCS_BASE : /${{ github.event.repository.name }}
40+ # Manual cache-bust knob. Bump this when the build pipeline changes in a
41+ # way that affects the rendered HTML output but isn't captured by the
42+ # source-ref SHA -- e.g. bumping python-version, uv version, or changing
43+ # build flags in the steps below. Forgetting to bump is harmless (you
44+ # just serve a stale cached build for one push); bumping unnecessarily
45+ # is also harmless (one extra rebuild).
46+ CACHE_VERSION : " v1"
2847
29- # This job installs dependencies, builds the book, and pushes it to `gh-pages`
3048jobs :
31- deploy-book :
49+ # ------------------------------------------------------------------
50+ # 1. Read .github/docs-versions.yml and turn it into a build matrix.
51+ # ------------------------------------------------------------------
52+ versions :
53+ name : Resolve version matrix
3254 runs-on : ubuntu-latest
33- permissions :
34- pages : write
35- id-token : write
55+ outputs :
56+ matrix : ${{ steps.matrix.outputs.matrix }}
57+ default : ${{ steps.matrix.outputs.default }}
58+ stable : ${{ steps.matrix.outputs.stable }}
59+ versions_json : ${{ steps.matrix.outputs.versions_json }}
3660 steps :
37- - uses : actions/checkout@v6
38-
39- # Install dependencies
40- - name : Set up Python 3.11
41- uses : actions/setup-python@v6
42- with :
43- python-version : 3.11
44-
45- - name : Install uv
46- uses : astral-sh/setup-uv@v7
47- with :
48- # Install a specific version of uv.
49- version : " 0.9.17"
50- enable-cache : true
51- cache-dependency-glob : |
52- **/pyproject.toml
53- **/uv.lock
54-
55- - name : Install PyRIT with uv
56- run : uv sync --extra all
57-
58- # LaTeX toolchain needed for the PDF export (jupyter-book --pdf via xelatex).
59- # Mirrors the ReadTheDocs build so CI catches PDF-only issues (e.g. broken
60- # image paths) instead of letting them silently break the RTD build.
61- - name : Install LaTeX (for PDF export)
62- run : |
63- sudo apt-get update
64- sudo apt-get install -y --no-install-recommends \
65- texlive-xetex \
66- texlive-fonts-recommended \
67- texlive-fonts-extra \
68- texlive-plain-generic \
69- texlive-latex-extra \
70- latexmk
71-
72- # Build the book (HTML site + PDF export)
73- - name : Build the book
74- run : |
75- make docs-build-all
76- # Upload the book's HTML as an artifact
77- - name : Upload artifact
78- uses : actions/upload-pages-artifact@v5
79- with :
80- path : " doc/_build/html"
81- - name : Deploy to GitHub Pages
82- if : ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
83- id : deployment
84- uses : actions/deploy-pages@v5
61+ - uses : actions/checkout@v6
62+ with :
63+ sparse-checkout : |
64+ .github/docs-versions.yml
65+ build_scripts/resolve_docs_matrix.py
66+ sparse-checkout-cone-mode : false
67+
68+ - name : Set up Python 3.13
69+ uses : actions/setup-python@v6
70+ with :
71+ python-version : " 3.13"
72+
73+ - name : Install PyYAML
74+ run : pip install --quiet "pyyaml>=6.0"
75+
76+ - name : Compute matrix
77+ id : matrix
78+ run : |
79+ python build_scripts/resolve_docs_matrix.py \
80+ --config .github/docs-versions.yml \
81+ --github-output "$GITHUB_OUTPUT"
82+
83+ # ------------------------------------------------------------------
84+ # 2. Build each version in parallel.
85+ # ------------------------------------------------------------------
86+ build :
87+ name : Build ${{ matrix.slug }}
88+ needs : versions
89+ runs-on : ubuntu-latest
90+ strategy :
91+ fail-fast : false
92+ matrix : ${{ fromJson(needs.versions.outputs.matrix) }}
93+ env :
94+ BASE_URL : ${{ format('/{0}/{1}', github.event.repository.name, matrix.slug) }}
95+ steps :
96+ - name : Checkout ${{ matrix.ref }}
97+ uses : actions/checkout@v6
98+ with :
99+ ref : ${{ matrix.ref }}
100+
101+ - name : Resolve commit SHA
102+ # matrix.ref is a branch name (e.g. "releases/v0.13.0") that can move,
103+ # so resolve to the actual commit SHA we just checked out. This is the
104+ # primary cache key component: frozen release branches that don't move
105+ # produce identical SHAs and hit the cache forever; main produces a
106+ # new SHA on every push so it always rebuilds.
107+ id : sha
108+ run : echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
109+
110+ - name : Restore built site from cache
111+ # Cache hit -> we skip the entire install + build pipeline and just
112+ # upload the restored doc/_build/html as the matrix artifact. Cache
113+ # miss -> we do a fresh build and actions/cache saves the result on
114+ # job success, so the next run with the same SHA hits.
115+ id : cache
116+ uses : actions/cache@v4
117+ with :
118+ path : doc/_build/html
119+ key : docs-${{ env.CACHE_VERSION }}-${{ matrix.slug }}-${{ steps.sha.outputs.sha }}
120+
121+ - name : Set up Python 3.13
122+ if : steps.cache.outputs.cache-hit != 'true'
123+ uses : actions/setup-python@v6
124+ with :
125+ python-version : " 3.13"
126+
127+ - name : Install uv
128+ if : steps.cache.outputs.cache-hit != 'true'
129+ uses : astral-sh/setup-uv@v7
130+ with :
131+ version : " 0.9.17"
132+ enable-cache : true
133+ cache-dependency-glob : |
134+ **/pyproject.toml
135+ **/uv.lock
136+
137+ - name : Install PyRIT with dev dependencies
138+ if : steps.cache.outputs.cache-hit != 'true'
139+ # Use `uv sync --frozen` so each release branch installs the exact
140+ # versions pinned in its own uv.lock. Frozen branch = frozen deps =
141+ # bit-for-bit reproducible rebuild forever.
142+ #
143+ # Newer release branches (incl. main) use PEP 735 [dependency-groups];
144+ # older releases use legacy [project.optional-dependencies]. We pick
145+ # the right flag up front by reading pyproject.toml (via stdlib
146+ # tomllib) so any uv sync failure surfaces normally instead of being
147+ # swallowed by `|| fallback` with stderr redirected to /dev/null.
148+ run : |
149+ set -euo pipefail
150+ choice=$(python -c "
151+ import sys, tomllib
152+ d = tomllib.loads(open('pyproject.toml').read())
153+ if 'dev' in (d.get('dependency-groups') or {}):
154+ print('--group dev')
155+ elif 'dev' in (d.get('project', {}).get('optional-dependencies') or {}):
156+ print('--extra dev')
157+ else:
158+ print('error: no dev group/extra in pyproject.toml', file=sys.stderr); sys.exit(1)
159+ ")
160+ echo "Installing with: uv sync --frozen $choice"
161+ uv sync --frozen $choice
162+
163+ - name : Generate API reference (pydoc2json + gen_api_md)
164+ if : steps.cache.outputs.cache-hit != 'true'
165+ run : |
166+ source .venv/bin/activate
167+ python build_scripts/pydoc2json.py pyrit --submodules -o doc/_api/pyrit_all.json
168+ python build_scripts/gen_api_md.py
169+
170+ - name : Build the static HTML site
171+ if : steps.cache.outputs.cache-hit != 'true'
172+ # --all builds frontmatter exports (PDF, per doc/myst.yml).
173+ # --html is required to produce the static HTML site we deploy.
174+ # NOT using --strict because older release branches have known
175+ # warnings (e.g. unresolved cross-refs) that aren't worth fixing
176+ # retroactively just to satisfy strict mode.
177+ working-directory : doc
178+ run : |
179+ source ../.venv/bin/activate
180+ jupyter-book build --all --html
181+
182+ - name : Upload built site
183+ uses : actions/upload-artifact@v4
184+ with :
185+ name : site-${{ matrix.slug }}
186+ path : doc/_build/html
187+ retention-days : 7
188+ if-no-files-found : error
189+
190+ # ------------------------------------------------------------------
191+ # 3. Compose dist/, inject picker, deploy to GH Pages.
192+ # ------------------------------------------------------------------
193+ deploy :
194+ name : Deploy
195+ needs : [versions, build]
196+ runs-on : ubuntu-latest
197+ # Deploy on push to main, or when triggered manually from main. Skipped
198+ # for pushes to release branches (those builds are validation-only) and
199+ # for pull requests.
200+ if : >-
201+ ${{ github.ref == 'refs/heads/main' &&
202+ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
203+ environment :
204+ name : github-pages
205+ url : ${{ steps.deployment.outputs.page_url }}
206+ steps :
207+ - uses : actions/checkout@v6
208+ with :
209+ sparse-checkout : |
210+ .github/docs-versions.yml
211+ build_scripts/inject_version_picker.py
212+ build_scripts/version_picker_assets
213+ build_scripts/generate_pages_manifest.py
214+ build_scripts/compose_docs_dist.py
215+ sparse-checkout-cone-mode : false
216+
217+ - name : Set up Python 3.13
218+ uses : actions/setup-python@v6
219+ with :
220+ python-version : " 3.13"
221+
222+ - name : Install PyYAML
223+ run : pip install --quiet "pyyaml>=6.0"
224+
225+ - name : Download all version artifacts
226+ uses : actions/download-artifact@v4
227+ with :
228+ path : artifacts
229+ pattern : site-*
230+
231+ - name : Compose dist/
232+ # Compose script does everything: stage artifacts into dist/<slug>/,
233+ # write per-version pages.json manifests, write top-level versions.json,
234+ # write the root + /stable/ redirects, and write 404.html (with the
235+ # auto-redirect script that uses the manifest to find the closest
236+ # sibling page in the same version).
237+ run : |
238+ python build_scripts/compose_docs_dist.py \
239+ --artifacts-dir artifacts \
240+ --dist-dir dist \
241+ --config .github/docs-versions.yml \
242+ --base "${DOCS_BASE}"
243+
244+ - name : Inject version picker
245+ run : |
246+ python build_scripts/inject_version_picker.py \
247+ --site-dir dist \
248+ --base "${DOCS_BASE}"
249+
250+ - name : Upload Pages artifact
251+ uses : actions/upload-pages-artifact@v5
252+ with :
253+ path : dist
254+
255+ - name : Deploy to GitHub Pages
256+ id : deployment
257+ uses : actions/deploy-pages@v5
0 commit comments