Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ A term is listed only when there is a synonym to reject, or a meaning subtle eno
docs must agree on it. General semver and CI vocabulary — tag, version, release, major/minor/patch —
does not belong here, however heavily this project uses it.

**Head commit**:
The single commit a run judges: the tip of the default branch at the moment the run starts, fetched
by `get_latest_commit_on_default_branch` and passed whole to `BumpStrategy.decide`. Nothing between
the latest tag and the head is read.
_Avoid_: latest commit. "Latest" is already pinned by *latest tag* to mean highest by precedence,
not newest by date, and the head is newest by position.

**Forge**:
A repository-hosting service semvertag talks to over REST — GitLab or GitHub today.
_Avoid_: host. `host` is already spoken for by the same-origin pagination guard, whose two
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ semvertag:
```

It runs `uvx semvertag tag` against your repo on the default branch.
semvertag inspects the latest commit + tag history, decides the
semvertag inspects the head commit + tag history, decides the
appropriate semver bump, and creates the new tag via the GitLab API.

> A one-line `include: - component: …` via the GitLab CI Catalog will
Expand Down Expand Up @@ -82,10 +82,10 @@ GitHub Enterprise setup, outputs, and troubleshooting.

## Strategies

- **branch-prefix** (default): the latest commit on the default branch
- **branch-prefix** (default): the head commit on the default branch
must be a merge commit whose source branch starts with `feature/`
(minor), `bugfix/`, or `hotfix/` (patch).
- **conventional-commits**: parses the latest commit's
- **conventional-commits**: parses the head commit's
[Conventional Commits](https://www.conventionalcommits.org/)
header (`feat:` minor, `fix:`/`perf:` patch, `!` or `BREAKING
CHANGE:` major).
Expand Down
13 changes: 13 additions & 0 deletions docs/adr/0006-strategies-decide-from-the-head-commit-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Strategies decide from the head commit only

`SemvertagUseCase` fetches one commit, the head of the default branch, and hands it to
`BumpStrategy.decide`; neither strategy sees the commits between the latest tag and the head. The
Conventional Commits convention is usually applied as a scan of that range, taking the highest bump
found, and the docs promised exactly that until they were corrected. The scan was declined because
the action runs once per push and the org's pull requests land as squash merges, so a push is one
commit and the range is the head; a merge-commit workflow is served by `branch-prefix`, which reads
the merge commit that is the head. A range scan would need a new `Provider` operation on two
independently versioned REST APIs, a bound on long ranges, and would change the bump existing users
get from the same history. The accepted cost is that a failed run is not recovered by the next
push, which is judged on its own head, so a failed tagging job has to be re-run. A range scan
stays a possible opt-in if a merge-commit team using Conventional Commits asks for it.
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
Auto-tag your GitLab repository with semantic version tags from CI —
one tool, two strategies.

semvertag reads the latest commit and tag history from your GitLab
semvertag reads the head commit and tag history from your GitLab
project via the API, decides the appropriate semver bump based on the
strategy you've configured, and creates the new git tag — all from a
single command in your CI pipeline.
Expand Down Expand Up @@ -52,7 +52,7 @@ semvertag ships with two bump-decision strategies:
source branch of the latest merge commit (`feature/` → minor,
`bugfix/` / `hotfix/` → patch). The default.
- [**conventional-commits**](strategies/conventional-commits.md) —
bump based on the latest commit's Conventional Commits header
bump based on the head commit's Conventional Commits message
(`feat:` → minor, `fix:` / `perf:` → patch, `!` or
`BREAKING CHANGE:` → major).

Expand Down
31 changes: 20 additions & 11 deletions docs/providers/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- uses: modern-python/semvertag@v0
```

The job runs against the latest commit on the default branch and, if
The job runs against the head commit on the default branch and, if
a bump is warranted by the configured strategy, creates a new tag
ref via the GitHub API. If no bump is warranted, the job exits 0
without pushing.
Expand All @@ -58,8 +58,8 @@ Pass `--strategy` (or set `SEMVERTAG_STRATEGY`) to one of:

| Value | Description |
|---|---|
| `branch-prefix` (default) | Bump from the source-branch prefix of the latest merge commit. |
| `conventional-commits` | Bump from Conventional Commits headers since the last tag. |
| `branch-prefix` (default) | Bump from the source-branch prefix of the head commit, which must be a merge commit. |
| `conventional-commits` | Bump from the head commit's Conventional Commits message. |

