-
Notifications
You must be signed in to change notification settings - Fork 1
226 lines (197 loc) · 8.78 KB
/
Copy pathrelease.yml
File metadata and controls
226 lines (197 loc) · 8.78 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
name: Release Tag and Publish Package
on:
workflow_dispatch:
inputs:
version_bump:
description: "Version bump type."
required: true
default: "patch"
type: choice
options:
- patch
- minor
- major
pre_release:
description: "Publish a release candidate (`<version>rcN`) instead of the version itself. Dispatch again with the same bump and this off to promote it to stable."
required: false
default: false
type: boolean
release_notes:
description: "Release notes (optional)"
required: false
type: string
concurrency:
group: release
cancel-in-progress: false
jobs:
release-and-publish:
runs-on: ubuntu-latest
# Dispatchable from any branch by anyone who can dispatch it, and a
# candidate reaches PyPI without a commit to review: the published artefact
# must come from the reviewed branch.
if: github.ref == 'refs/heads/main'
permissions:
contents: write
# Publishing is by PyPI Trusted Publisher, so there is no API token.
id-token: write
steps:
# Checked before the checkout, so a missing one fails on the reason
# rather than on a token that renders empty three steps later.
- name: Check the release credentials are visible to this repository
env:
APP_CLIENT_ID: ${{ vars.PUSH_TO_MAIN_APP_CLIENT_ID }}
APP_PRIVATE_KEY: ${{ secrets.PUSH_TO_MAIN_APP_PRIVATE_KEY }}
run: |
missing=""
[ -n "$APP_CLIENT_ID" ] || missing="$missing vars.PUSH_TO_MAIN_APP_CLIENT_ID"
[ -n "$APP_PRIVATE_KEY" ] || missing="$missing secrets.PUSH_TO_MAIN_APP_PRIVATE_KEY"
if [ -n "$missing" ]; then
echo "Missing:$missing" >&2
echo "An organisation-level variable or secret also has to list this repository." >&2
exit 1
fi
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.PUSH_TO_MAIN_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_TO_MAIN_APP_PRIVATE_KEY }}
owner: Zipstack
repositories: |
unstract-cli
- uses: actions/checkout@v4
with:
token: ${{ steps.generate-token.outputs.token }}
fetch-depth: 0
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v6
with:
version: "0.6.14"
enable-cache: true
# The same install as ci.yml, so what the release run lints and tests is
# what the PR gate lints and tests.
- run: uv sync --extra dev --python 3.12
# Staged locally only: nothing is committed, tagged or released until the
# checks and the build have passed, so a failure leaves main untouched.
- name: Compute new version
id: version
run: |
VERSION_FILE=src/unstract_cli/__init__.py
CURRENT_VERSION=$(sed -nE 's/^__version__ = "(.*)"/\1/p' "$VERSION_FILE")
echo "Current version: $CURRENT_VERSION"
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
case "${{ github.event.inputs.version_bump }}" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
NEXT_VERSION="$MAJOR.$MINOR.$PATCH"
# A pre-release is a candidate for NEXT_VERSION, not a version of its
# own, so it never moves the committed one: the file keeps naming the
# last stable release, and repeat dispatches count up from the rc tags
# already published for that target.
if [ "${{ github.event.inputs.pre_release }}" = "true" ]; then
HIGHEST_RC=$(git tag -l "v${NEXT_VERSION}rc*" \
| sed -nE "s/^v${NEXT_VERSION}rc([0-9]+)$/\1/p" | sort -n | tail -1)
NEW_VERSION="${NEXT_VERSION}rc$(( ${HIGHEST_RC:-0} + 1 ))"
else
NEW_VERSION="$NEXT_VERSION"
fi
echo "New version: $NEW_VERSION"
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
sed -i "s/^__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" "$VERSION_FILE"
if git rev-parse -q --verify "refs/tags/v$NEW_VERSION" >/dev/null; then
echo "Tag v$NEW_VERSION already exists. Exiting..."
exit 1
fi
- name: Verify version update
run: |
BUILT_VERSION=$(uv run python -c "import unstract_cli; print(unstract_cli.__version__)")
echo "Package version: $BUILT_VERSION"
echo "Target version: ${{ steps.version.outputs.version }}"
if [ "$BUILT_VERSION" != "${{ steps.version.outputs.version }}" ]; then
echo "Version mismatch! Exiting..."
exit 1
fi
- name: Run linting
run: |
uv run ruff check .
uv run ruff format --check .
- name: Run tests
run: uv run pytest -q
- name: Build package
run: uv build
# Publishing is the one step that cannot be undone, so it runs last, and
# the release is created as a draft so a publish that fails leaves
# nothing public. Until then the bump commit exists only in the tag, so
# a failed publish leaves main untouched.
- name: Commit version bump and create draft release
env:
RELEASE_NOTES: ${{ github.event.inputs.release_notes }}
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
NEW_VERSION="${{ steps.version.outputs.version }}"
# A pre-release leaves the committed version alone.
if [ "${{ github.event.inputs.pre_release }}" = "true" ]; then
git checkout -- src/unstract_cli/__init__.py
elif ! git diff --quiet; then
git add src/unstract_cli/__init__.py
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
fi
git tag "v$NEW_VERSION"
git push origin "v$NEW_VERSION"
if [ -z "$RELEASE_NOTES" ]; then
gh release create "v$NEW_VERSION" \
--title "Release v$NEW_VERSION" \
--generate-notes \
--draft \
${{ github.event.inputs.pre_release == 'true' && '--prerelease' || '' }}
else
gh release create "v$NEW_VERSION" \
--title "Release v$NEW_VERSION" \
--notes "$RELEASE_NOTES" \
--generate-notes \
--draft \
${{ github.event.inputs.pre_release == 'true' && '--prerelease' || '' }}
fi
echo "Created draft release v$NEW_VERSION"
- name: Publish to PyPI
run: uv publish
# Everything the run created before the publish, so a rerun after the
# cause is fixed starts clean rather than on the tag guard. Each removal
# tolerates its target being absent: the failure may have come partway
# through creating them.
- name: Remove the tag and draft release a failed publish leaves behind
if: failure() && steps.version.outcome == 'success'
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
NEW_VERSION="${{ steps.version.outputs.version }}"
# The wheel and the sdist upload separately, so a failed publish can
# still have put the version on PyPI; then the tag has to stay.
if curl -fsS -o /dev/null "https://pypi.org/pypi/unstract-cli/$NEW_VERSION/json"; then
echo "PyPI serves $NEW_VERSION; keeping the tag and draft release." >&2
exit 0
fi
gh release delete "v$NEW_VERSION" --yes || true
git push origin ":refs/tags/v$NEW_VERSION" || true
# If this fails the published version is on PyPI and in the tag but not
# on main; `git push origin v<version>:main` finishes the job by hand.
- name: Push version bump
if: github.event.inputs.pre_release != 'true'
run: git push origin main
- name: Publish release
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
run: gh release edit "v${{ steps.version.outputs.version }}" --draft=false
- name: Success message
run: |
echo "Published ${{ steps.version.outputs.version }} to PyPI with uv publish using Trusted Publishers"
echo "Release: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.version.outputs.version }}"
echo "PyPI: https://pypi.org/project/unstract-cli/${{ steps.version.outputs.version }}/"