Skip to content

Commit af3bd2a

Browse files
committed
feat: consolidate version source of truth and add release tooling
- Make pyproject.toml the single source of truth for version - Use importlib.metadata in __init__.py to read version at runtime - Add scripts/bump_version.py (stdlib-only) for version bumping - Add Makefile targets: version, bump-patch, bump-minor, bump-major, release - Add RELEASE.md documenting the full release process - Update README.md with new Makefile targets and link to RELEASE.md - Replace hardcoded version assertion in tests with dynamic check - Remove stale [tool.hatch.build.targets.wheel] from pyproject.toml - Bump version to 0.3.1
1 parent 514d86c commit af3bd2a

File tree

7 files changed

+193
-25
lines changed

7 files changed

+193
-25
lines changed

Makefile

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: test lint format typecheck coverage build clean help completions-bash completions-zsh completions-fish
1+
.PHONY: test lint format typecheck coverage build clean help completions-bash completions-zsh completions-fish version bump-patch bump-minor bump-major release
22

33
help: ## Show this help message
44
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
@@ -28,6 +28,30 @@ clean: ## Remove build artifacts
2828
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
2929
find . -type f -name '*.pyc' -delete 2>/dev/null || true
3030

31+
version: ## Show current version
32+
@uv run python -c "from importlib.metadata import version; print(version('worktreeflow'))"
33+
34+
bump-patch: ## Bump patch version (e.g. 0.3.0 → 0.3.1)
35+
@uv run python scripts/bump_version.py patch
36+
37+
bump-minor: ## Bump minor version (e.g. 0.3.0 → 0.4.0)
38+
@uv run python scripts/bump_version.py minor
39+
40+
bump-major: ## Bump major version (e.g. 0.3.0 → 1.0.0)
41+
@uv run python scripts/bump_version.py major
42+
43+
release: ## Bump, sync, commit, tag (usage: make release BUMP=patch)
44+
@BUMP=$${BUMP:-patch}; \
45+
$(MAKE) bump-$$BUMP; \
46+
uv sync; \
47+
VERSION=$$(uv run python -c "from importlib.metadata import version; print(version('worktreeflow'))"); \
48+
git add pyproject.toml; \
49+
git commit -m "chore: bump version to $$VERSION"; \
50+
git tag "v$$VERSION"; \
51+
echo ""; \
52+
echo "Created tag v$$VERSION."; \
53+
echo "Run 'git push && git push --tags' to publish."
54+
3155
completions-bash: ## Generate bash completions
3256
_WTF_COMPLETE=bash_source uv run wtf
3357

README.md

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ make typecheck # Run ty type checking
163163
make coverage # Run tests with coverage (35% threshold)
164164
make build # Build the package
165165
make clean # Remove build artifacts
166+
make version # Show current version
167+
make bump-patch # Bump patch version (0.3.0 → 0.3.1)
168+
make bump-minor # Bump minor version (0.3.0 → 0.4.0)
169+
make bump-major # Bump major version (0.3.0 → 1.0.0)
170+
make release # Bump, commit, tag (usage: make release BUMP=minor)
166171
```
167172

168173
### Shell Completions
@@ -182,27 +187,17 @@ _WTF_COMPLETE=fish_source wtf | source
182187

183188
Or generate completion scripts via `make completions-bash`, `make completions-zsh`, or `make completions-fish`.
184189

185-
### Building and Publishing
190+
### Versioning and Releasing
186191

187-
1. **Build the package**:
188-
```bash
189-
uv build
190-
```
191-
This creates `dist/worktreeflow-0.1.0-py3-none-any.whl` and `dist/worktreeflow-0.1.0.tar.gz`
192-
193-
2. **Test locally**:
194-
```bash
195-
uv pip install dist/worktreeflow-0.1.0-py3-none-any.whl
196-
wtf --help
197-
```
192+
Version bumping, tagging, and publishing are documented in **[RELEASE.md](RELEASE.md)**.
198193

199-
3. **Publish to PyPI**:
200-
```bash
201-
# First time: Get API token from https://pypi.org/manage/account/token/
202-
# Then set it in your environment or use --token flag
194+
Quick reference:
203195

204-
uv publish
205-
```
196+
```bash
197+
make version # Show current version
198+
make bump-patch # Bump patch version
199+
make release # Bump, commit, and tag (then push manually)
200+
```
206201

207202
### Dual Functionality
208203

RELEASE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Release Process
2+
3+
## Versioning
4+
5+
worktreeflow follows [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`).
6+
7+
The **single source of truth** for the version is `pyproject.toml`. The Python package reads the version at runtime via `importlib.metadata`, so there is only one place to update.
8+
9+
## Bump Version
10+
11+
Use the Makefile targets to bump the version:
12+
13+
```bash
14+
# Show current version
15+
make version
16+
17+
# Bump patch (0.3.0 → 0.3.1) — bug fixes
18+
make bump-patch
19+
20+
# Bump minor (0.3.0 → 0.4.0) — new features, backwards compatible
21+
make bump-minor
22+
23+
# Bump major (0.3.0 → 1.0.0) — breaking changes
24+
make bump-major
25+
```
26+
27+
After bumping, run `uv sync` to refresh the editable install so `wtf version` reflects the new version.
28+
29+
## Full Release
30+
31+
The `make release` target automates: bump → sync → commit → tag.
32+
33+
```bash
34+
# Patch release (default)
35+
make release
36+
37+
# Minor release
38+
make release BUMP=minor
39+
40+
# Major release
41+
make release BUMP=major
42+
```
43+
44+
This will:
45+
1. Bump the version in `pyproject.toml`
46+
2. Run `uv sync` to update the local install
47+
3. Commit the version change
48+
4. Create a git tag (e.g., `v0.4.0`)
49+
50+
It does **not** push automatically. Review the commit and tag, then:
51+
52+
```bash
53+
git push && git push --tags
54+
```
55+
56+
## Publishing to PyPI
57+
58+
### Automated (Recommended)
59+
60+
Publishing is handled by GitHub Actions. The workflow (`.github/workflows/publish.yml`) triggers when a **GitHub Release** is published:
61+
62+
1. Push the version bump and tag (see above)
63+
2. Go to [GitHub Releases](https://github.com/smorinlabs/worktreeflow/releases)
64+
3. Click **Draft a new release**
65+
4. Select the tag you just pushed (e.g., `v0.4.0`)
66+
5. Add release notes
67+
6. Click **Publish release**
68+
69+
The CI will automatically:
70+
- Run the test suite
71+
- Build the package with `uv build`
72+
- Publish to PyPI via OIDC (trusted publisher)
73+
74+
### Manual
75+
76+
If you need to publish manually:
77+
78+
```bash
79+
uv build
80+
uv publish # requires PyPI API token
81+
```
82+
83+
## Pre-Release Checklist
84+
85+
- [ ] All tests pass (`make test`)
86+
- [ ] Linting passes (`make lint`)
87+
- [ ] Type checking passes (`make typecheck`)
88+
- [ ] Version bumped (`make bump-patch` / `bump-minor` / `bump-major`)
89+
- [ ] `uv sync` run after bump
90+
- [ ] `wtf version` shows correct version
91+
- [ ] Changes committed and tagged (`make release`)
92+
- [ ] Pushed to remote (`git push && git push --tags`)
93+
- [ ] GitHub Release created

pyproject.toml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "uv_build"
44

55
[project]
66
name = "worktreeflow"
7-
version = "0.3.0"
7+
version = "0.3.1"
88
description = "Git workflow manager for feature branches using worktrees, shows you cli equivalent commands"
99
readme = "README.md"
1010
requires-python = ">=3.11"
@@ -78,6 +78,3 @@ exclude_lines = [
7878
"if __name__ == .__main__.",
7979
"if TYPE_CHECKING:",
8080
]
81-
82-
[tool.hatch.build.targets.wheel]
83-
packages = ["src/worktreeflow"]

scripts/bump_version.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python3
2+
"""Bump the version in pyproject.toml. Usage: python scripts/bump_version.py [patch|minor|major]"""
3+
4+
import re
5+
import sys
6+
from pathlib import Path
7+
8+
PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
9+
VERSION_RE = re.compile(r'(version\s*=\s*")(\d+)\.(\d+)\.(\d+)(")')
10+
11+
12+
def bump(part: str) -> None:
13+
text = PYPROJECT.read_text()
14+
m = VERSION_RE.search(text)
15+
if not m:
16+
print("ERROR: Could not find version in pyproject.toml", file=sys.stderr)
17+
sys.exit(1)
18+
19+
major, minor, patch = int(m.group(2)), int(m.group(3)), int(m.group(4))
20+
old = f"{major}.{minor}.{patch}"
21+
22+
if part == "patch":
23+
patch += 1
24+
elif part == "minor":
25+
minor += 1
26+
patch = 0
27+
elif part == "major":
28+
major += 1
29+
minor = 0
30+
patch = 0
31+
else:
32+
print(f"ERROR: Unknown bump part '{part}'. Use patch, minor, or major.", file=sys.stderr)
33+
sys.exit(1)
34+
35+
new = f"{major}.{minor}.{patch}"
36+
text = VERSION_RE.sub(rf"\g<1>{new}\5", text, count=1)
37+
PYPROJECT.write_text(text)
38+
print(f"{old} -> {new}")
39+
40+
41+
if __name__ == "__main__":
42+
if len(sys.argv) != 2:
43+
print("Usage: python scripts/bump_version.py [patch|minor|major]", file=sys.stderr)
44+
sys.exit(1)
45+
bump(sys.argv[1])

src/worktreeflow/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
worktreeflow - Git workflow manager for feature branches using worktrees
33
"""
44

5-
__version__ = "0.3.0"
5+
try:
6+
from importlib.metadata import version as _metadata_version
7+
8+
__version__ = _metadata_version("worktreeflow")
9+
except Exception:
10+
__version__ = "0.0.0+unknown"
11+
612
__author__ = "Steve Morin"
713
__license__ = "MIT"
814

tests/test_new_features.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,19 @@ def _make_manager(dry_run=False, config=None, json_output=False):
4040

4141
class TestVersionCommand:
4242
def test_version_output(self):
43+
from importlib.metadata import version as _meta_version
44+
4345
runner = CliRunner()
4446
result = runner.invoke(cli, ["version"])
4547
assert result.exit_code == 0
4648
assert "worktreeflow" in result.output
47-
assert "0.3.0" in result.output
49+
assert _meta_version("worktreeflow") in result.output
50+
51+
def test_version_is_not_unknown(self):
52+
from worktreeflow import __version__
53+
54+
assert __version__ != "0.0.0+unknown"
55+
assert "." in __version__
4856

4957
def test_version_in_help(self):
5058
runner = CliRunner()

0 commit comments

Comments
 (0)