Skip to content

chore(docs): keep latest patch per minor in multi-version builds - #7096

Open
rh-hemartin wants to merge 1 commit into
mainfrom
hemartin/7071-mvb-latest-patch-per-minor
Open

chore(docs): keep latest patch per minor in multi-version builds#7096
rh-hemartin wants to merge 1 commit into
mainfrom
hemartin/7071-mvb-latest-patch-per-minor

Conversation

@rh-hemartin

Copy link
Copy Markdown
Member

Summary

Keep only the latest patch of each minor in the docs multi-version build. mvb currently ships every tag that satisfies >=0.37.0, so both v0.42.0 and v0.42.1 appear once both exist.

Related Issue

Fixes #7071

Changes

  • Add getLatestPatchMatching to turn the existing floor into a caret range-set (^0.37.0 || ^0.38.1 || …) so semver.satisfies per tag rejects older patches of the same minor
  • Share MVB_TAG_MATCH (v[0-9].*) between git tag --list and multiVersionBuild.match
  • Add semver as a direct dependency and unit tests for the range builder
  • Document the filter in docs/doc-site.md

The >=0.37.0 floor is unchanged.

Testing

  • make lint passes (stage changes first, then run)
  • Tests added/updated for new or modified logic

Checklist

  • PR title follows Conventional Commits (correct type, ! for breaking changes)
  • Commits are signed off (DCO) — human and human-directed agent sessions only
  • I wrote this contribution myself and can explain all changes in it

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Keep latest patch per minor in multi-version docs builds

🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Select only the latest qualifying patch for each minor documentation version.
• Share version-tag filtering between Git discovery and the multi-version builder.
• Add semver tests, dependency metadata, and documentation for version selection.
Diagram

graph TD
  A["Git Tags"] --> B["Tag Listing"] --> C["Patch Selector"] --> D["Caret Range"] --> E["MVB Filter"] --> F["Docs Versions"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Exact-version range-set
  • ➕ Expresses the latest-patch-only requirement directly
  • ➕ Remains correct for versions at or above 1.0.0
  • ➕ Avoids caret ranges overlapping later minors
  • ➖ Produces a deliberately restrictive predicate tied to currently discovered tags
  • ➖ Requires regenerating the range whenever tag discovery changes, as does the current approach
2. Add grouping support upstream to MVB
  • ➕ Makes latest-per-minor selection a first-class builder capability
  • ➕ Avoids running Git and constructing ranges inside VitePress configuration
  • ➖ Requires an upstream API change and release
  • ➖ Adds coordination and upgrade overhead for a narrowly scoped need

Recommendation: The PR's local range builder is a pragmatic fit for MVB's per-tag predicate API. However, an exact-version disjunction is the safest formulation because caret ranges isolate minors for current 0.y.z releases but can overlap later minors after 1.0.0; use exact comparators if the helper is intended to remain valid beyond pre-1.0 versions.

Files changed (6) +94 / -8

Bug fix (2) +52 / -1
config.tsGenerate the MVB version predicate from repository tags +22/-1

Generate the MVB version predicate from repository tags

• Adds shared tag matching and Git tag discovery, then generates the multi-version build's satisfies range from the latest qualifying patch of each minor. Tag-listing failures safely fall back through an empty result.

docs/.vitepress/config.ts

mvbSatisfies.tsBuild latest-patch-per-minor semver ranges +30/-0

Build latest-patch-per-minor semver ranges

• Introduces a helper that cleans and filters tags, groups stable qualifying versions by minor, and emits a caret range-set based on each minor's latest patch. It preserves the original range when no tags qualify.

docs/.vitepress/mvbSatisfies.ts

Tests (1) +33 / -0
mvbSatisfies.test.tsTest latest-patch semver range generation +33/-0

Test latest-patch semver range generation

• Covers latest-patch selection, per-tag rejection of older patches, prerelease and invalid-tag handling, and fallback behavior when no versions qualify.

docs/.vitepress/mvbSatisfies.test.ts

Documentation (1) +1 / -1
doc-site.mdDocument multi-version patch filtering +1/-1

Document multi-version patch filtering

• Explains the shared Git tag glob, latest-patch range generation, and why MVB requires a custom predicate for per-minor selection.

docs/doc-site.md

Other (2) +8 / -6
package-lock.jsonRecord semver as a direct dependency +1/-0

Record semver as a direct dependency

• Adds semver to the root package's locked direct development dependencies.

package-lock.json

package.jsonDeclare semver for documentation version filtering +7/-6

Declare semver for documentation version filtering

• Adds semver as a direct development dependency used by the range builder and tests. Existing development dependencies are also reordered consistently.

package.json

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 8, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 9:56 AM UTC · Completed 10:11 AM UTC

Commit: 18eddce · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.57

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Docs builds omit valid release minors 📘 Rule violation ⚙ Maintainability
Description
getLatestPatchMatching passes ^major.minor to semver.maxSatisfying and emits ^version, but
for major versions of at least 1 those caret ranges span later minors within the same major instead
of selecting one minor’s latest patch. Because MVB_TAG_MATCH and the >=0.37.0 floor admit stable
majors, tags such as v1.2.1 and v1.3.2 make both iterations select 1.3.2, excluding the entire
v1.2 documentation set despite the documentation promising the latest patch from every minor.
Code

docs/.vitepress/mvbSatisfies.ts[26]

+    .map((minor) => semver.maxSatisfying(versions, `^${minor}`))
Relevance

●●● Strong

Directly exposes a real stable-major semver bug contradicting this PR’s stated latest-per-minor
intent.

PR-#6799
PR-#6683

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2748504 requires documentation of changed user-facing behavior to remain consistent
with the implementation. The shared tag pattern accepts v1.x and later tags, and the helper’s
>=0.37.0 floor does not exclude them; although the implementation groups versions by major and
minor, it calls maxSatisfying with a patchless caret range and emits another caret range. Carets
are minor-bounded for the covered 0.y.z cases, but stable-major carets span subsequent minors,
while the tests cover only major zero, proving that the implementation does not uphold the
documented promise to retain the latest patch of every minor.

Rule 2748504: Update docs when changing CLI behavior or public API
docs/.vitepress/mvbSatisfies.ts[22-29]
docs/doc-site.md[33-33]
docs/.vitepress/config.ts[21-25]
docs/.vitepress/mvbSatisfies.ts[7-10]
docs/.vitepress/mvbSatisfies.ts[15-28]
docs/.vitepress/mvbSatisfies.test.ts[5-17]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description

`getLatestPatchMatching` relies on caret ranges to select and emit each minor’s latest patch, but those ranges are minor-bounded only for relevant `0.y.z` versions. For major versions above zero, they span later minors within the same major, causing older minor documentation to be omitted.

## Issue Context

`MVB_TAG_MATCH` admits all numeric major versions, and the unchanged `>=0.37.0` floor also admits versions at or above `1.0.0`. Construct an explicit minor-bounded range for each selected `major.minor`, or emit exact selected versions, rather than relying on caret semantics; add a regression test containing multiple minors within the same stable major release.

## Fix Focus Areas

- docs/.vitepress/mvbSatisfies.ts[22-29]
- docs/.vitepress/mvbSatisfies.test.ts[5-17]
- docs/.vitepress/config.ts[21-22]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 67 rules
✅ Cross-repo context — repo relationships
  Explored: repo: fullsend-ai/experiments (sha: 25946a93)
Review mode: ⚖️ Balanced: This changes docs build selection logic, semver range construction, Git tag discovery, dependencies, and tests across several files, creating meaningful behavioral and configuration risk despite the localized scope.

Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/.vitepress/mvbSatisfies.ts Outdated
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Site preview

Preview: https://84231f20-site.fullsend-ai.workers.dev

Commit: e53f0f6007e09aa468934d240d291fba5ae63e33

@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Sep 8, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 8, 2026

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Re-review confirms prior moderate score of 2: Tier 1 signals unchanged (6 files, 0 protected paths, 2 dependency files, non-bot non-first-time author), composite 2.1; Tier 2 moderate at 2.8 driven by high churn and author diversity on config.ts and package files offset by two brand-new files with no history; Tier 3 low at 1.7 reflecting tight issue-to-PR alignment and rollback safety; weighted composite (50%×2.1 + 30%×2.8 + 20%×1.7)=2.23 rounds to 2.

Previous run

Risk Assessment: moderate (2/5)

Details

Re-review confirms prior moderate score: Tier 1 signals unchanged (same file count, protected paths, dependency files, author type), composite of 2.10 rounds to 2. Well-scoped docs-infrastructure change with new utility and tests aligned to issue #7071; moderate risk driven by two dependency files touched and high author diversity/churn on config.ts and package files, offset by no security, CI, or protected-path exposure and strong issue alignment.

Previous run (2)

Risk Assessment: moderate (2/5)

Details

Small, well-scoped docs-infrastructure change with test coverage and clear issue alignment; risk is moderate primarily due to two dependency files being touched and high author diversity in the docs area, but no security, CI, or protected-path exposure.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 8, 2026

Copy link
Copy Markdown

Looks good to me

Previous run

Review

Findings

Medium

  • [documentation-accuracy] docs/doc-site.md:33 — The documentation describes the output of getLatestPatchMatching as a "caret range-set" but the implementation builds tilde ranges (~version). The JSDoc explicitly says "Join one tilde per minor", the variable is named tildes, and every test assertion confirms tilde semantics. The word "caret" in the doc is incorrect.
    Remediation: Replace "caret range-set" with "tilde range-set" on line 33 of docs/doc-site.md.

Low

  • [naming-convention] docs/.vitepress/mvbSatisfies.ts — New file uses camelCase naming (mvbSatisfies.ts) while every existing sibling in docs/.vitepress/ uses lowercase or kebab-case: seo.ts, config.ts, lando-theme.d.ts, search.d.ts. The corresponding test file (mvbSatisfies.test.ts) has the same inconsistency.
    Remediation: Rename to mvb-satisfies.ts (and mvb-satisfies.test.ts) to match the kebab-case/lowercase convention. Update the import in config.ts accordingly.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

Low

  • [future-proofing] docs/.vitepress/mvbSatisfies.ts:26 — The caret-range approach (^version) for the maxSatisfying lookup and output only constrains to a single minor for 0.x versions. For major ≥ 1, ^X.Y.Z desugars to >=X.Y.Z <(X+1).0.0-0 (per-major), which would cross minors and produce incorrect filtering. The code works correctly for its documented and current 0.x scope (the JSDoc explicitly notes “On 0.y.z, ^0.y.z desugars to >=0.y.z <0.(y+1).0”), but using tilde (~) instead of caret (^) would future-proof the function at no cost to current behavior — ~0.y.z is identical to ^0.y.z, and ~1.y.z correctly constrains to >=1.y.z <1.(y+1).0-0.
    Remediation: Replace ^ with ~ on lines 26 and 28 (semver.maxSatisfying(versions, \${minor}`)and.map((version) => `${version}`)`), and update the JSDoc comment to reference tilde semantics.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@rh-hemartin
rh-hemartin force-pushed the hemartin/7071-mvb-latest-patch-per-minor branch from 18eddce to 1fa3a4d Compare September 8, 2026 12:12
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 8, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 12:14 PM UTC · Completed 12:29 PM UTC

Commit: 1fa3a4d · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $4.26

fullsend-ai-review[bot]

This comment was marked as outdated.

Comment thread docs/.vitepress/mvb-satisfies.ts
Comment thread docs/.vitepress/config.ts Outdated
mvb tests each git tag with semver.satisfies independently, so a
plain range includes every patch. Build a caret range-set from the
latest qualifying patch of each minor instead.

Signed-off-by: Hector Martinez <hemartin@redhat.com>
@rh-hemartin
rh-hemartin force-pushed the hemartin/7071-mvb-latest-patch-per-minor branch from 1fa3a4d to e53f0f6 Compare September 9, 2026 08:33
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 9, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:35 AM UTC · Completed 8:52 AM UTC

Commit: e53f0f6 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $3.90

@fullsend-ai-review
fullsend-ai-review Bot dismissed stale reviews from themself September 9, 2026 08:52

Superseded by updated review

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Sep 9, 2026
Comment thread docs/.vitepress/config.ts
const MVB_TAG_MATCH = "v[0-9].*";

/** Git tags mvb will later re-test with `semver.satisfies` (one version at a time). */
function gitVersionTags(match: string): string[] {

@waynesun09 waynesun09 Sep 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Tag enumeration runs before mvb fetches tags — a stale local clone now ships the OLD patch and drops the newest

Verified against @lando/vitepress-theme-default-plus@1.2.0 (the pinned version). bin/mvb.js resolves the site config FIRST (line 48: const siteConfig = await resolveConfig(osource, 'build', 'production'), reading multiVersionBuild.satisfies at line 60) and only AFTER that copies the repo into tmpDir and fetches tags (line 123: ['fetch', 'origin'|'--all', '--tags', '--no-filter', '--force'], plus --unshallow at line 125). mvb's own tag enumeration (getTags(options.tmpDir, options), line 148) therefore sees the post-fetch tag set, while this PR's satisfies value is computed pre-fetch from whatever git tag --list returns in the local clone.

Consequence: if the local clone has v0.42.0 but not yet v0.42.1, getLatestPatchMatching pins the range-set to ... || 0.42.0 || ...; mvb then fetches v0.42.1 into tmpDir and get-tags.js rejects it (semver.satisfies('0.42.1', '…|| 0.42.0 ||…') is false). The build ships the OLDER patch, omits the newest, and stable (get-tags.js line 51, first non-prerelease of the rsorted list) regresses to it. Before this PR the same stale clone built both, because >=0.37.0 was evaluated against the post-fetch list.

CI is safe: .github/workflows/site-build.yml checks out with fetch-depth: 0 and fetch-tags: true. The exposure is npm run docs:build, which is git submodule update --init && mvb docs — no tag fetch. The latest.length > 0 ? … : range fallback in mvb-satisfies.ts:26 only covers an empty list, never a partial one, so this degrades silently.

Distinct from the already-posted thread on gitVersionTags error handling (discussion_r3958967731), which is about git failing, not about the list being stale-but-valid.

Suggested fix: Fetch tags ahead of config evaluation in the build entrypoint rather than inside config.ts (which is also loaded by vitepress dev, where a fetch would be unwanted): change the docs:build script to git submodule update --init && git fetch --tags --force && mvb docs, and note the requirement in docs/doc-site.md next to the new paragraph.

Comment thread docs/.vitepress/config.ts
multiVersionBuild: {
satisfies: ">=0.37.0",
match: MVB_TAG_MATCH,
satisfies: getLatestPatchMatching(gitVersionTags(MVB_TAG_MATCH), ">=0.37.0"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[MEDIUM] Superseded patch dirs stop being built, but per-build /v/ listings are computed from each ref's own config and will link to them

The new range-set only decides WHICH versions mvb builds; it does not drive the version list rendered inside each build. Verified chain: docs/v/index.md calls useTags() -> the theme's client/tags.data.js, which reads globalThis.VITEPRESS_CONFIG.userConfig.themeConfig.multiVersionBuild and calls get-tags.js with it. I confirmed mvb does NOT propagate the parent's satisfies to sub-builds: bin/mvb.js sets only VPL_MVB_BASE / VPL_MVB_BUILD / VPL_MVB_DEV_VERSION / VPL_MVB_BRANCH / VPL_MVB_SOURCE (lines 234-238), and utils/normalize-mvb.js reads no env for satisfies or match. So every build's /v/ page is generated from the multiVersionBuild.satisfies of the ref that build checked out (bin/mvb.js line 212, git checkout ref).

Two consequences:

(a) The root build is the checkout of the stable tag (build: "stable"; extended.unshift of the stable alias, mvb.js line 158). Until a tag containing this PR is cut, stable is v0.43.0, whose tree still has satisfies: ">=0.37.0" — so the live /docs/v/ page will list every patch of a minor while the parent build only emits one dir per minor.

(b) Worse and durable: site-build.yml caches each versioned build under docs/.vitepress/cache/@lando/mvb, and mvb restores from cachePath and never rebuilds a cached tag (mvb.js lines 197-201). The cache key is base/versionBase/version/base hashed (line 174) — it contains NO hash of the config or of the version set, so a cached per-version /v/ page is frozen at first-build time forever. In the backport scenario the linked issue itself uses as its example (v0.42.1 tagged after v0.43.0 is stable), every already-cached build whose /v/ page listed v0.42.0 keeps linking to /docs/v/v0.42.0/, which the new build set no longer produces (each build's outDir is removed and re-copied, mvb.js line 199 / 250).

Those links then fall through to the Cloudflare 404.html; I grepped cloudflare_site/worker/ and found no redirect handling for /docs/v/ paths. Today's tag set (v0.37.0, v0.38.0, v0.39.0, v0.40.0, v0.41.0, v0.42.0, v0.43.0) has no duplicate patches, so nothing breaks yet — this is latent, triggered by the first patch release, which is exactly the case the PR exists to handle. Neither the PR body nor the one-line docs/doc-site.md change mentions the trade-off.

Suggested fix: At minimum, document the consequence in the new docs/doc-site.md paragraph: older cached builds — and the root build until the next tag is cut — still list superseded patch dirs that are no longer emitted. If dangling links are unacceptable, either add a worker-side redirect from a superseded /docs/v/vX.Y.Z/* to the built patch of that minor (the built set is derivable from the same range-set), or keep superseded patch dirs in dist and drop them only from the listing. Busting the mvb cache key when the computed range-set changes would also force stale per-version listings to regenerate.

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

Labels

ready-for-merge All reviewers approved — ready to merge risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docs multi-version build should keep only the latest patch per minor

2 participants