scripts/check-version-consistency.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""Verify FlintTrade release version consistency across publishable surfaces."""
from __future__ import annotations
import json
import re
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PACKAGE_JSONS = [
"package.json",
"packages/core/design-system/package.json",
"packages/apps/terminal/package.json",
"packages/apps/site/package.json",
"packages/apps/desktop/package.json",
]
CARGO_MANIFESTS = [
"packages/core/ticks/Cargo.toml",
]
CURRENT_TAG_FILES = [
"readme.md",
"disclaimer.md",
"CLAUDE.md",
"PLAN.md",
"docs/ARCHITECTURE.md",
"docs/DESKTOP.md",
"docs/INVENTORY.md",
"docs/USER_GUIDE.md",
"docs/setup/macos.md",
"docs/setup/QUICKSTART.md",
"docs/setup/windows.md",
"packages/apps/site/src/app/page.tsx",
"templates/agent-context/AGENTS.md.template",
"templates/agent-context/CLAUDE.md.template",
"templates/agent-context/PLAN.md.template",
]
INSTALLER_POLICY_FILES = [
"readme.md",
"docs/DESKTOP.md",
"docs/setup/macos.md",
"docs/setup/windows.md",
"packages/apps/site/src/app/page.tsx",
"Makefile",
".github/workflows/desktop-release.yml",
]
# Subsystems that no longer exist; docs describing the CURRENT system must not
# name them (LM Studio → managed Ollama; OpenClaw → agent_backends).
RETIRED_NAMES = ["OpenClaw", "LM Studio"]
RETIRED_NAME_FILES = [
"security.md",
"readme.md",
"docs/DESKTOP.md",
"docs/USER_GUIDE.md",
"docs/ARCHITECTURE.md",
]
STALE_INSTALLER_PHRASES = [
"`.dmg`, `.app`",
"`.dmg` or `.app`",
".dmg or .app",
".dmg + .app",
".dmg/.app",
"`.msi`, `.exe`",
"`.msi` or `.exe`",
".exe or .msi",
".msi/.exe",
]
def _stale_current_pattern(version_tag: str) -> re.Pattern[str] | None:
"""Pattern flagging sibling pre-releases of the current tag (e.g. an older beta).
Derived from VERSION so a release bump cannot leave this check pinned to a
previous tag. Stable (non-prerelease) versions have no sibling family.
"""
match = re.match(r"^v(\d+\.\d+\.\d+-[A-Za-z]+)(?:\.(\d+))?$", version_tag)
if not match:
return None
family = re.escape(match.group(1))
suffix = match.group(2)
if suffix is None:
return re.compile(rf"\bv?{family}\.\d+\b")
return re.compile(rf"\bv?{family}(?!\.{re.escape(suffix)}\b)\b")
SEMVER_IDENTIFIER = r"(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
RELEASE_VERSION_PATTERN = rf"(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-{SEMVER_IDENTIFIER}(?:\.{SEMVER_IDENTIFIER})*)?"
SEMVER_WITH_PRERELEASE = re.compile(rf"^v?{RELEASE_VERSION_PATTERN}$")
def _read_json(path: str) -> object:
return json.loads((ROOT / path).read_text(encoding="utf-8"))
def _read_toml(path: str) -> dict[str, object]:
with (ROOT / path).open("rb") as handle:
return tomllib.load(handle)
def _fail(failures: list[str], label: str, actual: object, expected: object) -> None:
if actual != expected:
failures.append(f"{label}: expected {expected!r}, got {actual!r}")
def main() -> int:
failures: list[str] = []
version_tag = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
if not SEMVER_WITH_PRERELEASE.match(version_tag):
failures.append(f"VERSION is not SemVer-like: {version_tag!r}")
if not version_tag.startswith("v"):
failures.append(f"VERSION must be a git tag with leading v: {version_tag!r}")
bare_version = version_tag.removeprefix("v")
release_note = f"docs/releases/{version_tag}.md"
_fail(failures, "flint.toml project.version", _read_toml("flint.toml")["project"]["version"], version_tag)
for path in PACKAGE_JSONS:
package = _read_json(path)
assert isinstance(package, dict)
_fail(failures, f"{path} version", package.get("version"), bare_version)
desktop_package = _read_json("packages/apps/desktop/package.json")
assert isinstance(desktop_package, dict)
desktop_dependencies = desktop_package.get("devDependencies")
desktop_build = desktop_package.get("build")
assert isinstance(desktop_dependencies, dict)
assert isinstance(desktop_build, dict)
electron_version = desktop_dependencies.get("electron")
if not isinstance(electron_version, str) or re.fullmatch(r"\d+\.\d+\.\d+", electron_version) is None:
failures.append(
"packages/apps/desktop/package.json devDependencies.electron must be an exact version"
)
else:
_fail(
failures,
"packages/apps/desktop/package.json build.electronVersion",
desktop_build.get("electronVersion"),
electron_version,
)
for path in sorted((ROOT / "packages").glob("*/*/pyproject.toml")):
data = _read_toml(str(path.relative_to(ROOT)))
project = data.get("project")
if isinstance(project, dict):
_fail(failures, f"{path.relative_to(ROOT)} project.version", project.get("version"), bare_version)
for path in CARGO_MANIFESTS:
data = _read_toml(path)
package = data.get("package")
if isinstance(package, dict):
_fail(failures, f"{path} package.version", package.get("version"), bare_version)
tick_init = (ROOT / "packages/core/ticks/python/tick_engine/__init__.py").read_text(encoding="utf-8")
match = re.search(r'^__version__ = "([^"]+)"$', tick_init, re.MULTILINE)
_fail(failures, "tick_engine __version__", match.group(1) if match else None, bare_version)
if not (ROOT / release_note).exists():
failures.append(f"missing current release note: {release_note}")
site_generator = (ROOT / "packages/apps/site/scripts/generate-content.mjs").read_text(encoding="utf-8")
if release_note not in site_generator:
failures.append(f"site docs generator does not include {release_note}")
if "const releasePages = releaseDocs.map" not in site_generator:
failures.append("site docs generator must derive release sidebar pages from releaseDocs")
for path in CURRENT_TAG_FILES:
text = (ROOT / path).read_text(encoding="utf-8")
if version_tag not in text:
failures.append(f"{path} does not mention current VERSION {version_tag}")
stale_pattern = _stale_current_pattern(version_tag)
stale = stale_pattern.search(text) if stale_pattern else None
if stale:
failures.append(f"{path} still mentions stale current beta tag {stale.group(0)!r}")
for path in INSTALLER_POLICY_FILES:
text = (ROOT / path).read_text(encoding="utf-8")
for phrase in STALE_INSTALLER_PHRASES:
if phrase in text:
failures.append(f"{path} still advertises unsupported beta installer artefact phrase {phrase!r}")
# Retired subsystem names must not resurface as current-state claims in the
# policy/docs surfaces (migration code, credits, and changelog history are
# exempt by not being listed here).
for path in RETIRED_NAME_FILES:
text = (ROOT / path).read_text(encoding="utf-8")
for name in RETIRED_NAMES:
if name in text:
failures.append(f"{path} still names retired subsystem {name!r}")
if failures:
print("Version consistency check failed:")
for failure in failures:
print(f" - {failure}")
return 1
print(f"Version consistency check passed for {version_tag}")
return 0
if __name__ == "__main__":
sys.exit(main())