From 6860250c3997745343325b71b7c8c3c7f81554d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 19:01:56 +0000 Subject: [PATCH 1/2] fix(ci): pin GitPython so the release pipeline can start `RELEASE.yml` installs the release tooling unpinned: pip install poetry python-semantic-release python-semantic-release requires only `gitpython~=3.0`, and GitPython 3.1.60 removed `Actor.name_email_regex`, which python-semantic-release reads unconditionally while loading its configuration. pip therefore resolves a broken pair and the very first step of a release aborts: ::ERROR:: type object 'Actor' has no attribute 'name_email_regex' No tag, no PyPI upload, no docs deploy - the whole pipeline stops before it computes a version. Verified against the real tooling: python-semantic-release 10.6.1 with GitPython 3.1.60 fails as above; with 3.1.59 the same version resolves this repository's configuration and `semantic-release version --print` answers 2.2.1, and `--noop version` renders the full changelog. Cap GitPython rather than pinning python-semantic-release, so the release picks up the upstream fix when it lands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4 --- .github/workflows/RELEASE.yml | 10 +++++++++- tests/test_ci_workflow_config.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/RELEASE.yml b/.github/workflows/RELEASE.yml index 41d067b..b2e8b75 100644 --- a/.github/workflows/RELEASE.yml +++ b/.github/workflows/RELEASE.yml @@ -48,7 +48,15 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install poetry python-semantic-release + # GitPython 3.1.60 removed `Actor.name_email_regex`, which + # python-semantic-release reads unconditionally while loading its + # configuration. python-semantic-release only asks for + # `gitpython~=3.0`, so an unpinned install picks the broken one and + # every release aborts before it does anything: + # ::ERROR:: type object 'Actor' has no attribute 'name_email_regex' + # Cap GitPython below that release rather than pinning + # python-semantic-release, so this picks up the upstream fix. + pip install poetry python-semantic-release "gitpython<3.1.60" - name: Semantic Release id: semantic-release diff --git a/tests/test_ci_workflow_config.py b/tests/test_ci_workflow_config.py index 1b7adc9..dc7a2b1 100644 --- a/tests/test_ci_workflow_config.py +++ b/tests/test_ci_workflow_config.py @@ -83,3 +83,32 @@ def test_titles_the_project_already_uses_are_accepted(title): def test_malformed_titles_are_still_rejected(title): """The check has to keep checking something.""" assert not re.match(_pr_title_pattern(), title), f"{title!r} should not pass the PR-title check" + + +RELEASE_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "RELEASE.yml" + + +@pytest.mark.skipif(not RELEASE_WORKFLOW.exists(), reason="release workflow not present") +def test_the_release_workflow_constrains_gitpython(): + """An unpinned install of the release tooling breaks the release. + + GitPython 3.1.60 removed ``Actor.name_email_regex``, which + python-semantic-release reads while loading its configuration. + python-semantic-release asks only for ``gitpython~=3.0``, so pip resolves + the broken pair and ``semantic-release version`` aborts on the first step + of the release with:: + + ::ERROR:: type object 'Actor' has no attribute 'name_email_regex' + + Nothing else in the pipeline runs, so the release produces no tag, no + PyPI upload and no docs deploy. + """ + workflow = yaml.safe_load(RELEASE_WORKFLOW.read_text(encoding="utf-8")) + steps = workflow["jobs"]["SEMANTIC_RELEASE"]["steps"] + install = next(s for s in steps if s.get("name") == "Install dependencies")["run"] + + assert "python-semantic-release" in install + assert "gitpython" in install.lower(), ( + "The release workflow installs python-semantic-release without " + "constraining GitPython; pip will resolve a version that breaks it." + ) From 13c43887f6b6df9d7cd38122a5d438fd9fd0c93d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 21:02:09 +0000 Subject: [PATCH 2/2] fix(release): let semantic-release actually bump the sidebar version `version_variables` pointed at `.../Sidebar.jsx:FlowyML v`, but semantic-release rewrites assignments - it looks for ` = ""`. The version was printed straight into the JSX:

FlowyML v2.2.0

