Skip to content

Commit a4017b1

Browse files
kricheljclaude
andauthored
uv-first docs, PyPI/GitHub README parity, and automatic carry-at-9 versioning (#26)
- README: lead with uv (uv add / uv sync), keep pip as a documented fallback; convert image/file links to absolute URLs so the PyPI long-description (which is README.md) renders identically to GitHub - docs/README.md: sync to the canonical README (was stale: old rigged LQR comparison and figures) - tools/bump_version.py: increment the version with single-digit carry-at-9 components (2.0.9 -> 2.1.0, 2.9.9 -> 3.0.0), updating pyproject.toml and __init__.py together - publish workflow: auto-increment the version (commit to master) before building/publishing/releasing, so every release bumps automatically - CONTRIBUTING: document the auto-bump release flow - CLAUDE.md: record the uv-first + carry-at-9 versioning conventions Claude-Session: https://claude.ai/code/session_013KvuS9HKbnZAwwFJyBkyHc Co-authored-by: Claude <noreply@anthropic.com>
1 parent 71ce3f7 commit a4017b1

6 files changed

Lines changed: 384 additions & 129 deletions

File tree

.github/workflows/python-publish.yml

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# Publishes the package to PyPI and creates the matching GitHub Release.
1+
# Publishes the package to PyPI and creates the matching GitHub Release,
2+
# auto-incrementing the version each run.
23
#
3-
# Flow: bump `version` in pyproject.toml, then run this workflow
4-
# (Actions -> Upload Python Package -> Run workflow). It builds the dists,
5-
# uploads them to PyPI via Trusted Publishing, and creates a GitHub Release
6-
# tagged v<version> with auto-generated notes and the dists attached.
4+
# Flow (just run it — Actions -> Upload Python Package -> Run workflow):
5+
# 1. bump-version : increment the version (carry-at-9 via tools/bump_version.py),
6+
# commit it back to master.
7+
# 2. release-build : build the dists from the bumped master.
8+
# 3. pypi-publish : upload to PyPI via Trusted Publishing (OIDC, no tokens).
9+
# 4. github-release: create the v<version> GitHub Release with notes + dists.
710
#
8-
# It is idempotent: re-running for an already-published version skips the PyPI
9-
# upload (skip-existing) and updates the existing release's assets, so it is
10-
# safe to run against the current version to backfill a release.
11+
# Versions roll over at 9: 2.0.9 -> 2.1.0, 2.9.9 -> 3.0.0.
1112

1213
name: Upload Python Package
1314

@@ -18,11 +19,39 @@ permissions:
1819
contents: read
1920

2021
jobs:
21-
release-build:
22+
bump-version:
2223
runs-on: ubuntu-latest
24+
permissions:
25+
contents: write
26+
outputs:
27+
version: ${{ steps.bump.outputs.version }}
28+
steps:
29+
- uses: actions/checkout@v5
30+
with:
31+
ref: master
32+
33+
- name: Increment the version (carry-at-9)
34+
id: bump
35+
run: |
36+
new="$(python3 tools/bump_version.py)"
37+
echo "version=$new" >> "$GITHUB_OUTPUT"
38+
echo "Bumped to $new"
39+
40+
- name: Commit and push the bump
41+
run: |
42+
git config user.name "github-actions[bot]"
43+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
44+
git add pyproject.toml src/PyDiffGame/__init__.py
45+
git commit -m "chore: bump version to ${{ steps.bump.outputs.version }} [skip ci]"
46+
git push origin HEAD:master
2347
48+
release-build:
49+
needs: bump-version
50+
runs-on: ubuntu-latest
2451
steps:
2552
- uses: actions/checkout@v5
53+
with:
54+
ref: master # the bumped commit
2655

2756
- name: Install uv
2857
uses: astral-sh/setup-uv@v6
@@ -37,9 +66,9 @@ jobs:
3766
path: dist/
3867

3968
pypi-publish:
40-
runs-on: ubuntu-latest
4169
needs:
4270
- release-build
71+
runs-on: ubuntu-latest
4372
permissions:
4473
# IMPORTANT: this permission is mandatory for trusted publishing
4574
id-token: write
@@ -66,15 +95,18 @@ jobs:
6695
skip-existing: true
6796

6897
github-release:
69-
runs-on: ubuntu-latest
7098
needs:
99+
- bump-version
71100
- pypi-publish
101+
runs-on: ubuntu-latest
72102
permissions:
73103
# Needed to create the release and its tag.
74104
contents: write
75105

76106
steps:
77107
- uses: actions/checkout@v5
108+
with:
109+
ref: master
78110

79111
- name: Retrieve release distributions
80112
uses: actions/download-artifact@v5
@@ -85,9 +117,9 @@ jobs:
85117
- name: Create or update the GitHub Release
86118
env:
87119
GH_TOKEN: ${{ github.token }}
120+
VERSION: ${{ needs.bump-version.outputs.version }}
88121
run: |
89-
version="$(grep -m1 '^version' pyproject.toml | sed -E 's/version *= *"([^"]+)".*/\1/')"
90-
tag="v${version}"
122+
tag="v${VERSION}"
91123
echo "Releasing ${tag}"
92124
if gh release view "$tag" >/dev/null 2>&1; then
93125
echo "Release ${tag} already exists - refreshing its assets."
@@ -96,5 +128,5 @@ jobs:
96128
gh release create "$tag" dist/* \
97129
--title "$tag" \
98130
--generate-notes \
99-
--target "$GITHUB_SHA"
131+
--target master
100132
fi

CLAUDE.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# PyDiffGame — repository guide for Claude
2+
3+
## Python tooling (uv-first)
4+
- Use **uv** for everything; pip is only a documented fallback.
5+
- `uv sync --extra dev`, `uv run pytest`, `uv run ruff check`, `uv run ruff format`,
6+
`uv run mypy src/PyDiffGame`, `uv build`.
7+
- Keep the quality gates green before committing: ruff format, ruff check, mypy on
8+
`src/PyDiffGame`, and the pytest suite — all via `uv run`.
9+
10+
## Versioning (carry-at-9)
11+
- The version is `X.Y.Z` with single-digit components that roll over at 9:
12+
`2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0` (the major keeps growing).
13+
- Increment **only** via `tools/bump_version.py`, which updates the version in both
14+
`pyproject.toml` and `src/PyDiffGame/__init__.py`. Never hand-edit version strings.
15+
- The version is bumped **automatically** by the publish workflow on each release, so
16+
do not bump it in ordinary PRs — the release run does it.
17+
18+
## Releasing
19+
- `Actions -> Upload Python Package -> Run workflow` (on `master`). The workflow
20+
auto-increments the version, commits it to `master`, builds with `uv build`,
21+
publishes to PyPI via Trusted Publishing (OIDC, no tokens), and creates the matching
22+
`v<version>` GitHub Release with notes and the built dists attached. It is idempotent
23+
(`skip-existing`).
24+
25+
## Docs
26+
- `README.md` is the single canonical readme and is also the PyPI long-description
27+
(`pyproject.toml: readme = "README.md"`), so its image/file links must be **absolute**
28+
(`raw.githubusercontent.com/.../master/...` for images, `github.com/.../blob/master/...`
29+
for files) so they render on PyPI.
30+
- Keep `docs/README.md` identical to `README.md`.
31+
- README figures are generated from the live solver:
32+
`uv run python tools/generate_readme_figures.py`.

CONTRIBUTING.md

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,26 @@ they pass locally.
4141

4242
## Releasing
4343

44-
Publishing a new version to PyPI and creating the matching GitHub Release is a
45-
single automated step:
44+
Publishing a new version is a single automated step — just run the publish
45+
workflow: **Actions -> Upload Python Package -> Run workflow** (on `master`).
4646

47-
1. Bump `version` in `pyproject.toml` (e.g. `2.0.0` -> `2.1.0`) and merge it to
48-
`master`.
49-
2. Run the publish workflow: **Actions -> Upload Python Package -> Run workflow**
50-
(on `master`).
51-
52-
That run builds the distributions with `uv build`, uploads them to PyPI via
53-
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no
54-
tokens or secrets), and then creates a `v<version>` GitHub Release with
55-
auto-generated notes and the built wheels/sdist attached.
47+
The run automatically:
5648

57-
The workflow is idempotent: re-running it for a version already on PyPI skips
58-
the upload (`skip-existing`) and just refreshes the release assets, so it is
59-
safe to re-run.
49+
1. **Increments the version** with `tools/bump_version.py`, which rolls each
50+
component over at 9 (`2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0`), updating both
51+
`pyproject.toml` and `src/PyDiffGame/__init__.py`, and commits the bump to
52+
`master`.
53+
2. Builds the distributions with `uv build`.
54+
3. Uploads them to PyPI via
55+
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no
56+
tokens or secrets).
57+
4. Creates a `v<version>` GitHub Release with auto-generated notes and the built
58+
wheels/sdist attached.
59+
60+
You normally never edit the version by hand. To bump it locally (e.g. to test),
61+
run `uv run python tools/bump_version.py` (`--dry-run` to preview, `--current`
62+
to print the current version). The PyPI upload is idempotent (`skip-existing`),
63+
so re-running the workflow is safe.
6064

6165
Thank you for your contribution!
6266

README.md

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<p align="center">
2-
<img alt="PyDiffGame logo" src="images/logo.png" width="420"/>
2+
<img alt="PyDiffGame logo" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/logo.png" width="420"/>
33
</p>
44

55
<p align="center">
@@ -68,22 +68,32 @@ accounting on a common yardstick, and ready-made plotting.
6868

6969
# Installation
7070

71-
Install the latest release from PyPI:
71+
PyDiffGame is published on [PyPI](https://pypi.org/project/PyDiffGame/) and is
72+
managed with [**uv**](https://docs.astral.sh/uv/). Add it to your project:
7273

7374
```bash
74-
pip install PyDiffGame
75+
uv add PyDiffGame
7576
```
7677

7778
To run the bundled examples (which additionally need
7879
[`python-control`](https://python-control.readthedocs.io/)):
7980

8081
```bash
81-
pip install "PyDiffGame[examples]"
82+
uv add "PyDiffGame[examples]"
8283
```
8384

84-
To work on the package itself, this project is managed with
85-
[**uv**](https://docs.astral.sh/uv/). Clone it and sync the locked development
86-
environment:
85+
<details>
86+
<summary><b>Prefer pip?</b> It works as a fallback.</summary>
87+
88+
```bash
89+
pip install PyDiffGame
90+
pip install "PyDiffGame[examples]" # with the examples extra
91+
```
92+
93+
</details>
94+
95+
To work on the package itself, clone it and sync the locked development
96+
environment with uv:
8797

8898
```bash
8999
git clone https://github.com/krichelj/PyDiffGame.git
@@ -92,7 +102,10 @@ uv sync --extra dev # creates .venv with the exact locked dependencies
92102
uv run pre-commit install # enable the formatting / lint / type-check hooks
93103
```
94104

95-
Then run anything through `uv run` (`uv run pytest`, `uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …).
105+
Then run anything through `uv run` (`uv run pytest`,
106+
`uv run python -m PyDiffGame.examples.MassesWithSpringsComparison`, …). Pip users
107+
can instead `pip install -e ".[dev]"`, though uv is recommended for the exact
108+
locked environment.
96109

97110
# Quick start
98111

@@ -183,7 +196,7 @@ To show the package in action we compare a differential game against an LQR on a
183196
masses connected by springs — a textbook coupled, oscillatory system:
184197

185198
<p align="center">
186-
<img alt="Two masses connected by springs between two walls" src="images/readme/masses_schematic.png" width="760"/>
199+
<img alt="Two masses connected by springs between two walls" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_schematic.png" width="760"/>
187200
</p>
188201

189202
The physical input space is decomposed along the **modal** directions of $M^{-1}K$, so each
@@ -245,19 +258,19 @@ monolithic optimum **to numerical precision**: the two state trajectories coinci
245258
(they differ by ~10⁻⁷) and the costs are equal:
246259

247260
<p align="center">
248-
<img alt="State trajectories: the decomposed game reproduces the monolithic LQR" src="images/readme/masses_game_vs_lqr.png" width="860"/>
261+
<img alt="State trajectories: the decomposed game reproduces the monolithic LQR" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_game_vs_lqr.png" width="860"/>
249262
</p>
250263

251264
<p align="center">
252-
<img alt="Cost comparison: the modal game recovers the LQR optimum" src="images/readme/masses_cost.png" width="440"/>
265+
<img alt="Cost comparison: the modal game recovers the LQR optimum" src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/readme/masses_cost.png" width="440"/>
253266
</p>
254267

255268
For this modally-decoupled system the decomposition is **lossless** — and it buys
256269
**compositionality**: you can add, drop or re-weight a control task by editing a single
257270
player, without re-tuning one monolithic cost matrix.
258271

259272
> The figures above are regenerated from the live solver by
260-
> [`tools/generate_readme_figures.py`](tools/generate_readme_figures.py)
273+
> [`tools/generate_readme_figures.py`](https://github.com/krichelj/PyDiffGame/blob/master/tools/generate_readme_figures.py)
261274
> (`uv run python tools/generate_readme_figures.py`), so they always match the current code.
262275
263276
# More examples
@@ -288,7 +301,7 @@ uv run mypy src/PyDiffGame # type-check
288301
```
289302

290303
Continuous integration runs the formatter check, linter, type checker and full suite on
291-
Python 3.11–3.14. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
304+
Python 3.11–3.14. See [CONTRIBUTING.md](https://github.com/krichelj/PyDiffGame/blob/master/CONTRIBUTING.md) for details.
292305

293306
# Citing
294307

@@ -304,7 +317,7 @@ If you use this work, please cite our paper:
304317
doi={10.1109/MED51440.2021.9480269}}
305318
```
306319

307-
Further details can be found in the [citation document](CITATIONS.bib).
320+
Further details can be found in the [citation document](https://github.com/krichelj/PyDiffGame/blob/master/CITATIONS.bib).
308321

309322
# Acknowledgments
310323

@@ -316,7 +329,7 @@ and Bar-Ilan Universities, Israel.
316329

317330
<p align="center">
318331
<a href="https://istrc.net.technion.ac.il/">
319-
<img src="images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
332+
<img src="https://raw.githubusercontent.com/krichelj/PyDiffGame/master/images/Logo_ISTRC_Green_English.png" height="80" alt="ISTRC"/>
320333
</a>
321334
&emsp;
322335
<a href="https://in.bgu.ac.il/en/Pages/default.aspx">

0 commit comments

Comments
 (0)