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
20 changes: 20 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
ref:
description: "tag to publish (e.g. v0.2.0a1)"
required: true

permissions:
contents: write
Expand All @@ -13,9 +17,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- uses: astral-sh/setup-uv@v6
with:
enable-cache: false
- name: Resolve tag
id: tag
run: echo "name=${{ github.event.inputs.ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
- name: Check version consistency
run: python scripts/check_version.py "${{ steps.tag.outputs.name }}"
- name: Detect pre-release
id: rel
run: |
if [[ "${{ steps.tag.outputs.name }}" =~ ^v[0-9]+(\.[0-9]+)*[-.](a|b|rc|alpha|beta|preview|dev)([-.]?[0-9]+)?$ ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Build wheel + sdist
run: uv build
- name: Publish to PyPI
Expand All @@ -25,6 +44,7 @@ jobs:
- name: Attach artifacts to release
uses: softprops/action-gh-release@v2
with:
prerelease: ${{ steps.rel.outputs.prerelease }}
files: |
dist/*.whl
dist/*.tar.gz
78 changes: 78 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Releasing

urag publishes two kinds of releases to PyPI:

- **Stable releases** (`vX.Y.Z`) — the default for `pip install urag-cli`.
- **Pre-releases** (`vX.Y.ZaN` / `vX.Y.ZrcN`) — opt-in for testers. PyPI
accepts them, but installers exclude pre-releases by default, so a
pre-release never shadows the latest stable version.

Both flows use the same `publish` workflow (`.github/workflows/publish.yml`),
which builds the wheel + sdist, uploads them to PyPI, and attaches the
artifacts to a GitHub release (flagged "pre-release" when the tag contains
an `a`/`b`/`rc`/`dev` segment). The workflow verifies that the tag, the
`[project] version` in `pyproject.toml`, and `__version__` in
`src/urag/__init__.py` all agree, so a mismatch can never publish.

## Prerequisites

- `PYPI_TOKEN` secret (PyPI API token for the `urag-cli` project) set on the
repository.
- The branch/tag being published passes CI (tests + lint).

## Stable release

1. Merge the feature PRs into `main`.
2. Bump the version in **two places**:
- `pyproject.toml` → `[project] version`
- `src/urag/__init__.py` → `__version__`
3. Update `CHANGELOG.md`: rename the `## Unreleased` section to
`## X.Y.Z - YYYY-MM-DD`.
4. Commit (`chore(release): prepare X.Y.Z`), push, and tag:

```bash
git tag -a v0.2.0 -m "urag 0.2.0"
git push origin v0.2.0
```

5. The publish workflow runs on the tag, uploads to PyPI, and creates the
GitHub release. Verify with:

```bash
pip install urag-cli # resolves to the new stable
urag --version
```

## Pre-release (based on a PR branch)

Use when testers should try a pending change set (e.g. the latest PR)
before it is finalized:

1. From the branch with the changes, bump the version to the next
pre-release of the upcoming release line, e.g. `0.2.0a1` (or `0.2.0rc1`
when the branch is believed final). Update both version locations and
the CHANGELOG header.
2. Commit and tag:

```bash
git tag -a v0.2.0a1 -m "urag 0.2.0a1 (pre-release)"
git push origin v0.2.0a1
```

3. The workflow publishes to PyPI and creates a GitHub **pre-release**.
Testers install it explicitly (pre-releases are never the default):

```bash
pip install urag-cli==0.2.0a1
# or the latest pre-release of the project:
pip install --pre urag-cli
```

4. When the branch merges into `main`, follow the stable flow with the
final version (e.g. `0.2.0`).

## Manual re-publish

The workflow also accepts a `workflow_dispatch` with a `ref` input (the tag
name). Use it to re-run a failed publish without re-tagging; the version
checks still apply.
35 changes: 35 additions & 0 deletions scripts/check_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Release consistency check: pyproject version, __init__ version, and tag.

Usage: python scripts/check_version.py [vX.Y.Z]

Exits non-zero (with a joined message) when:
- src/urag/__init__.py __version__ != pyproject [project].version
- the git tag (without leading 'v') != pyproject [project].version
"""

import re
import sys
import tomllib
from pathlib import Path

root = Path(__file__).resolve().parents[1]
data = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
pyproject_version = data["project"]["version"]

init = (root / "src" / "urag" / "__init__.py").read_text(encoding="utf-8")
match = re.search(r'__version__\s*=\s*"([^"]+)"', init)
init_version = match.group(1) if match else None

errors: list[str] = []
if init_version != pyproject_version:
errors.append(
f"src/urag/__init__.py __version__={init_version!r} does not match "
f"pyproject version {pyproject_version!r}"
)
tag = sys.argv[1] if len(sys.argv) > 1 else ""
if tag.startswith("v") and tag[1:] != pyproject_version:
errors.append(f"tag {tag!r} does not match pyproject version {pyproject_version!r}")

if errors:
raise SystemExit("\n".join(errors))
print(f"version ok: {pyproject_version}")
Loading