Skip to content

Commit ff0b140

Browse files
authored
Merge pull request #69 from hotdata-dev/ci/standardize-release-process
ci: standardize release process
2 parents fd6c8a7 + 6ad261a commit ff0b140

17 files changed

Lines changed: 736 additions & 3 deletions
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Check release metadata
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'pyproject.toml'
7+
- 'CHANGELOG.md'
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
check:
14+
name: Verify changelog matches version bump
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
18+
with:
19+
fetch-depth: 0
20+
21+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
22+
with:
23+
python-version: '3.12'
24+
25+
- name: Check release metadata
26+
run: python scripts/check-release.py

.github/workflows/publish.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ jobs:
2828

2929
- name: Verify tag matches pyproject version
3030
run: |
31-
# Release tags must start with `v` followed by a PEP 440 version (e.g. v1.2.3, v1.2.3a1).
3231
if [[ ! "$GITHUB_REF_NAME" =~ ^v[0-9] ]]; then
3332
echo "Release tag '$GITHUB_REF_NAME' must start with 'v' followed by a digit (e.g. v1.0.0)" >&2
3433
exit 1

.github/workflows/regenerate.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ jobs:
110110
p.write_text(src)
111111
PY
112112
113+
- name: Patch ApiClient close lifecycle
114+
run: python3 scripts/patch_api_client_close.py
115+
113116
- name: Clean up generated artifacts
114117
run: |
115118
rm -f openapi.yaml

.github/workflows/release.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: GitHub Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v[0-9]*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
name: Create GitHub Release
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
17+
18+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
19+
with:
20+
python-version: '3.12'
21+
22+
- name: Read package metadata
23+
id: meta
24+
run: |
25+
pkg_name=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['name'])")
26+
pkg_version="${GITHUB_REF_NAME#v}"
27+
echo "name=${pkg_name}" >> "$GITHUB_OUTPUT"
28+
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"
29+
30+
- name: Extract changelog notes
31+
id: notes
32+
run: |
33+
set -euo pipefail
34+
version="${GITHUB_REF_NAME#v}"
35+
if [[ -f CHANGELOG.md ]]; then
36+
body="$(python scripts/extract-changelog.py "$version")"
37+
else
38+
body="Release ${version}."
39+
fi
40+
delimiter="EOF_${RANDOM}_${RANDOM}"
41+
{
42+
echo "body<<${delimiter}"
43+
echo "$body"
44+
echo "${delimiter}"
45+
} >> "$GITHUB_OUTPUT"
46+
47+
- name: Create GitHub Release
48+
uses: softprops/action-gh-release@1e812e8210a4a8a0b23075e5795f2a4e2b2a0b7 # v2.2.2
49+
with:
50+
tag_name: ${{ github.ref_name }}
51+
name: ${{ steps.meta.outputs.name }} ${{ steps.meta.outputs.version }}
52+
body: ${{ steps.notes.outputs.body }}
53+
generate_release_notes: false
54+
make_latest: true

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [0.2.2] - 2026-05-20
11+
12+
### Fixed
13+
14+
- Add `ApiClient.close()` and `RESTClientObject.close()` so callers can release urllib3 connection pools and use context managers safely.
15+
16+
## [0.2.1] - 2026-05-20
17+
18+
### Changed
19+
20+
- Regenerated Results API client from the latest OpenAPI spec.
21+
22+
## [0.2.0] - 2026-05-19
23+
24+
### Changed
25+
26+
- Managed database API updates and publish workflow.
27+
28+
## [0.1.0] - 2026-04-25
29+
30+
### Changed
31+
32+
- Initial published release.

RELEASING.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Releasing
2+
3+
Every release uses `./scripts/release.sh`. Do not bump versions, tag, or create GitHub Releases manually.
4+
5+
## One-time setup
6+
7+
- Install [GitHub CLI](https://cli.github.com/) (`gh`) and authenticate.
8+
- Ensure PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) is configured for this repo (`publish.yml` uses the `pypi` GitHub environment).
9+
10+
## Release steps
11+
12+
1. Add user-facing notes under `## [Unreleased]` in `CHANGELOG.md`.
13+
2. Prepare the release PR:
14+
15+
```bash
16+
./scripts/release.sh prepare patch # or minor | major | 1.2.3
17+
```
18+
19+
3. Merge the PR after CI passes (including the changelog check).
20+
4. Publish from a clean default branch checkout:
21+
22+
```bash
23+
git checkout main # or master for hotdata-marimo
24+
git pull
25+
./scripts/release.sh publish
26+
```
27+
28+
## What happens automatically
29+
30+
Pushing a `vX.Y.Z` tag triggers two workflows:
31+
32+
| Workflow | Purpose |
33+
|----------|---------|
34+
| `publish.yml` | Build wheel/sdist and publish to PyPI |
35+
| `release.yml` | Create the GitHub Release with notes from `CHANGELOG.md` |
36+
37+
## Enforcement
38+
39+
- **PR check** (`check-release.yml`): if `pyproject.toml` version changes, `CHANGELOG.md` must contain a matching `## [X.Y.Z]` section.
40+
- **Tag check** (`publish.yml`): the tag (without `v`) must match `[project].version` in `pyproject.toml`.
41+
- **Publish guard** (`release.sh publish`): refuses to tag if the changelog section is missing.
42+
43+
Together, these make it hard to ship a version without changelog notes or a GitHub Release.

hotdata/api_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ def __init__(
9797
def __enter__(self):
9898
return self
9999

100+
def close(self) -> None:
101+
if self.rest_client is not None:
102+
self.rest_client.close()
103+
100104
def __exit__(self, exc_type, exc_value, traceback):
101-
pass
105+
self.close()
102106

103107
@property
104108
def user_agent(self):

hotdata/rest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ def __init__(self, configuration) -> None:
118118
else:
119119
self.pool_manager = urllib3.PoolManager(**pool_args)
120120

121+
def close(self) -> None:
122+
if self.pool_manager is not None:
123+
self.pool_manager.clear()
124+
121125
def request(
122126
self,
123127
method,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "hotdata"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
description = "Hotdata API"
55
authors = [
66
{name = "Hotdata",email = "developers@hotdata.dev"},

scripts/check-release.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
"""Fail CI when pyproject.toml version changes without a matching CHANGELOG entry."""
3+
4+
from __future__ import annotations
5+
6+
import re
7+
import subprocess
8+
import sys
9+
from pathlib import Path
10+
11+
12+
def git_show(path: str, ref: str) -> str:
13+
try:
14+
return subprocess.check_output(["git", "show", f"{ref}:{path}"], text=True)
15+
except subprocess.CalledProcessError:
16+
return ""
17+
18+
19+
def read_version(text: str) -> str:
20+
match = re.search(r'(?m)^version = "([^"]+)"', text)
21+
if not match:
22+
raise SystemExit("could not read version from pyproject.toml")
23+
return match.group(1)
24+
25+
26+
def has_changelog_section(version: str) -> bool:
27+
changelog = Path("CHANGELOG.md")
28+
if not changelog.exists():
29+
return False
30+
return bool(re.search(rf"^## \[{re.escape(version)}\]", changelog.read_text(), re.M))
31+
32+
33+
def main() -> None:
34+
base = "origin/main"
35+
for candidate in ("origin/main", "origin/master"):
36+
if subprocess.call(["git", "rev-parse", "--verify", candidate], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
37+
base = candidate
38+
break
39+
40+
current = Path("pyproject.toml").read_text()
41+
previous = git_show("pyproject.toml", base)
42+
if not previous:
43+
print("skip: no base pyproject.toml to compare")
44+
return
45+
46+
old_version = read_version(previous)
47+
new_version = read_version(current)
48+
if old_version == new_version:
49+
print(f"version unchanged ({new_version})")
50+
return
51+
52+
if not has_changelog_section(new_version):
53+
raise SystemExit(
54+
f"pyproject.toml version bumped to {new_version} but CHANGELOG.md "
55+
f"has no '## [{new_version}]' section"
56+
)
57+
58+
print(f"release metadata ok for {new_version}")
59+
60+
61+
if __name__ == "__main__":
62+
main()

0 commit comments

Comments
 (0)