-
Notifications
You must be signed in to change notification settings - Fork 775
196 lines (188 loc) · 8.96 KB
/
Copy pathrelease.yml
File metadata and controls
196 lines (188 loc) · 8.96 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
# Tagged-release wheels: build the cp310-cp313 runtime matrix + the kernel-cache
# wheel on the self-hosted node, then publish
# - runtime wheels (manylinux, bare version) -> PyPI, via twine + an API token
# - kernel-cache wheel (<version>+cu130) + runtime -> a DRAFT GitHub release on this repo
#
# INERT until a v* tag is pushed. The tag must point at a commit whose version.py
# matches (vX.Y.Z == __version__ X.Y.Z); scripts/build-release-wheels.sh enforces
# this in FREETOKEN_BUILD_RELEASE mode and refuses otherwise.
#
# This lane is separate from nightly-wheels.yml on purpose:
# - nightly ships +g<sha>-stamped, linux_x86_64-tagged cp312 wheels to the
# FreeToken-Web rolling `beta` release, which shipped Desktops resolve by
# asset name -- that channel's naming must not change.
# - release ships bare-versioned, manylinux-retagged cp310-cp313 wheels, which
# is what PyPI accepts.
#
# Security shape mirrors nightly-wheels.yml: fork code never reaches the
# self-hosted node (no pull_request trigger + repo guard), and credentials only
# exist on hosted runners. The upload tokens are ENVIRONMENT secrets on `pypi` /
# `testpypi`, so the environment's reviewers gate every use of them -- NOT the
# existing `release` environment, which holds the FreeToken-Web PAT and must
# stay off this lane.
#
# One-time owner setup:
# - GitHub environments `pypi` and `testpypi`, `pypi` WITH required reviewers:
# once this workflow is on main, any v* tag reaches publish-pypi, and that
# reviewer gate is the only thing in front of it.
# - Environment secret `PYPI_TOKEN` on `pypi` and `TEST_PYPI_TOKEN` on
# `testpypi`. Scope each token to the single project `freetoken`, never
# account-wide, and store them per-environment rather than repo-wide.
name: Release wheels
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
testpypi_rehearsal:
description: "Build .dev<run> wheels and publish them to TestPyPI (never touches PyPI)"
type: boolean
default: false
permissions:
contents: read
# Per-ref group, own namespace: sharing nightly-wheels' group would queue a release
# behind a 40-minute nightly build, and a SHARED release group would let a newer tag
# replace an earlier tag's still-PENDING run (GitHub keeps at most one pending run
# per group). Per-ref, back-to-back tags each keep their run; the single build
# runner serializes them anyway. No cancel-in-progress -- never kill a release mid-way.
concurrency:
group: release-wheels-${{ github.ref }}
jobs:
build:
# Guard against runs from forks of this repo; update on an org transfer.
# Mode is keyed on the EVENT, never on ref_type: a workflow_dispatch can be
# pointed at a tag ref, and that must stay a rehearsal, not become a release.
if: >-
github.repository == 'FlashML-org/FreeToken' &&
(github.event_name == 'push' || inputs.testpypi_rehearsal)
runs-on: [self-hosted, linux, engine-build]
timeout-minutes: 60
steps:
# The build container runs as root; an interrupted build can leave root-owned
# files that a plain checkout cannot delete. Wipe via a root container first.
- name: Clean workspace
run: |
docker run --rm -v "${{ github.workspace }}:/workspace" alpine:3 \
sh -c 'rm -rf /workspace/..?* /workspace/.[!.]* /workspace/*' || true
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
# Full history + tags: release mode verifies HEAD == tag == version.py
# via `git describe --exact-match`.
fetch-depth: 0
- name: Build wheels (manylinux container, cp310-cp313)
run: |
if [ "${{ github.event_name }}" = "push" ]; then
export FREETOKEN_BUILD_RELEASE=1
else
# Rehearsal: .dev<run_id> versions -- PEP 440-clean (no local
# segment) AND unique per run, so repeated rehearsals never collide
# with TestPyPI's no-reupload-ever rule. run_id, not run_number:
# run_number is stable across "Re-run all jobs", which would rebuild
# the identical filenames and let --skip-existing turn a re-run into
# a green no-op that leaves the previous attempt's bytes published.
export FREETOKEN_BUILD_DEV_STAMP="${{ github.run_id }}"
fi
export FT_PYTHON_MATRIX="cp310 cp311 cp312 cp313"
export FT_MANYLINUX_RETAG=1
scripts/ci/manylinux-build.sh
- name: Check the wheel set
run: |
ls -l dist/
rt="$(ls dist/freetoken-*manylinux*.whl 2>/dev/null | wc -l)"
kc="$(ls dist/freetoken_kernel_cache-*.whl 2>/dev/null | wc -l)"
[ "$rt" -eq 4 ] || { echo "expected 4 manylinux runtime wheels, got $rt" >&2; exit 1; }
[ "$kc" -eq 1 ] || { echo "expected 1 kernel-cache wheel, got $kc" >&2; exit 1; }
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-wheels
path: dist/*.whl
if-no-files-found: error
# Must outlast the `pypi` environment's review gate: GitHub lets a
# pending deployment sit for up to 30 days, and an expired artifact
# means redoing the whole 4-interpreter self-hosted build.
retention-days: 30
publish-testpypi:
needs: build
if: github.event_name == 'workflow_dispatch' && inputs.testpypi_rehearsal
runs-on: ubuntu-latest
timeout-minutes: 10
environment: testpypi
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-wheels
path: dist
- name: Select the PyPI wheel set (runtime only, no local versions)
run: |
mkdir pypi-dist
cp dist/freetoken-*manylinux*.whl pypi-dist/
# The kernel-cache wheel stays on GitHub releases; a +local version
# here would be rejected by (Test)PyPI anyway -- fail early and loudly.
if ls pypi-dist/ | grep -qF '+'; then
echo "local-versioned wheel in the PyPI upload set:" >&2; ls pypi-dist/ >&2; exit 1
fi
pipx run twine check --strict pypi-dist/*
# --skip-existing keeps a partial rerun safe: (Test)PyPI never accepts the
# same filename twice, so without it a rerun after a mid-upload failure
# fails on the wheels that already landed.
- name: Upload to TestPyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }}
TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/
run: pipx run twine upload --verbose --skip-existing pypi-dist/*
publish-pypi:
needs: build
# Tag PUSH only -- a workflow_dispatch pointed at a tag ref must never reach PyPI.
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-wheels
path: dist
- name: Select the PyPI wheel set (runtime only, no local versions)
run: |
mkdir pypi-dist
cp dist/freetoken-*manylinux*.whl pypi-dist/
if ls pypi-dist/ | grep -qF '+'; then
echo "local-versioned wheel in the PyPI upload set:" >&2; ls pypi-dist/ >&2; exit 1
fi
pipx run twine check --strict pypi-dist/*
# See the note on --skip-existing in publish-testpypi. It matters more here:
# a PyPI filename burned by a half-finished upload can never be reused.
- name: Upload to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: pipx run twine upload --verbose --skip-existing pypi-dist/*
# Upload-only draft release on THIS repo carrying every wheel, most importantly
# the kernel-cache one PyPI cannot host. Deliberately not scripts/publish-wheels.sh:
# its prune-then-upload semantics and stamp-pairing gate are for the rolling beta
# channel, and would be wrong against an immutable version tag.
github-release:
needs: build
if: github.event_name == 'push' && github.ref_type == 'tag'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-wheels
path: dist
- name: Create draft release with all wheels
env:
GH_TOKEN: ${{ github.token }}
run: |
# Idempotent for re-runs: create the draft, or refresh assets on the
# existing release if a previous run got that far.
gh release create "$GITHUB_REF_NAME" --draft --verify-tag \
--title "$GITHUB_REF_NAME" \
--notes "Draft -- edit notes, then publish." \
-R "$GITHUB_REPOSITORY" \
dist/*.whl \
|| gh release upload "$GITHUB_REF_NAME" dist/*.whl --clobber -R "$GITHUB_REPOSITORY"