Files
2026-07-13 12:40:00 +08:00

184 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""bump-version.py — Atomically bump every parity-locked file to a new version.
The repo intentionally keeps per-IDE frontmatter and per-language prose divergent
across SKILL.md variants, but the `metadata.version` field (plus the version
field in plugin.json, marketplace.json, CITATION.cff) must always agree across
the parity set. Past releases (v2.34.1, v2.36.0, v2.36.2, v2.36.3) repeatedly
hit "missed one variant" regressions because the bump was done by hand.
Run before tagging a release:
python scripts/bump-version.py 2.37.0
python scripts/bump-version.py 2.37.0 --dry-run
Files touched (parity set, 19 entries):
skills/planning-with-files/SKILL.md (canonical)
skills/planning-with-files-{ar,de,es,zh,zht}/SKILL.md
.{codebuddy,codex,cursor,factory,hermes,mastracode,opencode,pi,kiro}/skills/planning-with-files/SKILL.md
clawhub-upload/SKILL.md
.claude-plugin/plugin.json
.claude-plugin/marketplace.json
CITATION.cff
Files intentionally left behind (do not bump automatically):
.continue/skills/planning-with-files/SKILL.md
.gemini/skills/planning-with-files/SKILL.md
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
# (path relative to repo root, kind)
# kind: "skill_md", "plugin_json", "marketplace_json", "citation_cff"
PARITY_FILES = [
("skills/planning-with-files/SKILL.md", "skill_md"),
("skills/planning-with-files-ar/SKILL.md", "skill_md"),
("skills/planning-with-files-de/SKILL.md", "skill_md"),
("skills/planning-with-files-es/SKILL.md", "skill_md"),
("skills/planning-with-files-zh/SKILL.md", "skill_md"),
("skills/planning-with-files-zht/SKILL.md", "skill_md"),
(".codebuddy/skills/planning-with-files/SKILL.md", "skill_md"),
(".codex/skills/planning-with-files/SKILL.md", "skill_md"),
(".cursor/skills/planning-with-files/SKILL.md", "skill_md"),
(".factory/skills/planning-with-files/SKILL.md", "skill_md"),
(".hermes/skills/planning-with-files/SKILL.md", "skill_md"),
(".mastracode/skills/planning-with-files/SKILL.md", "skill_md"),
(".opencode/skills/planning-with-files/SKILL.md", "skill_md"),
("clawhub-upload/SKILL.md", "skill_md"),
(".claude-plugin/plugin.json", "plugin_json"),
(".claude-plugin/marketplace.json", "marketplace_json"),
("CITATION.cff", "citation_cff"),
]
# Files left behind on purpose. Documented to make the omission explicit.
LAGGING_FILES = [
".continue/skills/planning-with-files/SKILL.md",
".gemini/skills/planning-with-files/SKILL.md",
".pi/skills/planning-with-files/SKILL.md", # npm scheme (1.0.x)
".kiro/skills/planning-with-files/SKILL.md", # kiro scheme (2.32.0-kiro)
]
VERSION_RE = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.\-]+)?$")
def bump_skill_md(path: Path, new: str, *, dry_run: bool) -> tuple[str | None, str | None]:
text = path.read_text(encoding="utf-8")
pattern = re.compile(r'(version:\s*")([^"]+)(")')
match = pattern.search(text)
if not match:
return None, f"no version field found in {path}"
old = match.group(2)
if old == new:
return old, None
new_text = pattern.sub(rf'\g<1>{new}\g<3>', text, count=1)
if not dry_run:
path.write_text(new_text, encoding="utf-8")
return old, None
def bump_plugin_or_marketplace(path: Path, new: str, *, dry_run: bool) -> tuple[str | None, str | None]:
"""Bump the version field in plugin.json or marketplace.json.
Both files keep the canonical plugin version in a single "version": "..."
pair, but marketplace.json nests it under plugins[0]. We rewrite every
occurrence in the file (there is only one for our manifests), which keeps
the script tolerant of either layout.
"""
text = path.read_text(encoding="utf-8")
pattern = re.compile(r'("version"\s*:\s*")([^"]+)(")')
matches = list(pattern.finditer(text))
if not matches:
return None, f"no version field found in {path}"
old = matches[0].group(2)
if all(m.group(2) == new for m in matches):
return old, None
new_text = pattern.sub(rf'\g<1>{new}\g<3>', text)
if not dry_run:
path.write_text(new_text, encoding="utf-8")
return old, None
def bump_citation_cff(path: Path, new: str, *, dry_run: bool) -> tuple[str | None, str | None]:
text = path.read_text(encoding="utf-8")
# CITATION.cff carries `version: "2.36.3"` (quoted) or `version: 2.36.3`.
pattern = re.compile(r'^(version:\s*)"?([^"\s#]+)"?(\s*(?:#.*)?)$', re.MULTILINE)
match = pattern.search(text)
if not match:
return None, f"no version field found in {path}"
old = match.group(2)
if old == new:
return old, None
new_text = pattern.sub(rf'\g<1>"{new}"\g<3>', text, count=1)
if not dry_run:
path.write_text(new_text, encoding="utf-8")
return old, None
HANDLERS = {
"skill_md": bump_skill_md,
"plugin_json": bump_plugin_or_marketplace,
"marketplace_json": bump_plugin_or_marketplace,
"citation_cff": bump_citation_cff,
}
def parse_args(argv=None):
p = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
p.add_argument("new_version", help="Target semver, e.g. 2.37.0")
p.add_argument("--dry-run", action="store_true", help="Preview without writing")
return p.parse_args(argv)
def main(argv=None) -> int:
args = parse_args(argv)
new = args.new_version.lstrip("v")
if not VERSION_RE.match(new):
print(f"Error: '{new}' is not a valid semver string.", file=sys.stderr)
return 2
print(f"{'[DRY RUN] ' if args.dry_run else ''}Bumping parity set to {new}\n")
failures: list[str] = []
changed = 0
skipped = 0
for rel, kind in PARITY_FILES:
path = REPO_ROOT / rel
if not path.exists():
failures.append(f"missing: {rel}")
print(f" MISSING: {rel}")
continue
handler = HANDLERS[kind]
old, err = handler(path, new, dry_run=args.dry_run)
if err:
failures.append(err)
print(f" ERROR: {rel} ({err})")
continue
if old == new:
skipped += 1
print(f" unchanged: {rel} (already {new})")
else:
changed += 1
print(f" bumped: {rel} {old} -> {new}")
print(f"\nChanged: {changed} Unchanged: {skipped} Errors: {len(failures)}")
print(f"Lagging (not auto-bumped): {len(LAGGING_FILES)}")
for rel in LAGGING_FILES:
print(f" -> {rel}")
if failures:
print("\nFAILED:", *failures, sep="\n ", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())