Skip to content

chore(deps): update dependency chrome-for-testing to v157 (#1799) #1759

chore(deps): update dependency chrome-for-testing to v157 (#1799)

chore(deps): update dependency chrome-for-testing to v157 (#1799) #1759

Workflow file for this run

name: Docs
# Build the mkdocs-material site (sources added in #176) and publish it to GitHub
# Pages via `mike`, which keeps a version selector on the `gh-pages` branch (#1084):
#
# - push to main -> rolling `dev` snapshot (unreleased docs), opt-in via the
# selector, carrying Material's version-warning banner
# - release tag vX.Y.Z -> per-MINOR `X.Y` snapshot, and moves the `latest` alias
# (the site default) onto it, so visitors land on the
# version they can actually install. Snapshots are keyed by
# minor, so a patch release REBUILDS its line's directory
# (v1.0.1 rewrites `1.0`) — `1.0` always shows the newest
# 1.0.x. That is deliberate: per-patch snapshots would add a
# full copy of the site per patch and make the version
# selector unusable.
# - pull requests -> `mkdocs build --strict` only (link/config validation).
# Never deploy: a docs change cannot reach the live site
# without landing on main first.
#
# NO `paths:` FILTER ON PUSH — deliberate, and load-bearing twice over:
# 1. A release tag's commit often touches no docs file at all. A paths filter
# applies to tag pushes too, so it would silently skip the X.Y snapshot for
# that release — the one deploy that matters most.
# 2. Five root files are pulled into the site by mkdocs-include-markdown
# (CHANGELOG, CONTRIBUTING, SUPPORT, UPGRADING, SECURITY). The old filter
# listed only `docs/**` + `mkdocs.yml`, so a knope release writing CHANGELOG.md
# did NOT rebuild the site, and the published changelog stayed stale until an
# unrelated docs edit happened to trigger a build.
# The cost is a `dev` redeploy on main pushes that changed nothing on the site.
# That is cheap; a missing release snapshot is not.
#
# Action refs are pinned to full commit SHAs (repo Actions policy + Scorecard), and
# the deploy job pushes via an explicit tokened remote rather than persisted
# checkout credentials (zizmor `artipacked`).
on:
push:
branches: [main]
tags: ["v[0-9]+.[0-9]+.[0-9]+"]
pull_request:
# Explicit list = the implicit defaults (opened/synchronize/reopened) PLUS
# `ready_for_review`, which re-fires the build on the draft → ready flip —
# setting any `types:` replaces the defaults (#623).
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "docs/**"
- "mkdocs.yml"
- "mike/**"
- ".github/workflows/docs.yml"
# The docs toolchain itself. Without these a Renovate bump of
# mkdocs-material/mike touches only pyproject.toml + uv.lock, the Docs build
# never runs on that PR, and — since "build docs (strict)" is not a required
# check — it merges unvalidated, with the main push then also deploying it.
- "pyproject.toml"
- "uv.lock"
- ".github/actions/setup/**"
# Surfaced on the site by mkdocs-include-markdown — a change here changes a
# published page, so it must face the --strict gate.
- "CHANGELOG.md"
- "CONTRIBUTING.md"
- "SUPPORT.md"
- "UPGRADING.md"
- "SECURITY.md"
workflow_dispatch:
inputs:
bootstrap_version:
description: >-
One-time: publish THIS ref's docs as version X.Y (e.g. 1.0) and make it
`latest`. Needed once, because no existing release tag carries this
workflow. Leave empty for a normal deploy.
required: false
default: ""
promote_to_latest:
description: >-
Point `latest`/default at an ALREADY-DEPLOYED version (X.Y, e.g. 1.0).
Fails if that version was never deployed — use bootstrap_version first.
required: false
default: ""
permissions:
contents: read
jobs:
build:
name: build docs (strict)
# Draft PRs skip; `ready_for_review` re-fires this. `!= true` (not `== false`)
# so push:main / tags / workflow_dispatch — where `draft` is null — fail-safe
# to running, which keeps the deploy lane (needs: build) intact.
if: github.event.pull_request.draft != true
runs-on: ubuntu-latest
timeout-minutes: 10
# Per-ref build group: a newer push/PR-sync supersedes an in-flight build.
# Use github.ref (refs/pull/<n>/merge on PRs — unique per PR) rather than
# github.head_ref (the source branch name), so two PRs that happen to share a
# branch name (e.g. forks both on `main`/`patch-1`) don't cancel each other.
# Deploy is a SEPARATE group (below) that never cancels — so this fast
# cancellation can't kill a live gh-pages push.
# ⚠️ `github.event_name` is part of the key, not decoration. `deploy` is
# `needs: build`, and a workflow_dispatch from main — the ONLY way to move
# `latest` before the next release tag — resolves github.ref to refs/heads/main,
# i.e. the same group as an ordinary main push. With cancel-in-progress: true a
# merge landing mid-bootstrap would SIGKILL that build, skip deploy, and leave
# `latest` unmoved as a grey "cancelled" run. Splitting the deploy lane by
# ref_type protects the queue but not the job upstream of it.
concurrency:
group: docs-build-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up uv + Python
uses: $/.github/actions/setup
with:
python-version: "3.13"
extras: docs
- name: mkdocs build --strict
# --no-sync: use the env the setup action already synced (extras: docs);
# don't let a `.python-version` drift rebuild it without the docs extra.
run: uv run --no-sync mkdocs build --strict
deploy:
name: deploy to Pages (mike)
needs: build
# Never on PRs (fork tokens are read-only anyway); same-repo only; and only for
# main or a release tag — otherwise a manual workflow_dispatch from a feature
# branch would fall through and overwrite `latest` with that branch's docs.
if: >-
github.event_name != 'pull_request'
&& github.repository == 'schubydoo/clauster'
&& (github.ref == 'refs/heads/main' || github.ref_type == 'tag')
runs-on: ubuntu-latest
timeout-minutes: 10
# Serialize gh-pages pushes (concurrent ones reject as non-fast-forward) without
# SIGKILLing one mid-push.
#
# ⚠️ KEYED BY ref_type ON PURPOSE. `cancel-in-progress: false` protects the
# RUNNING job, not a QUEUED one: GitHub keeps at most one pending run per group
# and cancels the previously pending one. A release produces exactly the burst
# that exploits this — release-prep merge (deploy A running), tag push (deploy B
# pending), packaging-bump merge (deploy C queues, B CANCELLED) — silently
# dropping the X.Y snapshot and leaving `latest` behind, shown only as a grey
# "cancelled" run. Separate lanes mean main-push churn can only evict another
# main deploy, where newest-wins is correct. A simultaneous main+tag push can
# now collide on the gh-pages push instead, which fails LOUDLY and is
# re-runnable — the right trade against a silent drop.
#
# ⚠️ RESIDUAL, deliberately not fixed: the key omits the ref NAME, so the same
# eviction still exists WITHIN the tag lane. Two release tags pushed close
# together give deploy A (v1.2.3) running, B (v1.2.4) pending, C queued -> B
# cancelled, and 1.2's snapshot is dropped. Adding github.ref would defeat the
# gh-pages serialization this group exists for, so there is no clean key-level
# fix. Recovery is to re-run the skipped tag's deploy (or dispatch
# promote_to_latest for it). Do not read the ref_type split as closing this.
# (`event_name` for the same reason as the build job above: a manual deploy from
# main would otherwise sit in the same lane as main pushes, where a newly queued
# run cancels the pending one regardless of cancel-in-progress.)
concurrency:
group: docs-deploy-${{ github.ref_type }}-${{ github.event_name }}
cancel-in-progress: false
permissions:
contents: write # mike commits and pushes the built site to gh-pages
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Full history for the DEPLOY ref (mike's documented CI setup). gh-pages
# itself arrives via the explicit fetch in the next step, not from here —
# don't "optimize" this line on the assumption that it supplies gh-pages.
fetch-depth: 0
persist-credentials: false
- name: Set up uv + Python
uses: $/.github/actions/setup
with:
python-version: "3.13"
extras: docs
- name: Configure git + push auth
env:
GH_TOKEN: ${{ github.token }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git fetch origin gh-pages --depth=1 || true
- name: Deploy
env:
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
PROMOTE: ${{ github.event.inputs.promote_to_latest }}
BOOTSTRAP: ${{ github.event.inputs.bootstrap_version }}
run: |
set -euo pipefail
# X.Y shape guard, shared by both dispatch inputs. The values only ever
# reach mike as a single quoted argv element (never eval/backticks), so
# this is about failing loudly on a typo — `1.0.0` or `latest` would
# otherwise become a real, wrongly-named version directory.
check_xy() {
case "$1" in
*[!0-9.]* | *.*.* | .* | *. | "") return 1 ;;
*.*) return 0 ;;
*) return 1 ;;
esac
}
# Same discipline for a release tag's X.Y.Z. A glob like v[0-9]*.[0-9]*.[0-9]*
# looks strict but is not: the trailing `*` swallows anything, so `v1.0.0-rc1`
# passes and `${version%.*}` yields `1.0` — moving `latest` onto a release
# candidate and clobbering the real 1.0 snapshot. `v1.1.0-beta.2` would publish
# a directory literally named `1.1.0-beta`. The push trigger's glob does not
# help here, because the whole point of this guard is that workflow_dispatch
# is NOT constrained by it.
check_xyz() {
case "$1" in
*[!0-9.]* | *.*.*.* | .* | *. | "") return 1 ;;
*.*.*) return 0 ;;
*) return 1 ;;
esac
}
if [ -n "${BOOTSTRAP:-}" ] && [ -n "${PROMOTE:-}" ]; then
echo "::error::Set bootstrap_version OR promote_to_latest, not both"; exit 1
fi
if [ -n "${BOOTSTRAP:-}" ]; then
# ONE-TIME BOOTSTRAP. `latest` is an alias, and `mike alias` refuses an
# identifier that was never deployed — so promote_to_latest cannot create
# the first version. Neither can a release tag: every tag up to and
# including v1.0.0 carries the OLD Pages-artifact workflow, so dispatching
# on that tag runs the old file, not this one. This input closes that gap
# by publishing the CURRENT ref's docs under an explicit X.Y.
#
# Run it from a ref whose docs match the release you are naming.
if ! check_xy "$BOOTSTRAP"; then
echo "::error::bootstrap_version must be X.Y (e.g. 1.0), got '$BOOTSTRAP'"; exit 1
fi
uv run --no-sync mike deploy --push --update-aliases "$BOOTSTRAP" latest
uv run --no-sync mike set-default --push --template mike/redirect.html latest
elif [ -n "${PROMOTE:-}" ]; then
# One-time migration / manual re-point: make an already-deployed release
# the site default under the `latest` alias.
#
# Use `mike alias` (NOT `mike deploy`): this only re-points the alias at
# an already-published snapshot. `mike deploy $PROMOTE` would rebuild the
# docs from the current checkout (main) into the $PROMOTE directory,
# clobbering that release's snapshot with main's content.
if ! check_xy "$PROMOTE"; then
echo "::error::promote_to_latest must be X.Y (e.g. 1.0), got '$PROMOTE'"; exit 1
fi
uv run --no-sync mike alias --push --update-aliases "$PROMOTE" latest
uv run --no-sync mike set-default --push --template mike/redirect.html latest
elif [ "$REF_TYPE" = "tag" ]; then
# Release vX.Y.Z -> per-MINOR "X.Y" snapshot, and move `latest` (the site
# default) onto it: the newest release is what visitors land on.
#
# `${version%.*}` is what makes the snapshot per-minor: v1.0.1 deploys into
# `1.0`, REBUILDING it from the 1.0.1 tree. A patch therefore replaces its
# line's docs rather than adding a directory, so `1.0` always shows the
# newest 1.0.x — which is what a 1.0.x reader wants, and it matches the
# docs' own minor-granular "New in X.Y" convention.
#
# Assumes linear releases — a backport (1.0.2 shipping AFTER 1.1.0) would
# drag `latest` backwards onto `1.0`; re-point via promote_to_latest.
#
# The vX.Y.Z glob on the `push` trigger does NOT constrain a
# workflow_dispatch, which accepts any tag. Without this guard a dispatch
# from tag `nightly` would deploy a version literally named "nightly" AND
# move `latest` onto it; from `v0.3.0` it would silently downgrade
# `latest`. Re-check the shape here, where it actually binds.
case "$REF_NAME" in
v*) ;;
*) echo "::error::refusing to deploy from tag '$REF_NAME' (no 'v' prefix)"; exit 1 ;;
esac
version="${REF_NAME#v}"
if ! check_xyz "$version"; then
echo "::error::refusing to deploy from tag '$REF_NAME' (not vX.Y.Z)"; exit 1
fi
uv run --no-sync mike deploy --push --update-aliases "${version%.*}" latest
uv run --no-sync mike set-default --push --template mike/redirect.html latest
else
# Push to main -> rolling `dev` snapshot. Deliberately never touches
# `latest`/default, so unreleased changes are not what users see by
# default; Material's version-warning banner flags `dev` as ahead of the
# newest release.
uv run --no-sync mike deploy --push --update-aliases dev
fi
- name: Sync the gh-pages root (404 fallback + llms.txt)
# Two files that must live at the SITE ROOT, which mike never writes to
# beyond index.html and versions.json.
#
# 404.html — before versioning, every page lived at the root. mike's
# set-default writes a root index.html, which rescues `/` and nothing beneath
# it, so already-published deep links (README, bookmarks, forum posts, search
# results) would 404. Pages serves the repo-root 404.html for any unresolved
# path, so mike/404.html forwards /<path> to /latest/<path>. Kept OUT of docs/
# on purpose: anything under docs/ is built into each version directory
# (/latest/404.html), never the root where Pages looks for it.
#
# llms.txt — docs/llms.txt IS built into every version directory, and the
# format is defined around the well-known ROOT location. Versioning alone
# would 404 it there, and the 404.html rescue cannot help: it redirects with
# window.location.replace, and an llms.txt fetcher is exactly the non-JS
# client that comment warns about. Copied from the DEPLOYED latest/ inside the
# clone rather than from the checkout, so it always matches whatever `latest`
# currently resolves to — on a plain `dev` push latest/ is unchanged, so this
# correctly leaves the root copy alone instead of overwriting it with main's.
#
# Idempotent — commits only when a file actually changed.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tmp="$(mktemp -d)"
# Remove the clone on ANY exit: its .git/config holds a tokened remote URL.
# Runners are ephemeral, so this is defence in depth rather than a leak fix.
trap 'rm -rf "$tmp"' EXIT
# A fresh shallow clone of gh-pages rather than a worktree of the deploy
# checkout: this step then depends on nothing about that checkout's ref
# state (which mike has just rewritten), and the tokened remote stays
# confined to a temp directory that the trap above deletes.
git clone --depth=1 --branch gh-pages \
"https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$tmp"
cp mike/404.html "$tmp/404.html"
git -C "$tmp" add 404.html
if [ -f "$tmp/latest/llms.txt" ]; then
cp "$tmp/latest/llms.txt" "$tmp/llms.txt"
git -C "$tmp" add llms.txt
else
# Expected before the first bootstrap, when only dev/ exists.
echo "no latest/llms.txt yet — leaving the root copy alone"
fi
if git -C "$tmp" diff --cached --quiet; then
echo "gh-pages root already current"
else
git -C "$tmp" \
-c user.name="github-actions[bot]" \
-c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -m "docs: sync gh-pages root (404 fallback + llms.txt)"
git -C "$tmp" push origin HEAD:gh-pages
fi