Skip to content

Publish old versions of docs#1178

Merged
alexdewar merged 4 commits intomainfrom
versioned-docs
Mar 11, 2026
Merged

Publish old versions of docs#1178
alexdewar merged 4 commits intomainfrom
versioned-docs

Conversation

@alexdewar
Copy link
Copy Markdown
Collaborator

@alexdewar alexdewar commented Mar 5, 2026

Description

I fancied doing something a bit different, so I had a go at including docs for previous releases in our GitHub Pages deployment.

My implementation treats the version of the docs in the current checked-out version of the repo as the main one and generates a TOC in a versions.md file, which is linked to from the main documentation. I've added a Python script to check out each previous version of the docs in turn and run just build-docs on it. The previous versions of the docs are not included when you run just build-docs by default, partly because this would be unnecessary and slow (you likely don't care about previous versions of the docs when developing locally) and also to avoid infinite recursion when calling just build-docs for previous versions. Instead, you have to run just build-docs::all_with_old.

Some of the old versions of the docs may have problems of one kind or another that prevent them from being built with this script without modification. For example, the book.toml config file in v2.0.0 is incompatible with the latest version of mdbook. To work around these sorts of problems, I've added a feature where we can add patch files to docs/release/patches and these are applied (using git) before the documentation is built. You can generate these files with the git format-patch command. For v2.0.0, we just need @Aurashk's fix in 3129c56. In this particular case, we could just cherry-pick the commit instead, as it is in main, but note that this may not be true of every change we need to make to old broken documentation. We don't want things to depend on commits in random unmerged branches, so it seemed cleaner to just allow for adding arbitrary patches. Let me know if you think this process should be documented, because I haven't done so yet 😄.

I've tested this locally and by deploying directly from this branch and it all seems to work, at least at first glance...

Shortcomings/future work:

  • Hyperlinks in old docs aren't currently checked by CI. We could easily do this, but I figured it was unnecessary and would slow things down.
  • It isn't obvious from the site for v2.0.0 that this isn't the main documentation page. When v2.1.0 is released, it won't be obvious from the v2.0.0 site that it is for an old version of the docs.
  • It would be nicer to have the "main" docs be listed as just another "version" ("development"?), but that was harder to do so I've left it for now
  • We could list all versions of the documentation in the TOC in the left-hand pane; currently there's just a link to a page with all of them listed. Again this would have been a bit of a faff to do.

I figure this is probably good enough to merge for now. We can always polish in future.

Let me know what you think!

Closes #1072.

Type of change

  • Bug fix (non-breaking change to fix an issue)
  • New feature (non-breaking change to add functionality)
  • Refactoring (non-breaking, non-functional change to improve maintainability)
  • Optimization (non-breaking change to speed up the code)
  • Breaking change (whatever its nature)
  • Documentation (improve or add documentation)

Key checklist

  • All tests pass: $ cargo test
  • The documentation builds and looks OK: $ cargo doc
  • Update release notes for the latest release if this PR adds a new feature or fixes a bug
    present in the previous release

Further checks

  • Code is commented, particularly in hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.32%. Comparing base (312f691) to head (b19525a).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1178   +/-   ##
=======================================
  Coverage   87.32%   87.32%           
=======================================
  Files          57       57           
  Lines        7726     7726           
  Branches     7726     7726           
=======================================
  Hits         6747     6747           
  Misses        673      673           
  Partials      306      306           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@alexdewar alexdewar force-pushed the versioned-docs branch 2 times, most recently from db1970a to d57bcd8 Compare March 6, 2026 07:55
@alexdewar alexdewar marked this pull request as ready for review March 6, 2026 12:25
Copilot AI review requested due to automatic review settings March 6, 2026 12:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-version documentation publishing to the GitHub Pages deployment by generating a versions TOC and building docs for historical release tags (optionally patched) into the deployed book/ output.

Changes:

  • Generate a versions.md page (from a Jinja template) listing links to per-release docs builds.
  • Add tooling to iterate git release tags, optionally apply per-release patch files, and build/copy old docs into book/release/<tag>/.
  • Update Just/CI wiring so the deploy workflow builds all_with_old (current + old versions) for GitHub Pages.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
