diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 72cc2ccf..9c257e9f 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -2,6 +2,10 @@ name: Documentation on: push: + branches: + - master + tags: + - 'v*' pull_request: branches: - '**' @@ -9,11 +13,16 @@ on: permissions: {} concurrency: + # Versioned docs update the same Git branch and Pages deployment, so publish + # jobs must run one at a time. Do not cancel queued release-tag deploys. group: pages cancel-in-progress: false jobs: build: + # Pushes are published by the deploy job. This job keeps PR docs checks fast + # without building the same commit twice. + if: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -28,26 +37,76 @@ jobs: cache: 'pip' - name: Install documentation dependencies run: make docs-install - - name: Build documentation - run: make docs-build - - name: Upload Pages artifact - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 - with: - path: site + - name: Configure git identity + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - name: Build versioned documentation + run: | + # Exercise the same `mike deploy` and export path used by releases, + # but on a local throwaway branch with no push, alias, or default. + VERSION=pr-preview \ + DOCS_DEPLOY_BRANCH=docs-preview \ + MIKE_FLAGS="--ignore-remote-status" \ + make docs-deploy + + DOCS_DEPLOY_BRANCH=docs-preview \ + DOCS_EXPORT_DIR=site-versioned \ + make docs-export deploy: - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - needs: build + # Release tags publish stable docs. Master publishes the moving `dev` + # version without moving the `latest` alias or default version. + if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) }} runs-on: ubuntu-latest timeout-minutes: 10 permissions: + # `mike` stores the complete versioned site on `gh-pages`. + contents: write + # GitHub Pages still publishes from this workflow's uploaded artifact. pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + with: + # `mike` needs the existing `gh-pages` branch history so each release + # adds one version without dropping older versions. + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: '3.14' + - name: Install documentation dependencies + run: make docs-install + - name: Configure git identity + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - name: Build and export documentation version + run: | + if [ "$GITHUB_REF" = "refs/heads/master" ]; then + # Publish current master as moving development docs only. + make docs-deploy VERSION=dev PUSH=1 + else + VERSION="${GITHUB_REF_NAME#v}" + # Build this tag into the version store, mark it as the only `latest`, + # then export the full versioned site for GitHub Actions Pages. + make docs-deploy VERSION="$VERSION" ALIASES=latest + make docs-set-default DEFAULT_VERSION=latest PUSH=1 + fi + + make docs-export + env: + # Let `mike` push through git without writing the token into .git/config. + GIT_CONFIG_COUNT: 1 + GIT_CONFIG_KEY_0: http.https://github.com/.extraheader + GIT_CONFIG_VALUE_0: "AUTHORIZATION: bearer ${{ github.token }}" + - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: site-versioned - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc565910..22491f09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,23 @@ The public documentation site is built with MkDocs Material. * `make docs-serve` (override port with `PORT=8008 make docs-serve`) +* Deploy a version of the documentation (with + [mike](https://github.com/jimporter/mike)): + + * `VERSION=3.2 ALIASES=latest PUSH=1 make docs-deploy` + * `VERSION=3.1 GIT_REF=v3.1.0 make docs-deploy` + * `DEFAULT_VERSION=latest PUSH=1 make docs-set-default` + * `make docs-export` + +The GitHub Pages source is GitHub Actions. The `gh-pages` branch is still used +by `mike` to store all rendered documentation versions, then the workflow +exports that branch and deploys it with `actions/deploy-pages`. + +Pushing to `master` publishes the current branch as the moving `dev` +documentation version. Pushing a `v*` release tag publishes that release +version, updates the `latest` docs alias, and keeps `latest` as the default +documentation version. + * Refresh bundled JSON-LD context files: * `make download-bundled-contexts` @@ -44,10 +61,12 @@ The public documentation site is built with MkDocs Material. * commit changes * `$EDITOR lib/pyld/__about__.py`: update to release version and remove `-dev` suffix. * `git commit CHANGELOG.md lib/pyld/__about__.py -m "Release {version}."` -* `git tag {version}` +* `git tag v{version}`. Release tags must use the `v` prefix because the + documentation publish workflow only runs for `v*` tags. * `$EDITOR lib/pyld/__about__.py`: update to next version and add `-dev` suffix. * `git commit lib/pyld/__about__.py -m "Start {next-version}."` -* `git push --tags` +* `git push origin v{version}`. This deploys the versioned documentation site + and updates the `latest` docs alias. To ensure a clean [package](https://pypi.org/project/PyLD/) upload to [PyPI](https://pypi.org/), use a clean checkout, and run the following: @@ -60,7 +79,7 @@ use a clean checkout, and run the following: for use in the upload command. * The below builds and uploads a sdist and wheel. Adjust as needed depending on how you manage and clean "dist/" dir files. -* `git checkout {version}` +* `git checkout v{version}` * `python3 -m build` * `twine check dist/*` * `twine upload -r PyLD dist/*` diff --git a/Makefile b/Makefile index 2904be4d..d5d20257 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,18 @@ -.PHONY: install test docs-install docs-build docs-serve upgrade-submodules download-bundled-contexts +.PHONY: install test docs-install docs-build docs-serve docs-deploy docs-export docs-set-default upgrade-submodules download-bundled-contexts PORT ?= 8000 +VERSION ?= +GIT_REF ?= +ALIASES ?= +DEFAULT_VERSION ?= latest +DOCS_ALIAS_TYPE ?= copy +PUSH ?= +DOCS_DEPLOY_BRANCH ?= gh-pages +DOCS_DEPLOY_REMOTE ?= origin +DOCS_EXPORT_DIR ?= site-versioned +DOCS_RETRO_WORKTREE ?= .docs-retro-worktree +MIKE_FLAGS ?= +MIKE_PUSH = $(if $(PUSH),--push,) install: pip install -e . @@ -18,6 +30,29 @@ docs-build: docs-serve: mkdocs serve --dev-addr 127.0.0.1:$(PORT) + +docs-deploy: + @test -n "$(VERSION)" || (echo "VERSION is required, e.g. VERSION=3.2 make docs-deploy"; exit 1) + @if [ -n "$(GIT_REF)" ]; then \ + test ! -e "$(DOCS_RETRO_WORKTREE)" || (echo "$(DOCS_RETRO_WORKTREE) already exists"; exit 1); \ + set -e; \ + trap 'git worktree remove --force "$(DOCS_RETRO_WORKTREE)"' EXIT; \ + git worktree add --detach "$(DOCS_RETRO_WORKTREE)" "$(GIT_REF)"; \ + : "Older tags predate Material's mike version selector config, so patch mkdocs.yml."; \ + python -c 'from pathlib import Path; p = Path("$(DOCS_RETRO_WORKTREE)/mkdocs.yml"); s = p.read_text(); b = "extra:\n version:\n provider: mike\n\n"; p.write_text(s if "provider: mike" in s else s.replace("extra_css:", b + "extra_css:", 1) if "extra_css:" in s else s.rstrip() + "\n\n" + b)'; \ + python -m pip install -e "$(DOCS_RETRO_WORKTREE)"; \ + mike deploy --config-file "$(DOCS_RETRO_WORKTREE)/mkdocs.yml" --update-aliases --alias-type=$(DOCS_ALIAS_TYPE) --remote $(DOCS_DEPLOY_REMOTE) --branch $(DOCS_DEPLOY_BRANCH) $(MIKE_PUSH) $(MIKE_FLAGS) $(VERSION) $(ALIASES); \ + else \ + mike deploy --update-aliases --alias-type=$(DOCS_ALIAS_TYPE) --remote $(DOCS_DEPLOY_REMOTE) --branch $(DOCS_DEPLOY_BRANCH) $(MIKE_PUSH) $(MIKE_FLAGS) $(VERSION) $(ALIASES); \ + fi + +docs-export: + @test ! -e "$(DOCS_EXPORT_DIR)" || (echo "$(DOCS_EXPORT_DIR) already exists"; exit 1) + mkdir "$(DOCS_EXPORT_DIR)" + git archive "$(DOCS_DEPLOY_BRANCH)" | tar -x -C "$(DOCS_EXPORT_DIR)" + +docs-set-default: + mike set-default --remote $(DOCS_DEPLOY_REMOTE) --branch $(DOCS_DEPLOY_BRANCH) $(MIKE_PUSH) $(MIKE_FLAGS) $(DEFAULT_VERSION) upgrade-submodules: git submodule update --remote --init --recursive diff --git a/docs/requirements.txt b/docs/requirements.txt index 949e1aca..f1b25e94 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,7 @@ -r ../requirements.txt typing_extensions mkdocs-material==9.7.6 +mike>=2.1 mkdocs-macros-plugin==1.5.0 mkdocs-awesome-pages-plugin==2.10.1 mkdocstrings[python]>=0.30 diff --git a/mkdocs.yml b/mkdocs.yml index 1c1c9f6c..589c2c78 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,10 @@ repo_name: digitalbazaar/pyld exclude_docs: | **/AGENTS.md +extra: + version: + provider: mike + extra_css: - stylesheets/extra.css