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
40 changes: 29 additions & 11 deletions .github/workflows/bump.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Reusable workflow: bump version (optionally with changelog) and optionally
# draft a GitHub release in the same dispatch.
#
# An optional follow-up commit can be made to bump the version onto a pre-release (e.g. 1.2.3 -> 1.3.0a1) after tagging the stable release (1.2.3).
# An optional follow-up commit can be made to bump the version onto a pre-release (e.g. 1.2.3 -> 1.3.0a1) after tagging the stable release (1.2.3).
# This is useful to differentiate between the tagged release commit and the post-release commit on main.
# The default is to make a "patch dev" post-release commit (e.g. 1.2.3 -> 1.2.4.dev0).
#
Expand All @@ -15,24 +15,41 @@
# with:
# bump-rule: ${{ inputs.bump_rule }}
#
# Projects whose version comes from git tags (e.g. hatch-vcs) pass `dynamic-versioning: true`,
# which tags without editing any manifest and lands no post-tag commit.
#
# A PAT is only needed when branch protection blocks the github-actions bot or
# when the release is auto-published and downstream workflows must fire. In
# that case pass it as `secrets: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}`.
# when the release is auto-published and downstream workflows must fire.
# In that case pass it as `secrets: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}`.

name: Bump
on:
workflow_call:
inputs:
project-type:
description: >
Kind of project to bump, named after the package manager that owns the
lockfile: 'uv' (Python), or 'yarn' / 'npm' / 'pnpm' (Node). The Node types
behave identically apart from which lockfile is refreshed. See the
bump-version action for how bump-rule/pre-release-base/pre-release-bump
differ per type.
Kind of project to bump, named after the package manager that owns the lockfile:
'uv' (Python), or 'yarn' / 'npm' / 'pnpm' (Node).

The Node types behave identically apart from which lockfile is refreshed.
See the bump-version action for how bump-rule/pre-release-base/pre-release-bump differ per type.
required: false
type: string
default: "uv"
dynamic-versioning:
description: |
Take the base version from the latest reachable v* tag rather than from a manifest,
for projects whose version is derived from git tags (e.g. hatch-vcs).
With no such tag the base is 0.0.0.

No manifest is rewritten and no lockfile is refreshed,
so workspace-packages does not apply and neither does the post-tag commit,
which makes pre-release-bump and pre-release-base inert.

Only project-type 'uv' is supported.
required: false
type: boolean
default: false
bump-rule:
description: |
For project-type 'uv': whitespace-separated list of segments passed to
Expand All @@ -51,7 +68,7 @@ on:
Base bump applied before the pre-release segment (patch|minor|major|none).
This is useful to land the post-tag commit on a more appropriate version.

i.e. from 1.2.3, a pre-release-base of minor and pre-release-bump of alpha would land on 1.3.0a0,
i.e. from 1.2.3, a pre-release-base of minor and pre-release-bump of alpha would land on 1.3.0a0,
while a pre-release-base of none would land on 1.2.4a1.

