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
126 changes: 90 additions & 36 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,52 @@
name: comlink-python release

# Tag-driven release.
#
# The package version is derived from the Git tag at build time via hatch-vcs
# (see [tool.hatch.version] in pyproject.toml), so a release does NOT commit a
# version bump to the repository. This keeps the workflow compatible with the
# protected `main` branch (which forbids direct pushes).
#
# Ordering is build -> publish -> tag so the Git tag / GitHub Release is created
# only after a successful PyPI publish, which avoids leaving an orphaned tag
# behind if the build or upload fails.

on:
workflow_dispatch:
inputs:
bump:
description: "Semantic version bump from the latest release tag"
type: choice
options:
- patch
- minor
- major
default: minor

permissions:
contents: read

jobs:
release:
name: Semantic Release
build:
name: Build distributions
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

concurrency:
group: ${{ github.workflow }}-release-${{ github.ref_name }}
cancel-in-progress: false

environment:
name: pypi
url: https://pypi.org/project/swgoh-comlink/

permissions:
id-token: write
contents: write
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
sha: ${{ steps.version.outputs.sha }}

steps:
- name: Check-out repository
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
fetch-depth: 0
fetch-depth: 0 # full history + tags for version derivation

- name: Install uv
uses: astral-sh/setup-uv@v7
Expand All @@ -36,48 +56,61 @@ jobs:
with:
python-version: "3.12"

# Update version
- name: Update version
run: uvx hatch version minor

# Update CHANGELOG.md
- name: Update CHANGELOG.md
run: uvx git-changelog -B auto -Tio CHANGELOG.md -c angular -s build,deps,fix,feat,refactor -n semver

# Commit version bump and changelog, then tag and push
- name: Commit and tag
- name: Compute next version from the latest tag
id: version
run: |
VERSION=$(uvx hatch version)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add src/swgoh_comlink/version.py CHANGELOG.md
git commit -m "chore(release): bump version to $VERSION [skip ci]"
git tag -a "v$VERSION" -m "Release version $VERSION"
git push origin ${{ github.ref_name }} --follow-tags
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
set -euo pipefail
latest="$(git tag --list 'v[0-9]*' --sort=-v:refname | head -n1)"
latest="${latest:-v0.0.0}"
base="${latest#v}"
base="${base%%[-+]*}" # strip any pre-release / build metadata
IFS='.' read -r major minor patch <<< "$base"
case "${{ inputs.bump }}" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
version="${major}.${minor}.${patch}"
tag="v${version}"
if git rev-parse "$tag" >/dev/null 2>&1; then
echo "::error::Tag $tag already exists — choose a different bump or delete the tag."
exit 1
fi
{
echo "version=$version"
echo "tag=$tag"
echo "sha=$GITHUB_SHA"
} >> "$GITHUB_OUTPUT"
echo "Releasing $tag from $latest (bump=${{ inputs.bump }}) at $GITHUB_SHA"

# Build the package
- name: Build the package
run: uvx hatch build
env:
# Force the build version without needing the tag to exist yet, so the
# tag can be created last (after a successful publish).
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.version.outputs.version }}
run: uvx --with hatch-vcs hatch build

- name: Verify built version matches the target
run: |
set -euo pipefail
ls -1 dist/
test -f "dist/swgoh_comlink-${{ steps.version.outputs.version }}.tar.gz"
test -f "dist/swgoh_comlink-${{ steps.version.outputs.version }}-py3-none-any.whl"

# Upload build artifacts for the publish job
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: dist
path: dist/

# Publish the package to PyPI
pypi-publish:
name: Publish to PyPI
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs:
- release
- build
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
contents: write

environment:
name: pypi
Expand All @@ -92,3 +125,24 @@ jobs:

- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0

tag-release:
name: Tag and create GitHub Release
runs-on: ubuntu-latest
needs:
- build
- pypi-publish
permissions:
contents: write # create the tag + GitHub Release

steps:
- name: Create tag and GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release create "${{ needs.build.outputs.tag }}" \
--repo "${{ github.repository }}" \
--target "${{ needs.build.outputs.sha }}" \
--title "${{ needs.build.outputs.tag }}" \
--generate-notes
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,6 @@ Pipfile*
pyvenv.cfg
/src/swgoh_comlink/tools.py
/docs/exhaustive-tests.md

