Skip to content

Commit a5854ee

Browse files
authored
chore: Publish python via trusted publishing and unify release process (#183)
Part 2 for #180 adds pypi trusted publishing, and then unifies the release process to publish both python and javascript libraries at once. Also bumps versions, so can test cutting a release after this.
1 parent 398ded6 commit a5854ee

8 files changed

Lines changed: 490 additions & 35 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import re
4+
import sys
5+
from pathlib import Path
6+
7+
ROOT = Path(__file__).resolve().parents[2]
8+
PACKAGE_JSON = ROOT / "package.json"
9+
PY_VERSION = ROOT / "py" / "autoevals" / "version.py"
10+
11+
package_version = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))["version"]
12+
match = re.search(
13+
r'^VERSION\s*=\s*["\']([^"\']+)["\']\s*$',
14+
PY_VERSION.read_text(encoding="utf-8"),
15+
re.MULTILINE,
16+
)
17+
18+
if not match:
19+
print(f"Could not parse VERSION from {PY_VERSION}", file=sys.stderr)
20+
sys.exit(1)
21+
22+
python_version = match.group(1)
23+
24+
if package_version != python_version:
25+
print(
26+
"Version mismatch detected:\n"
27+
f"- package.json: {package_version}\n"
28+
f"- py/autoevals/version.py: {python_version}",
29+
file=sys.stderr,
30+
)
31+
sys.exit(1)
32+
33+
print(f"Versions are in sync: {package_version}")

.github/workflows/publish-js.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ on:
2020
required: true
2121
default: main
2222
type: string
23+
prerelease_suffix:
24+
description: Optional shared prerelease suffix
25+
required: false
26+
default: ""
27+
type: string
2328

2429
jobs:
2530
prepare-release:
@@ -36,6 +41,8 @@ jobs:
3641
with:
3742
fetch-depth: 1
3843
ref: ${{ inputs.branch }}
44+
- name: Check version sync
45+
run: python3 .github/scripts/check_version_sync.py
3946
- name: Set up Node.js
4047
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
4148
with:
@@ -45,12 +52,17 @@ jobs:
4552
env:
4653
RELEASE_TYPE: ${{ inputs.release_type }}
4754
TARGET_BRANCH: ${{ inputs.branch }}
55+
PRERELEASE_SUFFIX: ${{ inputs.prerelease_suffix }}
4856
run: |
4957
set -euo pipefail
5058
5159
CURRENT_VERSION=$(node -p "require('./package.json').version")
5260
RELEASE_COMMIT=$(git rev-parse HEAD)
5361
62+
if [[ -z "${PRERELEASE_SUFFIX}" ]]; then
63+
PRERELEASE_SUFFIX="${GITHUB_RUN_NUMBER}"
64+
fi
65+
5466
echo "release_type=${RELEASE_TYPE}" >> "$GITHUB_OUTPUT"
5567
echo "branch=${TARGET_BRANCH}" >> "$GITHUB_OUTPUT"
5668
echo "commit=${RELEASE_COMMIT}" >> "$GITHUB_OUTPUT"
@@ -66,7 +78,7 @@ jobs:
6678
echo "version=${CURRENT_VERSION}" >> "$GITHUB_OUTPUT"
6779
echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
6880
else
69-
VERSION="${CURRENT_VERSION}-rc.${GITHUB_RUN_NUMBER}"
81+
VERSION="${CURRENT_VERSION}-rc.${PRERELEASE_SUFFIX}"
7082
7183
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
7284
echo "release_tag=" >> "$GITHUB_OUTPUT"
@@ -79,7 +91,6 @@ jobs:
7991
permissions:
8092
contents: write
8193
id-token: write
82-
environment: npm-publish
8394
env:
8495
PACKAGE_NAME: autoevals
8596
VERSION: ${{ needs.prepare-release.outputs.version }}
@@ -93,6 +104,9 @@ jobs:
93104
fetch-depth: 0
94105
ref: ${{ needs.prepare-release.outputs.branch }}
95106

