Author: julianfoad Date: Tue Dec 12 22:14:52 2017 New Revision: 1817962 URL: http://svn.apache.org/viewvc?rev=1817962&view=rev Log: First draft of a new tool to automate steps in branching a minor release.
Incomplete and untested. * tools/dist/create-minor-release-branch.py: New file. Added: subversion/trunk/tools/dist/create-minor-release-branch.py (with props) Added: subversion/trunk/tools/dist/create-minor-release-branch.py URL: http://svn.apache.org/viewvc/subversion/trunk/tools/dist/create-minor-release-branch.py?rev=1817962&view=auto ============================================================================== --- subversion/trunk/tools/dist/create-minor-release-branch.py (added) +++ subversion/trunk/tools/dist/create-minor-release-branch.py Tue Dec 12 22:14:52 2017 @@ -0,0 +1,403 @@ +#!/usr/bin/env python +# python: coding=utf-8 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + + +# About this script: +# This script is intended to simplify creating Subversion releases for +# any of the supported release lines of Subversion. +# It works well with our Apache infrastructure, and should make rolling, +# posting, and announcing releases dirt simple. +# +# This script may be run on a number of platforms, but it is intended to +# be run on people.apache.org. As such, it may have dependencies (such +# as Python version) which may not be common, but are guaranteed to be +# available on people.apache.org. + +# It'd be kind of nice to use the Subversion python bindings in this script, +# but people.apache.org doesn't currently have them installed + +# Stuff we need +import os +import re +import sys +import glob +import fnmatch +import shutil +import urllib2 +import hashlib +import tarfile +import logging +import datetime +import tempfile +import operator +import itertools +import subprocess +import argparse # standard in Python 2.7 + + +# Some constants +repos = 'https://svn.apache.org/repos/asf/subversion' +secure_repos = 'https://svn.apache.org/repos/asf/subversion' + +# Local working copies +trunk_wc_path = 'svn-trunk' +def get_trunk_wc_path(path=None): + if path is None: return trunk_wc_path + return os.path.join(trunk_wc_path, path) +def get_branch_wc_path(ver, path=None): + branch_wc_path = os.path.join('svn-branches', ver.branch + '.x') + if path is None: return branch_wc_path + return os.path.join(branch_wc_path, path) + +def get_trunk_url(): + return secure_repos + '/trunk' +def get_branch_url(ver): + return secure_repos + '/branches/' + ver.branch + '.x' +def get_tag_url(ver): + return secure_repos + '/tags/' + ver.base + +#---------------------------------------------------------------------- +# Utility functions + +class Version(object): + regex = re.compile(r'(\d+).(\d+).(\d+)(?:-(?:(rc|alpha|beta)(\d+)))?') + + def __init__(self, ver_str): + # Special case the 'trunk-nightly' version + if ver_str == 'trunk-nightly': + self.major = None + self.minor = None + self.patch = None + self.pre = 'nightly' + self.pre_num = None + self.base = 'nightly' + self.branch = 'trunk' + return + + match = self.regex.search(ver_str) + + if not match: + raise RuntimeError("Bad version string '%s'" % ver_str) + + self.major = int(match.group(1)) + self.minor = int(match.group(2)) + self.patch = int(match.group(3)) + + if match.group(4): + self.pre = match.group(4) + self.pre_num = int(match.group(5)) + else: + self.pre = None + self.pre_num = None + + self.base = '%d.%d.%d' % (self.major, self.minor, self.patch) + self.branch = '%d.%d' % (self.major, self.minor) + + def is_prerelease(self): + return self.pre != None + + def is_recommended(self): + return self.branch == recommended_release + + def get_download_anchor(self): + if self.is_prerelease(): + return 'pre-releases' + else: + if self.is_recommended(): + return 'recommended-release' + else: + return 'supported-releases' + + def get_ver_tags(self, revnum): + # These get substituted into svn_version.h + ver_tag = '' + ver_numtag = '' + if self.pre == 'alpha': + ver_tag = '" (Alpha %d)"' % self.pre_num + ver_numtag = '"-alpha%d"' % self.pre_num + elif self.pre == 'beta': + ver_tag = '" (Beta %d)"' % args.version.pre_num + ver_numtag = '"-beta%d"' % self.pre_num + elif self.pre == 'rc': + ver_tag = '" (Release Candidate %d)"' % self.pre_num + ver_numtag = '"-rc%d"' % self.pre_num + elif self.pre == 'nightly': + ver_tag = '" (Nightly Build r%d)"' % revnum + ver_numtag = '"-nightly-r%d"' % revnum + else: + ver_tag = '" (r%d)"' % revnum + ver_numtag = '""' + return (ver_tag, ver_numtag) + + def __serialize(self): + return (self.major, self.minor, self.patch, self.pre, self.pre_num) + + def __eq__(self, that): + return self.__serialize() == that.__serialize() + + def __ne__(self, that): + return self.__serialize() != that.__serialize() + + def __hash__(self): + return hash(self.__serialize()) + + def __lt__(self, that): + if self.major < that.major: return True + if self.major > that.major: return False + + if self.minor < that.minor: return True + if self.minor > that.minor: return False + + if self.patch < that.patch: return True + if self.patch > that.patch: return False + + if not self.pre and not that.pre: return False + if not self.pre and that.pre: return False + if self.pre and not that.pre: return True + + # We are both pre-releases + if self.pre != that.pre: + return self.pre < that.pre + else: + return self.pre_num < that.pre_num + + def __str__(self): + "Return an SVN_VER_NUMBER-formatted string, or 'nightly'." + if self.pre: + if self.pre == 'nightly': + return 'nightly' + else: + extra = '-%s%d' % (self.pre, self.pre_num) + else: + extra = '' + + return self.base + extra + + def __repr__(self): + + return "Version(%s)" % repr(str(self)) + +#---------------------------------------------------------------------- +def get_prefix(base_dir): + return os.path.join(base_dir, 'prefix') + +def get_tempdir(base_dir): + return os.path.join(base_dir, 'tempdir') + +def get_workdir(base_dir): + return os.path.join(get_tempdir(base_dir), 'working') + +def run(cmd, dry_run=False): + print('+ ' + ' '.join(cmd)) + if not dry_run: + stdout = subprocess.check_output(cmd) + print(stdout) + +def run_svn(cmd, dry_run=False): + run(['svn'] + cmd, dry_run) + +def svn_commit(cmd): + run_svn(['commit'] + cmd, dry_run=True) + +#---------------------------------------------------------------------- +def edit_file(path, pattern, replacement): + print("Editing '%s'" % (path,)) + print(" pattern='%s'" % (pattern,)) + print(" replace='%s'" % (replacement,)) + old_text = open(path, 'r').read(): + new_text = re.sub(pattern, replacement, old_text) + assert new_text != old_text + open(path, 'w').write(new_text) + +def prepend_file(relpath, text): + print("Prepending to '%s'" % (path,)) + print(" text='%s'" % (text,)) + original = open(path, 'r').read() + open(path, 'w').write(text).write(original) + +#---------------------------------------------------------------------- +def make_release_branch(ver): + run_svn(['copy', get_trunk_url(), get_branch_url(ver), + '-m', 'Create the ' + ver.branch + '.x release branch.'], + dry_run=True) + +#---------------------------------------------------------------------- +def update_minor_ver_in_trunk(ver): + trunk_wc = get_trunk_wc_path() + trunk_url = get_trunk_url() + run_svn(['checkout', trunk_url, trunk_wc]) + + prev_ver = Version('1.%d.0' % (ver.minor - 1,)) + next_ver = Version('1.%d.0' % (ver.minor + 1,)) + relpaths = [] + + relpath = 'subversion/include/svn_version.h' + relpaths.append(relpath) + edit_file(get_trunk_wc_path(relpath), + r'^#define SVN_VER_MINOR *%s$' % (prev_ver.minor,), + r'^#define SVN_VER_MINOR *%s$' % (ver.minor,)) + + relpath = 'subversion/tests/cmdline/svntest/main.py' + relpaths.append(relpath) + edit_file(get_trunk_wc_path(relpath), + r'^SVN_VER_MINOR = %s$' % (prev_ver.minor,), + r'^SVN_VER_MINOR = %s$' % (ver.minor,)) + + relpath = 'subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java' + relpaths.append(relpath) + try: + # since r1817921 (just after branching 1.10) + edit_file(get_trunk_wc_path(relpath), + r'SVN_VER_MINOR = %s;' % (prev_ver.minor,), + r'SVN_VER_MINOR = %s;' % (ver.minor,)) + except: + # before r1817921: two separate places + edit_file(get_trunk_wc_path(relpath), + r'version.isAtLeast\(1, %s, 0\)' % (prev_ver.minor,), + r'version.isAtLeast\(1, %s, 0\)' % (ver.minor,)) + edit_file(get_trunk_wc_path(relpath), + r'1.%s.0, but' % (prev_ver.minor,), + r'1.%s.0, but' % (ver.minor,)) + + relpath = 'CHANGES' + relpaths.append(relpath) + # insert at beginning of CHANGES file + prepend_file(get_trunk_wc_path(relpath), + 'Version ' + next_ver.base + '\n' + + '(?? ??? 20XX, from /branches/' + next_ver.branch + '.x)\n' + + get_tag_url(next_ver) + '\n' + + '\n') + + log_msg = '''\ +Increment the trunk version number, and introduce a new CHANGES +section for the upcoming %s release. + +* subversion/include/svn_version.h, + subversion/tests/cmdline/svntest/main.py, + subversion/bindings/javahl/src/org/apache/subversion/javahl/NativeResources.java: + (SVN_VER_MINOR): Increment version number. + +* CHANGES: New section for %s. +''' % (ver.base, next_ver.base) + commit_paths = [get_trunk_wc_path(p) for p in relpaths] + svn_commit(commit_paths + ['-m', log_msg]) + +#---------------------------------------------------------------------- +def create_status_file_on_branch(ver): + branch_wc = get_branch_wc_path(ver) + branch_url = get_branch_url(ver) + run_svn(['checkout', branch_url, branch_wc, '--depth=immediates']) + + status_local_path = os.path.join(branch_wc, 'STATUS') + text='''\ + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * THIS RELEASE STREAM IS OPEN FOR STABILIZATION. * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +This file tracks the status of releases in the %s.x line. + +See http://subversion.apache.org/docs/community-guide/releasing.html#release-stabilization +for details on how release lines and voting work, what kinds of bugs can +delay a release, etc. + +Status of %s: + +Candidate changes: +================== + + +Veto-blocked changes: +===================== + + +Approved changes: +================= +''' % (ver.branch, ver.base) + open(status_local_path, 'wx').write(text) + run_svn(['add', status_local_path]) + svn_commit([status_local_path, + '-m', '* branches/' + ver.branch + '.x/STATUS: New file.']) + +#---------------------------------------------------------------------- +def update_backport_bot(ver): + print("""MANUAL STEP: Fork & edit & pull-request on GitHub: +https://github.com/apache/infrastructure-puppet/blob/deployment/modules/svnqavm_pvm_asf/manifests/init.pp +"Add new %s.x branch to list of backport branches" +""" % (ver.branch,)) + +#---------------------------------------------------------------------- +def steps(args): + ver = Version('1.10.0') + + make_release_branch(ver) + update_minor_ver_in_trunk(ver) + create_status_file_on_branch(ver) + update_backport_bot(ver) + # update_buildbot_config(ver) + + +#---------------------------------------------------------------------- +# Main entry point for argument parsing and handling + +def main(): + 'Parse arguments, and drive the appropriate subcommand.' + + # Setup our main parser + parser = argparse.ArgumentParser( + description='Create an Apache Subversion release branch.') + parser.add_argument('--verbose', action='store_true', default=False, + help='Increase output verbosity') + parser.add_argument('--base-dir', default=os.getcwd(), + help='''The directory in which to create needed files and + folders. The default is the current working + directory.''') + subparsers = parser.add_subparsers(title='subcommands') + + # Setup the parser for the build-env subcommand + subparser = subparsers.add_parser('steps', + help='''Run the release-branch-creation steps.''') + subparser.set_defaults(func=steps) + + # Parse the arguments + args = parser.parse_args() + + # Set up logging + logger = logging.getLogger() + if args.verbose: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + # Fix up our path so we can use our installed versions + os.environ['PATH'] = os.path.join(get_prefix(args.base_dir), 'bin') + ':' \ + + os.environ['PATH'] + + # Make timestamps in tarballs independent of local timezone + os.environ['TZ'] = 'UTC' + + # finally, run the subcommand, and give it the parsed arguments + args.func(args) + + +if __name__ == '__main__': + main() Propchange: subversion/trunk/tools/dist/create-minor-release-branch.py ------------------------------------------------------------------------------ svn:eol-style = native Propchange: subversion/trunk/tools/dist/create-minor-release-branch.py ------------------------------------------------------------------------------ svn:executable = * Propchange: subversion/trunk/tools/dist/create-minor-release-branch.py ------------------------------------------------------------------------------ svn:mime-type = text/x-python