From dde8d38b5084f5210b0de1b94161a527afe8f115 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Sun, 20 Sep 2026 16:16:19 -0400 Subject: [PATCH 1/2] feat(docs): support versioned Zensical deployments --- .github/workflows/docs-deploy.yml | 97 +++++++++++++++++++- .github/workflows/docs.yml | 144 +++++++++++++++++++++++++++++- docs/documentation-workflows.md | 67 +++++++++++++- tests/test_docs.py | 18 ++++ zensical.toml | 7 ++ 5 files changed, 328 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index f01625f..78ab705 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -4,6 +4,8 @@ on: push: branches: - main + tags: + - 'v*' paths: - '.github/workflows/docs.yml' - '.github/workflows/docs-deploy.yml' @@ -12,23 +14,116 @@ on: - 'pyproject.toml' - 'src/common_python_tasks/tasks.py' workflow_dispatch: + inputs: + source_ref: + description: Optional tag or commit to build for a manual version deployment + required: false + default: '' + type: string + docs_version: + description: Optional version identifier, such as 0.12 + required: false + default: '' + type: string + docs_version_title: + description: Optional version title, such as 0.12.1 + required: false + default: '' + type: string + update_latest: + description: Move the latest alias to this version + required: false + default: false + type: boolean + default_version: + description: Optional version or alias used as the site root + required: false + default: '' + type: string concurrency: group: github-pages cancel-in-progress: false jobs: + metadata: + name: Resolve documentation version + runs-on: ubuntu-latest + outputs: + aliases: ${{ steps.metadata.outputs.aliases }} + checkout_ref: ${{ steps.metadata.outputs.checkout_ref }} + default_version: ${{ steps.metadata.outputs.default_version }} + version: ${{ steps.metadata.outputs.version }} + version_title: ${{ steps.metadata.outputs.version_title }} + steps: + - name: Resolve version metadata + id: metadata + env: + DEFAULT_VERSION: ${{ inputs.default_version }} + DOCS_VERSION: ${{ inputs.docs_version }} + DOCS_VERSION_TITLE: ${{ inputs.docs_version_title }} + REF_NAME: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} + SOURCE_REF: ${{ inputs.source_ref }} + UPDATE_LATEST: ${{ inputs.update_latest }} + run: | + if [[ -n "$DOCS_VERSION" ]]; then + manual_inputs="$DOCS_VERSION$DOCS_VERSION_TITLE$SOURCE_REF$DEFAULT_VERSION" + if [[ "$manual_inputs" == *$'\n'* || "$manual_inputs" == *$'\r'* ]]; then + echo 'Manual documentation inputs cannot contain newlines.' >&2 + exit 2 + fi + if [[ "$UPDATE_LATEST" == "true" ]]; then + aliases='["latest"]' + else + aliases='[]' + fi + { + echo "aliases=$aliases" + echo "checkout_ref=$SOURCE_REF" + echo "default_version=$DEFAULT_VERSION" + echo "version=$DOCS_VERSION" + echo "version_title=$DOCS_VERSION_TITLE" + } >> "$GITHUB_OUTPUT" + elif [[ "$REF_TYPE" == "tag" ]]; then + if [[ ! "$REF_NAME" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + echo "Stable documentation tags must use vMAJOR.MINOR.PATCH: $REF_NAME" >&2 + exit 2 + fi + { + echo 'aliases=["latest"]' + echo 'checkout_ref=' + echo 'default_version=latest' + echo "version=${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + echo "version_title=${REF_NAME#v}" + } >> "$GITHUB_OUTPUT" + else + { + echo 'aliases=[]' + echo 'checkout_ref=' + echo 'default_version=' + echo 'version=dev' + echo 'version_title=Development' + } >> "$GITHUB_OUTPUT" + fi + docs: + needs: metadata uses: ./.github/workflows/docs.yml with: python_version: '3.14' dependency_group: '' locked: false artifact_name: common-python-tasks-docs + checkout_ref: ${{ needs.metadata.outputs.checkout_ref }} generated_docs_check_task: check-docs-references deploy_github_pages: true + docs_version: ${{ needs.metadata.outputs.version }} + docs_version_title: ${{ needs.metadata.outputs.version_title }} + docs_aliases: ${{ needs.metadata.outputs.aliases }} + docs_default_version: ${{ needs.metadata.outputs.default_version }} permissions: - contents: read + contents: write deployments: write pages: write id-token: write diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c21abe6..caf9062 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,11 +32,41 @@ on: required: false default: docs-site type: string + checkout_ref: + description: Optional Git ref to check out when building documentation + required: false + default: '' + type: string deploy_github_pages: description: Deploy the rendered site to GitHub Pages required: false default: false type: boolean + docs_version: + description: Optional version identifier used for a versioned GitHub Pages deployment + required: false + default: '' + type: string + docs_version_title: + description: Optional display title for the documentation version + required: false + default: '' + type: string + docs_aliases: + description: JSON array of aliases assigned to the documentation version + required: false + default: '[]' + type: string + docs_default_version: + description: Optional version or alias used as the documentation site root + required: false + default: '' + type: string + docs_versions_branch: + description: Git branch that stores assembled versioned documentation + required: false + default: gh-pages + type: string publish_cloudflare: description: Publish an internal pull request to Cloudflare Pages required: false @@ -74,12 +104,19 @@ on: description: Stable Cloudflare Pages branch alias when a preview was published value: ${{ jobs.build.outputs.preview_url }} +concurrency: + group: >- + ${{ inputs.docs_version != '' && + format('versioned-docs-{0}-{1}', github.repository, inputs.docs_versions_branch) || + format('docs-build-{0}', github.run_id) }} + cancel-in-progress: false + jobs: build: name: Build documentation runs-on: ubuntu-latest permissions: - contents: read + contents: write deployments: write pages: write outputs: @@ -87,6 +124,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7 + with: + fetch-depth: 0 + ref: ${{ inputs.checkout_ref }} - name: Set up Python ${{ inputs.python_version }} uses: actions/setup-python@v7 @@ -131,6 +171,100 @@ jobs: path: ${{ inputs.site_path }}/ if-no-files-found: error + - name: Validate versioned documentation configuration + if: inputs.docs_version != '' + env: + DEPLOY_GITHUB_PAGES: ${{ inputs.deploy_github_pages }} + DOCS_ALIASES: ${{ inputs.docs_aliases }} + DOCS_DEFAULT_VERSION: ${{ inputs.docs_default_version }} + DOCS_VERSION: ${{ inputs.docs_version }} + DOCS_VERSION_TITLE: ${{ inputs.docs_version_title }} + DOCS_VERSIONS_BRANCH: ${{ inputs.docs_versions_branch }} + run: | + identifier_pattern='^[A-Za-z0-9][A-Za-z0-9._-]*$' + if [[ "$DEPLOY_GITHUB_PAGES" != "true" ]]; then + echo 'A documentation version requires GitHub Pages deployment.' >&2 + exit 2 + fi + if [[ ! "$DOCS_VERSION" =~ $identifier_pattern ]]; then + echo "Invalid documentation version: $DOCS_VERSION" >&2 + exit 2 + fi + if [[ "$DOCS_VERSION_TITLE" == *$'\n'* || "$DOCS_VERSION_TITLE" == *$'\r'* ]]; then + echo 'Documentation version titles cannot contain newlines.' >&2 + exit 2 + fi + if [[ -n "$DOCS_DEFAULT_VERSION" && ! "$DOCS_DEFAULT_VERSION" =~ $identifier_pattern ]]; then + echo "Invalid default documentation version: $DOCS_DEFAULT_VERSION" >&2 + exit 2 + fi + if ! git check-ref-format "refs/heads/$DOCS_VERSIONS_BRANCH"; then + echo "Invalid documentation versions branch: $DOCS_VERSIONS_BRANCH" >&2 + exit 2 + fi + if ! jq --exit-status \ + --arg pattern "$identifier_pattern" \ + 'type == "array" and all(.[]; type == "string" and test($pattern))' \ + <<< "$DOCS_ALIASES" > /dev/null; then + echo 'Documentation aliases must be a JSON array of valid identifiers.' >&2 + exit 2 + fi + + - name: Install versioned documentation support + if: inputs.docs_version != '' + env: + MIKE_PACKAGE: mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac + run: uv pip install "$MIKE_PACKAGE" + + - name: Assemble versioned documentation + if: inputs.docs_version != '' + env: + DOCS_ALIASES: ${{ inputs.docs_aliases }} + DOCS_DEFAULT_VERSION: ${{ inputs.docs_default_version }} + DOCS_VERSION: ${{ inputs.docs_version }} + DOCS_VERSION_TITLE: ${{ inputs.docs_version_title }} + DOCS_VERSIONS_BRANCH: ${{ inputs.docs_versions_branch }} + SITE_PATH: ${{ inputs.site_path }} + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + + args=( + deploy + --branch "$DOCS_VERSIONS_BRANCH" + --alias-type redirect + --update-aliases + ) + if [[ -n "$DOCS_VERSION_TITLE" ]]; then + args+=(--title "$DOCS_VERSION_TITLE") + fi + args+=("$DOCS_VERSION") + mapfile -t aliases < <(jq --raw-output '.[]' <<< "$DOCS_ALIASES") + args+=("${aliases[@]}") + uv run --no-sync mike "${args[@]}" + + if ! grep --recursive --include='*.html' --extended-regexp --quiet \ + '"provider"[[:space:]]*:[[:space:]]*"mike"' \ + "$SITE_PATH"; then + echo 'The versioned build does not enable the mike version selector.' >&2 + exit 2 + fi + + if [[ -n "$DOCS_DEFAULT_VERSION" ]]; then + uv run --no-sync mike set-default \ + --branch "$DOCS_VERSIONS_BRANCH" \ + "$DOCS_DEFAULT_VERSION" + elif ! git cat-file --exit-code \ + "$DOCS_VERSIONS_BRANCH:index.html" > /dev/null 2>&1; then + uv run --no-sync mike set-default \ + --branch "$DOCS_VERSIONS_BRANCH" \ + "$DOCS_VERSION" + fi + git push -- origin "$DOCS_VERSIONS_BRANCH" + + mkdir versioned-site + git archive "$DOCS_VERSIONS_BRANCH" | tar --extract --directory versioned-site + - name: Validate Cloudflare preview configuration if: inputs.publish_cloudflare env: @@ -344,11 +478,17 @@ jobs: uses: actions/configure-pages@v6 - name: Upload GitHub Pages artifact - if: inputs.deploy_github_pages + if: inputs.deploy_github_pages && inputs.docs_version == '' uses: actions/upload-pages-artifact@v5 with: path: ${{ inputs.site_path }}/ + - name: Upload versioned GitHub Pages artifact + if: inputs.deploy_github_pages && inputs.docs_version != '' + uses: actions/upload-pages-artifact@v5 + with: + path: versioned-site/ + deploy: name: Deploy documentation if: inputs.deploy_github_pages diff --git a/docs/documentation-workflows.md b/docs/documentation-workflows.md index e669d29..edaf00d 100644 --- a/docs/documentation-workflows.md +++ b/docs/documentation-workflows.md @@ -83,9 +83,9 @@ jobs: The project is created on the first trusted pull request, using `main` as its production branch by default. Set `cloudflare_production_branch` if the repository uses a different default branch. Set `cloudflare_preview_domain` to publish previews at `pr-42.preview.example.com`; leave it empty to use `pr-42.stacksmith-docs.pages.dev`. Set `cloudflare_preview_zone` when the preview domain is a subdomain of the DNS zone. The API token needs Pages Write, Zone Read, and Zone DNS Edit permissions when custom preview domains are enabled. Fork pull requests receive the build artifact but are not published because repository secrets are unavailable. A separate `pull_request`-closed workflow can remove the custom hostname, its matching DNS record, and older deployments for that preview branch while preserving the shared Pages project. Cloudflare retains the latest branch deployment. -## Deploy production documentation to GitHub Pages +## Deploy a single documentation site to GitHub Pages -Enable GitHub Actions as the repository's Pages source. A default-branch caller can then select production deployment. +Enable GitHub Actions as the repository's Pages source. A default-branch caller can then select production deployment. Existing callers continue to use this mode when `docs_version` is empty. ```yaml jobs: @@ -102,3 +102,66 @@ jobs: ``` Pin the workflow to the release that matches the installed package. Pin an exact commit SHA when an immutable workflow reference is required. + +## Deploy versioned documentation to GitHub Pages + +Versioned deployment uses the Zensical-compatible `mike` fork. The workflow keeps generated versions on a Git branch and publishes the assembled branch through GitHub Actions, so the repository's Pages source remains GitHub Actions. The fork is a transitional dependency until Zensical provides native versioning support. + +Enable the version selector in `zensical.toml`. + +```toml +[project.extra.version] +provider = "mike" +default = ["latest", "dev"] + +[project.plugins.mike] +alias_type = "redirect" +``` + +Pass a version identifier when deploying. The caller must grant `contents: write` so the workflow can update the versions branch. + +```yaml +jobs: + docs: + uses: ci-sourcerer/common-python-tasks/.github/workflows/docs.yml@v0.12.1 + with: + python_version: '3.14' + dependency_group: dev + deploy_github_pages: true + docs_version: '2.4' + docs_version_title: '2.4.3' + docs_aliases: '["latest"]' + docs_default_version: latest + permissions: + contents: write + pages: write + id-token: write +``` + +`docs_aliases` must be a JSON array. Versions and aliases accept letters, digits, periods, underscores, and hyphens. The default versions branch is `gh-pages`; set `docs_versions_branch` to use another valid Git branch. The reusable workflow serializes updates to each repository and versions branch. Alias redirects are updated atomically with the version metadata, and versions not named by the current deployment remain unchanged. + +The workflow still performs the strict `poe docs-build` check and uploads the current checkout as the normal Actions artifact. It then builds through `mike`, updates the versions branch, and gives the complete assembled site to GitHub Pages. + +## Publish development and release versions + +Use one serialized production workflow for default-branch and tag deployment. Publish the default branch as `dev`, and publish stable tags to a major/minor documentation series. For example, `v2.4.3` updates version `2.4`, uses `2.4.3` as its display title, moves the `latest` alias, and makes `latest` the site root. Later default-branch deployments update `dev` without moving the root away from `latest`. + +The repository's own deployment workflow demonstrates this policy. Pull requests continue to use the unversioned artifact and Cloudflare preview flow, so a pull request previews the next `dev` site without modifying the versions branch. + +## Backfill an existing release + +A repository can expose manual workflow inputs that pass an older tag through `checkout_ref` while deploying it under an explicit documentation version. For this repository, run `docs-deploy` manually with values such as the following. + +| Input | Value | +| - | - | +| `source_ref` | A tag that already enables the `mike` version selector, or empty to build the selected workflow ref | +| `docs_version` | `0.12` | +| `docs_version_title` | `0.12.1` | +| `update_latest` | `true` | +| `default_version` | `latest` | + +Backfill supported series from oldest to newest so the selector order and `latest` alias finish in the desired state. Historical refs created before versioning was configured cannot produce a working selector without a compatible configuration change; build those docs from a suitable maintenance branch or the current branch instead. A normal default-branch deployment uses `dev` as the initial site root only when the versions branch has no root redirect, which keeps the documentation reachable before the first release or backfill. + +## Maintain published versions + +The versions branch contains `versions.json`, the generated version directories, aliases, and the root redirect. Inspect a local checkout with the same pinned Zensical-compatible fork before deleting or retitling published versions. Since the fork is installed directly from GitHub, changes to its pinned commit require a versioned workflow update and a representative multi-version build test. diff --git a/tests/test_docs.py b/tests/test_docs.py index 116ba40..9a831fa 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -29,6 +29,17 @@ def test_zensical_site_has_expected_pages(): assert Path("docs/tasks/reference/build-image.md").is_file() +def test_zensical_site_enables_mike_versioning(): + with Path("zensical.toml").open("rb") as config_file: + project = tomllib.load(config_file)["project"] + + assert project["extra"]["version"] == { + "provider": "mike", + "default": ["latest", "dev"], + } + assert project["plugins"]["mike"]["alias_type"] == "redirect" + + def test_reusable_docs_workflow_supports_artifacts_and_publishers(): workflow = Path(".github/workflows/docs.yml").read_text(encoding="utf-8") @@ -44,6 +55,11 @@ def test_reusable_docs_workflow_supports_artifacts_and_publishers(): ) assert "actions/upload-pages-artifact@v5" in workflow assert "actions/deploy-pages@v5" in workflow + assert "docs_version:" in workflow + assert "docs_aliases:" in workflow + assert "docs_default_version:" in workflow + assert "github.com/squidfunk/mike.git@2d4ad799" in workflow + assert "Upload versioned GitHub Pages artifact" in workflow def test_repository_uses_reusable_docs_workflow(): @@ -67,6 +83,8 @@ def test_repository_uses_reusable_docs_workflow(): assert "generated_docs_check_task: check-docs-references" in preview_workflow assert "uses: ./.github/workflows/docs.yml" in deploy_workflow assert "deploy_github_pages: true" in deploy_workflow + assert "docs_version: ${{ needs.metadata.outputs.version }}" in deploy_workflow + assert "- 'v*'" in deploy_workflow assert "generated_docs_check_task: check-docs-references" in deploy_workflow cleanup_workflow = Path(".github/workflows/docs-preview-cleanup.yml").read_text( encoding="utf-8" diff --git a/zensical.toml b/zensical.toml index f1a02f2..0499649 100644 --- a/zensical.toml +++ b/zensical.toml @@ -5,6 +5,13 @@ site_url = "https://ci-sourcerer.github.io/common-python-tasks/" repo_name = "ci-sourcerer/common-python-tasks" repo_url = "https://github.com/ci-sourcerer/common-python-tasks" +[project.extra.version] +provider = "mike" +default = ["latest", "dev"] + +[project.plugins.mike] +alias_type = "redirect" + [project.theme] features = [ "navigation.sections", From 7adfe48c2423204dfa7eba6d6e9ca849539ebe20 Mon Sep 17 00:00:00 2001 From: Joseph Asbury Date: Sun, 20 Sep 2026 16:23:47 -0400 Subject: [PATCH 2/2] refactor(docs): make versioning the default --- .github/workflows/docs.yml | 25 +++++++------------------ docs/documentation-workflows.md | 28 ++++++++++++++++++++++------ tests/test_docs.py | 9 +++++++++ 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index caf9062..f0b328c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -43,9 +43,9 @@ on: default: false type: boolean docs_version: - description: Optional version identifier used for a versioned GitHub Pages deployment + description: Version identifier used for a GitHub Pages deployment required: false - default: '' + default: dev type: string docs_version_title: description: Optional display title for the documentation version @@ -106,7 +106,7 @@ on: concurrency: group: >- - ${{ inputs.docs_version != '' && + ${{ inputs.deploy_github_pages && format('versioned-docs-{0}-{1}', github.repository, inputs.docs_versions_branch) || format('docs-build-{0}', github.run_id) }} cancel-in-progress: false @@ -172,9 +172,8 @@ jobs: if-no-files-found: error - name: Validate versioned documentation configuration - if: inputs.docs_version != '' + if: inputs.deploy_github_pages env: - DEPLOY_GITHUB_PAGES: ${{ inputs.deploy_github_pages }} DOCS_ALIASES: ${{ inputs.docs_aliases }} DOCS_DEFAULT_VERSION: ${{ inputs.docs_default_version }} DOCS_VERSION: ${{ inputs.docs_version }} @@ -182,10 +181,6 @@ jobs: DOCS_VERSIONS_BRANCH: ${{ inputs.docs_versions_branch }} run: | identifier_pattern='^[A-Za-z0-9][A-Za-z0-9._-]*$' - if [[ "$DEPLOY_GITHUB_PAGES" != "true" ]]; then - echo 'A documentation version requires GitHub Pages deployment.' >&2 - exit 2 - fi if [[ ! "$DOCS_VERSION" =~ $identifier_pattern ]]; then echo "Invalid documentation version: $DOCS_VERSION" >&2 exit 2 @@ -211,13 +206,13 @@ jobs: fi - name: Install versioned documentation support - if: inputs.docs_version != '' + if: inputs.deploy_github_pages env: MIKE_PACKAGE: mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac run: uv pip install "$MIKE_PACKAGE" - name: Assemble versioned documentation - if: inputs.docs_version != '' + if: inputs.deploy_github_pages env: DOCS_ALIASES: ${{ inputs.docs_aliases }} DOCS_DEFAULT_VERSION: ${{ inputs.docs_default_version }} @@ -477,14 +472,8 @@ jobs: if: inputs.deploy_github_pages uses: actions/configure-pages@v6 - - name: Upload GitHub Pages artifact - if: inputs.deploy_github_pages && inputs.docs_version == '' - uses: actions/upload-pages-artifact@v5 - with: - path: ${{ inputs.site_path }}/ - - name: Upload versioned GitHub Pages artifact - if: inputs.deploy_github_pages && inputs.docs_version != '' + if: inputs.deploy_github_pages uses: actions/upload-pages-artifact@v5 with: path: versioned-site/ diff --git a/docs/documentation-workflows.md b/docs/documentation-workflows.md index edaf00d..4e5b875 100644 --- a/docs/documentation-workflows.md +++ b/docs/documentation-workflows.md @@ -83,9 +83,9 @@ jobs: The project is created on the first trusted pull request, using `main` as its production branch by default. Set `cloudflare_production_branch` if the repository uses a different default branch. Set `cloudflare_preview_domain` to publish previews at `pr-42.preview.example.com`; leave it empty to use `pr-42.stacksmith-docs.pages.dev`. Set `cloudflare_preview_zone` when the preview domain is a subdomain of the DNS zone. The API token needs Pages Write, Zone Read, and Zone DNS Edit permissions when custom preview domains are enabled. Fork pull requests receive the build artifact but are not published because repository secrets are unavailable. A separate `pull_request`-closed workflow can remove the custom hostname, its matching DNS record, and older deployments for that preview branch while preserving the shared Pages project. Cloudflare retains the latest branch deployment. -## Deploy a single documentation site to GitHub Pages +## Deploy documentation to GitHub Pages -Enable GitHub Actions as the repository's Pages source. A default-branch caller can then select production deployment. Existing callers continue to use this mode when `docs_version` is empty. +Enable GitHub Actions as the repository's Pages source. GitHub Pages deployments are versioned by default and publish the current checkout as `dev` unless the caller selects another version. The caller must grant `contents: write` so the workflow can update the versions branch. ```yaml jobs: @@ -96,14 +96,14 @@ jobs: dependency_group: dev deploy_github_pages: true permissions: - contents: read + contents: write pages: write id-token: write ``` -Pin the workflow to the release that matches the installed package. Pin an exact commit SHA when an immutable workflow reference is required. +This initial deployment creates `dev` and uses it as the site root when the versions branch does not already contain a root redirect. Pin the workflow to the release that matches the installed package. Pin an exact commit SHA when an immutable workflow reference is required. -## Deploy versioned documentation to GitHub Pages +## Configure versioned documentation Versioned deployment uses the Zensical-compatible `mike` fork. The workflow keeps generated versions on a Git branch and publishes the assembled branch through GitHub Actions, so the repository's Pages source remains GitHub Actions. The fork is a transitional dependency until Zensical provides native versioning support. @@ -118,7 +118,7 @@ default = ["latest", "dev"] alias_type = "redirect" ``` -Pass a version identifier when deploying. The caller must grant `contents: write` so the workflow can update the versions branch. +Pass a version identifier, title, aliases, and default version when publishing a release. ```yaml jobs: @@ -142,6 +142,22 @@ jobs: The workflow still performs the strict `poe docs-build` check and uploads the current checkout as the normal Actions artifact. It then builds through `mike`, updates the versions branch, and gives the complete assembled site to GitHub Pages. +## Preview multiple versions locally + +`poe docs-serve` builds and watches only the current checkout. It is the quickest way to review content, but its version selector has no assembled `versions.json` to load. + +Install the same pinned Zensical-compatible `mike` fork used by the workflow, assemble two versions on a disposable local Git branch, and serve that branch. + +```shell +uv pip install 'mike @ git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac' +uv run --no-sync mike deploy --branch local-docs --alias-type redirect --update-aliases --title Development dev +uv run --no-sync mike deploy --branch local-docs --alias-type redirect --update-aliases --title 0.12.1 0.12 latest +uv run --no-sync mike set-default --branch local-docs latest +uv run --no-sync mike serve --branch local-docs +``` + +Open `http://localhost:8000/`. The root redirects to `latest`, and the selector switches between `0.12` and `dev`. These commands create only local commits on `local-docs`; they do not push anything. Stop the server before removing the disposable branch with `git branch -D local-docs`. + ## Publish development and release versions Use one serialized production workflow for default-branch and tag deployment. Publish the default branch as `dev`, and publish stable tags to a major/minor documentation series. For example, `v2.4.3` updates version `2.4`, uses `2.4.3` as its display title, moves the `latest` alias, and makes `latest` the site root. Later default-branch deployments update `dev` without moving the root away from `latest`. diff --git a/tests/test_docs.py b/tests/test_docs.py index 9a831fa..7ab65d4 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -60,6 +60,15 @@ def test_reusable_docs_workflow_supports_artifacts_and_publishers(): assert "docs_default_version:" in workflow assert "github.com/squidfunk/mike.git@2d4ad799" in workflow assert "Upload versioned GitHub Pages artifact" in workflow + assert ( + """docs_version: + description: Version identifier used for a GitHub Pages deployment + required: false + default: dev""" + in workflow + ) + assert workflow.count("actions/upload-pages-artifact@v5") == 1 + assert "if: inputs.deploy_github_pages && inputs.docs_version == ''" not in workflow def test_repository_uses_reusable_docs_workflow():