Use 'none' to skip the base bump and only add the pre-release segment.
Expand Down Expand Up @@ -144,8 +161,8 @@ on:
token:
description: >
Optional PAT used to push commits/tags and create the release.
Defaults to the workflow's GITHUB_TOKEN.
Provide a PAT when the repo has branch protection that blocks the github-actions bot,
Defaults to the workflow's GITHUB_TOKEN.
Provide a PAT when the repo has branch protection that blocks the github-actions bot,
or when the release is auto-published and downstream `release: published` workflows must fire.
required: false
outputs:
Expand Down Expand Up @@ -206,6 +223,7 @@ jobs:
uses: climate-resource/github-actions/bump-version@v1.5.1
with:
project-type: ${{ inputs.project-type }}
dynamic-versioning: ${{ inputs.dynamic-versioning }}
bump-rule: ${{ inputs.bump-rule }}
pre-release-bump: ${{ inputs.pre-release-bump }}
pre-release-base: ${{ inputs.pre-release-base }}
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,31 @@ jobs:
dist/*
```

### Dynamically versioned projects (hatch-vcs)

Projects whose version is derived from git tags hold no version to edit, so the tag is the release:

```yaml
jobs:
bump:
uses: climate-resource/github-actions/.github/workflows/bump.yaml@v1
with:
dynamic-versioning: true
bump-rule: ${{ inputs.bump_rule }}
create-release: true
build-command: "uv build"
release-files: |
dist/*
```

The base version is the latest reachable `v*` tag,
and `bump-rule` is applied to it exactly as it would be to a version read from `pyproject.toml`.
Nothing is written to `pyproject.toml`, no lockfile is refreshed, and no post-tag commit is landed,
so `pre-release-bump`, `pre-release-base` and `workspace-packages` are ignored.
The changelog build and `pre-commit-command` still run, and their changes land in the tagged commit.



### Skipping the changelog or the pre-release dev commit

```yaml
Expand Down
62 changes: 56 additions & 6 deletions bump-version/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ tagged version.
`project-type` selects the backend, and is named after the package manager that
owns the lockfile:

| `project-type` | Version lives in | Bumped with | `lock: true` runs |
| --- | --- | --- | --- |
| `uv` (default) | `pyproject.toml` | `uv version --bump` | `uv lock` |
| `yarn` | `package.json` | `npm version` | `yarn install --mode=update-lockfile` |
| `npm` | `package.json` | `npm version` | `npm install --package-lock-only` |
| `pnpm` | `package.json` | `npm version` | `pnpm install --lockfile-only` |
| `project-type` | Version lives in | Bumped with | `lock: true` runs |
| -------------- | ---------------- | ------------------- | ------------------------------------- |
| `uv` (default) | `pyproject.toml` | `uv version --bump` | `uv lock` |
| `yarn` | `package.json` | `npm version` | `yarn install --mode=update-lockfile` |
| `npm` | `package.json` | `npm version` | `npm install --package-lock-only` |
| `pnpm` | `package.json` | `npm version` | `pnpm install --lockfile-only` |

The three Node types behave identically apart from that last column. `npm
version` is used purely as a version-bumping CLI — it ships with Node, and the
package manager you named stays in charge of the lockfile.

Projects that derive their version from git tags (e.g. hatch-vcs) set `dynamic-versioning: true` instead.
The tag is then the only thing that carries the version, so no manifest is touched.
See [Dynamic versioning](#dynamic-versioning) below.

The logic lives in [`bump.py`](bump.py), which `action.yml` invokes with
`uv run --script`. Its unit tests are in [`../tests/test_bump.py`](../tests/test_bump.py).

Expand All @@ -40,6 +44,7 @@ The logic lives in [`bump.py`](bump.py), which `action.yml` invokes with
| Input | Default | Description |
| --- | --- | --- |
| `project-type` | `uv` | `uv` for a Python project; `yarn`, `npm` or `pnpm` for a Node project. See the table above. |
| `dynamic-versioning` | `false` | Take the base version from the latest reachable `v*` tag rather than from a manifest. `uv` projects only. See [Dynamic versioning](#dynamic-versioning). |
| `bump-rule` | _required_ | Whitespace-separated arguments describing the bump. For `uv`, each segment becomes a separate `--bump`: `patch`, `minor`, `major`, `stable`, `minor alpha`, `patch rc`. From a stable version, prerelease segments (`alpha`, `beta`, `rc`, `dev`) must be combined with a release segment. For the Node types, passed verbatim to `npm version`: `patch`, `preminor --preid alpha`, `prerelease --preid rc`. |
| `pre-release-bump` | `dev` | Pre-release segment for the second commit. For `uv`: `dev`, `alpha`, `beta`, `rc`. For the Node types: verbatim `npm version` arguments, e.g. `--preid dev`. Use `none` to skip the second commit. |
| `pre-release-base` | `patch` | Base bump applied before the pre-release segment in the second commit. For `uv`: a bump rule (`patch`, `minor`, `major`, …). For the Node types: an `npm version` strategy word, e.g. `prepatch`. Use `none` to add the pre-release marker without bumping the base. |
Expand Down Expand Up @@ -93,6 +98,51 @@ back off the version `npm version` produced, so `--preid dev` landing on
Pre-release detection is version-scheme aware: PEP 440 for `uv` (via
`packaging`), semver for the Node types.

## Dynamic versioning

With `dynamic-versioning: true` the version is not stored anywhere in the repo,
so the base is the highest `v*` tag reachable from `HEAD`, with the leading `v` stripped.
A repo with no such tag starts from `0.0.0`.

Tags are ranked by PEP 440.
A tag that is not a valid version, such as `vendor-3`, is ignored rather than treated as one.

`bump-rule` means exactly what it means in static mode (`uv version --bump` is still used to calculate the tag).
So `patch`, `minor`, `major`, `stable` and the pre-release segments (`alpha`, `beta`, `rc`, `dev`)
all behave as they usually do, and an unsupported combination fails with uv's own message.

What changes:

- `pyproject.toml` or the lockfile is not edited as it isn't needed,
so `workspace-packages` and `lock` do not apply.
- There is no post-tag commit, `pre-release-bump` and `pre-release-base` are ignored,
and `dev-version` is empty.
- The commit is made only if something is left to commit,
which in practice means the changelog build or `pre-commit-command` changed a tracked file.
With nothing to commit the tag lands on `HEAD` rather than on an empty commit.

`update-changelog` and `pre-commit-command` work as they do in static mode.

```yaml
- uses: climate-resource/github-actions/setup-uv@v1
- uses: climate-resource/github-actions/bump-version@v1
with:
dynamic-versioning: true
bump-rule: ${{ inputs.bump_rule }}
```

### Gotchas

The tag is the only record of the version, so anything that muddies the tags muddies the release.

- Releasing twice from one commit is refused, because hatch-vcs would read the lower tag.
Moving aliases such as `v1` or `v1.3` trip the same guard, so a repo that publishes them cannot use `dynamic-versioning`.
- Every package in the repo shares one version, because they all read the same tag.
A release moves them together and no single member can be released on its own.
- A member in a subdirectory needs `raw-options = { search_parent_directories = true }`
under `[tool.hatch.version]`, or hatch-vcs fails to find the repository at all.
This configuration is needed for monorepos.

## Example

```yaml
Expand Down
17 changes: 17 additions & 0 deletions bump-version/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ description: >
update the CHANGELOG via towncrier, commit, tag, push,
and optionally land a second commit bumping main onto a pre-release version.

Projects that derive their version from git tags (e.g. hatch-vcs) set
`dynamic-versioning` and are released by the tag alone.

inputs:
project-type:
description: >
Expand All @@ -16,6 +19,19 @@ inputs:
differ only in which lockfile the `lock` input refreshes.
required: false
default: "uv"
dynamic-versioning:
description: >
If 'true', take the base version from the latest reachable `v*` tag rather than from a manifest,
for projects whose version is derived from git tags (e.g. hatch-vcs).
With no such tag the base is 0.0.0.

No manifest is rewritten and no lockfile is refreshed, so `workspace-packages` and `lock` do not apply.
The `pre-release-bump` and `pre-release-base` flags are ignored as they do not apply.
Only project-type 'uv' is supported.

The checkout needs the tags, so use `fetch-depth: 0` when calling this action directly.
required: false
default: "false"
bump-rule:
description: >
Whitespace-separated arguments describing the bump.
Expand Down Expand Up @@ -132,6 +148,7 @@ runs:
shell: bash
env:
PROJECT_TYPE: ${{ inputs.project-type }}
DYNAMIC_VERSIONING: ${{ inputs.dynamic-versioning }}
BUMP_RULE: ${{ inputs.bump-rule }}
PRE_RELEASE_BUMP: ${{ inputs.pre-release-bump }}
PRE_RELEASE_BASE: ${{ inputs.pre-release-base }}
Expand Down
Loading