```yaml
- uses: modern-python/semvertag@v0
Expand Down Expand Up @@ -189,20 +189,24 @@ is the entire setup.
## Branch-prefix vs conventional-commits

Pick `branch-prefix` if your team merges PRs with branch names that
follow a `fix/...`, `feat/...`, `chore/...` convention. semvertag
reads the most recent merge commit's source-branch prefix and bumps
accordingly — `fix/` bumps patch, `feat/` bumps minor, `chore/`
bumps nothing. This is the default. See
follow a `fix/...`, `feat/...`, `chore/...` convention and lands them
as merge commits. semvertag reads the head commit's source-branch
prefix and bumps accordingly — `fix/` bumps patch, `feat/` bumps
minor, `chore/` bumps nothing. With squash merges the head is not a
merge commit and the run reports `no_merge_commit`. This is the
default. See
[Branch-prefix strategy](../strategies/branch-prefix.md) for the full
prefix-to-bump table and edge-case behavior.

Pick `conventional-commits` if your team writes
[Conventional Commits](https://www.conventionalcommits.org/) messages
directly on the default branch (e.g. `feat: add X`, `fix: handle Y`,
`feat!: drop Z`). semvertag scans commits since the last tag and
chooses the highest bump implied by their type prefixes (`feat!` or
`BREAKING CHANGE:` → major, `feat:` → minor, `fix:` → patch,
everything else → none). See
`feat!: drop Z`), typically with squash merges so that each push is
one commit. semvertag reads the head commit's type prefix and body
(`feat!` or a `BREAKING CHANGE:` footer → major, `feat:` → minor,
`fix:` → patch,
everything else → none); it does not scan the commits since the
latest tag. See
[Conventional Commits strategy](../strategies/conventional-commits.md)
for the full type-to-bump mapping.

Expand Down Expand Up @@ -265,3 +269,8 @@ envelope yourself.
`SEMVERTAG_GITHUB__ENDPOINT` (note the double underscore) as a
workflow-level env pointing to the instance's API root, e.g.
`https://github.example.com/api/v3`.

