Skip to content

Implement multi-version documentation support (follow-up to #320) #325

Description

@miharp

Summary

Add multi-version documentation support, following the copy-on-major-release model
agreed in #320 (versioned directories on one branch, per-product independent latest,
pinned reference generation, a version selector in the UI).

Driver: OpenVox 9 is approaching and we can't yet stand up _openvox_9x while
freezing _openvox_8x. This issue tracks the work.

Scope is split deliberately: a small must-have track to ship 9.0 docs, and a
hardening backlog of correctness/automation items that are good ideas but are
not release-blocking and should not pad the critical path. Follow-up to #320.


Must-have track (ship OpenVox 9 docs)

Three pieces. This is the whole critical path.

1. Reference generator: per-version pinning (the only real engineering)

Today output dirs and emitted URLs are fixed at load time (OUTPUT_DIR
lib/puppet_references.rb:15, puppet/type.rb:19; OpenBolt hardcodes _openbolt_5x
in openbolt/docs.rb:9), and repo.newest_release rejects prereleases (repo.rb:50).
We need to build a chosen version into a chosen collection without cross-writing.

  • Fix the OpenFact URL bug first: generators write into _openfact_* but stamp
    @latest = '/openvox/latest' (facter/core_facts.rb:12, facter/facter_cli.rb:10).
    Wrong product URL today, independent of versioning.
  • Move output paths from load-time constants to instance/config-level values so
    one process can target a specific collection (don't drive it by mutating
    ENV['COLLECTION'] in a loop — the constants freeze at require time).
  • Generator emits intra-product links/canonical: relative to its target
    collection, not hardcoded /<product>/latest/.
  • Rakefile accepts COLLECTION=<dir> + VERSION=; an explicit VERSION bypasses
    the prerelease filter (so we can build 9.x from an RC). No-arg behavior unchanged.
  • A references:all-style task builds each pinned (tag → version dir) pair;
    build.yaml calls it. latest is an alias, never its own build row — only
    real version dirs are built; nothing writes through the _latest symlink.

