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
9 changes: 9 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Description

## Checklist

Please confirm that this pull request has done the following:

- [ ] Tests added
- [ ] Documentation added (where applicable)
- [ ] Changelog item added to `changelog/`
7 changes: 7 additions & 0 deletions .github/release_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Announcements

* Announcement 1

## Changes

* Change 1
51 changes: 43 additions & 8 deletions .github/workflows/bump.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,27 @@ 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.
required: false
type: string
default: "uv"
bump-rule:
description: |
Whitespace-separated list of segments passed to uv version --bump.
Each segment becomes a separate --bump.
Examples: "patch", "minor", "major", "stable", "minor alpha", "patch rc".
description: |
For project-type 'uv': whitespace-separated list of segments passed to
uv version --bump. Each segment becomes a separate --bump.

Examples: "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 project types: passed through verbatim as arguments to
`npm version <bump-rule words...>`, e.g. "patch" or "preminor --preid alpha".
required: true
type: string
pre-release-base:
Expand Down Expand Up @@ -60,7 +73,10 @@ on:
type: boolean
default: true
workspace-packages:
description: "Newline-separated uv workspace packages to mirror the version onto."
description: >
Newline-separated workspace packages to mirror the version onto: uv package
names for project-type 'uv', or paths relative to the repo root for the
Node project types.
required: false
type: string
default: ""
Expand All @@ -75,7 +91,12 @@ on:
type: string
default: ""
python-version:
description: "Python version to install via setup-uv."
description: "Python version to install via setup-uv. Also used to run towncrier (via uvx) for the Node project types."
required: false
type: string
default: ""
node-version:
description: "Node version to install via actions/setup-node. Only used for the Node project types."
required: false
type: string
default: ""
Expand Down Expand Up @@ -167,10 +188,24 @@ jobs:
with:
python-version: ${{ inputs.python-version }}

- name: Setup Node
if: contains(fromJSON('["yarn", "npm", "pnpm"]'), inputs.project-type)
uses: actions/setup-node@v7.0.0
with:
node-version: ${{ inputs.node-version }}

# corepack provisions yarn and pnpm from the project's packageManager
# field. npm ships with Node, so it needs nothing here.
- name: Enable corepack
if: contains(fromJSON('["yarn", "pnpm"]'), inputs.project-type)
shell: bash
run: corepack enable

- name: Bump version
id: bump
uses: climate-resource/github-actions/bump-version@v1.4.3
with:
project-type: ${{ inputs.project-type }}
bump-rule: ${{ inputs.bump-rule }}
pre-release-bump: ${{ inputs.pre-release-bump }}
pre-release-base: ${{ inputs.pre-release-base }}
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Tests

on:
pull_request:
paths:
- "**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/tests.yml"
push:
branches:
- main
paths:
- "**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/tests.yml"

permissions:
contents: read

jobs:
pytest:
runs-on: gus-small
steps:
- name: Check out repository
uses: actions/checkout@v6.0.2

- name: Setup uv
uses: ./setup-uv

- name: Run tests
shell: bash
run: uv run --group dev pytest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
.DS_Store
__pycache__/
.pytest_cache/
.venv/
135 changes: 102 additions & 33 deletions bump-version/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,52 @@
# bump-version

Composite action that bumps a Python project's version with `uv version --bump`,
updates the CHANGELOG via towncrier, commits, tags, pushes, and (by default)
lands a follow-up commit that moves `main` onto a pre-release version so future commits don't share the tagged version.
Composite action that bumps a project's version, updates the CHANGELOG via
towncrier, commits, tags, pushes, and (by default) lands a follow-up commit that
moves the branch onto a pre-release version so future commits don't share the
tagged version.

## Prerequisites
`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` |

The action assumes:
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.

- `uv` is on PATH (use [`setup-uv`](../setup-uv) before this action).
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).

## Prerequisites

- **`uv` is on PATH for all project types** (use [`setup-uv`](../setup-uv)
before this action). It runs the script itself, so it is required even for
the Node project types, where it also provides towncrier via `uvx`.
- For the Node project types, `node` and `npm` must be on PATH, plus `yarn` or
`pnpm` if `lock: true` (use `actions/setup-node`, and `corepack enable` for
yarn/pnpm).
- The checked-out repo has full history (`fetch-depth: 0`).
The default `GITHUB_TOKEN` is sufficient to push the bump commit and tag.
The default `GITHUB_TOKEN` is sufficient to push the bump commit and tag.
Check out with a PAT only when branch protection blocks the `github-actions` bot,
or when downstream workflows must fire from the tag push (a push made with `GITHUB_TOKEN` does not trigger other workflows).
- The project's `pyproject.toml` is managed by `uv` (i.e. `uv version --short` returns the current version).
- `towncrier` is available via `uv run` when `update-changelog: true` (the default).

## Inputs