docs/templates/versions.md.jinja Template for a generated page linking to historical docs versions.
docs/release/patches/v2.0.0/0001-remove-unrecognised-parameter.patch Patch applied to v2.0.0 docs so it can build with current tooling.
docs/release/init.py Utilities to detect and list release tags.
docs/generate_versions_docs.py Script to render versions.md from tags via Jinja.
docs/build_old_docs.py Script to clone, checkout tags, apply patches, and build/copy old docs.
docs/SUMMARY.md Adds the “Documentation for old versions” page to the book TOC.
docs/.gitignore Ignores generated versions.md.
build-docs.just Adds versions generation to default docs build and all_with_old target for old docs builds.
.github/workflows/deploy-docs.yml Fetches tags and runs all_with_old for deployments; configures git identity for patch application.
.github/actions/generate-docs/action.yml Adds an input to select which build-docs target to run.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# Build TOC for old versions
versions:
@echo Building TOC for old versions of documentation
@uv run docs/generate_versions_docs.py
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The versions target invokes uv run docs/generate_versions_docs.py, which declares an unpinned jinja2 dependency in its # /// script dependencies header; uv will fetch and execute the latest jinja2 from PyPI at docs-build time, creating a supply-chain risk. If an attacker compromises the jinja2 package or the PyPI registry, they could run arbitrary code in the docs build environment (e.g. in CI with access to repository tokens or the deployed docs). To mitigate this, pin jinja2 (and any other script dependencies) to immutable versions via a lockfile or explicit version specifiers, or vendor these dependencies so the build does not perform on-the-fly installs from the public registry.

Copilot uses AI. Check for mistakes.
@alexdewar alexdewar requested review from Aurashk and tsmbland March 6, 2026 12:34
Copy link
Copy Markdown
Collaborator

@tsmbland tsmbland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done just build-docs::all_with_old, but still getting "Document not found (404)" when I click the link for v2.0.0. Maybe I'm doing something wrong?

Assuming this does work, I guess it does the job quite nicely, but a bit annoying that we end up having to compile all previous releases of MUSE and build all previous versions of the documentation on every push to main. It's fine for now with only one previous release, but what if there were 100? Mostly concerned about energy use to be honest, and whether this is really worth it (although probably quite small in the grand scheme of things). Also annoying that old versions are sensitive to updates in mdbook and having to keep track of this, but hopefully that's a rare problem.

If this is the way it has to be then so be it, but just wondering if you've considered any alternatives? How is this problem addressed in other projects? Pandas, for example? Looks like they only keep documentation for the latest of each minor version (e.g. 2.3.x), which seems reasonable to me (we probably won't, and indeed shouldn't, have major documentation changes for patch versions).

@alexdewar
Copy link
Copy Markdown
Collaborator Author

I've done just build-docs::all_with_old, but still getting "Document not found (404)" when I click the link for v2.0.0. Maybe I'm doing something wrong?

Hmm... that's weird. You opened it via book/index.html, right? The old docs should be in book/release/.

Assuming this does work, I guess it does the job quite nicely, but a bit annoying that we end up having to compile all previous releases of MUSE and build all previous versions of the documentation on every push to main. It's fine for now with only one previous release, but what if there were 100? Mostly concerned about energy use to be honest, and whether this is really worth it (although probably quite small in the grand scheme of things). Also annoying that old versions are sensitive to updates in mdbook and having to keep track of this, but hopefully that's a rare problem.

Yeah, it is annoying 😞. I couldn't think of a cleaner way of doing it though. I guess it's a downside of the complex docs-generation pipeline we have. Bear in mind that the build artifacts should be cached in target/ though, so performance (and energy usage) will be much better the next time you run it (it took around ~10s on my machine). We store the cargo cache with GitHub's cache feature, so hopefully it will generally be relatively fast on CI too. We could also run the just command via asyncio, which will help with performance (though not energy consumption, I'd imagine).

