Skip to content

Commit 7da03ee

Browse files
author
Lukas Pühringer
authored
Merge pull request #1913 from jku/verify-release
build: Add verify-release script
2 parents b7b035a + 6819d41 commit 7da03ee

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

docs/RELEASE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,7 @@
3333
* Upload to PyPI `twine upload dist/*`
3434
* Verify the package at https://pypi.org/project/tuf/ and by installing with pip
3535
* Attach both signed dists and their detached signatures to the release on GitHub
36+
* `verify_release` should be used to make sure the release artifacts match the
37+
git sources, preferably by another developer on a different machine.
3638
* Announce the release on [#tuf on CNCF Slack](https://cloud-native.slack.com/archives/C8NMD3QJ3)
3739
* Ensure [POUF 1](https://github.com/theupdateframework/taps/blob/master/POUFs/reference-POUF/pouf1.md), for the reference implementation, is up-to-date

verify_release

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2022, TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""verify_release - verify that published release matches a locally built one
7+
8+
Builds a release from current commit and verifies that the release artifacts
9+
on GitHub and PyPI match the built release artifacts.
10+
"""
11+
12+
import json
13+
import os
14+
import subprocess
15+
import sys
16+
from filecmp import dircmp
17+
from tempfile import TemporaryDirectory
18+
19+
import requests
20+
21+
# Project variables
22+
# Note that only these project artifacts are supported:
23+
# [f"{PYPI_PROJECT}-{VER}-none-any.whl", f"{PYPI_PROJECT}-{VER}.tar.gz"]
24+
GITHUB_ORG = "theupdateframework"
25+
GITHUB_PROJECT = "python-tuf"
26+
PYPI_PROJECT = "tuf"
27+
28+
29+
def build(build_dir: str) -> str:
30+
"""Build release locally. Return version as string"""
31+
cmd = ["python3", "-m", "build", "--outdir", build_dir]
32+
subprocess.run(cmd, stdout=subprocess.DEVNULL, check=True)
33+
build_version = None
34+
for filename in os.listdir(build_dir):
35+
prefix, postfix = f"{PYPI_PROJECT}-", ".tar.gz"
36+
if filename.startswith(prefix) and filename.endswith(postfix):
37+
build_version = filename[len(prefix) : -len(postfix)]
38+
assert build_version
39+
return build_version
40+
41+
42+
def get_git_version() -> str:
43+
"""Return version string from git describe"""
44+
cmd = ["git", "describe"]
45+
process = subprocess.run(cmd, text=True, capture_output=True, check=True)
46+
assert process.stdout.startswith("v") and process.stdout.endswith("\n")
47+
return process.stdout[1:-1]
48+
49+
50+
def get_github_version() -> str:
51+
"""Return version string of latest GitHub release"""
52+
release_json = f"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_PROJECT}/releases/latest"
53+
releases = json.loads(requests.get(release_json).content)
54+
return releases["tag_name"][1:]
55+
56+
57+
def get_pypi_pip_version() -> str:
58+
"""Return latest version string available on PyPI according to pip"""
59+
# pip can't tell us what the newest available version is... So we download
60+
# newest tarball and figure out the version from the filename
61+
with TemporaryDirectory() as pypi_dir:
62+
cmd = ["pip", "download", "--no-deps", "--dest", pypi_dir]
63+
source_download = cmd + ["--no-binary", ":all:", PYPI_PROJECT]
64+
subprocess.run(source_download, stdout=subprocess.DEVNULL, check=True)
65+
for filename in os.listdir(pypi_dir):
66+
prefix, postfix = f"{PYPI_PROJECT}-", ".tar.gz"
67+
if filename.startswith(prefix) and filename.endswith(postfix):
68+
return filename[len(prefix) : -len(postfix)]
69+
assert False
70+
71+
72+
def verify_github_release(version: str, compare_dir: str) -> bool:
73+
"""Verify that given GitHub version artifacts match expected artifacts"""
74+
base_url = (
75+
f"https://github.com/{GITHUB_ORG}/{GITHUB_PROJECT}/releases/download"
76+
)
77+
tar = f"{PYPI_PROJECT}-{version}.tar.gz"
78+
wheel = f"{PYPI_PROJECT}-{version}-py3-none-any.whl"
79+
with TemporaryDirectory() as github_dir:
80+
for filename in [tar, wheel]:
81+
url = f"{base_url}/v{version}/{filename}"
82+
response = requests.get(url, stream=True)
83+
with open(os.path.join(github_dir, filename), "wb") as f:
84+
for data in response.iter_content():
85+
f.write(data)
86+
87+
same = dircmp(github_dir, compare_dir).same_files
88+
return sorted(same) == [wheel, tar]
89+
90+
91+
def verify_pypi_release(version: str, compare_dir: str) -> bool:
92+
"""Verify that given PyPI version artifacts match expected artifacts"""
93+
tar = f"{PYPI_PROJECT}-{version}.tar.gz"
94+
wheel = f"{PYPI_PROJECT}-{version}-py3-none-any.whl"
95+
96+
with TemporaryDirectory() as pypi_dir:
97+
cmd = ["pip", "download", "--no-deps", "--dest", pypi_dir]
98+
target = f"{PYPI_PROJECT}=={version}"
99+
binary_download = cmd + [target]
100+
source_download = cmd + ["--no-binary", ":all:", target]
101+
102+
subprocess.run(binary_download, stdout=subprocess.DEVNULL, check=True)
103+
subprocess.run(source_download, stdout=subprocess.DEVNULL, check=True)
104+
105+
same = dircmp(pypi_dir, compare_dir).same_files
106+
return sorted(same) == [wheel, tar]
107+
108+
109+
def finished(s: str) -> None:
110+
# clear line
111+
sys.stdout.write("\033[K")
112+
print(f"* {s}")
113+
114+
115+
def progress(s: str) -> None:
116+
# clear line
117+
sys.stdout.write("\033[K")
118+
# carriage return but no newline: next print will overwrite this one
119+
print(f" {s}...", end="\r", flush=True)
120+
121+
122+
def main() -> int:
123+
success = True
124+
with TemporaryDirectory() as build_dir:
125+
126+
progress("Building release")
127+
build_version = build(build_dir)
128+
finished(f"Built release {build_version}")
129+
130+
git_version = get_git_version()
131+
assert git_version.startswith(build_version)
132+
if git_version != build_version:
133+
finished(f"WARNING: Git describes version as {git_version}")
134+
135+
progress("Checking GitHub latest version")
136+
github_version = get_github_version()
137+
if github_version != build_version:
138+
finished(f"WARNING: GitHub latest version is {github_version}")
139+
140+
progress("Checking PyPI latest version")
141+
pypi_version = get_pypi_pip_version()
142+
if pypi_version != build_version:
143+
finished(f"WARNING: PyPI latest version is {pypi_version}")
144+
145+
progress("Downloading release from PyPI")
146+
if not verify_pypi_release(build_version, build_dir):
147+
# This is expected while build is not reproducible
148+
finished("ERROR: PyPI artifacts do not match built release")
149+
success = False
150+
151+
progress("Downloading release from GitHub")
152+
if not verify_github_release(build_version, build_dir):
153+
# This is expected while build is not reproducible
154+
finished("ERROR: GitHub artifacts do not match built release")
155+
success = False
156+
157+
if success:
158+
finished("Github and PyPI artifacts match the built release")
159+
160+
return 0 if success else 1
161+
162+
163+
if __name__ == "__main__":
164+
sys.exit(main())

0 commit comments

Comments
 (0)