Skip to content

Commit 0e40d4c

Browse files
kricheljclaude
andauthored
Auto-release on every source-file commit (continuous deployment, gated) (#28)
Switch the publish workflow from manual-only (workflow_dispatch) to also fire on push to master when source files change. "Source" is gated to: - src/PyDiffGame/** (the package itself) - pyproject.toml (packaging metadata, deps, classifiers) Docs / tests / tooling / CI changes do NOT cut a release on their own; a mixed commit (e.g. src/foo.py + README.md) does. workflow_dispatch stays available for on-demand publishing. Three independent loop guards stop the workflow's own "chore: bump version" commit from re-triggering itself: 1. The bump commit message contains [skip ci]; GitHub Actions natively skips creating a workflow run for such pushes. 2. Defense-in-depth job-level if: filtering out github-actions[bot] and re-checking the commit message. 3. paths: filter (src changes only) on the trigger. Add a concurrency group (publish-master, no cancel) so back-to-back pushes serialize on the git push of the bump commit. Also fix two pre-existing lint errors (unused import, unsorted imports) in tools/render_logo.py that would otherwise block CI. Claude-Session: https://claude.ai/code/session_013KvuS9HKbnZAwwFJyBkyHc Co-authored-by: Claude <noreply@anthropic.com>
1 parent c55203b commit 0e40d4c

4 files changed

Lines changed: 91 additions & 16 deletions

File tree

.github/workflows/python-publish.yml

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
# Publishes the package to PyPI and creates the matching GitHub Release,
2-
# auto-incrementing the version each run.
2+
# auto-incrementing the version when source files change on master.
33
#
4-
# Flow (just run it — Actions -> Upload Python Package -> Run workflow):
4+
# Triggers:
5+
# - push to master that touches SOURCE files (the package or its packaging
6+
# metadata) — docs / tests / tooling / CI changes do not cut a release.
7+
# - workflow_dispatch: manual trigger remains available for on-demand re-runs
8+
# (use this if you ever need to publish despite no source-file changes).
9+
#
10+
# Loop prevention: the workflow itself commits a "chore: bump version" commit
11+
# back to master. Three independent guards stop that commit from re-triggering
12+
# the workflow:
13+
# 1. GitHub Actions natively skips push events whose head commit message
14+
# contains "[skip ci]" (which the bump commit always does).
15+
# 2. The bump-version job is guarded with an explicit `if:` so it only runs
16+
# for human commits / manual dispatch, never for github-actions[bot].
17+
# 3. The bump commit message itself contains "[skip ci]" so even if (1) and
18+
# (2) ever break, the message still signals "do not republish".
19+
#
20+
# Concurrency: serialize runs so two pushes close together don't race on the
21+
# bump commit's git push.
22+
#
23+
# Flow per run:
524
# 1. bump-version : increment the version (carry-at-9 via tools/bump_version.py),
6-
# commit it back to master.
25+
# commit it back to master with [skip ci].
726
# 2. release-build : build the dists from the bumped master.
827
# 3. pypi-publish : upload to PyPI via Trusted Publishing (OIDC, no tokens).
928
# 4. github-release: create the v<version> GitHub Release with notes + dists.
@@ -13,14 +32,36 @@
1332
name: Upload Python Package
1433

1534
on:
35+
push:
36+
branches: [master]
37+
# Only publish when source files change. Docs/tests/tooling commits do not
38+
# cut a release; mixed commits do (the filter matches if ANY changed file
39+
# matches).
40+
paths:
41+
- 'src/PyDiffGame/**'
42+
- 'pyproject.toml'
1643
workflow_dispatch:
1744

1845
permissions:
1946
contents: read
2047

48+
concurrency:
49+
group: publish-master
50+
cancel-in-progress: false
51+
2152
jobs:
2253
bump-version:
2354
runs-on: ubuntu-latest
55+
# Defense-in-depth loop guard: only release for human commits or manual
56+
# dispatch — never for the workflow's own [skip ci] bump commit.
57+
if: >-
58+
${{
59+
github.event_name == 'workflow_dispatch' ||
60+
(
61+
github.actor != 'github-actions[bot]' &&
62+
!contains(github.event.head_commit.message, '[skip ci]')
63+
)
64+
}}
2465
permissions:
2566
contents: write
2667
outputs:

CLAUDE.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@
1616
do not bump it in ordinary PRs — the release run does it.
1717

1818
## 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`).
19+
- **Continuous deployment, gated on source changes.** Every commit to `master`
20+
that touches `src/PyDiffGame/**` or `pyproject.toml` automatically triggers
21+
the publish workflow. Docs / tests / tooling / CI changes do **not** trigger
22+
a release on their own; mixed commits do.
23+
- The workflow auto-increments the version (`tools/bump_version.py`), commits
24+
the bump to `master` as `chore: bump version to X.Y.Z [skip ci]`, builds with
25+
`uv build`, publishes to PyPI via Trusted Publishing (OIDC, no tokens), and
26+
creates the matching `v<version>` GitHub Release with notes and the built
27+
dists attached. It is idempotent (`skip-existing`).
28+
- Manual on-demand publish stays available via
29+
`Actions -> Upload Python Package -> Run workflow` (`workflow_dispatch`).
30+
- Three independent guards prevent the bump commit from re-triggering the
31+
workflow (an infinite loop): `[skip ci]` in the message (which GitHub Actions
32+
natively honors), an explicit job-level `if:` filtering out
33+
`github-actions[bot]`, and `paths:` filter scoping to source files.
2434

2535
## Docs
2636
- `README.md` is the single canonical readme and is also the PyPI long-description

CONTRIBUTING.md

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,52 @@ they pass locally.
4141

4242
## Releasing
4343

44-
Publishing a new version is a single automated step — just run the publish
45-
workflow: **Actions -> Upload Python Package -> Run workflow** (on `master`).
44+
The project is on **continuous deployment for source changes**: every commit
45+
to `master` that touches source files automatically cuts a new release.
4646

47-
The run automatically:
47+
A commit triggers a release when it changes any of:
48+
49+
- `src/PyDiffGame/**` — the package itself
50+
- `pyproject.toml` — packaging metadata, dependencies, classifiers
51+
52+
Docs (`*.md`, `docs/**`), tests (`tests/**`), tooling (`tools/**`, `.github/**`,
53+
`.pre-commit-config.yaml`), images and the lock file do **not** trigger a
54+
release on their own. A mixed commit (e.g. `src/foo.py` + `README.md`) does
55+
trigger one — the path filter matches if any changed file matches.
56+
57+
When a release-triggering commit lands on `master`, the publish workflow:
4858

4959
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`.
60+
component over at 9 (`2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0`), updates both
61+
`pyproject.toml` and `src/PyDiffGame/__init__.py`, and commits the bump back
62+
to `master` as `chore: bump version to X.Y.Z [skip ci]`.
5363
2. Builds the distributions with `uv build`.
5464
3. Uploads them to PyPI via
5565
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no
5666
tokens or secrets).
5767
4. Creates a `v<version>` GitHub Release with auto-generated notes and the built
5868
wheels/sdist attached.
5969

70+
The workflow can also be triggered manually from **Actions -> Upload Python
71+
Package -> Run workflow** if you ever need to re-run it.
72+
6073
You normally never edit the version by hand. To bump it locally (e.g. to test),
6174
run `uv run python tools/bump_version.py` (`--dry-run` to preview, `--current`
6275
to print the current version). The PyPI upload is idempotent (`skip-existing`),
6376
so re-running the workflow is safe.
6477

78+
### What stops an infinite loop?
79+
80+
The publish workflow itself commits the version bump back to `master`. Three
81+
independent guards stop that commit from re-triggering the workflow:
82+
83+
1. The bump commit message contains `[skip ci]`, which GitHub Actions natively
84+
honors by **not creating a workflow run at all** for that push.
85+
2. The `bump-version` job has an explicit `if:` that skips when the actor is
86+
`github-actions[bot]` or the head-commit message contains `[skip ci]`.
87+
3. `bump_version.py` is the single source of truth for the version, so a
88+
tampered bump commit still wouldn't double-bump.
89+
6590
Thank you for your contribution!
6691

6792
Joshua Shay Kricheli

tools/render_logo.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
from pathlib import Path
1818

1919
import numpy as np
20-
from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFilter
21-
20+
from PIL import Image, ImageChops, ImageEnhance, ImageFilter
2221

2322
ROOT = Path(__file__).resolve().parent.parent
2423
# Source is the immutable original (red mark + black wordmark) committed

0 commit comments

Comments
 (0)