We might hit problems when we have many releases, but performance might be surprisingly good with 100 releases, if the code is in the cargo cache. The problem there may be that we run out of cache space on GitHub (although you can buy more). Another option would be to use the GitHub Actions cache to store the docs in book/release/ -- rather than the code artifacts. We'd need to get things right so we actually rebuild the docs when needed (e.g. because we update scripts), but that approach could work.

We currently only need to run cargo to generate the command-line help and build the API docs. Another solution would be to skip these steps (maybe with the patching feature) for older versions. API docs are also available on docs.rs, although unfortunately it only documents public items. Might be good enough though. I think this option is probably more trouble than it's worth though.

If this is the way it has to be then so be it, but just wondering if you've considered any alternatives? How is this problem addressed in other projects? Pandas, for example? Looks like they only keep documentation for the latest of each minor version (e.g. 2.3.x), which seems reasonable to me (we probably won't, and indeed shouldn't, have major documentation changes for patch versions).

I'd be happy with this as an option. We can open an issue for it.

@tsmbland
Copy link
Copy Markdown
Collaborator

I've done just build-docs::all_with_old, but still getting "Document not found (404)" when I click the link for v2.0.0. Maybe I'm doing something wrong?

Hmm... that's weird. You opened it via book/index.html, right? The old docs should be in book/release/.

There wasn't anything there (or I think just an index file). But now I can't even get the just command to run (just build-docs works fine, but not just build-docs::all_with_old). Weird, but I guess there's no reason it has to work on my machine

 CMake Error: The current CMakeCache.txt directory C:/Users/tbland/AppData/Local/Temp/tmp7j0lkyws/target/debug/build/highs-sys-aa6616730ae9ae5f/out/build/CMakeCache.txt is different than the directory c:/Users/tbland/AppData/Local/Temp/tmp34bapunb/target/debug/build/highs-sys-aa6616730ae9ae5f/out/build where CMakeCache.txt was created. This may result in binaries being created in the wrong place. If you are not sure, reedit the CMakeCache.txt
  C:/Users/tbland/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/highs-sys-1.11.0/HiGHS/examples/call_highs_from_c_minimal.c

  thread 'main' panicked at C:\Users\tbland\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\cmake-0.1.54\src\lib.rs:1119:5:

  command did not execute successfully, got: exit code: 1

  build script failed, must exit now
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: Recipe `cli-help` failed on line 25 with exit code 101
Traceback (most recent call last):
  File "C:\Users\tbland\Documents\Code\MUSE_2.0\docs\build_old_docs.py", line 74, in <module>
    build_old_docs()
  File "C:\Users\tbland\Documents\Code\MUSE_2.0\docs\build_old_docs.py", line 70, in build_old_docs
    build_docs_for_release(release, repo_path, outdir)
  File "C:\Users\tbland\Documents\Code\MUSE_2.0\docs\build_old_docs.py", line 50, in build_docs_for_release
    sp.run(("just", f"{repo_path!s}/build-docs"), check=True)
  File "C:\Users\tbland\AppData\Roaming\uv\python\cpython-3.10.19-windows-x86_64-none\lib\subprocess.py", line 526, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '('just', 'C:\\Users\\tbland\\AppData\\Local\\Temp\\tmp7j0lkyws/build-docs')' returned non-zero exit status 101.
error: Recipe `old` failed on line 50 with exit code 1

Assuming this does work, I guess it does the job quite nicely, but a bit annoying that we end up having to compile all previous releases of MUSE and build all previous versions of the documentation on every push to main. It's fine for now with only one previous release, but what if there were 100? Mostly concerned about energy use to be honest, and whether this is really worth it (although probably quite small in the grand scheme of things). Also annoying that old versions are sensitive to updates in mdbook and having to keep track of this, but hopefully that's a rare problem.

Yeah, it is annoying 😞. I couldn't think of a cleaner way of doing it though. I guess it's a downside of the complex docs-generation pipeline we have. Bear in mind that the build artifacts should be cached in target/ though, so performance (and energy usage) will be much better the next time you run it (it took around ~10s on my machine). We store the cargo cache with GitHub's cache feature, so hopefully it will generally be relatively fast on CI too. We could also run the just command via asyncio, which will help with performance (though not energy consumption, I'd imagine).

