diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 9ebd94b..42e013c 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -1,26 +1,44 @@ -# 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 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 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: '' + 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: '' -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 }} - name: Install uv uses: astral-sh/setup-uv@v10.3.0 @@ -30,14 +48,21 @@ jobs: # This workflow contains a single **/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 @@ -55,37 +80,95 @@ 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 - # (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: - 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' }} + LANDING: ${{ inputs.landing }} - 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' + # 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} {landing} docs.' + f' Other versions: versions.html.

') + # a page without a version, e.g. /ISLP/labs.html (the links from + # before the versions): the landing version's + open('404.html', 'w').write( + f'{site} docs' + f'' + 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' + f'

    {site} documentation

    ') + 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 730dd39..da3f979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ 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 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