Repository navigation
185 lines (171 loc) · 7.12 KB
/
Copy pathrelease.yml
File metadata and controls
185 lines (171 loc) · 7.12 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
# Publishes a release to PyPI and creates the GitHub release. Triggered by pushing a tag
# "v<version>" whose version matches the version of every member of the workspace, see "Releasing"
# in AGENTS.md.
#
# The distributions are the four libraries easyssf-core, easyssf-receiver, easyssf-test and
# easyssf-receiver-fastapi (the conformance harness and the examples carry the classifier
# "Private :: Do Not Upload" and are not built here), built once, tested, checked, and uploaded
# with trusted publishing: no stored API token, each publish job's GitHub OIDC token is exchanged
# for a PyPI token that lives a quarter of an hour and reaches its one project. The trusted
# publisher of each of the four projects on PyPI names this repository, this workflow file and the
# environment "pypi-<distribution>". Only the publish jobs may request the OIDC token, as PyPI
# recommends; they run no code of the repository.
name: Release
# a tag push carries no commit for GitHub to title the run with, so name it by the tag
run-name: Release ${{ github.ref_name }}
on:
push:
tags: ['v*']
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
env:
DISTRIBUTIONS: easyssf-core easyssf-receiver easyssf-test easyssf-receiver-fastapi
jobs:
build:
name: Build, test and check
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v10.3.2
- name: Check that the tag matches the version of every member
# the members pin each other at the minor version, so all of them move together
run: |
version=${GITHUB_REF_NAME#v}
echo "tag: $GITHUB_REF_NAME"
status=0
for pyproject in pyproject.toml */pyproject.toml examples/*/*/pyproject.toml; do
member=$(sed -n 's/^version = "\(.*\)"$/\1/p' "$pyproject")
echo "$pyproject: $member"
if [ "$member" != "$version" ]; then
echo "::error file=$pyproject::version $member does not match tag $GITHUB_REF_NAME"
status=1
fi
done
exit $status
- name: Take the release notes from the changelog
# the section of this version, without its heading, up to the next version heading; the
# workflow fails without it so that no release goes out undocumented
run: |
version=${GITHUB_REF_NAME#v}
awk -v v="$version" '
/^## \[/ { in_section = (index($0, "## [" v "]") == 1) ; next }
in_section && !/^\[/ { print }
' CHANGELOG.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > notes.md
if [ ! -s notes.md ]; then
echo "::error::CHANGELOG.md has no section for $version"
exit 1
fi
cat notes.md
- run: uv sync --all-packages --locked
- run: uv run pytest -q
- run: uv run ruff check .
- run: uv run ruff format --check .
- name: Build
run: |
for distribution in $DISTRIBUTIONS; do
uv build --package "$distribution" --out-dir dist
done
- name: Check the distributions
# a wheel and an sdist of each of the four, at the version of the tag, with metadata PyPI
# renders
run: |
version=${GITHUB_REF_NAME#v}
ls -l dist
for distribution in $DISTRIBUTIONS; do
name=${distribution//-/_}
for file in "dist/$name-$version-py3-none-any.whl" "dist/$name-$version.tar.gz"; do
if [ ! -f "$file" ]; then
echo "::error::$file is missing"
exit 1
fi
done
done
count=$(find dist -type f ! -name '.*' | wc -l)
if [ "$count" -ne 8 ]; then
echo "::error::expected 8 files, found $count"
exit 1
fi
uvx twine check --strict dist/*
- name: Install the wheels in a clean environment
# the published distributions have to resolve each other and their dependencies from the
# index and the wheels alone
run: |
version=${GITHUB_REF_NAME#v}
uv venv --python 3.11 "$RUNNER_TEMP/clean"
uv pip install --python "$RUNNER_TEMP/clean" --find-links dist --no-build \
"easyssf-receiver-fastapi==$version" "easyssf-test==$version"
"$RUNNER_TEMP/clean/bin/python" -c "import easyssf.core, easyssf.receiver, easyssf.testing, easyssf.fastapi"
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
- uses: actions/upload-artifact@v7
with:
name: notes
path: notes.md
publish:
name: Publish ${{ matrix.distribution }} to PyPI
needs: build
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
# one upload that fails (a project without its trusted publisher yet) leaves the others be;
# "Re-run failed jobs" retries it and then creates the GitHub release
fail-fast: false
matrix:
# the DISTRIBUTIONS of the build job; the env context is not available to a matrix
distribution: [easyssf-core, easyssf-receiver, easyssf-test, easyssf-receiver-fastapi]
# one GitHub environment per distribution, which its trusted publisher names: PyPI accepts a
# pending publisher only once per owner, repository, workflow and environment
environment:
name: pypi-${{ matrix.distribution }}
url: https://pypi.org/project/${{ matrix.distribution }}/
permissions:
# the OIDC token that trusted publishing exchanges for the PyPI token
id-token: write
steps:
- uses: actions/download-artifact@v7
with:
name: dist
path: dist
- name: Select the files of the distribution
# the version in the pattern keeps easyssf_receiver- from matching easyssf_receiver_fastapi-
run: |
version=${GITHUB_REF_NAME#v}
name=${DISTRIBUTION//-/_}
mkdir upload
mv "dist/$name-$version-py3-none-any.whl" "dist/$name-$version.tar.gz" upload/
ls -l upload
env:
DISTRIBUTION: ${{ matrix.distribution }}
- name: Upload to PyPI
# with digital attestations (PEP 740); skip-existing makes a rerun after a partial upload
# idempotent, PyPI refuses to replace a file it has
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: upload/
skip-existing: true
github-release:
name: Create the GitHub release
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/download-artifact@v7
with:
path: .
- name: Create the GitHub release
# a version with a prerelease label (0.8.0rc1, 0.8.0a1) is marked as a prerelease
run: |
version=${GITHUB_REF_NAME#v}
prerelease=
case "$version" in *a*|*b*|*rc*|*dev*) prerelease=--prerelease ;; esac
gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --verify-tag $prerelease \
--notes-file notes/notes.md --title "$version" dist/*
env:
GH_TOKEN: ${{ github.token }}