We might hit problems when we have many releases, but performance might be surprisingly good with 100 releases, if the code is in the cargo cache. The problem there may be that we run out of cache space on GitHub (although you can buy more). Another option would be to use the GitHub Actions cache to store the docs in book/release/ -- rather than the code artifacts. We'd need to get things right so we actually rebuild the docs when needed (e.g. because we update scripts), but that approach could work.

We currently only need to run cargo to generate the command-line help and build the API docs. Another solution would be to skip these steps (maybe with the patching feature) for older versions. API docs are also available on docs.rs, although unfortunately it only documents public items. Might be good enough though. I think this option is probably more trouble than it's worth though.

Ah ok, thanks for explaining! I didn't fully appreciate the caching thing so this alleviates my concerns

If this is the way it has to be then so be it, but just wondering if you've considered any alternatives? How is this problem addressed in other projects? Pandas, for example? Looks like they only keep documentation for the latest of each minor version (e.g. 2.3.x), which seems reasonable to me (we probably won't, and indeed shouldn't, have major documentation changes for patch versions).

I'd be happy with this as an option. We can open an issue for it.

Less concerned about this now

@alexdewar
Copy link
Copy Markdown
Collaborator Author

I've done just build-docs::all_with_old, but still getting "Document not found (404)" when I click the link for v2.0.0. Maybe I'm doing something wrong?

Hmm... that's weird. You opened it via book/index.html, right? The old docs should be in book/release/.

There wasn't anything there (or I think just an index file). But now I can't even get the just command to run (just build-docs works fine, but not just build-docs::all_with_old). Weird, but I guess there's no reason it has to work on my machine

 CMake Error: The current CMakeCache.txt directory C:/Users/tbland/AppData/Local/Temp/tmp7j0lkyws/target/debug/build/highs-sys-aa6616730ae9ae5f/out/build/CMakeCache.txt is different than the directory c:/Users/tbland/AppData/Local/Temp/tmp34bapunb/target/debug/build/highs-sys-aa6616730ae9ae5f/out/build where CMakeCache.txt was created. This may result in binaries being created in the wrong place. If you are not sure, reedit the CMakeCache.txt
  C:/Users/tbland/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/highs-sys-1.11.0/HiGHS/examples/call_highs_from_c_minimal.c

  thread 'main' panicked at C:\Users\tbland\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\cmake-0.1.54\src\lib.rs:1119:5:

  command did not execute successfully, got: exit code: 1

  build script failed, must exit now
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: Recipe `cli-help` failed on line 25 with exit code 101
Traceback (most recent call last):
  File "C:\Users\tbland\Documents\Code\MUSE_2.0\docs\build_old_docs.py", line 74, in <module>
    build_old_docs()
  File "C:\Users\tbland\Documents\Code\MUSE_2.0\docs\build_old_docs.py", line 70, in build_old_docs
    build_docs_for_release(release, repo_path, outdir)
  File "C:\Users\tbland\Documents\Code\MUSE_2.0\docs\build_old_docs.py", line 50, in build_docs_for_release
    sp.run(("just", f"{repo_path!s}/build-docs"), check=True)
  File "C:\Users\tbland\AppData\Roaming\uv\python\cpython-3.10.19-windows-x86_64-none\lib\subprocess.py", line 526, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '('just', 'C:\\Users\\tbland\\AppData\\Local\\Temp\\tmp7j0lkyws/build-docs')' returned non-zero exit status 101.
error: Recipe `old` failed on line 50 with exit code 1

I actually had a similar problem. I assumed it was because there was version of highs-sys in my cache built with an old version of my C++ compiler that I didn't have installed any more (sadly, CMake is not particularly robust to this sort of thing). I nuked the entire target folder, but you might want to just try removing the offending item so you don't end up having to rebuild a bunch of other packages (i.e. delete c:/Users/tbland/AppData/Local/Temp/tmp34bapunb/target/debug/build/highs-sys-aa6616730ae9ae5f).

Copy link
Copy Markdown
Collaborator

