From 820f990b41f75a1c066603d97825a9027e9b8361 Mon Sep 17 00:00:00 2001 From: Jonathan Taylor Date: Tue, 27 Oct 2026 11:02:41 -0700 Subject: [PATCH 1/3] Docs for each version on gh-pages, without R Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build_docs.yml | 139 +++++++++++++++++++++++-------- CHANGELOG.md | 9 ++ 2 files changed, 112 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 1a69807..24f47f4 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -1,37 +1,56 @@ -# This builds and deploys ISLP docs +# This builds the ISLP docs into a folder of the gh-pages branch, one for +# each version: / for a release (its tag's docs and labs, with its +# package from PyPI), dev/ for the branch the workflow is run on. The site's +# root redirects to the latest release, versions.html lists them, and a path +# without a version (e.g. /ISLP/labs.html) goes to the latest release's page. +# +# gh workflow run build_docs.yml # dev/, from main +# gh workflow run build_docs.yml -f version=0.6.0 # 0.6.0/, after its PyPI upload name: Build docs -# Controls when the workflow will run on: workflow_dispatch: - inputs: null - -# A workflow run is made up of one or more jobs that can run -# sequentially or in parallel + inputs: + version: + description: 'A release (e.g. 0.6.0), built from its tag and its PyPI package; empty for dev/, from source' + required: false + default: '' -jobs: # This workflow contains a single - # job called "build" +# one push to gh-pages at a time +concurrency: + group: gh-pages + cancel-in-progress: false + +jobs: build: - # The type of runner that the job will run on runs-on: ubuntu-latest + env: + VERSION: ${{ inputs.version }} - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + # a release's docs (and the labs its submodule pins) are those of its tag - uses: actions/checkout@v7 + with: + ref: ${{ inputs.version && format('v{0}', inputs.version) || github.ref }} + - uses: actions/setup-python@v7 with: python-version: '3.12' cache: 'pip' - # Install - - name: Install dependencies + + # docs/requirements.txt but rpy2: no page or lab runs R + - name: Install dependencies run: | - sudo apt-get update - sudo apt-get install -y r-base libtirpc-dev - pip install -r docs/requirements.txt - pip install . + grep -v '^rpy2' docs/requirements.txt > docs-requirements.txt + pip install -r docs-requirements.txt + if [ -n "$VERSION" ]; then + pip install "ISLP==$VERSION" + else + pip install . + fi + (cd /tmp && python -c "import importlib.metadata as m; print('ISLP', m.version('ISLP'), 'glmstar', m.version('glmstar'))") # Checkout labs - name: Checkout version of labs @@ -49,35 +68,83 @@ jobs: # This workflow contains a single cd docs make html - # Store the output - name: Upload docs uses: actions/upload-artifact@v7 with: name: ISLP_docs path: docs/build/html retention-days: 5 + if-no-files-found: error deploy: runs-on: ubuntu-latest needs: build - - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source + contents: write # to push to gh-pages + env: + FOLDER: ${{ inputs.version || 'dev' }} - environment: - name: github-pages - url: ${{steps.deployment.outputs.page_url}} - steps: - - uses: actions/download-artifact@v8 - with: - name: ISLP_docs - path: . - - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v5 - with: - path: . - - id: deployment - uses: actions/deploy-pages@v5 + - uses: actions/download-artifact@v8 + with: + name: ISLP_docs + path: built + + # the folder replaced, then the root redirect, 404.html and versions.html + # remade from the folders there + - name: Push to gh-pages + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + SITE: ${{ github.event.repository.name }} + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + git init -q pages && cd pages + git remote add origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" + if git fetch -q --depth 1 origin gh-pages; then + git checkout -q -b gh-pages FETCH_HEAD + else + git checkout -q --orphan gh-pages + fi + rm -rf "$FOLDER" + cp -r ../built "$FOLDER" + touch .nojekyll # (serve the folders starting with _ too) + python3 - <<'EOF' + import json, os, re + site = os.environ['SITE'] + releases = sorted((d for d in os.listdir('.') if re.fullmatch(r'\d+\.\d+\.\d+', d)), + key=lambda v: tuple(map(int, v.split('.'))), reverse=True) + folders = releases + (['dev'] if os.path.isdir('dev') else []) + latest = releases[0] if releases else 'dev' + json.dump({'latest': latest, 'versions': folders}, open('versions.json', 'w'), indent=1) + open('index.html', 'w').write( + f'{site} docs' + f'' + f'

