From 50a4ce4d2a953413b891f0f77dc22d762e2e77bc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 22:04:06 +0000 Subject: [PATCH] ci: version check fails PRs that aren't finalized Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013Z8iBZ2KQLeHeYms5c9reX --- .github/scripts/check-version.py | 92 +++++++++++++++++++++++++++++ .github/workflows/version-check.yml | 24 ++++++++ CHANGELOG.md | 11 ++++ CLAUDE.md | 2 + 4 files changed, 129 insertions(+) create mode 100644 .github/scripts/check-version.py create mode 100644 .github/workflows/version-check.yml diff --git a/.github/scripts/check-version.py b/.github/scripts/check-version.py new file mode 100644 index 0000000..f03dc45 --- /dev/null +++ b/.github/scripts/check-version.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Version check for FinLens PRs (CLAUDE.md §0 "Finalizing a PR", §17). + +Fails when: + 1. CHANGELOG.md still has a `## [Unreleased]` entry (the PR isn't finalized yet); + 2. the top CHANGELOG version isn't higher than the base branch's (no new version, + or another PR already merged with this number); + 3. any version spot from §17 doesn't show the top CHANGELOG version. + +Usage: check-version.py [BASE_CHANGELOG] +Run from the repository root. BASE_CHANGELOG is the base branch's CHANGELOG.md; +without it, check 2 is skipped. +""" +import re +import sys + +VERSION_HEADING = re.compile(r"^## \[(\d+)\.(\d+)\.(\d+)\]", re.M) +UNRELEASED = re.compile(r"^## \[Unreleased\]", re.M | re.I) + +# File, a description for the error message, and a pattern for the version spot. +# Each pattern captures the version written there. +SPOTS = [ + ("index.html", "sidebar footer", r"v(\d+\.\d+\.\d+) · FinLens"), + ("README.md", "header line", r"\*\*Version:\*\* v(\d+\.\d+\.\d+)"), + ("README.md", "footer line", r"\*FinLens v(\d+\.\d+\.\d+) —"), + ("js/views-settings.js", "General tab", r"FinLens v(\d+\.\d+\.\d+)"), + ("CLAUDE.md", "§1 Current version", r"\*\*Current version:\*\* v(\d+\.\d+\.\d+)"), +] + + +def read(path): + with open(path, encoding="utf-8") as f: + return f.read() + + +def top_version(text): + m = VERSION_HEADING.search(text) + return tuple(int(x) for x in m.groups()) if m else None + + +def fmt(v): + return ".".join(map(str, v)) + + +def main(): + errors = [] + changelog = read("CHANGELOG.md") + + if UNRELEASED.search(changelog): + errors.append( + "CHANGELOG.md still has a `## [Unreleased]` entry. This PR isn't finalized: " + "ask Claude to finalize it (merge master in, pick the version, update every " + "version spot), then merge." + ) + # The other checks would only compare the previous release's number. + print("Version check failed:") + print(f" - {errors[0]}") + return 1 + + version = top_version(changelog) + if version is None: + errors.append("CHANGELOG.md has no `## [X.Y.Z]` entry.") + else: + if len(sys.argv) > 1: + base = top_version(read(sys.argv[1])) + if base and version <= base: + errors.append( + f"The top CHANGELOG version v{fmt(version)} isn't higher than the base " + f"branch's v{fmt(base)}. Every change needs a new version (§17); if " + "another PR merged first, merge master in and finalize again." + ) + for path, where, pattern in SPOTS: + found = re.findall(pattern, read(path)) + if not found: + errors.append(f"{path} ({where}): no version string found.") + elif any(v != fmt(version) for v in found): + errors.append( + f"{path} ({where}) shows v{', v'.join(sorted(set(found)))}, " + f"expected v{fmt(version)}." + ) + + if errors: + print("Version check failed:") + for e in errors: + print(f" - {e}") + return 1 + print(f"Version check passed: v{fmt(version)}, shown in every version spot.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..7ddced0 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,24 @@ +name: Version check + +# Blocks merging a PR that isn't finalized (CLAUDE.md §0 "Finalizing a PR", §17): +# fails while CHANGELOG.md says [Unreleased], when the version isn't higher than +# master's, or when a version spot shows a different number. +on: + pull_request: + branches: [master] + types: [opened, synchronize, reopened, edited] + +permissions: + contents: read + +jobs: + version-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Get master's CHANGELOG + run: | + git fetch --depth=1 origin "${{ github.base_ref }}" + git show FETCH_HEAD:CHANGELOG.md > "$RUNNER_TEMP/base-CHANGELOG.md" + - name: Check version + run: python3 .github/scripts/check-version.py "$RUNNER_TEMP/base-CHANGELOG.md" diff --git a/CHANGELOG.md b/CHANGELOG.md index a41488d..d64dea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [Unreleased] + +### Technical +- **Version check on every PR into `master`.** A GitHub Actions check (`.github/workflows/version-check.yml`, running `.github/scripts/check-version.py`) fails when the PR isn't finalized. It catches three cases: + - CHANGELOG.md still has a `## [Unreleased]` entry; + - the top version isn't higher than `master`'s, either because nothing was bumped or because another PR merged first with the same number; + - the sidebar footer, README, Settings → General or CLAUDE.md §1 shows a different version. +- **CLAUDE.md §17** describes the check. + +--- + ## [1.10.5] — 2026-11-02 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index d895c1b..e2f50d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -783,6 +783,8 @@ Bump the patch version for bug fixes, minor version for new features, major vers Also update **Current version** in §1 of this file. Never close out a change without updating all of these. +**Version check (GitHub Actions).** `.github/workflows/version-check.yml` runs `.github/scripts/check-version.py` on every PR into `master`. It fails while CHANGELOG has a `## [Unreleased]` entry, when the top version isn't higher than `master`'s, or when any spot above (plus §1) shows a different number. A red check means the PR isn't finalized: don't merge it. If you add or move a version spot, update `SPOTS` in the script. + --- ## 18. Supported Account Types