From e27bbf85de3d7db0dae5233ba4156bdabb537798 Mon Sep 17 00:00:00 2001 From: FreekHeijting Date: Mon, 6 Jul 2026 11:54:15 +0200 Subject: [PATCH] ADD VERSION-bestand fallback als laagste manifest-precedentie detect-release.py detecteert nu een plain-text VERSION-bestand op de repo-root (een enkele semver-regel) als laagste precedentie NA plugin.json, pyproject.toml en package.json. Zo kan elke release-waardige repo een version hebben op de voor z'n type conventionele plek, ook repos zonder taal-manifest (docs, templates, scaffolds). De fail-loud-bij-geen-enkele-bron blijft intact: een bestaande package.json zonder version-veld faalt nog steeds luid en valt niet stil terug op VERSION. Het VERSION-bestand wordt strikt gevalideerd op een enkele semver-regel. docs/release-workflow.md documenteert de VERSION-optie en de precedentie. Co-Authored-By: Claude Opus 4.8 --- docs/release-workflow.md | 16 ++++++++++++++-- scripts/detect-release.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/release-workflow.md b/docs/release-workflow.md index bb93de2..4050e6f 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -38,16 +38,28 @@ detecteert het in deze volgorde (meest specifiek eerst): 2. `pyproject.toml` — Frappe- en Python-apps (statische `[project].version` of een dynamische version via flit/hatch, uit `/__init__.py`) 3. `package.json` — skill-packages, templates, JS-apps +4. `VERSION` — taal-agnostische fallback: een plain-text bestand op de repo-root + met precies een semver-regel (bijvoorbeeld `0.1.0`). Bedoeld voor repos zonder + taal-manifest, zoals docs-, template- en scaffold-repos, zodat ook die een + conventionele version-plek hebben. Laagste precedentie: hij wordt alleen + gelezen als geen van de drie manifesten hierboven bestaat. Ontbreekt een manifest, of is de version niet leesbaar, dan faalt de workflow -luid. Er is geen stille fallback naar een ander manifest. +luid. Er is geen stille fallback naar een ander manifest. Let op: dit geldt ook +voor `VERSION`. Bestaat er wel een `package.json` maar zonder `version`-veld, dan +faalt de workflow luid op die `package.json`; hij valt niet stilletjes terug op +een `VERSION`-bestand. Elk repo heeft precies een version-bron. ## Adoptie in een repo 1. Kopieer `docs/release.yml.template` uit deze repo naar `.github/workflows/release.yml` in je eigen repo. 2. Commit dat via de normale branch- en PR-flow (`GIT_PRACTICES.md`). -3. Zorg dat de repo een `CHANGELOG.md` in Keep a Changelog-formaat heeft, met +3. Zorg dat de repo een version-bron heeft op de voor z'n type conventionele + plek: een taal-manifest (`.claude-plugin/plugin.json`, `pyproject.toml` of + `package.json`), of, voor een repo zonder taal-manifest, een `VERSION`-bestand + op de repo-root met een enkele semver-regel (bijvoorbeeld `0.1.0`). +4. Zorg dat de repo een `CHANGELOG.md` in Keep a Changelog-formaat heeft, met per version een `## []`-sectie. Een bump zonder bijbehorende CHANGELOG-sectie laat de workflow luid falen. diff --git a/scripts/detect-release.py b/scripts/detect-release.py index c3feba0..ad8945b 100644 --- a/scripts/detect-release.py +++ b/scripts/detect-release.py @@ -18,6 +18,9 @@ 1. .claude-plugin/plugin.json (Claude Code plugins) 2. pyproject.toml (Frappe / Python apps) 3. package.json (skill-packages / templates / JS apps) + 4. VERSION (language-agnostic plain-text fallback: + docs / scaffolds / templates without any + language manifest; one semver line) Notes are NOT required to be present here: on a genuine bump the caller workflow enforces that a CHANGELOG section exists (notes_found=true). Keeping that policy @@ -47,6 +50,27 @@ def read_json_version(path: Path) -> str: return version.strip() +# A plain-text VERSION file must hold exactly one semver line. This is stricter +# than the JSON/TOML readers on purpose: a schemaless text file has no field to +# key on, so the semver shape is the only guard against a stray "VERSION" file +# (e.g. licensing text) being mistaken for a release manifest. Fail loud, no +# guessing. +_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$") + + +def read_version_file(path: Path) -> str: + lines = [ln.strip() for ln in path.read_text(encoding="utf-8").splitlines()] + nonblank = [ln for ln in lines if ln] + if not nonblank: + die(f"{path} is empty; expected a single semver line (e.g. 0.1.0)") + if len(nonblank) > 1: + die(f"{path} has multiple non-blank lines; expected a single semver line") + version = nonblank[0] + if not _SEMVER_RE.match(version): + die(f"{path} content '{version}' is not a valid semver (e.g. 0.1.0)") + return version + + def read_module_version(module_file: Path) -> str | None: if not module_file.is_file(): return None @@ -111,9 +135,13 @@ def detect(repo: Path) -> tuple[str, str]: if package.is_file(): return "package.json", read_json_version(package) + version_file = repo / "VERSION" + if version_file.is_file(): + return "VERSION", read_version_file(version_file) + die( f"no release manifest found in '{repo}'. Expected one of: " - ".claude-plugin/plugin.json, pyproject.toml, package.json" + ".claude-plugin/plugin.json, pyproject.toml, package.json, VERSION" )