- **A bump-worthy push was never tagged** — the run for that push
failed or was skipped. Re-run it. Each run judges only the head
commit of its own push and does not look back, so the next push
cannot recover an earlier bump.
33 changes: 21 additions & 12 deletions docs/providers/gitlab.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ semvertag:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
```

The job runs against the latest commit on the default branch and, if a
The job runs against the head commit on the default branch and, if a
bump is warranted by the configured strategy, pushes a new tag to the
project's `origin`. If no bump is warranted, the job exits 0 without
pushing.
Expand All @@ -56,8 +56,8 @@ Set `SEMVERTAG_STRATEGY` to one of:

| Value | Description |
|---|---|
| `branch-prefix` (default) | Bump from the source-branch prefix of the latest merge commit. |
| `conventional-commits` | Bump from Conventional Commits headers since the last tag. |
| `branch-prefix` (default) | Bump from the source-branch prefix of the head commit, which must be a merge commit. |
| `conventional-commits` | Bump from the head commit's Conventional Commits message. |

When the Catalog component lands, this will become a typed `inputs:`
block on the `include:`. The values and default match
Expand Down Expand Up @@ -117,22 +117,26 @@ write scope, the minimal job snippet above is the entire setup.
## Branch-prefix vs conventional-commits

Pick `branch-prefix` if your team merges merge requests with branch
names that follow a `fix/...`, `feat/...`, `chore/...` convention.
semvertag reads the most recent merge commit's source-branch prefix
and bumps accordingly — `fix/` bumps patch, `feat/` bumps minor,
`chore/` bumps nothing. This is the default. See
names that follow a `fix/...`, `feat/...`, `chore/...` convention
and lands them as merge commits. semvertag reads the head commit's
source-branch prefix and bumps accordingly — `fix/` bumps patch,
`feat/` bumps minor, `chore/` bumps nothing. With squash merges the
head is not a merge commit and the run reports `no_merge_commit`.
This is the default. See
[Branch-prefix strategy](../strategies/branch-prefix.md) for the full
prefix-to-bump table and edge-case behavior.

Pick `conventional-commits` if your team writes
[Conventional Commits](https://www.conventionalcommits.org/) messages
directly on the default branch (e.g. `feat: add X`, `fix: handle Y`,
`feat!: drop Z`). semvertag scans commits since the last tag and
chooses the highest bump implied by their type prefixes (`feat!` or
`BREAKING CHANGE:` → major, `feat:` → minor, `fix:` → patch,
everything else → none). See
`feat!: drop Z`), typically with squash merges so that each push is
one commit. semvertag reads the head commit's type prefix and body
(`feat!` or a `BREAKING CHANGE:` footer → major, `feat:` → minor,
`fix:` → patch,
everything else → none); it does not scan the commits since the
latest tag. See
[Conventional Commits strategy](../strategies/conventional-commits.md)
for the full type-to-bump mapping and commit-scanning rules.
for the full type-to-bump mapping and the head-commit rule.

Set the strategy per project by swapping the `SEMVERTAG_STRATEGY`
value in the job:
Expand Down Expand Up @@ -164,3 +168,8 @@ semvertag:
auto-derived from `CI_SERVER_FQDN`. Set
`SEMVERTAG_GITLAB__ENDPOINT` as a project-level CI/CD variable
pointing to the instance's API root.

- **A bump-worthy push was never tagged** — the run for that push
failed or was skipped. Re-run it. Each run judges only the head
commit of its own push and does not look back, so the next push
cannot recover an earlier bump.
11 changes: 9 additions & 2 deletions docs/strategies/branch-prefix.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,20 @@ These are set via the same pydantic-settings env-var mechanism used
for tokens / endpoints — see the provider docs for the variable
naming convention.

## Head commit only

Like every strategy, `branch-prefix` sees only the head commit of the
push that triggered the run. A run that fails after a bump-worthy
merge must be re-run: the next push is judged on its own head, and
the earlier bump is not recovered.

## When to pick a different strategy

If your team commits Conventional Commits messages directly to the
default branch (without merge commits), switch to
[Conventional Commits](conventional-commits.md) — that strategy
scans every commit since the last tag and does not depend on merge
metadata.
reads the head commit's subject and body and does not depend on
merge metadata.

## Consumer integration

Expand Down
38 changes: 21 additions & 17 deletions docs/strategies/conventional-commits.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# Conventional Commits strategy

The `conventional-commits` strategy parses each commit's subject line
against the
The `conventional-commits` strategy parses the subject line of the
head commit on the default branch against the
[Conventional Commits](https://www.conventionalcommits.org/) grammar
and decides a per-commit bump. The orchestrator combines per-commit
bumps across the commit range and applies the highest one to the
release.
and decides the bump from that one commit. It does not scan the
commits since the latest tag; see [Head commit only](#head-commit-only).

## Default type-to-bump mapping

Expand Down Expand Up @@ -37,18 +36,23 @@ Both lists are validated against the lowercase-letters-only regex
`^[a-z]+$`. Major bumps come from `BREAKING CHANGE:` / `!` markers
only and are not configurable.

## Commit scanning

The strategy decides a bump per-commit; the orchestrator scans the
commit range and takes the highest bump across all commits. One
`feat!:` (or `BREAKING CHANGE:` body) anywhere in the range promotes
the release to major even if every other commit is a patch.

Merge commits are scanned the same as any other commit — their
subject is matched against the type grammar. If your merge commits do
not follow Conventional Commits format (e.g. default `Merge branch
'foo' into main` subjects), they contribute `none` and the bump is
decided by the merged commits' types.
## Head commit only

semvertag runs once per push to the default branch and fetches exactly
one commit, the head. The strategy reads that commit's subject and
body; nothing else on the branch is considered, so a `feat:` two
commits back does not promote a `chore:` head to a minor bump. This
matches a squash-merge workflow, where one push is one commit whose
subject is the pull request title. With a merge-commit workflow the
head is the merge commit, and a default `Merge branch 'foo' into
main` subject does not match the grammar, so the strategy declines
with `no_conforming_commit`; use [Branch prefix](branch-prefix.md)
there, since it reads the merge commit's source branch instead,
keeping in mind that it produces no major bumps.

Because nothing looks back, a run that fails after a bump-worthy push
must be re-run: the next push is judged on its own head, and the
earlier bump is not recovered.

## When to pick a different strategy

Expand Down
Loading