so there was nothing to match. semantic-release updated `pyproject.toml` and `flowyml/__init__.py`, reported success, and silently left the sidebar advertising the previous release. Verified against the real tool: on a scratch clone of `main`, `semantic-release version` bumps pyproject and `__init__.py` to 2.2.1 and leaves `FlowyML v2.2.0` in place. With the version held in a constant it bumps all three. That also turned CI red on `main` after every release: the version commit is pushed first and only the follow-up `[skip ci]` sync commit repaired the sidebar with sed, so the push in between carried an inconsistent tree that `test_the_frontend_sidebar_shows_the_current_version` rejects. Hold the version in `const VERSION` and render `{VERSION}`, point `version_variables` at the constant, and retarget the workflow's sed at the new shape so the safety net still works. Three tests: the sidebar constant tracks pyproject, no version is printed inline in the markup again, and every `version_variables` entry matches something semantic-release can actually rewrite. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01S1CErYvpggWAogJ9MDVqj4 --- .github/workflows/RELEASE.yml | 2 +- .../src/components/sidebar/Sidebar.jsx | 8 ++- pyproject.toml | 2 +- tests/test_version_consistency.py | 56 ++++++++++++++++--- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/.github/workflows/RELEASE.yml b/.github/workflows/RELEASE.yml index b2e8b75..08f64ab 100644 --- a/.github/workflows/RELEASE.yml +++ b/.github/workflows/RELEASE.yml @@ -105,7 +105,7 @@ jobs: # 3. Update UI sidebar version (full semver) SIDEBAR="flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx" if [ -f "$SIDEBAR" ]; then - sed -i "s/FlowyML v[0-9]*\.[0-9]*\.[0-9]*/FlowyML v${VERSION}/" "$SIDEBAR" + sed -i "s/^const VERSION = \"[0-9]*\.[0-9]*\.[0-9]*\";/const VERSION = \"${VERSION}\";/" "$SIDEBAR" echo "✓ Updated UI sidebar → FlowyML v${VERSION}" fi diff --git a/flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx b/flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx index 6266cd4..6cbfa76 100644 --- a/flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +++ b/flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx @@ -21,6 +21,12 @@ import { } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +// Bumped by semantic-release via [tool.semantic_release].version_variables, +// which rewrites assignments only. The entry used to point at the version +// printed inline in the JSX below, which it cannot match, so every release +// silently left the sidebar showing the previous version. +const VERSION = "2.2.0"; + const NAV_GROUPS = [ { title: 'Workspace', @@ -171,7 +177,7 @@ export function Sidebar({ collapsed, setCollapsed, isMobile = false, mobileOpen <>
-

FlowyML v2.2.0

+

FlowyML v{VERSION}

Made with ❤️ by UnicoLab diff --git a/pyproject.toml b/pyproject.toml index b1040b8..8b8412e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -203,7 +203,7 @@ markers = [ version_toml = ["pyproject.toml:tool.poetry.version"] version_variables = [ "flowyml/__init__.py:__version__", - "flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx:FlowyML v", + "flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx:VERSION", ] branch = "main" upload_to_pypi = false diff --git a/tests/test_version_consistency.py b/tests/test_version_consistency.py index 17e0f0b..20175e1 100644 --- a/tests/test_version_consistency.py +++ b/tests/test_version_consistency.py @@ -85,15 +85,57 @@ def test_server_info_reports_the_package_version(self, client): assert client.get("/api/execution/info").json()["version"] == flowyml.__version__ +SIDEBAR = REPO_ROOT / "flowyml" / "ui" / "frontend" / "src" / "components" / "sidebar" / "Sidebar.jsx" + + def test_the_frontend_sidebar_shows_the_current_version(declared_version): - """The sidebar literal is updated by semantic-release; verify it kept up.""" - sidebar = REPO_ROOT / "flowyml" / "ui" / "frontend" / "src" / "components" / "sidebar" - sidebar_file = sidebar / "Sidebar.jsx" - if not sidebar_file.exists(): + """The sidebar constant is bumped by semantic-release; verify it kept up.""" + if not SIDEBAR.exists(): pytest.skip("frontend sources not present") - text = sidebar_file.read_text(encoding="utf-8") - versions = re.findall(r"FlowyML v(\d+\.\d+\.\d+)", text) + versions = re.findall(r'^const VERSION = "(\d+\.\d+\.\d+)";', SIDEBAR.read_text(encoding="utf-8"), re.M) - assert versions, "no 'FlowyML v' string found in the sidebar" + assert versions, 'no `const VERSION = "x.y.z";` found in the sidebar' assert all(v == declared_version for v in versions), f"sidebar shows {versions}, expected {declared_version}" + + +def test_the_sidebar_does_not_print_a_version_inline(): + """An inline literal is invisible to the release automation. + + ``version_variables`` rewrites assignments; it cannot touch a number + sitting in JSX text. While the version was written straight into the + markup, every release bumped ``pyproject.toml`` and ``__init__.py`` and + left the sidebar advertising the previous version. + """ + if not SIDEBAR.exists(): + pytest.skip("frontend sources not present") + + inline = re.findall(r">FlowyML v\d+\.\d+\.\d+<", SIDEBAR.read_text(encoding="utf-8")) + + assert not inline, f"the sidebar prints a hardcoded version in its markup: {inline}; render {{VERSION}} instead" + + +def test_every_release_version_variable_can_actually_be_rewritten(declared_version): + """A ``version_variables`` entry that matches nothing fails silently. + + semantic-release looks for `` = ""``. If the + entry names something that is not assigned that way it updates nothing, + reports success, and the surface keeps showing the old version - which is + exactly what happened to the sidebar. + """ + config = toml.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + entries = config["tool"]["semantic_release"]["version_variables"] + + unmatched = [] + for entry in entries: + path_part, _, variable = entry.rpartition(":") + target = REPO_ROOT / path_part + if not target.exists(): + continue # frontend sources may be absent in a packaged checkout + pattern = rf'{re.escape(variable)}\s*[:=]\s*["\']{re.escape(declared_version)}["\']' + if not re.search(pattern, target.read_text(encoding="utf-8")): + unmatched.append(entry) + + assert not unmatched, ( + "these version_variables entries match nothing, so semantic-release " f"will silently skip them: {unmatched}" + )