Skip to content

Commit 1296b83

Browse files
ColonistOneclaude
andauthored
chore: release v1.5.0 + OIDC release automation (#24)
Two changes that ship together so v1.5.0 can be the first release cut via the new automation: 1. Release workflow at .github/workflows/release.yml — triggered on `v*` tag push. Stages: - test: runs ruff, mypy, pytest before anything else - build: builds wheel + sdist, refuses to proceed if the tag version doesn't match pyproject.toml - publish: uploads to PyPI via OIDC trusted publishing (no API token stored anywhere — short-lived token minted by PyPI from the GitHub Actions OIDC identity at publish time) - github-release: extracts the matching CHANGELOG section and creates a GitHub Release with the wheel + sdist attached 2. Version bump 1.4.0 → 1.5.0 in pyproject.toml and __init__.py. 3. CHANGELOG: consolidated the 1.5.0 section into a clean, ordered summary covering everything that's landed since 1.4.0: - AsyncColonyClient (PR #18) - Typed error hierarchy (PR #19) - RetryConfig + 5xx default retry (PR #20) - py.typed + verify_webhook + Dependabot (PR #21) - Pagination iterators (PR #23) - Coverage + Codecov (PR #17) - This release automation Coverage at 100% (514/514 statements). 215 tests passing. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6edf2ef commit 1296b83

4 files changed

Lines changed: 139 additions & 21 deletions

File tree

.github/workflows/release.yml

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: Release
2+
3+
# Publishes to PyPI via OIDC trusted publishing whenever a `v*` tag is pushed.
4+
# No API tokens are stored anywhere — PyPI mints a short-lived token from
5+
# the GitHub Actions OIDC identity at publish time.
6+
#
7+
# To cut a release:
8+
# 1. Bump the version in pyproject.toml and src/colony_sdk/__init__.py
9+
# 2. Move the "## Unreleased" section in CHANGELOG.md under a new
10+
# "## X.Y.Z — YYYY-MM-DD" heading
11+
# 3. Merge to main
12+
# 4. git tag vX.Y.Z && git push origin vX.Y.Z
13+
#
14+
# This workflow will then: run the test suite, build wheel + sdist,
15+
# publish to PyPI via OIDC, and create a GitHub Release with the
16+
# CHANGELOG section as the release notes.
17+
18+
on:
19+
push:
20+
tags:
21+
- "v*"
22+
23+
jobs:
24+
test:
25+
name: Test before release
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v6
29+
- uses: actions/setup-python@v6
30+
with:
31+
python-version: "3.12"
32+
- run: pip install pytest pytest-cov pytest-asyncio httpx ruff mypy
33+
- run: ruff check src/ tests/
34+
- run: ruff format --check src/ tests/
35+
- run: mypy src/
36+
- run: pytest
37+
38+
build:
39+
name: Build distributions
40+
needs: test
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v6
44+
- uses: actions/setup-python@v6
45+
with:
46+
python-version: "3.12"
47+
- run: pip install build
48+
- run: python -m build
49+
- name: Verify version matches tag
50+
run: |
51+
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
52+
PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
53+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
54+
echo "::error::Tag v$TAG_VERSION does not match pyproject.toml version $PKG_VERSION"
55+
exit 1
56+
fi
57+
echo "Tag and pyproject.toml agree on version $PKG_VERSION"
58+
- uses: actions/upload-artifact@v4
59+
with:
60+
name: dist
61+
path: dist/
62+
63+
publish:
64+
name: Publish to PyPI
65+
needs: build
66+
runs-on: ubuntu-latest
67+
environment:
68+
name: pypi
69+
url: https://pypi.org/p/colony-sdk
70+
permissions:
71+
id-token: write # required for OIDC trusted publishing
72+
steps:
73+
- uses: actions/download-artifact@v4
74+
with:
75+
name: dist
76+
path: dist/
77+
- name: Publish to PyPI via OIDC
78+
uses: pypa/gh-action-pypi-publish@release/v1
79+
80+
github-release:
81+
name: Create GitHub Release
82+
needs: publish
83+
runs-on: ubuntu-latest
84+
permissions:
85+
contents: write # required for gh release create
86+
steps:
87+
- uses: actions/checkout@v6
88+
- uses: actions/download-artifact@v4
89+
with:
90+
name: dist
91+
path: dist/
92+
- name: Extract changelog section for this version
93+
run: |
94+
VERSION="${GITHUB_REF#refs/tags/v}"
95+
# Print everything under "## VERSION " up to (but not including)
96+
# the next "## " heading. Strips the heading line itself.
97+
awk -v ver="$VERSION" '
98+
/^## / {
99+
if (in_section) exit
100+
if ($0 ~ "^## " ver " ") { in_section = 1; next }
101+
next
102+
}
103+
in_section { print }
104+
' CHANGELOG.md > release_notes.md
105+
if [ ! -s release_notes.md ]; then
106+
echo "::warning::No CHANGELOG entry found for $VERSION — release notes will be empty"
107+
fi
108+
echo "--- release_notes.md ---"
109+
cat release_notes.md
110+
- name: Create GitHub Release
111+
env:
112+
GH_TOKEN: ${{ github.token }}
113+
run: |
114+
gh release create "${GITHUB_REF_NAME}" \
115+
--title "${GITHUB_REF_NAME}" \
116+
--notes-file release_notes.md \
117+
dist/*

CHANGELOG.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
11
# Changelog
22

3-
## Unreleased
3+
## 1.5.0 — 2026-04-09
4+
5+
A large quality-and-ergonomics release. **Backward compatible** — every change either adds new surface area or refines internals. The one behavior change (5xx retry defaults) is opt-out.
46

57
### New features
68

7-
- **`iter_posts()` and `iter_comments()`** — generator methods that auto-paginate paginated endpoints, yielding one item at a time. Available on both `ColonyClient` (sync) and `AsyncColonyClient` (async, as `async for`). Accept `max_results=` to stop early; `iter_posts` accepts `page_size=` to tune the per-request size. `get_all_comments()` is now a thin wrapper around `iter_comments()` that buffers into a list.
8-
- **`verify_webhook(payload, signature, secret)`** — HMAC-SHA256 verification helper for incoming webhook deliveries. Constant-time comparison via `hmac.compare_digest`. Tolerates a leading `sha256=` prefix on the signature header. Accepts `bytes` or `str` payloads.
9+
- **`AsyncColonyClient`** — full async mirror of `ColonyClient` built on `httpx.AsyncClient`. Every method is a coroutine, supports `async with` for connection cleanup, and shares the same JWT refresh / 401 retry / 429 backoff behaviour. Install via `pip install "colony-sdk[async]"`. The synchronous client remains zero-dependency.
10+
- **Typed error hierarchy**`ColonyAuthError` (401/403), `ColonyNotFoundError` (404), `ColonyConflictError` (409), `ColonyValidationError` (400/422), `ColonyRateLimitError` (429), `ColonyServerError` (5xx), and `ColonyNetworkError` (DNS / connection / timeout) all subclass `ColonyAPIError`. Catch the specific subclass or fall back to the base class — old `except ColonyAPIError` code keeps working unchanged.
11+
- **`ColonyRateLimitError.retry_after`** — exposes the server's `Retry-After` header value (in seconds) when rate-limit retries are exhausted, so callers can implement higher-level backoff above the SDK's built-in retries.
12+
- **HTTP status hints in error messages** — error messages now include a short human-readable hint (`"not found — the resource doesn't exist or has been deleted"`, `"rate limited — slow down and retry after the backoff window"`, etc.) so logs and LLMs don't need to consult docs.
13+
- **`RetryConfig`** — pass `retry=RetryConfig(max_retries, base_delay, max_delay, retry_on)` to `ColonyClient` or `AsyncColonyClient` to tune the transient-failure retry policy. `RetryConfig(max_retries=0)` disables retries entirely. The default retries 2× on `{429, 502, 503, 504}` with exponential backoff capped at 10 seconds. The server's `Retry-After` header always overrides the computed delay. The 401 token-refresh path is unaffected — it always runs once independently and does not consume the retry budget.
14+
- **`iter_posts()` and `iter_comments()`** — generator methods that auto-paginate paginated endpoints, yielding one item at a time. Available on both `ColonyClient` (sync, regular generators) and `AsyncColonyClient` (async generators, used with `async for`). Both accept `max_results=` to stop early; `iter_posts` accepts `page_size=` to tune the per-request size. `get_all_comments()` is now a thin wrapper around `iter_comments()` that buffers into a list.
15+
- **`verify_webhook(payload, signature, secret)`** — HMAC-SHA256 verification helper for incoming webhook deliveries. Matches the canonical Colony format (raw body, hex digest, `X-Colony-Signature` header). Constant-time comparison via `hmac.compare_digest`. Tolerates a leading `sha256=` prefix on the signature for frameworks that normalise that way. Accepts `bytes` or `str` payloads.
916
- **PEP 561 `py.typed` marker** — type checkers (mypy, pyright) now recognise `colony_sdk` as a typed package, so consumers get full type hints out of the box without `--ignore-missing-imports`.
1017

11-
### Infrastructure
12-
13-
- **Dependabot**`.github/dependabot.yml` watches `pip` and `github-actions` weekly, grouped into single PRs to minimise noise.
14-
18+
### Behavior changes
1519

16-
- **`AsyncColonyClient`** — full async mirror of `ColonyClient` built on `httpx.AsyncClient`. Every method is a coroutine, supports `async with` for connection cleanup, and shares the same JWT refresh / 401 retry / 429 backoff behaviour. Install via `pip install "colony-sdk[async]"`.
17-
- **Optional `[async]` extra**`httpx>=0.27` is only required if you import `AsyncColonyClient`. The sync client remains zero-dependency.
18-
- **Typed error hierarchy**`ColonyAuthError` (401/403), `ColonyNotFoundError` (404), `ColonyConflictError` (409), `ColonyValidationError` (400/422), `ColonyRateLimitError` (429), `ColonyServerError` (5xx), and `ColonyNetworkError` (DNS / connection / timeout) all subclass `ColonyAPIError`. Catch the specific subclass or fall back to the base class — old `except ColonyAPIError` code keeps working unchanged.
19-
- **`ColonyRateLimitError.retry_after`** — exposes the server's `Retry-After` header value (in seconds) when rate-limit retries are exhausted, so callers can implement their own backoff above the SDK's built-in retries.
20-
- **HTTP status hints in error messages** — error messages now include a short, human-readable hint (`"not found — the resource doesn't exist or has been deleted"`, `"rate limited — slow down and retry after the backoff window"`, etc.) so logs and LLMs don't need to consult docs to understand what happened.
21-
- **`RetryConfig`** — pass `retry=RetryConfig(max_retries, base_delay, max_delay, retry_on)` to `ColonyClient` or `AsyncColonyClient` to tune the transient-failure retry policy. `RetryConfig(max_retries=0)` disables retries; the default retries 2× on `{429, 502, 503, 504}` with exponential backoff capped at 10 seconds. The server's `Retry-After` header always overrides the computed delay. The 401 token-refresh path is unaffected — it always runs once independently.
20+
- **5xx gateway errors are now retried by default.** Previously the SDK only retried 429s; it now also retries `502 Bad Gateway`, `503 Service Unavailable`, and `504 Gateway Timeout` (the defaults `RetryConfig` ships with). `500 Internal Server Error` is intentionally **not** retried by default — it more often indicates a bug in the request than a transient infra issue, so retrying just amplifies the problem. Opt back into the old 1.4.x behaviour with `ColonyClient(retry=RetryConfig(retry_on=frozenset({429})))`.
2221

23-
### Behavior changes
22+
### Infrastructure
2423

25-
- **5xx gateway errors are now retried by default.** Previously the SDK only retried 429s; it now also retries `502 Bad Gateway`, `503 Service Unavailable`, and `504 Gateway Timeout` (the same defaults `RetryConfig` ships with). `500 Internal Server Error` is intentionally **not** retried by default — it more often indicates a bug in the request than a transient infra issue, so retrying just amplifies the problem. Opt in with `RetryConfig(retry_on=frozenset({429, 500, 502, 503, 504}))` if you want the old behaviour back, or with `retry_on=frozenset({429})` for the previous 1.4.x behaviour.
24+
- **OIDC release automation** — releases now ship via PyPI Trusted Publishing on tag push. `git tag vX.Y.Z && git push origin vX.Y.Z` triggers `.github/workflows/release.yml`, which runs the test suite, builds wheel + sdist, publishes to PyPI via short-lived OIDC tokens (no API token stored anywhere), and creates a GitHub Release with the changelog entry as release notes. The workflow refuses to publish if the tag version doesn't match `pyproject.toml`.
25+
- **Dependabot**`.github/dependabot.yml` watches `pip` and `github-actions` weekly, **grouped** into single PRs per ecosystem to minimise noise.
26+
- **Coverage on CI**`pytest-cov` runs on the 3.12 job with Codecov upload via `codecov-action@v6` and a token. Codecov badge added to the README.
2627

2728
### Internal
2829

2930
- Extracted `_parse_error_body` and `_build_api_error` helpers in `client.py` so the sync and async clients format errors identically.
30-
- `_error_class_for_status` dispatches HTTP status codes to the correct typed-error subclass; sync and async transports both wrap network failures as `ColonyNetworkError` (`status=0`).
31+
- `_error_class_for_status` dispatches HTTP status codes to the correct typed-error subclass; sync and async transports both wrap network failures as `ColonyNetworkError(status=0)`.
32+
- `_should_retry` and `_compute_retry_delay` helpers shared by sync + async `_raw_request` paths so retry semantics stay in lockstep.
3133

3234
### Testing
3335

34-
- Added 60 async tests using `httpx.MockTransport` covering every method, the auth flow, 401 refresh, 429 backoff (with `Retry-After`), network errors, and registration.
35-
- Added 13 sync + 7 async tests for the typed error hierarchy: subclass dispatch for every status, `retry_after` propagation, network-error wrapping, and base-class fallback for unknown status codes.
36-
- Package coverage stays at **100%** (448 statements).
36+
- **100% line coverage** (514/514 statements across 4 source files), enforced by Codecov on every PR.
37+
- Added 60+ async tests using `httpx.MockTransport`, 20+ typed-error tests, 21+ retry-config tests, 15+ pagination-iterator tests, and 10 webhook-verification tests.
3738

3839
## 1.4.0 — 2026-04-08
3940

pyproject.toml

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

55
[project]
66
name = "colony-sdk"
7-
version = "1.4.0"
7+
version = "1.5.0"
88
description = "Python SDK for The Colony (thecolony.cc) — the official Python client for the AI agent internet"
99
readme = "README.md"
1010
license = {text = "MIT"}

src/colony_sdk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def main():
4141
if TYPE_CHECKING: # pragma: no cover
4242
from colony_sdk.async_client import AsyncColonyClient
4343

44-
__version__ = "1.4.0"
44+
__version__ = "1.5.0"
4545
__all__ = [
4646
"COLONIES",
4747
"AsyncColonyClient",

0 commit comments

Comments
 (0)