Skip to content
Open
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
5 changes: 3 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ No runtime dependencies - keep it that way.
- This is a public repo: describe the library on its own terms.

## CI and release
- `.github/workflows/ci.yml`: push to `main` + PRs, matrix ubuntu/macos/windows x Python 3.12/3.13,
- `.github/workflows/ci.yml`: push to `main` + PRs, matrix ubuntu/macos/windows x Python 3.12 to 3.15,
`uv sync --locked`, `uv run poe ci`, `uv build`. Keep `uv.lock` committed and in sync.
- Actions are pinned by commit SHA with the version in a trailing comment
(`uses: actions/checkout@<sha> # v7.0.1`); bump both together.
Expand All @@ -52,7 +52,8 @@ No runtime dependencies - keep it that way.
`workflow_call`, its ubuntu/3.12 job uploads the `dist` artifact), `release` tags the
project version and pushes the tag (refuses a dev version), publishes `dist` via PyPI
trusted publishing (OIDC, GitHub environment `pypi`, no API token; the job must stay
in `release.yml`, the publisher is bound to that file), then `release:post` opens a PR
in `release.yml`, the publisher is bound to that file), then `release:github` creates
the GitHub release from the version's changelog entries and `release:post` opens a PR
from `feat/post-release-vX.Y.Z-dev` setting the next version. Merge it too.
- `pyproject.toml`'s `version` is `X.Y.Z.dev0` between releases (`uv version` normalizes
`-dev`).
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.12", "3.13"]
python: ["3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: techcode-io/github-actions/setup-uv@f1054ff1cbac66b5acdc7c4d0d1d38f18a3a6030 # setup-uv/v1
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ jobs:
path: dist/
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3

github-release:
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: techcode-io/github-actions/setup-uv@f1054ff1cbac66b5acdc7c4d0d1d38f18a3a6030 # setup-uv/v1
- run: uv sync --locked
- run: uv run poe release:github
env:
GH_TOKEN: ${{ github.token }}

post-release:
needs: publish
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Support for Python 3.14 and 3.15.

## [0.1.0] - 2026-11-03

First public release.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<a href="https://github.com/techcode-io/proclet/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/techcode-io/proclet/ci.yml?branch=main&label=ci&style=flat-square" alt="continuous integration" style="height: 20px;">
</a>
<a href="https://github.com/techcode-io/proclet/graphs/contributors">
<img src="https://img.shields.io/github/contributors-anon/techcode-io/proclet?color=yellow&style=flat-square" alt="contributors" style="height: 20px;">
<a href="https://github.com/techcode-io/proclet/blob/main/pyproject.toml">
<img src="https://img.shields.io/badge/dependencies-none-brightgreen?style=flat-square" alt="no runtime dependencies" style="height: 20px;">
</a>
<a href="https://opensource.org/licenses/Apache-2.0">
<img src="https://img.shields.io/badge/apache%202.0-blue.svg?style=flat-square&label=license" alt="license" style="height: 20px;">
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ keywords = ["subprocess", "process", "pipeline", "asyncio", "shell"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Topic :: System :: Shells",
"Typing :: Typed",
]
Expand All @@ -26,6 +29,7 @@ requires-python = ">=3.12"
[project.urls]
Repository = "https://github.com/techcode-io/proclet"
Documentation = "https://github.com/techcode-io/proclet#readme"
Changelog = "https://github.com/techcode-io/proclet/blob/main/CHANGELOG.md"
Tracker = "https://github.com/techcode-io/proclet/issues"

[dependency-groups]
Expand Down Expand Up @@ -67,6 +71,10 @@ script = "scripts.release:run"
help = "Create a PR with changes for release."
script = "scripts.release:pre"

[tool.poe.tasks."release:github"]
help = "Create the GitHub release of the project version."
script = "scripts.release:github"

[tool.poe.tasks."release:post"]
help = "Create a PR preparing the next iteration."
script = "scripts.release:post"
Expand Down
68 changes: 61 additions & 7 deletions scripts/release.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Release proclet, one task per step of the release workflows.

`pre` opens a pull request stamping the changelog and setting the release version,
`run` tags the version on `main`, and `post` opens a pull request setting the next
iteration version. Publishing to PyPI is left to the `release` workflow.
`run` tags the version on `main`, `github` creates its GitHub release, and `post` opens
a pull request setting the next iteration version. Publishing to PyPI is left to the
`release` workflow.
"""

import datetime
Expand Down Expand Up @@ -57,6 +58,22 @@ def _project_version() -> str:
return out.getvalue().strip()


def _release_version() -> str:
"""Read the project version, which must be a release version.

Returns:
The version.
Raises:
SystemExit: if the project version isn't a release version, which means the
pull request opened by `pre` isn't merged.
ErrorReturnCode: if uv fails.
"""
version = _project_version()
if not re.fullmatch(r"\d+\.\d+\.\d+", version):
sys.exit(f"`{version}` isn't a release version: run `pre` first")
return version


def _version_from_env(name: str, pattern: str) -> str:
"""Read a version from the environment.

Expand Down Expand Up @@ -131,6 +148,26 @@ def _stamp_changelog(version: str) -> None:
CHANGELOG_PATH.write_text(data)


def _release_notes(version: str) -> str:
"""Read the changelog entries of `version`.

Args:
version: the released version.
Returns:
The entries, without the section's heading.
Raises:
SystemExit: if the changelog has no entry for `version`.
"""
section = re.search(
rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|^\[)",
CHANGELOG_PATH.read_text(),
re.MULTILINE | re.DOTALL,
)
if section is None or not section.group(1).strip():
sys.exit(f"CHANGELOG.md has no entry under `## [{version}]`")
return section.group(1).strip() + "\n"


def pre() -> None:
"""Open a pull request preparing the release of `RELEASE_VERSION`.

Expand Down Expand Up @@ -161,18 +198,35 @@ def run() -> None:
"""Tag the project version and push the tag to `origin`.

Raises:
SystemExit: if the project version isn't a release version, which means the
pull request opened by `pre` isn't merged.
SystemExit: if the project version isn't a release version.
ErrorReturnCode: if git or uv fails, including when the tag already exists.
"""
release_version = _project_version()
if not re.fullmatch(r"\d+\.\d+\.\d+", release_version):
sys.exit(f"`{release_version}` isn't a release version: run `pre` first")
release_version = _release_version()

_git("tag", release_version)
_git("push", "origin", release_version)


def github() -> None:
"""Create the GitHub release of the project version from its changelog entries.

Raises:
SystemExit: if the project version isn't a release version or has no
changelog entry.
ErrorReturnCode: if uv or gh fails, including when the tag doesn't exist.
"""
release_version = _release_version()

Command("gh").args(
"release",
"create",
release_version,
"--verify-tag",
f"--title={release_version}",
"--notes-file=-",
).cwd(ROOT_PATH).stdin(_release_notes(release_version)).inherit_out().exec()


def post() -> None:
"""Open a pull request preparing the iteration of `NEXT_VERSION`.

Expand Down
Loading