From 3dee14a8a493ee1763de835041b9f610ff7491cf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 18:58:10 +0000 Subject: [PATCH 1/4] Publish package changelogs on the docs site under /docs/changelog/ At startup the docs app now reaches up to the repo root and pulls in the towncrier-managed changelogs (repo-root CHANGELOG.md plus each packages/*/CHANGELOG.md), serving them through the regular docgen pipeline: - /changelog/ renders the main reflex changelog; each subpackage gets /changelog//. New package changelogs appear automatically. - The reflex-enterprise changelog is read from the installed distribution (via its dist RECORD) instead of a checked-in copy, so it can never go stale; the page appears once the published wheel ships a CHANGELOG.md. - Changelog markdown is normalized with a canonical H1 title, and the on-page TOC is limited to version headings. - Pages are listed in a Changelog section of the API Reference sidebar, included in llms.txt/llms-full.txt and .md asset serving, and the footer/navbar Changelog links now point at the new page instead of GitHub releases. https://claude.ai/code/session_01ToXo8Yg1VTuuT2jgBqK6T7 --- CONTRIBUTING.md | 7 + docs/app/agent_files/_plugin.py | 2 +- docs/app/reflex_docs/changelogs.py | 109 ++++++++++++ docs/app/reflex_docs/docgen_pipeline.py | 15 ++ docs/app/reflex_docs/pages/docs/__init__.py | 50 +++++- .../reflex_docs/templates/docpage/docpage.py | 4 +- .../templates/docpage/sidebar/sidebar.py | 15 +- .../sidebar/sidebar_items/reference.py | 11 ++ docs/app/tests/test_agent_files.py | 8 + docs/app/tests/test_changelogs.py | 156 ++++++++++++++++++ docs/app/tests/test_routes.py | 12 ++ .../src/reflex_site_shared/constants.py | 2 +- 12 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 docs/app/reflex_docs/changelogs.py create mode 100644 docs/app/tests/test_changelogs.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec81045b765..552fd7f0945 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,6 +96,13 @@ following command to generate the `CHANGELOG.md` file in each subpackage. uv run towncrier build --config pyproject.toml --version v0.9.4 ``` +**Where changelogs are published:** the docs site renders every `CHANGELOG.md` +in the repo (repo root and `packages/*/`) under +[reflex.dev/docs/changelog/](https://reflex.dev/docs/changelog/). The +`reflex-enterprise` changelog is read from the installed `reflex-enterprise` +distribution at docs build time; it appears once the published wheel ships a +`CHANGELOG.md` and the docs app's lockfile picks up that version. + ## ✅ Making a PR Once you solve a current issue or improvement to Reflex, you can make a PR, and we will review the changes. diff --git a/docs/app/agent_files/_plugin.py b/docs/app/agent_files/_plugin.py index c3e3aaba506..57d118f3bce 100644 --- a/docs/app/agent_files/_plugin.py +++ b/docs/app/agent_files/_plugin.py @@ -205,7 +205,7 @@ def _section_for_path(url_path: Path) -> str: return "Skills" if path.startswith("ai/"): return "AI Builder" - return _format_title(path.split("/", maxsplit=1)[0]) + return _format_title(path.split("/", maxsplit=1)[0].removesuffix(".md")) def _ordered_sections( diff --git a/docs/app/reflex_docs/changelogs.py b/docs/app/reflex_docs/changelogs.py new file mode 100644 index 00000000000..62048c6620b --- /dev/null +++ b/docs/app/reflex_docs/changelogs.py @@ -0,0 +1,109 @@ +"""Discovery of package changelogs surfaced on the docs site. + +The monorepo packages manage their changelogs with towncrier: the main +``reflex`` changelog lives at the repo root and each subpackage ships its own +under ``packages//CHANGELOG.md``. The ``reflex-enterprise`` package is +developed in a separate repo, so its changelog is read from the installed +distribution instead (it only appears once the published wheel ships a +``CHANGELOG.md``). +""" + +from importlib.metadata import PackageNotFoundError, distribution +from pathlib import Path + +ENTERPRISE_PACKAGE = "reflex-enterprise" + + +def discover_repo_changelogs(repo_root: Path) -> dict[str, Path]: + """Find the changelogs maintained in this repo. + + Args: + repo_root: The repo checkout root (parent of the docs content tree). + + Returns: + A mapping of package name to its CHANGELOG.md path — ``reflex`` for + the repo-root changelog plus one entry per subpackage that ships one. + """ + changelogs: dict[str, Path] = {} + root_changelog = repo_root / "CHANGELOG.md" + if root_changelog.is_file(): + changelogs["reflex"] = root_changelog + for pkg_changelog in (repo_root / "packages").glob("*/CHANGELOG.md"): + changelogs[pkg_changelog.parent.name] = pkg_changelog + return changelogs + + +def find_distribution_changelog(package: str) -> Path | None: + """Locate the CHANGELOG.md shipped with an installed distribution. + + Args: + package: The distribution name (e.g. ``reflex-enterprise``). + + Returns: + The path to the installed CHANGELOG.md, or None when the distribution + is not installed or does not ship one. + """ + try: + dist = distribution(package) + except PackageNotFoundError: + return None + for file in dist.files or (): + if file.name == "CHANGELOG.md": + path = Path(str(dist.locate_file(file))) + if path.is_file(): + return path + return None + + +def discover_changelogs(repo_root: Path) -> dict[str, Path]: + """Find all package changelogs to publish on the docs site. + + Args: + repo_root: The repo checkout root. + + Returns: + A mapping of package name to CHANGELOG.md path, with ``reflex`` first + and the remaining packages in alphabetical order. + """ + changelogs = discover_repo_changelogs(repo_root) + enterprise_changelog = find_distribution_changelog(ENTERPRISE_PACKAGE) + if enterprise_changelog is not None: + changelogs[ENTERPRISE_PACKAGE] = enterprise_changelog + return { + name: changelogs[name] + for name in sorted(changelogs, key=lambda name: (name != "reflex", name)) + } + + +def changelog_page_title(package: str) -> str: + """Return the display title for a package changelog page. + + Args: + package: The package name. + + Returns: + The page title. + """ + return "Reflex Changelog" if package == "reflex" else f"{package} Changelog" + + +def normalize_changelog(source: str, title: str) -> str: + """Give changelog markdown a canonical top-level heading. + + Towncrier-generated changelogs have no top-level heading, while + Keep-a-Changelog files (e.g. reflex-enterprise) start with a generic + ``# Changelog``. Replace any existing H1 with *title* so every changelog + page renders consistently. + + Args: + source: The raw changelog markdown. + title: The canonical page title. + + Returns: + The normalized markdown. + """ + lines = source.lstrip().splitlines() + if lines and lines[0].startswith("# "): + del lines[0] + body = "\n".join(lines).strip("\n") + return f"# {title}\n\n{body}\n" diff --git a/docs/app/reflex_docs/docgen_pipeline.py b/docs/app/reflex_docs/docgen_pipeline.py index 100a1d8b731..7a737329535 100644 --- a/docs/app/reflex_docs/docgen_pipeline.py +++ b/docs/app/reflex_docs/docgen_pipeline.py @@ -869,6 +869,21 @@ def render_markdown(text: str) -> rx.Component: return transformer.transform(doc) +def render_markdown_with_toc(text: str) -> tuple[list[tuple[int, str]], rx.Component]: + """Render a plain markdown text string, also extracting its TOC headings. + + Args: + text: The markdown source. + + Returns: + A ``(toc, body)`` tuple where ``toc`` is a list of ``(level, text)`` + heading tuples and ``body`` is the rendered component. + """ + doc = parse_document(text) + toc = [(h.level, _spans_to_plaintext(h.children)) for h in doc.headings] + return toc, ReflexDocTransformer().transform(doc) + + def render_inline_markdown(text: str, class_name: str = "") -> rx.Component: """Render a short markdown string inline (links, code spans, emphasis). diff --git a/docs/app/reflex_docs/pages/docs/__init__.py b/docs/app/reflex_docs/pages/docs/__init__.py index 971ca615b7a..957f9b25e8a 100644 --- a/docs/app/reflex_docs/pages/docs/__init__.py +++ b/docs/app/reflex_docs/pages/docs/__init__.py @@ -11,7 +11,16 @@ from reflex_pyplot import pyplot as pyplot from reflex_site_shared.route import Route -from reflex_docs.docgen_pipeline import get_docgen_toc, render_docgen_document +from reflex_docs.changelogs import ( + changelog_page_title, + discover_changelogs, + normalize_changelog, +) +from reflex_docs.docgen_pipeline import ( + get_docgen_toc, + render_docgen_document, + render_markdown_with_toc, +) from reflex_docs.pages.docs.component import multi_docs from reflex_docs.pages.library_previews import components_previews_pages from reflex_docs.templates.docpage import docpage @@ -203,6 +212,26 @@ def make_docpage(route: str, title: str, doc_virtual: str, render_fn): return docpage(set_path=route, t=title)(render_fn) +CHANGELOG_VIRTUAL_PREFIX = "docs/changelog/" + + +def handle_changelog_doc(doc: str, actual_path: str, resolved: ResolvedDoc): + """Handle docs/changelog/** docs — package changelogs pulled from outside the docs tree. + + Changelog markdown ships without a meaningful top-level heading, so the + canonical page title is normalized in and the table of contents is limited + to version headings. + """ + + def comp(_actual=actual_path, _title=resolved.display_title): + source = normalize_changelog(Path(_actual).read_text(encoding="utf-8"), _title) + toc, body = render_markdown_with_toc(source) + toc = [(level, text) for level, text in toc if level <= 2] + return ((toc, source), body) + + return make_docpage(resolved.route, resolved.display_title, doc, comp) + + def handle_library_doc( doc: str, actual_path: str, @@ -240,6 +269,9 @@ def get_component_docgen(virtual_doc: str, actual_path: str, title: str): if virtual_doc.startswith("docs/library"): return handle_library_doc(virtual_doc, actual_path, title, resolved) + if virtual_doc.startswith(CHANGELOG_VIRTUAL_PREFIX): + return handle_changelog_doc(virtual_doc, actual_path, resolved) + def comp(_actual=actual_path, _virtual=virtual_doc): toc = get_docgen_toc(_actual) doc_content = Path(_actual).read_text(encoding="utf-8") @@ -253,6 +285,22 @@ def comp(_actual=actual_path, _virtual=virtual_doc): return make_docpage(resolved.route, resolved.display_title, virtual_doc, comp) +# Package changelogs live outside the docs tree — the towncrier-managed ones +# at the repo root (CHANGELOG.md and packages/*/CHANGELOG.md) and the +# reflex-enterprise one inside the installed distribution. Reach up and pull +# them in as regular docs under docs/changelog/, with the main reflex +# changelog served at the section index. +changelog_packages: dict[str, str] = {} # package name → route +for _package, _changelog_path in discover_changelogs(_docs_dir.parent).items(): + _virtual = ( + f"{CHANGELOG_VIRTUAL_PREFIX}index.md" + if _package == "reflex" + else f"{CHANGELOG_VIRTUAL_PREFIX}{_package}.md" + ) + all_docs[_virtual] = str(_changelog_path) + manual_titles[_virtual] = changelog_page_title(_package) + changelog_packages[_package] = doc_route_from_path(_virtual) + # Build doc_markdown_sources mapping for _virtual, _actual in all_docs.items(): if _virtual.endswith("-style.md"): diff --git a/docs/app/reflex_docs/templates/docpage/docpage.py b/docs/app/reflex_docs/templates/docpage/docpage.py index 9a76ecef990..f6a2d6b9f07 100644 --- a/docs/app/reflex_docs/templates/docpage/docpage.py +++ b/docs/app/reflex_docs/templates/docpage/docpage.py @@ -361,9 +361,7 @@ def docpage_footer(path: rx.Var[str]) -> rx.Component: [ footer_link("Home", "/"), footer_link("Blog", "/blog"), - footer_link( - "Changelog", "https://github.com/reflex-dev/reflex/releases" - ), + footer_link("Changelog", "/changelog/"), ], ), footer_link_flex( diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py index 53ba88f6b7f..0dd9cced424 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar.py @@ -21,7 +21,7 @@ ) from .sidebar_items.learn import backend, frontend, hosting, learn from .sidebar_items.recipes import recipes -from .sidebar_items.reference import api_reference +from .sidebar_items.reference import api_reference, changelog_items from .state import SideBarBase, SideBarItem SIDEBAR_ICON_MAP = { @@ -318,6 +318,7 @@ def append_to_items(items, flat_items): + mcp_items + skills_items + api_reference + + changelog_items + enterprise_items, flat_items, ) @@ -451,6 +452,7 @@ def sidebar_comp( html_lib_index: rx.vars.ArrayVar[list[int]], graphing_libs_index: rx.vars.ArrayVar[list[int]], api_reference_index: rx.vars.ArrayVar[list[int]], + changelog_index: rx.vars.ArrayVar[list[int]], recipes_index: rx.vars.ArrayVar[list[int]], enterprise_usage_index: rx.vars.ArrayVar[list[int]], enterprise_component_index: rx.vars.ArrayVar[list[int]], @@ -485,7 +487,7 @@ def sidebar_comp( ) is_library = url.contains("library") | url.contains("/mcp-") - is_api_reference = url.contains("api-reference") + is_api_reference = url.contains("api-reference") | url.startswith("/changelog/") is_enterprise = url.contains("enterprise") is_default_docs = ~is_library & ~is_api_reference & ~is_enterprise @@ -652,6 +654,14 @@ def sidebar_comp( api_reference_index, url, ), + create_sidebar_section( + "Changelog", + "/changelog/", + changelog_items, + changelog_index, + url, + connected_line=True, + ), class_name="m-0 p-0 flex flex-col items-start gap-8 w-full list-none list-style-none", ) enterprise_content = rx.el.ul( @@ -761,6 +771,7 @@ def sidebar(url=None, width: str = "100%") -> rx.Component: html_lib_index=calculate_index(html_lib, normalized_url), graphing_libs_index=calculate_index(graphing_libs, normalized_url), api_reference_index=calculate_index(api_reference, normalized_url), + changelog_index=calculate_index(changelog_items, normalized_url), recipes_index=calculate_index(recipes, normalized_url), enterprise_usage_index=calculate_index( enterprise_usage_items, normalized_url diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/reference.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/reference.py index 62ced3ff040..99ebe52d58c 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/reference.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/reference.py @@ -1,6 +1,16 @@ +from ..state import SideBarItem from .item import create_item +def get_sidebar_items_changelog(): + from reflex_docs.pages.docs import changelog_packages + + return [ + SideBarItem(names=package, link=route) + for package, route in changelog_packages.items() + ] + + def get_sidebar_items_api_reference(): from reflex_docs.pages.docs import api_reference, apiref @@ -24,3 +34,4 @@ def get_sidebar_items_api_reference(): api_reference = get_sidebar_items_api_reference() +changelog_items = get_sidebar_items_changelog() diff --git a/docs/app/tests/test_agent_files.py b/docs/app/tests/test_agent_files.py index 382cd2111bc..9993df7a2d8 100644 --- a/docs/app/tests/test_agent_files.py +++ b/docs/app/tests/test_agent_files.py @@ -271,6 +271,14 @@ def test_generate_dynamic_api_reference_files(monkeypatch): ) +def test_section_for_root_level_markdown_strips_extension(): + """Root-level docs (e.g. the changelog index) get a clean section name.""" + from agent_files._plugin import _section_for_path + + assert _section_for_path(Path("changelog.md")) == "Changelog" + assert _section_for_path(Path("changelog/reflex-base.md")) == "Changelog" + + def test_generate_llms_full_txt_stitches_markdown_docs(monkeypatch, tmp_path): """llms-full.txt contains full Markdown page bodies with source URLs.""" monkeypatch.setattr( diff --git a/docs/app/tests/test_changelogs.py b/docs/app/tests/test_changelogs.py new file mode 100644 index 00000000000..9296bdec55a --- /dev/null +++ b/docs/app/tests/test_changelogs.py @@ -0,0 +1,156 @@ +"""Tests for package changelog discovery and normalization.""" + +from pathlib import Path, PurePosixPath + +from reflex_docs.changelogs import ( + ENTERPRISE_PACKAGE, + changelog_page_title, + discover_changelogs, + discover_repo_changelogs, + find_distribution_changelog, + normalize_changelog, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +class _FakeDist: + """Minimal stand-in for importlib.metadata.Distribution.""" + + def __init__(self, files, root): + self._files = files + self._root = root + + @property + def files(self): + return self._files + + def locate_file(self, file): + return self._root / file + + +def test_discover_repo_changelogs(tmp_path): + """Repo discovery picks up the root changelog and one per subpackage.""" + (tmp_path / "CHANGELOG.md").write_text("## v1.0.0\n") + foo = tmp_path / "packages" / "foo" + foo.mkdir(parents=True) + (foo / "CHANGELOG.md").write_text("## v0.1.0\n") + (tmp_path / "packages" / "bar").mkdir() + + changelogs = discover_repo_changelogs(tmp_path) + + assert changelogs == { + "reflex": tmp_path / "CHANGELOG.md", + "foo": foo / "CHANGELOG.md", + } + + +def test_discover_repo_changelogs_real_repo(): + """The docs app reaches up to the actual repo root for changelogs.""" + changelogs = discover_repo_changelogs(REPO_ROOT) + + assert changelogs["reflex"] == REPO_ROOT / "CHANGELOG.md" + assert "reflex-base" in changelogs + assert all(path.is_file() for path in changelogs.values()) + + +def test_discover_changelogs_orders_reflex_first(tmp_path, monkeypatch): + """The reflex changelog sorts first; the rest are alphabetical.""" + (tmp_path / "CHANGELOG.md").write_text("## v1.0.0\n") + for pkg in ("reflex-zeta", "reflex-alpha"): + pkg_dir = tmp_path / "packages" / pkg + pkg_dir.mkdir(parents=True) + (pkg_dir / "CHANGELOG.md").write_text("## v0.1.0\n") + enterprise = tmp_path / "enterprise-changelog.md" + enterprise.write_text("# Changelog\n") + monkeypatch.setattr( + "reflex_docs.changelogs.find_distribution_changelog", + lambda package: enterprise, + ) + + changelogs = discover_changelogs(tmp_path) + + assert list(changelogs) == [ + "reflex", + "reflex-alpha", + ENTERPRISE_PACKAGE, + "reflex-zeta", + ] + assert changelogs[ENTERPRISE_PACKAGE] == enterprise + + +def test_discover_changelogs_skips_missing_enterprise(tmp_path, monkeypatch): + """No enterprise entry when the installed distribution has no changelog.""" + (tmp_path / "CHANGELOG.md").write_text("## v1.0.0\n") + monkeypatch.setattr( + "reflex_docs.changelogs.find_distribution_changelog", + lambda package: None, + ) + + assert ENTERPRISE_PACKAGE not in discover_changelogs(tmp_path) + + +def test_find_distribution_changelog_not_installed(): + assert find_distribution_changelog("definitely-not-a-real-package-xyz") is None + + +def test_find_distribution_changelog_found(tmp_path, monkeypatch): + """The changelog is located through the distribution's file records.""" + changelog = tmp_path / "reflex_enterprise" / "CHANGELOG.md" + changelog.parent.mkdir() + changelog.write_text("# Changelog\n") + files = [ + PurePosixPath("reflex_enterprise/__init__.py"), + PurePosixPath("reflex_enterprise/CHANGELOG.md"), + ] + monkeypatch.setattr( + "reflex_docs.changelogs.distribution", + lambda package: _FakeDist(files, tmp_path), + ) + + assert find_distribution_changelog(ENTERPRISE_PACKAGE) == changelog + + +def test_find_distribution_changelog_no_file_records(monkeypatch): + """Distributions without file records (files is None) are skipped.""" + monkeypatch.setattr( + "reflex_docs.changelogs.distribution", + lambda package: _FakeDist(None, Path("/nonexistent")), + ) + + assert find_distribution_changelog(ENTERPRISE_PACKAGE) is None + + +def test_find_distribution_changelog_stale_record(tmp_path, monkeypatch): + """A recorded changelog that is missing on disk is ignored.""" + files = [PurePosixPath("reflex_enterprise/CHANGELOG.md")] + monkeypatch.setattr( + "reflex_docs.changelogs.distribution", + lambda package: _FakeDist(files, tmp_path), + ) + + assert find_distribution_changelog(ENTERPRISE_PACKAGE) is None + + +def test_changelog_page_title(): + assert changelog_page_title("reflex") == "Reflex Changelog" + assert changelog_page_title("reflex-base") == "reflex-base Changelog" + + +def test_normalize_changelog_towncrier(): + """Towncrier changelogs (no H1) get the canonical title prepended.""" + source = "## v0.9.4 (2026-06-03)\n\n### Features\n\n- A change.\n" + + assert normalize_changelog(source, "Reflex Changelog") == ( + "# Reflex Changelog\n\n## v0.9.4 (2026-06-03)\n\n### Features\n\n- A change.\n" + ) + + +def test_normalize_changelog_replaces_existing_h1(): + """Keep-a-Changelog files have their generic H1 replaced.""" + source = "# Changelog\n\n## [0.9.0] - 2026-06-09\n\n### Changed\n\n- A change.\n" + + assert normalize_changelog(source, "reflex-enterprise Changelog") == ( + "# reflex-enterprise Changelog\n\n" + "## [0.9.0] - 2026-06-09\n\n### Changed\n\n- A change.\n" + ) diff --git a/docs/app/tests/test_routes.py b/docs/app/tests/test_routes.py index f764d3adc0c..1ca1692ae45 100644 --- a/docs/app/tests/test_routes.py +++ b/docs/app/tests/test_routes.py @@ -28,6 +28,18 @@ def test_unique_routes(routes_fixture): print(f"Test passed. All {len(paths)} routes are unique.") +def test_changelog_routes(routes_fixture): + """Every discovered package changelog is served under /changelog/.""" + from reflex_docs.pages.docs import changelog_packages + + paths = {route.path for route in routes_fixture if route.path} + + assert changelog_packages["reflex"] == "/changelog/" + assert "/changelog/reflex-base/" in paths + for changelog_route in changelog_packages.values(): + assert changelog_route in paths + + def test_ai_builder_routes_use_ai_prefix(routes_fixture): paths = {route.path for route in routes_fixture if route.path} diff --git a/packages/reflex-site-shared/src/reflex_site_shared/constants.py b/packages/reflex-site-shared/src/reflex_site_shared/constants.py index 1c2379b1b86..1c8588b654f 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/constants.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/constants.py @@ -2,7 +2,7 @@ import os -CHANGELOG_URL = "https://github.com/reflex-dev/reflex/releases" +CHANGELOG_URL = "https://reflex.dev/docs/changelog/" CONTRIBUTING_URL = "https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md" DISCUSSIONS_URL = "https://github.com/orgs/reflex-dev/discussions" GITHUB_STARS = 28000 From ff4d8d32c6e324e65775f94d6033714a0b94bae1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 19:30:35 +0000 Subject: [PATCH 2/4] Prefer the shallowest CHANGELOG.md record in installed distributions A wheel can vendor third-party changelogs deeper in its tree (e.g. bundled frontend assets); picking the first RECORD match could surface the wrong file. Sort candidates by path depth so the package-level changelog wins. https://claude.ai/code/session_01ToXo8Yg1VTuuT2jgBqK6T7 --- docs/app/reflex_docs/changelogs.py | 12 +++++++----- docs/app/tests/test_changelogs.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/app/reflex_docs/changelogs.py b/docs/app/reflex_docs/changelogs.py index 62048c6620b..a06339ab2e0 100644 --- a/docs/app/reflex_docs/changelogs.py +++ b/docs/app/reflex_docs/changelogs.py @@ -47,11 +47,13 @@ def find_distribution_changelog(package: str) -> Path | None: dist = distribution(package) except PackageNotFoundError: return None - for file in dist.files or (): - if file.name == "CHANGELOG.md": - path = Path(str(dist.locate_file(file))) - if path.is_file(): - return path + candidates = [file for file in dist.files or () if file.name == "CHANGELOG.md"] + # Prefer the shallowest record — a wheel may also vendor third-party + # changelogs deeper in its tree. + for file in sorted(candidates, key=lambda file: len(file.parts)): + path = Path(str(dist.locate_file(file))) + if path.is_file(): + return path return None diff --git a/docs/app/tests/test_changelogs.py b/docs/app/tests/test_changelogs.py index 9296bdec55a..00a68ae3d89 100644 --- a/docs/app/tests/test_changelogs.py +++ b/docs/app/tests/test_changelogs.py @@ -111,6 +111,25 @@ def test_find_distribution_changelog_found(tmp_path, monkeypatch): assert find_distribution_changelog(ENTERPRISE_PACKAGE) == changelog +def test_find_distribution_changelog_prefers_shallowest_record(tmp_path, monkeypatch): + """Vendored changelogs deeper in the tree don't shadow the package one.""" + vendored = tmp_path / "reflex_enterprise" / "vendor" / "lib" / "CHANGELOG.md" + vendored.parent.mkdir(parents=True) + vendored.write_text("# Vendored\n") + changelog = tmp_path / "reflex_enterprise" / "CHANGELOG.md" + changelog.write_text("# Changelog\n") + files = [ + PurePosixPath("reflex_enterprise/vendor/lib/CHANGELOG.md"), + PurePosixPath("reflex_enterprise/CHANGELOG.md"), + ] + monkeypatch.setattr( + "reflex_docs.changelogs.distribution", + lambda package: _FakeDist(files, tmp_path), + ) + + assert find_distribution_changelog(ENTERPRISE_PACKAGE) == changelog + + def test_find_distribution_changelog_no_file_records(monkeypatch): """Distributions without file records (files is None) are skipped.""" monkeypatch.setattr( From 47bf341a00330cd6f7b93815fc6776bf9d8aeca2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 20:14:27 +0000 Subject: [PATCH 3/4] Bump reflex-enterprise floor to 0.9.0.post1 for the bundled changelog The 0.9.0.post1 wheel ships reflex_enterprise/CHANGELOG.md, so the docs site now renders the enterprise changelog at /changelog/reflex-enterprise/. Exempt reflex-enterprise from the exclude-newer cutoff like the other internal lockstep packages. https://claude.ai/code/session_01ToXo8Yg1VTuuT2jgBqK6T7 --- docs/app/pyproject.toml | 2 +- pyproject.toml | 1 + uv.lock | 9 +++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/app/pyproject.toml b/docs/app/pyproject.toml index 0214e7fdea3..1a36dec57b5 100644 --- a/docs/app/pyproject.toml +++ b/docs/app/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "python-frontmatter", "reflex", "reflex-docgen", - "reflex-enterprise", + "reflex-enterprise>=0.9.0.post1", "reflex-hosting-cli", "reflex-pyplot", "reflex-integrations-docs", diff --git a/pyproject.toml b/pyproject.toml index 525ef5332f5..d4a8ff77b4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -392,6 +392,7 @@ reflex-components-radix = false reflex-components-react-player = false reflex-components-recharts = false reflex-components-sonner = false +reflex-enterprise = false reflex-hosting-cli = false [tool.uv.sources] diff --git a/uv.lock b/uv.lock index 1980545ad95..789b7db8b7e 100644 --- a/uv.lock +++ b/uv.lock @@ -23,6 +23,7 @@ reflex-components-sonner = false hatch-reflex-pyi = false reflex-components-gridjs = false reflex = false +reflex-enterprise = false reflex-components-code = false reflex-components-recharts = false reflex-components-core = false @@ -3841,7 +3842,7 @@ requires-dist = [ { name = "reflex", editable = "." }, { name = "reflex-components-internal", editable = "packages/reflex-components-internal" }, { name = "reflex-docgen", editable = "packages/reflex-docgen" }, - { name = "reflex-enterprise" }, + { name = "reflex-enterprise", specifier = ">=0.9.0.post1" }, { name = "reflex-hosting-cli", editable = "packages/reflex-hosting-cli" }, { name = "reflex-integrations-docs", editable = "packages/integrations-docs" }, { name = "reflex-pyplot" }, @@ -3868,7 +3869,7 @@ source = { editable = "docs/package" } [[package]] name = "reflex-enterprise" -version = "0.7.0.post1" +version = "0.9.0.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiproxy" }, @@ -3877,9 +3878,9 @@ dependencies = [ { name = "psutil" }, { name = "reflex", extra = ["db"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/5e/386cda3b7bd54aba75267f52fa32f93766bf9973382a6dec384b83779493/reflex_enterprise-0.7.0.post1.tar.gz", hash = "sha256:cc23343bd0d8b2d5ed45a0c551ca0b833c735125a855a74cae608ef39be94ccf", size = 396669, upload-time = "2026-04-21T17:36:43.127Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/51/77a3a97de2466fd66d0e78ea644e4e7793604afe01fd81c8f2383a88e22e/reflex_enterprise-0.9.0.post1.tar.gz", hash = "sha256:dc40120fecff5c5ba6d1471b7c7cbb56db8596305a83a34142cc810bde3cd5f1", size = 431732, upload-time = "2026-06-09T20:10:58.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/02/b45cdf9a9322ae0c1f31ca51736f70b2508e05339149153c6e902d609651/reflex_enterprise-0.7.0.post1-py3-none-any.whl", hash = "sha256:bc510d05a4bc8abe26ab4d02e1f06e82af4d255ea29209d620c8d3d662c8a203", size = 219320, upload-time = "2026-04-21T17:36:44.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/55/01983923203174b8e3adf1cb702f62785bef30735c2d81f3c8e67ae60639/reflex_enterprise-0.9.0.post1-py3-none-any.whl", hash = "sha256:0bc2d2d92ba79a7620d4e1fcf6bc609c791f3eba5c6c051daf65fb41921d1634", size = 233834, upload-time = "2026-06-09T20:10:56.82Z" }, ] [[package]] From c8aad6c606fe7039de14a1967656c03871f7120b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 20:20:20 +0000 Subject: [PATCH 4/4] Set APP_HARNESS_FLAG for CI jobs that run the docs app in prod mode reflex-enterprise 0.9.0 restricts `reflex run --env prod` to paid tiers, which broke the reflex-docs integration jobs after the version bump. The gate explicitly exempts reflex's own integration tests via the app harness flag, so set it for every job that boots the docs app in prod. https://claude.ai/code/session_01ToXo8Yg1VTuuT2jgBqK6T7 --- .github/workflows/check_outdated_dependencies.yml | 4 ++++ .github/workflows/integration_tests.yml | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/check_outdated_dependencies.yml b/.github/workflows/check_outdated_dependencies.yml index 74524e56d43..80e90f617fe 100644 --- a/.github/workflows/check_outdated_dependencies.yml +++ b/.github/workflows/check_outdated_dependencies.yml @@ -42,6 +42,10 @@ jobs: frontend: runs-on: ubuntu-latest + env: + # reflex-enterprise restricts `reflex run --env prod` to paid tiers but + # exempts reflex's own integration tests via the app harness flag. + APP_HARNESS_FLAG: "true" steps: - name: Checkout code diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index a3a086de83b..1f218240cb5 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -112,6 +112,9 @@ jobs: env: REFLEX_WEB_WINDOWS_OVERRIDE: "1" + # reflex-enterprise restricts `reflex run --env prod` to paid tiers but + # exempts reflex's own integration tests via the app harness flag. + APP_HARNESS_FLAG: "true" runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -220,6 +223,10 @@ jobs: matrix: # Note: py311 version chosen due to available arm64 darwin builds. python-version: ["3.11", "3.12"] + env: + # reflex-enterprise restricts `reflex run --env prod` to paid tiers but + # exempts reflex's own integration tests via the app harness flag. + APP_HARNESS_FLAG: "true" runs-on: macos-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2