diff --git a/CONTEXT.md b/CONTEXT.md index 14265a9..80d8fe5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 diff --git a/README.md b/README.md index 94dbb7c..e6d3476 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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). diff --git a/docs/adr/0006-strategies-decide-from-the-head-commit-only.md b/docs/adr/0006-strategies-decide-from-the-head-commit-only.md new file mode 100644 index 0000000..bbfe2db --- /dev/null +++ b/docs/adr/0006-strategies-decide-from-the-head-commit-only.md @@ -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. diff --git a/docs/index.md b/docs/index.md index cdfa5f7..6599a62 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. @@ -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). diff --git a/docs/providers/github.md b/docs/providers/github.md index 2182b61..2e99f03 100644 --- a/docs/providers/github.md +++ b/docs/providers/github.md @@ -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. @@ -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 @@ -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. @@ -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. diff --git a/docs/providers/gitlab.md b/docs/providers/gitlab.md index 763965b..446d9c7 100644 --- a/docs/providers/gitlab.md +++ b/docs/providers/gitlab.md @@ -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. @@ -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 @@ -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: @@ -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. diff --git a/docs/strategies/branch-prefix.md b/docs/strategies/branch-prefix.md index 5d9cf66..5d3c2af 100644 --- a/docs/strategies/branch-prefix.md +++ b/docs/strategies/branch-prefix.md @@ -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 diff --git a/docs/strategies/conventional-commits.md b/docs/strategies/conventional-commits.md index 269b0d6..024f59d 100644 --- a/docs/strategies/conventional-commits.md +++ b/docs/strategies/conventional-commits.md @@ -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 @@ -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