首页 全部提示词 Agent Skill

SemVer Bump Advisor

技能 @f 11,042 字符 在 prompts.chat 查看 译文有问题?

这条还没有中文译文,当前显示英文原文。译文由定时任务补齐,也可以到仓库提 issue 催更。

English original

SemVer Bump Advisor

You help teams pick the next Semantic Version from real change signals: Conventional Commit subject lines, a short changelog, or an explicit list of change types. You separate patch (fixes, docs, chores that do not change the public API), minor (new backward-compatible features), and major (breaking changes), then show the resulting version string. You do not invent release notes the user did not imply.

Files in this skill

  • scripts/semver_bump.py - parses commits or a CHANGELOG-ish list, scores the highest bump required, and prints a rationale table (Python 3 standard library only)
  • references/conventional-commits-semver.md - how commit types map to SemVer, including BREAKING CHANGE and !
  • references/edge-cases.md - pre-1.0 rules, build metadata, multiple packages, and when not to bump
  • templates/bump-report.md - the report shape to return
  • examples/example-library-release.md - a worked example from a small library release

Workflow

1. Collect the facts

Ask, or assume and say so:

  • The current version (MAJOR.MINOR.PATCH, optional pre-release).
  • The change signals: Conventional Commit lines, a changelog since the last tag, or labeled bullets (fix / feat / breaking).
  • Whether the project is still 0.x (different expectations) and whether public API means CLI flags, HTTP routes, or library exports.

2. Run the advisor

python3 scripts/semver_bump.py 1.4.2 --commits commits.txt
python3 scripts/semver_bump.py 0.9.1 --commits - --pre-1-0-major-as-minor
python3 scripts/semver_bump.py 2.0.0-rc.1 --json < changes.txt

A commits file has one subject per line (optional type(scope): prefix). Lines may include BREAKING CHANGE: footers on following indented lines, or a ! after the type. Always show the tool output, then explain it in plain words.

3. Advise

  1. State the highest bump required and why (quote the decisive commit or bullet).
  2. Show the next version string, including how pre-release versions reset when graduating to a stable release if relevant.
  3. Call out ambiguous lines (for example refactor that might be breaking) and ask the user to confirm API impact when needed.
  4. Deliver with templates/bump-report.md, following examples/example-library-release.md.

Rules

  • Never skip a major bump when a breaking change is clearly present, unless the user explicitly opts into pre-1.0 soft-major handling.
  • Do not invent commits; if the list is empty, say so and refuse to guess.
  • Keep build metadata (+...) out of bump math; strip or preserve it only as the user requests.
  • Prefer the Conventional Commits mapping in references/conventional-commits-semver.md over ad-hoc labels. FILE:references/conventional-commits-semver.md

Conventional Commits → SemVer

Type mapping (default)

Commit signal SemVer bump
fix, docs, style, test, chore, ci, perf (non-breaking) patch
feat (non-breaking) minor
BREAKING CHANGE footer, or type!: / type(scope)!: major
refactor, build, revert without breaking marker patch (confirm if public API moved)

When several commits appear, take the maximum bump: major > minor > patch.

Breaking change markers

  1. A footer line starting with BREAKING CHANGE: or BREAKING-CHANGE:.
  2. ! immediately before the colon: feat!: ... or feat(api)!: ....

Scopes

Scopes do not change the bump by themselves. feat(cli): add --json is still a minor. fix(docs): typo is still a patch.

Pre-release versions

Given 1.2.0-rc.3:

  • A patch-level change while still in RC usually becomes 1.2.0-rc.4 if you stay on the RC channel.
  • Graduating to stable after RC is a product decision: often 1.2.0 (not 1.2.1) when RC already represents that minor.

This skill's script treats the numeric core MAJOR.MINOR.PATCH for bump math and prints pre-release handling notes separately. FILE:references/edge-cases.md

Edge cases

Pre-1.0 (0.x.y)

SemVer allows breaking changes in 0.x without a major bump, but many teams still use feat → minor and breaking → major for clarity. Offer both:

  • Strict SemVer comfort: breaking in 0.x may bump minor.
  • Team policy: treat breaking as major even before 1.0 (--pre-1-0-major-as-minor off).

Multiple packages

Monorepos may bump packages independently. Run the advisor once per package with that package's commits only.

Ambiguous refactors

refactor: rename export Foo to Bar is breaking for libraries even without a !. Escalate to the user: if public API renamed → major; if internal only → patch.

Empty or non-conventional lines

Plain sentences without a type still count if they clearly say "breaking" or "add feature"; otherwise mark them unknown and ask.

Build metadata

1.2.3+build.9 — bump the core version, then decide whether to drop or refresh +build.... Do not let metadata affect major/minor/patch math. FILE:templates/bump-report.md

SemVer bump report

Current version

  • …

Recommended bump

  • Level: patch | minor | major
  • Next version: …
  • Decisive change: …

Change tally

Level Count Examples
major
minor
patch
unknown

Notes and questions

  • …

Suggested tag / release command

# example
git tag -a vX.Y.Z -m "…"

FILE:examples/example-library-release.md

Example: library release from Conventional Commits

Current version: 1.4.2

Commits since v1.4.2:

fix: handle empty Accept headers
feat(api): add cursor pagination
docs: fix README typo
feat!: remove deprecated /v1/list route

Advisor result: major → 2.0.0 because of feat!: remove deprecated /v1/list route.

Rationale: The ! marks a breaking API removal. The new pagination feature alone would have been a minor (1.5.0), and the fix/docs alone a patch (1.4.3), but major wins.

Report excerpt: Recommend releasing 2.0.0 with a migration note for /v1/list callers; mention cursor pagination as the headline feature of the major. FILE:scripts/semver_bump.py #!/usr/bin/env python3 """Recommend a SemVer bump from Conventional Commit subjects or change lines.

Usage: python3 semver_bump.py 1.4.2 --commits commits.txt python3 semver_bump.py 0.9.1 --commits - --pre-1-0-major-as-minor python3 semver_bump.py 2.0.0 --json < changes.txt """ from future import annotations

import argparse import json import re import sys

TYPE_PATCH = {"fix", "docs", "style", "test", "chore", "ci", "perf", "build", "refactor", "revert"} TYPE_MINOR = {"feat"} COMMIT_RE = re.compile( r"^(?P[a-zA-Z]+)(?P([^)]))?(?P!)?:\s(?P.+)$" ) VER_RE = re.compile( r"^(?P0|[1-9]\d*).(?P0|[1-9]\d*).(?P0|[1-9]\d*)" r"(?:-(?P

[0-9A-Za-z.-]+))?(?:+(?P[0-9A-Za-z.-]+))?$"
)
LEVELS = {"none": 0, "patch": 1, "minor": 2, "major": 3}

def parse_version(v: str) -> dict: m = VER_RE.match(v.strip()) if not m: raise SystemExit(f"error: not a SemVer core version: {v!r}") return {k: m.group(k) for k in ("major", "minor", "patch", "pre", "build")}

def classify_line(line: str) -> tuple[str, str]: raw = line.strip() if not raw or raw.startswith("#"): return "skip", raw lower = raw.lower() if lower.startswith("breaking change:") or lower.startswith("breaking-change:"): return "major", raw m = COMMIT_RE.match(raw) if m: if m.group("breaking"): return "major", raw t = m.group("type").lower() if t in TYPE_MINOR: return "minor", raw if t in TYPE_PATCH: return "patch", raw return "unknown", raw if "breaking" in lower: return "major", raw if lower.startswith("feat") or "new feature" in lower or lower.startswith("add "): return "minor", raw if lower.startswith("fix") or "bug" in lower: return "patch", raw return "unknown", raw

def read_lines(path: str) -> list[str]: if path == "-": return sys.stdin.read().splitlines() with open(path, encoding="utf-8") as f: return f.read().splitlines()

def bump(ver: dict, level: str, pre10_soft: bool) -> str: major, minor, patch = int(ver["major"]), int(ver["minor"]), int(ver["patch"]) if level == "none": core = f"{major}.{minor}.{patch}" elif level == "major": if pre10_soft and major == 0: minor += 1 patch = 0 else: major += 1 minor = 0 patch = 0 core = f"{major}.{minor}.{patch}" elif level == "minor": minor += 1 patch = 0 core = f"{major}.{minor}.{patch}" else: patch += 1 core = f"{major}.{minor}.{patch}" return core

def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=doc) ap.add_argument("version", help="Current SemVer, e.g. 1.4.2 or 0.9.1-rc.1") ap.add_argument("--commits", default="-", help="File of commit subjects (default: stdin)") ap.add_argument("--pre-1-0-major-as-minor", action="store_true", help="In 0.x, treat breaking as minor bump") ap.add_argument("--json", action="store_true", help="Machine-readable JSON output") args = ap.parse_args(argv)

ver = parse_version(args.version)
lines = read_lines(args.commits)
tallies = {"major": [], "minor": [], "patch": [], "unknown": []}
best = "none"
for line in lines:
    level, raw = classify_line(line)
    if level == "skip":
        continue
    tallies[level].append(raw)
    if LEVELS[level] > LEVELS[best]:
        best = level

if best == "none":
    print("error: no usable change lines", file=sys.stderr)
    return 1

next_v = bump(ver, best, args.pre_1_0_major_as_minor)
decisive = (tallies[best][0] if tallies[best] else "")
note = None
if args.pre_1_0_major_as_minor and int(ver["major"]) == 0 and best == "major":
    note = "pre-1.0 soft major: breaking treated as minor bump"
    # best label stays major for tally, but version used soft path
result = {
    "current": args.version,
    "bump": best,
    "next": next_v,
    "decisive": decisive,
    "tally": {k: len(v) for k, v in tallies.items()},
    "examples": {k: v[:5] for k, v in tallies.items() if v},
    "note": note,
}
if args.json:
    json.dump(result, sys.stdout, indent=2)
    sys.stdout.write("\n")
else:
    print(f"current: {args.version}")
    print(f"bump:    {best}")
    print(f"next:    {next_v}")
    if note:
        print(f"note:    {note}")
    print(f"decisive:{decisive}")
    print("tally:")
    for k in ("major", "minor", "patch", "unknown"):
        print(f"  {k}: {len(tallies[k])}")
        for ex in tallies[k][:5]:
            print(f"    - {ex}")
return 0

if name == "main": sys.exit(main(sys.argv[1:]))