From f245923eaff6ac9481c020285efa712a0d95e132 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:32:35 +0200 Subject: [PATCH 01/22] Version the docs site with mike: stable release as public default, pre-releases and dev deployed alongside. --- .github/workflows/docs.yml | 91 ++++++++++++++++++++++++++++++++------ CONTRIBUTING.md | 17 +++++++ zensical.toml | 7 ++- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 15699a1..89839a6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,15 +4,24 @@ on: push: branches: - main - -permissions: - contents: read - pages: write - id-token: write + release: + types: + - published + workflow_dispatch: + inputs: + version: + description: "Docs version to deploy (e.g. 0.3), built from the selected ref" + required: true + type: string + latest: + description: "Point the 'latest' alias (public default) at this version" + required: false + type: boolean + default: false concurrency: - group: "pages" - cancel-in-progress: true + group: "docs" + cancel-in-progress: false defaults: run: @@ -21,6 +30,8 @@ defaults: jobs: build: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v6 @@ -30,13 +41,53 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 - - name: Build documentation - run: uvx --with "mkdocstrings[python]" zensical build + - 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: Upload artifact - uses: actions/upload-pages-artifact@v4 - with: - path: ./site + - name: Resolve docs version + id: docs + env: + EVENT_NAME: ${{ github.event_name }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_PRERELEASE: ${{ github.event.release.prerelease }} + INPUT_VERSION: ${{ inputs.version }} + INPUT_LATEST: ${{ inputs.latest }} + run: | + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + version="$INPUT_VERSION" + latest="$INPUT_LATEST" + elif [[ "$EVENT_NAME" == "release" ]]; then + # v0.4.0-rc.1 -> 0.4; pre-releases never move the 'latest' alias + version="$(echo "${RELEASE_TAG#v}" | cut -d. -f1,2)" + if [[ "$RELEASE_PRERELEASE" == "true" ]]; then + latest="false" + else + latest="true" + fi + else + version="dev" + latest="false" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "latest=${latest}" >> "$GITHUB_OUTPUT" + + - name: Deploy documentation version + env: + VERSION: ${{ steps.docs.outputs.version }} + LATEST: ${{ steps.docs.outputs.latest }} + run: | + mike() { + uvx --from git+https://github.com/squidfunk/mike.git \ + --with "mkdocstrings[python]" mike "$@" + } + if [[ "$LATEST" == "true" ]]; then + mike deploy --push --update-aliases "$VERSION" latest + mike set-default --push latest + else + mike deploy --push "$VERSION" + fi deploy: environment: @@ -44,7 +95,21 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build + permissions: + contents: read + pages: write + id-token: write + steps: + - uses: actions/checkout@v6 + with: + ref: gh-pages + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: . + - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e39cb7..c23cd97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,6 +84,23 @@ To create a release, please do the following: 7. Click "Publish Release". This will kick of a GitHub action to build the project and push the binaries + wheels to PyPI. 8. Celebrate a successful release! +### Documentation versions + +Publishing a release also deploys the documentation, versioned with +[mike](https://zensical.org/docs/setup/versioning/) on the `gh-pages` branch: + +- A stable release deploys its docs under `XX.YY/` and moves the `latest` + alias (the version the site root redirects to) to it. +- A pre-release (e.g. `vXX.YY.0-rc.1`) deploys under `XX.YY/` but leaves + `latest` untouched, so the public default stays on the latest stable + release. +- Every push to `main` refreshes the `dev` version. + +Docs for any version can be (re)deployed manually from the +[docs workflow](https://github.com/xqlsystems/xarray-sql/actions/workflows/docs.yml) +via "Run workflow": pick the git tag as the ref, set the docs version (e.g. +`0.3`), and tick "latest" only if the site root should point there. + ## Undoing a bad release We all mess up sometimes. For example, I have often forgotten to do one of the steps (often, step 1) in the above process, and it leads to a failed release (i.e. an unsuccessful push to PyPI.) diff --git a/zensical.toml b/zensical.toml index b80208b..a2c0f80 100644 --- a/zensical.toml +++ b/zensical.toml @@ -2,7 +2,7 @@ site_name = "xarray-sql" site_description = "Query Xarray with SQL" site_author = "Alexander Merose" -site_url = "https://xqlsystems.github.io/xarray-sql" +site_url = "https://xqlsystems.github.io/xarray-sql/" repo_url = "https://github.com/xqlsystems/xarray-sql" repo_name = "xqlsystems/xarray-sql" edit_uri = "edit/main/docs/" @@ -129,6 +129,11 @@ filters = ["!^_"] # Extra configuration [project.extra] +# Versioned docs via mike (Zensical fork); versions live on the gh-pages branch +[project.extra.version] +provider = "mike" +default = "latest" + [[project.extra.social]] icon = "fontawesome/brands/github" link = "https://github.com/xqlsystems/xarray-sql" From a8ad9a1eb4bbb70a1c5c801574333dd916fd5e2a Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:41:51 +0200 Subject: [PATCH 02/22] Point README docs links at the latest/ alias; the site root redirect only covers the root path. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 794605e..9386df6 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ rel = con.sql('SELECT time, AVG("air") AS air FROM air GROUP BY time ORDER BY ti xql.to_dataset(rel, template=ds) # any engine's Arrow result round-trips ``` -See [Engines](https://xqlsystems.github.io/xarray-sql/engines/) for the support matrix, DuckDB/Polars details, +See [Engines](https://xqlsystems.github.io/xarray-sql/latest/engines/) for the support matrix, DuckDB/Polars details, and the lazy chunked round-trip. ## A bigger example: ARCO-ERA5 @@ -245,7 +245,7 @@ Every case matches its array reference. The headline finding: these operations are not really "array" operations at all — they are `GROUP BY`, `JOIN`, window functions, and `CASE` in disguise, and a query engine runs them at scale. See [`benchmarks/geospatial/`](https://github.com/xqlsystems/xarray-sql/tree/main/benchmarks/geospatial/) and the write-up, -[Geospatial operations are relational operations](https://xqlsystems.github.io/xarray-sql/geospatial/). +[Geospatial operations are relational operations](https://xqlsystems.github.io/xarray-sql/latest/geospatial/). ## Why does this work? @@ -260,10 +260,10 @@ anything that speaks Arrow). ## What are the current limitations? The sharp edges we know about — per engine and fundamental — are cataloged in -[Known issues & limitations](https://xqlsystems.github.io/xarray-sql/limitations/). Currently, we're looking for +[Known issues & limitations](https://xqlsystems.github.io/xarray-sql/latest/limitations/). Currently, we're looking for early users – "tire kickers", if you will. We'd love your input to shape the direction of this project! Please, give this a try and [file issues](https://github.com/xqlsystems/xarray-sql/issues) as -you see fit. Check out our [contributing guide](https://xqlsystems.github.io/xarray-sql/contributing/), too 😉. +you see fit. Check out our [contributing guide](https://xqlsystems.github.io/xarray-sql/latest/contributing/), too 😉. ## What would a deeper integration look like? From b4408c04a614cdafc812c4a00f88a62c2c84c167 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:42:41 +0200 Subject: [PATCH 03/22] Skip Pages publish until a default docs version exists, so the pre-versioning site stays live through the migration. --- .github/workflows/docs.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 89839a6..506b37b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -105,11 +105,26 @@ jobs: with: ref: gh-pages + - name: Check a default version exists + id: bootstrapped + run: | + # The root redirect only exists once a stable version has been + # deployed and set as default; until then, keep serving the + # previously published site instead of a broken root + if [[ -f index.html ]]; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + echo "No default docs version set yet; skipping Pages publish" >&2 + fi + - name: Upload artifact + if: steps.bootstrapped.outputs.ok == 'true' uses: actions/upload-pages-artifact@v4 with: path: . - name: Deploy to GitHub Pages + if: steps.bootstrapped.outputs.ok == 'true' id: deployment uses: actions/deploy-pages@v5 From cce14a3168c9ec5cb2fa312603aa30f3fe693d3d Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:44:23 +0200 Subject: [PATCH 04/22] Serialize only the Pages publish and retry the gh-pages push: a queued workflow-level group can drop a pending release run, losing its latest-alias move. --- .github/workflows/docs.yml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 506b37b..d8e97cb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,10 +19,6 @@ on: type: boolean default: false -concurrency: - group: "docs" - cancel-in-progress: false - defaults: run: shell: bash @@ -82,11 +78,21 @@ jobs: uvx --from git+https://github.com/squidfunk/mike.git \ --with "mkdocstrings[python]" mike "$@" } + # Concurrent runs race on the gh-pages push; mike re-fetches the + # branch on each attempt, so retrying resolves the race + retry() { + for attempt in 1 2; do + "$@" && return 0 + echo "attempt ${attempt} failed; retrying" >&2 + sleep 15 + done + "$@" + } if [[ "$LATEST" == "true" ]]; then - mike deploy --push --update-aliases "$VERSION" latest - mike set-default --push latest + retry mike deploy --push --update-aliases "$VERSION" latest + retry mike set-default --push latest else - mike deploy --push "$VERSION" + retry mike deploy --push "$VERSION" fi deploy: @@ -99,6 +105,12 @@ jobs: contents: read pages: write id-token: write + # Serialize Pages publishes only; every build job must run so no mike + # deploy is lost. gh-pages state is cumulative, so a pending deploy + # superseded in this queue is covered by the newer one that replaced it + concurrency: + group: "pages" + cancel-in-progress: false steps: - uses: actions/checkout@v6 From 25500e68990b812b480bee2561a13b60cdcca8e1 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:45:16 +0200 Subject: [PATCH 05/22] Validate release tags and dispatch versions before handing them to mike as version identifiers. --- .github/workflows/docs.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d8e97cb..5629407 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -52,9 +52,19 @@ jobs: INPUT_LATEST: ${{ inputs.latest }} run: | if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + # Reject anything that isn't MAJOR.MINOR, in particular the + # reserved 'latest' and 'dev' identifiers + if [[ ! "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "Invalid docs version '$INPUT_VERSION'; expected MAJOR.MINOR (e.g. 0.3)" >&2 + exit 1 + fi version="$INPUT_VERSION" latest="$INPUT_LATEST" elif [[ "$EVENT_NAME" == "release" ]]; then + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+].*)?$ ]]; then + echo "Tag '$RELEASE_TAG' is not vMAJOR.MINOR.PATCH; refusing to deploy docs" >&2 + exit 1 + fi # v0.4.0-rc.1 -> 0.4; pre-releases never move the 'latest' alias version="$(echo "${RELEASE_TAG#v}" | cut -d. -f1,2)" if [[ "$RELEASE_PRERELEASE" == "true" ]]; then From 5816a38e047f458a59233c407d70992e77fb823d Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:46:05 +0200 Subject: [PATCH 06/22] Pin the mike fork to a commit SHA; it is installed from a mutable branch in a job with contents: write. --- .github/workflows/docs.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5629407..0232fc2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -84,8 +84,10 @@ jobs: VERSION: ${{ steps.docs.outputs.version }} LATEST: ${{ steps.docs.outputs.latest }} run: | + # The zensical fork of mike is GitHub-only (no PyPI releases), so + # pin it to a commit; this job holds contents: write mike() { - uvx --from git+https://github.com/squidfunk/mike.git \ + uvx --from "git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac" \ --with "mkdocstrings[python]" mike "$@" } # Concurrent runs race on the gh-pages push; mike re-fetches the From 8106da5b15fbef204df92cb6f4de5e301219b33b Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:46:05 +0200 Subject: [PATCH 07/22] Drop the redundant version default; the theme already falls back to the 'latest' alias. --- zensical.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/zensical.toml b/zensical.toml index a2c0f80..a820e55 100644 --- a/zensical.toml +++ b/zensical.toml @@ -132,7 +132,6 @@ filters = ["!^_"] # Versioned docs via mike (Zensical fork); versions live on the gh-pages branch [project.extra.version] provider = "mike" -default = "latest" [[project.extra.social]] icon = "fontawesome/brands/github" From e63fdab88f09d5253cbd52e7f1b2509482058ed1 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:46:44 +0200 Subject: [PATCH 08/22] Document how to roll back the docs deployment of a bad release. --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c23cd97..0ec1109 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,6 +101,11 @@ Docs for any version can be (re)deployed manually from the via "Run workflow": pick the git tag as the ref, set the docs version (e.g. `0.3`), and tick "latest" only if the site root should point there. +When undoing a bad release (see below), note that deleting the release and +tag does not undo its docs deployment. Re-run the docs workflow from the last +good tag with "latest" ticked to move the public default back, and remove the +bad version entirely with `mike delete --push XX.YY` locally if needed. + ## Undoing a bad release We all mess up sometimes. For example, I have often forgotten to do one of the steps (often, step 1) in the above process, and it leads to a failed release (i.e. an unsuccessful push to PyPI.) From 56c9bb581ba231c4361bf11f36dc68099a4936b3 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:53:06 +0200 Subject: [PATCH 09/22] Deploy pre-release docs to a rolling 'rc' version: a patch-level RC must not overwrite the stable X.Y that 'latest' points to. --- .github/workflows/docs.yml | 14 ++++++++++---- CONTRIBUTING.md | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0232fc2..53d1183 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -65,11 +65,15 @@ jobs: echo "Tag '$RELEASE_TAG' is not vMAJOR.MINOR.PATCH; refusing to deploy docs" >&2 exit 1 fi - # v0.4.0-rc.1 -> 0.4; pre-releases never move the 'latest' alias - version="$(echo "${RELEASE_TAG#v}" | cut -d. -f1,2)" if [[ "$RELEASE_PRERELEASE" == "true" ]]; then + # Pre-releases go to a rolling 'rc' version so they can never + # overwrite the stable X.Y that 'latest' may already point to + version="rc" + title="${RELEASE_TAG#v}" latest="false" else + # v0.4.0 -> 0.4 + version="$(echo "${RELEASE_TAG#v}" | cut -d. -f1,2)" latest="true" fi else @@ -77,11 +81,13 @@ jobs: latest="false" fi echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "title=${title:-${version}}" >> "$GITHUB_OUTPUT" echo "latest=${latest}" >> "$GITHUB_OUTPUT" - name: Deploy documentation version env: VERSION: ${{ steps.docs.outputs.version }} + TITLE: ${{ steps.docs.outputs.title }} LATEST: ${{ steps.docs.outputs.latest }} run: | # The zensical fork of mike is GitHub-only (no PyPI releases), so @@ -101,10 +107,10 @@ jobs: "$@" } if [[ "$LATEST" == "true" ]]; then - retry mike deploy --push --update-aliases "$VERSION" latest + retry mike deploy --push --update-aliases --title "$TITLE" "$VERSION" latest retry mike set-default --push latest else - retry mike deploy --push "$VERSION" + retry mike deploy --push --title "$TITLE" "$VERSION" fi deploy: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ec1109..a145da2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,9 +91,9 @@ Publishing a release also deploys the documentation, versioned with - A stable release deploys its docs under `XX.YY/` and moves the `latest` alias (the version the site root redirects to) to it. -- A pre-release (e.g. `vXX.YY.0-rc.1`) deploys under `XX.YY/` but leaves - `latest` untouched, so the public default stays on the latest stable - release. +- A pre-release (e.g. `vXX.YY.0-rc.1`) deploys under a rolling `rc/` version + (titled with the full tag) and leaves `latest` untouched, so the public + default stays on the latest stable release. - Every push to `main` refreshes the `dev` version. Docs for any version can be (re)deployed manually from the From 9fdb8f2d38f9b015122418cda36b775e5133f8df Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:16:51 +0200 Subject: [PATCH 10/22] Take the docs source ref as a dispatch input: dispatching from an old tag uses that tag's workflow definition, which has no manual trigger. --- .github/workflows/docs.yml | 10 +++++++++- CONTRIBUTING.md | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 53d1183..bc606f8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,8 +9,13 @@ on: - published workflow_dispatch: inputs: + ref: + description: "Git ref (tag) to build the docs from; empty builds the ref the workflow runs on" + required: false + type: string + default: "" version: - description: "Docs version to deploy (e.g. 0.3), built from the selected ref" + description: "Docs version to deploy (e.g. 0.3)" required: true type: string latest: @@ -30,9 +35,12 @@ jobs: contents: write steps: + # Dispatch runs on main (older tags predate the manual trigger) and + # select the tag to build via the ref input instead - uses: actions/checkout@v6 with: fetch-depth: 0 + ref: ${{ inputs.ref }} - name: Install uv uses: astral-sh/setup-uv@v5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a145da2..921a72f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,8 +98,10 @@ Publishing a release also deploys the documentation, versioned with Docs for any version can be (re)deployed manually from the [docs workflow](https://github.com/xqlsystems/xarray-sql/actions/workflows/docs.yml) -via "Run workflow": pick the git tag as the ref, set the docs version (e.g. -`0.3`), and tick "latest" only if the site root should point there. +via "Run workflow": run it on `main` (dispatch uses the workflow definition +at the selected ref, and tags predating it have no manual trigger), put the +git tag to build from in the "ref" input, set the docs version (e.g. `0.3`), +and tick "latest" only if the site root should point there. When undoing a bad release (see below), note that deleting the release and tag does not undo its docs deployment. Re-run the docs workflow from the last From 86715b7add4140c4aade2d1dc39c8aeac6135b98 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:18:06 +0200 Subject: [PATCH 11/22] Never move 'latest' backward: a maintenance release for an older line must not repoint the public default. --- .github/workflows/docs.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bc606f8..a4769b6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -83,6 +83,15 @@ jobs: # v0.4.0 -> 0.4 version="$(echo "${RELEASE_TAG#v}" | cut -d. -f1,2)" latest="true" + # A maintenance release for an older line (e.g. v0.3.4 after + # v0.4.0) must not move 'latest' backward + current="$(git show origin/gh-pages:versions.json 2>/dev/null \ + | jq -r '.[] | select(.aliases | index("latest")) | .version' || true)" + if [[ -n "$current" ]] && \ + [[ "$(printf '%s\n%s\n' "$current" "$version" | sort -V | tail -1)" != "$version" ]]; then + echo "Line ${version} is older than current latest (${current}); not moving the alias" >&2 + latest="false" + fi fi else version="dev" From 973f0a5016af7c7b003f9175fb9778c9dc9af317 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:18:54 +0200 Subject: [PATCH 12/22] Rollback docs: use the pinned mike fork for deletion and republish through the workflow, since gh-pages pushes alone never reach Pages. --- CONTRIBUTING.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 921a72f..6f506ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,9 +104,22 @@ git tag to build from in the "ref" input, set the docs version (e.g. `0.3`), and tick "latest" only if the site root should point there. When undoing a bad release (see below), note that deleting the release and -tag does not undo its docs deployment. Re-run the docs workflow from the last -good tag with "latest" ticked to move the public default back, and remove the -bad version entirely with `mike delete --push XX.YY` locally if needed. +tag does not undo its docs deployment. To roll the docs back: + +1. If the bad version should disappear entirely, delete it from `gh-pages` + with the same pinned mike fork the workflow uses (the `mike` on PyPI is + not Zensical-compatible): + + ```shell + uvx --from "git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac" \ + --with "mkdocstrings[python]" mike delete --push XX.YY + ``` + +2. Re-run the docs workflow on `main` with "ref" set to the last good tag, + its docs version, and "latest" ticked. Pages only updates through the + workflow — never from `gh-pages` pushes alone — so this single run moves + the public default back and republishes the site without the deleted + version. ## Undoing a bad release From f5214c8ba58fc1de70d96d39f5118578deaeba99 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:27:39 +0200 Subject: [PATCH 13/22] Evaluate the latest-alias guard against freshly fetched gh-pages on every push attempt; the resolve-time check could act on stale state when releases overlap. --- .github/workflows/docs.yml | 45 ++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a4769b6..1ff783b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -83,15 +83,6 @@ jobs: # v0.4.0 -> 0.4 version="$(echo "${RELEASE_TAG#v}" | cut -d. -f1,2)" latest="true" - # A maintenance release for an older line (e.g. v0.3.4 after - # v0.4.0) must not move 'latest' backward - current="$(git show origin/gh-pages:versions.json 2>/dev/null \ - | jq -r '.[] | select(.aliases | index("latest")) | .version' || true)" - if [[ -n "$current" ]] && \ - [[ "$(printf '%s\n%s\n' "$current" "$version" | sort -V | tail -1)" != "$version" ]]; then - echo "Line ${version} is older than current latest (${current}); not moving the alias" >&2 - latest="false" - fi fi else version="dev" @@ -106,6 +97,7 @@ jobs: VERSION: ${{ steps.docs.outputs.version }} TITLE: ${{ steps.docs.outputs.title }} LATEST: ${{ steps.docs.outputs.latest }} + GUARD_LATEST: ${{ github.event_name == 'release' }} run: | # The zensical fork of mike is GitHub-only (no PyPI releases), so # pin it to a commit; this job holds contents: write @@ -113,8 +105,32 @@ jobs: uvx --from "git+https://github.com/squidfunk/mike.git@2d4ad799442f4592db8ad53b179bfb33db8c69ac" \ --with "mkdocstrings[python]" mike "$@" } - # Concurrent runs race on the gh-pages push; mike re-fetches the - # branch on each attempt, so retrying resolves the race + # A maintenance release for an older line (e.g. v0.3.4 after + # v0.4.0) must not move 'latest' backward. Checked against freshly + # fetched gh-pages state on every push attempt, since builds for + # overlapping releases are not serialized; manual dispatches honor + # the explicit input instead + may_move_latest() { + [[ "$GUARD_LATEST" == "true" ]] || return 0 + git fetch --quiet origin gh-pages 2>/dev/null || return 0 + current="$(git show FETCH_HEAD:versions.json 2>/dev/null \ + | jq -r '.[] | select(.aliases | index("latest")) | .version' || true)" + if [[ -n "$current" ]] && \ + [[ "$(printf '%s\n%s\n' "$current" "$VERSION" | sort -V | tail -1)" != "$VERSION" ]]; then + echo "Line ${VERSION} is older than current latest (${current}); not moving the alias" >&2 + return 1 + fi + } + deploy() { + if [[ "$LATEST" == "true" ]] && may_move_latest; then + mike deploy --push --update-aliases --title "$TITLE" "$VERSION" latest && + mike set-default --push latest + else + mike deploy --push --title "$TITLE" "$VERSION" + fi + } + # Concurrent runs race on the gh-pages push; each attempt re-fetches + # the branch, so retrying resolves the race retry() { for attempt in 1 2; do "$@" && return 0 @@ -123,12 +139,7 @@ jobs: done "$@" } - if [[ "$LATEST" == "true" ]]; then - retry mike deploy --push --update-aliases --title "$TITLE" "$VERSION" latest - retry mike set-default --push latest - else - retry mike deploy --push --title "$TITLE" "$VERSION" - fi + retry deploy deploy: environment: From bb468c4d7ea45ecff45680d43732a0eed4a17a99 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:28:28 +0200 Subject: [PATCH 14/22] Newest-wins concurrency for rolling docs builds so an overlapping older build cannot overwrite dev/ or rc/ content. --- .github/workflows/docs.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1ff783b..ffca85a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,6 +33,13 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + # Rolling versions (dev from main pushes, rc from pre-releases) are + # newest-wins: an overlapping older build must not finish last and + # overwrite the newer content. Stable releases and manual dispatches + # get unique groups and always run + concurrency: + group: docs-build-${{ github.event_name == 'push' && 'dev' || github.event_name == 'release' && github.event.release.prerelease && 'rc' || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && github.event.release.prerelease) }} steps: # Dispatch runs on main (older tags predate the manual trigger) and From 8678e6ac8f078e3654255101bdd3add4cf7aa5b0 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:30:02 +0200 Subject: [PATCH 15/22] Allow manual redeploys of the rolling rc docs version, but never as the public default. --- .github/workflows/docs.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ffca85a..4696839 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,7 @@ on: type: string default: "" version: - description: "Docs version to deploy (e.g. 0.3)" + description: "Docs version to deploy (e.g. 0.3, or rc for the rolling pre-release docs)" required: true type: string latest: @@ -67,10 +67,14 @@ jobs: INPUT_LATEST: ${{ inputs.latest }} run: | if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then - # Reject anything that isn't MAJOR.MINOR, in particular the - # reserved 'latest' and 'dev' identifiers - if [[ ! "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then - echo "Invalid docs version '$INPUT_VERSION'; expected MAJOR.MINOR (e.g. 0.3)" >&2 + # Only MAJOR.MINOR or the rolling 'rc' target; in particular + # this rejects the 'latest' alias and the auto-managed 'dev' + if [[ ! "$INPUT_VERSION" =~ ^([0-9]+\.[0-9]+|rc)$ ]]; then + echo "Invalid docs version '$INPUT_VERSION'; expected MAJOR.MINOR (e.g. 0.3) or rc" >&2 + exit 1 + fi + if [[ "$INPUT_VERSION" == "rc" && "$INPUT_LATEST" == "true" ]]; then + echo "The public default cannot point at the rolling rc version" >&2 exit 1 fi version="$INPUT_VERSION" From 1665abdc0fcecebc660faeeee2846b1f51906497 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:38:36 +0200 Subject: [PATCH 16/22] Manual rc rebuilds join the rc concurrency group and keep their tag-derived title. --- .github/workflows/docs.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4696839..79e85c2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,8 +38,8 @@ jobs: # overwrite the newer content. Stable releases and manual dispatches # get unique groups and always run concurrency: - group: docs-build-${{ github.event_name == 'push' && 'dev' || github.event_name == 'release' && github.event.release.prerelease && 'rc' || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && github.event.release.prerelease) }} + group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} steps: # Dispatch runs on main (older tags predate the manual trigger) and @@ -65,6 +65,7 @@ jobs: RELEASE_PRERELEASE: ${{ github.event.release.prerelease }} INPUT_VERSION: ${{ inputs.version }} INPUT_LATEST: ${{ inputs.latest }} + INPUT_REF: ${{ inputs.ref }} run: | if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then # Only MAJOR.MINOR or the rolling 'rc' target; in particular @@ -79,6 +80,10 @@ jobs: fi version="$INPUT_VERSION" latest="$INPUT_LATEST" + # An rc rebuild keeps its tag-derived title instead of 'rc' + if [[ "$INPUT_VERSION" == "rc" && -n "$INPUT_REF" ]]; then + title="${INPUT_REF#v}" + fi elif [[ "$EVENT_NAME" == "release" ]]; then if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-+].*)?$ ]]; then echo "Tag '$RELEASE_TAG' is not vMAJOR.MINOR.PATCH; refusing to deploy docs" >&2 From 299702b56c23354732b7ebac4e916c6d9818bcd0 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:46:11 +0200 Subject: [PATCH 17/22] Queue stable-release docs builds so releases of the same line deploy in publish order. --- .github/workflows/docs.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 79e85c2..f2899a2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,10 +35,12 @@ jobs: contents: write # Rolling versions (dev from main pushes, rc from pre-releases) are # newest-wins: an overlapping older build must not finish last and - # overwrite the newer content. Stable releases and manual dispatches - # get unique groups and always run + # overwrite the newer content. Stable releases queue in publish order + # so releases of the same line cannot deploy out of order; they never + # cancel and stay separate from rc so an incoming pre-release cannot + # cancel a running stable build. Manual non-rc dispatches run freely concurrency: - group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.run_id }} + group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.event_name == 'release' && 'stable' || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} steps: From a320d4915a884352bdda872d12700b55e52a4eaf Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:48:41 +0200 Subject: [PATCH 18/22] Tighten workflow comments. --- .github/workflows/docs.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f2899a2..208b621 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,19 +33,17 @@ jobs: runs-on: ubuntu-latest permissions: contents: write - # Rolling versions (dev from main pushes, rc from pre-releases) are - # newest-wins: an overlapping older build must not finish last and - # overwrite the newer content. Stable releases queue in publish order - # so releases of the same line cannot deploy out of order; they never - # cancel and stay separate from rc so an incoming pre-release cannot - # cancel a running stable build. Manual non-rc dispatches run freely + # Rolling targets (dev, rc) are newest-wins; stable releases queue in + # publish order. The groups must stay separate: cancel-in-progress is + # evaluated per incoming run, so a shared group would let an arriving + # pre-release cancel a running stable build concurrency: group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.event_name == 'release' && 'stable' || github.run_id }} cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} steps: - # Dispatch runs on main (older tags predate the manual trigger) and - # select the tag to build via the ref input instead + # Dispatch builds a tag via the ref input; dispatching from a tag + # would use that tag's workflow definition, which lacks this trigger - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -124,10 +122,9 @@ jobs: --with "mkdocstrings[python]" mike "$@" } # A maintenance release for an older line (e.g. v0.3.4 after - # v0.4.0) must not move 'latest' backward. Checked against freshly - # fetched gh-pages state on every push attempt, since builds for - # overlapping releases are not serialized; manual dispatches honor - # the explicit input instead + # v0.4.0) must not move 'latest' backward; re-checked against + # fresh gh-pages state on every attempt since release builds are + # not serialized. Dispatches honor the explicit input may_move_latest() { [[ "$GUARD_LATEST" == "true" ]] || return 0 git fetch --quiet origin gh-pages 2>/dev/null || return 0 From 795fa4b73d85ff29f27d33274a01e9082fe8deb8 Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 13:57:48 +0200 Subject: [PATCH 19/22] Treat a tag pre-release suffix as pre-release even without the release checkbox, and inject the version provider when backfilling docs from pre-versioning tags. --- .github/workflows/docs.yml | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 208b621..194d2cc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,8 +38,8 @@ jobs: # evaluated per incoming run, so a shared group would let an arriving # pre-release cancel a running stable build concurrency: - group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.event_name == 'release' && 'stable' || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && github.event.release.prerelease) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} + group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && (github.event.release.prerelease || contains(github.event.release.tag_name, '-'))) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.event_name == 'release' && 'stable' || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && (github.event.release.prerelease || contains(github.event.release.tag_name, '-'))) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} steps: # Dispatch builds a tag via the ref input; dispatching from a tag @@ -52,6 +52,15 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v5 + # Tags predating the versioning setup lack the provider key, and + # their docs would build without the version selector + - name: Ensure version provider on backfill builds + if: github.event_name == 'workflow_dispatch' && inputs.ref != '' + run: | + if ! grep -q 'provider = "mike"' zensical.toml; then + printf '\n[project.extra.version]\nprovider = "mike"\n' >> zensical.toml + fi + - name: Configure git identity run: | git config user.name "github-actions[bot]" @@ -89,7 +98,9 @@ jobs: echo "Tag '$RELEASE_TAG' is not vMAJOR.MINOR.PATCH; refusing to deploy docs" >&2 exit 1 fi - if [[ "$RELEASE_PRERELEASE" == "true" ]]; then + # The tag's own pre-release suffix counts even when the GitHub + # pre-release checkbox was forgotten + if [[ "$RELEASE_PRERELEASE" == "true" || "$RELEASE_TAG" == *-* ]]; then # Pre-releases go to a rolling 'rc' version so they can never # overwrite the stable X.Y that 'latest' may already point to version="rc" From 5b872a67d6ca8f018030eccd65d8d703c2cd58bf Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:05:06 +0200 Subject: [PATCH 20/22] Fail closed when the current latest target cannot be verified; only a confirmed-absent gh-pages branch may bypass the guard. --- .github/workflows/docs.yml | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 194d2cc..bb28747 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -135,12 +135,23 @@ jobs: # A maintenance release for an older line (e.g. v0.3.4 after # v0.4.0) must not move 'latest' backward; re-checked against # fresh gh-pages state on every attempt since release builds are - # not serialized. Dispatches honor the explicit input + # not serialized. Dispatches honor the explicit input. + # Returns 0 = may move, 1 = older line, 2 = could not verify — + # fail closed, except for a confirmed-absent gh-pages (bootstrap) may_move_latest() { [[ "$GUARD_LATEST" == "true" ]] || return 0 - git fetch --quiet origin gh-pages 2>/dev/null || return 0 + if ! git fetch --quiet origin gh-pages 2>/dev/null; then + rc=0 + git ls-remote --exit-code origin refs/heads/gh-pages >/dev/null 2>&1 || rc=$? + [[ "$rc" -eq 2 ]] && return 0 + echo "Cannot verify the current latest target; failing this attempt" >&2 + return 2 + fi current="$(git show FETCH_HEAD:versions.json 2>/dev/null \ - | jq -r '.[] | select(.aliases | index("latest")) | .version' || true)" + | jq -r '.[] | select(.aliases | index("latest")) | .version')" || { + echo "Cannot read versions.json from gh-pages; failing this attempt" >&2 + return 2 + } if [[ -n "$current" ]] && \ [[ "$(printf '%s\n%s\n' "$current" "$VERSION" | sort -V | tail -1)" != "$VERSION" ]]; then echo "Line ${VERSION} is older than current latest (${current}); not moving the alias" >&2 @@ -148,12 +159,18 @@ jobs: fi } deploy() { - if [[ "$LATEST" == "true" ]] && may_move_latest; then - mike deploy --push --update-aliases --title "$TITLE" "$VERSION" latest && - mike set-default --push latest - else + if [[ "$LATEST" != "true" ]]; then mike deploy --push --title "$TITLE" "$VERSION" + return fi + rc=0 + may_move_latest || rc=$? + case "$rc" in + 0) mike deploy --push --update-aliases --title "$TITLE" "$VERSION" latest && + mike set-default --push latest ;; + 1) mike deploy --push --title "$TITLE" "$VERSION" ;; + *) return 2 ;; + esac } # Concurrent runs race on the gh-pages push; each attempt re-fetches # the branch, so retrying resolves the race From 8626b341f0e6b29e992b5ba9b613e88acea1a66a Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:12:59 +0200 Subject: [PATCH 21/22] Detect pre-releases by the suffix after the patch number, not by any hyphen in the tag; build metadata may contain hyphens. --- .github/workflows/docs.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bb28747..9b2b95c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -38,8 +38,8 @@ jobs: # evaluated per incoming run, so a shared group would let an arriving # pre-release cancel a running stable build concurrency: - group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && (github.event.release.prerelease || contains(github.event.release.tag_name, '-'))) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.event_name == 'release' && 'stable' || github.run_id }} - cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && (github.event.release.prerelease || contains(github.event.release.tag_name, '-'))) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} + group: docs-build-${{ github.event_name == 'push' && 'dev' || ((github.event_name == 'release' && (github.event.release.prerelease || (contains(github.event.release.tag_name, '-') && !contains(github.event.release.tag_name, '+')))) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc')) && 'rc' || github.event_name == 'release' && 'stable' || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'push' || (github.event_name == 'release' && (github.event.release.prerelease || (contains(github.event.release.tag_name, '-') && !contains(github.event.release.tag_name, '+')))) || (github.event_name == 'workflow_dispatch' && inputs.version == 'rc') }} steps: # Dispatch builds a tag via the ref input; dispatching from a tag @@ -99,8 +99,9 @@ jobs: exit 1 fi # The tag's own pre-release suffix counts even when the GitHub - # pre-release checkbox was forgotten - if [[ "$RELEASE_PRERELEASE" == "true" || "$RELEASE_TAG" == *-* ]]; then + # pre-release checkbox was forgotten: a '-' right after the + # patch number, not a hyphen inside '+' build metadata + if [[ "$RELEASE_PRERELEASE" == "true" || "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+- ]]; then # Pre-releases go to a rolling 'rc' version so they can never # overwrite the stable X.Y that 'latest' may already point to version="rc" From cf719da82fc1d33a86d4acaf0dbca7a208a076ba Mon Sep 17 00:00:00 2001 From: Miguel Moncada Isla <48254102+Mmoncadaisla@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:26:06 +0200 Subject: [PATCH 22/22] Reset the local gh-pages branch to the remote between push retries; a stale local branch replays the same non-fast-forward rejection. --- .github/workflows/docs.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9b2b95c..52bbd5e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -173,13 +173,15 @@ jobs: *) return 2 ;; esac } - # Concurrent runs race on the gh-pages push; each attempt re-fetches - # the branch, so retrying resolves the race + # Concurrent runs race on the gh-pages push; the local branch must + # be reset to the remote before rerunning, or every retry replays + # the same non-fast-forward rejection retry() { for attempt in 1 2; do "$@" && return 0 echo "attempt ${attempt} failed; retrying" >&2 sleep 15 + git fetch --force origin gh-pages:gh-pages 2>/dev/null || true done "$@" }