Skip to content

Commit 884baed

Browse files
authored
Import improvements from cryptography wheel building and release (#840)
Upload to PyPI from GHA
1 parent dd72470 commit 884baed

File tree

3 files changed

+94
-107
lines changed

3 files changed

+94
-107
lines changed

.github/workflows/pypi-publish.yml

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
run_id:
7+
description: The run of wheel-builder to use for finding artifacts.
8+
required: true
9+
environment:
10+
description: Which PyPI environment to upload to
11+
required: true
12+
type: choice
13+
options: ["testpypi", "pypi"]
14+
workflow_run:
15+
workflows: ["Wheel Builder"]
16+
types: [completed]
17+
18+
permissions:
19+
contents: read
20+
21+
jobs:
22+
publish:
23+
runs-on: ubuntu-latest
24+
# We're not actually verifying that the triggering push event was for a
25+
# tag, because github doesn't expose enough information to do so.
26+
# wheel-builder.yml currently only has push events for tags.
27+
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.conclusion == 'success')
28+
permissions:
29+
id-token: "write"
30+
attestations: "write"
31+
steps:
32+
- run: echo "$EVENT_CONTEXT"
33+
env:
34+
EVENT_CONTEXT: ${{ toJson(github.event) }}
35+
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
36+
with:
37+
python-version: "3.11"
38+
- uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6
39+
with:
40+
path: dist/
41+
run_id: ${{ github.event.inputs.run_id || github.event.workflow_run.id }}
42+
- run: pip install twine requests
43+
44+
- run: |
45+
echo "OIDC_AUDIENCE=pypi" >> $GITHUB_ENV
46+
echo "PYPI_DOMAIN=pypi.org" >> $GITHUB_ENV
47+
echo "TWINE_REPOSITORY=pypi" >> $GITHUB_ENV
48+
echo "TWINE_USERNAME=__token__" >> $GITHUB_ENV
49+
if: github.event_name == 'workflow_run' || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'pypi')
50+
- run: |
51+
echo "OIDC_AUDIENCE=testpypi" >> $GITHUB_ENV
52+
echo "PYPI_DOMAIN=test.pypi.org" >> $GITHUB_ENV
53+
echo "TWINE_REPOSITORY=testpypi" >> $GITHUB_ENV
54+
echo "TWINE_USERNAME=__token__" >> $GITHUB_ENV
55+
if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'testpypi'
56+
57+
- run: |
58+
import os
59+
60+
import requests
61+
62+
response = requests.get(
63+
os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"],
64+
params={"audience": os.environ["OIDC_AUDIENCE"]},
65+
headers={"Authorization": f"bearer {os.environ['ACTIONS_ID_TOKEN_REQUEST_TOKEN']}"}
66+
)
67+
response.raise_for_status()
68+
token = response.json()["value"]
69+
70+
response = requests.post(f"https://{os.environ['PYPI_DOMAIN']}/_/oidc/mint-token", json={"token": token})
71+
response.raise_for_status()
72+
pypi_token = response.json()["token"]
73+
74+
with open(os.environ["GITHUB_ENV"], "a") as f:
75+
print(f"::add-mask::{pypi_token}")
76+
f.write(f"TWINE_PASSWORD={pypi_token}\n")
77+
shell: python
78+
79+
- run: twine upload --skip-existing $(find dist/ -type f -name 'bcrypt*')
80+
81+
# Do not perform attestation for things for TestPyPI. This is because
82+
# there's nothing that would prevent a malicious PyPI from serving a
83+
# signed TestPyPI asset in place of a release intended for PyPI.
84+
- uses: actions/attest-build-provenance@5e9cb68e95676991667494a6a4e59b8a2f13e1d0 # v1.3.3
85+
with:
86+
subject-path: 'dist/**/bcrypt*'
87+
if: env.TWINE_REPOSITORY == 'pypi'

.github/workflows/wheel-builder.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ on:
77
version:
88
description: The version to build
99
required: false
10+
# Do not add any non-tag push events without updating pypi-publish.yml. If
11+
# you do, it'll upload wheels to PyPI.
12+
push:
13+
tags:
14+
- '*.*'
15+
- '*.*.*'
1016
pull_request:
1117
paths:
1218
- .github/workflows/wheel-builder.yml

