mirror of
https://github.com/opentripplanner/OpenTripPlanner.git
synced 2025-02-22 14:54:12 +01:00
826 lines
30 KiB
Python
Executable file
826 lines
30 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Global constants ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
POM_FILE_NAME = 'pom.xml'
|
|
# GitHub label to indicate that a PR needs to be bumped
|
|
LBL_BUMP_SER_VER_ID = 'bump serialization id'
|
|
SER_VER_ID_PROPERTY = 'otp.serialization.version.id'
|
|
SER_VER_ID_PROPERTY_PTN = SER_VER_ID_PROPERTY.replace('.', r'\.')
|
|
SER_VER_ID_PATTERN = re.compile(
|
|
'<' + SER_VER_ID_PROPERTY_PTN + r'>\s*(.*)\s*</' + SER_VER_ID_PROPERTY_PTN + '>')
|
|
STATE_FILE = '.custom_release_resume_state.json'
|
|
SUMMARY_FILE = '.custom_release_summary.md'
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Classes ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
# Configuration read from the 'custom-release-env.json' file
|
|
class Config:
|
|
def __init__(self):
|
|
self.upstream_remote = None
|
|
self.release_remote = None
|
|
self.release_branch = None
|
|
self.ext_branches = None
|
|
self.include_prs_label = None
|
|
self.ser_ver_id_prefix = None
|
|
|
|
def release_path(self, branch):
|
|
return f'{self.release_remote}/{branch}'
|
|
|
|
def release_branch_path(self):
|
|
return self.release_path(self.release_branch)
|
|
|
|
def __str__(self):
|
|
return (f"<"
|
|
f"upstream_remote: '{self.upstream_remote}', "
|
|
f"release_remote: '{self.release_remote}', "
|
|
f"release_branch: '{self.release_branch}', "
|
|
f"ext_branches: {self.ext_branches}, "
|
|
f"include_prs_label: '{self.include_prs_label}', "
|
|
f"ser_ver_id_prefix: '{self.ser_ver_id_prefix}'>")
|
|
|
|
|
|
# CLI Arguments and Options
|
|
class CliOptions:
|
|
|
|
def __init__(self):
|
|
self.base_revision = None
|
|
self.dry_run = False
|
|
self.debugging = False
|
|
self.releaseOnly = False
|
|
self.bump_ser_ver_id = False
|
|
self.skip_prs = False
|
|
self.printSummary = False
|
|
|
|
def verify(self):
|
|
if self.releaseOnly and self.base_revision:
|
|
error(f"<base-revision> is not allowed with option '--release', was: {options.base_revision}")
|
|
|
|
# Return the script <base revision> argument if set, if not use 'HEAD'(--release)
|
|
def release_base(self):
|
|
return self.base_revision if self.base_revision else 'HEAD'
|
|
|
|
def __str__(self):
|
|
return (f"<"
|
|
f"base_revision: {self.base_revision}, "
|
|
f"dry_run: {self.dry_run}, "
|
|
f"debugging: {self.debugging}, "
|
|
f"releaseOnly: '{self.releaseOnly}', "
|
|
f"bump_ser_ver_id: '{self.bump_ser_ver_id}', "
|
|
f"skip_prs: '{self.skip_prs}', "
|
|
f"printSummary: '{self.printSummary}'>")
|
|
|
|
|
|
# The execution state of the script + the CLI arguments
|
|
class ScriptState:
|
|
|
|
def __init__(self):
|
|
self.current_ser_ver_id = None
|
|
self.new_ser_ver_id = None
|
|
self.major_version = None
|
|
self.current_version = None
|
|
self.new_version = None
|
|
self.pr_labels = {}
|
|
self.pr_titles = {}
|
|
self.prs_bump_ser_ver_id = False
|
|
self.gotoStep = False
|
|
self.step = None
|
|
|
|
def new_version_tag(self):
|
|
return f'v{self.new_version}'
|
|
|
|
def curr_version_tag(self):
|
|
return f'v{self.current_version}'
|
|
|
|
def new_version_description(self):
|
|
return f'Version {self.new_version} ({self.new_ser_ver_id})'
|
|
|
|
def is_ser_ver_id_new(self):
|
|
return self.new_ser_ver_id != self.current_ser_ver_id
|
|
|
|
def run(self, step):
|
|
if not self.gotoStep:
|
|
debug(f'Run step: {step}')
|
|
return True
|
|
if self.step == step:
|
|
self.gotoStep = False
|
|
debug(f'Resume step: {step}')
|
|
return True
|
|
else:
|
|
debug(f'Skip step: {step}')
|
|
return False
|
|
|
|
@staticmethod
|
|
def do_resume():
|
|
return os.path.exists(STATE_FILE)
|
|
|
|
def delete_progress_file(self):
|
|
execute('rm', STATE_FILE)
|
|
self.gotoStep = False
|
|
self.step = None
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Global Variables ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
config: Config = Config()
|
|
options = CliOptions()
|
|
state = ScriptState()
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Main ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
def main():
|
|
setup_and_verify()
|
|
|
|
# Prepare release
|
|
if not options.releaseOnly:
|
|
reset_release_branch_to_base_revision()
|
|
merge_in_labeled_prs()
|
|
merge_in_ext_branches()
|
|
merge_in_old_release_with_no_changes()
|
|
run_custom_release_extensions()
|
|
|
|
merge_in_old_release_with_no_changes()
|
|
set_maven_pom_version()
|
|
set_ser_ver_id()
|
|
run_maven_test()
|
|
commit_new_versions()
|
|
tag_release()
|
|
push_release_branch_and_tag()
|
|
print_summary()
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Top level functions ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
def setup_and_verify():
|
|
section('Setting up release process and verifying the environment')
|
|
parse_and_verify_cli_arguments_and_options()
|
|
if state.do_resume():
|
|
resume()
|
|
else:
|
|
load_config()
|
|
verify_script_run_from_root()
|
|
verify_git_installed()
|
|
verify_maven_installed()
|
|
fetch_all_git_remotes()
|
|
verify_release_base_and_release_branch_exist()
|
|
verify_no_local_git_changes()
|
|
resolve_version_number()
|
|
resolve_new_version()
|
|
list_labeled_prs()
|
|
resolve_new_ser_ver_id()
|
|
print_setup()
|
|
|
|
|
|
def reset_release_branch_to_base_revision():
|
|
section('Reset release branch to base revision ...')
|
|
# Fetching is normally not necessary, but the default depth is often just the top commit in a
|
|
# CI environment. We need tags and ser.ver.id from older commits and common ancestor commit
|
|
# when merging . If git fails with "fatal: refusing to merge unrelated histories" the depth
|
|
# is not deep enough.
|
|
git('fetch', config.release_remote, config.release_branch)
|
|
git('checkout', '-B', config.release_branch, config.release_branch_path())
|
|
git('reset', '--hard', options.base_revision)
|
|
|
|
|
|
def merge_in_labeled_prs():
|
|
section('Merge in labeled PRs ...')
|
|
for pr in state.pr_labels:
|
|
# A temp branch is needed here since the PR is in the upstream remote repo
|
|
temp_branch = f'pull-request-{pr}'
|
|
if section_w_resume(temp_branch, f'Merge in PR #{pr} - {state.pr_titles[pr]}'):
|
|
git('fetch', config.upstream_remote, f'pull/{pr}/head:{temp_branch}')
|
|
git('merge', temp_branch)
|
|
git('branch', '-D', temp_branch)
|
|
|
|
|
|
def merge_in_ext_branches():
|
|
if len(config.ext_branches) == 0:
|
|
info('\nNo config branch configured, mering is skipped.')
|
|
return
|
|
if section_w_resume('merge_in_ext_branches', 'Merge in config branch ...'):
|
|
for branch in config.ext_branches:
|
|
# See reset_release_branch_to_base_revision() for why wee ned to fetch.
|
|
git('fetch', config.release_remote, branch)
|
|
git('merge', config.release_path(branch))
|
|
info(f'Config branch merged: {config.release_path(branch)}')
|
|
|
|
|
|
# Merge the old version into the new version. This only keep a reference to the old version, the
|
|
# resulting git tree of the merge is that of the new branch head, effectively ignoring all changes
|
|
# from the old release. This creates a continuous line of releases in the release branch.
|
|
def merge_in_old_release_with_no_changes():
|
|
section('Merge old release into the release branch ...')
|
|
git('merge', '-s', 'ours', config.release_branch_path(), '-m',
|
|
'Merge old release into the release branch - NO CHANGES COPIED OVER')
|
|
info('Merged - NO CHANGES COPIED OVER')
|
|
|
|
|
|
def run_custom_release_extensions():
|
|
section('Run custom release extensions bash script ...')
|
|
ext_script = 'script/custom-release-extension'
|
|
if os.path.exists(ext_script):
|
|
execute(ext_script)
|
|
else:
|
|
print(f"Script '{ext_script}' not found!")
|
|
|
|
|
|
def set_maven_pom_version():
|
|
section('Set Maven project version ...')
|
|
execute('mvn', 'versions:set', f'-DnewVersion={state.new_version}',
|
|
'-DgenerateBackupPoms=false', quiet=True)
|
|
info(f'New version set: {state.new_version}')
|
|
|
|
|
|
def set_ser_ver_id():
|
|
section('Set serialization.version.id ...')
|
|
new_ser_ver_id_element = f'<{SER_VER_ID_PROPERTY}>{state.new_ser_ver_id}</{SER_VER_ID_PROPERTY}>'
|
|
|
|
with open(POM_FILE_NAME, 'r') as input_stream:
|
|
pom_file = input_stream.read()
|
|
pom_file = re.sub(SER_VER_ID_PATTERN, new_ser_ver_id_element, pom_file, count=1)
|
|
with open(POM_FILE_NAME, 'w') as output:
|
|
output.write(pom_file)
|
|
prefix = 'New' if (state.is_ser_ver_id_new()) else 'Same'
|
|
info(f'{prefix} serialization.version.id set: {state.new_ser_ver_id}')
|
|
|
|
|
|
def commit_new_versions():
|
|
section('Commit new version with version and serialization version id set ...')
|
|
git('add', '.')
|
|
git_dr('commit', '--all', '-m', state.new_version_description())
|
|
info(f'Commit done: {state.new_version_description()}')
|
|
|
|
|
|
def tag_release():
|
|
section(f'Tag release with {state.new_version} ...')
|
|
git_im('tag', '-a', state.new_version_tag(), '-m', state.new_version_description())
|
|
info(f'Tag done: {state.new_version_tag()}')
|
|
|
|
|
|
def push_release_branch_and_tag():
|
|
section('Push new release with pom.xml versions and new tag')
|
|
git_dr('push', '-f', f'{config.release_remote}', f'{config.release_branch}')
|
|
git_dr('push', '-f', f'{config.release_remote}', f'v{state.new_version}')
|
|
info(f'Release pushed to: {config.release_branch_path()}')
|
|
delete_script_state()
|
|
info('\nRELEASE SUCCESS!\n')
|
|
|
|
|
|
def print_summary():
|
|
if not options.printSummary:
|
|
return
|
|
section(f'Print summary file: {SUMMARY_FILE}')
|
|
|
|
release_version_summary = f"""
|
|
# OTP Release Summary
|
|
|
|
## Version
|
|
|
|
- New version/git tag: `{state.new_version}`
|
|
- New serialization version: `{state.new_ser_ver_id}`
|
|
- Old serialization version: `{state.current_ser_ver_id}`
|
|
|
|
"""
|
|
with open(SUMMARY_FILE, mode="w", encoding="UTF-8") as f:
|
|
print(release_version_summary, file=f)
|
|
if len(state.pr_titles) > 0:
|
|
print("## Pull Requests", file=f)
|
|
print(f"These PRs are tagged with {config.include_prs_label}.\n", file=f)
|
|
for pr in state.pr_titles:
|
|
url = f"https://github.com/opentripplanner/OpenTripPlanner/pull/{pr}"
|
|
print(f" - {state.pr_titles[pr]} [#{pr}]({url}) {state.pr_labels[pr]}".replace("'", "`"),
|
|
file=f)
|
|
p = execute("./script/changelog-diff.py", state.curr_version_tag(), state.new_version_tag())
|
|
print(p.stdout, file=f)
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Setup and verify ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
def parse_and_verify_cli_arguments_and_options():
|
|
if os.getenv('CUSTOM_RELEASE_LOG_LEVEL') == 'debug':
|
|
options.debugging = True
|
|
args = []
|
|
for arg in sys.argv[1:]:
|
|
if re.match(r'(-h|--help)', arg):
|
|
print_help()
|
|
elif re.match(r'(--dryRun)', arg):
|
|
options.dry_run = True
|
|
elif re.match(r'(--debug)', arg):
|
|
options.debugging = True
|
|
elif re.match(r'(--release)', arg):
|
|
options.releaseOnly = True
|
|
elif re.match(r'(--serVerId)', arg):
|
|
options.bump_ser_ver_id = True
|
|
elif re.match(r'(--skipPRs)', arg):
|
|
options.skip_prs = True
|
|
elif re.match(r'(--summary)', arg):
|
|
options.printSummary = True
|
|
else:
|
|
options.base_revision = arg
|
|
args.append(arg)
|
|
if len(args) > 1:
|
|
error(f'Expected one argument, <base-revision>, but got: {args}')
|
|
options.verify()
|
|
|
|
|
|
def resume():
|
|
section('Resume')
|
|
print(f'''
|
|
Do you want to resume the previous execution of the script?
|
|
- You must first fix the merge conflict or unit-test failing.
|
|
- Then commit your changes.
|
|
- Then run this script again, the script will automatically resume the release
|
|
process at the same place it failed.
|
|
|
|
| NOTE! The script entered resume because a progress tracking file exist. To
|
|
| avoid this you may delete the {STATE_FILE} before starting the script.
|
|
|
|
|
| > rm {STATE_FILE}
|
|
|
|
Press
|
|
'y' to resume release.
|
|
'x' to exit.
|
|
'd' to exit and delete progress file.
|
|
''')
|
|
answer = 'NOT_SET'
|
|
p = re.compile(r'[\syYxXdD]')
|
|
while not p.match(answer):
|
|
answer = input("Press 'x', 'y', or 'd' then <enter>: ").lower()
|
|
if answer == 'x':
|
|
exit(0)
|
|
if answer == 'd':
|
|
state.delete_progress_file()
|
|
exit(0)
|
|
read_script_state()
|
|
|
|
|
|
def load_config():
|
|
section('Load configuration...')
|
|
with open('script/custom-release-env.json', 'r') as f:
|
|
doc = json.load(f)
|
|
config.upstream_remote = doc['upstream_remote']
|
|
config.release_remote = doc['release_remote']
|
|
config.release_branch = doc['release_branch']
|
|
config.ext_branches = doc['ext_branches']
|
|
config.include_prs_label = doc['include_prs_label']
|
|
config.ser_ver_id_prefix = doc['ser_ver_id_prefix']
|
|
debug(f'Config loaded: {config}')
|
|
|
|
if len(config.ser_ver_id_prefix) > 2:
|
|
error(f"Configure the 'ser_ver_id_prefix'. The prefix must be maximum two characters long. "
|
|
f"Value: <{config.ser_ver_id_prefix}>")
|
|
|
|
|
|
def verify_script_run_from_root():
|
|
# Assume we are in the root directory if the 'LICENSE' file exist
|
|
execute('ls', 'LICENSE', error_msg=f'Run script from project root directory.')
|
|
|
|
|
|
def verify_git_installed():
|
|
execute('git', '--version', quiet=False)
|
|
|
|
|
|
def verify_maven_installed():
|
|
execute('mvn', '--version')
|
|
|
|
|
|
def verify_release_base_and_release_branch_exist():
|
|
if options.releaseOnly:
|
|
info('Verify release branch/commit exist ...')
|
|
else:
|
|
info('Verify base revision and release branch/commit exist ...')
|
|
git('rev-parse', '--quiet', '--verify', options.release_base(),
|
|
error_msg='Base revision not found!')
|
|
git('rev-parse', '--quiet', '--verify', config.release_branch_path(),
|
|
error_msg='Release branch not found!')
|
|
|
|
|
|
def verify_no_local_git_changes():
|
|
info('Verify no local changes exist ...')
|
|
git('diff-index', '--quiet', 'HEAD', error_msg='There are local changes!')
|
|
|
|
|
|
# Make sure all remote repos are up-to date
|
|
def fetch_all_git_remotes():
|
|
git('fetch', '--all', error_msg='Git fetch all remotes failed!')
|
|
|
|
|
|
def resolve_version_number():
|
|
info(f'Resolve version number ...')
|
|
version_qualifier = config.release_remote
|
|
state.major_version = read_major_version_from_pom(version_qualifier, options.release_base())
|
|
|
|
|
|
def resolve_new_version():
|
|
info('Resolve new version number ...')
|
|
p = git('tag', '--list', '--sort=-v:refname', error_msg='Fetch git tags failed!')
|
|
tags = p.stdout.splitlines()
|
|
|
|
prefix = f'{state.major_version}-{config.release_remote}-'
|
|
pattern = re.compile('v' + prefix.replace('.', r'\.') + r'(\d+)')
|
|
max_tag_version = max((int(m.group(1)) for tag in tags if (m := pattern.match(tag))), default=0)
|
|
state.current_version = prefix + str(max_tag_version)
|
|
state.new_version = prefix + str(1 + max_tag_version)
|
|
|
|
|
|
def list_labeled_prs():
|
|
if options.skip_prs or (not config.include_prs_label):
|
|
info('Skip merging in GitHub PRs.')
|
|
return
|
|
info('Get PRs to include and their labels from GitHub - This requires authentication ...')
|
|
|
|
|
|
query_text = '''
|
|
query ReadOpenPullRequests {
|
|
repository(owner:\\"opentripplanner\\", name:\\"OpenTripPlanner\\") {
|
|
pullRequests(first: 100, states: OPEN, labels: \\"''' + config.include_prs_label + '''\\") {
|
|
nodes {
|
|
number,
|
|
title,
|
|
labels(first: 20) {
|
|
nodes {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
'''
|
|
|
|
# The query body needs to be on one line, for an unknown reason. Replace 2 or more whitespace
|
|
# with a singe space.
|
|
query_text = re.sub(r'(?s)\s{2,}', ' ', query_text)
|
|
post_body = '''
|
|
{
|
|
"query":"''' + query_text + '''",
|
|
"operationName":"ReadOpenPullRequests"
|
|
}
|
|
'''
|
|
git_hub_access_token = os.environ['CUSTOM_RELEASE_GIT_HUB_API_TOKEN']
|
|
result = execute('curl', '-H', f'Authorization: Bearer {git_hub_access_token}', '-X', 'POST',
|
|
'-d', post_body, 'https://api.github.com/graphql',
|
|
error_msg='GitHub GraphQL Query failed!', quiet_err=True)
|
|
|
|
# Example response
|
|
# {"data":{"repository":{"pullRequests":{"nodes":[{"number":2222,"labels":{"nodes":[{"name":"bump serialization id"},{"name":"Entur Test"}]}}]}}}}
|
|
json_doc = json.loads(result.stdout)
|
|
for node in json_doc['data']['repository']['pullRequests']['nodes']:
|
|
pr_number = node['number']
|
|
state.pr_titles[str(pr_number)] = node['title']
|
|
pr_labels = []
|
|
labels = node['labels']['nodes']
|
|
# GitHub labels are not case-sensitive, hence the '(?i)'
|
|
ptn = re.compile(f'(?i)({LBL_BUMP_SER_VER_ID}|{config.include_prs_label})')
|
|
for label in labels:
|
|
lbl_name = label['name']
|
|
if ptn.match(lbl_name):
|
|
pr_labels.append(lbl_name)
|
|
state.pr_labels[str(pr_number)] = pr_labels
|
|
|
|
state.prs_bump_ser_ver_id = any(
|
|
LBL_BUMP_SER_VER_ID in labels for labels in state.pr_labels.values())
|
|
|
|
|
|
def resolve_new_ser_ver_id():
|
|
info('Resolve the new serialization version id ...')
|
|
curr_release_hash = git_show_ref(git_tag(state.current_version))
|
|
curr_ser_ver_id = read_ser_ver_id_from_pom_file(curr_release_hash)
|
|
bump_ser_ver_id = options.bump_ser_ver_id
|
|
|
|
# If none of the PRs have the 'bump serialization id' label set, then find the upstream
|
|
# serialization-version-id for the release-base and the current version. If these
|
|
# serialization-version-ids are different, then we need to bump the new release serialization
|
|
# version id. To find the *upstream* ids we step back in the git history looking for an id
|
|
# not matching the project serialization version id prefix - this is assumed to be the latest
|
|
# serialization version from the upstream project.
|
|
if not bump_ser_ver_id:
|
|
info(' - Find upstream serialization version id for current release ...')
|
|
curr_upstream_id = find_upstream_ser_ver_id_in_history(curr_release_hash)
|
|
|
|
info(f' - Find base serialization version id ...')
|
|
base_hash = git_show_ref(options.release_base())
|
|
base_upstream_id = find_upstream_ser_ver_id_in_history(base_hash)
|
|
|
|
# Update serialization version id in release if serialization version id has changed
|
|
bump_ser_ver_id = curr_upstream_id != base_upstream_id
|
|
info(f' - The current upstream serialization.ver.id is {curr_upstream_id} '
|
|
f'and the base upstream id is {base_upstream_id}.')
|
|
|
|
state.current_ser_ver_id = curr_ser_ver_id
|
|
if bump_ser_ver_id:
|
|
state.new_ser_ver_id = bump_release_ser_ver_id(curr_ser_ver_id)
|
|
else:
|
|
state.new_ser_ver_id = curr_ser_ver_id
|
|
|
|
|
|
# Find the serialization-version-id for the upstream git project using the git log starting
|
|
# with the given revision (abort if not found in previous 20 commits)
|
|
def find_upstream_ser_ver_id_in_history(revision: str):
|
|
output = git('log', '-20', '--format=oneline', revision).stdout
|
|
for line in output.splitlines():
|
|
git_hash = line.split()[0]
|
|
ser_ver_id = read_ser_ver_id_from_pom_file(git_hash)
|
|
if not ser_ver_id.startswith(config.ser_ver_id_prefix):
|
|
return ser_ver_id
|
|
error(f'No ser.ver.id found for revision {revision} and previous 20 commits.')
|
|
|
|
|
|
def print_setup():
|
|
section('Configuration')
|
|
info(f'''
|
|
CLI Arguments
|
|
- Base revision for release ... : {options.base_revision}
|
|
|
|
CLI Options
|
|
- Bump ser.ver.id ............. : {options.bump_ser_ver_id}
|
|
- Dry run .................... : {options.dry_run}
|
|
- Debugging ................... : {options.debugging}
|
|
- Release ...................... : {options.releaseOnly}
|
|
|
|
Config
|
|
- Upstream git repo remote name : {config.upstream_remote}
|
|
- Release to remote git repo .. : {config.release_remote}
|
|
- Release branch .............. : {config.release_branch}
|
|
- Configuration branches ...... : {config.ext_branches}
|
|
- Ser.ver.id prefix ........... : {config.ser_ver_id_prefix}
|
|
''')
|
|
if config.include_prs_label:
|
|
info(f'PRs to merge')
|
|
for pr in state.pr_labels:
|
|
info(f' - {pr} with labels {state.pr_labels[pr]}')
|
|
info(f'''
|
|
Release info
|
|
- Project major version ....... : {state.major_version}
|
|
- Current version ............. : {state.current_version}
|
|
- New version ................. : {state.new_version}
|
|
- Current ser.ver.id .......... : {state.current_ser_ver_id}
|
|
- New ser.ver.id .............. : {state.new_ser_ver_id}
|
|
''')
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Resume ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
def read_script_state():
|
|
info('The script will resume and print the state of the previous process.')
|
|
global options, config, state
|
|
with open(STATE_FILE, 'r') as f:
|
|
doc = json.load(f)
|
|
options.__dict__ = doc['options']
|
|
config.__dict__ = doc['config']
|
|
state.__dict__ = doc['state']
|
|
state.gotoStep = True
|
|
|
|
|
|
def save_script_state(step):
|
|
state.step = step
|
|
doc = {'options': options.__dict__, 'config': config.__dict__, 'state': state.__dict__}
|
|
with open(STATE_FILE, 'w') as f:
|
|
json.dump(doc, f)
|
|
|
|
|
|
# Delete the script state file - this avoids going into resume the nest time the
|
|
# script is run.
|
|
def delete_script_state():
|
|
execute('rm', '-f', STATE_FILE)
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Utility functions ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
# Get the full git hash for a qualified branch name, tag or hash
|
|
def git_show_ref(ref):
|
|
if re.compile(r'[0-9a-f]{40}').match(ref):
|
|
return ref
|
|
output = execute('git', 'show-ref', ref).stdout
|
|
return output.split()[0]
|
|
|
|
|
|
# Create a tag name used in git for a given version
|
|
def git_tag(version):
|
|
return f'v{version}'
|
|
|
|
|
|
def bump_release_ser_ver_id(current_id):
|
|
# The id format can be either 'A-00053' or 'AA-0053'
|
|
if len(config.ser_ver_id_prefix) == 1:
|
|
ver_number = int(current_id[2:])
|
|
ser_format = '{:05d}'
|
|
else:
|
|
ver_number = int(current_id[3:])
|
|
ser_format = '{:04d}'
|
|
v = config.ser_ver_id_prefix + '-' + ser_format.format(ver_number + 1)
|
|
debug(f'New serialization version id: {v}')
|
|
return v
|
|
|
|
|
|
def read_major_version_from_pom(version_qualifier: str, revision: str):
|
|
pom_xml = execute('git', 'show', f'{revision}:{POM_FILE_NAME}').stdout
|
|
token_ptn = re.compile(r'<version>(.*)</version>')
|
|
ver_ptn = re.compile(r'(\d+\.\d+\.\d+)-(' + version_qualifier + r'-\d+|SNAPSHOT)')
|
|
|
|
# Find the first <version>... in the pom_file.
|
|
match = token_ptn.search(pom_xml)
|
|
if match:
|
|
debug(f'Version found: {match.group(1)}')
|
|
match = ver_ptn.match(match.group(1).strip())
|
|
if match:
|
|
debug('Main version number: {m.group(1)}')
|
|
return match.group(1)
|
|
error(f"Version not found in '{POM_FILE_NAME}'.")
|
|
|
|
|
|
def read_ser_ver_id_from_pom_file(git_hash):
|
|
pom_xml = execute('git', 'show', f'{git_hash}:{POM_FILE_NAME}').stdout
|
|
m = SER_VER_ID_PATTERN.search(pom_xml)
|
|
return m.group(1)
|
|
|
|
|
|
def run_maven_test():
|
|
if section_w_resume('run_maven_test', 'Run unit tests'):
|
|
mvn('clean', '-PprettierSkip', 'test')
|
|
|
|
|
|
def git(*cmd, error_msg=None):
|
|
return execute('git', *cmd, error_msg=error_msg)
|
|
|
|
|
|
# Git dru-run should be used if the command change or update the system, this command
|
|
# is NOT run if the '--dryRun' flag is set. Only some git commands has this flag, for example
|
|
# 'merge' and 'commit'.
|
|
def git_dr(*cmd, error_msg=None):
|
|
if options.dry_run:
|
|
return git(*cmd, '--dry-run', error_msg=error_msg)
|
|
else:
|
|
return execute('git', *cmd, error_msg=error_msg)
|
|
|
|
|
|
# Similar as 'git_dr()', but the command is just printed, not executed with the git '--dry-run'
|
|
# flag set. This is required if the git command do not support the '--dry-run' flag.
|
|
def git_im(*cmd, error_msg=None):
|
|
return execute('git', *cmd, error_msg=error_msg, impact=True)
|
|
|
|
|
|
# Run maven and pipe the output to sdtout and stderr
|
|
def mvn(*cmd):
|
|
cmd_line = ['mvn'] + list(cmd)
|
|
info(f'Run: {cmd_line}')
|
|
p = subprocess.run(cmd_line)
|
|
if p.returncode:
|
|
exit(p.returncode)
|
|
|
|
|
|
def execute(*cmd, quiet=True, quiet_err=False, error_msg=None, impact=False):
|
|
if options.dry_run and impact:
|
|
info(f'=> {cmd} (--dryRun SKIPPED)')
|
|
return
|
|
info(f'Run: {cmd}')
|
|
p = subprocess.run(args=list(cmd), capture_output=True, text=True, timeout=20)
|
|
|
|
if options.debugging:
|
|
if p.stdout:
|
|
debug(crop(p.stdout))
|
|
if p.stderr:
|
|
info(p.stderr)
|
|
debug(f' <= {p.returncode}')
|
|
else:
|
|
if p.stdout and (options.debugging or not quiet):
|
|
info(p.stdout)
|
|
if p.stderr and not quiet_err:
|
|
info(p.stderr)
|
|
if p.returncode:
|
|
if error_msg:
|
|
error(f'{error_msg} Command {cmd} failed.')
|
|
else:
|
|
error(f'Command {cmd} failed.')
|
|
return p
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Log functions ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
def section_w_resume(step: str, msg: str) -> bool:
|
|
if state.run(step):
|
|
save_script_state(step)
|
|
section(msg)
|
|
return True
|
|
else:
|
|
save_script_state(step)
|
|
section(f'{msg} (SKIP STEP)')
|
|
return False
|
|
|
|
|
|
def section(msg: str):
|
|
hr = '-------------------------------------------------------------------------------------------'
|
|
print('')
|
|
print(hr)
|
|
print(f' {msg}')
|
|
print(hr)
|
|
|
|
|
|
def info(msg):
|
|
print(f'{msg}', flush=True)
|
|
|
|
|
|
def error(msg):
|
|
print(f'\nERROR {msg}\n', flush=True)
|
|
exit(1)
|
|
|
|
|
|
def debug(msg):
|
|
if options.debugging:
|
|
print(f'DEBUG {msg}', flush=True)
|
|
|
|
|
|
def crop(text):
|
|
return text if len(text) <= 1600 else ("%s..." % (text[:1597]))
|
|
|
|
|
|
## ------------------------------------------------------------------------------------ ##
|
|
## Help function ##
|
|
## ------------------------------------------------------------------------------------ ##
|
|
|
|
def print_help():
|
|
section('Help')
|
|
print(f"""
|
|
This script is used to create a new release in a downstream fork of OTP. It will set both
|
|
the Maven version(2.7.0-entur-23) and the serialization-version-id(EN-0020) to a unique id
|
|
using the fork project name. For the script to work the git remote name must match the
|
|
GitHub 'owner' name.
|
|
|
|
Release process overview
|
|
1. The configured release-branch is reset hard to the <base-revision>.
|
|
2. Then the labeled PRs are merged into the release-branch, if configured.
|
|
3. The config-branches are merged into the release-branch.
|
|
4. The pom.xml file is updated with a new version and serialization version id.
|
|
5. The release is tested, tagged and pushed to Git repo.
|
|
|
|
See the RELEASE_README.md for more details.
|
|
|
|
Usage
|
|
script/custom-release.py [options] [<base-revision>]
|
|
|
|
Arguments
|
|
<base-revision> : The base branch or commit to use as the base for the release. The
|
|
'otp/dev-2.x' is the most common base branch to use, but you can
|
|
create a new release on top of any <commit>.
|
|
This parameter is required unless option --release is used.
|
|
|
|
Options
|
|
-h, --help : Print this help.
|
|
--debug : Run script with debug output enabled.
|
|
--dryRun : Run script locally, nothing is pushed to remote server.
|
|
--release : Create a new release from the current local Git repo HEAD. It updates the
|
|
maven-project-version and the serialization-version-id, creates a new tag
|
|
and push the release. You should apply all fixes and commit BEFORE running
|
|
this script. Can not be used with the <base-revision> argument set. The
|
|
'custom-release-extension' script is NOT run.
|
|
--serVerId : Force incrementation of the serialization version id.
|
|
--skipPRs : Skip PRs labeled with the configured 'include_prs_label'.
|
|
--summary : Print a markdown summary to '.custom_release_summary.md'
|
|
|
|
Examples
|
|
# script/custom-release.py otp/dev-2.x --printSummary
|
|
# script/custom-release.py 0715be88
|
|
# script/custom-release.py --dryRun --debug entur/my-feature-branch
|
|
# script/custom-release.py --release --serVerId
|
|
|
|
|
|
Failure
|
|
If a release fails, you may resume the release process after fixing the problem by running
|
|
the custom-release script again. The script will automatically detect that the script failed
|
|
and resume the release. Typical errors are merge conflicts and unit-test failures. Remember
|
|
to commit you changes after the problem is fixed, before running the script.
|
|
|
|
If you do not want to resume, and instead start over, delete the {STATE_FILE} and run the
|
|
script again. You also need to delete the tag, if the release was tagged - this is last step
|
|
before the release is pushed to the remote git repo.
|
|
""")
|
|
exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|