| Input | Default | Description |
| --- | --- | --- |
| `bump-rule` | _required_ | Whitespace-separated list of segments passed to `uv version --bump`. Each segment becomes a separate `--bump`. Examples: `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. |
| `pre-release-bump` | `dev` | Pre-release segment for the second commit (`dev`, `alpha`, `beta`, `rc`). Use `none` to skip the second commit. |
| `pre-release-base` | `patch` | Base bump applied before the pre-release segment in the second commit. Use `none` to add the pre-release marker without bumping the base. |
| `update-changelog` | `true` | Run `uv run towncrier build` for the new version. |
| `project-type` | `uv` | `uv` for a Python project; `yarn`, `npm` or `pnpm` for a Node project. See the table above. |
| `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. |
| `update-changelog` | `true` | Build the CHANGELOG with towncrier for the new version. |
| `commit-email` | `ci-runner@climate-resource.invalid` | Author email for both commits. |
| `workspace-packages` | _empty_ | Newline-separated workspace package names to mirror the version onto. |
| `lock` | `true` | Run `uv lock` after each version change. |
| `workspace-packages` | _empty_ | Newline-separated workspace packages to mirror the version onto. For `uv`: package names. For the Node types: directory paths relative to the repo root, e.g. `apps/analysis-portal`. |
| `lock` | `true` | Refresh the lockfile after each version change, using the command for the project type (see the table above). |
| `pre-commit-command` | _empty_ | Shell command run after the changelog build and before each bump commit. Use it to regenerate version-derived files (e.g. an OpenAPI schema) so they stay in sync in the tagged commit. The command must succeed; a non-zero exit aborts the bump. |
| `pre-commit-skip` | `false` | Pass `-n` to `git commit` to bypass pre-commit hooks. |
| `push` | `true` | Push the bump commit, tag, and pre-release commit. |
Expand All @@ -38,27 +58,40 @@ The action assumes:
| `base-version` | Version before the bump. |
| `new-version` | Tagged version (no `v` prefix). |
| `tag` | New git tag, with `v` prefix. |
| `dev-version` | Pre-release version landed on `main` after tagging (when applicable). |
| `is-prerelease` | `'true'` when the tagged version matches `(a\|b\|rc\|dev)`. Use to gate downstream release steps. |
| `dev-version` | Pre-release version landed on the branch after tagging (when applicable). |
| `is-prerelease` | `'true'` when the tagged version is a pre-release. Use to gate downstream release steps. |

`base-version`, `new-version`, `tag` and `is-prerelease` are written as soon as
the release is tagged, so they remain available to later steps even if the
pre-release commit fails.

## Behaviour

1. Reads the current version (`BASE_VERSION = uv version --short`).
2. `uv version --frozen --bump <bump-rule>` to obtain `NEW_VERSION`.
3. Mirrors `NEW_VERSION` to each `workspace-packages` entry.
4. If `update-changelog: true`, runs
`uv run towncrier build --yes --version v$NEW_VERSION`.
5. Optionally `uv lock`.
6. If `pre-commit-command` is set, runs it (so version-derived files are
regenerated before the commit).
7. `git commit -a -m "bump: version $BASE_VERSION -> $NEW_VERSION"`.
8. `git tag v$NEW_VERSION`.
9. Pushes the commit and tag.
10. If `pre-release-bump != none` and the tagged version is not already a
pre-release, applies `pre-release-base` (default `patch`) then
`pre-release-bump` (default `dev`), mirrors workspace packages, locks, runs
`pre-commit-command` again, and creates a `bump(<pre-release-bump>): ...`
commit which is pushed.
**Phase 1 — tag the release**

1. Read the current version (`base-version`).
2. Apply `bump-rule` to the root project to obtain `new-version`.
3. Mirror `new-version` onto each `workspace-packages` entry.
4. Refresh the lockfile, if `lock: true`.
5. Build the CHANGELOG with towncrier, if `update-changelog: true`.
6. Run `pre-commit-command`, if set.
7. `git commit -a -m "bump: version <base-version> -> <new-version>"`.
8. `git tag v<new-version>`, then push the commit and tag if `push: true`.

**Phase 2 — land the pre-release**

Skipped entirely when `pre-release-bump: none`, or when the tagged version is
already a pre-release. Otherwise it applies `pre-release-base` then
`pre-release-bump`, repeats the mirror / lock / `pre-commit-command` steps, and
creates a `bump(<label>): ...` commit which is pushed.

The `<label>` scope is the pre-release identifier, not the raw input. For `uv`
that is the `pre-release-bump` word itself (`bump(dev)`); for Node it is read
back off the version `npm version` produced, so `--preid dev` landing on
`1.2.5-dev.0` gives `bump(dev)` rather than `bump(--preid dev)`.

Pre-release detection is version-scheme aware: PEP 440 for `uv` (via
`packaging`), semver for the Node types.

## Example

Expand All @@ -71,3 +104,39 @@ The action assumes:
bookshelf
bookshelf-producer
```

A Node project needs Node on PATH as well as uv:

```yaml
- uses: climate-resource/github-actions/setup-uv@v1
- uses: actions/setup-node@v7.0.0
with:
node-version: "22"
- run: corepack enable
shell: bash
- uses: climate-resource/github-actions/bump-version@v1
with:
project-type: yarn
bump-rule: patch
pre-release-base: prepatch
pre-release-bump: --preid dev
workspace-packages: |
apps/analysis-portal
```

## Developing

The script is a self-contained [PEP 723](https://peps.python.org/pep-0723/)
script, so it can be run directly against a scratch repository:

```bash
cd /path/to/a/throwaway/clone
PROJECT_TYPE=uv BUMP_RULE=patch UPDATE_CHANGELOG=false DO_PUSH=false \
uv run --script /path/to/github-actions/bump-version/bump.py
```

Unit tests cover argument assembly, pre-release detection and input parsing:

```bash
uv run --group dev pytest
```
Loading