-
Notifications
You must be signed in to change notification settings - Fork 42
280 lines (259 loc) · 10 KB
/
Copy pathrelease.yml
File metadata and controls
280 lines (259 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# Branch-based publishing workflow.
#
# Release policy:
# - Pushing/merging into test-release publishes the exact package version to TestPyPI.
# - Pushing/merging into release publishes the exact package version to PyPI,
# creates the matching git tag, and creates the GitHub Release.
# - Pushing/merging into main is intentionally not a release trigger.
#
# Common maintainer commands:
# git push origin main:test-release # TestPyPI rehearsal
# git push origin main:release # Real PyPI release
#
# Web UI equivalent:
# Open a PR with base=test-release or base=release and compare=main, then merge it.
#
# Trusted publishing setup:
# Configure pending/trusted publishers on TestPyPI and PyPI with:
# Repository: evaleval/every_eval_ever
# Workflow: release.yml
# Environment: testpypi # for TestPyPI
# Environment: pypi # for PyPI
#
# This workflow intentionally does not use PyPI API tokens. The publish jobs request
# id-token: write only inside their protected environments, which allows PyPI's
# trusted-publishing/OIDC handshake to issue short-lived credentials.
name: Release
on:
push:
branches:
- test-release
- release
# Default to no permissions. Each job opts into only what it needs.
permissions: {}
jobs:
build:
name: Test and build distributions
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
- name: Set up uv
uses: astral-sh/setup-uv@v7.6.0
with:
enable-cache: true
# The package version is the single source of truth for release naming.
# Both TestPyPI and PyPI receive the exact version from pyproject.toml.
# No CI-only version rewriting happens in this workflow.
#
# Release candidates are represented directly in pyproject.toml using
# normal Python packaging / PEP 440 spelling, e.g. 0.2.0rc1. That gives:
# package version: 0.2.0rc1
# git tag: v0.2.0rc1
# GitHub Release: prerelease=true
# Later, the final release should change pyproject.toml to 0.2.0, which
# produces tag v0.2.0 and a non-prerelease GitHub Release.
- name: Read package version
id: version
shell: bash
run: |
version="$(python3 - <<'PY'
import tomllib
with open('pyproject.toml', 'rb') as file:
print(tomllib.load(file)['project']['version'])
PY
)"
is_prerelease="$(VERSION="${version}" python3 - <<'PY'
import os
import re
version = os.environ['VERSION'].lower()
# Keep this intentionally small: we only need to decide whether the
# GitHub Release should be marked as a prerelease. The packaging
# backend remains responsible for validating the project version.
# Matches common prerelease spellings such as 0.2.0a1, 0.2.0b1,
# 0.2.0rc1, and normalized/separator variants like 0.2.0-rc1.
print('true' if re.search(r'(^|[0-9._+-])(a|b|rc)[0-9]+($|[0-9._+-])', version) else 'false')
PY
)"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tag=v${version}" >> "$GITHUB_OUTPUT"
echo "is_prerelease=${is_prerelease}" >> "$GITHUB_OUTPUT"
echo "Package version: ${version}"
echo "Release tag: v${version}"
echo "GitHub prerelease: ${is_prerelease}"
# Use uv directly and require the lockfile to match the project metadata.
# If dependencies changed without updating the lockfile, this fails early.
- name: Run tests
run: uv run --locked pytest tests -v
# Build both wheel and sdist into dist/ using the project's configured backend.
- name: Build package
run: uv build
- name: Show built distributions
run: ls -la dist
# The publish jobs consume the exact artifacts built and tested here.
# This keeps the trusted-publishing jobs small and avoids rebuilding in
# jobs that have elevated OIDC publishing permissions.
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: python-distributions
path: dist/*
if-no-files-found: error
prepare-release-tag:
name: Prepare release tag
needs: build
runs-on: ubuntu-latest
permissions:
# The release branch uses this job to push v<project.version>.
# The test-release branch runs the same logic but uses git push --dry-run,
# which exercises the refspec/auth path without creating a remote tag.
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6.0.2
with:
fetch-depth: 0
# This job is what makes the branch workflow safer than manual tagging:
# maintainers push/merge main into release, and CI derives the tag from
# pyproject.toml. If the tag already exists, it must point at this exact
# commit; otherwise the workflow fails rather than moving an existing tag.
#
# On test-release, this creates the tag locally and dry-runs the remote
# push. That gives confidence in the release tag logic without cluttering
# the repository with rehearsal tags.
- name: Create, verify, or rehearse tag
shell: bash
env:
TAG: ${{ needs.build.outputs.tag }}
BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
git fetch --force --tags origin
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
if git rev-parse --verify --quiet "${TAG}^{commit}" >/dev/null; then
tag_target="$(git rev-list -n 1 "${TAG}")"
if [[ "${tag_target}" != "${GITHUB_SHA}" ]]; then
echo "Tag ${TAG} already exists but points at ${tag_target}, not ${GITHUB_SHA}" >&2
exit 1
fi
echo "Tag ${TAG} already points at this commit."
else
echo "Creating local annotated tag ${TAG} at ${GITHUB_SHA}."
git tag -a "${TAG}" -m "${TAG}"
fi
if [[ "${BRANCH}" == "release" ]]; then
echo "Pushing ${TAG} for the real release."
git push origin "${TAG}"
elif [[ "${BRANCH}" == "test-release" ]]; then
echo "Dry-running ${TAG} push for TestPyPI rehearsal."
git push --dry-run origin "${TAG}"
echo "Dry-run complete. No remote tag was created."
else
echo "Unexpected branch: ${BRANCH}" >&2
exit 1
fi
publish-testpypi:
name: Publish to TestPyPI
needs:
- build
- prepare-release-tag
if: github.ref == 'refs/heads/test-release'
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/every-eval-ever
permissions:
contents: read
# Required for PyPI/TestPyPI trusted publishing. Do not replace this with
# username/password or TWINE_* secrets.
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: python-distributions
path: dist
# TestPyPI is a rehearsal lane. skip-existing makes repeated attempts with
# the same version harmless, which is useful while validating publisher setup.
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
publish-pypi:
name: Publish to PyPI
needs:
- build
- prepare-release-tag
if: github.ref == 'refs/heads/release'
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/every-eval-ever
permissions:
contents: read
# Required for PyPI trusted publishing. Keep this permission scoped to the
# publishing job, not the whole workflow.
id-token: write
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: python-distributions
path: dist
# Real PyPI releases should fail on duplicate versions, so skip-existing is
# intentionally not set here.
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
github-release:
name: Create GitHub Release
needs:
- build
- publish-pypi
if: github.ref == 'refs/heads/release'
runs-on: ubuntu-latest
permissions:
# Needed for gh release create.
contents: write
steps:
- name: Download distributions
uses: actions/download-artifact@v4
with:
name: python-distributions
path: dist
# Create the GitHub Release only after PyPI publish succeeds, so the GitHub
# release page means the package is live on PyPI.
#
# This job intentionally does not check out the repository. Pass --repo so
# gh knows which repository to operate on, and pass --verify-tag so the
# release can only be created for the tag pushed by prepare-release-tag.
#
# If pyproject.toml contains an RC / alpha / beta version such as
# 0.2.0rc1, the GitHub Release is marked as a prerelease automatically.
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.build.outputs.tag }}
IS_PRERELEASE: ${{ needs.build.outputs.is_prerelease }}
shell: bash
run: |
set -euo pipefail
args=(
release create "${TAG}" dist/*
--repo "${GITHUB_REPOSITORY}"
--verify-tag
--title "${TAG}"
--generate-notes
)
if [[ "${IS_PRERELEASE}" == "true" ]]; then
args+=(--prerelease)
fi
gh "${args[@]}"