Skip to content

Commit 7f3aa9a

Browse files
jantmanclaude
andcommitted
Automate release on pyproject version bump
Switch the release model from "push a tag" to "bump the version in pyproject.toml on main." This matches the pattern used in our other modern repos (e.g. equipment-status-board). release.yml now triggers on push to main, reads the version from pyproject.toml, and compares it to the latest GitHub release. When the version is higher it auto-tags the commit (bare version, no `v` prefix, matching existing tags), builds and pushes the Docker image to GHCR, publishes to PyPI, and creates a GitHub release with generated notes plus docker-pull / PyPI pointers. When the version is unchanged the workflow no-ops. Carries forward the lowercase-GHCR-repo fix and the SBOM/OCI labels; the image version label now comes from the detected version rather than github.ref_name. Docs updated accordingly: contributing.rst Release Process and a new Releasing section in CLAUDE.md (README has no release content). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b15ac5e commit 7f3aa9a

3 files changed

Lines changed: 90 additions & 34 deletions

File tree

.github/workflows/release.yml

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,114 @@
22
#
33
# 1. In repository Settings -> Actions -> General, ensure "Allow all actions and reusable workflows" is selected, and that under "Workflow permissions", "Read repository contents and packages permissions" is checked.
44
#
5-
name: Release on Tag
5+
# Releases are driven by the version in pyproject.toml: when a commit lands on
6+
# main with a version higher than the latest GitHub release, this workflow
7+
# tags it, builds and pushes the Docker image to GHCR, publishes to PyPI, and
8+
# creates a GitHub release. No manual tagging required.
9+
name: Release
610
on:
711
push:
8-
tags:
9-
- '*'
12+
branches:
13+
- main
14+
permissions:
15+
contents: write
16+
packages: write
1017
jobs:
1118
release:
1219
runs-on: ubuntu-latest
13-
permissions:
14-
contents: write
15-
packages: write
1620
steps:
1721
- uses: actions/checkout@v6
18-
- name: Set up Python
19-
uses: actions/setup-python@v6
2022
with:
21-
python-version: "3.13"
22-
- name: Install dependencies
23-
run: python -m pip install --upgrade pip && pip install poetry && poetry install
24-
- name: Get Version
25-
id: get-version
26-
run: echo "APP_VERSION=$(poetry version -s)" >> $GITHUB_OUTPUT
27-
- name: Ensure tag matches version
28-
if: github.ref_name != steps.get-version.outputs.APP_VERSION
23+
fetch-depth: 0
24+
- name: Check version vs. latest release
25+
id: version_check
26+
env:
27+
GH_TOKEN: ${{ github.token }}
2928
run: |
30-
echo "ERROR: tag name (${{ github.ref_name }}) does not match current version in version.py (${{ steps.get-version.outputs.APP_VERSION }})"
31-
exit 2
29+
CURRENT_VERSION=$(python3 -c "
30+
import re, pathlib
31+
text = pathlib.Path('pyproject.toml').read_text()
32+
m = re.search(r'^version\s*=\s*\"([^\"]+)\"', text, re.MULTILINE)
33+
print(m.group(1))
34+
")
35+
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
36+
37+
LATEST_TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName // empty' 2>/dev/null || true)
38+
39+
if [ -z "$LATEST_TAG" ]; then
40+
echo "No existing releases found — first release"
41+
echo "should_release=true" >> "$GITHUB_OUTPUT"
42+
else
43+
LATEST_VERSION="${LATEST_TAG#v}"
44+
SHOULD=$(python3 -c "
45+
current = tuple(int(x) for x in '$CURRENT_VERSION'.split('.'))
46+
latest = tuple(int(x) for x in '$LATEST_VERSION'.split('.'))
47+
print('true' if current > latest else 'false')
48+
")
49+
echo "Latest release: $LATEST_TAG, current version: $CURRENT_VERSION, should_release: $SHOULD"
50+
echo "should_release=$SHOULD" >> "$GITHUB_OUTPUT"
51+
fi
3252
- name: Set up Docker Buildx
53+
if: steps.version_check.outputs.should_release == 'true'
3354
uses: docker/setup-buildx-action@v4
3455
- name: Login to GHCR
56+
if: steps.version_check.outputs.should_release == 'true'
3557
run: echo "${{secrets.GITHUB_TOKEN}}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
3658
- name: Lowercase image repository name
59+
if: steps.version_check.outputs.should_release == 'true'
3760
id: image
3861
run: echo "repo=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
3962
- name: Docker Build and Push
63+
if: steps.version_check.outputs.should_release == 'true'
4064
uses: docker/build-push-action@v7
4165
with:
4266
push: true
4367
sbom: true
4468
labels: |
4569
org.opencontainers.image.url=https://github.com/${{ github.repository }}
4670
org.opencontainers.image.source=https://github.com/${{ github.repository }}
47-
org.opencontainers.image.version=${{ github.ref_name }}
71+
org.opencontainers.image.version=${{ steps.version_check.outputs.current_version }}
4872
org.opencontainers.image.revision=${{ github.sha }}
4973
tags: |
50-
ghcr.io/${{ steps.image.outputs.repo }}:${{ github.ref_name }}
74+
ghcr.io/${{ steps.image.outputs.repo }}:${{ steps.version_check.outputs.current_version }}
5175
ghcr.io/${{ steps.image.outputs.repo }}:latest
5276
- name: Build and publish to pypi
77+
if: steps.version_check.outputs.should_release == 'true'
5378
uses: JRubics/poetry-publish@v2.1
5479
with:
5580
pypi_token: ${{ secrets.PYPI_TOKEN }}
56-
- name: Create Release
57-
id: create_release
58-
uses: softprops/action-gh-release@v3
81+
- name: Create git tag
82+
if: steps.version_check.outputs.should_release == 'true'
83+
run: |
84+
VERSION="${{ steps.version_check.outputs.current_version }}"
85+
git tag "$VERSION"
86+
git push origin "$VERSION"
87+
- name: Create GitHub Release
88+
if: steps.version_check.outputs.should_release == 'true'
5989
env:
60-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
61-
with:
62-
tag_name: ${{ github.ref_name }}
63-
name: Release ${{ github.ref_name }}
64-
body: |
65-
Release ${{ github.ref_name }}.
66-
Docker images: https://github.com/orgs/Decaturmakers/packages/container/package/machine-access-control
67-
Python packages: https://pypi.org/project/machine_access_control/
68-
draft: false
69-
prerelease: false
90+
GH_TOKEN: ${{ github.token }}
91+
run: |
92+
VERSION="${{ steps.version_check.outputs.current_version }}"
93+
REPO="${{ github.repository }}"
94+
IMAGE="ghcr.io/${{ steps.image.outputs.repo }}"
95+
96+
# Generate changelog via GitHub API
97+
CHANGELOG=$(gh api \
98+
"repos/$REPO/releases/generate-notes" \
99+
-f tag_name="$VERSION" \
100+
-f target_commitish="main" \
101+
--jq '.body')
102+
103+
# Build release body in a file to avoid shell escaping issues
104+
{
105+
printf '%s\n' "$CHANGELOG"
106+
printf '\n---\n\n## Docker Image\n\n```bash\n'
107+
printf 'docker pull %s:%s\n' "$IMAGE" "$VERSION"
108+
printf 'docker pull %s:latest\n' "$IMAGE"
109+
printf '```\n'
110+
printf '\n## Python Package\n\nhttps://pypi.org/project/machine_access_control/%s/\n' "$VERSION"
111+
} > /tmp/release_body.md
112+
113+
gh release create "$VERSION" \
114+
--title "Release $VERSION" \
115+
--notes-file /tmp/release_body.md

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,16 @@ poetry run neon-fob-adder --csv members.csv --field account_id
9292

9393
Both tools use the same environment variables: ``NEON_ORG``, ``NEON_KEY``, and ``NEONGETTER_CONFIG``.
9494

95+
### Releasing
96+
Releases are automated and driven by the version in `pyproject.toml` — do **not** create or push git tags manually.
97+
98+
```bash
99+
# Bump the version (minor shown; use major/patch as appropriate), then commit and merge to main
100+
poetry version minor
101+
```
102+
103+
On every push to `main`, `.github/workflows/release.yml` compares the `pyproject.toml` version against the latest GitHub release. If it is higher, the workflow automatically creates the git tag (bare version, e.g. `0.14.0` — no `v` prefix), builds and pushes the Docker image to GHCR, publishes to PyPI, and creates a GitHub release with generated notes. If the version is unchanged, the workflow no-ops.
104+
95105
## Architecture
96106

97107
### Core Components

docs/source/contributing.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,4 @@ your approach.
138138
Release Process
139139
---------------
140140

141-
Use ``poetry version`` to increment the version number, commit push and merge that. Tag the repo and push the tag. `GitHub Actions <https://github.com/Decaturmakers/machine-access-control/actions>`__ will run a Docker build, push to GHCR (GitHub Container Registry), build to PyPI, and create a release on the repo.
141+
Releases are driven by the version number in ``pyproject.toml``. Use ``poetry version`` to increment the version number, then commit, push, and merge that to ``main``. When a commit lands on ``main`` with a version higher than the latest GitHub release, the release `GitHub Actions <https://github.com/Decaturmakers/machine-access-control/actions>`__ workflow automatically tags the commit, runs a Docker build and pushes it to GHCR (GitHub Container Registry), builds and publishes to PyPI, and creates a release on the repo. No manual tagging is required.

0 commit comments

Comments
 (0)