Skip to content

Commit 9391d97

Browse files
lesnik512claude
andauthored
httpware 0.12 + opt-in patch bump for non-merge commits (branch-prefix) (#24)
* providers: bump httpware to 0.12.0, adopt get_with_response Raise the dependency floor from >=0.8.2 to >=0.12.0 and collapse the two Link-header pagination call sites from send_with_response(build_request(...)) to the 0.12.0 get_with_response shortcut. No behavior change; picks up 0.11.0 security/correctness hardening via the version move. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(planning): add branch-prefix patch-on-non-merge change bundle Spec (design.md) and implementation plan (plan.md) for an opt-in branch-prefix flag that patch-bumps plain (non-merge) commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * strategies: add opt-in patch bump for non-merge commits Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document branch-prefix patch_on_non_merge_commit flag Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: correct stale Justfile recipe references in CLAUDE.md Drop the nonexistent test-branch / test-branch-strategies / test-cc-strategies recipes; document that just test already runs --cov-branch with a project-wide fail_under=100 gate, and reference just docs-build for the strict docs gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(planning): archive the two 2026-06-16 change bundles (#24) Mark both bundles shipped (status: shipped, pr: 24, outcome filled), move them from changes/active/ to changes/archive/, and move their Index lines from Active to Archived. architecture/ conclusions were already promoted in their respective commits. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(planning): register deferred follow-ups from PR #24 review Open the deferred register with two real-but-unscheduled items surfaced in the PR #24 review, each with a revisit trigger: conventional-commits parity for the non-merge/non-conforming fallback flag, and httpware max_error_body_bytes adoption. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6be1557 commit 9391d97

13 files changed

Lines changed: 518 additions & 17 deletions

File tree

‎CLAUDE.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,13 @@ belong to the retired BMad workflow.
6666
See `Justfile` for the canonical commands. Quick reference:
6767

6868
- `just lint-ci` — eof-fixer, ruff format check, ruff check, ty check
69-
- `just test` — pytest with coverage
70-
- `just test-branch` — pytest with branch coverage
71-
- `just test-branch-strategies` / `just test-cc-strategies`
72-
— 100% branch coverage gates on specific modules
73-
- `mkdocs build --strict` — docs build gate
69+
(check-only; `just lint` is the autofixing variant)
70+
- `just test` — pytest. The `addopts` in `pyproject.toml` add `--cov-branch`
71+
with a project-wide `fail_under = 100` gate, so every branch (strategy
72+
modules included) must be covered. Pass args through, e.g.
73+
`just test tests/unit/test_branch_prefix_strategy.py -q`.
74+
- `just docs-build` — strict mkdocs build (`mkdocs build --strict`), the docs
75+
gate.
7476

7577
## What the codebase ships
7678

‎architecture/strategies.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ of the message (via `subject_line`, below) and applies, in order:
4141
request"`, so an ordinary GitHub PR merge-commit subject
4242
(`Merge pull request #N from owner/feature/...`) and a GitLab merge-commit
4343
subject both qualify under the defaults; a plain non-merge commit does not.
44+
When `config.patch_on_non_merge_commit` is `True` (default `False`), this
45+
non-merge case returns `Bump.PATCH` instead of `Bump.NONE`, so a direct push
46+
to the default branch auto-tags a patch release. The flag governs only this
47+
exit — a merge commit with an unrecognized prefix (step 4) still returns
48+
`Bump.NONE`.
4449
2. If any string in `config.minor` appears in the subject, return `Bump.MINOR`.
4550
Default: `("feature/",)`.
4651
3. If any string in `config.patch` appears, return `Bump.PATCH`. Default:

‎docs/strategies/branch-prefix.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ means:
3030

3131
- Standard `git merge feature/foo` → subject `Merge branch 'feature/foo' into main` → bump = minor ✓
3232
- GitHub's `Merge pull request #N from user/feature/foo` → bump = minor ✓
33-
- Direct pushes to the default branch → bump = none.
33+
- Direct pushes to the default branch → bump = none, unless
34+
`patch_on_non_merge_commit` is enabled (see below), in which case
35+
bump = patch.
3436

3537
The `merge_mark_texts` tuple is configurable (defaults to
3638
`("Merge branch", "Merge pull request")`); adapt it for non-default
@@ -46,6 +48,11 @@ The strategy reads its prefixes from the application's settings layer:
4648
`("bugfix/", "hotfix/")`).
4749
- `merge_mark_texts` — tuple of substrings that mark a subject as a
4850
merge commit (default `("Merge branch", "Merge pull request")`).
51+
- `patch_on_non_merge_commit` — when `true`, a plain (non-merge) commit on
52+
the default branch bumps patch instead of producing no bump (default
53+
`false`). Set via `SEMVERTAG_BRANCH_PREFIX__PATCH_ON_NON_MERGE_COMMIT=true`.
54+
Affects only the non-merge case; a merge commit with an unrecognized prefix
55+
still produces no bump.
4956

5057
These are set via the same pydantic-settings env-var mechanism used
5158
for tokens / endpoints — see the provider docs for the variable

‎planning/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ _None._
7474

7575
### Archived (shipped)
7676

77+
- **[branch-prefix-patch-on-non-merge](changes/archive/2026-06-16.02-branch-prefix-patch-on-non-merge/design.md)**
78+
(#24, 2026-06-16) — Opt-in `patch_on_non_merge_commit` flag: a non-merge HEAD
79+
commit bumps patch instead of nothing.
80+
- **[httpware-0.12-get-with-response](changes/archive/2026-06-16.01-httpware-0.12-get-with-response/change.md)**
81+
(#24, 2026-06-16) — Bump httpware to 0.12.0; adopt `get_with_response` at the
82+
pagination call sites.
7783
- **[portable-planning-convention](changes/archive/2026-06-13.01-portable-planning-convention/design.md)**
7884
(#21, 2026-06-13) — Adopt the portable two-axis convention: `architecture/`
7985
truth home + `changes/` bundles, migrate the 15 spec/plan pairs, fresh Index.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
status: shipped
3+
date: 2026-06-16
4+
slug: httpware-0.12-get-with-response
5+
supersedes: null
6+
superseded_by: null
7+
pr: 24
8+
outcome: Shipped. httpware floor raised to >=0.12.0; both Link-header pagination
9+
call sites now use get_with_response. No behavior change; no architecture
10+
contract moved.
11+
---
12+
13+
# Change: Bump httpware to 0.12.0 and adopt get_with_response in pagination
14+
15+
**Lane:** lightweight — net change is a dependency-floor bump plus a two-line
16+
mechanical refactor at the two pagination call sites. Four files touched, but
17+
`pyproject.toml` + `uv.lock` are dependency config and the code delta is a
18+
straight 1:1 method swap with no behavior change. No public-API change.
19+
20+
## Goal
21+
22+
We pin `httpware[pydantic]>=0.8.2` (lock 0.8.2) while latest is 0.12.0 — every
23+
intervening release is additive/no-break. Raise the floor to `>=0.12.0` and
24+
adopt 0.12.0's `get_with_response`, which collapses the
25+
`send_with_response(build_request("GET", ...))` pair used for Link-header
26+
pagination into a single call. Picks up 0.11.0's security/correctness hardening
27+
(URL secret redaction, RetryBudget fix) for free via the version move.
28+
29+
## Approach
30+
31+
`get_with_response(url, *, params=..., response_model=...) -> tuple[Response, T]`
32+
is the ergonomic shortcut for "raw response + typed body in one call" — exactly
33+
the pagination shape in both providers. Swap the two call sites; behavior is
34+
identical (same request, same returned `(response, page)` tuple), so existing
35+
pagination tests stay green. No `architecture/` contract moves — the providers'
36+
external behavior is unchanged; the HTTP-client prose in `providers.md` does not
37+
name `send_with_response`/`build_request`, so no doc edit is required.
38+
39+
Deferred (not in this bundle): adopting `max_error_body_bytes` /
40+
`ResponseTooLargeError` from 0.11.0 — that's a real behavior/config decision.
41+
42+
## Files
43+
44+
- `pyproject.toml` — `httpware[pydantic]>=0.8.2` → `>=0.12.0`
45+
- `uv.lock` — relock via `uv lock`
46+
- `semvertag/providers/gitlab.py` — `list_tags` call site → `get_with_response`
47+
- `semvertag/providers/github.py` — `list_tags` call site → `get_with_response`
48+
49+
## Verification
50+
51+
- [x] `uv lock` resolves httpware to 0.12.0.
52+
- [x] Refactor both call sites to `get_with_response`.
53+
- [x] `just test` — 428 passed (existing pagination tests cover behavior).
54+
- [x] `just lint-ci` — clean (ruff format, ruff check, ty all pass).
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
status: shipped
3+
date: 2026-06-16
4+
slug: branch-prefix-patch-on-non-merge
5+
supersedes: null
6+
superseded_by: null
7+
pr: 24
8+
outcome: Shipped. Opt-in patch_on_non_merge_commit flag (default False) added to
9+
branch-prefix; a non-merge HEAD commit bumps patch when enabled. Conclusions
10+
promoted into architecture/strategies.md.
11+
---
12+
13+
# Design: Opt-in patch bump for non-merge commits (branch-prefix)
14+
15+
## Summary
16+
17+
Add an opt-in `branch-prefix` config flag, `patch_on_non_merge_commit` (default
18+
`False`), that makes the strategy return `Bump.PATCH` instead of `Bump.NONE`
19+
when the HEAD commit on the default branch is a plain (non-merge) commit. Teams
20+
that allow direct pushes to the default branch can then auto-tag a patch release
21+
for each such push instead of silently skipping. Default-off preserves today's
22+
behavior byte-for-byte. Scope: the `branch-prefix` strategy only, the
23+
non-merge exit only.
24+
25+
## Motivation
26+
27+
`branch-prefix` exists to bump on merge commits: the merged branch name carries
28+
the level (`feature/` → minor, `bugfix/`/`hotfix/` → patch). A commit pushed
29+
directly to the default branch — not via an MR/PR — carries no merge mark, so
30+
`BranchPrefixStrategy.decide` returns `Bump.NONE` and the run ends with status
31+
`no_merge_commit` (`semvertag/strategies/branch_prefix.py:33-34`,
32+
`semvertag/_use_case.py:52`). For a team that permits direct pushes, that means
33+
real changes land on the default branch with no version movement at all. A
34+
sensible default for "code changed but no merge told me how much" is the
35+
smallest bump: patch. Making it opt-in lets those teams get an automatic patch
36+
release per direct push without affecting anyone relying on merge-only bumping.
37+
38+
## Non-goals
39+
40+
- Not changing the default behavior: the flag defaults to `False`, so existing
41+
users see no change.
42+
- Not touching the `conventional-commits` strategy; a non-conforming commit
43+
there still returns `Bump.NONE`. An analogous fallback can be a later change.
44+
- Not changing the merge-with-unrecognized-prefix exit
45+
(`branch_prefix.py:39`): a commit that *is* a merge but matches neither the
46+
minor nor patch tables still returns `Bump.NONE`. The flag only governs the
47+
"not a merge at all" case, matching the request's framing.
48+
- No CLI flag: the existing `branch_prefix` config fields (`minor`, `patch`,
49+
`merge_mark_texts`) are env/config-only; the new field follows suit.
50+
- No use-case or output-messaging change (see Design §3).
51+
52+
## Design
53+
54+
### 1. Config field
55+
56+
Add one field to `BranchPrefixConfig`
57+
(`semvertag/strategies/branch_prefix.py`):
58+
59+
```python
60+
class BranchPrefixConfig(pydantic.BaseModel):
61+
model_config = pydantic.ConfigDict(frozen=True)
62+
63+
minor: tuple[_NonEmptyStr, ...] = pydantic.Field(default=("feature/",), min_length=1)
64+
patch: tuple[_NonEmptyStr, ...] = pydantic.Field(default=("bugfix/", "hotfix/"), min_length=1)
65+
merge_mark_texts: tuple[_NonEmptyStr, ...] = pydantic.Field(
66+
default=("Merge branch", "Merge pull request"),
67+
min_length=1,
68+
)
69+
patch_on_non_merge_commit: bool = False
70+
```
71+
72+
It rides the existing `Settings.branch_prefix: BranchPrefixConfig` wiring
73+
(`semvertag/_settings.py:98`) and the IoC builder
74+
(`semvertag/ioc.py:68-69`), so it is settable via
75+
`SEMVERTAG_BRANCH_PREFIX__PATCH_ON_NON_MERGE_COMMIT=true` with no new plumbing.
76+
Standard pydantic-settings bool coercion applies (`true`/`1`/`yes` → `True`).
77+
78+
### 2. `decide` change
79+
80+
The non-merge exit gains a single ternary; nothing else in the method moves:
81+
82+
```python
83+
def decide(self, commit: Commit) -> Bump:
84+
subject: typing.Final = subject_line(commit.message)
85+
if not any(mark in subject for mark in self.config.merge_mark_texts):
86+
return Bump.PATCH if self.config.patch_on_non_merge_commit else Bump.NONE
87+
if any(prefix in subject for prefix in self.config.minor):
88+
return Bump.MINOR
89+
if any(prefix in subject for prefix in self.config.patch):
90+
return Bump.PATCH
91+
return Bump.NONE
92+
```
93+
94+
### 3. Why no use-case or messaging change
95+
96+
`semvertag/_use_case.py:51-60` turns any non-`NONE` bump into a real tag and
97+
only emits the strategy's `no_bump_status` / `no_bump_reason` on the `NONE`
98+
path. When the flag fires, a non-merge commit yields `Bump.PATCH`, so the run
99+
produces a normal `tagged` (or `dry_run`) result through the existing path. The
100+
`no_merge_commit` status/reason ClassVars are still correct: they are read only
101+
when `decide` returns `NONE`, which still happens for unrecognized merges and
102+
for non-merge commits when the flag is off. No new status string is introduced.
103+
104+
## Testing
105+
106+
Unit tests in `tests/unit/test_branch_prefix_strategy.py`. The global pytest
107+
config runs `--cov-branch` with `fail_under = 100`, so the new ternary's `True`
108+
and `False` arms must both be exercised.
109+
110+
- **Flag on, non-merge → PATCH.** A `BranchPrefixStrategy` built with
111+
`BranchPrefixConfig(patch_on_non_merge_commit=True)` returns `Bump.PATCH` for
112+
each existing `_NON_MERGE_CASES` subject (`feat: ...`, `docs: ...`, `""`,
113+
lowercase `merge branch ...`).
114+
- **Flag on does not disturb the merge paths.** With the flag on, a recognized
115+
feature merge still returns `MINOR`, a bugfix/hotfix merge still `PATCH`, and
116+
an unrecognized *merge* (`_UNRECOGNIZED_MERGE_CASES`) still `NONE` — proving
117+
the flag governs only the non-merge exit, not line 39.
118+
- **Flag off (default) unchanged.** The existing
119+
`test_returns_none_when_message_is_not_a_merge_commit` already covers the
120+
`False` arm; it stays green untouched.
121+
- **Default value.** Assert `BranchPrefixConfig().patch_on_non_merge_commit is
122+
False`.
123+
124+
No config-validation case is needed (a bool cannot be empty). No
125+
integration-level test is required: the `decide → PATCH → tag` wiring is already
126+
covered by existing use-case tests that exercise a non-`NONE` bump.
127+
128+
## Docs
129+
130+
On merge (per the planning convention, the change promotes its conclusions into
131+
the affected capability doc):
132+
133+
- `architecture/strategies.md` — note the opt-in `patch_on_non_merge_commit`
134+
under the `branch-prefix` section's step 1.
135+
- mkdocs config/reference page for `branch-prefix` — document the new env var
136+
and its default.
137+
138+
## Risk
139+
140+
Low. The behavior change is gated behind a default-`False` flag, so existing
141+
installs are unaffected. The single new branch is fully covered by the
142+
100%-branch gate. The main user-facing consideration is conceptual, not a code
143+
risk: with the flag on, *any* direct push to the default branch (including a
144+
docs typo fix) triggers a patch tag — which is exactly the intended semantics,
145+
and is documented as such. No rollback concern: flipping the flag back to
146+
`False` restores prior behavior immediately.

0 commit comments

Comments
 (0)