# hatch-vcs generated version file (derived from Git tag at build time)
/src/swgoh_comlink/_version.py
22 changes: 11 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Be respectful, constructive, and patient. We're all here because we enjoy the ga
gh auth login
gh repo fork swgoh-utils/comlink-python
```

```bash
git clone https://github.com/<your-username>/comlink-python.git
cd comlink-python
Expand All @@ -60,7 +60,7 @@ Be respectful, constructive, and patient. We're all here because we enjoy the ga
git config --local branch.main.remote upstream
git remote set-url --push upstream github@github.com:<your-username>/comlink-python.git
```

2. **Install uv** (if you don't have it)

```bash
Expand Down Expand Up @@ -114,7 +114,7 @@ comlink-python/
│ │ ├── commitlint.yml # Commit message validation (npm-based)
│ │ ├── integration.yml # Integration tests against live comlink services
│ │ ├── labeler.yml # Auto-label PRs by file path
│ │ ├── release.yml # Semantic release → PyPI publish
│ │ ├── release.yml # Tag-driven release (build → PyPI → tag) via hatch-vcs
│ │ └── test.yml # Quick test + docs build
│ ├── CODEOWNERS # Code ownership
│ ├── dependabot.yml # Automated dependency updates
Expand Down Expand Up @@ -164,7 +164,7 @@ comlink-python/
│ ├── globals.py # Logging configuration
│ ├── swgoh_comlink.py # SwgohComlink (sync client)
│ ├── swgoh_comlink_async.py # SwgohComlinkAsync (async client)
│ └── version.py # Package version (managed by hatch)
│ └── version.py # Exposes __version__ (derived from the Git tag via hatch-vcs)
├── tests/
│ ├── integration/ # Tests requiring a running comlink service
│ ├── resources/ # Test fixture data (example-player.json, etc.)
Expand All @@ -191,7 +191,7 @@ comlink-python/
| `helpers/` | `DataItems` IntFlag enum, `Constants` class, 25+ utility functions split across focused submodules |
| `exceptions.py` | `SwgohComlinkException`, `SwgohComlinkValueError`, and `SwgohComlinkTypeError` |
| `globals.py` | Shared logging setup (`get_logger()`) |
| `version.py` | Single `__version__` string, managed by hatch during releases |
| `version.py` | Exposes `__version__`, derived from the Git tag at build time via hatch-vcs |

---

Expand Down Expand Up @@ -506,13 +506,13 @@ Before opening an issue, check the [existing issues](https://github.com/swgoh-ut

Releases are handled by the maintainer through the GitHub Actions `release.yml` workflow. Contributors don't need to manage versioning or releases, but here's how it works for reference:

1. The release workflow is triggered manually (`workflow_dispatch`)
2. [Hatch](https://hatch.pypa.io/) bumps the version in `src/swgoh_comlink/version.py`
3. [git-changelog](https://pawamoy.github.io/git-changelog/) regenerates `CHANGELOG.md` from commit history
4. The new version is tagged and pushed
5. The package is built with `hatch build` and published to [PyPI](https://pypi.org/project/swgoh-comlink/)
1. The release workflow is triggered manually (`workflow_dispatch`) with a `bump` input (`patch`, `minor`, or `major`)
2. The next version is computed from the latest release tag and the chosen bump level
3. The package is built — [hatch-vcs](https://github.com/ofek/hatch-vcs) derives the version from that target tag, so **no version string is committed to the repo**
4. The distributions are published to [PyPI](https://pypi.org/project/swgoh-comlink/) via trusted publishing
5. Only after a successful publish is the Git tag created and a GitHub Release cut, with release notes auto-generated from the commit/PR history

This is why conventional commit messages matter — they become the release notes automatically.
Because the version comes from the Git tag (not a committed file), the release workflow never pushes to the protected `main` branch. This is why conventional commit messages matter — they become the auto-generated release notes.

**Version scheme:** [Semantic Versioning](https://semver.org/)

Expand Down
38 changes: 10 additions & 28 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["hatchling>=1.27.0"]
requires = ["hatchling>=1.27.0", "hatch-vcs>=0.4.0"]
build-backend = "hatchling.build"

[project]
Expand Down Expand Up @@ -55,8 +55,16 @@ markers = [
"cat_stress: load endurance and connection pool tests",
]

# Version is derived from the Git tag (e.g. v2.1.0) at build time.
# No version string is committed to the repo — tag the release and the
# build reads it. See .github/workflows/release.yml.
[tool.hatch.version]
path = 'src/swgoh_comlink/version.py'
source = "vcs"

# Write the resolved version to a generated, git-ignored module so it is
# importable at runtime (consumed by src/swgoh_comlink/version.py).
[tool.hatch.build.hooks.vcs]
version-file = "src/swgoh_comlink/_version.py"

[tool.hatch.build.targets.wheel]
packages = ["src/swgoh_comlink"]
Expand Down Expand Up @@ -150,29 +158,3 @@ exclude_lines = [
"if TYPE_CHECKING:",
"if __name__ == .__main__.",
]

# ── Semantic Release ─────────────────────────────────────────────────────

[tool.semantic_release]
version_variable = [
"src/swgoh_comlink/version.py"
]
version_pattern = "src/swgoh_comlink/version.py:__version__ = '{version}'"
version_source = "commit"
branch = "main"
changelog_file = "CHANGELOG.md"
build_command = "uv build"
dist_path = "dist/"
upload_to_release = true
upload_to_pypi = false
remove_dist = true
patch_without_tag = true

[tool.semantic_release.branches.main]
match = "main"
prerelease = false

[tool.semantic_release.branches.tools]
match = "tools"
prerelease = true
prerelease_token = "rc"
25 changes: 24 additions & 1 deletion src/swgoh_comlink/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,25 @@
# coding=utf-8
__version__ = "2.0.7"
"""Expose the installed package version as ``__version__``.

The version is derived from the Git tag at build time via ``hatch-vcs``
(see ``[tool.hatch.version]`` in ``pyproject.toml``). The build writes the
resolved value to the git-ignored ``_version.py``; we read it from there,
falling back to installed package metadata, then to a sentinel for an
unbuilt source tree.
"""

from __future__ import annotations

try:
# Generated at build time by hatch-vcs (git-ignored).
from swgoh_comlink._version import __version__
except ImportError: # pragma: no cover - exercised only in unbuilt trees
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _package_version

try:
__version__ = _package_version("swgoh_comlink")
except PackageNotFoundError:
__version__ = "0.0.0+unknown"

__all__ = ["__version__"]
Loading