@Aurashk Aurashk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a good solution for now!

Copilot AI review requested due to automatic review settings March 10, 2026 21:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

build-docs.just:13

  • docs/SUMMARY.md now references versions.md, which is generated and gitignored. If someone runs only the book recipe (or any recipe that ends up calling mdbook build without first running versions), mdBook will fail because docs/versions.md won’t exist. Consider making book depend on versions (or generating versions.md inside book) so partial builds still work.
# Build book
book:
    @echo Building book
    @mdbook build

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


Previous versions of the MUSE2 documentation are available below.
{% for release in releases %}
- [{{ release }}](release/{{ release }}/index.html)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The links are relative to the current page (e.g. release/v2.0.0/index.html). If this versions page ever appears inside an old release build (e.g. under release/v2.1.0/), those links will resolve to release/v2.1.0/release/v2.0.0/... and be broken. Consider rendering absolute links (e.g. https://…/MUSE2/release/<tag>/index.html) or passing a base_url into the template that can differ between main docs and old-release docs builds.

Suggested change
- [{{ release }}](release/{{ release }}/index.html)
- [{{ release }}]({{ base_url | default('') }}release/{{ release }}/index.html)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually these versions.md files won't actually end up being reachable from anywhere, so it doesn't matter

@alexdewar alexdewar enabled auto-merge March 10, 2026 21:39
@alexdewar alexdewar disabled auto-merge March 11, 2026 08:30
Copilot AI review requested due to automatic review settings March 11, 2026 08:30
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +28 to +29
with (DOCS_DIR / "versions.md").open("w", encoding="utf-8") as f:
f.write(out)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the other doc-generation scripts (e.g. generate_example_docs.py), consider writing versions.md via Path.write_text(..., encoding="utf-8") (or open with an explicit UTF-8 encoding). This avoids locale-dependent output and keeps the scripts stylistically consistent.

Suggested change
with (DOCS_DIR / "versions.md").open("w", encoding="utf-8") as f:
f.write(out)
(DOCS_DIR / "versions.md").write_text(out, encoding="utf-8")

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 11, 2026 08:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +40
def apply_patches_for_release(release: str, repo_path: Path) -> None:
"""Apply patches (if any) for the given release."""
patches_dir = DOCS_DIR / "release" / "patches" / release
for patch_path in sorted(patches_dir.glob("*.patch")):
sp.run(("git", "-C", str(repo_path), "am", str(patch_path)), check=True)

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

git am requires a committer identity; on machines/CI environments without user.name/user.email configured, applying patches will fail with “Please tell me who you are”. Since this script is intended to run in a temp clone, consider setting a local identity for that clone (e.g. git -C <repo> config user.name/user.email, or passing -c user.name=... -c user.email=... to git am) so just build-docs::all_with_old works in a clean environment.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this explicitly on the CI

Copilot AI review requested due to automatic review settings March 11, 2026 09:24
@alexdewar alexdewar enabled auto-merge March 11, 2026 09:24
@alexdewar alexdewar disabled auto-merge March 11, 2026 09:25
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6 to +8
{%- for release in releases %}
- [{{ release }}](release/{{ release }}/index.html)
{%- endfor %}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Jinja whitespace control in {%- for release in releases %} will strip the newline before the loop, which is likely to merge the “Current development version” bullet with the first generated release bullet into a single line. Use {% for release in releases %} (and similarly for endfor) or move the - to the end tag ({% for ... -%}) if you only intended to trim trailing whitespace.

Suggested change
{%- for release in releases %}
- [{{ release }}](release/{{ release }}/index.html)
{%- endfor %}
{% for release in releases %}
- [{{ release }}](release/{{ release }}/index.html)
{% endfor %}

Copilot uses AI. Check for mistakes.
@alexdewar alexdewar merged commit ceb32bc into main Mar 11, 2026
8 checks passed
@alexdewar alexdewar deleted the versioned-docs branch March 11, 2026 09:36
@alexdewar
Copy link
Copy Markdown
Collaborator Author

I've opened an umbrella issue for extra polishing we could do: #1187

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Documentation page for each release

4 participants