Redirecting to the {site} {latest} docs.' + f' Other versions: versions.html.

') + # a page without a version, e.g. /ISLP/labs.html (the links from + # before the versions): the latest release's + open('404.html', 'w').write( + f'{site} docs' + f'' + f'

Not found. The {site} {latest} docs,' + f' or other versions.

') + items = ''.join(f'
  • {v}' + f'{" (latest release)" if v == latest else ""}' + f'{" (main)" if v == "dev" else ""}
  • ' for v in folders) + open('versions.html', 'w').write( + f'{site} docs: versions' + f'

    {site} documentation

      {items}
    ') + EOF + cat versions.json + git add -A + git commit -q -m "Docs for $FOLDER (${GITHUB_SHA::7})" + git push -q origin gh-pages diff --git a/CHANGELOG.md b/CHANGELOG.md index c76caa3..4436b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ [modelmatrix](https://github.com/intro-stat-learning/modelmatrix), which ISLP now depends on (`modelmatrix>=0.3`). +### Documentation + +- **Docs for each version**: the docs workflow builds a release's docs + (from its tag, with its labs, and the package from PyPI) into + `/` of the `gh-pages` branch, and main's into `dev/`. The site's + root redirects to the latest release, `versions.html` lists them, and a + page without a version (e.g. `/ISLP/labs.html`) goes to the latest + release's. The docs no longer install R, nor `rpy2`: no page or lab runs R. + ### Breaking changes - **`ModelSpec`'s `intercept` defaults to `False`**, as 0.6.0 announced. Pass From 43778628c5d7df63c140b7c90cd59ff9f1d0bde3 Mon Sep 17 00:00:00 2001 From: Jonathan Taylor Date: Tue, 27 Oct 2026 11:10:26 -0700 Subject: [PATCH 2/3] Docs: the landing version, kept in landing.txt Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build_docs.yml | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 24f47f4..82ba5bd 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -1,8 +1,9 @@ # This builds the ISLP docs into a folder of the gh-pages branch, one for # each version: / for a release (its tag's docs and labs, with its # package from PyPI), dev/ for the branch the workflow is run on. The site's -# root redirects to the latest release, versions.html lists them, and a path -# without a version (e.g. /ISLP/labs.html) goes to the latest release's page. +# root redirects to the landing version (the latest release, unless set with +# -f landing=X.Y.Z), versions.html lists them, and a path without a version +# (e.g. /ISLP/labs.html) goes to the landing version's page. # # gh workflow run build_docs.yml # dev/, from main # gh workflow run build_docs.yml -f version=0.6.0 # 0.6.0/, after its PyPI upload @@ -16,6 +17,10 @@ on: description: 'A release (e.g. 0.6.0), built from its tag and its PyPI package; empty for dev/, from source' required: false default: '' + landing: + description: 'The version the site root (and a page without a version) goes to, kept in landing.txt; empty: as before (the latest release, if never set)' + required: false + default: '' # one push to gh-pages at a time concurrency: @@ -83,6 +88,7 @@ jobs: contents: write # to push to gh-pages env: FOLDER: ${{ inputs.version || 'dev' }} + LANDING: ${{ inputs.landing }} steps: - uses: actions/download-artifact@v8 @@ -117,14 +123,21 @@ jobs: key=lambda v: tuple(map(int, v.split('.'))), reverse=True) folders = releases + (['dev'] if os.path.isdir('dev') else []) latest = releases[0] if releases else 'dev' - json.dump({'latest': latest, 'versions': folders}, open('versions.json', 'w'), indent=1) + # the landing version: given, or kept from before in landing.txt, or the latest + if os.environ.get('LANDING'): + open('landing.txt', 'w').write(os.environ['LANDING'] + '\n') + landing = open('landing.txt').read().strip() if os.path.exists('landing.txt') else latest + if landing not in folders: + raise SystemExit(f'the landing version {landing} has no docs here: {folders}') + json.dump({'latest': latest, 'landing': landing, 'versions': folders}, + open('versions.json', 'w'), indent=1) open('index.html', 'w').write( f'{site} docs' - f'' - f'

    Redirecting to the {site} {latest} docs.' + f'' + f'

    Redirecting to the {site} {landing} docs.' f' Other versions: versions.html.

    ') # a page without a version, e.g. /ISLP/labs.html (the links from - # before the versions): the latest release's + # before the versions): the landing version's open('404.html', 'w').write( f'{site} docs' f'' - f'

    Not found. The {site} {latest} docs,' + f'

    Not found. The {site} {landing} docs,' f' or other versions.

    ') items = ''.join(f'
  • {v}' f'{" (latest release)" if v == latest else ""}' + f'{" (the default)" if v == landing and v != latest else ""}' f'{" (main)" if v == "dev" else ""}
  • ' for v in folders) open('versions.html', 'w').write( f'{site} docs: versions' From 65ef9664795d91eeb609a64ce6aa47855da2113b Mon Sep 17 00:00:00 2001 From: Jonathan Taylor Date: Tue, 27 Oct 2026 11:26:24 -0700 Subject: [PATCH 3/3] Docs: restore the versions' checkout and install after the merge with #74 The merge took main's checkout and install, so a release's build used main's docs and ISLP from source. Back: the tag's checkout, ISLP==X.Y.Z from PyPI (with uv), rpy2 filtered from an old tag's requirements, and the release builds deploy from any branch (dev/ only from main). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build_docs.yml | 24 ++++++++++++++++-------- CHANGELOG.md | 7 ++++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 45a75eb..42e013c 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -37,6 +37,8 @@ jobs: steps: # a release's docs (and the labs its submodule pins) are those of its tag - uses: actions/checkout@v7 + with: + ref: ${{ inputs.version && format('v{0}', inputs.version) || github.ref }} - name: Install uv uses: astral-sh/setup-uv@v10.3.0 @@ -46,14 +48,21 @@ jobs: **/pyproject.toml docs/requirements.txt - # a virtual environment, on the PATH of the later steps (no R: no page - # or lab runs it) + # a virtual environment, on the PATH of the later steps; a release's + # package from PyPI. docs/requirements.txt but rpy2 (an old tag's lists + # it): no page or lab runs R - name: Install dependencies run: | uv venv --python 3.12 echo "$PWD/.venv/bin" >> "$GITHUB_PATH" - uv pip install -r docs/requirements.txt - uv pip install . + grep -v '^rpy2' docs/requirements.txt > docs-requirements.txt + uv pip install -r docs-requirements.txt + if [ -n "$VERSION" ]; then + uv pip install "ISLP==$VERSION" + else + uv pip install . + fi + uv pip list | grep -i -E '^(islp|glmstar|modelmatrix) ' # Checkout labs - name: Checkout version of labs @@ -82,10 +91,9 @@ jobs: deploy: runs-on: ubuntu-latest needs: build - # (a run on another branch only builds, to try it) - if: github.ref == 'refs/heads/main' - - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + # dev/ only from main (dev from another branch only builds, to try it); + # a release's docs are its tag's, whichever branch the workflow is run from + if: github.ref == 'refs/heads/main' || inputs.version != '' permissions: contents: write # to push to gh-pages env: diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b2009..da3f979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,10 @@ ISLP now depends on (`modelmatrix>=0.3`). - **Docs for each version**: the docs workflow builds a release's docs (from its tag, with its labs, and the package from PyPI) into `/` of the `gh-pages` branch, and main's into `dev/`. The site's - root redirects to the latest release, `versions.html` lists them, and a - page without a version (e.g. `/ISLP/labs.html`) goes to the latest - release's. The docs no longer install R, nor `rpy2`: no page or lab runs R. + root redirects to the landing version (`-f landing=X.Y.Z`, kept in + `landing.txt`; the latest release if never set), `versions.html` lists + them, and a page without a version (e.g. `/ISLP/labs.html`) goes to the + landing version's. - **The docs build with uv, without conda or R**: the docs workflow installs with uv, and `docs/make_notebooks.py` runs a lab in a uv virtual environment (Python 3.12 by default) instead of a conda