-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpython-package-release-on-pypi-and-github.yml
More file actions
262 lines (262 loc) Β· 9.88 KB
/
python-package-release-on-pypi-and-github.yml
File metadata and controls
262 lines (262 loc) Β· 9.88 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
---
name: Python package release on PyPI and GitHub
on:
workflow_call:
inputs:
package-path:
required: false
type: string
description: Path to a Python package or project
default: .
create-github-release:
required: false
type: boolean
description: Create a GitHub release
default: true
publish-to-pypi:
required: false
type: boolean
description: Publish the distribution to PyPI
default: true
publish-to-testpypi-before-pypi:
required: false
type: boolean
description: Publish to TestPyPI before PyPI when PyPI publishing is enabled
default: false
python-version:
required: false
type: string
description: Python version to use (not applicable if uv.lock is present)
default: 3.x
uv-version:
required: false
type: string
description: Version of uv to use (applicable only if uv.lock is present)
default: latest
requirements-txt:
required: false
type: string
description: Path to the requirements.txt file (not applicable if uv.lock is present)
default: null
distribution-artifact-name:
required: false
type: string
description: Name of the artifact for the distribution packages
default: python-package-distributions
artifact-retention-days:
required: false
type: number
description: Number of days to retain artifacts
default: 1
artifact-overwrite:
required: false
type: boolean
description: Overwrite existing artifacts
default: true
runs-on:
required: false
type: string
description: GitHub Actions runner to use
default: ubuntu-latest
outputs:
project-name:
description: Project name of the distribution
value: ${{ jobs.build.outputs.project-name }}
distribution-artifact-name:
description: Name of the uploaded artifact
value: ${{ inputs.distribution-artifact-name }}
secrets:
PYPI_API_TOKEN:
required: false
description: PyPI API token
GH_TOKEN:
required: false
description: GitHub token
permissions:
contents: read
defaults:
run:
shell: bash -euo pipefail {0}
working-directory: .
jobs:
build:
name: Build the Python π distribution π¦
runs-on: ${{ inputs.runs-on }}
outputs:
project-name: ${{ steps.read-project-name.outputs.project_name }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
persist-credentials: false
- name: Detect uv.lock or poetry.lock
working-directory: ${{ inputs.package-path }}
run: |
if [[ -f uv.lock ]]; then
echo "LOCK_FILE=uv.lock" >> "${GITHUB_ENV}"
elif [[ -f poetry.lock ]]; then
echo "LOCK_FILE=poetry.lock" >> "${GITHUB_ENV}"
else
echo "LOCK_FILE=" >> "${GITHUB_ENV}"
fi
- name: Set up uv
if: env.LOCK_FILE == 'uv.lock'
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ inputs.uv-version }}
- name: Set up Python
if: env.LOCK_FILE != 'uv.lock'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ inputs.python-version }}
- name: Read the project name
id: read-project-name
working-directory: ${{ inputs.package-path }}
shell: python {0}
run: |
import os
import tomllib
with open("pyproject.toml", "rb") as f:
d = tomllib.load(f)
if "project" in d and "name" in d["project"]:
name = d["project"]["name"]
elif "tool" in d and "poetry" in d["tool"] and "name" in d["tool"]["poetry"]:
name = d["tool"]["poetry"]["name"]
else:
raise ValueError(f"project name not found in pyproject.toml: {d}")
print(f"project_name={name}")
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
print(f"project_name={name}", file=f)
- name: Build a binary wheel and a source tarball
env:
REQUIREMENTS_TXT_PATH: ${{ inputs.requirements-txt }}
STEPS_READ_PROJECT_NAME_OUTPUTS_PROJECT_NAME: ${{ steps.read-project-name.outputs.project_name }}
working-directory: ${{ inputs.package-path }}
run: |
if [[ "${LOCK_FILE}" == "uv.lock" ]]; then
uv build
else
pip install -U --no-cache-dir pip
if [[ -n "${REQUIREMENTS_TXT_PATH}" ]]; then
pip install -U --no-cache-dir -r "${REQUIREMENTS_TXT_PATH}"
fi
if [[ "${LOCK_FILE}" == "poetry.lock" ]]; then
pip install --no-cache-dir poetry
poetry build --format=wheel --no-interaction
else
pip install --no-cache-dir build
python -m build
fi
fi
mv dist "/tmp/${STEPS_READ_PROJECT_NAME_OUTPUTS_PROJECT_NAME}_dist"
- name: Store the distribution packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.distribution-artifact-name }}
path: /tmp/${{ steps.read-project-name.outputs.project_name }}_dist/
retention-days: ${{ inputs.artifact-retention-days }}
overwrite: ${{ inputs.artifact-overwrite }}
publish-to-testpypi:
name: Publish the Python π distribution π¦ to TestPyPI
if: >
startsWith(github.ref, 'refs/tags/')
&& inputs.publish-to-pypi
&& inputs.publish-to-testpypi-before-pypi
needs:
- build
runs-on: ${{ inputs.runs-on }}
environment:
name: testpypi
url: https://test.pypi.org/p/${{ needs.build.outputs.project-name }}
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.distribution-artifact-name }}
path: dist/
- name: Publish distribution π¦ to TestPyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
repository-url: https://test.pypi.org/legacy/
github-release:
name: Sign the Python π distribution π¦ with Sigstore and upload them to GitHub Release
if: >
startsWith(github.ref, 'refs/tags/')
&& inputs.create-github-release
&& (! (failure() || cancelled()))
needs:
- build
runs-on: ${{ inputs.runs-on }}
permissions:
contents: write # IMPORTANT: mandatory for making GitHub Releases
id-token: write # IMPORTANT: mandatory for sigstore
steps:
- name: Download all the dists
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.distribution-artifact-name }}
path: dist/
- name: Validate the version-tag consistency
env:
TAG_NAME: ${{ github.ref_name }}
NEEDS_BUILD_OUTPUTS_PROJECT_NAME: ${{ needs.build.outputs.project-name }}
run: |
v="$( \
find dist -type f -name "${NEEDS_BUILD_OUTPUTS_PROJECT_NAME}-*" -exec basename {} \; \
| head -n 1 \
| cut -d '-' -f 2 \
)"
v="${v%.tar.gz}"
if [[ "${TAG_NAME}" != "${v}" ]] && [[ "${TAG_NAME}" != "v${v}" ]]; then
echo "The tag (${TAG_NAME}) is inconsistent with the version (${v})." && exit 1
fi
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0
with:
inputs: >-
./dist/*.whl
- name: Create GitHub Release
env:
REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} # zizmor: ignore[secrets-outside-env] caller-provided secret
run: |
gh release create "${GITHUB_REF_NAME}" --repo "${REPOSITORY}" --generate-notes --verify-tag
- name: Upload artifact signatures to GitHub Release
env:
REPOSITORY: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GH_TOKEN || secrets.GITHUB_TOKEN }} # zizmor: ignore[secrets-outside-env] caller-provided secret
# Upload to GitHub Release using the `gh` CLI.
# `dist/` contains the built packages, and the
# sigstore-produced signatures and certificates.
run: |
gh release upload "${GITHUB_REF_NAME}" dist/** --repo "${REPOSITORY}"
publish-to-pypi:
name: Publish the Python π distribution π¦ to PyPI
if: >
startsWith(github.ref, 'refs/tags/')
&& inputs.publish-to-pypi
&& (! (failure() || cancelled()))
needs:
- build
- publish-to-testpypi
- github-release
runs-on: ${{ inputs.runs-on }}
environment:
name: pypi
url: https://pypi.org/p/${{ needs.build.outputs.project-name }}
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.distribution-artifact-name }}
path: dist/
- name: Publish distribution π¦ to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
password: ${{ secrets.PYPI_API_TOKEN }} # zizmor: ignore[use-trusted-publishing] Reusable workflow supports only API-token-based PyPI publishing.
verbose: true