-
Notifications
You must be signed in to change notification settings - Fork 0
214 lines (197 loc) · 8.94 KB
/
Copy pathrelease.yml
File metadata and controls
214 lines (197 loc) · 8.94 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
# Build and publish the installable lshell release artifacts (sdist + wheel).
#
# The wheel is what QuickBox servers install: the release proxy serves it and
# the updater pip-installs it directly, so no build toolchain (flit) is needed
# on a user box. Every release is gated on the unit-test suite passing first.
#
# Triggers (workflow_dispatch ONLY — deliberately no `push: tags` trigger, so
# the tag this workflow pushes for a stable release can never retrigger it):
# - workflow_dispatch stable auto-selects the next free patch version, bumps
# lshell/__init__.py, commits + tags it, and cuts a stable release. No
# manual __version__ edit is ever required.
# - workflow_dispatch dev pre-release from current HEAD (kept to last 5)
#
# Runs on GitHub-hosted ubuntu-latest (like the test workflow): the build is a
# pure flit/pip step with no self-hosted runner state, so it stays portable and
# free of infrastructure coupling.
name: release
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release type'
required: true
default: 'stable'
type: choice
options:
- dev
- stable
permissions:
contents: write
# Never cancel a release mid-publish: a run tags the commit and uploads the
# artifacts. A superseding push queues behind it.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build and test dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flit pexpect
python -m pip install .
# Test gate: the unit suite must pass before anything is published.
- name: Unit tests
run: python -m unittest discover -s test -p 'test_unit.py' -v
# Functional suite spawns bin/lshell via pexpect. Non-blocking for the
# same reason as the test workflow (a path-portability fixture on
# usr-merged runners), so it does not gate the release.
- name: Functional tests (non-blocking)
continue-on-error: true
run: python -m unittest discover -s test -p 'test_functional.py' -v
# Auto-versioning: a stable dispatch never needs a manual __version__ edit.
# The next patch version is selected automatically from __version__, walking
# past any already-released tag until a free one is found. dev keeps its
# SHA-embedded (inherently unique) tag and never bumps.
- name: Determine version and tag
id: version
run: |
set -euo pipefail
MODULE_VERSION="$(python -c 'import lshell; print(lshell.__version__)')"
if [[ "${{ inputs.release_type }}" == "dev" ]]; then
SHORT_SHA="$(git rev-parse --short HEAD)"
VERSION="${MODULE_VERSION}-dev.${SHORT_SHA}"
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
echo "tag=v${VERSION}" >> "${GITHUB_OUTPUT}"
echo "release_type=dev" >> "${GITHUB_OUTPUT}"
echo "bumped=false" >> "${GITHUB_OUTPUT}"
echo "Dev pre-release ${VERSION}"
exit 0
fi
if [[ "${{ inputs.release_type }}" != "stable" ]]; then
echo "ERROR: unexpected release_type '${{ inputs.release_type }}' (expected stable or dev)" >&2
exit 1
fi
if [[ ! "${MODULE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "ERROR: __version__ '${MODULE_VERSION}' is not MAJOR.MINOR.PATCH" >&2
exit 1
fi
# Start at __version__ and increment the patch until the tag is free.
# A stale __version__ simply advances past the released tags; the value
# committed back becomes the next cut's starting point.
IFS=. read -r MA MI PA <<<"${MODULE_VERSION}"
VERSION="${MODULE_VERSION}"
tries=0
while git ls-remote --tags origin "refs/tags/v${VERSION}" | grep -q .; do
PA=$((PA + 1))
VERSION="${MA}.${MI}.${PA}"
tries=$((tries + 1))
if [[ ${tries} -gt 100 ]]; then
echo "ERROR: no free v${MA}.${MI}.x tag within 100 increments of ${MODULE_VERSION}" >&2
exit 1
fi
done
BUMPED=false
[[ "${VERSION}" != "${MODULE_VERSION}" ]] && BUMPED=true
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
echo "tag=v${VERSION}" >> "${GITHUB_OUTPUT}"
echo "release_type=stable" >> "${GITHUB_OUTPUT}"
echo "bumped=${BUMPED}" >> "${GITHUB_OUTPUT}"
echo "Stable release ${VERSION} (bumped from ${MODULE_VERSION}: ${BUMPED})"
# Write the resolved version into the source BEFORE build so the artifacts
# (lshell-<version>-*) and the module's own __version__ agree with the tag.
# Two literals carry the version: lshell/__init__.py (authoritative — flit's
# dynamic version, and variables.py imports it for `lshell --version`) and
# the man page .TH header. Stamp both so neither skews from the release.
- name: Apply resolved version to source
if: steps.version.outputs.release_type == 'stable'
run: |
set -euo pipefail
sed -i "s/^__version__ = .*/__version__ = \"${{ steps.version.outputs.version }}\"/" lshell/__init__.py
grep -qx "__version__ = \"${{ steps.version.outputs.version }}\"" lshell/__init__.py
sed -i "s/^\(\.TH lshell 1 \"[^\"]*\" \)\"v[0-9][0-9.]*\"/\1\"v${{ steps.version.outputs.version }}\"/" man/lshell.1
grep -q "\"v${{ steps.version.outputs.version }}\"" man/lshell.1
echo "stamped -> $(grep '^__version__' lshell/__init__.py) | $(grep '^\.TH lshell' man/lshell.1)"
# flit build reads the version from lshell.__version__ and produces both
# the wheel (lshell-<version>-py3-none-any.whl) and sdist
# (lshell-<version>.tar.gz) in dist/.
- name: Build sdist and wheel
run: |
set -euo pipefail
rm -rf dist
flit build
ls -lh dist/
# SHA256SUMS gives an independent integrity reference alongside the
# server-computed hash the proxy returns to the updater.
- name: Generate SHA256SUMS
run: |
set -euo pipefail
cd dist
sha256sum ./* > SHA256SUMS
cat SHA256SUMS
# Persist the released state to the repo: commit the auto-bump (when the
# version advanced) and push the tag at that commit. Pushed with the
# checkout-persisted GITHUB_TOKEN — GitHub does not retrigger a workflow on
# a ref pushed by GITHUB_TOKEN, and this workflow has no push trigger
# anyway, so there is no release loop. Pushing an existing tag fails
# (no --force), which is the final backstop against overwriting a release.
- name: Commit version bump and push tag
if: steps.version.outputs.release_type == 'stable'
run: |
set -euo pipefail
git config user.name "QBXWatcher"
git config user.email "[email protected]"
if [[ "${{ steps.version.outputs.bumped }}" == "true" ]]; then
git add lshell/__init__.py man/lshell.1
git commit -m "chore(release): bump version to ${{ steps.version.outputs.version }}"
git push origin "HEAD:${GITHUB_REF_NAME}"
fi
git tag "${{ steps.version.outputs.tag }}"
git push origin "${{ steps.version.outputs.tag }}"
- name: Create stable release
if: steps.version.outputs.release_type == 'stable'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.version }}
prerelease: false
make_latest: true
files: |
dist/*.whl
dist/*.tar.gz
dist/SHA256SUMS
- name: Create dev pre-release
if: steps.version.outputs.release_type == 'dev'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
name: ${{ steps.version.outputs.tag }}
prerelease: true
files: |
dist/*.whl
dist/*.tar.gz
dist/SHA256SUMS
- name: Cleanup old dev pre-releases
if: steps.version.outputs.release_type == 'dev'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release list --json tagName,isPrerelease --limit 100 \
| jq -r '.[] | select(.isPrerelease) | .tagName' \
| tail -n +6 \
| while read -r tag; do
echo "Deleting old pre-release: ${tag}"
gh release delete "${tag}" --yes --cleanup-tag 2>/dev/null || true
done