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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ Auto-tag your GitLab or GitHub repository with semantic version tags from CI —
uvx semvertag tag
```

semvertag bumps from the highest existing semver tag and never creates
the first one. Before the first run, create a plain semver tag such as
`0.1.0` (a `v` prefix does not parse and is ignored).

## Use it in GitLab CI

Paste this job into your `.gitlab-ci.yml`:
Expand Down Expand Up @@ -83,8 +87,8 @@ GitHub Enterprise setup, outputs, and troubleshooting.
## Strategies

- **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).
must be a merge commit whose subject names a `feature/` (minor),
`bugfix/`, or `hotfix/` (patch) branch.
- **conventional-commits**: parses the head commit's
[Conventional Commits](https://www.conventionalcommits.org/)
header (`feat:` minor, `fix:`/`perf:` patch, `!` or `BREAKING
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ inputs:

outputs:
tag:
description: 'The created tag (e.g. v1.2.3), or empty string if no bump was warranted.'
description: 'The created tag (e.g. 1.2.3), or empty string if no bump was warranted.'
value: ${{ steps.run.outputs.tag }}
bump:
description: 'The computed bump: none | patch | minor | major.'
Expand Down
4 changes: 4 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ semvertag:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
```

semvertag bumps from the highest existing semver tag and never creates
the first one. Before the first run, create a plain semver tag such as
`0.1.0` (a `v` prefix does not parse and is ignored).

For local testing or one-off invocations:

```sh
Expand Down
20 changes: 13 additions & 7 deletions docs/providers/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ 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.

> **First tag.** semvertag bumps from the highest existing semver tag
> and never creates the first one. It reads only plain semver tags such
> as `0.1.0`; a `v` prefix (`v0.1.0`) does not parse and is ignored. Until
> one exists, every run reports `no_tags` and exits 0. Push one with
> `git tag 0.1.0 && git push origin 0.1.0`.

> **Auto-detection.** semvertag detects GitHub Actions from the
> `GITHUB_ACTIONS=true` env var that GHA sets automatically. The
> `--provider` flag is therefore optional inside GHA — explicit
Expand Down Expand Up @@ -92,7 +98,7 @@ When you give the step an `id:`, downstream steps can read three outputs:

| Output | Value |
|---|---|
| `tag` | The created tag (e.g. `v1.2.3`), or empty string when `status` is `no-bump`. |
| `tag` | The created tag (e.g. `1.2.3`), or empty string when `status` is `no-bump`. |
| `bump` | `none` \| `patch` \| `minor` \| `major`. |
| `status` | `created` (tag pushed) \| `no-bump` (nothing to tag — no prior tag, already tagged, no merge commit, or non-conforming commit). On CLI error the action itself exits non-zero and this output is not written. |

Expand Down Expand Up @@ -189,12 +195,12 @@ 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 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
follow a `feature/...`, `bugfix/...`, `hotfix/...` convention and lands
them as merge commits. semvertag reads the head commit's source-branch
prefix and bumps accordingly: `feature/` bumps minor, `bugfix/` and
`hotfix/` bump patch, and any other prefix 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.

Expand Down
15 changes: 11 additions & 4 deletions docs/providers/gitlab.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ 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.

> **First tag.** semvertag bumps from the highest existing semver tag
> and never creates the first one. It reads only plain semver tags such
> as `0.1.0`; a `v` prefix (`v0.1.0`) does not parse and is ignored. Until
> one exists, every run reports `no_tags` and exits 0. Push one with
> `git tag 0.1.0 && git push origin 0.1.0`.

> **Concurrency default.** `resource_group: semvertag` makes GitLab
> serialize concurrent `semvertag` jobs across pipelines on the same
> project — back-to-back pushes will queue rather than race the
Expand Down Expand Up @@ -117,10 +123,11 @@ 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
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
names that follow a `feature/...`, `bugfix/...`, `hotfix/...`
convention and lands them as merge commits. semvertag reads the head
commit's source-branch prefix and bumps accordingly: `feature/` bumps
minor, `bugfix/` and `hotfix/` bump patch, and any other prefix 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
Expand Down
6 changes: 4 additions & 2 deletions semvertag/_outcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
# These are the JSON wire reasons. The human terminal path (_output._format_outcome)
# words NoTags/AlreadyTagged differently on purpose — edit both if you change the
# message for one audience.
_NO_TAGS_REASON: typing.Final = "No prior semver-conforming tags found; not seeding an initial tag in v1.0."
_NO_TAGS_REASON: typing.Final = (
"No prior semver-conforming tags found; create an initial tag such as 0.1.0 on a default-branch commit."
)
_ALREADY_TAGGED_REASON: typing.Final = "Latest commit already tagged."


Expand All @@ -31,7 +33,7 @@ class DryRun:

@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
class NoTags:
"""No prior semver tag to bump from; v1.0 does not seed one."""
"""No prior semver tag to bump from; semvertag does not create the first one."""

commit: str

Expand Down
5 changes: 4 additions & 1 deletion semvertag/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ def _format_outcome(outcome: Outcome, *, strategy: str) -> str:
short = commit[:_COMMIT_SHORT_LEN]
return f"Dry run: would create tag {tag} on commit {short} (strategy: {strategy}, bump: {bump.value})"
case NoTags():
return "No tag created — no prior semver-conforming tag to bump from."
return (
"No tag created — no prior semver-conforming tag to bump from; "
"create an initial tag such as 0.1.0 on a default-branch commit."
)
case AlreadyTagged(tag=tag):
return f"No tag created — latest commit is already tagged {tag}."
case NoBump(reason=reason):
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/test_outcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ def test_no_tags_maps_with_none_bump_and_fixed_reason() -> None:
)


def test_no_tags_reason_says_how_to_seed_the_first_tag() -> None:
reason: typing.Final = to_run_result(NoTags(commit=_COMMIT), strategy=_STRATEGY).reason
assert reason is not None
assert "create an initial tag such as 0.1.0" in reason
assert "v1.0" not in reason


def test_already_tagged_maps_with_tag_and_fixed_reason() -> None:
result: typing.Final = to_run_result(AlreadyTagged(tag="0.3.1", commit=_COMMIT), strategy=_STRATEGY)
assert result == RunResult(
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/test_output_rich.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def test_matrix_keeps_stderr_for_errors(quiet: bool) -> None:
("outcome", "expected"),
[
(NoTags(commit="abc1234def"), "no prior semver-conforming tag"),
(NoTags(commit="abc1234def"), "create an initial tag such as 0.1.0"),
(AlreadyTagged(tag="1.2.0", commit="abc1234def"), "already tagged 1.2.0"),
(
NoBump(status="no_merge_commit", reason="Latest commit is not a merge commit.", commit="abc1234def"),
Expand All @@ -105,7 +106,7 @@ def test_matrix_keeps_stderr_for_errors(quiet: bool) -> None:
def test_emit_renders_no_bump_outcomes_as_human_sentences(outcome: Outcome, expected: str) -> None:
output, stdout_buf, _stderr = _make_pair()
output.emit(outcome, strategy=_STRATEGY)
stdout_text: typing.Final = stdout_buf.getvalue()
stdout_text: typing.Final = " ".join(stdout_buf.getvalue().split())
assert "No tag created" in stdout_text
assert expected in stdout_text

Expand Down
Loading