Skip to content

Commit 74d09f6

Browse files
lionuncleclaude
andcommitted
Add release workflow for PyPI, npm, and Homebrew
Triggers on push to main, no-ops if pyproject.toml version is already tagged. Otherwise builds + uploads to PyPI, publishes the npm package, bumps the homebrew-rpr tap formula with the new sdist sha256, then tags v<version> and creates a GitHub release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 58bcac3 commit 74d09f6

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
name: release
2+
3+
# Auto-publishes rpr to PyPI, npm, and the Homebrew tap whenever a merge to
4+
# main bumps the version in pyproject.toml. The workflow is a no-op if the
5+
# version hasn't changed (i.e. v<version> is already tagged).
6+
#
7+
# Required secrets (Settings → Secrets and variables → Actions):
8+
# PYPI_API_TOKEN — pypi.org token (scope: project rpr, or whole-account
9+
# until the project exists). __token__ user is implied.
10+
# NPM_TOKEN — npmjs.com automation token with publish rights.
11+
# HOMEBREW_TAP_TOKEN — fine-grained PAT with Contents: read/write on
12+
# dedev-llc/homebrew-rpr. Used to commit the bumped
13+
# formula to the tap repo.
14+
15+
on:
16+
push:
17+
branches: [main]
18+
workflow_dispatch:
19+
20+
permissions:
21+
contents: write
22+
23+
concurrency:
24+
group: release
25+
cancel-in-progress: false
26+
27+
jobs:
28+
check-version:
29+
runs-on: ubuntu-latest
30+
outputs:
31+
version: ${{ steps.v.outputs.version }}
32+
should_release: ${{ steps.v.outputs.should_release }}
33+
steps:
34+
- uses: actions/checkout@v4
35+
with:
36+
fetch-depth: 0
37+
38+
- id: v
39+
name: Read version and check if already released
40+
run: |
41+
set -euo pipefail
42+
py_version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
43+
npm_version=$(node -p "require('./npm/package.json').version")
44+
45+
if [ "$py_version" != "$npm_version" ]; then
46+
echo "::error::Version mismatch — pyproject.toml=$py_version, npm/package.json=$npm_version. Bump both."
47+
exit 1
48+
fi
49+
50+
echo "version=$py_version" >> "$GITHUB_OUTPUT"
51+
52+
if git rev-parse --verify "refs/tags/v$py_version" >/dev/null 2>&1; then
53+
echo "v$py_version already tagged — nothing to release."
54+
echo "should_release=false" >> "$GITHUB_OUTPUT"
55+
else
56+
echo "v$py_version is new — proceeding."
57+
echo "should_release=true" >> "$GITHUB_OUTPUT"
58+
fi
59+
60+
pypi:
61+
needs: check-version
62+
if: needs.check-version.outputs.should_release == 'true'
63+
runs-on: ubuntu-latest
64+
environment: release
65+
steps:
66+
- uses: actions/checkout@v4
67+
68+
- uses: actions/setup-python@v5
69+
with:
70+
python-version: "3.12"
71+
72+
- name: Install build tooling
73+
run: python -m pip install --upgrade build twine
74+
75+
- name: Build sdist + wheel
76+
run: python -m build
77+
78+
- name: Validate distributions
79+
run: twine check dist/*
80+
81+
- name: Upload to PyPI
82+
env:
83+
TWINE_USERNAME: __token__
84+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
85+
run: twine upload dist/*
86+
87+
npm:
88+
needs: [check-version, pypi]
89+
if: needs.check-version.outputs.should_release == 'true'
90+
runs-on: ubuntu-latest
91+
environment: release
92+
steps:
93+
- uses: actions/checkout@v4
94+
95+
- uses: actions/setup-node@v4
96+
with:
97+
node-version: "20"
98+
registry-url: "https://registry.npmjs.org"
99+
100+
- name: Publish to npm
101+
working-directory: npm
102+
env:
103+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
104+
run: npm publish --access public
105+
106+
homebrew:
107+
needs: [check-version, pypi]
108+
if: needs.check-version.outputs.should_release == 'true'
109+
runs-on: ubuntu-latest
110+
environment: release
111+
steps:
112+
- name: Wait for PyPI to expose new version
113+
env:
114+
VERSION: ${{ needs.check-version.outputs.version }}
115+
run: |
116+
set -euo pipefail
117+
for i in $(seq 1 30); do
118+
if curl -sSf "https://pypi.org/pypi/rpr/${VERSION}/json" >/dev/null; then
119+
echo "PyPI has rpr ${VERSION}"
120+
exit 0
121+
fi
122+
echo "Waiting for PyPI propagation... (${i}/30)"
123+
sleep 10
124+
done
125+
echo "::error::PyPI never exposed rpr ${VERSION}"
126+
exit 1
127+
128+
- name: Resolve sdist URL and sha256
129+
id: sdist
130+
env:
131+
VERSION: ${{ needs.check-version.outputs.version }}
132+
run: |
133+
set -euo pipefail
134+
json=$(curl -sSf "https://pypi.org/pypi/rpr/${VERSION}/json")
135+
url=$(echo "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin)["urls"]; s=[x for x in d if x["packagetype"]=="sdist"][0]; print(s["url"])')
136+
sha=$(echo "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin)["urls"]; s=[x for x in d if x["packagetype"]=="sdist"][0]; print(s["digests"]["sha256"])')
137+
echo "url=$url" >> "$GITHUB_OUTPUT"
138+
echo "sha=$sha" >> "$GITHUB_OUTPUT"
139+
140+
- name: Checkout tap repo
141+
uses: actions/checkout@v4
142+
with:
143+
repository: dedev-llc/homebrew-rpr
144+
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
145+
path: tap
146+
147+
- name: Update Formula/rpr.rb
148+
env:
149+
VERSION: ${{ needs.check-version.outputs.version }}
150+
URL: ${{ steps.sdist.outputs.url }}
151+
SHA: ${{ steps.sdist.outputs.sha }}
152+
run: |
153+
set -euo pipefail
154+
python3 - <<'PY'
155+
import os, pathlib, re
156+
formula = pathlib.Path("tap/Formula/rpr.rb")
157+
text = formula.read_text()
158+
text = re.sub(r'^(\s*)url\s+".*"', rf'\1url "{os.environ["URL"]}"', text, count=1, flags=re.M)
159+
text = re.sub(r'^(\s*)sha256\s+".*"', rf'\1sha256 "{os.environ["SHA"]}"', text, count=1, flags=re.M)
160+
formula.write_text(text)
161+
PY
162+
cd tap
163+
git config user.name "github-actions[bot]"
164+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
165+
git add Formula/rpr.rb
166+
git commit -m "rpr ${VERSION}"
167+
git push
168+
169+
tag-and-release:
170+
needs: [check-version, pypi, npm, homebrew]
171+
if: needs.check-version.outputs.should_release == 'true'
172+
runs-on: ubuntu-latest
173+
permissions:
174+
contents: write
175+
steps:
176+
- uses: actions/checkout@v4
177+
with:
178+
fetch-depth: 0
179+
180+
- name: Tag and create GitHub release
181+
env:
182+
GH_TOKEN: ${{ github.token }}
183+
VERSION: ${{ needs.check-version.outputs.version }}
184+
run: |
185+
set -euo pipefail
186+
git config user.name "github-actions[bot]"
187+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
188+
git tag "v${VERSION}"
189+
git push origin "v${VERSION}"
190+
gh release create "v${VERSION}" --generate-notes --title "v${VERSION}"

0 commit comments

Comments
 (0)