2. The cutover + runbook

  • Write the "add a major version" runbook: copy _openvox_8x → _openvox_9x,
    repoint the _latest symlink, register the collection + defaults in
    _config.yml, add nav files (_data/nav/, nav_map.yml, navigation.yml),
    build both versions.
  • Include a rollback line (repoint symlink back, drop the new collection/nav
    entries, republish).
  • Dry-run against an OpenVox 9 prerelease tag before the real release — this is
    the gate the timeline is really about. Eyeball both trees; confirm 8.x didn't move.
    (Done in Add maintainer runbook for the major-version cutover #337: full two-phase cutover dry-run against 9.0.0-alpha1, 8.x confirmed frozen. A final dry-run at the real 9.0 tag is still worth doing at cutover time.)

3. Version selector

  • _includes/version-selector.html at the top of sidebar.html; reuse the existing
    current_nav_page_subpath / current_nav_base logic for path mapping.
  • Show only the current product's versions (from a small _data/products.yml);
    mark current; keep the reader on the same page path when switching, falling back
    to the version root when the target page doesn't exist.
    (Shipped in Add a per-product documentation version selector #335: per-product picker from products.yml, marks current, badges the
    latest-aliased version, hidden until a product has 2+ versions. It links to each
    version's root, not the same page path — path-preserving switching was
    deferred as a follow-up and is tracked in the hardening backlog below.)

_data/products.yml is introduced here as a lightweight list the selector reads (and a
handy home for the reference pins in piece 1) — not a heavyweight source-of-truth that
everything else is generated from.


Hardening backlog (not release-blocking)

Good ideas surfaced in review. Do them incrementally; none should gate 9.0. Most exist
because we're optimizing an event that happens ~once a year — a runbook checklist and a
human in the loop cover the risk for now.

  • Intra-product relative-link sweep. Convert /<product>/latest/ authored body
    links to relative so frozen docs stay internally coherent (~148 in OpenVox).
    Cheaper before the copy, but non-blocking: if left as latest, frozen 8.x links
    drift to 9.x — a real but fixable-later UX bug. Depth-aware (subdir pages need ../).
  • Cross-product link policy. Relative links can't span products. For v1, accept
    cross-product links following latest; later, optionally pin partner versions via
    a cross_product_affinity map for lockstep products (e.g. openvoxdb 8x → openvox 8x).
  • Drift validation. (Partial: rake test:products_data runs in CI, but only
    validates products.yml internally.)
    Optional Rake task asserting _config.yml / navigation.yml
    / nav_map.yml / _latest symlinks agree with products.yml, run in CI before
    jekyll build. A runbook checklist covers the once-a-year case until then. Note:
    _config.yml collections can't be generated within Jekyll (read before _data),
    so generation isn't an option for the highest-drift surface anyway.
  • Lint gate flagging new hardcoded intra-product /latest/ body links
    (pattern-based, not existence-based). Pairs with the relative-link sweep.
  • Cross-write check. When building piece 1, diff touched paths from a two-version
    build once to confirm no cross-write. Don't need a standing CI gate.
    (Done: Add maintainer runbook for the major-version cutover #337 dry run and the Add "Component versions in recent OpenVox releases" reference page #339 throwaway 9.x cutover; Add OpenVox 9.x docs collection as a preview (latest stays on 8.x) #430 in prod confirms.)
  • Frozen-version link proofing. Extend rake test:links (today checks /latest/
    only, Rakefile:76) to also proof frozen version trees.
  • Canonical on frozen builds. Decide whether frozen versions keep
    canonical: /<product>/latest/... (SEO → current) or get per-version canonicals.
    Default: keep pointing at latest.
    (Decided in Add pinned out-of-date banner and /latest/ canonicals for versioned docs #437: the numbered copy of the latest version canonicalizes to its
    /latest/ twin; frozen older versions keep the theme's self-canonical.)
  • No-redirect awareness. When latest moves 8→9, pages removed/renamed in 9.x
    404 at /<product>/latest/<page> for old bookmarks (content still at /8.x/).
    Note in the runbook; we have no redirect mechanism. (In MAINTAINING.md, Phase 2 step 5.)
  • OpenVox Containers edge case. It has a real _openvox-containers_latest dir
    (not a symlink) and no numbered collection — selector/validator logic must not
    assume the uniform model. (single_version: true in products.yml; selector, banner,
    and test:products_data all skip it.)
  • Shared/version-invariant content (link vs. copy): explicitly out of scope; revisit.
  • Path-preserving version switch. The selector (Add a per-product documentation version selector #335) links to each version's
    root; keeping the reader on the same page path across a switch (falling back to
    the root when the target page doesn't exist) needs per-navigation JS. Deferred
    follow-up from piece 3.

Part of #320.

Activity

  1. miharp commented on Jun 11, 2026

    @miharp
    ContributorAuthor

    Progress update

    The first two pieces of the must-have track are up:

    Piece 1 — generator per-version pinning ✅ merged (#326)

    • Reference generators are now parameterized by target collection (COLLECTION=),
      with URLs/canonicals derived from the collection rather than hardcoded /latest/.
    • localize_links rewrites a generated page's own intra-product links to its
      version base, so frozen versions stay coherent (cross-product links still follow
      latest per policy).
    • Fixed a latent OpenFact bug (pages stamped /openvox/latest while writing into
      _openfact_*) and keyed the Strings JSON cache per collection.
    • An explicit VERSION already bypasses the prerelease filter, so a version can be
      built from a prerelease tag (verified against 9.0.0-alpha1).
    • CONTRIBUTING documents the COLLECTION flag.

    Piece 1b + CI wiring — references:all + _data/versions.yml 🔍 in review (#328)

    • _data/versions.yml is the source-of-truth registry: per product/version
      collection, base URL, latest alias, and (for generated products) the rake task
      • exact upstream tag to build from. latest is an alias, never its own build row;
        authored-only products carry no pin.
    • rake references:all builds every pinned (tag → collection) pair in its own
      subprocess; build.yaml now calls it instead of the three hardcoded lines.

    Heads-up — one intended, deploy-visible behavior change in #328: generated
    reference pages now build into the real version dirs, so their inline body links
    resolve to the concrete version (e.g. /openvox/8.x/...) rather than /latest/.
    This is what keeps a frozen version coherent once a new major ships. Scope is narrow
    (the type-preamble links); nav chrome and canonicals are unaffected; a full local
    build + htmlproofer found 0 broken links across 1776.

    Decisions settled along the way

    • Reference pins use exact tags, not branches or "newest" — reproducible and
      avoids newest/main jumping majors.
    • Cross-product links follow latest (already the documented policy); only
      intra-product links get version-localized.
    • The version selector keys (label, base, latest, single_version) live in
      versions.yml now but aren't consumed until the selector lands.

    Remaining on the must-have track

    • Piece 2 — version selector UI (consumes the registry fields above).
    • Cutover runbook + dry run against an OpenVox 9 prerelease.

    The earlier hardening backlog (drift validation, link-hygiene lint, frozen-version
    link tests, etc.) remains non-blocking.

  2. miharp commented on Jun 11, 2026

    @miharp
    ContributorAuthor

    Update

    Progress since the last note:

    Version selector — built and verified 🔍 (held to stack on #328)

    A custom per-product selector at the top of the sidebar (driven by the pin
    table). It lists the current product's versions, badges the one latest aliases,
    marks the version being viewed, and is hidden until a product actually has 2+
    versions. Verified against a local two-version test bed (OpenVox 8.x + a
    9.0.0-alpha1 9.x): correct badge/active on direct loads of 8.x, 9.x and the latest
    alias, correct group-swap + active marking across Turbo navigation between
    products, and Containers excluded as single-version.

    Notable: the VitePress theme ships a built-in version selector, but it's a single
    global dropdown — it can't express per-product versions on a multi-product
    site, so a custom selector was the right call. It also meant our data file was
    squatting on the theme's reserved site.data.versions, so the pin table was
    renamed _data/versions.yml → _data/products.yml (folded into #328).

    Version links currently point at each version's root; keeping the reader on the
    same page across a switch (path-preserving) needs per-navigation JS in the
    persistent Turbo sidebar and is a noted follow-up.

    Cutover mechanics validated

    Standing up the test bed doubled as a dry-run of the "add a major version" steps:
    cp -r the docs dir → register the collection in _config.yml (collections: +
    defaults:) → add it to the product's nav_map.yml entry → add a version block to
    products.yml → generate refs. All worked end-to-end, so the cutover runbook is
    largely de-risked.

    State of the must-have track

  3. miharp commented on Jun 11, 2026

    @miharp
    ContributorAuthor

    Cutover-runbook note: version-string content sweep

    A dry run of the cutover (standing up a local _openvox_9x from _openvox_8x)
    surfaced a step beyond the structural mechanics: after copying a product's docs
    directory, the authored pages still carry the old version's strings — the index
    # OpenVox 8 title, the "OpenVox 8 Platform" nav heading, "8.x" references, and
    version-specific prose. The version selector only routes between collections; it
    doesn't rewrite content.

    So the cutover runbook needs an explicit content-sweep step for the copied
    authored pages: update titles, nav headings, and intra-doc version references to the
    new major. A blind find/replace is risky — some "OpenVox 8" / "8.x" mentions are
    legitimately historical or compatibility notes — so it's a careful pass, not a
    global substitution.

    Generated reference pages don't need this: references:all rebuilds them from the
    new version's pinned tag, so they self-update.

    Updated cutover steps (structural mechanics validated by the dry run):

    1. cp -r docs/_<product>_<old> docs/_<product>_<new>
    2. Register the new collection in _config.yml (collections: + defaults:)
    3. Add the new collection to the product's nav_map.yml entry
    4. Add a version block to _data/products.yml (and move latest: if appropriate)
    5. Content sweep the copied authored pages for version-specific strings ← new
    6. Generate refs for the new version (references:all, or per-product with the pin)
  4. miharp commented on Jun 11, 2026

    @miharp
    ContributorAuthor

    Refinement on the content-sweep step (5)

    Looked at whether the version strings could be templated to avoid the sweep
    entirely — they can't, sensibly. In the OpenVox docs it's only ~40 hits (~23
    "OpenVox 8" + ~17 "8.x") across ~10 files, and most are version-specific
    assertions
    , not "current version" placeholders, e.g.:

    • "OpenVox 8.x still supports it for backward compatibility, but use v5 instead"
    • "This style guide applies to OpenVox 8 and later"
    • the title: "Upgrading OpenVox 8" page is inherently about 8

    A templated OpenVox {{ version }} would auto-falsify these on cutover (the compat
    note must not silently claim 9.x). Page title: also lives in front matter, which
    Jekyll doesn't run Liquid through. So the strings stay literal and the sweep stays a
    human review — but a guided one.

    Refined step 5: instead of an ad-hoc "content sweep," run a grep over the freshly
    copied collection and review each hit in context (decide bump vs. keep-as-historical):

    grep -rnE 'OpenVox [0-9]+|[0-9]+\.x' docs/_openvox_9x --include=*.md --include=*.markdown

    It's a few-minutes, once-per-major task, and human review is required regardless
    because the statements are version-specific (not mechanically substitutable).

  5. 4 remaining items

  6. miharp commented on Jun 15, 2026

    @miharp
    ContributorAuthor

    Status update — must-have track

    Piece 2 (cutover + runbook) — done in #337. MAINTAINING.md covers the copy → symlink repoint → _config.yml/nav registration → build-both-versions flow, plus a rollback section. The dry-run gate was exercised: a full two-phase OpenVox 9 cutover against 9.0.0-alpha1, with 8.x confirmed frozen. (A final dry-run at the real 9.0 tag is still worth doing at cutover time.)

    Piece 3 (version selector) — shipped in #335, with one deferred refinement. The per-product picker reads _data/products.yml, marks the current version, badges the latest-aliased one, and stays hidden until a product has 2+ versions. It links to each version's root, not the same page path — path-preserving switching was deferred and is now tracked as a hardening-backlog item. Flagging so the checked box isn't read as "same-page switch works."

    Related: per-series generated data (prereq #332)

    #339 (the new component-versions reference page) implements the multi-series data partitioning #332 proposed, and extends it beyond the agent table to server and OpenVoxDB:

    Note #339 also relocated the agent release-contents table off about_agent.md (now a pointer) onto the new page, so the original "about_agent shared-variable collision" in #332 no longer applies.

  7. miharp commented on Aug 27, 2026

    @miharp
    ContributorAuthor

    Status update — 9.x preview shipped; must-have track complete

    #430 merged today: docs/_openvox_9x is live as a preview at /openvox/9.x/ (pinned to
    9.0.0-beta2), latest still points at 8.x, the version selector now shows for OpenVox,
    and references:all builds both versions in prod. That was the driver for this issue, so
    the must-have track is done end to end. Server, OpenVoxDB, and OpenFact 6 get the same
    Phase 1 treatment in separate PRs; the Phase 2 promotion happens at 9.0.0 GA per
    MAINTAINING.md.

    Reconciling the hardening backlog against what has landed since:

    Done (closing the boxes):

    Mostly done:

    • Intra-product relative-link sweep — the ~148 OpenVox links are down to 3 (plus 4 in
      OpenVox Server), swept as part of the Fix broken internal links in the openvox collection #262 link work. The 3 OpenVox ones are duplicated
      into 9.x by the copy, so they currently point a 9.x reader at 8.x content
      (openvox_strings.md, reporting_about.md, system_requirements.markdown). Small fix.

    Still open, and more relevant now that a frozen/preview tree exists:

    • Frozen-version link proofing — rake test:links still proofs only <collection>/latest
      (Rakefile), so the 9.x tree is unchecked in CI. A manual htmlproofer pass during Add OpenVox 9.x docs collection as a preview (latest stays on 8.x) #430
      caught a real break (configuration.html#pluginsync, a setting removed in 9.x), so this is
      the highest-value remaining item.
    • Lint gate for new hardcoded /latest/ body links — not built.
    • Path-preserving version switch — selector still links to each version's root.
    • Cross-product link policy — v1 (follow latest) is in effect; no affinity map.
    • Shared/version-invariant content — still out of scope.

    Additions since the issue was written:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions