tahoe-lafs/release.py

382 lines
13 KiB
Python

#! /usr/bin/env python
# Tahoe-LAFS -- secure, distributed storage grid
#
# Copyright © 2006-2012 The Tahoe-LAFS Software Foundation
#
# This file is part of Tahoe-LAFS.
#
# See the docs/about.rst file for licensing information.
import os
import shutil
import re
import sys
import glob
import subprocess
import argparse
import datetime
import pathlib
class bcolors:
HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKCYAN = "\033[96m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
parser = argparse.ArgumentParser()
parser.add_argument(
"--clean", action="store_true", help="Cancel existing release process, clean files"
)
parser.add_argument(
"--ignore-deps", action="store_true", help="Ignore dependency checks"
)
parser.add_argument("--fin", action="store_true", help="Finish release proccess")
parser.add_argument("--sign", type=str, help="Signing key")
parser.add_argument("--ticket", type=int, help="Ticket number", required=True)
parser.add_argument(
"--repo",
type=str,
help="Destination repo (fork), example: git@github.com:meejah/tahoe-lafs.git",
)
parser.add_argument("--retry", action="store_true", help="Retry release")
parser.add_argument("--tag", type=str, help="Release tag", required=True)
args = parser.parse_args()
TAG = args.tag
TICKET = args.ticket
TODAY = datetime.date.today()
BRANCH = "{ticket}.release-{tag}".format(
ticket=TICKET, tag=TAG
) # looks like XXXX.release-1.18.0
RELEASE_TITLE = "Release {tag} ({date})".format(tag=TAG, date=TODAY)
RELEASE_FOLDER = pathlib.Path("../tahoe-release-{0}".format(TAG))
RELEASE_PROGRESS = RELEASE_FOLDER.joinpath(".release-progress")
CONTINUE_INSTRUCTION = (
bcolors.BOLD
+ "Instruction : run {path}/venv/bin/python release.py --ignore-deps --tag {tag} --ticket {ticket} --sign {sign} --repo {repo} --fin".format(
path=RELEASE_FOLDER,
tag=TAG,
ticket=TICKET,
sign=args.sign if args.sign else "YOUR_SIGNING_KEY_HERE",
repo=args.repo if args.repo else "origin",
)
+ bcolors.ENDC
)
def clean():
"""
Remove all files and folders generated by release process
"""
shutil.rmtree(RELEASE_FOLDER, ignore_errors=True)
shutil.rmtree(RELEASE_PROGRESS, ignore_errors=True)
def check_dependencies():
"""
Check requirements before starting release
"""
try:
import wheel
except ModuleNotFoundError:
print(
f"{bcolors.WARNING}Warning: wheel is not installed. Install via pip!{bcolors.ENDC}"
)
sys.exit(1)
def step_complete(step):
"""
Check if step completed successfully
"""
return args.retry and os.path.isfile(RELEASE_PROGRESS.joinpath(step))
def mkfile(path):
"""
Create new file in path
"""
with open(path, "w") as f:
pass
def record_step(step):
"""
Record successful completion of step
"""
mkfile(RELEASE_PROGRESS.joinpath(step))
def start_release():
"""
Start release: make fresh clone, release env, release branch, generate news
"""
if step_complete("clone_complete"):
print(f"{bcolors.OKCYAN}Skipping clone step...{bcolors.ENDC}")
else:
try:
subprocess.run(
[
"git",
"clone",
"https://github.com/tahoe-lafs/tahoe-lafs.git",
RELEASE_FOLDER,
],
check=True,
)
record_step("clone_complete")
except Exception as e:
print(f"{bcolors.FAIL}INFO: Failed to clone! Exiting :(...{bcolors.ENDC}")
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
os.chdir(RELEASE_FOLDER)
if step_complete("install_deps_on_venv_complete"):
print(
f"{bcolors.OKCYAN}Skipping venv setup and dependency installation...{bcolors.ENDC}"
)
else:
try:
subprocess.run(["python", "-m", "venv", "venv"], check=True)
subprocess.run(
["./venv/bin/pip", "install", "--editable", ".[test]"], check=True
)
record_step("install_deps_on_venv_complete")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to install virtualenv and and dependencies clone! :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("branch_complete"):
print(f"{bcolors.OKCYAN}Skipping create release branch...{bcolors.ENDC}")
else:
try:
subprocess.run(["git", "branch", BRANCH], check=True)
record_step("branch_complete")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to create release branch! :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
subprocess.run(["git", "checkout", BRANCH])
if step_complete("tox_news_complete"):
print(f"{bcolors.OKCYAN}Skipping news generation...{bcolors.ENDC}")
else:
try:
subprocess.run(["./venv/bin/tox", "-e", "news"], check=True)
record_step("tox_news_complete")
except Exception as e:
print(f"{bcolors.FAIL}INFO: Failed to generate news! :(...{bcolors.ENDC}")
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("newsfragment_complete"):
print(f"{bcolors.OKCYAN}Skipping add news fragment...{bcolors.ENDC}")
else:
try:
mkfile("newsfragments/{ticket}.minor".format(ticket=TICKET))
record_step("newsfragment_complete")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to add newsfragment file! :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("commit_newsfragment_complete"):
print(f"{bcolors.OKCYAN}Skipping commit fragment...{bcolors.ENDC}")
else:
try:
subprocess.run(
["git", "add", "newsfragments/{ticket}.minor".format(ticket=TICKET)],
check=True,
)
subprocess.run(
["git", "commit", "-s", "-m", "tahoe-lafs-{tag} news".format(tag=TAG)],
check=True,
)
record_step("commit_newsfragment_complete")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to commit newsfragment! :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
updated_content = None
LINE = "=" * len(RELEASE_TITLE)
with open("NEWS.rst", "r") as f:
content = f.read()
updated_content = re.sub(
r"\.\.\stowncrier start line\n(Release\s(\d+\.\d+\.\d)(\.)post\d+\s\(\d{4}-\d{02}-\d{02}\))+(\n\'+)",
RELEASE_TITLE + "\n" + LINE,
content,
)
with open("updated_news.rst", "w") as f:
f.write(updated_content)
shutil.move("updated_news.rst", "NEWS.rst")
print(f"{bcolors.OKGREEN}First set of release steps completed! :).{bcolors.ENDC}")
print(
f"{bcolors.OKBLUE}Instruction: Move (cd) into the release folder : {RELEASE_FOLDER}.{bcolors.ENDC}"
)
print(
f"{bcolors.OKBLUE}Instruction: Please review {RELEASE_FOLDER.joinpath('NEWS.rst')} {bcolors.ENDC}"
)
print(
f"{bcolors.OKBLUE}Instruction: Update 'docs/known_issues.rst' (if necessary){bcolors.ENDC}"
)
print(
f"{bcolors.OKBLUE}Instruction: If any, commit changes to {RELEASE_FOLDER.joinpath('NEWS.rst')} and 'docs/known_issues.rst' {bcolors.ENDC}"
)
if not args.repo:
print(
f"{bcolors.OKBLUE}Instruction: If your intention IS NOT to push to \"origin (git@github.com:tahoe-lafs/tahoe-lafs.git)\", please add and set the '--repo' (example : --repo git@github.com:meejah/tahoe-lafs.git) flag to release complete command before executing"
)
if not args.sign:
print(
f"{bcolors.OKBLUE}Instruction: Modify the continue instruction and include your signing key via the '--sign' flag (example : --sign XXXXXXXXSAMPLEKEYXXXXXXXXX)"
)
if args.repo and ("http:" in args.repo or "https:" in args.repo):
print(
f"{bcolors.OKBLUE}Instruction: You would have to type in your github username/password, keep it near."
)
print(
f"{bcolors.OKBLUE}Instruction: Make sure your gpg passphrase is ready for the next step."
)
print(CONTINUE_INSTRUCTION)
def complete_release():
"""
Finalize release: push release branch, make and sign artifacts
"""
os.chdir(RELEASE_FOLDER)
if step_complete("push_branch"):
print(f"{bcolors.OKCYAN}Skipping push release branch...{bcolors.ENDC}")
else:
try:
REPO = args.repo if args.repo else "origin"
subprocess.run(["git", "push", REPO, BRANCH], check=True)
record_step("push_branch")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to push release branch :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("sign_tag"):
print(f"{bcolors.OKCYAN}Skipping sign release tag...{bcolors.ENDC}")
else:
try:
SIGNING_KEY = args.sign
subprocess.run(
[
"git",
"tag",
"-s",
"-u",
SIGNING_KEY,
"-m",
RELEASE_TITLE.lower(),
TAG,
],
check=True,
)
record_step("sign_tag")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to sign release tag, exiting :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("build_code"):
print(f"{bcolors.OKCYAN}Skipping local code build...{bcolors.ENDC}")
else:
try:
subprocess.run(
["./venv/bin/tox", "-e", "py37,codechecks,docs,integration"], check=True
)
subprocess.run(
["./venv/bin/tox", "-e", "deprecations,upcoming-deprecations"],
check=True,
)
record_step("build_code")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed to build code locally :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("build_tarballs"):
print(
f"{bcolors.OKCYAN}Skipping build tarballs, using old tarballs...{bcolors.ENDC}"
)
else:
try:
subprocess.run(["./venv/bin/tox", "-e", "tarballs"], check=True)
record_step("build_tarballs")
except Exception as e:
print(
f"{bcolors.FAIL}INFO: Failed build release artifacts :(...{bcolors.ENDC}"
)
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
if step_complete("sign_tarballs"):
print(
f"{bcolors.OKCYAN}Skipping sign tarballs, using old tarballs...{bcolors.ENDC}"
)
else:
try:
paths = [
pathlib.Path(p)
for p in glob.glob(str(RELEASE_FOLDER.joinpath(f"dist/*")))
]
for path in paths:
subprocess.run(
[
"gpg",
"--pinentry=loopback",
"--armor",
"--detach-sign",
path,
],
check=True,
)
record_step("sign_tarballs")
except Exception as e:
print(f"{bcolors.FAIL}INFO: Failed to sign tarballs :(...{bcolors.ENDC}")
print(f"{bcolors.FAIL} {e} {bcolors.ENDC}")
sys.exit(1)
print(f"{bcolors.OKGREEN}Cleaning working files...{bcolors.ENDC}")
clean()
print(f"{bcolors.OKGREEN}Release complete...{bcolors.ENDC}")
if args.clean and not args.retry:
print(f"{bcolors.OKCYAN}Start cleaning...{bcolors.ENDC}")
clean()
print(f"{bcolors.OKGREEN}Cleaning complete...{bcolors.ENDC}")
if args.retry:
print(
f"{bcolors.OKCYAN}Picking up from last try in {RELEASE_FOLDER}...{bcolors.ENDC}"
)
if args.fin:
if args.sign is None:
print("Signing key required to complete release process")
sys.exit(1)
complete_release()
print(f"{bcolors.OKGREEN}INFO: Release procedure complete! :)...{bcolors.ENDC}")
sys.exit(0)
if not args.ignore_deps:
check_dependencies()
if not os.path.exists(RELEASE_PROGRESS):
os.mkdir(RELEASE_PROGRESS)
start_release()