mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
build: Add verify-release script
verify-release * Builds a release from current commit * Notifies if git describe does not match built version * Notifies if built version is not the latest GitHub or PyPI version * Asserts that the GitHub and PyPI release artifacts match the built release artifacts This should be useful after release as any developer (or a CI job) can easily verify that the release matches the sources in git. Note that the last checks currently fail as the 1.0 build was not reproducible. They should succeed after next release. Signed-off-by: Jussi Kukkonen <jkukkonen@vmware.com>
This commit is contained in:
parent
ff770eacd9
commit
53bacdf7e3
2 changed files with 161 additions and 0 deletions
|
|
@ -33,5 +33,7 @@
|
|||
* Upload to PyPI `twine upload dist/*`
|
||||
* Verify the package at https://pypi.org/project/tuf/ and by installing with pip
|
||||
* Attach both signed dists and their detached signatures to the release on GitHub
|
||||
* `verify_release` should be used to make sure the release artifacts match the
|
||||
git sources, preferably by another developer on a different machine.
|
||||
* Announce the release on [#tuf on CNCF Slack](https://cloud-native.slack.com/archives/C8NMD3QJ3)
|
||||
* Ensure [POUF 1](https://github.com/theupdateframework/taps/blob/master/POUFs/reference-POUF/pouf1.md), for the reference implementation, is up-to-date
|
||||
|
|
|
|||
159
verify_release
Executable file
159
verify_release
Executable file
|
|
@ -0,0 +1,159 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2022, TUF contributors
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
"""verify_release - verify that published release matches a locally built one
|
||||
|
||||
Builds a release from current commit and verifies that the release artifacts
|
||||
on GitHub and PyPI match the built release artifacts.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from filecmp import dircmp
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import requests
|
||||
|
||||
# Project variables
|
||||
# Note that only these project artifacts are supported:
|
||||
# [f"{PYPI_PROJECT}-{VER}-none-any.whl", f"{PYPI_PROJECT}-{VER}.tar.gz"]
|
||||
GITHUB_ORG = "theupdateframework"
|
||||
GITHUB_PROJECT = "python-tuf"
|
||||
PYPI_PROJECT = "tuf"
|
||||
|
||||
|
||||
def build(build_dir: str) -> str:
|
||||
"""Build release locally. Return version as string"""
|
||||
cmd = ["python3", "-m", "build", "--outdir", build_dir]
|
||||
subprocess.run(cmd, stdout=subprocess.DEVNULL, check=True)
|
||||
build_version = None
|
||||
for filename in os.listdir(build_dir):
|
||||
prefix, postfix = f"{PYPI_PROJECT}-", ".tar.gz"
|
||||
if filename.startswith(prefix) and filename.endswith(postfix):
|
||||
build_version = filename[len(prefix) : -len(postfix)]
|
||||
assert build_version
|
||||
return build_version
|
||||
|
||||
|
||||
def get_git_version() -> str:
|
||||
"""Return version string from git describe"""
|
||||
cmd = ["git", "describe"]
|
||||
process = subprocess.run(cmd, text=True, capture_output=True, check=True)
|
||||
assert process.stdout.startswith("v") and process.stdout.endswith("\n")
|
||||
return process.stdout[1:-1]
|
||||
|
||||
|
||||
def get_github_version() -> str:
|
||||
"""Return version string of latest GitHub release"""
|
||||
release_json = f"https://api.github.com/repos/{GITHUB_ORG}/{GITHUB_PROJECT}/releases/latest"
|
||||
releases = json.loads(requests.get(release_json).content)
|
||||
return releases["tag_name"][1:]
|
||||
|
||||
|
||||
def get_pypi_version() -> str:
|
||||
"""Return latest version string available on PyPI"""
|
||||
# pip is sad: it can't even tell what the newest available version is...
|
||||
# FIXME: Maybe think of something better than downloading latest tarball here?
|
||||
with TemporaryDirectory() as pypi_dir:
|
||||
cmd = ["pip", "download", "--no-deps", "--dest", pypi_dir]
|
||||
source_download = cmd + ["--no-binary", ":all:", PYPI_PROJECT]
|
||||
subprocess.run(source_download, stdout=subprocess.DEVNULL, check=True)
|
||||
for filename in os.listdir(pypi_dir):
|
||||
prefix, postfix = f"{PYPI_PROJECT}-", ".tar.gz"
|
||||
if filename.startswith(prefix) and filename.endswith(postfix):
|
||||
return filename[len(prefix) : -len(postfix)]
|
||||
assert False
|
||||
|
||||
|
||||
def verify_github_release(version: str, compare_dir: str) -> bool:
|
||||
"""Verify that given GitHub version artifacts match expected artifacts"""
|
||||
base_url = (
|
||||
f"https://github.com/{GITHUB_ORG}/{GITHUB_PROJECT}/releases/download"
|
||||
)
|
||||
tar = f"{PYPI_PROJECT}-{version}.tar.gz"
|
||||
wheel = f"{PYPI_PROJECT}-{version}-py3-none-any.whl"
|
||||
with TemporaryDirectory() as github_dir:
|
||||
for filename in [tar, wheel]:
|
||||
url = f"{base_url}/v{version}/{filename}"
|
||||
response = requests.get(url, stream=True)
|
||||
with open(os.path.join(github_dir, filename), "wb") as f:
|
||||
for data in response.iter_content():
|
||||
f.write(data)
|
||||
|
||||
return not dircmp(github_dir, compare_dir).diff_files
|
||||
|
||||
|
||||
def verify_pypi_release(version: str, compare_dir: str) -> bool:
|
||||
"""Verify that given PyPI version artifacts match expected artifacts"""
|
||||
with TemporaryDirectory() as pypi_dir:
|
||||
cmd = ["pip", "download", "--no-deps", "--dest", pypi_dir]
|
||||
target = f"{PYPI_PROJECT}=={version}"
|
||||
binary_download = cmd + [target]
|
||||
source_download = cmd + ["--no-binary", ":all:", target]
|
||||
|
||||
subprocess.run(binary_download, stdout=subprocess.DEVNULL, check=True)
|
||||
subprocess.run(source_download, stdout=subprocess.DEVNULL, check=True)
|
||||
|
||||
return not dircmp(pypi_dir, compare_dir).diff_files
|
||||
|
||||
|
||||
def finished(s: str):
|
||||
# clear line
|
||||
sys.stdout.write("\033[K")
|
||||
print(f"* {s}")
|
||||
|
||||
|
||||
def progress(s: str):
|
||||
# clear line
|
||||
sys.stdout.write("\033[K")
|
||||
# carriage return but no newline: next print will overwrite this one
|
||||
print(f" {s}...", end="\r", flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
success = True
|
||||
with TemporaryDirectory() as build_dir:
|
||||
|
||||
progress("Building release")
|
||||
build_version = build(build_dir)
|
||||
finished(f"Built release {build_version}")
|
||||
|
||||
git_version = get_git_version()
|
||||
assert git_version.startswith(build_version)
|
||||
if git_version != build_version:
|
||||
finished(f"WARNING: Git describes version as {git_version}")
|
||||
|
||||
progress("Checking GitHub latest version")
|
||||
github_version = get_github_version()
|
||||
if github_version != build_version:
|
||||
finished(f"WARNING: GitHub latest version is {github_version}")
|
||||
|
||||
progress("Checking PyPI latest version")
|
||||
pypi_version = get_pypi_version()
|
||||
if pypi_version != build_version:
|
||||
finished(f"WARNING: PyPI latest version is {pypi_version}")
|
||||
|
||||
progress("Downloading release from PyPI")
|
||||
if not verify_pypi_release(build_version, build_dir):
|
||||
# This is expected while build is not reproducible
|
||||
finished("ERROR: PyPI artifacts do not match built release")
|
||||
success = False
|
||||
|
||||
progress("Downloading release from GitHub")
|
||||
if not verify_github_release(build_version, build_dir):
|
||||
# This is expected while build is not reproducible
|
||||
finished("ERROR: GitHub artifacts do not match built release")
|
||||
success = False
|
||||
|
||||
if success:
|
||||
finished("Github and PyPI artifacts match the built release")
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue