From 1d1c63a4c40ad45a3800c0fd4241e5af14c39bc4 Mon Sep 17 00:00:00 2001 From: "yonghao.fyh" Date: Mon, 2 Nov 2026 17:09:21 +0800 Subject: [PATCH 1/6] docs: add version switcher and development-version warning banner --- .github/workflows/build_docs.yaml | 5 + .github/workflows/release_candidate.yaml | 1 + docs/source/_static/versions.json | 12 +- docs/source/conf.py | 27 ++++- docs/source/user_guide/catalog.rst | 2 +- scripts/releasing/bump_version.py | 103 +++++++++++++++--- scripts/releasing/tests/test_release_tools.py | 63 +++++++++-- 7 files changed, 179 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build_docs.yaml b/.github/workflows/build_docs.yaml index 5b2a76a5e..8836faa59 100644 --- a/.github/workflows/build_docs.yaml +++ b/.github/workflows/build_docs.yaml @@ -18,6 +18,11 @@ name: "Publish Docs" on: + pull_request: + paths: + - 'apidoc/**' + - 'docs/**' + - 'include/**' push: branches: - main diff --git a/.github/workflows/release_candidate.yaml b/.github/workflows/release_candidate.yaml index a1f8dafa1..f5ec2a573 100644 --- a/.github/workflows/release_candidate.yaml +++ b/.github/workflows/release_candidate.yaml @@ -20,6 +20,7 @@ on: pull_request: paths: - ".github/workflows/release_candidate.yaml" + - ".github/workflows/build_docs.yaml" - ".github/.rat-excludes" - "CMakeLists.txt" - "LICENSE" diff --git a/docs/source/_static/versions.json b/docs/source/_static/versions.json index 46c7d200e..50867e2df 100644 --- a/docs/source/_static/versions.json +++ b/docs/source/_static/versions.json @@ -1,7 +1,13 @@ [ { - "name": "0.4.0", - "version": "0.4.0", - "url": "https://paimon.apache.org/docs/cpp/" + "name": "main (development)", + "version": "main", + "url": "https://paimon.apache.org/docs/cpp/main/" + }, + { + "name": "0.3 (stable)", + "version": "", + "url": "https://paimon.apache.org/docs/cpp/", + "preferred": true } ] diff --git a/docs/source/conf.py b/docs/source/conf.py index 1a4a6444c..6cd422cc3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -116,13 +116,38 @@ version = "0.4.0" +# Documentation channel, set by the paimon-website build pipeline: +# "main" (default) for the main-branch docs published under main/, "stable" +# for the latest release published at docs/cpp/, or "X.Y" for an archived +# release series published under docs/cpp/X.Y/. +docs_channel = os.environ.get("PAIMON_DOCS_VERSION", "main") +switcher_version = "" if docs_channel == "stable" else docs_channel + +# The release string feeds the version warning banner: main-branch builds +# carry a ".dev" suffix so the banner calls them an unstable development +# version, as the Apache Arrow documentation does. Release builds (stable +# and archived series) keep the plain version, and the stable build matches +# the preferred entry, so no banner is shown at docs/cpp/. +release = f"{version}.dev" if docs_channel == "main" else version + html_theme_options = { "show_toc_level": 2, "show_nav_level": 2, "use_edit_page_button": True, "header_links_before_dropdown": 4, "navbar_align": "left", - "navbar_end": ["theme-switcher", "navbar-icon-links"], + "navbar_end": ["version-switcher", "theme-switcher", "navbar-icon-links"], + "switcher": { + # The main build is republished daily by the paimon-website pipeline, + # so its copy of the version list stays the freshest one on the site. + # PAIMON_DOCS_VERSIONS_URL allows local previews to point at their + # own copy instead of the live site. + "json_url": os.environ.get( + "PAIMON_DOCS_VERSIONS_URL", + "https://paimon.apache.org/docs/cpp/main/_static/versions.json", + ), + "version_match": switcher_version, + }, "icon_links": [ { "name": "GitHub", diff --git a/docs/source/user_guide/catalog.rst b/docs/source/user_guide/catalog.rst index 88e1eecd8..0592a7b74 100644 --- a/docs/source/user_guide/catalog.rst +++ b/docs/source/user_guide/catalog.rst @@ -418,7 +418,7 @@ the commit endpoint of ``tbl$branch_dev``, not the one of ``tbl``. A body sent to the bare table's URL publishes the branch's snapshot on the main branch. Authenticating with credentials of your own -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Credentials that a catalog does not issue — from a token service of your own, or from a store Paimon has no client for — reach a file system through a ``CredentialProvider`` that you consult yourself. Implement it together with a diff --git a/scripts/releasing/bump_version.py b/scripts/releasing/bump_version.py index f2c6df80a..1bba2155f 100755 --- a/scripts/releasing/bump_version.py +++ b/scripts/releasing/bump_version.py @@ -27,6 +27,34 @@ VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$") +SERIES_PATTERN = re.compile(r"^\d+\.\d+$") + +DOCS_URL = "https://paimon.apache.org/docs/cpp/" +MAIN_ENTRY_VERSION = "main" + + +def series(version: str) -> str: + """The major.minor documentation series a version belongs to.""" + return ".".join(version.split(".")[:2]) + + +def stable_entry(series_name: str) -> Dict[str, object]: + """Dropdown entry for the latest release published at the site root.""" + return { + "name": f"{series_name} (stable)", + "version": "", + "url": DOCS_URL, + "preferred": True, + } + + +def series_entry(series_name: str) -> Dict[str, object]: + """Dropdown entry for an archived release series under /.""" + return { + "name": series_name, + "version": series_name, + "url": f"{DOCS_URL}{series_name}/", + } class VersionError(RuntimeError): @@ -55,6 +83,54 @@ def load_versions(path: Path) -> List[Dict[str, object]]: return value +def check_version_list(versions: List[Dict[str, object]]) -> None: + """Validate the dev/stable/archived layout of versions.json.""" + if len(versions) < 2: + raise VersionError( + "versions.json must contain a dev entry followed by a stable entry" + ) + dev = versions[0] + if ( + dev.get("version") != MAIN_ENTRY_VERSION + or dev.get("name") != "main (development)" + or dev.get("url") != f"{DOCS_URL}main/" + ): + raise VersionError( + "the first versions.json entry must be the main-branch docs: " + f"name 'main (development)', version {MAIN_ENTRY_VERSION!r}, " + f"url {DOCS_URL}main/" + ) + stable = versions[1] + name = stable.get("name") + match = ( + re.fullmatch(r"(\d+\.\d+) \(stable\)", name) + if isinstance(name, str) + else None + ) + if ( + match is None + or stable.get("version") != "" + or stable.get("url") != DOCS_URL + or stable.get("preferred") is not True + ): + raise VersionError( + "the second versions.json entry must be the stable docs: name " + "'X.Y (stable)', version '', url the site root, preferred true" + ) + for item in versions[2:]: + item_name = item.get("name") + if ( + not isinstance(item_name, str) + or SERIES_PATTERN.fullmatch(item_name) is None + or item.get("version") != item_name + or item.get("url") != f"{DOCS_URL}{item_name}/" + ): + raise VersionError( + "archived versions.json entries must use name == version == " + f"X.Y with url {DOCS_URL}X.Y/, found {item!r}" + ) + + def checked_files(root: Path, expected: str) -> Dict[Path, str]: cmake_path = root / "CMakeLists.txt" docs_path = root / "docs/source/conf.py" @@ -70,23 +146,13 @@ def checked_files(root: Path, expected: str) -> Dict[Path, str]: docs_matches = re.findall( r'^version = "(\d+\.\d+\.\d+)"$', docs, re.MULTILINE ) - json_matches = [ - item - for item in versions - if item.get("name") == expected and item.get("version") == expected - ] - if cmake_matches != [expected]: raise VersionError(f"CMake version is {cmake_matches}, expected [{expected!r}]") if docs_matches != [expected]: raise VersionError( f"documentation version is {docs_matches}, expected [{expected!r}]" ) - if len(json_matches) != 1: - raise VersionError( - "versions.json must contain exactly one entry whose name and " - f"version are both {expected}" - ) + check_version_list(versions) return { cmake_path: cmake, docs_path: docs, @@ -114,13 +180,14 @@ def updated_files(root: Path, current: str, new: str) -> Dict[Path, str]: ) versions = load_versions(versions_path) - matches = [item for item in versions if item.get("version") == current] - if len(matches) != 1: - raise VersionError( - f"versions.json contains {len(matches)} entries for {current}" - ) - matches[0]["name"] = new - matches[0]["version"] = new + if series(current) != series(new): + # A new documentation series ships with this release: the released + # version becomes the stable docs at docs/cpp/ and the previous + # stable docs move to their series directory. The main entry is + # fixed. Patch release cycles keep the version list unchanged. + previous_series = versions[1]["name"].split(" ")[0] + versions[1] = stable_entry(series(current)) + versions.insert(2, series_entry(previous_series)) files[versions_path] = json.dumps(versions, indent=4) + "\n" return files diff --git a/scripts/releasing/tests/test_release_tools.py b/scripts/releasing/tests/test_release_tools.py index 827ddfeac..6a91bfdbb 100644 --- a/scripts/releasing/tests/test_release_tools.py +++ b/scripts/releasing/tests/test_release_tools.py @@ -105,8 +105,11 @@ def create_verifier_archive(self, directory: Path) -> Path: ), "docs/source/conf.py": b'version = "1.2.3"\n', "docs/source/_static/versions.json": ( - b'[{"name": "1.2.3", "version": "1.2.3", ' - b'"url": "https://paimon.apache.org/docs/cpp/"}]\n' + b'[{"name": "main (development)", "version": "main", ' + b'"url": "https://paimon.apache.org/docs/cpp/main/"}, ' + b'{"name": "1.1 (stable)", "version": "", ' + b'"url": "https://paimon.apache.org/docs/cpp/", ' + b'"preferred": true}]\n' ), ".github/.rat-excludes": b"", "scripts/releasing/create_source_release.sh": b"#!/usr/bin/env bash\n", @@ -356,8 +359,11 @@ def test_verifier_uses_keys_file_for_unsigned_artifact_tag(self) -> None: "NOTICE": "Apache Paimon\n", "docs/source/conf.py": 'version = "1.2.3"\n', "docs/source/_static/versions.json": ( - '[{"name": "1.2.3", "version": "1.2.3", ' - '"url": "https://paimon.apache.org/docs/cpp/"}]\n' + '[{"name": "main (development)", "version": "main", ' + '"url": "https://paimon.apache.org/docs/cpp/main/"}, ' + '{"name": "1.1 (stable)", "version": "", ' + '"url": "https://paimon.apache.org/docs/cpp/", ' + '"preferred": true}]\n' ), ".github/.rat-excludes": "", } @@ -610,10 +616,16 @@ def create_version_tree(self, root: Path) -> None: json.dumps( [ { - "name": "1.2.3", - "version": "1.2.3", + "name": "main (development)", + "version": "main", + "url": "https://paimon.apache.org/docs/cpp/main/", + }, + { + "name": "1.1 (stable)", + "version": "", "url": "https://paimon.apache.org/docs/cpp/", - } + "preferred": True, + }, ], indent=4, ) @@ -626,11 +638,40 @@ def test_version_tool_checks_and_updates_all_metadata(self) -> None: root = Path(temp) self.create_version_tree(root) self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.2.3") - self.run_tool(VERSION_TOOL, "--root", str(root), "1.2.3", "1.2.4") - self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.2.4") - self.assertIn("VERSION 1.2.4", (root / "CMakeLists.txt").read_text()) + self.run_tool(VERSION_TOOL, "--root", str(root), "1.2.3", "1.3.0") + self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.3.0") + self.assertIn("VERSION 1.3.0", (root / "CMakeLists.txt").read_text()) self.assertIn( - 'version = "1.2.4"', (root / "docs/source/conf.py").read_text() + 'version = "1.3.0"', (root / "docs/source/conf.py").read_text() + ) + versions_path = root / "docs/source/_static/versions.json" + versions = json.loads(versions_path.read_text(encoding="utf-8")) + self.assertEqual( + versions, + [ + { + "name": "main (development)", + "version": "main", + "url": "https://paimon.apache.org/docs/cpp/main/", + }, + { + "name": "1.2 (stable)", + "version": "", + "url": "https://paimon.apache.org/docs/cpp/", + "preferred": True, + }, + { + "name": "1.1", + "version": "1.1", + "url": "https://paimon.apache.org/docs/cpp/1.1/", + }, + ], + ) + # A patch release cycle keeps the version list unchanged. + self.run_tool(VERSION_TOOL, "--root", str(root), "1.3.0", "1.3.1") + self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.3.1") + self.assertEqual( + json.loads(versions_path.read_text(encoding="utf-8")), versions ) def test_version_tool_rejects_inconsistent_metadata(self) -> None: From ad6e556280a206189118aeccc386a0cd2cab072b Mon Sep 17 00:00:00 2001 From: "yonghao.fyh" Date: Tue, 3 Nov 2026 09:43:50 +0800 Subject: [PATCH 2/6] fix(docs): correct stable banner and release version promotion --- docs/source/conf.py | 6 +-- scripts/releasing/README.md | 7 ++++ scripts/releasing/bump_version.py | 13 +++---- scripts/releasing/tests/test_release_tools.py | 37 ++++++++++++++++++- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6cd422cc3..f33b4a129 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -126,8 +126,8 @@ # The release string feeds the version warning banner: main-branch builds # carry a ".dev" suffix so the banner calls them an unstable development # version, as the Apache Arrow documentation does. Release builds (stable -# and archived series) keep the plain version, and the stable build matches -# the preferred entry, so no banner is shown at docs/cpp/. +# and archived series) keep the plain version. The stable channel disables +# the banner because its empty switcher identifier is not a release number. release = f"{version}.dev" if docs_channel == "main" else version html_theme_options = { @@ -158,7 +158,7 @@ "logo": { "text": "Apache Paimon C++", }, - "show_version_warning_banner": True, + "show_version_warning_banner": docs_channel != "stable", } html_context = { diff --git a/scripts/releasing/README.md b/scripts/releasing/README.md index a251bed86..4f057011c 100644 --- a/scripts/releasing/README.md +++ b/scripts/releasing/README.md @@ -60,6 +60,13 @@ scripts/releasing/bump_version.py 0.2.0 0.3.0 scripts/releasing/bump_version.py --check 0.3.0 ``` +The target version's major.minor series becomes the stable documentation +entry. If it differs from the previous stable series, that previous series +is moved to an archived entry. Further patch releases in the same stable +series leave the documentation list unchanged. Use this command for release +preparation; it also promotes documentation and is not a development-only +version bump. + ### Signing key setup and security Complete signing-key setup well before creating the first release candidate: diff --git a/scripts/releasing/bump_version.py b/scripts/releasing/bump_version.py index 1bba2155f..c0490eb2f 100755 --- a/scripts/releasing/bump_version.py +++ b/scripts/releasing/bump_version.py @@ -180,13 +180,12 @@ def updated_files(root: Path, current: str, new: str) -> Dict[Path, str]: ) versions = load_versions(versions_path) - if series(current) != series(new): - # A new documentation series ships with this release: the released - # version becomes the stable docs at docs/cpp/ and the previous - # stable docs move to their series directory. The main entry is - # fixed. Patch release cycles keep the version list unchanged. - previous_series = versions[1]["name"].split(" ")[0] - versions[1] = stable_entry(series(current)) + previous_series = versions[1]["name"].split(" ")[0] + if previous_series != series(new): + # Release preparation promotes the target series, even when the + # project version already belongs to that series. Archive the + # previous stable series only when it changes. + versions[1] = stable_entry(series(new)) versions.insert(2, series_entry(previous_series)) files[versions_path] = json.dumps(versions, indent=4) + "\n" return files diff --git a/scripts/releasing/tests/test_release_tools.py b/scripts/releasing/tests/test_release_tools.py index 6a91bfdbb..ec02307c3 100644 --- a/scripts/releasing/tests/test_release_tools.py +++ b/scripts/releasing/tests/test_release_tools.py @@ -21,12 +21,14 @@ import json import os import re +import runpy import shutil import subprocess import sys import tarfile import tempfile import unittest +from unittest import mock from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -655,7 +657,7 @@ def test_version_tool_checks_and_updates_all_metadata(self) -> None: "url": "https://paimon.apache.org/docs/cpp/main/", }, { - "name": "1.2 (stable)", + "name": "1.3 (stable)", "version": "", "url": "https://paimon.apache.org/docs/cpp/", "preferred": True, @@ -674,6 +676,39 @@ def test_version_tool_checks_and_updates_all_metadata(self) -> None: json.loads(versions_path.read_text(encoding="utf-8")), versions ) + def test_version_tool_promotes_series_on_patch_release(self) -> None: + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + self.create_version_tree(root) + self.run_tool(VERSION_TOOL, "--root", str(root), "1.2.3", "1.2.4") + self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.2.4") + versions = json.loads( + (root / "docs/source/_static/versions.json").read_text(encoding="utf-8") + ) + self.assertEqual(versions[1]["name"], "1.2 (stable)") + self.assertEqual(versions[2]["version"], "1.1") + self.assertEqual(len(versions), 3) + + def test_docs_channels_configure_version_warning(self) -> None: + for channel, match, warning in [ + (None, "main", True), + ("main", "main", True), + ("stable", "", False), + ("0.3", "0.3", True), + ]: + with self.subTest(channel=channel), mock.patch.dict(os.environ): + os.environ.pop("PAIMON_DOCS_VERSION", None) + if channel is not None: + os.environ["PAIMON_DOCS_VERSION"] = channel + config = runpy.run_path(str(SOURCE_ROOT / "docs/source/conf.py")) + options = config["html_theme_options"] + self.assertEqual(options["switcher"]["version_match"], match) + self.assertEqual(options["show_version_warning_banner"], warning) + expected_release = config["version"] + if match == "main": + expected_release += ".dev" + self.assertEqual(config["release"], expected_release) + def test_version_tool_rejects_inconsistent_metadata(self) -> None: with tempfile.TemporaryDirectory() as temp: root = Path(temp) From fdbc04487ceabdce1d9678a9b019276e1e3d28c5 Mon Sep 17 00:00:00 2001 From: "yonghao.fyh" Date: Tue, 3 Nov 2026 13:24:38 +0800 Subject: [PATCH 3/6] fix(docs): show full release versions and resolve build warnings --- docs/source/_static/versions.json | 2 +- docs/source/api/global_index.rst | 4 -- docs/source/conf.py | 2 +- docs/source/user_guide/file_index.rst | 2 +- scripts/releasing/README.md | 12 +++-- scripts/releasing/bump_version.py | 45 ++++++++++--------- scripts/releasing/tests/test_release_tools.py | 26 ++++++++--- 7 files changed, 53 insertions(+), 40 deletions(-) diff --git a/docs/source/_static/versions.json b/docs/source/_static/versions.json index 50867e2df..0c4814e17 100644 --- a/docs/source/_static/versions.json +++ b/docs/source/_static/versions.json @@ -5,7 +5,7 @@ "url": "https://paimon.apache.org/docs/cpp/main/" }, { - "name": "0.3 (stable)", + "name": "0.3.0 (stable)", "version": "", "url": "https://paimon.apache.org/docs/cpp/", "preferred": true diff --git a/docs/source/api/global_index.rst b/docs/source/api/global_index.rst index 5420df989..5aada67f0 100644 --- a/docs/source/api/global_index.rst +++ b/docs/source/api/global_index.rst @@ -72,10 +72,6 @@ Interface :members: :undoc-members: -.. doxygenclass:: paimon::FullTextScan::Plan - :members: - :undoc-members: - .. doxygenclass:: paimon::FullTextRead :members: :undoc-members: diff --git a/docs/source/conf.py b/docs/source/conf.py index f33b4a129..d98bb9c1e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -114,7 +114,7 @@ # The master toctree document. master_doc = "index" -version = "0.4.0" +version = "0.3.0" # Documentation channel, set by the paimon-website build pipeline: # "main" (default) for the main-branch docs published under main/, "stable" diff --git a/docs/source/user_guide/file_index.rst b/docs/source/user_guide/file_index.rst index a402cc6e8..f3dcbad7d 100644 --- a/docs/source/user_guide/file_index.rst +++ b/docs/source/user_guide/file_index.rst @@ -19,7 +19,7 @@ File Index ========== Full-text predicates and root TopK ---------------------------------- +---------------------------------- ``PredicateBuilder::FullTextMatch`` creates a ``LeafPredicate`` for a full-text condition without TopK truncation. It holds a shared ``FullTextMatch`` request, diff --git a/scripts/releasing/README.md b/scripts/releasing/README.md index 4f057011c..21778b0db 100644 --- a/scripts/releasing/README.md +++ b/scripts/releasing/README.md @@ -60,10 +60,14 @@ scripts/releasing/bump_version.py 0.2.0 0.3.0 scripts/releasing/bump_version.py --check 0.3.0 ``` -The target version's major.minor series becomes the stable documentation -entry. If it differs from the previous stable series, that previous series -is moved to an archived entry. Further patch releases in the same stable -series leave the documentation list unchanged. Use this command for release +The target version becomes the stable documentation label (for example, +`0.3.0 (stable)`). If its major.minor series differs from the previous stable +series, the previous release is moved to an archived entry. Archived labels +also show the full version, while their identifiers and URLs retain the +major.minor series. Patch releases update the stable label without adding +an archive. The documentation version may track the latest stable release +while the CMake version advances; release preparation updates both to the +target version. Use this command for release preparation; it also promotes documentation and is not a development-only version bump. diff --git a/scripts/releasing/bump_version.py b/scripts/releasing/bump_version.py index c0490eb2f..c0522667c 100755 --- a/scripts/releasing/bump_version.py +++ b/scripts/releasing/bump_version.py @@ -27,7 +27,6 @@ VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$") -SERIES_PATTERN = re.compile(r"^\d+\.\d+$") DOCS_URL = "https://paimon.apache.org/docs/cpp/" MAIN_ENTRY_VERSION = "main" @@ -38,22 +37,22 @@ def series(version: str) -> str: return ".".join(version.split(".")[:2]) -def stable_entry(series_name: str) -> Dict[str, object]: +def stable_entry(version: str) -> Dict[str, object]: """Dropdown entry for the latest release published at the site root.""" return { - "name": f"{series_name} (stable)", + "name": f"{version} (stable)", "version": "", "url": DOCS_URL, "preferred": True, } -def series_entry(series_name: str) -> Dict[str, object]: +def series_entry(version: str) -> Dict[str, object]: """Dropdown entry for an archived release series under /.""" return { - "name": series_name, - "version": series_name, - "url": f"{DOCS_URL}{series_name}/", + "name": version, + "version": series(version), + "url": f"{DOCS_URL}{series(version)}/", } @@ -103,7 +102,7 @@ def check_version_list(versions: List[Dict[str, object]]) -> None: stable = versions[1] name = stable.get("name") match = ( - re.fullmatch(r"(\d+\.\d+) \(stable\)", name) + re.fullmatch(r"(\d+\.\d+\.\d+) \(stable\)", name) if isinstance(name, str) else None ) @@ -115,19 +114,19 @@ def check_version_list(versions: List[Dict[str, object]]) -> None: ): raise VersionError( "the second versions.json entry must be the stable docs: name " - "'X.Y (stable)', version '', url the site root, preferred true" + "'X.Y.Z (stable)', version '', url the site root, preferred true" ) for item in versions[2:]: item_name = item.get("name") if ( not isinstance(item_name, str) - or SERIES_PATTERN.fullmatch(item_name) is None - or item.get("version") != item_name - or item.get("url") != f"{DOCS_URL}{item_name}/" + or VERSION_PATTERN.fullmatch(item_name) is None + or item.get("version") != series(item_name) + or item.get("url") != f"{DOCS_URL}{series(item_name)}/" ): raise VersionError( - "archived versions.json entries must use name == version == " - f"X.Y with url {DOCS_URL}X.Y/, found {item!r}" + "archived versions.json entries must use name X.Y.Z, " + f"version X.Y and url {DOCS_URL}X.Y/, found {item!r}" ) @@ -148,11 +147,13 @@ def checked_files(root: Path, expected: str) -> Dict[Path, str]: ) if cmake_matches != [expected]: raise VersionError(f"CMake version is {cmake_matches}, expected [{expected!r}]") - if docs_matches != [expected]: + check_version_list(versions) + stable_version = versions[1]["name"].split(" ")[0] + if docs_matches not in ([expected], [stable_version]): raise VersionError( - f"documentation version is {docs_matches}, expected [{expected!r}]" + f"documentation version is {docs_matches}, expected project version " + f"{expected!r} or stable documentation version {stable_version!r}" ) - check_version_list(versions) return { cmake_path: cmake, docs_path: docs, @@ -174,19 +175,19 @@ def updated_files(root: Path, current: str, new: str) -> Dict[Path, str]: ) files[docs_path] = replace_once( files[docs_path], - rf'^version = "{re.escape(current)}"$', + r'^version = "\d+\.\d+\.\d+"$', f'version = "{new}"', f"documentation version {current}", ) versions = load_versions(versions_path) - previous_series = versions[1]["name"].split(" ")[0] - if previous_series != series(new): + previous_version = versions[1]["name"].split(" ")[0] + if series(previous_version) != series(new): # Release preparation promotes the target series, even when the # project version already belongs to that series. Archive the # previous stable series only when it changes. - versions[1] = stable_entry(series(new)) - versions.insert(2, series_entry(previous_series)) + versions.insert(2, series_entry(previous_version)) + versions[1] = stable_entry(new) files[versions_path] = json.dumps(versions, indent=4) + "\n" return files diff --git a/scripts/releasing/tests/test_release_tools.py b/scripts/releasing/tests/test_release_tools.py index ec02307c3..38a70e6b5 100644 --- a/scripts/releasing/tests/test_release_tools.py +++ b/scripts/releasing/tests/test_release_tools.py @@ -109,7 +109,7 @@ def create_verifier_archive(self, directory: Path) -> Path: "docs/source/_static/versions.json": ( b'[{"name": "main (development)", "version": "main", ' b'"url": "https://paimon.apache.org/docs/cpp/main/"}, ' - b'{"name": "1.1 (stable)", "version": "", ' + b'{"name": "1.1.0 (stable)", "version": "", ' b'"url": "https://paimon.apache.org/docs/cpp/", ' b'"preferred": true}]\n' ), @@ -363,7 +363,7 @@ def test_verifier_uses_keys_file_for_unsigned_artifact_tag(self) -> None: "docs/source/_static/versions.json": ( '[{"name": "main (development)", "version": "main", ' '"url": "https://paimon.apache.org/docs/cpp/main/"}, ' - '{"name": "1.1 (stable)", "version": "", ' + '{"name": "1.1.0 (stable)", "version": "", ' '"url": "https://paimon.apache.org/docs/cpp/", ' '"preferred": true}]\n' ), @@ -623,7 +623,7 @@ def create_version_tree(self, root: Path) -> None: "url": "https://paimon.apache.org/docs/cpp/main/", }, { - "name": "1.1 (stable)", + "name": "1.1.0 (stable)", "version": "", "url": "https://paimon.apache.org/docs/cpp/", "preferred": True, @@ -657,21 +657,22 @@ def test_version_tool_checks_and_updates_all_metadata(self) -> None: "url": "https://paimon.apache.org/docs/cpp/main/", }, { - "name": "1.3 (stable)", + "name": "1.3.0 (stable)", "version": "", "url": "https://paimon.apache.org/docs/cpp/", "preferred": True, }, { - "name": "1.1", + "name": "1.1.0", "version": "1.1", "url": "https://paimon.apache.org/docs/cpp/1.1/", }, ], ) - # A patch release cycle keeps the version list unchanged. + # A patch release updates the label without adding an archive. self.run_tool(VERSION_TOOL, "--root", str(root), "1.3.0", "1.3.1") self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.3.1") + versions[1]["name"] = "1.3.1 (stable)" self.assertEqual( json.loads(versions_path.read_text(encoding="utf-8")), versions ) @@ -685,10 +686,21 @@ def test_version_tool_promotes_series_on_patch_release(self) -> None: versions = json.loads( (root / "docs/source/_static/versions.json").read_text(encoding="utf-8") ) - self.assertEqual(versions[1]["name"], "1.2 (stable)") + self.assertEqual(versions[1]["name"], "1.2.4 (stable)") self.assertEqual(versions[2]["version"], "1.1") self.assertEqual(len(versions), 3) + def test_version_tool_updates_docs_from_previous_stable_release(self) -> None: + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + self.create_version_tree(root) + docs_path = root / "docs/source/conf.py" + docs_path.write_text('version = "1.1.0"\n', encoding="utf-8") + self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.2.3") + self.run_tool(VERSION_TOOL, "--root", str(root), "1.2.3", "1.3.0") + self.run_tool(VERSION_TOOL, "--root", str(root), "--check", "1.3.0") + self.assertIn('version = "1.3.0"', docs_path.read_text(encoding="utf-8")) + def test_docs_channels_configure_version_warning(self) -> None: for channel, match, warning in [ (None, "main", True), From 165fc36e0d22d36ff4f4937533ff992d80577d59 Mon Sep 17 00:00:00 2001 From: "yonghao.fyh" Date: Tue, 3 Nov 2026 13:30:20 +0800 Subject: [PATCH 4/6] fix(release): reuse metadata validation when creating source archives --- scripts/releasing/create_source_release.sh | 29 +++++----- scripts/releasing/tests/test_release_tools.py | 53 +++++++++++++++++++ 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/scripts/releasing/create_source_release.sh b/scripts/releasing/create_source_release.sh index 67e4d0932..43aa56e51 100755 --- a/scripts/releasing/create_source_release.sh +++ b/scripts/releasing/create_source_release.sh @@ -146,20 +146,18 @@ for required_file in CMakeLists.txt LICENSE NOTICE; do fail "${required_file} is missing from ${GIT_REF}" done -CMAKE_VERSION=$( - git -C "${SOURCE_ROOT}" show "${GIT_REF}:CMakeLists.txt" | - awk '$1 == "VERSION" && $2 ~ /^[0-9]+\.[0-9]+\.[0-9]+$/ { print $2; exit }' -) -[[ "${CMAKE_VERSION}" == "${RELEASE_VERSION}" ]] || - fail "CMake version ${CMAKE_VERSION:-} does not match ${RELEASE_VERSION}" - -DOCS_VERSION=$( - git -C "${SOURCE_ROOT}" show "${GIT_REF}:docs/source/conf.py" | - sed -n 's/^version = "\([^"]*\)"$/\1/p' | - head -n 1 -) -[[ "${DOCS_VERSION}" == "${RELEASE_VERSION}" ]] || - fail "documentation version ${DOCS_VERSION:-} does not match ${RELEASE_VERSION}" +command -v python3 >/dev/null 2>&1 || fail "python3 is required" +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "${TEMP_DIR}"' EXIT + +# Check the immutable ref with the same metadata rules as archive verification. +mkdir -p "${TEMP_DIR}/metadata/docs/source/_static" +for metadata_file in CMakeLists.txt docs/source/conf.py docs/source/_static/versions.json; do + git -C "${SOURCE_ROOT}" show "${GIT_REF}:${metadata_file}" \ + >"${TEMP_DIR}/metadata/${metadata_file}" +done +python3 "${SCRIPT_DIR}/bump_version.py" \ + --root "${TEMP_DIR}/metadata" --check "${RELEASE_VERSION}" GZIP_BIN=$(find_gnu_gzip) @@ -174,9 +172,6 @@ for suffix in "" ".sha512" ".asc"; do fail "refusing to overwrite ${OUTPUT_DIR}/${ARTIFACT_NAME}${suffix}" done -TEMP_DIR=$(mktemp -d) -trap 'rm -rf "${TEMP_DIR}"' EXIT - echo "Creating ${ARTIFACT_NAME} from ${GIT_REF}..." git -C "${SOURCE_ROOT}" -c tar.umask=0022 archive \ --format=tar \ diff --git a/scripts/releasing/tests/test_release_tools.py b/scripts/releasing/tests/test_release_tools.py index 38a70e6b5..9cd82b05e 100644 --- a/scripts/releasing/tests/test_release_tools.py +++ b/scripts/releasing/tests/test_release_tools.py @@ -534,6 +534,59 @@ def test_verifier_uses_keys_file_for_unsigned_artifact_tag(self) -> None: ) self.assertIn("Git ref reproducibility: valid", result.stdout) + def test_source_creator_checks_documentation_from_git_ref(self) -> None: + for docs_version, expected_returncode in [("1.1.0", 0), ("9.9.9", 1)]: + with self.subTest(docs_version=docs_version), tempfile.TemporaryDirectory() as temp: + directory = Path(temp) + root = directory / "source" + self.create_version_tree(root) + docs_path = root / "docs/source/conf.py" + docs_path.write_text(f'version = "{docs_version}"\n', encoding="utf-8") + releasing_dir = root / "scripts/releasing" + releasing_dir.mkdir(parents=True) + for script in ("create_source_release.sh", "bump_version.py"): + shutil.copy2(RELEASING_DIR / script, releasing_dir / script) + for name in ("LICENSE", "NOTICE"): + (root / name).write_text("Apache Paimon\n", encoding="utf-8") + subprocess.run(["git", "init", "-q", str(root)], check=True) + subprocess.run(["git", "-C", str(root), "add", "."], check=True) + subprocess.run( + [ + "git", "-C", str(root), + "-c", "user.name=Release Test", + "-c", "user.email=release-test@example.com", + "-c", "commit.gpgsign=false", + "-c", "core.hooksPath=/dev/null", + "commit", "-qm", "Release metadata", + ], + check=True, + ) + # The working tree must not affect validation of the committed metadata. + docs_path.write_text('version = "1.2.3"\n', encoding="utf-8") + result = subprocess.run( + [ + "bash", str(releasing_dir / "create_source_release.sh"), + "--version", "1.2.3", "--git-ref", "HEAD", + "--output-dir", str(directory / "release"), + ], + universal_newlines=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + self.assertEqual( + result.returncode, expected_returncode, msg=result.stdout + result.stderr + ) + artifact = directory / "release/apache-paimon-cpp-1.2.3-src.tgz" + if expected_returncode == 0: + with tarfile.open(artifact) as archive: + member = archive.extractfile("paimon-cpp-1.2.3/docs/source/conf.py") + self.assertIsNotNone(member) + self.assertIn(docs_version, member.read().decode("utf-8")) + else: + self.assertIn("documentation version", result.stderr) + self.assertFalse(artifact.exists()) + def test_source_creator_rejects_non_gnu_gzip(self) -> None: with tempfile.TemporaryDirectory() as temp: directory = Path(temp) From 4f90d8d34354b0dd0efb67489d47fa72d1e9b42d Mon Sep 17 00:00:00 2001 From: "yonghao.fyh" Date: Tue, 3 Nov 2026 14:03:11 +0800 Subject: [PATCH 5/6] docs: simplify development version label to main --- docs/source/_static/versions.json | 2 +- scripts/releasing/bump_version.py | 5 +++-- scripts/releasing/tests/test_release_tools.py | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/source/_static/versions.json b/docs/source/_static/versions.json index 0c4814e17..eabb550e1 100644 --- a/docs/source/_static/versions.json +++ b/docs/source/_static/versions.json @@ -1,6 +1,6 @@ [ { - "name": "main (development)", + "name": "main", "version": "main", "url": "https://paimon.apache.org/docs/cpp/main/" }, diff --git a/scripts/releasing/bump_version.py b/scripts/releasing/bump_version.py index c0522667c..92735d728 100755 --- a/scripts/releasing/bump_version.py +++ b/scripts/releasing/bump_version.py @@ -91,12 +91,13 @@ def check_version_list(versions: List[Dict[str, object]]) -> None: dev = versions[0] if ( dev.get("version") != MAIN_ENTRY_VERSION - or dev.get("name") != "main (development)" + # Older Git refs retain the previous display label. + or dev.get("name") not in ("main", "main (development)") or dev.get("url") != f"{DOCS_URL}main/" ): raise VersionError( "the first versions.json entry must be the main-branch docs: " - f"name 'main (development)', version {MAIN_ENTRY_VERSION!r}, " + f"name 'main', version {MAIN_ENTRY_VERSION!r}, " f"url {DOCS_URL}main/" ) stable = versions[1] diff --git a/scripts/releasing/tests/test_release_tools.py b/scripts/releasing/tests/test_release_tools.py index 9cd82b05e..2e3089230 100644 --- a/scripts/releasing/tests/test_release_tools.py +++ b/scripts/releasing/tests/test_release_tools.py @@ -107,7 +107,7 @@ def create_verifier_archive(self, directory: Path) -> Path: ), "docs/source/conf.py": b'version = "1.2.3"\n', "docs/source/_static/versions.json": ( - b'[{"name": "main (development)", "version": "main", ' + b'[{"name": "main", "version": "main", ' b'"url": "https://paimon.apache.org/docs/cpp/main/"}, ' b'{"name": "1.1.0 (stable)", "version": "", ' b'"url": "https://paimon.apache.org/docs/cpp/", ' @@ -361,7 +361,7 @@ def test_verifier_uses_keys_file_for_unsigned_artifact_tag(self) -> None: "NOTICE": "Apache Paimon\n", "docs/source/conf.py": 'version = "1.2.3"\n', "docs/source/_static/versions.json": ( - '[{"name": "main (development)", "version": "main", ' + '[{"name": "main", "version": "main", ' '"url": "https://paimon.apache.org/docs/cpp/main/"}, ' '{"name": "1.1.0 (stable)", "version": "", ' '"url": "https://paimon.apache.org/docs/cpp/", ' @@ -671,7 +671,7 @@ def create_version_tree(self, root: Path) -> None: json.dumps( [ { - "name": "main (development)", + "name": "main", "version": "main", "url": "https://paimon.apache.org/docs/cpp/main/", }, @@ -705,7 +705,7 @@ def test_version_tool_checks_and_updates_all_metadata(self) -> None: versions, [ { - "name": "main (development)", + "name": "main", "version": "main", "url": "https://paimon.apache.org/docs/cpp/main/", }, From 6077302ccde009ac0769c0e0da97137f827d9b47 Mon Sep 17 00:00:00 2001 From: "yonghao.fyh" Date: Tue, 3 Nov 2026 16:00:07 +0800 Subject: [PATCH 6/6] docs: set development documentation version to 0.4.0 --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index d98bb9c1e..f33b4a129 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -114,7 +114,7 @@ # The master toctree document. master_doc = "index" -version = "0.3.0" +version = "0.4.0" # Documentation channel, set by the paimon-website build pipeline: # "main" (default) for the main-branch docs published under main/, "stable"