107+
- name: Check version sync
108+
run: python3 .github/scripts/check_version_sync.py
109+
96110
- name: Set up Node.js
97111
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
98112
with:
@@ -160,7 +174,7 @@ jobs:
160174
owner: context.repo.owner,
161175
repo: context.repo.repo,
162176
tag_name: process.env.RELEASE_TAG,
163-
name: `autoevals v${process.env.VERSION}`,
177+
name: `autoevals JavaScript v${process.env.VERSION}`,
164178
draft: false,
165179
prerelease: false,
166180
generate_release_notes: true,

.github/workflows/publish-py.yaml

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
name: publish-py
2+
3+
concurrency:
4+
group: publish-py-${{ inputs.release_type }}-${{ inputs.branch }}
5+
cancel-in-progress: false
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
release_type:
11+
description: Release type
12+
required: true
13+
default: stable
14+
type: choice
15+
options:
16+
- stable
17+
- prerelease
18+
branch:
19+
description: Branch to release from
20+
required: true
21+
default: main
22+
type: string
23+
prerelease_suffix:
24+
description: Optional shared prerelease suffix
25+
required: false
26+
default: ""
27+
type: string
28+
29+
jobs:
30+
prepare-release:
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 10
33+
outputs:
34+
version: ${{ steps.release_metadata.outputs.version }}
35+
release_tag: ${{ steps.release_metadata.outputs.release_tag }}
36+
branch: ${{ steps.release_metadata.outputs.branch }}
37+
commit: ${{ steps.release_metadata.outputs.commit }}
38+
release_type: ${{ steps.release_metadata.outputs.release_type }}
39+
steps:
40+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
41+
with:
42+
fetch-depth: 1
43+
ref: ${{ inputs.branch }}
44+
45+
- name: Check version sync
46+
run: python3 .github/scripts/check_version_sync.py
47+
48+
- name: Set up Python
49+
uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3.1.4
50+
with:
51+
python-version: "3.12"
52+
53+
- name: Determine release metadata
54+
id: release_metadata
55+
env:
56+
RELEASE_TYPE: ${{ inputs.release_type }}
57+
TARGET_BRANCH: ${{ inputs.branch }}
58+
PRERELEASE_SUFFIX: ${{ inputs.prerelease_suffix }}
59+
run: |
60+
set -euo pipefail
61+
62+
CURRENT_VERSION=$(python -c 'from pathlib import Path; ns = {}; exec(Path("py/autoevals/version.py").read_text(encoding="utf-8"), ns); print(ns["VERSION"])')
63+
RELEASE_COMMIT=$(git rev-parse HEAD)
64+
65+
if [[ -z "${PRERELEASE_SUFFIX}" ]]; then
66+
PRERELEASE_SUFFIX="${GITHUB_RUN_NUMBER}"
67+
fi
68+
69+
echo "release_type=${RELEASE_TYPE}" >> "$GITHUB_OUTPUT"
70+
echo "branch=${TARGET_BRANCH}" >> "$GITHUB_OUTPUT"
71+
echo "commit=${RELEASE_COMMIT}" >> "$GITHUB_OUTPUT"
72+
73+
if [[ "$RELEASE_TYPE" == "stable" ]]; then
74+
RELEASE_TAG="py-${CURRENT_VERSION}"
75+
76+
if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
77+
echo "Tag ${RELEASE_TAG} already exists on origin" >&2
78+
exit 1
79+
fi
80+
81+
echo "version=${CURRENT_VERSION}" >> "$GITHUB_OUTPUT"
82+
echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
83+
else
84+
VERSION="${CURRENT_VERSION}rc${PRERELEASE_SUFFIX}"
85+
86+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
87+
echo "release_tag=" >> "$GITHUB_OUTPUT"
88+
fi
89+
90+
publish:
91+
needs: prepare-release
92+
runs-on: ubuntu-latest
93+
timeout-minutes: 20
94+
permissions:
95+
contents: write
96+
id-token: write # Required for PyPI trusted publishing
97+
env:
98+
PACKAGE_NAME: autoevals
99+
VERSION: ${{ needs.prepare-release.outputs.version }}
100+
RELEASE_TAG: ${{ needs.prepare-release.outputs.release_tag }}
101+
RELEASE_TYPE: ${{ needs.prepare-release.outputs.release_type }}
102+
TARGET_BRANCH: ${{ needs.prepare-release.outputs.branch }}
103+
RELEASE_COMMIT: ${{ needs.prepare-release.outputs.commit }}
104+
steps:
105+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
106+
with:
107+
fetch-depth: 0
108+
ref: ${{ needs.prepare-release.outputs.branch }}
109+
110+
- name: Check version sync
111+
run: python3 .github/scripts/check_version_sync.py
112+
113+
- name: Set up Python
114+
uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3.1.4
115+
with:
116+
python-version: "3.12"
117+
118+
- name: Check PyPI version availability
119+
run: |
120+
set -euo pipefail
121+
122+
python - <<'PY'
123+
import os
124+
import sys
125+
import urllib.error
126+
import urllib.request
127+
128+
package = os.environ["PACKAGE_NAME"]
129+
version = os.environ["VERSION"]
130+
url = f"https://pypi.org/pypi/{package}/{version}/json"
131+
132+
try:
133+
urllib.request.urlopen(url)
134+
except urllib.error.HTTPError as exc:
135+
if exc.code == 404:
136+
raise SystemExit(0)
137+
raise
138+
except urllib.error.URLError as exc:
139+
print(f"Failed to query PyPI: {exc}", file=sys.stderr)
140+
raise
141+
else:
142+
print(f"{package}=={version} already exists on PyPI", file=sys.stderr)
143+
raise SystemExit(1)
144+
PY
145+
146+
- name: Install build dependencies
147+
run: python -m pip install --upgrade pip build twine
148+
149+
- name: Prepare prerelease package metadata
150+
if: ${{ env.RELEASE_TYPE == 'prerelease' }}
151+
run: |
152+
set -euo pipefail
153+
154+
python - <<'PY'
155+
import os
156+
import re
157+
from pathlib import Path
158+
159+
path = Path("py/autoevals/version.py")
160+
text = path.read_text(encoding="utf-8")
161+
new_text, count = re.subn(
162+
r'^VERSION\s*=\s*["\'][^"\']+["\']\s*$',
163+
f'VERSION = "{os.environ["VERSION"]}"',
164+
text,
165+
count=1,
166+
flags=re.MULTILINE,
167+
)
168+
if count != 1:
169+
raise SystemExit("Could not update py/autoevals/version.py for prerelease publish")
170+
path.write_text(new_text + ("" if new_text.endswith("\n") else "\n"), encoding="utf-8")
171+
PY
172+
173+
- name: Build package
174+
run: python -m build
175+
176+
- name: Verify package metadata
177+
run: python -m twine check dist/*
178+
179+
- name: Publish to PyPI
180+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
181+
with:
182+
packages-dir: dist/
183+
184+
- name: Create and push stable release tag
185+
if: ${{ env.RELEASE_TYPE == 'stable' }}
186+
run: |
187+
set -euo pipefail
188+
189+
git config user.name "github-actions[bot]"
190+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
191+
git tag "${RELEASE_TAG}" "${RELEASE_COMMIT}"
192+
git push origin "${RELEASE_TAG}"
193+
194+
- name: Create GitHub release
195+
if: ${{ env.RELEASE_TYPE == 'stable' }}
196+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
197+
env:
198+
RELEASE_TAG: ${{ env.RELEASE_TAG }}
199+
VERSION: ${{ env.VERSION }}
200+
with:
201+
script: |
202+
await github.rest.repos.createRelease({
203+
owner: context.repo.owner,
204+
repo: context.repo.repo,
205+
tag_name: process.env.RELEASE_TAG,
206+
name: `autoevals Python v${process.env.VERSION}`,
207+
draft: false,
208+
prerelease: false,
209+
generate_release_notes: true,
210+
});
211+
212+
- name: Summarize release
213+
run: |
214+
set -euo pipefail
215+
216+
{
217+
echo "## PyPI publish complete"
218+
echo
219+
echo "- Package: \`${PACKAGE_NAME}\`"
220+
echo "- Version: \`${VERSION}\`"
221+
echo "- Release type: \`${RELEASE_TYPE}\`"
222+
if [ "${RELEASE_TYPE}" = "prerelease" ]; then
223+
echo "- Install: \`pip install --pre ${PACKAGE_NAME}\`"
224+
else
225+
echo "- Git tag: \`${RELEASE_TAG}\`"
226+
echo "- Install: \`pip install ${PACKAGE_NAME}\`"
227+
fi
228+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)