release.py

Lines changed: 1 addition & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -10,130 +10,24 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13-
import getpass
14-
import io
15-
import json
16-
import os
1713
import subprocess
18-
import time
19-
import zipfile
2014

2115
import click
22-
import requests
2316

2417

2518
def run(*args, **kwargs):
2619
print(f"[running] {list(args)}")
2720
subprocess.check_call(list(args), **kwargs)
2821

2922

30-
def wait_for_build_complete_github_actions(session, token, run_url):
31-
while True:
32-
response = session.get(
33-
run_url,
34-
headers={
35-
"Content-Type": "application/json",
36-
"Authorization": f"token {token}",
37-
},
38-
)
39-
response.raise_for_status()
40-
if response.json()["conclusion"] is not None:
41-
break
42-
time.sleep(3)
43-
44-
45-
def download_artifacts_github_actions(session, token, run_url):
46-
response = session.get(
47-
run_url,
48-
headers={
49-
"Content-Type": "application/json",
50-
"Authorization": f"token {token}",
51-
},
52-
)
53-
response.raise_for_status()
54-
55-
response = session.get(
56-
response.json()["artifacts_url"],
57-
headers={
58-
"Content-Type": "application/json",
59-
"Authorization": f"token {token}",
60-
},
61-
)
62-
response.raise_for_status()
63-
paths = []
64-
for artifact in response.json()["artifacts"]:
65-
response = session.get(
66-
artifact["archive_download_url"],
67-
headers={
68-
"Content-Type": "application/json",
69-
"Authorization": f"token {token}",
70-
},
71-
)
72-
with zipfile.ZipFile(io.BytesIO(response.content)) as z:
73-
for name in z.namelist():
74-
if not name.endswith((".whl", ".tar.gz")):
75-
continue
76-
p = z.open(name)
77-
out_path = os.path.join(
78-
os.path.dirname(__file__),
79-
"dist",
80-
os.path.basename(name),
81-
)
82-
with open(out_path, "wb") as f:
83-
f.write(p.read())
84-
paths.append(out_path)
85-
return paths
86-
87-
88-
def build_github_actions_sdist_wheels(token, version):
89-
session = requests.Session()
90-
91-
response = session.post(
92-
"https://api.github.com/repos/pyca/bcrypt/actions/workflows/"
93-
"wheel-builder.yml/dispatches",
94-
headers={
95-
"Content-Type": "application/json",
96-
"Accept": "application/vnd.github.v3+json",
97-
"Authorization": f"token {token}",
98-
},
99-
data=json.dumps({"ref": "main", "inputs": {"version": version}}),
100-
)
101-
response.raise_for_status()
102-
103-
# Give it a few seconds for the run to kick off.
104-
time.sleep(5)
105-
response = session.get(
106-
(
107-
"https://api.github.com/repos/pyca/bcrypt/actions/workflows/"
108-
"wheel-builder.yml/runs?event=workflow_dispatch"
109-
),
110-
headers={
111-
"Content-Type": "application/json",
112-
"Authorization": f"token {token}",
113-
},
114-
)
115-
response.raise_for_status()
116-
run_url = response.json()["workflow_runs"][0]["url"]
117-
wait_for_build_complete_github_actions(session, token, run_url)
118-
return download_artifacts_github_actions(session, token, run_url)
119-
120-
12123
@click.command()
12224
@click.argument("version")
12325
def release(version):
12426
"""
12527
``version`` should be a string like '0.4' or '1.0'.
12628
"""
127-
github_token = getpass.getpass("Github person access token: ")
128-
12929
run("git", "tag", "-s", version, "-m", f"{version} release")
130-
run("git", "push", "--tags")
131-
132-
github_actions_paths = build_github_actions_sdist_wheels(
133-
github_token, version
134-
)
135-
136-
run("twine", "upload", *github_actions_paths)
30+
run("git", "push", "--tags", "git@github.com:pyca/bcrypt.git")
13731

13832

13933
if __name__ == "__main__":

0 commit comments

Comments
 (0)