Skip to content

chore(ci): scan only commit subjects for version-bump classification#36

Merged
artyomsv merged 1 commit into
masterfrom
fix/release-workflow-subject-only-scan
Jun 5, 2026
Merged

chore(ci): scan only commit subjects for version-bump classification#36
artyomsv merged 1 commit into
masterfrom
fix/release-workflow-subject-only-scan

Conversation

@artyomsv

@artyomsv artyomsv commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the false-positive minor bump on PR #35 (docs restructure) that shipped v1.13.0 with no user-visible changes.

The release pipeline was scanning both subject AND body of every commit since the last tag, hoping to catch type lines preserved in squash bodies when the subject was branch-name-derived. That heuristic produced this false positive: the #35 PR description contained the literal back-tick-wrapped reference

This PR is independent of #34 (`feat(tui): add Stop daemon action`).

…and the regex \bfeat[(:/] matched the substring feat( inside the backticks just as readily as it would have matched a real feat(tui): commit. The release job classified the PR as minor and shipped v1.13.0 with nothing user-facing in it.

Fix

What Before After
Type classification (feat/fix/perf/bang) scans %s%n%b (subject + body) scans %s (subject only) — the merger sets the squash subject explicitly and it IS the authoritative classification
BREAKING CHANGE detection matched anywhere in subject+body via BREAKING CHANGE matches ^BREAKING[ -]CHANGE: in body anchored to line start with trailing colon — the Conventional Commits footer form. Prose mentions like "this is not a BREAKING CHANGE" no longer match.
Branch-name fallback (feat/foo-bar) covered via body scan still covered — the existing [(:/] regex on subjects accepts feat/foo-bar form natively

Diff is ~30 lines, all in .github/workflows/release.yml.

Why this never bit before

The body-scan was a defense-in-depth measure for branch-name-derived squash subjects. In practice, every merge in this repo's history has had a properly conventional subject set by the merger, so the body scan never had to do its intended job — it just sat there waiting to false-positive. PR #35 was the first PR whose description text contained a conventional-commit-style reference to another PR, and that's when the bug surfaced.

Self-test

Smoke-tested the new regex on the prose vs. footer distinction:

$ echo "BREAKING CHANGE: yes" | grep -qE '^BREAKING[ -]CHANGE:'
→ match (correct — real footer)

$ echo "no BREAKING CHANGE here" | grep -qE '^BREAKING[ -]CHANGE:'
→ no match (correct — prose mention)

$ echo "feat(api): add foo" | grep -qiE '\bfeat[(:/]'
→ match (correct — real subject)

$ echo "ref to feat(api)" | grep -qiE '\bfeat[(:/]'
→ matches the regex but the body is no longer scanned for feat —
  irrelevant to the bump path now

Why this PR doesn't bump

The PR is committed as chore(ci):, which falls through to the "skip release" branch in the workflow itself. No v1.14.1 release will be cut by merging this; the next user-visible feature PR will bump as normal.

Test plan

  • Inspected the PR docs: restructure into navigable docs/ tree and add MCP user guide #35 squash body to confirm the matching string (`feat(tui)` inside backticks)
  • Verified the new ^BREAKING[ -]CHANGE: anchor matches the Conventional Commits footer form but not casual prose
  • Confirmed chore(ci): does not match the bump regexes — this PR's own merge will skip the release job (no v1.14.1 will ship)
  • Diff is small and reviewable (~30 lines, single file)

Fixes the false-positive minor bump on PR #35 (docs restructure) that
shipped v1.13.0 with no user-visible changes.

The previous script scanned both subject (%s) and body (%b) of every
commit since the last tag, hoping to catch conventional-commit type
lines preserved in squash-commit bodies when the squash subject was
derived from a branch name. That hope produced a false positive: the
PR #35 description contained a literal back-tick-wrapped reference to
PR #34's title (`feat(tui): add Stop daemon action`), and the regex
`\bfeat[(:/]` matched the substring `feat(` inside those backticks
exactly as if it had been a real `feat(tui):` commit.

Fix:

- Type classification (feat / fix / perf / bang-syntax) reads only the
  squash subject. The merger sets the subject explicitly during squash-
  merge — it is the authoritative classification, not the body.
- BREAKING CHANGE detection still reads the body, because per the
  Conventional Commits spec it lives in the footer, not the subject.
  The regex is now anchored to line start (`^BREAKING[ -]CHANGE:` with
  trailing colon) so prose references like "this is not a BREAKING
  CHANGE" do NOT match.
- The branch-name-derived subject case (`feat/foo-bar` when the merger
  forgets to retype) is still handled — the `[(:/]` character class
  already accepts `/` as a separator, so `feat/foo-bar` triggers a minor
  bump from subject scan alone.

Self-test (run before commit):

  echo "BREAKING CHANGE: yes" | grep -qE '^BREAKING[ -]CHANGE:'
    → match (correct — real footer)
  echo "no BREAKING CHANGE here" | grep -qE '^BREAKING[ -]CHANGE:'
    → no match (correct — prose mention)
  echo "feat(api): add foo" | grep -qiE '\bfeat[(:/]'
    → match (correct — real commit)
  echo "this references feat(api):" | grep -qiE '\bfeat[(:/]'
    → match (no longer relevant — this string would now only appear
      in the body, which is no longer scanned for feat)
@artyomsv artyomsv merged commit 075db61 into master Jun 5, 2026
5 checks passed
@artyomsv artyomsv deleted the fix/release-workflow-subject-only-scan branch June 5, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant