Repository navigation
feat(docs): implement versioned documentation deployment and version … - #7023
Conversation
WareWolf-MoonWall
left a comment
There was a problem hiding this comment.
Reviewed at 4a0a3ed. CI 13/13 ✅. No prior reviews or inline threads. Closes #6997.
The versioned docs architecture is sound: incremental rsync-into-gh-pages, a versions.json manifest, the stable/ symlink-by-copy pattern, and the JS version selector are all well designed. One blocking regression and a few smaller items need addressing before merge.
🔴 Blocking — CNAME file not written; regresses fix(ci/docs-deploy): persist CNAME on every Pages deploy (#6142)
The previous peaceiris/actions-gh-pages step carried:
cname: docs.zeroclawlabs.aiwhich wrote a CNAME file to the root of gh-pages on every deploy. The new custom script does not write a CNAME file anywhere. Commit #6142 in the repo's history was a deliberate fix specifically titled "persist CNAME on every Pages deploy" — this PR re-introduces the same regression that #6142 resolved.
On the first invocation that recreates the gh-pages orphan branch (or any manual branch reset), docs.zeroclawlabs.ai would stop resolving to the Pages site. Even on incremental deploys, the CNAME survives only as long as nobody recreates the branch — there is no guaranteed write path.
The fix is one line added inside the pushd "$CLONE_DIR" block, before git add --all:
echo "docs.zeroclawlabs.ai" > CNAME🟡 Warning — .vscode/extensions.json changes are unrelated to versioned docs
The diff removes usernamehw.errorlens and dbaeumer.vscode-eslint from .vscode/extensions.json and drops the file's trailing newline. Neither change has anything to do with versioned documentation deployment. Per the project's anti-patterns: "Do not modify unrelated modules 'while here'." These changes belong in a separate chore: PR or can be dropped entirely — they add noise to the review and to the commit history.
🟡 Warning — orphan branch creation uses || true inside set -euo pipefail
git checkout --orphan gh-pages || git switch --orphan gh-pages || true
cd - >/dev/nullWith set -euo pipefail active, the || true means both checkout --orphan and switch --orphan can fail silently and execution continues. If both commands fail (permissions, git version mismatch), the script proceeds in an undefined state — not necessarily on the orphan branch — and git add --all / git push would operate on whatever the current branch is. Adding an explicit failure check after the || true (e.g., verifying the branch name is what was expected) would close this gap.
🟢 What looks good — incremental deploy model is the right approach
Replacing force_orphan: true (which wiped the entire gh-pages history on every push) with an incremental rsync-into-existing-branch is the correct call for versioned docs. Each version lives in its own subdirectory; the old branch history accumulates cleanly. The --delete flag on rsync ensures a stale version subdirectory is always replaced atomically.
🟢 What looks good — set -euo pipefail and REPO token pattern are correct
The deploy script uses strict bash error handling throughout (except the intentional || true cases). The https://x-access-token:${GITHUB_TOKEN}@github.com/... pattern for authenticated pushes is standard in GitHub Actions, and GITHUB_TOKEN is automatically masked in logs.
🟢 What looks good — version-selector.js handles subdirectory hosting and silent failure
The JS correctly constructs the basePath from segments before the version segment (so it works both at root and under a project-page subdirectory), fetches versions.json with cache: "no-cache", and catches all fetch/parse errors with a console.debug rather than a visible error — correct behavior for a progressive enhancement. The aria attributes (aria-haspopup, aria-expanded, aria-controls, role="menu", role="menuitem") are complete.
🟢 What looks good — SHA-pinned actions/checkout is preserved
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd is unchanged from the pre-PR workflow. The removal of peaceiris/actions-gh-pages (which was also SHA-pinned) is fine because it is replaced by a run: step — no new unverified action is introduced.
🟢 What looks good — stable/ copy-on-release logic is correct
The ^v[0-9]+\.[0-9]+\.[0-9]+$ guard (no pre-release suffix) is the right predicate for "this is a stable release." A v0.8.0-beta-1 tag triggers a versioned deploy but does not overwrite /stable/; a v0.8.0 tag does both. That's exactly the right user-facing behaviour.
🔵 Suggestion — generate-versions.py sorts lexicographically, not by semver
dirs.sort(...) produces string order, so v0.10.0 < v0.7.5 (because "1" < "7"). The dropdown version list would show versions out of semver order once the version number hits double digits in any component. A simple fix is parsing the version tuple for sort:
import functools
def semver_key(tag):
if tag == 'master': return (0,)
if tag == 'stable': return (1,)
m = re.match(r'^v(\d+)\.(\d+)\.(\d+)', tag)
if m:
return (2, int(m.group(1)), int(m.group(2)), int(m.group(3)))
return (3, tag)
dirs.sort(key=semver_key)🔵 Suggestion — workflow_dispatch tag input has no format guard
The tag input is passed directly to ref: in the checkout step. While only collaborators can trigger workflow_dispatch, a typo or test value (e.g., "my-feature-branch") would check out an arbitrary ref and deploy it as a versioned docs slot. Adding an if: guard or a regex check at the top of the deploy script (e.g., [[ "$TAG" =~ ^(master|v[0-9]+\.[0-9]+\.[0-9]+) ]] || { echo "Invalid TAG: $TAG"; exit 1; }) would prevent accidental or unexpected deployments.
|
@singlerider @theonlyhennygod — milestone alignment needed: this PR does not clearly fit within the scope boundary of any open milestone. Please advise on placement or deferral. |
Changes applieddocs-deploy.yml
generate-versions.py
.vscode/extensions.jsonReverted, but one of the extensions is deprecated with a replacement. Crates -> Dependi. |
Audacity88
left a comment
There was a problem hiding this comment.
Context: I re-reviewed head 9615c2d1802dfe3e126c4899643dc8cd58922e23, the current workflow/docs diff, green checks, #6997, WareWolf's earlier review, and the author's follow-up comment.
✅ Resolved — prior deployment review items are addressed
The latest head fixes the earlier CNAME regression, removes the silent orphan-branch failure path, drops the unrelated .vscode change, adds a workflow_dispatch tag guard, and replaces the lexicographic version ordering with a semver-aware sort. Those were the right follow-ups.
🟢 What looks good — versioned docs shape is still the right direction
The overall deployment model still makes sense: build each docs version into its own directory, copy stable releases to /stable/, generate a root redirect, and let the docs UI switch between available versions. That directly matches the problem in #6997.
🔴 Blocking — manual deploys of older release tags lose the new helper scripts
The workflow now lets workflow_dispatch choose an older release tag, but the checkout step uses that tag as the main workspace ref:
ref: ${{ github.event.inputs.tag || github.ref }}After that checkout, the deploy step runs helpers from ${GITHUB_WORKSPACE}, for example:
python3 "${GITHUB_WORKSPACE}/.github/scripts/generate-versions.py" > versions.json
bash "${GITHUB_WORKSPACE}/.github/scripts/gen-index-stable.sh"That breaks the main backfill path for this PR. Existing release tags such as v0.7.5, v0.7.4, and v0.8.0-beta-1 predate this PR, so they do not contain .github/scripts/generate-versions.py, gen-index-stable.sh, gen-index-master.sh, or the new TAG-aware docs xtask code. When the manual deploy checks out one of those tags, the workflow replaces the workspace with old source and then tries to run helper files that are no longer present.
The fallback for old unversioned docs layout helps only after the build exists; it does not solve the missing helper-script problem. As written, the workflow can deploy master and future tags created after this PR, but it cannot reliably backfill the already-published release docs needed to close #6997.
Please keep the workflow/deploy helpers from the current workflow revision while checking out the target docs ref separately, or otherwise preserve/copy the new helper scripts before switching the workspace to the old tag. The important invariant is that v0.7.5 and other existing tags can be deployed into versioned docs without requiring those old tags to already contain this PR's helper files.
🟡 Warning — PR body risk metadata is stale after workflow changes
The live labels currently include risk: high and ci, but the PR body still says risk: low and uses the low-risk rollback text. Since this changes the Pages deployment workflow with contents: write, please update the body before merge so the label snapshot, risk section, and rollback section match the actual workflow risk.
WareWolf-MoonWall
left a comment
There was a problem hiding this comment.
Re-reviewing at 9615c2d. Prior CHANGES_REQUESTED was at 4a0a3ed. CI 13/13 ✅. @Audacity88 reviewed this head and holds an active CHANGES_REQUESTED with a new blocking issue — I'm not overriding that block; using --comment accordingly.
The author pushed a substantial follow-up addressing all five items I raised. Here's the record of what changed.
✅ Resolved — CNAME regression
A CNAME file is now written before git add --all inside the clone block, restoring the persistence that #6142 established. The custom deploy script no longer allows the CNAME to silently vanish on any deploy invocation.
✅ Resolved — .vscode/extensions.json unrelated changes
Dropped entirely from the new head. No more noise from editor tooling changes in a docs-deployment PR.
✅ Resolved — orphan branch || true fragile failure path
The new head explicitly verifies the current branch name after the orphan creation sequence. Silent failure followed by git add --all on the wrong branch is no longer possible.
✅ Resolved — workflow_dispatch tag input has no format guard
The Determine version tag step now validates the tag input against ^(master|v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?)$ before proceeding. Invalid inputs produce a visible ::error:: annotation and exit 1.
✅ Resolved — generate-versions.py sorts lexicographically
The updated generate-versions.py introduces semver_key() with negated major/minor/patch components so that v0.10.0 correctly precedes v0.9.x in the dropdown without a separate reverse pass. master and stable are always pinned to the front.
@Audacity88's new blocking issue about manual deploys of older release tags (the checkout step using the tag ref replaces the workspace with old source, but the deploy helpers in ${GITHUB_WORKSPACE}/.github/scripts/ were added in this PR and don't exist in pre-PR tags) is valid. I agree with the block: as written, workflow_dispatch deploying v0.7.5 would fail trying to run generate-versions.py that isn't present in that tag's source. The invariant needed — keep the current workflow's helper scripts available regardless of which doc version is being built — requires checking out the helpers from master or a pinned ref separately from the docs content checkout. I have nothing new to block on beyond @Audacity88's finding.
To ensure scripts are always present. And consolidated the three script references to `$SCRIPTS` | Trigger | `github.sha` points to | Scripts present? | |---|---|---| | `workflow_dispatch` (any tag input) | master HEAD | yes | | `push` to master | master HEAD | yes | | `push` to future tag (post-PR) | tag commit (created from master) | yes | | `push` to pre-PR tag | cannot trigger - old tag lacks this workflow file | n/a |
WareWolf-MoonWall
left a comment
There was a problem hiding this comment.
Re-reviewing at a7edd5a. CI 13/13 ✅. @Audacity88's CHANGES_REQUESTED at 9615c2d is still formally active; using --comment accordingly.
The author pushed a new commit addressing @Audacity88's blocking issue. Here is the assessment of that fix, plus a record of where things stand overall.
✅ Resolved — helper scripts now fetched from the current workflow revision for all deploy paths
@Audacity88's block was: when workflow_dispatch checks out an older tag (e.g. v0.7.5), the deploy step references ${GITHUB_WORKSPACE}/.github/scripts/generate-versions.py which does not exist in that old tag's tree.
The fix adds a second actions/checkout step immediately after the primary checkout:
- name: Fetch deploy helpers from current workflow revision
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
path: .workflow-scripts
sparse-checkout: |
.github/scripts
sparse-checkout-cone-mode: false
fetch-depth: 1For a workflow_dispatch run, github.sha is always master's HEAD at trigger time — the commit that carries this workflow file and the helper scripts — regardless of which tag input was specified. The helper scripts land in .workflow-scripts/.github/scripts/. The deploy step then references them through:
SCRIPTS="${GITHUB_WORKSPACE}/.workflow-scripts/.github/scripts"
python3 "${SCRIPTS}/generate-versions.py" > versions.json
bash "${SCRIPTS}/gen-index-stable.sh"This correctly separates the docs-content checkout (the old tag, for building the actual docs) from the deploy-helper checkout (always master's HEAD, for running the deployment logic). The fallback path for old-layout docs (versions that predate the TAG-aware xtask) is also intact.
The approach is sound. @Audacity88's blocking issue appears addressed.
✅ Resolved (prior) — all five items from my original CHANGES_REQUESTED at 4a0a3ed remain resolved
CNAME persistence, .vscode cleanup, orphan-branch guard, workflow_dispatch tag validation, and semver-aware version ordering were all addressed in the prior head (9615c2d) and are unchanged here.
@Audacity88: the .workflow-scripts helper-fetch step directly addresses your blocking concern. Please re-review at a7edd5a and dismiss or convert your CHANGES_REQUESTED if you're satisfied with the fix. The 🟡 stale risk metadata in the PR body is a quick body-edit away if you want that resolved before merge.
|
Does this require rendering |
No, only version master is compiled on push. And version The other subdirectories remain untouched. |
|
I recommend making a copy ( |
singlerider
left a comment
There was a problem hiding this comment.
Reviewing at a7edd5a. CI 13/13 ✅. Closes #6997.
First: thank you for taking this on, @kitswas. Versioned docs is real infrastructure that most projects punt on indefinitely, and the shape here is right — per-tag subdirectory routing, a scanned versions.json manifest, the stable/-copy-on-GA pattern, the progressive-enhancement JS selector, and the two-checkout split that lets us deploy a tag whose source predates these scripts. I genuinely love this feature and I'm grateful you built it. The asks below aren't about the design being wrong — they're about escaping tech debt up front so this lands ready for immediate deployment and ongoing enforcement by maintainers, rather than something we have to harden after it's already the live deploy path.
I read @WareWolf-MoonWall's original CHANGES_REQUESTED (4a0a3ed, five items, all resolved), @Audacity88's CHANGES_REQUESTED (9615c2d, helper-scripts blocker + metadata warning), and both of @WareWolf-MoonWall's follow-up reviews. @Audacity88's block is still formally active; my blockers below stack with theirs.
✅ Resolved — @Audacity88's helper-scripts blocker is addressed in a7edd5a
The second actions/checkout pins ref: ${{ github.sha }} with a sparse checkout of .github/scripts into .workflow-scripts/, and the deploy step sources helpers via SCRIPTS="${GITHUB_WORKSPACE}/.workflow-scripts/.github/scripts". For workflow_dispatch, github.sha is master's HEAD regardless of the tag input, so the helpers are always present even when the content checkout lands on an older tag. The docs-content checkout (old tag) and deploy-helper checkout (master HEAD) are correctly decoupled. @Audacity88 — worth a re-stamp at a7edd5a.
🟢 What looks good — concurrency, CNAME persistence, and the freeze model
concurrency: { group: gh-pages, cancel-in-progress: false } serializes deploys rather than cancelling mid-push. echo "docs.zeroclawlabs.ai" > CNAME before every git add --all keeps the custom domain (the #6142 fix, preserved). And I traced the deploy flow rather than trust the "incremental" claim: each run clones the existing gh-pages, rsync -a --deletes only the freshly-built $TAG into its own subdir (siblings are never in rsync's destination, so they survive), then pushes. A master build does not recompile v0.7.5 — it inherits it from the clone and overwrites only master/. Each run recompiles exactly one version. That's the correct model and the body describes it accurately.
🟢 What looks good — semver-aware ordering and the GA-vs-prerelease tiebreak
semver_key() negating major/minor/patch gives newest-first ordering in a single ascending sort without disrupting the master/stable pin, and pre_rank correctly sorts v0.8.0 ahead of v0.8.0-beta-1.
The three blockers below are what stand between "great prototype" and "we can flip this on today and never babysit it."
🔴 Blocking — theme/chrome changes must cascade across all versions without rebuilding frozen content
mdBook bakes the theme into each build: rendered pages reference CSS via content-hashed relative paths (../theme/custom-a6e9ac27.css). That hash is the cascade-killer — it means a future custom.css edit (or a logo/theme refresh) repaints only master and the next release. Every already-deployed version keeps its frozen CSS forever, and the hash changes on edit so you can't even swap the file in place without 404ing the old <link>. For a docs site we want a single visual identity across versions, that's tech debt baked in on day one.
The fix is to make chrome (CSS/theme/JS shell) a shared layer while content stays frozen per version. Do this at build time in the xtask (Option C): have assemble/build emit the theme to a shared, unhashed location and reference it with a base-path-aware URL so new builds natively point at shared chrome — and so cargo mdbook serve and subdirectory hosting still resolve it (the version-selector's existing basePath logic is the model for computing this correctly). Content prose/API stays per-version and frozen; only chrome cascades. A CSS edit then repaints v0.7.5, v0.8.0-beta-1, master, and every future version on their next deploy. Keep the cascade scoped strictly to chrome so a bad CSS change can't bleed content backward into a frozen version.
🔴 Blocking — restrict the deployable version set; declare the floor once, no magic literals
We want exactly v0.7.5 (oldest), v0.8.0-beta-1 (tagged pre-release), and master (rolling) to start — then a new versioned deploy on each release tag going forward. But the tag-validation regex ^(master|v[0-9]+\.[0-9]+\.[0-9]+(-...)?)$ accepts anything that parses, including v0.7.4 and the older v0.7.x-beta tags that still exist in the repo. A maintainer fat-fingering workflow_dispatch v0.7.4 would deploy a version we explicitly don't want, and generate-versions.py would happily list it.
Enforce a minimum-version floor in two places: the Determine version tag gate rejects sub-floor tags the same way it rejects malformed ones, and generate-versions.py filters sub-floor directories out of the manifest so it's self-correcting. Per our anti-pattern on duplicated source-of-truth, declare the floor once as a named constant (e.g. a DOCS_MIN_VERSION: v0.7.5 workflow env, read by both the gate and the Python scan) rather than hardcoding v0.7.5 in multiple files. There are no already-deployed old versions to clean up, so this is purely forward-looking enforcement — no gh-pages surgery needed.
🔴 Blocking — ship the maintainers release-runbook section in this PR
This PR adds zero docs/book/src/ changes — it's feat(docs) shipping docs infrastructure that's undocumented for the people who'll operate it. We already have docs/book/src/maintainers/release-runbook.md (it already references docs-deploy), so extend it rather than create a new file. The section needs: (a) the versioned-docs deploy step at release time — tag push triggers a versioned deploy, GA tags also copy to /stable/; (b) the exact initial seed sequence to bootstrap gh-pages from empty — v0.7.5, then v0.8.0-beta-1, then master; (c) how to manually redeploy a specific version via workflow_dispatch, and the DOCS_MIN_VERSION floor so maintainers know why older tags are refused. The point is that the next person running a release can deploy versioned docs by following the runbook, not by reverse-engineering the workflow.
One small cleanup to fold in while you're touching this code: generate-versions.py emits a url field ({'tag', 'label', 'url': f'/{tag}/'}) that version-selector.js never reads — the selector builds links via urlForVersion(version.tag) and only consumes .tag and .label. Drop the dead field so the manifest has one source of truth.
Net: I want this merged — the architecture is sound, the freeze model genuinely works, and CI is green. The three blockers are about landing it debt-free and operator-ready: chrome that cascades so we have one visual identity, an enforced version set so only the versions we want can deploy, and the runbook so maintainers can drive it. None of these is a redesign; they're the finishing 20% that turns this from a strong prototype into something we flip on and forget. Happy to talk through the Option-C chrome split in more detail if useful — the version-selector's base-path handling is most of the pattern already.
|
@kitswas I've got some changed planned for the docs theming and I want the to work across versions. Excellent work so far here; great design. |
|
I have completed the implementation of all fixes for blockers. Here is a summary of the work done:
The changes have been thoroughly verified via local build ( |
|
Once |
singlerider
left a comment
There was a problem hiding this comment.
Re-reviewing at eeed5be3. CI green. Closes #6997. My prior CHANGES_REQUESTED (a7edd5a) and @Audacity88's are both still in play; this review adds one focused blocker on top and does not re-litigate the three I already raised (chrome cascade, version floor, runbook) — those remain as written.
First, thank you again, @kitswas. The shape continues to hold up and the helper-script two-checkout split is solid. This pass is narrow: it's about getting Python out of the source tree before this becomes the live deploy path.
🔴 Blocking — no Python in the source tree; port both helper scripts to xtask
This PR introduces two new Python scripts under .github/scripts/ — extract-shared-chrome.py and generate-versions.py. Neither exists on master; both are net-new here. Our standing rule is no Python in source beyond throwaway CI checks, so these need to land as Rust before merge, not after.
The good news is this is a small, well-scoped port and it actively reduces duplication:
-
extract-shared-chrome.pyis a line-for-line clone of the Rustextract_shared_chromeinxtask/src/cmd/mdbook/build.rs. Two implementations of the same algorithm (same prefixes, same 8-hex-hash strip, same_shared/rewrite) will drift. Refactor the existing Rust function so its core takes(version_dir, shared_dir)explicitly, expose it as a newcargo xtask mdbook extract-chrome <version_dir> <shared_dir>subcommand, and have the in-buildassemble()path call the same core. The workflow's old-tagelsebranch then invokes the subcommand instead ofpython3 .... Delete the.py. -
generate-versions.pyhas no Rust counterpart — it's pure new logic (semver sort,DOCS_MIN_VERSIONfloor filter, stable detection, JSON emit). All of it is trivially Rust:read_dir+ a semver parse +serde_json. Expose it ascargo xtask mdbook gen-versions(readsDOCS_MIN_VERSIONfrom env, scans the target dir, prints JSON to stdout). While you're rewriting it, drop the deadurlfield I flagged last round — the Rust struct should carry onlytagandlabel, whichversion-selector.jsis the only consumer of.
Both subcommands run from the current workflow revision, exactly like the helper scripts do today via the .workflow-scripts / github.sha checkout — so the v0.7.5 backfill still works against an old tag's built output without that tag needing this code. Functionally nothing changes; the algorithm just lives in Rust.
Keep the two gen-index-*.sh as-is — they're trivial bash heredocs, not Python, and forcing them into xtask would be over-engineering a six-line redirect writer.
A nice side effect: once both helpers are xtask subcommands, the deploy-helper layer is Rust + two thin shell scripts. The workflow needs the current-revision xtask binary built once and then invoked for both extract-chrome and gen-versions, which simplifies (and in places shrinks) the .workflow-scripts sparse-checkout dance rather than complicating it.
🔴 Blocking — once the helpers are in xtask, refresh the maintainer runbook to match
The runbook this PR adds (docs/book/src/maintainers/release-runbook.md) names the Python helpers directly — e.g. "generate-versions.py ignores any directories on gh-pages below this floor." After the port, that line (and any mention of running the scripts) is stale and points operators at files that no longer exist. Update the versioned-docs section so it describes the xtask subcommands (cargo xtask mdbook gen-versions, extract-chrome) instead of the .py files, and keep the DOCS_MIN_VERSION floor explanation. This closes the same loop my prior runbook blocker opened: the operator should be able to drive versioned docs from the runbook without reverse-engineering the workflow.
🟢 Coexistence with #7055 (mdBook dashboard reskin) — checked, compatible
I checked this PR against #7055 (reskin), which is close to landing. The goals are orthogonal and reinforce each other — #7055 owns how docs look, this PR owns where versions live — and they coexist cleanly. Two concrete integration points need to be handled by whichever PR merges second so the reskin doesn't silently break on the versioned deploy:
- Chrome-extraction prefixes: #7055 adds two new generated assets,
theme/pc-themes.cssandtheme/pc-enhance.js. Theprefixeslist in the extraction logic (whatever it ends up being after the port above) must include"theme/pc-themes"and"theme/pc-enhance", or those assets won't be hoisted into_shared/and won't cascade across versions — defeating the shared-chrome model for the reskin's two largest files. book.tomladditional-jsunion: #7055 rewritesadditional-jsand dropsversion-selector.js. The mergedbook.tomlmust keepversion-selector.jsalongside #7055'spc-enhance.js/lang-switcher.js, or the version dropdown disappears from the deployed site.
No action needed in this PR specifically — just flagging so the second merge owns it. Given #7055 is self-contained and works on the current unversioned deploy, landing it first and resolving these two points in this PR's merge is the cleaner sequence, since the prefix list and book.toml are already in your diff.
Net: I want this in. The only thing newly standing between here and merge-ready is getting the two Python helpers into xtask and refreshing the runbook to match — the rest of my prior asks stand unchanged. Happy to pair on the extract-chrome core refactor since it overlaps with the chrome-cascade blocker.
| @@ -0,0 +1,113 @@ | |||
| #!/usr/bin/env python3 | |||
There was a problem hiding this comment.
This is a line-for-line clone of extract_shared_chrome in xtask/src/cmd/mdbook/build.rs (same prefixes, same 8-hex-hash strip, same _shared/ rewrite). Two copies of one algorithm will drift.
Port to cargo xtask mdbook extract-chrome <version_dir> <shared_dir>: refactor the Rust core to take (version_dir, shared_dir) explicitly so both the in-build assemble() path and this standalone subcommand share it. The workflow's else branch calls the subcommand (run from the current revision, same as the helper scripts today), then delete this file. No Python in source.
| @@ -0,0 +1,108 @@ | |||
| #!/usr/bin/env python3 | |||
There was a problem hiding this comment.
No Rust counterpart for this one — it's all new logic (semver sort, DOCS_MIN_VERSION floor, stable detection, JSON emit), and all of it is trivially Rust: read_dir + semver parse + serde_json.
Port to cargo xtask mdbook gen-versions (reads DOCS_MIN_VERSION from env, scans the target dir, prints JSON to stdout), then delete this file. While rewriting, drop the dead url field — version-selector.js only consumes .tag and .label, so the struct should carry just those two.
| rsync -a --delete "${GITHUB_WORKSPACE}/docs/book/book/_shared/" "$CLONE_DIR/_shared/" | ||
| else | ||
| # Old-tag fallback: extract chrome from the deployed version content | ||
| python3 "${SCRIPTS}/extract-shared-chrome.py" "$CLONE_DIR/$TAG" "$CLONE_DIR/_shared" |
There was a problem hiding this comment.
After the port, these two python3 ... invocations become cargo xtask mdbook extract-chrome ... and cargo xtask mdbook gen-versions > versions.json, run from the current-revision xtask binary (the .workflow-scripts / github.sha pattern already in this workflow). The v0.7.5 backfill still works against an old tag's built output without that tag carrying this code.
| To prevent accidentally deploying very old or unsupported versions, the workflow enforces a minimum version floor (currently `v0.7.5`). | ||
|
|
||
| - Tags older than `DOCS_MIN_VERSION` (like `v0.7.4`) are rejected by the workflow. | ||
| - `generate-versions.py` ignores any directories on `gh-pages` below this floor, keeping them out of the version dropdown. |
There was a problem hiding this comment.
This line names generate-versions.py directly, so it goes stale the moment the helper is ported to xtask. After the port, update this (and any other script mentions in the versioned-docs section) to describe cargo xtask mdbook gen-versions / extract-chrome instead of the .py files. Keep the DOCS_MIN_VERSION floor explanation as-is.
Audacity88
left a comment
There was a problem hiding this comment.
Context: I re-reviewed head eeed5be against the current diff, green checks, #6997, the active review thread, the docs-deploy workflow, the helper-script checkout path, and the earlier Audacity88 blocker. I’m posting this as a comment because @singlerider’s latest CHANGES_REQUESTED review remains active.
✅ Resolved — current-revision helper checkout fixes older-tag manual deploys
My earlier blocker was that workflow_dispatch could check out an older docs tag and then try to run helper scripts from that old tag, where the new helpers do not exist. The current head fixes that by keeping the docs-content checkout separate from the deploy-helper checkout:
- the main checkout still uses the requested docs ref;
- the second checkout fetches
.github/scriptsfrom${{ github.sha }}into.workflow-scripts; - the deploy step runs helpers through
SCRIPTS="${GITHUB_WORKSPACE}/.workflow-scripts/.github/scripts".
For manual workflow_dispatch, github.sha is the workflow revision rather than the requested tag input, so existing tags such as v0.7.5 can be built without needing this PR's helper files in those old tag trees. That resolves the blocker I raised.
This clears my earlier helper-checkout blocker. I’m not approving while @singlerider’s active review still blocks merge; I’m leaving those remaining issues, especially the Python-to-xtask/runbook-refresh direction in the latest review, for the author and @singlerider to resolve. The SHA-pinning policy still looks satisfied in the workflow changes; the remaining concerns are captured in @singlerider’s active review, not in the overall versioned-docs architecture.
|
@kitswas |
…nt workflow
1. **Refactored `extract_shared_chrome`**: Moved the core logic in `xtask/src/cmd/mdbook/build.rs` into a public function that explicitly accepts `version_dir` and `shared_dir`. The standard build pipeline (`assemble()`) now invokes this refactored function.
2. **Created `mdbook gen-versions`**: Implemented `generate-versions.py` entirely in Rust under `xtask/src/cmd/mdbook/versions.rs`. It reads `DOCS_MIN_VERSION`, handles semver parsing, correctly skips legacy versions below the floor, and outputs JSON with only the `tag` and `label` fields (dropping the dead `url` field).
3. **Workflow Integration (`docs-deploy.yml`)**:
- Dropped the `.workflow-scripts` sparse checkout. The step now checks out the entire repository at the triggering commit to ensure Cargo has access to the full workspace required to build `xtask`.
- Replaced the `python3` invocations with `cargo run --manifest-path "${GITHUB_WORKSPACE}/.workflow-scripts/Cargo.toml" -p xtask --bin mdbook -- ...`, executing the current-revision `xtask` binary to extract chrome or generate versions, even if deploying an old tag like `v0.7.5`.
4. **Runbook Update**: Updated `docs/book/src/maintainers/release-runbook.md` to mention the new `cargo mdbook gen-versions` command instead of the old `.py` script.
5. **Compatibility with #7055**: Verified that `theme/pc-themes` and `theme/pc-enhance` are correctly included in the `prefixes` array for `extract_shared_chrome`.
|
Updated, please check. @singlerider
There are no pedefined targets for tags. Just select/input the correct tag when deploying. |
singlerider
left a comment
There was a problem hiding this comment.
Re-reviewing at d457bdfe7. Closes #6997.
All blockers from my prior reviews are resolved, and I resolved the merge conflict against master myself — which means this merge is also where the #7055 reskin integration I flagged lands. Approving.
✅ Resolved — chrome cascades across versions
extract_shared_chrome now hoists the unhashed chrome layer into _shared/ and rewrites the per-page references with a depth-safe substring replacement, so mdBook's ../ path-to-root prefixes survive and resolve correctly from both top-level and nested pages. The workflow rsyncs _shared/ on every deploy and master writes the definitive layer. A future custom.css / theme edit now repaints every already-deployed version on its next build instead of only master + the next release. That was the one I cared about most for not baking debt in on day one — it's done right.
✅ Resolved — version floor declared once, no magic literals
DOCS_MIN_VERSION: v0.7.5 is declared a single time as a workflow env and read by both the Determine version tag gate (which now rejects sub-floor tags the same way it rejects malformed ones) and cargo xtask mdbook gen-versions (which filters sub-floor directories out of the manifest, making it self-correcting). v0.7.4 and old betas can no longer sneak in.
✅ Resolved — runbook ships in this PR
docs/book/src/maintainers/release-runbook.md Step 7 documents the deploy-on-tag behavior, the exact bootstrap seed order (v0.7.5 → v0.8.0-beta-1 → master), the "master last" rule with the reason (it writes the shared chrome layer), manual workflow_dispatch redeploys, and the DOCS_MIN_VERSION floor with how to raise it. The next person can drive versioned docs from the runbook rather than reverse-engineering the workflow.
✅ Resolved — Python out of the source tree
Both extract-shared-chrome.py and generate-versions.py are deleted and ported to xtask subcommands (extract-chrome, gen-versions). The extraction logic is now single-source — the in-build assemble() path and the workflow's old-tag fallback call the same extract_shared_chrome, so the two implementations can't drift. The dead url field is gone from the manifest; versions.rs emits only tag and label, which is all version-selector.js consumes. The two gen-index-*.sh stayed as trivial bash heredocs, which is correct — forcing those into xtask would be over-engineering.
✅ #7055 integration handled in this merge
#7055 (the mdBook dashboard reskin) landed on master, so resolving this PR's conflict is where the two integration points I called out get owned:
book.toml—additional-jskeepsversion-selector.jsalongside #7055'spc-enhance.js, andadditional-csscarries bothpc-themes.cssandcustom.css. Neither side dropped the other's assets.- Chrome-extraction prefixes — the
prefixeslist already includestheme/pc-themesandtheme/pc-enhance, so the reskin's two largest generated assets get hoisted into_shared/and cascade across versions.themes::runruns insidebuild_localesbefore the mdBook build, sopc-themes.cssexists by the time extraction walks the tree. - Both xtask modules (
versionsandthemes) and all three new subcommands (extract-chrome,gen-versions,themes) are preserved.
🟢 The freeze model holds
I traced it again rather than trust the description: each run clones the existing gh-pages, rsync -a --deletes only the freshly-built $TAG into its own subdir, and pushes. Siblings are never in rsync's destination, so they survive untouched — a master build inherits v0.7.5 from the clone and overwrites only master/. One version recompiled per run. The model is correct and the body describes it accurately.
🟢 Bootstrap and dev-serve
serve.rs nests _shared so cargo mdbook serve resolves the shared chrome locally, which closes the loop on my original "serve must resolve it" concern. The orphan-branch bootstrap path handles an empty gh-pages, and the CNAME write before every git add --all keeps the custom domain (the #6142 fix).
Thank you for sticking with this through several rounds, @kitswas. Versioned docs is real infrastructure most projects punt on indefinitely, and you landed it debt-free with the chrome cascade, an enforced version set, and an operator runbook. The architecture was sound from the start; this is the finishing 20% that makes it something we flip on and forget. Nicely done.
|
I resolved the conflicts. 'Looking good. 'Prepping for merge, now. |
# Conflicts: # docs/book/book.toml # xtask/src/bin/mdbook.rs # xtask/src/cmd/mdbook/mod.rs
singlerider
left a comment
There was a problem hiding this comment.
Re-approving at a46049f30 after the master merge for conflict resolution. Tree is identical to my prior approval at df2242a7; CI is 13/13 green. My full review stands.
|
Deployed, but there are bugs. I'll open a follow-up to resolve. |
I see stable (v0.7.5) missing from the list. And the version selector is missing from other tags like v0.8.0-beta-1. What else? https://github.com/zeroclaw-labs/zeroclaw/actions/runs/26858578286 |
…eroclaw-labs#7023) - 4a0a3ed feat(docs): implement versioned documentation deployment and version selector - 7364dd2 feat(docs): enhance version sorting and validation in deployment workflow - 9615c2d fix(docs): replace hardcoded "master" with DEFAULT_TAG in build process - a7edd5a fix(docs): added a second checkout step - bae35ce feat(docs): implement versioned documentation deployment and shared chrome extraction - 8f2a846 refactor(docs): format scripts - eeed5be refactor(build): simplify conditional checks in extract_shared_chrome function - df2242a feat(docs): migrate documentation scripts to Rust and update deployment workflow

Summary
master/<tag>/en/) — minimal structural change, works with the existinggh-pagesbranch and GitHub Pages setup.rsync-into-gh-pagespipeline with aversions.jsonmanifest and JS version selector.gh-pagespush (re-fix of fix(ci/docs-deploy): persist CNAME on every Pages deploy #6142), orphan-branch bootstrap hardening, semver-aware manifest sorting,workflow_dispatchtag input validation.gh-pagesbranch (contents: write) on every run; a broken deploy can corrupt or wipe the live site atdocs.zeroclawlabs.ai. A missingCNAMEsilently drops the custom domain. Theworkflow_dispatchpath can be triggered manually against any tag, so a malformed run could overwrite a versioned directory ingh-pages.type: docs,risk: high,size: M,docs,ciValidation Evidence (required)
Commands run and tail output:
And built the docs locally:
Beyond CI — what did you manually verify?
local_deployment) containing/v0.7.4/,/v0.7.5/,/v0.8.0-beta-1/,/master/, and/stable/versions.generate-versions.pyand served the static folder with a Python HTTP server onhttp://localhost:8080./stable/en/and the version dropdown fetchesversions.jsonrelative to the base path, with successful navigation between all versioned docs including pre-release tags.gh-pagespush on the real repo; the orphan-branch bootstrap path is exercised only on first deploy.If any command was intentionally skipped, why: None.
Security & Privacy Impact (required)
NoNoNoNoCompatibility (required)
YesNoRollback (required for
risk: high)git revert <merge-sha>onmasterand merge the revert. The next push to master will use the reverted workflow. Note: this does not undo anygh-pageswrites that already occurred — see below.docs.zeroclawlabs.aireturns a 404/GitHub 404 page. Check:CNAMEfile absent at root ofgh-pages.versions.jsonmissing or malformed atgh-pagesroot; check the Actions run log forgenerate-versions.pyoutput.gh-pagespushes blocked by theconcurrency: group: gh-pageslock; if a run is stuck, cancel it in the Actions UI.gh-pagesbranch corrupt ->git log origin/gh-pages --onelineshows unexpected commits; reset withgit push origin <last-known-good-sha>:gh-pages --force-with-lease, then manually verifyCNAMEis present before GitHub Pages re-reads the branch.