From 38464aa025e5b6ee6e60e0ee927dc9996dece343 Mon Sep 17 00:00:00 2001 From: Matias Galarza Date: Mon, 17 Aug 2026 04:26:09 -0300 Subject: [PATCH] fix(ci): a BREAKING CHANGE footer is a footer, not a substring The first run this workflow ever did took the engine from 0.2.44 to 1.0.0, because the pull request that introduced it explained the rule in a bullet and the bash conditional matched 'BREAKING CHANGE:' anywhere in the body. The decision moves into bump_version.py with a self-test beside it - the regression is one of its twelve cases - and the footer is anchored to the start of a line, which is what Conventional Commits actually says. Restores the version to 0.2.45, the patch that PR should have been. --- .github/scripts/bump_version.py | 61 +++++++++++++++++++++++++++++++++ .github/workflows/version.yml | 29 +++++++++------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/.github/scripts/bump_version.py b/.github/scripts/bump_version.py index 074cdf1b..014eb1b4 100644 --- a/.github/scripts/bump_version.py +++ b/.github/scripts/bump_version.py @@ -17,6 +17,7 @@ """ import argparse +import os import re import subprocess import sys @@ -99,12 +100,66 @@ def retag(match: re.Match) -> str: return moved +# `type(scope)!:` — the `!` is what makes it breaking, per Conventional +# Commits. Anchored to the start of the subject so a `!` anywhere else in +# the sentence is not a declaration. +BREAKING_SUBJECT = re.compile(r"^[a-z]+(\([^)]+\))?!:") +FEATURE_SUBJECT = re.compile(r"^feat(\([^)]+\))?:") +# 🔴 A FOOTER, which means the start of its own line — not the string +# appearing anywhere in the body. This workflow's own pull request +# described the rule in a bullet, the unanchored version matched that +# bullet, and the engine went from 0.2.44 to 1.0.0 on the first run. +BREAKING_FOOTER = re.compile(r"^BREAKING[ -]CHANGE:", re.MULTILINE) + + +def decide(title: str, body: str) -> str: + """How far a PR with this title and body moves the version.""" + if BREAKING_SUBJECT.search(title) or BREAKING_FOOTER.search(body): + return "major" + if FEATURE_SUBJECT.search(title): + return "minor" + return "patch" + + +def self_test() -> int: + cases = [ + ("feat!: rip out the old renderer", "", "major"), + ("feat(render)!: rip out the old renderer", "", "major"), + ("refactor!: rename every crate", "", "major"), + ("fix: a footer declares it", "BREAKING CHANGE: the asset format moved", "major"), + ("fix: a hyphenated footer", "BREAKING-CHANGE: same thing", "major"), + # The regression this function exists for. + ("ci: every pull request moves the engine version", + "- `feat!:` / `BREAKING CHANGE:` -> major, `feat:` -> minor", "patch"), + ("feat: contact shadows", "", "minor"), + ("feat(lighting): the froxel grid", "", "minor"), + ("fix: the cascade seam", "", "patch"), + ("docs(book): the pipeline diagram", "", "patch"), + ("chore: bump wgpu", "", "patch"), + ("a title with no prefix at all", "", "patch"), + ] + failed = 0 + for title, body, want in cases: + got = decide(title, body) + if got != want: + failed += 1 + print(f"FAIL {title!r} -> {got}, wanted {want}") + print(f"{len(cases) - failed}/{len(cases)} decisions correct") + return 1 if failed else 0 + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--bump", choices=["major", "minor", "patch"]) group.add_argument("--set", dest="exact", metavar="X.Y.Z") group.add_argument("--print", action="store_true", help="read the current version and stop") + group.add_argument( + "--decide", + metavar="TITLE", + help="print major/minor/patch for this PR title; the body comes from $PR_BODY", + ) + group.add_argument("--self-test", action="store_true", help="check --decide against known cases") parser.add_argument( "--check", action="store_true", @@ -112,6 +167,12 @@ def main() -> int: ) args = parser.parse_args() + if args.self_test: + return self_test() + if args.decide: + print(decide(args.decide, os.environ.get("PR_BODY", ""))) + return 0 + current = read_version() if args.print: print(current) diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 46ed22ff..3457a4d9 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -50,22 +50,25 @@ jobs: id: decide env: TITLE: ${{ github.event.pull_request.title }} - BODY: ${{ github.event.pull_request.body }} + PR_BODY: ${{ github.event.pull_request.body }} run: | + set -euo pipefail # Conventional Commits, read off the PR title — which is what # ends up on `development` as the merge commit's subject. - # `feat!:` or a `BREAKING CHANGE:` in the body is a major; - # `feat:` is a minor; everything else — fix, docs, chore, - # refactor, test, ci, perf — is a patch, because the rule is - # that the version moves with EVERY change. A number that sits - # still turns the engine's own drift warning into noise. - if [[ "$TITLE" =~ ^[a-z]+(\(.+\))?!: ]] || [[ "$BODY" == *"BREAKING CHANGE:"* ]]; then - echo "part=major" >> "$GITHUB_OUTPUT" - elif [[ "$TITLE" =~ ^feat(\(.+\))?: ]]; then - echo "part=minor" >> "$GITHUB_OUTPUT" - else - echo "part=patch" >> "$GITHUB_OUTPUT" - fi + # `feat!:` or a `BREAKING CHANGE:` FOOTER is a major; `feat:` is + # a minor; everything else — fix, docs, chore, refactor, test, + # ci, perf — is a patch, because the rule is that the version + # moves with EVERY change. A number that sits still turns the + # engine's own drift warning into noise. + # + # 🔴 The rule lives in Python with a self-test beside it, not + # in a bash conditional here. The bash version matched + # "BREAKING CHANGE:" anywhere in the body, this workflow's own + # pull request explained the rule in a bullet, and the first + # run it ever did took the engine from 0.2.44 to 1.0.0. + python3 .github/scripts/bump_version.py --self-test + PART=$(python3 .github/scripts/bump_version.py --decide "$TITLE") + echo "part=$PART" >> "$GITHUB_OUTPUT" - name: Move the version id: move diff --git a/Cargo.toml b/Cargo.toml index 5ad74ce3..70f8d732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ ] [workspace.package] -version = "1.0.0" +version = "0.2.45" edition = "2024" license = "LicenseRef-Proprietary" repository = "https://github.com/lobinuxsoft/kooch"