xref: /Lucene/dev-tools/scripts/releaseWizard.py (revision 8c48475c4d8eb3296cc2da7adbad4d8634b03116)
187c131baSJan Høydahl#!/usr/bin/env python3
287c131baSJan Høydahl# -*- coding: utf-8 -*-
387c131baSJan Høydahl# Licensed to the Apache Software Foundation (ASF) under one or more
487c131baSJan Høydahl# contributor license agreements.  See the NOTICE file distributed with
587c131baSJan Høydahl# this work for additional information regarding copyright ownership.
687c131baSJan Høydahl# The ASF licenses this file to You under the Apache License, Version 2.0
787c131baSJan Høydahl# (the "License"); you may not use this file except in compliance with
887c131baSJan Høydahl# the License.  You may obtain a copy of the License at
987c131baSJan Høydahl#
1087c131baSJan Høydahl#     http://www.apache.org/licenses/LICENSE-2.0
1187c131baSJan Høydahl#
1287c131baSJan Høydahl# Unless required by applicable law or agreed to in writing, software
1387c131baSJan Høydahl# distributed under the License is distributed on an "AS IS" BASIS,
1487c131baSJan Høydahl# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1587c131baSJan Høydahl# See the License for the specific language governing permissions and
1687c131baSJan Høydahl# limitations under the License.
1787c131baSJan Høydahl
18462c6c70SJan Høydahl# This script is the Release Manager's best friend, ensuring all details of a release are handled correctly.
19462c6c70SJan Høydahl# It will walk you through the steps of the release process, asking for decisions or input along the way.
20462c6c70SJan Høydahl# CAUTION: You still need to use your head! Please read the HELP section in the main menu.
2187c131baSJan Høydahl#
2287c131baSJan Høydahl# Requirements:
2387c131baSJan Høydahl#   Install requirements with this command:
2487c131baSJan Høydahl#   pip3 install -r requirements.txt
2587c131baSJan Høydahl#
2687c131baSJan Høydahl# Usage:
2787c131baSJan Høydahl#   releaseWizard.py [-h] [--dry-run] [--root PATH]
2887c131baSJan Høydahl#
2987c131baSJan Høydahl#   optional arguments:
3087c131baSJan Høydahl#   -h, --help   show this help message and exit
3187c131baSJan Høydahl#   --dry-run    Do not execute any commands, but echo them instead. Display
3287c131baSJan Høydahl#   extra debug info
3387c131baSJan Høydahl#   --root PATH  Specify different root folder than ~/.lucene-releases
3487c131baSJan Høydahl
3587c131baSJan Høydahlimport argparse
3687c131baSJan Høydahlimport copy
3787c131baSJan Høydahlimport fcntl
3887c131baSJan Høydahlimport json
3987c131baSJan Høydahlimport os
4087c131baSJan Høydahlimport platform
4187c131baSJan Høydahlimport re
4287c131baSJan Høydahlimport shlex
4387c131baSJan Høydahlimport shutil
4487c131baSJan Høydahlimport subprocess
4587c131baSJan Høydahlimport sys
4687c131baSJan Høydahlimport textwrap
4787c131baSJan Høydahlimport time
4887c131baSJan Høydahlimport urllib
4987c131baSJan Høydahlfrom collections import OrderedDict
5087c131baSJan Høydahlfrom datetime import datetime
5187c131baSJan Høydahlfrom datetime import timedelta
5287c131baSJan Høydahl
5387c131baSJan Høydahltry:
5487c131baSJan Høydahl    import holidays
5587c131baSJan Høydahl    import yaml
5687c131baSJan Høydahl    from ics import Calendar, Event
5787c131baSJan Høydahl    from jinja2 import Environment
5887c131baSJan Høydahlexcept:
5987c131baSJan Høydahl    print("You lack some of the module dependencies to run this script.")
6087c131baSJan Høydahl    print("Please run 'pip3 install -r requirements.txt' and try again.")
6187c131baSJan Høydahl    sys.exit(1)
6287c131baSJan Høydahl
6387c131baSJan Høydahlimport scriptutil
6487c131baSJan Høydahlfrom consolemenu import ConsoleMenu
6587c131baSJan Høydahlfrom consolemenu.items import FunctionItem, SubmenuItem, ExitItem
6687c131baSJan Høydahlfrom consolemenu.screen import Screen
673134f10aSMike Drobfrom scriptutil import BranchType, Version, download, run
6887c131baSJan Høydahl
69674b66ddSJan Høydahl# Lucene-to-Java version mapping
70*8c48475cSJan Høydahljava_versions = {6: 8, 7: 8, 8: 8, 9: 11, 10: 11}
71329e7c7bSJan Høydahleditor = None
7287c131baSJan Høydahl
7387c131baSJan Høydahl# Edit this to add other global jinja2 variables or filters
7487c131baSJan Høydahldef expand_jinja(text, vars=None):
7587c131baSJan Høydahl    global_vars = OrderedDict({
7687c131baSJan Høydahl        'script_version': state.script_version,
7787c131baSJan Høydahl        'release_version': state.release_version,
7887c131baSJan Høydahl        'release_version_underscore': state.release_version.replace('.', '_'),
7987c131baSJan Høydahl        'release_date': state.get_release_date(),
8087c131baSJan Høydahl        'ivy2_folder': os.path.expanduser("~/.ivy2/"),
8187c131baSJan Høydahl        'config_path': state.config_path,
8287c131baSJan Høydahl        'rc_number': state.rc_number,
8387c131baSJan Høydahl        'script_branch': state.script_branch,
8487c131baSJan Høydahl        'release_folder': state.get_release_folder(),
8587c131baSJan Høydahl        'git_checkout_folder': state.get_git_checkout_folder(),
86329e7c7bSJan Høydahl        'git_website_folder': state.get_website_git_folder(),
8787c131baSJan Høydahl        'dist_url_base': 'https://dist.apache.org/repos/dist/dev/lucene',
8887c131baSJan Høydahl        'm2_repository_url': 'https://repository.apache.org/service/local/staging/deploy/maven2',
8987c131baSJan Høydahl        'dist_file_path': state.get_dist_folder(),
9087c131baSJan Høydahl        'rc_folder': state.get_rc_folder(),
9187c131baSJan Høydahl        'base_branch': state.get_base_branch_name(),
9287c131baSJan Høydahl        'release_branch': state.release_branch,
9387c131baSJan Høydahl        'stable_branch': state.get_stable_branch_name(),
9487c131baSJan Høydahl        'minor_branch': state.get_minor_branch_name(),
9587c131baSJan Høydahl        'release_type': state.release_type,
9687c131baSJan Høydahl        'is_feature_release': state.release_type in ['minor', 'major'],
9787c131baSJan Høydahl        'release_version_major': state.release_version_major,
9887c131baSJan Høydahl        'release_version_minor': state.release_version_minor,
9987c131baSJan Høydahl        'release_version_bugfix': state.release_version_bugfix,
10087c131baSJan Høydahl        'state': state,
101daf14981SAlan Woodward        'gpg_key' : state.get_gpg_key(),
1023134f10aSMike Drob        'gradle_cmd' : 'gradlew.bat' if is_windows() else './gradlew',
10387c131baSJan Høydahl        'epoch': unix_time_millis(datetime.utcnow()),
10487c131baSJan Høydahl        'get_next_version': state.get_next_version(),
10587c131baSJan Høydahl        'current_git_rev': state.get_current_git_rev(),
10687c131baSJan Høydahl        'keys_downloaded': keys_downloaded(),
10787c131baSJan Høydahl        'editor': get_editor(),
10887c131baSJan Høydahl        'rename_cmd': 'ren' if is_windows() else 'mv',
10987c131baSJan Høydahl        'vote_close_72h': vote_close_72h_date().strftime("%Y-%m-%d %H:00 UTC"),
11087c131baSJan Høydahl        'vote_close_72h_epoch': unix_time_millis(vote_close_72h_date()),
111d86b473aSJan Høydahl        'vote_close_72h_holidays': vote_close_72h_holidays(),
11287c131baSJan Høydahl        'lucene_news_file': lucene_news_file,
11387c131baSJan Høydahl        'load_lines': load_lines,
11487c131baSJan Høydahl        'set_java_home': set_java_home,
11587c131baSJan Høydahl        'latest_version': state.get_latest_version(),
11687c131baSJan Høydahl        'latest_lts_version': state.get_latest_lts_version(),
11757524c6aSChristine Poerschke        'main_version': state.get_main_version(),
11887c131baSJan Høydahl        'mirrored_versions': state.get_mirrored_versions(),
11987c131baSJan Høydahl        'mirrored_versions_to_delete': state.get_mirrored_versions_to_delete(),
12087c131baSJan Høydahl        'home': os.path.expanduser("~")
12187c131baSJan Høydahl    })
12287c131baSJan Høydahl    global_vars.update(state.get_todo_states())
12387c131baSJan Høydahl    if vars:
12487c131baSJan Høydahl        global_vars.update(vars)
12587c131baSJan Høydahl
12687c131baSJan Høydahl    filled = replace_templates(text)
12787c131baSJan Høydahl
12887c131baSJan Høydahl    try:
12987c131baSJan Høydahl        env = Environment(lstrip_blocks=True, keep_trailing_newline=False, trim_blocks=True)
13087c131baSJan Høydahl        env.filters['path_join'] = lambda paths: os.path.join(*paths)
13187c131baSJan Høydahl        env.filters['expanduser'] = lambda path: os.path.expanduser(path)
13287c131baSJan Høydahl        env.filters['formatdate'] = lambda date: (datetime.strftime(date, "%-d %B %Y") if date else "<date>" )
13387c131baSJan Høydahl        template = env.from_string(str(filled), globals=global_vars)
13487c131baSJan Høydahl        filled = template.render()
13587c131baSJan Høydahl    except Exception as e:
13687c131baSJan Høydahl        print("Exception while rendering jinja template %s: %s" % (str(filled)[:10], e))
13787c131baSJan Høydahl    return filled
13887c131baSJan Høydahl
13987c131baSJan Høydahl
14087c131baSJan Høydahldef replace_templates(text):
14187c131baSJan Høydahl    tpl_lines = []
14287c131baSJan Høydahl    for line in text.splitlines():
14387c131baSJan Høydahl        if line.startswith("(( template="):
14487c131baSJan Høydahl            match = re.search(r"^\(\( template=(.+?) \)\)", line)
14587c131baSJan Høydahl            name = match.group(1)
14687c131baSJan Høydahl            tpl_lines.append(replace_templates(templates[name].strip()))
14787c131baSJan Høydahl        else:
14887c131baSJan Høydahl            tpl_lines.append(line)
14987c131baSJan Høydahl    return "\n".join(tpl_lines)
15087c131baSJan Høydahl
15187c131baSJan Høydahldef getScriptVersion():
15208e38d34SMike Drob    return scriptutil.find_current_version()
15387c131baSJan Høydahl
15487c131baSJan Høydahl
15587c131baSJan Høydahldef get_editor():
156329e7c7bSJan Høydahl    global editor
157329e7c7bSJan Høydahl    if editor is None:
158329e7c7bSJan Høydahl      if 'EDITOR' in os.environ:
159329e7c7bSJan Høydahl          if os.environ['EDITOR'] in ['vi', 'vim', 'nano', 'pico', 'emacs']:
160329e7c7bSJan Høydahl              print("WARNING: You have EDITOR set to %s, which will not work when launched from this tool. Please use an editor that launches a separate window/process" % os.environ['EDITOR'])
161329e7c7bSJan Høydahl          editor = os.environ['EDITOR']
162329e7c7bSJan Høydahl      elif is_windows():
163329e7c7bSJan Høydahl          editor = 'notepad.exe'
164329e7c7bSJan Høydahl      elif is_mac():
165329e7c7bSJan Høydahl          editor = 'open -a TextEdit'
166329e7c7bSJan Høydahl      else:
167329e7c7bSJan Høydahl          sys.exit("On Linux you have to set EDITOR variable to a command that will start an editor in its own window")
168329e7c7bSJan Høydahl    return editor
16987c131baSJan Høydahl
17087c131baSJan Høydahl
17187c131baSJan Høydahldef check_prerequisites(todo=None):
17287c131baSJan Høydahl    if sys.version_info < (3, 4):
17387c131baSJan Høydahl        sys.exit("Script requires Python v3.4 or later")
17487c131baSJan Høydahl    try:
17587c131baSJan Høydahl        gpg_ver = run("gpg --version").splitlines()[0]
17687c131baSJan Høydahl    except:
17787c131baSJan Høydahl        sys.exit("You will need gpg installed")
17826a32d79SAlan Woodward    if not 'GPG_TTY' in os.environ:
179674c2c28SMike Drob        print("WARNING: GPG_TTY environment variable is not set, GPG signing may not work correctly (try 'export GPG_TTY=$(tty)'")
1803134f10aSMike Drob    if not 'JAVA11_HOME' in os.environ:
1813134f10aSMike Drob        sys.exit("Please set environment variables JAVA11_HOME")
18287c131baSJan Høydahl    try:
18387c131baSJan Høydahl        asciidoc_ver = run("asciidoctor -V").splitlines()[0]
18487c131baSJan Høydahl    except:
18587c131baSJan Høydahl        asciidoc_ver = ""
18687c131baSJan Høydahl        print("WARNING: In order to export asciidoc version to HTML, you will need asciidoctor installed")
18787c131baSJan Høydahl    try:
18887c131baSJan Høydahl        git_ver = run("git --version").splitlines()[0]
18987c131baSJan Høydahl    except:
19087c131baSJan Høydahl        sys.exit("You will need git installed")
19187c131baSJan Høydahl    if not 'EDITOR' in os.environ:
19287c131baSJan Høydahl        print("WARNING: Environment variable $EDITOR not set, using %s" % get_editor())
19387c131baSJan Høydahl
19487c131baSJan Høydahl    if todo:
19587c131baSJan Høydahl        print("%s\n%s\n%s\n" % (gpg_ver, asciidoc_ver, git_ver))
19687c131baSJan Høydahl    return True
19787c131baSJan Høydahl
19887c131baSJan Høydahl
19987c131baSJan Høydahlepoch = datetime.utcfromtimestamp(0)
20087c131baSJan Høydahl
20187c131baSJan Høydahl
20287c131baSJan Høydahldef unix_time_millis(dt):
20387c131baSJan Høydahl    return int((dt - epoch).total_seconds() * 1000.0)
20487c131baSJan Høydahl
20587c131baSJan Høydahl
20687c131baSJan Høydahldef bootstrap_todos(todo_list):
20787c131baSJan Høydahl    # Establish links from commands to to_do for finding todo vars
20887c131baSJan Høydahl    for tg in todo_list:
20987c131baSJan Høydahl        if dry_run:
21087c131baSJan Høydahl            print("Group %s" % tg.id)
21187c131baSJan Høydahl        for td in tg.get_todos():
21287c131baSJan Høydahl            if dry_run:
21387c131baSJan Høydahl                print("  Todo %s" % td.id)
21487c131baSJan Høydahl            cmds = td.commands
21587c131baSJan Høydahl            if cmds:
21687c131baSJan Høydahl                if dry_run:
21787c131baSJan Høydahl                    print("  Commands")
21887c131baSJan Høydahl                cmds.todo_id = td.id
21987c131baSJan Høydahl                for cmd in cmds.commands:
22087c131baSJan Høydahl                    if dry_run:
22187c131baSJan Høydahl                        print("    Command %s" % cmd.cmd)
22287c131baSJan Høydahl                    cmd.todo_id = td.id
22387c131baSJan Høydahl
22487c131baSJan Høydahl    print("Loaded TODO definitions from releaseWizard.yaml")
22587c131baSJan Høydahl    return todo_list
22687c131baSJan Høydahl
22787c131baSJan Høydahl
22887c131baSJan Høydahldef maybe_remove_rc_from_svn():
22987c131baSJan Høydahl    todo = state.get_todo_by_id('import_svn')
23087c131baSJan Høydahl    if todo and todo.is_done():
23187c131baSJan Høydahl        print("import_svn done")
23287c131baSJan Høydahl        Commands(state.get_git_checkout_folder(),
23387c131baSJan Høydahl                 """Looks like you uploaded artifacts for {{ build_rc.git_rev | default("<git_rev>", True) }} to svn which needs to be removed.""",
23487c131baSJan Høydahl                 [Command(
235674b66ddSJan Høydahl                 """svn -m "Remove cancelled Lucene {{ release_version }} RC{{ rc_number }}" rm {{ dist_url }}""",
23687c131baSJan Høydahl                 logfile="svn_rm.log",
23787c131baSJan Høydahl                 tee=True,
23887c131baSJan Høydahl                 vars={
239674b66ddSJan Høydahl                     'dist_folder': """lucene-{{ release_version }}-RC{{ rc_number }}-rev{{ build_rc.git_rev | default("<git_rev>", True) }}""",
24087c131baSJan Høydahl                     'dist_url': "{{ dist_url_base }}/{{ dist_folder }}"
24187c131baSJan Høydahl                 }
24287c131baSJan Høydahl             )],
24387c131baSJan Høydahl                 enable_execute=True, confirm_each_command=False).run()
24487c131baSJan Høydahl
24587c131baSJan Høydahl
24687c131baSJan Høydahl# To be able to hide fields when dumping Yaml
24787c131baSJan Høydahlclass SecretYamlObject(yaml.YAMLObject):
24887c131baSJan Høydahl    hidden_fields = []
24987c131baSJan Høydahl    @classmethod
25087c131baSJan Høydahl    def to_yaml(cls,dumper,data):
25187c131baSJan Høydahl        print("Dumping object %s" % type(data))
25287c131baSJan Høydahl
25387c131baSJan Høydahl        new_data = copy.deepcopy(data)
25487c131baSJan Høydahl        for item in cls.hidden_fields:
25587c131baSJan Høydahl            if item in new_data.__dict__:
25687c131baSJan Høydahl                del new_data.__dict__[item]
25787c131baSJan Høydahl        for item in data.__dict__:
25887c131baSJan Høydahl            if item in new_data.__dict__ and new_data.__dict__[item] is None:
25987c131baSJan Høydahl                del new_data.__dict__[item]
26087c131baSJan Høydahl        return dumper.represent_yaml_object(cls.yaml_tag, new_data, cls,
26187c131baSJan Høydahl                                            flow_style=cls.yaml_flow_style)
26287c131baSJan Høydahl
26387c131baSJan Høydahl
26487c131baSJan Høydahldef str_presenter(dumper, data):
26587c131baSJan Høydahl    if len(data.split('\n')) > 1:  # check for multiline string
26687c131baSJan Høydahl        return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
26787c131baSJan Høydahl    return dumper.represent_scalar('tag:yaml.org,2002:str', data)
26887c131baSJan Høydahl
26987c131baSJan Høydahl
27087c131baSJan Høydahlclass ReleaseState:
27187c131baSJan Høydahl    def __init__(self, config_path, release_version, script_version):
27287c131baSJan Høydahl        self.script_version = script_version
27387c131baSJan Høydahl        self.config_path = config_path
27487c131baSJan Høydahl        self.todo_groups = None
27587c131baSJan Høydahl        self.todos = None
27687c131baSJan Høydahl        self.latest_version = None
27787c131baSJan Høydahl        self.previous_rcs = {}
27887c131baSJan Høydahl        self.rc_number = 1
27987c131baSJan Høydahl        self.start_date = unix_time_millis(datetime.utcnow())
28087c131baSJan Høydahl        self.script_branch = run("git rev-parse --abbrev-ref HEAD").strip()
28187c131baSJan Høydahl        self.mirrored_versions = None
28287c131baSJan Høydahl        try:
28387c131baSJan Høydahl            self.script_branch_type = scriptutil.find_branch_type()
28487c131baSJan Høydahl        except:
28587c131baSJan Høydahl            print("WARNING: This script shold (ideally) run from the release branch, not a feature branch (%s)" % self.script_branch)
28687c131baSJan Høydahl            self.script_branch_type = 'feature'
28787c131baSJan Høydahl        self.set_release_version(release_version)
28887c131baSJan Høydahl
28987c131baSJan Høydahl    def set_release_version(self, version):
29087c131baSJan Høydahl        self.validate_release_version(self.script_branch_type, self.script_branch, version)
29187c131baSJan Høydahl        self.release_version = version
29287c131baSJan Høydahl        v = Version.parse(version)
29387c131baSJan Høydahl        self.release_version_major = v.major
29487c131baSJan Høydahl        self.release_version_minor = v.minor
29587c131baSJan Høydahl        self.release_version_bugfix = v.bugfix
29687c131baSJan Høydahl        self.release_branch = "branch_%s_%s" % (v.major, v.minor)
29787c131baSJan Høydahl        if v.is_major_release():
29887c131baSJan Høydahl            self.release_type = 'major'
29987c131baSJan Høydahl        elif v.is_minor_release():
30087c131baSJan Høydahl            self.release_type = 'minor'
30187c131baSJan Høydahl        else:
30287c131baSJan Høydahl            self.release_type = 'bugfix'
30387c131baSJan Høydahl
30487c131baSJan Høydahl    def is_released(self):
30587c131baSJan Høydahl        return self.get_todo_by_id('announce_lucene').is_done()
30687c131baSJan Høydahl
307daf14981SAlan Woodward    def get_gpg_key(self):
308daf14981SAlan Woodward        gpg_task = self.get_todo_by_id('gpg')
309daf14981SAlan Woodward        if gpg_task.is_done():
310daf14981SAlan Woodward            return gpg_task.get_state()['gpg_key']
311daf14981SAlan Woodward        else:
312daf14981SAlan Woodward            return None
313daf14981SAlan Woodward
31487c131baSJan Høydahl    def get_release_date(self):
31587c131baSJan Høydahl        publish_task = self.get_todo_by_id('publish_maven')
31687c131baSJan Høydahl        if publish_task.is_done():
31787c131baSJan Høydahl            return unix_to_datetime(publish_task.get_state()['done_date'])
31887c131baSJan Høydahl        else:
31987c131baSJan Høydahl            return None
32087c131baSJan Høydahl
321329e7c7bSJan Høydahl    def get_release_date_iso(self):
322329e7c7bSJan Høydahl        release_date = self.get_release_date()
323329e7c7bSJan Høydahl        if release_date is None:
324329e7c7bSJan Høydahl            return "yyyy-mm-dd"
325329e7c7bSJan Høydahl        else:
326329e7c7bSJan Høydahl            return release_date.isoformat()[:10]
327329e7c7bSJan Høydahl
32887c131baSJan Høydahl    def get_latest_version(self):
32987c131baSJan Høydahl        if self.latest_version is None:
33087c131baSJan Høydahl            versions = self.get_mirrored_versions()
33187c131baSJan Høydahl            latest = versions[0]
33287c131baSJan Høydahl            for ver in versions:
33387c131baSJan Høydahl                if Version.parse(ver).gt(Version.parse(latest)):
33487c131baSJan Høydahl                    latest = ver
33587c131baSJan Høydahl            self.latest_version = latest
33687c131baSJan Høydahl            self.save()
33787c131baSJan Høydahl        return state.latest_version
33887c131baSJan Høydahl
33987c131baSJan Høydahl    def get_mirrored_versions(self):
34087c131baSJan Høydahl        if state.mirrored_versions is None:
34187c131baSJan Høydahl            releases_str = load("https://projects.apache.org/json/foundation/releases.json", "utf-8")
34287c131baSJan Høydahl            releases = json.loads(releases_str)['lucene']
34387c131baSJan Høydahl            state.mirrored_versions = [ r for r in list(map(lambda y: y[7:], filter(lambda x: x.startswith('lucene-'), list(releases.keys())))) ]
34487c131baSJan Høydahl        return state.mirrored_versions
34587c131baSJan Høydahl
34687c131baSJan Høydahl    def get_mirrored_versions_to_delete(self):
34787c131baSJan Høydahl        versions = self.get_mirrored_versions()
34887c131baSJan Høydahl        to_keep = versions
34987c131baSJan Høydahl        if state.release_type == 'major':
35087c131baSJan Høydahl          to_keep = [self.release_version, self.get_latest_version()]
35187c131baSJan Høydahl        if state.release_type == 'minor':
35287c131baSJan Høydahl          to_keep = [self.release_version, self.get_latest_lts_version()]
35387c131baSJan Høydahl        if state.release_type == 'bugfix':
35487c131baSJan Høydahl          if Version.parse(state.release_version).major == Version.parse(state.get_latest_version()).major:
35587c131baSJan Høydahl            to_keep = [self.release_version, self.get_latest_lts_version()]
35687c131baSJan Høydahl          elif Version.parse(state.release_version).major == Version.parse(state.get_latest_lts_version()).major:
357329e7c7bSJan Høydahl            to_keep = [self.get_latest_version(), self.release_version]
35887c131baSJan Høydahl          else:
35987c131baSJan Høydahl            raise Exception("Release version %s must have same major version as current minor or lts release")
36087c131baSJan Høydahl        return [ver for ver in versions if ver not in to_keep]
36187c131baSJan Høydahl
36257524c6aSChristine Poerschke    def get_main_version(self):
36387c131baSJan Høydahl        v = Version.parse(self.get_latest_version())
36487c131baSJan Høydahl        return "%s.%s.%s" % (v.major + 1, 0, 0)
36587c131baSJan Høydahl
36687c131baSJan Høydahl    def get_latest_lts_version(self):
36787c131baSJan Høydahl        versions = self.get_mirrored_versions()
36887c131baSJan Høydahl        latest = self.get_latest_version()
36987c131baSJan Høydahl        lts_prefix = "%s." % (Version.parse(latest).major - 1)
37087c131baSJan Høydahl        lts_versions = list(filter(lambda x: x.startswith(lts_prefix), versions))
37187c131baSJan Høydahl        latest_lts = lts_versions[0]
37287c131baSJan Høydahl        for ver in lts_versions:
37387c131baSJan Høydahl            if Version.parse(ver).gt(Version.parse(latest_lts)):
37487c131baSJan Høydahl                latest_lts = ver
37587c131baSJan Høydahl        return latest_lts
37687c131baSJan Høydahl
37787c131baSJan Høydahl    def validate_release_version(self, branch_type, branch, release_version):
37887c131baSJan Høydahl        ver = Version.parse(release_version)
37987c131baSJan Høydahl        # print("release_version=%s, ver=%s" % (release_version, ver))
38087c131baSJan Høydahl        if branch_type == BranchType.release:
38187c131baSJan Høydahl            if not branch.startswith('branch_'):
38287c131baSJan Høydahl                sys.exit("Incompatible branch and branch_type")
38387c131baSJan Høydahl            if not ver.is_bugfix_release():
38487c131baSJan Høydahl                sys.exit("You can only release bugfix releases from an existing release branch")
38587c131baSJan Høydahl        elif branch_type == BranchType.stable:
38687c131baSJan Høydahl            if not branch.startswith('branch_') and branch.endswith('x'):
38787c131baSJan Høydahl                sys.exit("Incompatible branch and branch_type")
38887c131baSJan Høydahl            if not ver.is_minor_release():
38987c131baSJan Høydahl                sys.exit("You can only release minor releases from an existing stable branch")
39087c131baSJan Høydahl        elif branch_type == BranchType.unstable:
39157524c6aSChristine Poerschke            if not branch == 'main':
39287c131baSJan Høydahl                sys.exit("Incompatible branch and branch_type")
39387c131baSJan Høydahl            if not ver.is_major_release():
39457524c6aSChristine Poerschke                sys.exit("You can only release a new major version from main branch")
39587c131baSJan Høydahl        if not getScriptVersion() == release_version:
39687c131baSJan Høydahl            print("WARNING: Expected release version %s when on branch %s, but got %s" % (
39787c131baSJan Høydahl                getScriptVersion(), branch, release_version))
39887c131baSJan Høydahl
39987c131baSJan Høydahl    def get_base_branch_name(self):
40087c131baSJan Høydahl        v = Version.parse(self.release_version)
40187c131baSJan Høydahl        if v.is_major_release():
40257524c6aSChristine Poerschke            return 'main'
40387c131baSJan Høydahl        elif v.is_minor_release():
40487c131baSJan Høydahl            return self.get_stable_branch_name()
40587c131baSJan Høydahl        elif v.major == Version.parse(self.get_latest_version()).major:
40687c131baSJan Høydahl            return self.get_minor_branch_name()
40787c131baSJan Høydahl        else:
40887c131baSJan Høydahl            return self.release_branch
40987c131baSJan Høydahl
41087c131baSJan Høydahl    def clear_rc(self):
41187c131baSJan Høydahl        if ask_yes_no("Are you sure? This will clear and restart RC%s" % self.rc_number):
41287c131baSJan Høydahl            maybe_remove_rc_from_svn()
41387c131baSJan Høydahl            dict = {}
41487c131baSJan Høydahl            for g in list(filter(lambda x: x.in_rc_loop(), self.todo_groups)):
41587c131baSJan Høydahl                for t in g.get_todos():
41687c131baSJan Høydahl                    t.clear()
41787c131baSJan Høydahl            print("Cleared RC TODO state")
41887c131baSJan Høydahl            try:
41987c131baSJan Høydahl                shutil.rmtree(self.get_rc_folder())
42087c131baSJan Høydahl                print("Cleared folder %s" % self.get_rc_folder())
42187c131baSJan Høydahl            except Exception as e:
42287c131baSJan Høydahl                print("WARN: Failed to clear %s, please do it manually with higher privileges" % self.get_rc_folder())
42387c131baSJan Høydahl            self.save()
42487c131baSJan Høydahl
42587c131baSJan Høydahl    def new_rc(self):
42687c131baSJan Høydahl        if ask_yes_no("Are you sure? This will abort current RC"):
42787c131baSJan Høydahl            maybe_remove_rc_from_svn()
42887c131baSJan Høydahl            dict = {}
42987c131baSJan Høydahl            for g in list(filter(lambda x: x.in_rc_loop(), self.todo_groups)):
43087c131baSJan Høydahl                for t in g.get_todos():
43187c131baSJan Høydahl                    if t.applies(self.release_type):
43287c131baSJan Høydahl                        dict[t.id] = copy.deepcopy(t.state)
43387c131baSJan Høydahl                        t.clear()
43487c131baSJan Høydahl            self.previous_rcs["RC%d" % self.rc_number] = dict
43587c131baSJan Høydahl            self.rc_number += 1
43687c131baSJan Høydahl            self.save()
43787c131baSJan Høydahl
43887c131baSJan Høydahl    def to_dict(self):
43987c131baSJan Høydahl        tmp_todos = {}
44087c131baSJan Høydahl        for todo_id in self.todos:
44187c131baSJan Høydahl            t = self.todos[todo_id]
44287c131baSJan Høydahl            tmp_todos[todo_id] = copy.deepcopy(t.state)
44387c131baSJan Høydahl        dict = {
44487c131baSJan Høydahl            'script_version': self.script_version,
44587c131baSJan Høydahl            'release_version': self.release_version,
44687c131baSJan Høydahl            'start_date': self.start_date,
44787c131baSJan Høydahl            'rc_number': self.rc_number,
44887c131baSJan Høydahl            'script_branch': self.script_branch,
44987c131baSJan Høydahl            'todos': tmp_todos,
45087c131baSJan Høydahl            'previous_rcs': self.previous_rcs
45187c131baSJan Høydahl        }
45287c131baSJan Høydahl        if self.latest_version:
45387c131baSJan Høydahl            dict['latest_version'] = self.latest_version
45487c131baSJan Høydahl        return dict
45587c131baSJan Høydahl
45687c131baSJan Høydahl    def restore_from_dict(self, dict):
45787c131baSJan Høydahl        self.script_version = dict['script_version']
45887c131baSJan Høydahl        assert dict['release_version'] == self.release_version
45987c131baSJan Høydahl        if 'start_date' in dict:
46087c131baSJan Høydahl            self.start_date = dict['start_date']
46187c131baSJan Høydahl        if 'latest_version' in dict:
46287c131baSJan Høydahl            self.latest_version = dict['latest_version']
46387c131baSJan Høydahl        else:
46487c131baSJan Høydahl            self.latest_version = None
46587c131baSJan Høydahl        self.rc_number = dict['rc_number']
46687c131baSJan Høydahl        self.script_branch = dict['script_branch']
46787c131baSJan Høydahl        self.previous_rcs = copy.deepcopy(dict['previous_rcs'])
46887c131baSJan Høydahl        for todo_id in dict['todos']:
46987c131baSJan Høydahl            if todo_id in self.todos:
47087c131baSJan Høydahl                t = self.todos[todo_id]
47187c131baSJan Høydahl                for k in dict['todos'][todo_id]:
47287c131baSJan Høydahl                    t.state[k] = dict['todos'][todo_id][k]
47387c131baSJan Høydahl            else:
47487c131baSJan Høydahl                print("Warning: Could not restore state for %s, Todo definition not found" % todo_id)
47587c131baSJan Høydahl
47687c131baSJan Høydahl    def load(self):
47787c131baSJan Høydahl        if os.path.exists(os.path.join(self.config_path, self.release_version, 'state.yaml')):
47887c131baSJan Høydahl            state_file = os.path.join(self.config_path, self.release_version, 'state.yaml')
47987c131baSJan Høydahl            with open(state_file, 'r') as fp:
48087c131baSJan Høydahl                try:
48187c131baSJan Høydahl                    dict = yaml.load(fp, Loader=yaml.Loader)
48287c131baSJan Høydahl                    self.restore_from_dict(dict)
48387c131baSJan Høydahl                    print("Loaded state from %s" % state_file)
48487c131baSJan Høydahl                except Exception as e:
48587c131baSJan Høydahl                    print("Failed to load state from %s: %s" % (state_file, e))
48687c131baSJan Høydahl
48787c131baSJan Høydahl    def save(self):
48887c131baSJan Høydahl        print("Saving")
48987c131baSJan Høydahl        if not os.path.exists(os.path.join(self.config_path, self.release_version)):
49087c131baSJan Høydahl            print("Creating folder %s" % os.path.join(self.config_path, self.release_version))
49187c131baSJan Høydahl            os.makedirs(os.path.join(self.config_path, self.release_version))
49287c131baSJan Høydahl
49387c131baSJan Høydahl        with open(os.path.join(self.config_path, self.release_version, 'state.yaml'), 'w') as fp:
49487c131baSJan Høydahl            yaml.dump(self.to_dict(), fp, sort_keys=False, default_flow_style=False)
49587c131baSJan Høydahl
49687c131baSJan Høydahl    def clear(self):
49787c131baSJan Høydahl        self.previous_rcs = {}
49887c131baSJan Høydahl        self.rc_number = 1
49987c131baSJan Høydahl        for t_id in self.todos:
50087c131baSJan Høydahl            t = self.todos[t_id]
50187c131baSJan Høydahl            t.state = {}
50287c131baSJan Høydahl        self.save()
50387c131baSJan Høydahl
50487c131baSJan Høydahl    def get_rc_number(self):
50587c131baSJan Høydahl        return self.rc_number
50687c131baSJan Høydahl
50787c131baSJan Høydahl    def get_current_git_rev(self):
50887c131baSJan Høydahl        try:
50987c131baSJan Høydahl            return run("git rev-parse HEAD", cwd=self.get_git_checkout_folder()).strip()
51087c131baSJan Høydahl        except:
51187c131baSJan Høydahl            return "<git-rev>"
51287c131baSJan Høydahl
51387c131baSJan Høydahl    def get_group_by_id(self, id):
51487c131baSJan Høydahl        lst = list(filter(lambda x: x.id == id, self.todo_groups))
51587c131baSJan Høydahl        if len(lst) == 1:
51687c131baSJan Høydahl            return lst[0]
51787c131baSJan Høydahl        else:
51887c131baSJan Høydahl            return None
51987c131baSJan Høydahl
52087c131baSJan Høydahl    def get_todo_by_id(self, id):
52187c131baSJan Høydahl        lst = list(filter(lambda x: x.id == id, self.todos.values()))
52287c131baSJan Høydahl        if len(lst) == 1:
52387c131baSJan Høydahl            return lst[0]
52487c131baSJan Høydahl        else:
52587c131baSJan Høydahl            return None
52687c131baSJan Høydahl
52787c131baSJan Høydahl    def get_todo_state_by_id(self, id):
52887c131baSJan Høydahl        lst = list(filter(lambda x: x.id == id, self.todos.values()))
52987c131baSJan Høydahl        if len(lst) == 1:
53087c131baSJan Høydahl            return lst[0].state
53187c131baSJan Høydahl        else:
53287c131baSJan Høydahl            return {}
53387c131baSJan Høydahl
53487c131baSJan Høydahl    def get_release_folder(self):
53587c131baSJan Høydahl        folder = os.path.join(self.config_path, self.release_version)
53687c131baSJan Høydahl        if not os.path.exists(folder):
53787c131baSJan Høydahl            print("Creating folder %s" % folder)
53887c131baSJan Høydahl            os.makedirs(folder)
53987c131baSJan Høydahl        return folder
54087c131baSJan Høydahl
54187c131baSJan Høydahl    def get_rc_folder(self):
54287c131baSJan Høydahl        folder = os.path.join(self.get_release_folder(), "RC%d" % self.rc_number)
54387c131baSJan Høydahl        if not os.path.exists(folder):
54487c131baSJan Høydahl            print("Creating folder %s" % folder)
54587c131baSJan Høydahl            os.makedirs(folder)
54687c131baSJan Høydahl        return folder
54787c131baSJan Høydahl
54887c131baSJan Høydahl    def get_dist_folder(self):
54987c131baSJan Høydahl        folder = os.path.join(self.get_rc_folder(), "dist")
55087c131baSJan Høydahl        return folder
55187c131baSJan Høydahl
55287c131baSJan Høydahl    def get_git_checkout_folder(self):
553674b66ddSJan Høydahl        folder = os.path.join(self.get_release_folder(), "lucene")
55487c131baSJan Høydahl        return folder
55587c131baSJan Høydahl
556329e7c7bSJan Høydahl    def get_website_git_folder(self):
557329e7c7bSJan Høydahl        folder = os.path.join(self.get_release_folder(), "lucene-site")
558329e7c7bSJan Høydahl        return folder
559329e7c7bSJan Høydahl
56087c131baSJan Høydahl    def get_minor_branch_name(self):
56187c131baSJan Høydahl        latest = state.get_latest_version()
56287c131baSJan Høydahl        if latest is not None:
56387c131baSJan Høydahl          v = Version.parse(latest)
56487c131baSJan Høydahl          return "branch_%s_%s" % (v.major, v.minor)
56587c131baSJan Høydahl        else:
56687c131baSJan Høydahl            raise Exception("Cannot find latest version")
56787c131baSJan Høydahl
56887c131baSJan Høydahl    def get_stable_branch_name(self):
5693134f10aSMike Drob        if self.release_type == 'major':
57057524c6aSChristine Poerschke            v = Version.parse(self.get_main_version())
5713134f10aSMike Drob        else:
57287c131baSJan Høydahl            v = Version.parse(self.get_latest_version())
57387c131baSJan Høydahl        return "branch_%sx" % v.major
57487c131baSJan Høydahl
57587c131baSJan Høydahl    def get_next_version(self):
57687c131baSJan Høydahl        if self.release_type == 'major':
5773134f10aSMike Drob            return "%s.0.0" % (self.release_version_major + 1)
57887c131baSJan Høydahl        if self.release_type == 'minor':
5793134f10aSMike Drob            return "%s.%s.0" % (self.release_version_major, self.release_version_minor + 1)
58087c131baSJan Høydahl        if self.release_type == 'bugfix':
58187c131baSJan Høydahl            return "%s.%s.%s" % (self.release_version_major, self.release_version_minor, self.release_version_bugfix + 1)
58287c131baSJan Høydahl
58387c131baSJan Høydahl    def get_java_home(self):
58487c131baSJan Høydahl        return self.get_java_home_for_version(self.release_version)
58587c131baSJan Høydahl
58687c131baSJan Høydahl    def get_java_home_for_version(self, version):
58787c131baSJan Høydahl        v = Version.parse(version)
58887c131baSJan Høydahl        java_ver = java_versions[v.major]
58987c131baSJan Høydahl        java_home_var = "JAVA%s_HOME" % java_ver
59087c131baSJan Høydahl        if java_home_var in os.environ:
59187c131baSJan Høydahl            return os.environ.get(java_home_var)
59287c131baSJan Høydahl        else:
59387c131baSJan Høydahl            raise Exception("Script needs environment variable %s" % java_home_var )
59487c131baSJan Høydahl
59587c131baSJan Høydahl    def get_java_cmd_for_version(self, version):
59687c131baSJan Høydahl        return os.path.join(self.get_java_home_for_version(version), "bin", "java")
59787c131baSJan Høydahl
59887c131baSJan Høydahl    def get_java_cmd(self):
59987c131baSJan Høydahl        return os.path.join(self.get_java_home(), "bin", "java")
60087c131baSJan Høydahl
60187c131baSJan Høydahl    def get_todo_states(self):
60287c131baSJan Høydahl        states = {}
60387c131baSJan Høydahl        if self.todos:
60487c131baSJan Høydahl            for todo_id in self.todos:
60587c131baSJan Høydahl                t = self.todos[todo_id]
60687c131baSJan Høydahl                states[todo_id] = copy.deepcopy(t.state)
60787c131baSJan Høydahl        return states
60887c131baSJan Høydahl
60987c131baSJan Høydahl    def init_todos(self, groups):
61087c131baSJan Høydahl        self.todo_groups = groups
61187c131baSJan Høydahl        self.todos = {}
61287c131baSJan Høydahl        for g in self.todo_groups:
61387c131baSJan Høydahl            for t in g.get_todos():
61487c131baSJan Høydahl                self.todos[t.id] = t
61587c131baSJan Høydahl
61687c131baSJan Høydahl
61787c131baSJan Høydahlclass TodoGroup(SecretYamlObject):
61887c131baSJan Høydahl    yaml_tag = u'!TodoGroup'
61987c131baSJan Høydahl    hidden_fields = []
62087c131baSJan Høydahl    def __init__(self, id, title, description, todos, is_in_rc_loop=None, depends=None):
62187c131baSJan Høydahl        self.id = id
62287c131baSJan Høydahl        self.title = title
62387c131baSJan Høydahl        self.description = description
62487c131baSJan Høydahl        self.depends = depends
62587c131baSJan Høydahl        self.is_in_rc_loop = is_in_rc_loop
62687c131baSJan Høydahl        self.todos = todos
62787c131baSJan Høydahl
62887c131baSJan Høydahl    @classmethod
62987c131baSJan Høydahl    def from_yaml(cls, loader, node):
63087c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
63187c131baSJan Høydahl        return TodoGroup(**fields)
63287c131baSJan Høydahl
63387c131baSJan Høydahl    def num_done(self):
63487c131baSJan Høydahl        return sum(1 for x in self.todos if x.is_done() > 0)
63587c131baSJan Høydahl
63687c131baSJan Høydahl    def num_applies(self):
63787c131baSJan Høydahl        count = sum(1 for x in self.todos if x.applies(state.release_type))
63887c131baSJan Høydahl        # print("num_applies=%s" % count)
63987c131baSJan Høydahl        return count
64087c131baSJan Høydahl
64187c131baSJan Høydahl    def is_done(self):
64287c131baSJan Høydahl        # print("Done=%s, applies=%s" % (self.num_done(), self.num_applies()))
64387c131baSJan Høydahl        return self.num_done() >= self.num_applies()
64487c131baSJan Høydahl
64587c131baSJan Høydahl    def get_title(self):
64687c131baSJan Høydahl        # print("get_title: %s" % self.is_done())
64787c131baSJan Høydahl        prefix = ""
64887c131baSJan Høydahl        if self.is_done():
64987c131baSJan Høydahl            prefix = "✓ "
65087c131baSJan Høydahl        return "%s%s (%d/%d)" % (prefix, self.title, self.num_done(), self.num_applies())
65187c131baSJan Høydahl
65287c131baSJan Høydahl    def get_submenu(self):
65387c131baSJan Høydahl        menu = UpdatableConsoleMenu(title=self.title, subtitle=self.get_subtitle, prologue_text=self.get_description(),
65487c131baSJan Høydahl                           screen=MyScreen())
65587c131baSJan Høydahl        menu.exit_item = CustomExitItem("Return")
65687c131baSJan Høydahl        for todo in self.get_todos():
65787c131baSJan Høydahl            if todo.applies(state.release_type):
65887c131baSJan Høydahl                menu.append_item(todo.get_menu_item())
65987c131baSJan Høydahl        return menu
66087c131baSJan Høydahl
66187c131baSJan Høydahl    def get_menu_item(self):
66287c131baSJan Høydahl        item = UpdatableSubmenuItem(self.get_title, self.get_submenu())
66387c131baSJan Høydahl        return item
66487c131baSJan Høydahl
66587c131baSJan Høydahl    def get_todos(self):
66687c131baSJan Høydahl        return self.todos
66787c131baSJan Høydahl
66887c131baSJan Høydahl    def in_rc_loop(self):
66987c131baSJan Høydahl        return self.is_in_rc_loop is True
67087c131baSJan Høydahl
67187c131baSJan Høydahl    def get_description(self):
67287c131baSJan Høydahl        desc = self.description
67387c131baSJan Høydahl        if desc:
67487c131baSJan Høydahl            return expand_jinja(desc)
67587c131baSJan Høydahl        else:
67687c131baSJan Høydahl            return None
67787c131baSJan Høydahl
67887c131baSJan Høydahl    def get_subtitle(self):
67987c131baSJan Høydahl        if self.depends:
68087c131baSJan Høydahl            ret_str = ""
68187c131baSJan Høydahl            for dep in ensure_list(self.depends):
68287c131baSJan Høydahl                g = state.get_group_by_id(dep)
68387c131baSJan Høydahl                if not g:
68487c131baSJan Høydahl                    g = state.get_todo_by_id(dep)
68587c131baSJan Høydahl                if g and not g.is_done():
68687c131baSJan Høydahl                    ret_str += "NOTE: Please first complete '%s'\n" % g.title
68787c131baSJan Høydahl                    return ret_str.strip()
68887c131baSJan Høydahl        return None
68987c131baSJan Høydahl
69087c131baSJan Høydahl
69187c131baSJan Høydahlclass Todo(SecretYamlObject):
69287c131baSJan Høydahl    yaml_tag = u'!Todo'
69387c131baSJan Høydahl    hidden_fields = ['state']
69487c131baSJan Høydahl    def __init__(self, id, title, description=None, post_description=None, done=None, types=None, links=None,
69587c131baSJan Høydahl                 commands=None, user_input=None, depends=None, vars=None, asciidoc=None, persist_vars=None,
69687c131baSJan Høydahl                 function=None):
69787c131baSJan Høydahl        self.id = id
69887c131baSJan Høydahl        self.title = title
69987c131baSJan Høydahl        self.description = description
70087c131baSJan Høydahl        self.asciidoc = asciidoc
70187c131baSJan Høydahl        self.types = types
70287c131baSJan Høydahl        self.depends = depends
70387c131baSJan Høydahl        self.vars = vars
70487c131baSJan Høydahl        self.persist_vars = persist_vars
70587c131baSJan Høydahl        self.function = function
70687c131baSJan Høydahl        self.user_input = user_input
70787c131baSJan Høydahl        self.commands = commands
70887c131baSJan Høydahl        self.post_description = post_description
70987c131baSJan Høydahl        self.links = links
71087c131baSJan Høydahl        self.state = {}
71187c131baSJan Høydahl
71287c131baSJan Høydahl        self.set_done(done)
71387c131baSJan Høydahl        if self.types:
71487c131baSJan Høydahl            self.types = ensure_list(self.types)
71587c131baSJan Høydahl            for t in self.types:
71687c131baSJan Høydahl                if not t in ['minor', 'major', 'bugfix']:
71787c131baSJan Høydahl                    sys.exit("Wrong Todo config for '%s'. Type needs to be either 'minor', 'major' or 'bugfix'" % self.id)
71887c131baSJan Høydahl        if commands:
71987c131baSJan Høydahl            self.commands.todo_id = self.id
72087c131baSJan Høydahl            for c in commands.commands:
72187c131baSJan Høydahl                c.todo_id = self.id
72287c131baSJan Høydahl
72387c131baSJan Høydahl    @classmethod
72487c131baSJan Høydahl    def from_yaml(cls, loader, node):
72587c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
72687c131baSJan Høydahl        return Todo(**fields)
72787c131baSJan Høydahl
72887c131baSJan Høydahl    def get_vars(self):
72987c131baSJan Høydahl        myvars = {}
73087c131baSJan Høydahl        if self.vars:
73187c131baSJan Høydahl            for k in self.vars:
73287c131baSJan Høydahl                val = self.vars[k]
73387c131baSJan Høydahl                if callable(val):
73487c131baSJan Høydahl                    myvars[k] = expand_jinja(val(), vars=myvars)
73587c131baSJan Høydahl                else:
73687c131baSJan Høydahl                    myvars[k] = expand_jinja(val, vars=myvars)
73787c131baSJan Høydahl        return myvars
73887c131baSJan Høydahl
73987c131baSJan Høydahl    def set_done(self, is_done):
74087c131baSJan Høydahl        if is_done:
74187c131baSJan Høydahl            self.state['done_date'] = unix_time_millis(datetime.utcnow())
74287c131baSJan Høydahl            if self.persist_vars:
74387c131baSJan Høydahl                for k in self.persist_vars:
74487c131baSJan Høydahl                    self.state[k] = self.get_vars()[k]
74587c131baSJan Høydahl        else:
74687c131baSJan Høydahl            self.state.clear()
74787c131baSJan Høydahl        self.state['done'] = is_done
74887c131baSJan Høydahl
74987c131baSJan Høydahl    def applies(self, type):
75087c131baSJan Høydahl        if self.types:
75187c131baSJan Høydahl            return type in self.types
75287c131baSJan Høydahl        return True
75387c131baSJan Høydahl
75487c131baSJan Høydahl    def is_done(self):
75587c131baSJan Høydahl        return 'done' in self.state and self.state['done'] is True
75687c131baSJan Høydahl
75787c131baSJan Høydahl    def get_title(self):
75887c131baSJan Høydahl        prefix = ""
75987c131baSJan Høydahl        if self.is_done():
76087c131baSJan Høydahl            prefix = "✓ "
76187c131baSJan Høydahl        return expand_jinja("%s%s" % (prefix, self.title), self.get_vars_and_state())
76287c131baSJan Høydahl
76387c131baSJan Høydahl    def display_and_confirm(self):
76487c131baSJan Høydahl        try:
76587c131baSJan Høydahl            if self.depends:
76687c131baSJan Høydahl                ret_str = ""
76787c131baSJan Høydahl                for dep in ensure_list(self.depends):
76887c131baSJan Høydahl                    g = state.get_group_by_id(dep)
76987c131baSJan Høydahl                    if not g:
77087c131baSJan Høydahl                        g = state.get_todo_by_id(dep)
77187c131baSJan Høydahl                    if not g.is_done():
77287c131baSJan Høydahl                        print("This step depends on '%s'. Please complete that first\n" % g.title)
77387c131baSJan Høydahl                        return
77487c131baSJan Høydahl            desc = self.get_description()
77587c131baSJan Høydahl            if desc:
77687c131baSJan Høydahl                print("%s" % desc)
77787c131baSJan Høydahl            try:
77887c131baSJan Høydahl                if self.function and not self.is_done():
77987c131baSJan Høydahl                    if not eval(self.function)(self):
78087c131baSJan Høydahl                        return
78187c131baSJan Høydahl            except Exception as e:
78287c131baSJan Høydahl                print("Function call to %s for todo %s failed: %s" % (self.function, self.id, e))
78387c131baSJan Høydahl                raise e
78487c131baSJan Høydahl            if self.user_input and not self.is_done():
78587c131baSJan Høydahl                ui_list = ensure_list(self.user_input)
78687c131baSJan Høydahl                for ui in ui_list:
78787c131baSJan Høydahl                    ui.run(self.state)
78887c131baSJan Høydahl                print()
78987c131baSJan Høydahl            if self.links:
79087c131baSJan Høydahl                print("\nLinks:\n")
79187c131baSJan Høydahl                for link in self.links:
79287c131baSJan Høydahl                    print("- %s" % expand_jinja(link, self.get_vars_and_state()))
79387c131baSJan Høydahl                print()
79487c131baSJan Høydahl            cmds = self.get_commands()
79587c131baSJan Høydahl            if cmds:
79687c131baSJan Høydahl                if not self.is_done():
79787c131baSJan Høydahl                    if not cmds.logs_prefix:
79887c131baSJan Høydahl                        cmds.logs_prefix = self.id
79987c131baSJan Høydahl                    cmds.run()
80087c131baSJan Høydahl                else:
80187c131baSJan Høydahl                    print("This step is already completed. You have to first set it to 'not completed' in order to execute commands again.")
80287c131baSJan Høydahl                print()
80387c131baSJan Høydahl            if self.post_description:
80487c131baSJan Høydahl                print("%s" % self.get_post_description())
80587c131baSJan Høydahl            todostate = self.get_state()
80687c131baSJan Høydahl            if self.is_done() and len(todostate) > 2:
80787c131baSJan Høydahl                print("Variables registered\n")
80887c131baSJan Høydahl                for k in todostate:
80987c131baSJan Høydahl                    if k == 'done' or k == 'done_date':
81087c131baSJan Høydahl                        continue
81187c131baSJan Høydahl                    print("* %s = %s" % (k, todostate[k]))
81287c131baSJan Høydahl                print()
81387c131baSJan Høydahl            completed = ask_yes_no("Mark task '%s' as completed?" % self.get_title())
81487c131baSJan Høydahl            self.set_done(completed)
81587c131baSJan Høydahl            state.save()
81687c131baSJan Høydahl        except Exception as e:
81787c131baSJan Høydahl            print("ERROR while executing todo %s (%s)" % (self.get_title(), e))
81887c131baSJan Høydahl
81987c131baSJan Høydahl    def get_menu_item(self):
82087c131baSJan Høydahl        return UpdatableFunctionItem(self.get_title, self.display_and_confirm)
82187c131baSJan Høydahl
82287c131baSJan Høydahl    def clone(self):
82387c131baSJan Høydahl        clone = Todo(self.id, self.title, description=self.description)
82487c131baSJan Høydahl        clone.state = copy.deepcopy(self.state)
82587c131baSJan Høydahl        return clone
82687c131baSJan Høydahl
82787c131baSJan Høydahl    def clear(self):
82887c131baSJan Høydahl        self.state.clear()
82987c131baSJan Høydahl        self.set_done(False)
83087c131baSJan Høydahl
83187c131baSJan Høydahl    def get_state(self):
83287c131baSJan Høydahl        return self.state
83387c131baSJan Høydahl
83487c131baSJan Høydahl    def get_description(self):
83587c131baSJan Høydahl        desc = self.description
83687c131baSJan Høydahl        if desc:
83787c131baSJan Høydahl            return expand_jinja(desc, vars=self.get_vars_and_state())
83887c131baSJan Høydahl        else:
83987c131baSJan Høydahl            return None
84087c131baSJan Høydahl
84187c131baSJan Høydahl    def get_post_description(self):
84287c131baSJan Høydahl        if self.post_description:
84387c131baSJan Høydahl            return expand_jinja(self.post_description, vars=self.get_vars_and_state())
84487c131baSJan Høydahl        else:
84587c131baSJan Høydahl            return None
84687c131baSJan Høydahl
84787c131baSJan Høydahl    def get_commands(self):
84887c131baSJan Høydahl        cmds = self.commands
84987c131baSJan Høydahl        return cmds
85087c131baSJan Høydahl
85187c131baSJan Høydahl    def get_asciidoc(self):
85287c131baSJan Høydahl        if self.asciidoc:
85387c131baSJan Høydahl            return expand_jinja(self.asciidoc, vars=self.get_vars_and_state())
85487c131baSJan Høydahl        else:
85587c131baSJan Høydahl            return None
85687c131baSJan Høydahl
85787c131baSJan Høydahl    def get_vars_and_state(self):
85887c131baSJan Høydahl        d = self.get_vars().copy()
85987c131baSJan Høydahl        d.update(self.get_state())
86087c131baSJan Høydahl        return d
86187c131baSJan Høydahl
86287c131baSJan Høydahl
86387c131baSJan Høydahldef get_release_version():
86487c131baSJan Høydahl    v = str(input("Which version are you releasing? (x.y.z) "))
86587c131baSJan Høydahl    try:
86687c131baSJan Høydahl        version = Version.parse(v)
86787c131baSJan Høydahl    except:
86887c131baSJan Høydahl        print("Not a valid version %s" % v)
86987c131baSJan Høydahl        return get_release_version()
87087c131baSJan Høydahl
87187c131baSJan Høydahl    return str(version)
87287c131baSJan Høydahl
87387c131baSJan Høydahl
87487c131baSJan Høydahldef get_subtitle():
87587c131baSJan Høydahl    applying_groups = list(filter(lambda x: x.num_applies() > 0, state.todo_groups))
87687c131baSJan Høydahl    done_groups = sum(1 for x in applying_groups if x.is_done())
87787c131baSJan Høydahl    return "Please complete the below checklist (Complete: %s/%s)" % (done_groups, len(applying_groups))
87887c131baSJan Høydahl
87987c131baSJan Høydahl
88087c131baSJan Høydahldef get_todo_menuitem_title():
88187c131baSJan Høydahl    return "Go to checklist (RC%d)" % (state.rc_number)
88287c131baSJan Høydahl
88387c131baSJan Høydahl
88487c131baSJan Høydahldef get_releasing_text():
885674b66ddSJan Høydahl    return "Releasing Lucene %s RC%d" % (state.release_version, state.rc_number)
88687c131baSJan Høydahl
88787c131baSJan Høydahl
88887c131baSJan Høydahldef get_start_new_rc_menu_title():
88987c131baSJan Høydahl    return "Abort RC%d and start a new RC%d" % (state.rc_number, state.rc_number + 1)
89087c131baSJan Høydahl
89187c131baSJan Høydahl
89287c131baSJan Høydahldef start_new_rc():
89387c131baSJan Høydahl    state.new_rc()
89487c131baSJan Høydahl    print("Started RC%d" % state.rc_number)
89587c131baSJan Høydahl
89687c131baSJan Høydahl
89787c131baSJan Høydahldef reset_state():
89887c131baSJan Høydahl    global state
89987c131baSJan Høydahl    if ask_yes_no("Are you sure? This will erase all current progress"):
90087c131baSJan Høydahl        maybe_remove_rc_from_svn()
90187c131baSJan Høydahl        shutil.rmtree(os.path.join(state.config_path, state.release_version))
90287c131baSJan Høydahl        state.clear()
90387c131baSJan Høydahl
90487c131baSJan Høydahl
90587c131baSJan Høydahldef template(name, vars=None):
90687c131baSJan Høydahl    return expand_jinja(templates[name], vars=vars)
90787c131baSJan Høydahl
90887c131baSJan Høydahl
90987c131baSJan Høydahldef help():
91087c131baSJan Høydahl    print(template('help'))
91187c131baSJan Høydahl    pause()
91287c131baSJan Høydahl
91387c131baSJan Høydahl
91487c131baSJan Høydahldef ensure_list(o):
91587c131baSJan Høydahl    if o is None:
91687c131baSJan Høydahl        return []
91787c131baSJan Høydahl    if not isinstance(o, list):
91887c131baSJan Høydahl        return [o]
91987c131baSJan Høydahl    else:
92087c131baSJan Høydahl        return o
92187c131baSJan Høydahl
92287c131baSJan Høydahl
92387c131baSJan Høydahldef open_file(filename):
92487c131baSJan Høydahl    print("Opening file %s" % filename)
92587c131baSJan Høydahl    if platform.system().startswith("Win"):
92687c131baSJan Høydahl        run("start %s" % filename)
92787c131baSJan Høydahl    else:
92887c131baSJan Høydahl        run("open %s" % filename)
92987c131baSJan Høydahl
93087c131baSJan Høydahl
93187c131baSJan Høydahldef expand_multiline(cmd_txt, indent=0):
93287c131baSJan Høydahl    return re.sub(r'  +', " %s\n    %s" % (Commands.cmd_continuation_char, " "*indent), cmd_txt)
93387c131baSJan Høydahl
93487c131baSJan Høydahl
93587c131baSJan Høydahldef unix_to_datetime(unix_stamp):
93687c131baSJan Høydahl    return datetime.utcfromtimestamp(unix_stamp / 1000)
93787c131baSJan Høydahl
93887c131baSJan Høydahl
93987c131baSJan Høydahldef generate_asciidoc():
94087c131baSJan Høydahl    base_filename = os.path.join(state.get_release_folder(),
941674b66ddSJan Høydahl                                 "lucene_release_%s"
94287c131baSJan Høydahl                                 % (state.release_version.replace("\.", "_")))
94387c131baSJan Høydahl
94487c131baSJan Høydahl    filename_adoc = "%s.adoc" % base_filename
94587c131baSJan Høydahl    filename_html = "%s.html" % base_filename
94687c131baSJan Høydahl    fh = open(filename_adoc, "w")
94787c131baSJan Høydahl
948674b66ddSJan Høydahl    fh.write("= Lucene Release %s\n\n" % state.release_version)
949462c6c70SJan Høydahl    fh.write("(_Generated by releaseWizard.py v%s at %s_)\n\n"
95087c131baSJan Høydahl             % (getScriptVersion(), datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")))
95187c131baSJan Høydahl    fh.write(":numbered:\n\n")
95287c131baSJan Høydahl    fh.write("%s\n\n" % template('help'))
95387c131baSJan Høydahl    for group in state.todo_groups:
95487c131baSJan Høydahl        if group.num_applies() == 0:
95587c131baSJan Høydahl            continue
95687c131baSJan Høydahl        fh.write("== %s\n\n" % group.get_title())
95787c131baSJan Høydahl        fh.write("%s\n\n" % group.get_description())
95887c131baSJan Høydahl        for todo in group.get_todos():
95987c131baSJan Høydahl            if not todo.applies(state.release_type):
96087c131baSJan Høydahl                continue
96187c131baSJan Høydahl            fh.write("=== %s\n\n" % todo.get_title())
96287c131baSJan Høydahl            if todo.is_done():
96387c131baSJan Høydahl                fh.write("_Completed %s_\n\n" % unix_to_datetime(todo.state['done_date']).strftime(
96487c131baSJan Høydahl                    "%Y-%m-%d %H:%M UTC"))
96587c131baSJan Høydahl            if todo.get_asciidoc():
96687c131baSJan Høydahl                fh.write("%s\n\n" % todo.get_asciidoc())
96787c131baSJan Høydahl            else:
96887c131baSJan Høydahl                desc = todo.get_description()
96987c131baSJan Høydahl                if desc:
97087c131baSJan Høydahl                    fh.write("%s\n\n" % desc)
97187c131baSJan Høydahl            state_copy = copy.deepcopy(todo.state)
97287c131baSJan Høydahl            state_copy.pop('done', None)
97387c131baSJan Høydahl            state_copy.pop('done_date', None)
97487c131baSJan Høydahl            if len(state_copy) > 0 or todo.user_input is not None:
97587c131baSJan Høydahl                fh.write(".Variables collected in this step\n")
97687c131baSJan Høydahl                fh.write("|===\n")
97787c131baSJan Høydahl                fh.write("|Variable |Value\n")
97887c131baSJan Høydahl                mykeys = set()
97987c131baSJan Høydahl                for e in ensure_list(todo.user_input):
98087c131baSJan Høydahl                    mykeys.add(e.name)
98187c131baSJan Høydahl                for e in state_copy.keys():
98287c131baSJan Høydahl                    mykeys.add(e)
98387c131baSJan Høydahl                for key in mykeys:
98487c131baSJan Høydahl                    val = "(not set)"
98587c131baSJan Høydahl                    if key in state_copy:
98687c131baSJan Høydahl                        val = state_copy[key]
98787c131baSJan Høydahl                    fh.write("\n|%s\n|%s\n" % (key, val))
98887c131baSJan Høydahl                fh.write("|===\n\n")
98987c131baSJan Høydahl            cmds = todo.get_commands()
99087c131baSJan Høydahl            if cmds:
99187c131baSJan Høydahl                if cmds.commands_text:
99287c131baSJan Høydahl                    fh.write("%s\n\n" % cmds.get_commands_text())
99387c131baSJan Høydahl                fh.write("[source,sh]\n----\n")
99487c131baSJan Høydahl                if cmds.env:
99587c131baSJan Høydahl                    for key in cmds.env:
99687c131baSJan Høydahl                        val = cmds.env[key]
99787c131baSJan Høydahl                        if is_windows():
99887c131baSJan Høydahl                            fh.write("SET %s=%s\n" % (key, val))
99987c131baSJan Høydahl                        else:
100087c131baSJan Høydahl                            fh.write("export %s=%s\n" % (key, val))
100187c131baSJan Høydahl                fh.write(abbreviate_homedir("cd %s\n" % cmds.get_root_folder()))
100287c131baSJan Høydahl                cmds2 = ensure_list(cmds.commands)
100387c131baSJan Høydahl                for c in cmds2:
100487c131baSJan Høydahl                    for line in c.display_cmd():
100587c131baSJan Høydahl                        fh.write("%s\n" % line)
100687c131baSJan Høydahl                fh.write("----\n\n")
100787c131baSJan Høydahl            if todo.post_description and not todo.get_asciidoc():
100887c131baSJan Høydahl                fh.write("\n%s\n\n" % todo.get_post_description())
100987c131baSJan Høydahl            if todo.links:
101087c131baSJan Høydahl                fh.write("Links:\n\n")
101187c131baSJan Høydahl                for l in todo.links:
101287c131baSJan Høydahl                    fh.write("* %s\n" % expand_jinja(l))
101387c131baSJan Høydahl                fh.write("\n")
101487c131baSJan Høydahl
101587c131baSJan Høydahl    fh.close()
101687c131baSJan Høydahl    print("Wrote file %s" % os.path.join(state.get_release_folder(), filename_adoc))
101787c131baSJan Høydahl    print("Running command 'asciidoctor %s'" % filename_adoc)
101887c131baSJan Høydahl    run_follow("asciidoctor %s" % filename_adoc)
101987c131baSJan Høydahl    if os.path.exists(filename_html):
102087c131baSJan Høydahl        open_file(filename_html)
102187c131baSJan Høydahl    else:
102287c131baSJan Høydahl        print("Failed generating HTML version, please install asciidoctor")
102387c131baSJan Høydahl    pause()
102487c131baSJan Høydahl
102587c131baSJan Høydahl
102687c131baSJan Høydahldef load_rc():
102787c131baSJan Høydahl    lucenerc = os.path.expanduser("~/.lucenerc")
102887c131baSJan Høydahl    try:
102987c131baSJan Høydahl        with open(lucenerc, 'r') as fp:
103087c131baSJan Høydahl            return json.load(fp)
103187c131baSJan Høydahl    except:
103287c131baSJan Høydahl        return None
103387c131baSJan Høydahl
103487c131baSJan Høydahl
103587c131baSJan Høydahldef store_rc(release_root, release_version=None):
103687c131baSJan Høydahl    lucenerc = os.path.expanduser("~/.lucenerc")
103787c131baSJan Høydahl    dict = {}
103887c131baSJan Høydahl    dict['root'] = release_root
103987c131baSJan Høydahl    if release_version:
104087c131baSJan Høydahl        dict['release_version'] = release_version
104187c131baSJan Høydahl    with open(lucenerc, "w") as fp:
104287c131baSJan Høydahl        json.dump(dict, fp, indent=2)
104387c131baSJan Høydahl
104487c131baSJan Høydahl
104587c131baSJan Høydahldef release_other_version():
104687c131baSJan Høydahl    if not state.is_released():
104787c131baSJan Høydahl        maybe_remove_rc_from_svn()
104887c131baSJan Høydahl    store_rc(state.config_path, None)
104987c131baSJan Høydahl    print("Please restart the wizard")
105087c131baSJan Høydahl    sys.exit(0)
105187c131baSJan Høydahl
105287c131baSJan Høydahldef file_to_string(filename):
105387c131baSJan Høydahl    with open(filename, encoding='utf8') as f:
105487c131baSJan Høydahl        return f.read().strip()
105587c131baSJan Høydahl
105687c131baSJan Høydahldef download_keys():
105787c131baSJan Høydahl    download('KEYS', "https://archive.apache.org/dist/lucene/KEYS", state.config_path)
105887c131baSJan Høydahl
105987c131baSJan Høydahldef keys_downloaded():
106087c131baSJan Høydahl    return os.path.exists(os.path.join(state.config_path, "KEYS"))
106187c131baSJan Høydahl
106287c131baSJan Høydahl
106387c131baSJan Høydahldef dump_yaml():
106487c131baSJan Høydahl    file = open(os.path.join(script_path, "releaseWizard.yaml"), "w")
106587c131baSJan Høydahl    yaml.add_representer(str, str_presenter)
106687c131baSJan Høydahl    yaml.Dumper.ignore_aliases = lambda *args : True
106787c131baSJan Høydahl    dump_obj = {'templates': templates,
106887c131baSJan Høydahl                'groups': state.todo_groups}
106987c131baSJan Høydahl    yaml.dump(dump_obj, width=180, stream=file, sort_keys=False, default_flow_style=False)
107087c131baSJan Høydahl
107187c131baSJan Høydahl
107287c131baSJan Høydahldef parse_config():
107387c131baSJan Høydahl    description = 'Script to guide a RM through the whole release process'
107487c131baSJan Høydahl    parser = argparse.ArgumentParser(description=description, epilog="Go push that release!",
107587c131baSJan Høydahl                                     formatter_class=argparse.RawDescriptionHelpFormatter)
107687c131baSJan Høydahl    parser.add_argument('--dry-run', dest='dry', action='store_true', default=False,
107787c131baSJan Høydahl                        help='Do not execute any commands, but echo them instead. Display extra debug info')
107887c131baSJan Høydahl    parser.add_argument('--init', action='store_true', default=False,
107987c131baSJan Høydahl                        help='Re-initialize root and version')
108087c131baSJan Høydahl    config = parser.parse_args()
108187c131baSJan Høydahl
108287c131baSJan Høydahl    return config
108387c131baSJan Høydahl
108487c131baSJan Høydahl
108587c131baSJan Høydahldef load(urlString, encoding="utf-8"):
108687c131baSJan Høydahl    try:
108787c131baSJan Høydahl        content = urllib.request.urlopen(urlString).read().decode(encoding)
108887c131baSJan Høydahl    except Exception as e:
108987c131baSJan Høydahl        print('Retrying download of url %s after exception: %s' % (urlString, e))
109087c131baSJan Høydahl        content = urllib.request.urlopen(urlString).read().decode(encoding)
109187c131baSJan Høydahl    return content
109287c131baSJan Høydahl
109387c131baSJan Høydahl
109487c131baSJan Høydahldef configure_pgp(gpg_todo):
109587c131baSJan Høydahl    print("Based on your Apache ID we'll lookup your key online\n"
109687c131baSJan Høydahl          "and through this complete the 'gpg' prerequisite task.\n")
109787c131baSJan Høydahl    gpg_state = gpg_todo.get_state()
109887c131baSJan Høydahl    id = str(input("Please enter your Apache id: (ENTER=skip) "))
109987c131baSJan Høydahl    if id.strip() == '':
110087c131baSJan Høydahl        return False
1101059d06ceSHouston Putman    key_url = "https://home.apache.org/keys/committer/%s.asc" % id.strip()
1102059d06ceSHouston Putman    committer_key = load(key_url)
1103059d06ceSHouston Putman    lines = committer_key.splitlines()
110487c131baSJan Høydahl    keyid_linenum = None
110587c131baSJan Høydahl    for idx, line in enumerate(lines):
110687c131baSJan Høydahl        if line == 'ASF ID: %s' % id:
110787c131baSJan Høydahl            keyid_linenum = idx+1
110887c131baSJan Høydahl            break
110987c131baSJan Høydahl    if keyid_linenum:
111087c131baSJan Høydahl        keyid_line = lines[keyid_linenum]
111187c131baSJan Høydahl        assert keyid_line.startswith('LDAP PGP key: ')
111287c131baSJan Høydahl        gpg_id = keyid_line[14:].replace(" ", "")[-8:]
1113059d06ceSHouston Putman        print("Found gpg key id %s on file at Apache (%s)" % (gpg_id, key_url))
111487c131baSJan Høydahl    else:
111587c131baSJan Høydahl        print(textwrap.dedent("""\
111687c131baSJan Høydahl            Could not find your GPG key from Apache servers.
111787c131baSJan Høydahl            Please make sure you have registered your key ID in
111887c131baSJan Høydahl            id.apache.org, see links for more info."""))
111987c131baSJan Høydahl        gpg_id = str(input("Enter your key ID manually, 8 last characters (ENTER=skip): "))
112087c131baSJan Høydahl        if gpg_id.strip() == '':
112187c131baSJan Høydahl            return False
112287c131baSJan Høydahl        elif len(gpg_id) != 8:
112387c131baSJan Høydahl            print("gpg id must be the last 8 characters of your key id")
112487c131baSJan Høydahl        gpg_id = gpg_id.upper()
112587c131baSJan Høydahl    try:
112687c131baSJan Høydahl        res = run("gpg --list-secret-keys %s" % gpg_id)
112787c131baSJan Høydahl        print("Found key %s on your private gpg keychain" % gpg_id)
112887c131baSJan Høydahl        # Check rsa and key length >= 4096
112987c131baSJan Høydahl        match = re.search(r'^sec +((rsa|dsa)(\d{4})) ', res)
113087c131baSJan Høydahl        type = "(unknown)"
113187c131baSJan Høydahl        length = -1
113287c131baSJan Høydahl        if match:
113387c131baSJan Høydahl            type = match.group(2)
113487c131baSJan Høydahl            length = int(match.group(3))
113587c131baSJan Høydahl        else:
113687c131baSJan Høydahl            match = re.search(r'^sec +((\d{4})([DR])/.*?) ', res)
113787c131baSJan Høydahl            if match:
113887c131baSJan Høydahl                type = 'rsa' if match.group(3) == 'R' else 'dsa'
113987c131baSJan Høydahl                length = int(match.group(2))
114087c131baSJan Høydahl            else:
114187c131baSJan Høydahl                print("Could not determine type and key size for your key")
114287c131baSJan Høydahl                print("%s" % res)
114387c131baSJan Høydahl                if not ask_yes_no("Is your key of type RSA and size >= 2048 (ideally 4096)? "):
114487c131baSJan Høydahl                    print("Sorry, please generate a new key, add to KEYS and register with id.apache.org")
114587c131baSJan Høydahl                    return False
114687c131baSJan Høydahl        if not type == 'rsa':
114787c131baSJan Høydahl            print("We strongly recommend RSA type key, your is '%s'. Consider generating a new key." % type.upper())
114887c131baSJan Høydahl        if length < 2048:
114987c131baSJan Høydahl            print("Your key has key length of %s. Cannot use < 2048, please generate a new key before doing the release" % length)
115087c131baSJan Høydahl            return False
115187c131baSJan Høydahl        if length < 4096:
115287c131baSJan Høydahl            print("Your key length is < 4096, Please generate a stronger key.")
115387c131baSJan Høydahl            print("Alternatively, follow instructions in http://www.apache.org/dev/release-signing.html#note")
115487c131baSJan Høydahl            if not ask_yes_no("Have you configured your gpg to avoid SHA-1?"):
115587c131baSJan Høydahl                print("Please either generate a strong key or reconfigure your client")
115687c131baSJan Høydahl                return False
115787c131baSJan Høydahl        print("Validated that your key is of type RSA and has a length >= 2048 (%s)" % length)
115887c131baSJan Høydahl    except:
115987c131baSJan Høydahl        print(textwrap.dedent("""\
116087c131baSJan Høydahl            Key not found on your private gpg keychain. In order to sign the release you'll
116187c131baSJan Høydahl            need to fix this, then try again"""))
116287c131baSJan Høydahl        return False
116387c131baSJan Høydahl    try:
116487c131baSJan Høydahl        lines = run("gpg --check-signatures %s" % gpg_id).splitlines()
116587c131baSJan Høydahl        sigs = 0
116687c131baSJan Høydahl        apache_sigs = 0
116787c131baSJan Høydahl        for line in lines:
116887c131baSJan Høydahl            if line.startswith("sig") and not gpg_id in line:
116987c131baSJan Høydahl                sigs += 1
117087c131baSJan Høydahl                if '@apache.org' in line:
117187c131baSJan Høydahl                    apache_sigs += 1
117287c131baSJan Høydahl        print("Your key has %s signatures, of which %s are by committers (@apache.org address)" % (sigs, apache_sigs))
117387c131baSJan Høydahl        if apache_sigs < 1:
117487c131baSJan Høydahl            print(textwrap.dedent("""\
117587c131baSJan Høydahl                Your key is not signed by any other committer.
117687c131baSJan Høydahl                Please review http://www.apache.org/dev/openpgp.html#apache-wot
117787c131baSJan Høydahl                and make sure to get your key signed until next time.
117887c131baSJan Høydahl                You may want to run 'gpg --refresh-keys' to refresh your keychain."""))
117987c131baSJan Høydahl        uses_apacheid = is_code_signing_key = False
118087c131baSJan Høydahl        for line in lines:
118187c131baSJan Høydahl            if line.startswith("uid") and "%s@apache" % id in line:
118287c131baSJan Høydahl                uses_apacheid = True
118387c131baSJan Høydahl                if 'CODE SIGNING KEY' in line.upper():
118487c131baSJan Høydahl                    is_code_signing_key = True
118587c131baSJan Høydahl        if not uses_apacheid:
118687c131baSJan Høydahl            print("WARNING: Your key should use your apache-id email address, see http://www.apache.org/dev/release-signing.html#user-id")
118787c131baSJan Høydahl        if not is_code_signing_key:
118887c131baSJan Høydahl            print("WARNING: You code signing key should be labeled 'CODE SIGNING KEY', see http://www.apache.org/dev/release-signing.html#key-comment")
118987c131baSJan Høydahl    except Exception as e:
119087c131baSJan Høydahl        print("Could not check signatures of your key: %s" % e)
119187c131baSJan Høydahl
119287c131baSJan Høydahl    download_keys()
119387c131baSJan Høydahl    keys_text = file_to_string(os.path.join(state.config_path, "KEYS"))
119487c131baSJan Høydahl    if gpg_id in keys_text or "%s %s" % (gpg_id[:4], gpg_id[-4:]) in keys_text:
119587c131baSJan Høydahl        print("Found your key ID in official KEYS file. KEYS file is not cached locally.")
119687c131baSJan Høydahl    else:
119787c131baSJan Høydahl        print(textwrap.dedent("""\
119887c131baSJan Høydahl            Could not find your key ID in official KEYS file.
119987c131baSJan Høydahl            Please make sure it is added to https://dist.apache.org/repos/dist/release/lucene/KEYS
120087c131baSJan Høydahl            and committed to svn. Then re-try this initialization"""))
120187c131baSJan Høydahl        if not ask_yes_no("Do you want to continue without fixing KEYS file? (not recommended) "):
120287c131baSJan Høydahl            return False
120387c131baSJan Høydahl
120487c131baSJan Høydahl    gpg_state['apache_id'] = id
120587c131baSJan Høydahl    gpg_state['gpg_key'] = gpg_id
120687c131baSJan Høydahl    return True
120787c131baSJan Høydahl
120887c131baSJan Høydahl
120987c131baSJan Høydahldef pause(fun=None):
121087c131baSJan Høydahl    if fun:
121187c131baSJan Høydahl        fun()
121287c131baSJan Høydahl    input("\nPress ENTER to continue...")
121387c131baSJan Høydahl
121487c131baSJan Høydahl
121587c131baSJan Høydahl# Custom classes for ConsoleMenu, to make menu texts dynamic
121687c131baSJan Høydahl# Needed until https://github.com/aegirhall/console-menu/pull/25 is released
121787c131baSJan Høydahl# See https://pypi.org/project/console-menu/ for other docs
121887c131baSJan Høydahl
121987c131baSJan Høydahlclass UpdatableConsoleMenu(ConsoleMenu):
122087c131baSJan Høydahl
122187c131baSJan Høydahl    def __repr__(self):
122287c131baSJan Høydahl        return "%s: %s. %d items" % (self.get_title(), self.get_subtitle(), len(self.items))
122387c131baSJan Høydahl
122487c131baSJan Høydahl    def draw(self):
122587c131baSJan Høydahl        """
122687c131baSJan Høydahl        Refreshes the screen and redraws the menu. Should be called whenever something changes that needs to be redrawn.
122787c131baSJan Høydahl        """
122887c131baSJan Høydahl        self.screen.printf(self.formatter.format(title=self.get_title(), subtitle=self.get_subtitle(), items=self.items,
122987c131baSJan Høydahl                                                 prologue_text=self.get_prologue_text(), epilogue_text=self.get_epilogue_text()))
123087c131baSJan Høydahl
123187c131baSJan Høydahl    # Getters to get text in case method reference
123287c131baSJan Høydahl    def get_title(self):
123387c131baSJan Høydahl        return self.title() if callable(self.title) else self.title
123487c131baSJan Høydahl
123587c131baSJan Høydahl    def get_subtitle(self):
123687c131baSJan Høydahl        return self.subtitle() if callable(self.subtitle) else self.subtitle
123787c131baSJan Høydahl
123887c131baSJan Høydahl    def get_prologue_text(self):
123987c131baSJan Høydahl        return self.prologue_text() if callable(self.prologue_text) else self.prologue_text
124087c131baSJan Høydahl
124187c131baSJan Høydahl    def get_epilogue_text(self):
124287c131baSJan Høydahl        return self.epilogue_text() if callable(self.epilogue_text) else self.epilogue_text
124387c131baSJan Høydahl
124487c131baSJan Høydahl
124587c131baSJan Høydahlclass UpdatableSubmenuItem(SubmenuItem):
124687c131baSJan Høydahl    def __init__(self, text, submenu, menu=None, should_exit=False):
124787c131baSJan Høydahl        """
124887c131baSJan Høydahl        :ivar ConsoleMenu self.submenu: The submenu to be opened when this item is selected
124987c131baSJan Høydahl        """
125087c131baSJan Høydahl        super(SubmenuItem, self).__init__(text=text, menu=menu, should_exit=should_exit)
125187c131baSJan Høydahl
125287c131baSJan Høydahl        self.submenu = submenu
125387c131baSJan Høydahl        if menu:
125487c131baSJan Høydahl            self.get_submenu().parent = menu
125587c131baSJan Høydahl
125687c131baSJan Høydahl    def show(self, index):
125787c131baSJan Høydahl        return "%2d - %s" % (index + 1, self.get_text())
125887c131baSJan Høydahl
125987c131baSJan Høydahl    # Getters to get text in case method reference
126087c131baSJan Høydahl    def get_text(self):
126187c131baSJan Høydahl        return self.text() if callable(self.text) else self.text
126287c131baSJan Høydahl
126387c131baSJan Høydahl    def set_menu(self, menu):
126487c131baSJan Høydahl        """
126587c131baSJan Høydahl        Sets the menu of this item.
126687c131baSJan Høydahl        Should be used instead of directly accessing the menu attribute for this class.
126787c131baSJan Høydahl
126887c131baSJan Høydahl        :param ConsoleMenu menu: the menu
126987c131baSJan Høydahl        """
127087c131baSJan Høydahl        self.menu = menu
127187c131baSJan Høydahl        self.get_submenu().parent = menu
127287c131baSJan Høydahl
127387c131baSJan Høydahl    def action(self):
127487c131baSJan Høydahl        """
127587c131baSJan Høydahl        This class overrides this method
127687c131baSJan Høydahl        """
127787c131baSJan Høydahl        self.get_submenu().start()
127887c131baSJan Høydahl
127987c131baSJan Høydahl    def clean_up(self):
128087c131baSJan Høydahl        """
128187c131baSJan Høydahl        This class overrides this method
128287c131baSJan Høydahl        """
128387c131baSJan Høydahl        self.get_submenu().join()
128487c131baSJan Høydahl        self.menu.clear_screen()
128587c131baSJan Høydahl        self.menu.resume()
128687c131baSJan Høydahl
128787c131baSJan Høydahl    def get_return(self):
128887c131baSJan Høydahl        """
128987c131baSJan Høydahl        :return: The returned value in the submenu
129087c131baSJan Høydahl        """
129187c131baSJan Høydahl        return self.get_submenu().returned_value
129287c131baSJan Høydahl
129387c131baSJan Høydahl    def get_submenu(self):
129487c131baSJan Høydahl        """
129587c131baSJan Høydahl        We unwrap the submenu variable in case it is a reference to a method that returns a submenu
129687c131baSJan Høydahl        """
129787c131baSJan Høydahl        return self.submenu if not callable(self.submenu) else self.submenu()
129887c131baSJan Høydahl
129987c131baSJan Høydahl
130087c131baSJan Høydahlclass UpdatableFunctionItem(FunctionItem):
130187c131baSJan Høydahl    def show(self, index):
130287c131baSJan Høydahl        return "%2d - %s" % (index + 1, self.get_text())
130387c131baSJan Høydahl
130487c131baSJan Høydahl    # Getters to get text in case method reference
130587c131baSJan Høydahl    def get_text(self):
130687c131baSJan Høydahl        return self.text() if callable(self.text) else self.text
130787c131baSJan Høydahl
130887c131baSJan Høydahl
130987c131baSJan Høydahlclass MyScreen(Screen):
131087c131baSJan Høydahl    def clear(self):
131187c131baSJan Høydahl        return
131287c131baSJan Høydahl
131387c131baSJan Høydahl
131487c131baSJan Høydahlclass CustomExitItem(ExitItem):
131587c131baSJan Høydahl    def show(self, index):
131687c131baSJan Høydahl        return super(ExitItem, self).show(index)
131787c131baSJan Høydahl
131887c131baSJan Høydahl    def get_return(self):
131987c131baSJan Høydahl        return ""
132087c131baSJan Høydahl
132187c131baSJan Høydahl
132287c131baSJan Høydahldef main():
132387c131baSJan Høydahl    global state
132487c131baSJan Høydahl    global dry_run
132587c131baSJan Høydahl    global templates
132687c131baSJan Høydahl
1327674b66ddSJan Høydahl    print("Lucene releaseWizard v%s" % getScriptVersion())
132887c131baSJan Høydahl    c = parse_config()
132987c131baSJan Høydahl
133087c131baSJan Høydahl    if c.dry:
133187c131baSJan Høydahl        print("Entering dry-run mode where all commands will be echoed instead of executed")
133287c131baSJan Høydahl        dry_run = True
133387c131baSJan Høydahl
133487c131baSJan Høydahl    release_root = os.path.expanduser("~/.lucene-releases")
133587c131baSJan Høydahl    if not load_rc() or c.init:
133687c131baSJan Høydahl        print("Initializing")
133787c131baSJan Høydahl        dir_ok = False
133887c131baSJan Høydahl        root = str(input("Choose root folder: [~/.lucene-releases] "))
133987c131baSJan Høydahl        if os.path.exists(root) and (not os.path.isdir(root) or not os.access(root, os.W_OK)):
134087c131baSJan Høydahl            sys.exit("Root %s exists but is not a directory or is not writable" % root)
134187c131baSJan Høydahl        if not root == '':
134287c131baSJan Høydahl            if root.startswith("~/"):
134387c131baSJan Høydahl                release_root = os.path.expanduser(root)
134487c131baSJan Høydahl            else:
134587c131baSJan Høydahl                release_root = os.path.abspath(root)
134687c131baSJan Høydahl        if not os.path.exists(release_root):
134787c131baSJan Høydahl            try:
134887c131baSJan Høydahl                print("Creating release root %s" % release_root)
134987c131baSJan Høydahl                os.makedirs(release_root)
135087c131baSJan Høydahl            except Exception as e:
135187c131baSJan Høydahl                sys.exit("Error while creating %s: %s" % (release_root, e))
135287c131baSJan Høydahl        release_version = get_release_version()
135387c131baSJan Høydahl    else:
135487c131baSJan Høydahl        conf = load_rc()
135587c131baSJan Høydahl        release_root = conf['root']
135687c131baSJan Høydahl        if 'release_version' in conf:
135787c131baSJan Høydahl            release_version = conf['release_version']
135887c131baSJan Høydahl        else:
135987c131baSJan Høydahl            release_version = get_release_version()
136087c131baSJan Høydahl    store_rc(release_root, release_version)
136187c131baSJan Høydahl
136287c131baSJan Høydahl    check_prerequisites()
136387c131baSJan Høydahl
136487c131baSJan Høydahl    try:
136587c131baSJan Høydahl        y = yaml.load(open(os.path.join(script_path, "releaseWizard.yaml"), "r"), Loader=yaml.Loader)
136687c131baSJan Høydahl        templates = y.get('templates')
136787c131baSJan Høydahl        todo_list = y.get('groups')
136887c131baSJan Høydahl        state = ReleaseState(release_root, release_version, getScriptVersion())
136987c131baSJan Høydahl        state.init_todos(bootstrap_todos(todo_list))
137087c131baSJan Høydahl        state.load()
137187c131baSJan Høydahl    except Exception as e:
137287c131baSJan Høydahl        sys.exit("Failed initializing. %s" % e)
137387c131baSJan Høydahl
137487c131baSJan Høydahl    state.save()
137587c131baSJan Høydahl
13763134f10aSMike Drob    # Smoketester requires JAVA11_HOME to point to Java11
137787c131baSJan Høydahl    os.environ['JAVA_HOME'] = state.get_java_home()
137887c131baSJan Høydahl    os.environ['JAVACMD'] = state.get_java_cmd()
137987c131baSJan Høydahl
138087c131baSJan Høydahl    global lucene_news_file
1381329e7c7bSJan Høydahl    lucene_news_file = os.path.join(state.get_website_git_folder(), 'content', 'core', 'core_news',
1382329e7c7bSJan Høydahl      "%s-%s-available.md" % (state.get_release_date_iso(), state.release_version.replace(".", "-")))
1383329e7c7bSJan Høydahl    website_folder = state.get_website_git_folder()
138487c131baSJan Høydahl
1385674b66ddSJan Høydahl    main_menu = UpdatableConsoleMenu(title="Lucene ReleaseWizard",
138687c131baSJan Høydahl                            subtitle=get_releasing_text,
138787c131baSJan Høydahl                            prologue_text="Welcome to the release wizard. From here you can manage the process including creating new RCs. "
138887c131baSJan Høydahl                                          "All changes are persisted, so you can exit any time and continue later. Make sure to read the Help section.",
1389674b66ddSJan Høydahl                            epilogue_text="® 2021 The Lucene project. Licensed under the Apache License 2.0\nScript version v%s)" % getScriptVersion(),
139087c131baSJan Høydahl                            screen=MyScreen())
139187c131baSJan Høydahl
139287c131baSJan Høydahl    todo_menu = UpdatableConsoleMenu(title=get_releasing_text,
139387c131baSJan Høydahl                            subtitle=get_subtitle,
139487c131baSJan Høydahl                            prologue_text=None,
139587c131baSJan Høydahl                            epilogue_text=None,
139687c131baSJan Høydahl                            screen=MyScreen())
139787c131baSJan Høydahl    todo_menu.exit_item = CustomExitItem("Return")
139887c131baSJan Høydahl
139987c131baSJan Høydahl    for todo_group in state.todo_groups:
140087c131baSJan Høydahl        if todo_group.num_applies() >= 0:
140187c131baSJan Høydahl            menu_item = todo_group.get_menu_item()
140287c131baSJan Høydahl            menu_item.set_menu(todo_menu)
140387c131baSJan Høydahl            todo_menu.append_item(menu_item)
140487c131baSJan Høydahl
140587c131baSJan Høydahl    main_menu.append_item(UpdatableSubmenuItem(get_todo_menuitem_title, todo_menu, menu=main_menu))
140687c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem(get_start_new_rc_menu_title, start_new_rc))
140787c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Clear and restart current RC', state.clear_rc))
140887c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem("Clear all state, restart the %s release" % state.release_version, reset_state))
140987c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Start release for a different version', release_other_version))
141087c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Generate Asciidoc guide for this release', generate_asciidoc))
141187c131baSJan Høydahl    # main_menu.append_item(UpdatableFunctionItem('Dump YAML', dump_yaml))
141287c131baSJan Høydahl    main_menu.append_item(UpdatableFunctionItem('Help', help))
141387c131baSJan Høydahl
141487c131baSJan Høydahl    main_menu.show()
141587c131baSJan Høydahl
141687c131baSJan Høydahl
141787c131baSJan Høydahlsys.path.append(os.path.dirname(__file__))
141887c131baSJan Høydahlcurrent_git_root = os.path.abspath(
141987c131baSJan Høydahl    os.path.join(os.path.abspath(os.path.dirname(__file__)), os.path.pardir, os.path.pardir))
142087c131baSJan Høydahl
142187c131baSJan Høydahldry_run = False
142287c131baSJan Høydahl
142387c131baSJan Høydahlmajor_minor = ['major', 'minor']
142487c131baSJan Høydahlscript_path = os.path.dirname(os.path.realpath(__file__))
142587c131baSJan Høydahlos.chdir(script_path)
142687c131baSJan Høydahl
142787c131baSJan Høydahl
142887c131baSJan Høydahldef git_checkout_folder():
142987c131baSJan Høydahl    return state.get_git_checkout_folder()
143087c131baSJan Høydahl
143187c131baSJan Høydahl
143287c131baSJan Høydahldef tail_file(file, lines):
143387c131baSJan Høydahl    bufsize = 8192
143487c131baSJan Høydahl    fsize = os.stat(file).st_size
143587c131baSJan Høydahl    with open(file) as f:
143687c131baSJan Høydahl        if bufsize >= fsize:
143787c131baSJan Høydahl            bufsize = fsize
143887c131baSJan Høydahl        idx = 0
143987c131baSJan Høydahl        while True:
144087c131baSJan Høydahl            idx += 1
144187c131baSJan Høydahl            seek_pos = fsize - bufsize * idx
144287c131baSJan Høydahl            if seek_pos < 0:
144387c131baSJan Høydahl                seek_pos = 0
144487c131baSJan Høydahl            f.seek(seek_pos)
144587c131baSJan Høydahl            data = []
144687c131baSJan Høydahl            data.extend(f.readlines())
144787c131baSJan Høydahl            if len(data) >= lines or f.tell() == 0 or seek_pos == 0:
144887c131baSJan Høydahl                if not seek_pos == 0:
144987c131baSJan Høydahl                    print("Tailing last %d lines of file %s" % (lines, file))
145087c131baSJan Høydahl                print(''.join(data[-lines:]))
145187c131baSJan Høydahl                break
145287c131baSJan Høydahl
145387c131baSJan Høydahl
145487c131baSJan Høydahldef run_with_log_tail(command, cwd, logfile=None, tail_lines=10, tee=False, live=False, shell=None):
145587c131baSJan Høydahl    fh = sys.stdout
145687c131baSJan Høydahl    if logfile:
145787c131baSJan Høydahl        logdir = os.path.dirname(logfile)
145887c131baSJan Høydahl        if not os.path.exists(logdir):
145987c131baSJan Høydahl            os.makedirs(logdir)
146087c131baSJan Høydahl        fh = open(logfile, 'w')
146187c131baSJan Høydahl    rc = run_follow(command, cwd, fh=fh, tee=tee, live=live, shell=shell)
146287c131baSJan Høydahl    if logfile:
146387c131baSJan Høydahl        fh.close()
146487c131baSJan Høydahl        if not tee and tail_lines and tail_lines > 0:
146587c131baSJan Høydahl            tail_file(logfile, tail_lines)
146687c131baSJan Høydahl    return rc
146787c131baSJan Høydahl
146887c131baSJan Høydahl
146987c131baSJan Høydahldef ask_yes_no(text):
147087c131baSJan Høydahl    answer = None
147187c131baSJan Høydahl    while answer not in ['y', 'n']:
147287c131baSJan Høydahl        answer = str(input("\nQ: %s (y/n): " % text))
147387c131baSJan Høydahl    print("\n")
147487c131baSJan Høydahl    return answer == 'y'
147587c131baSJan Høydahl
147687c131baSJan Høydahl
147787c131baSJan Høydahldef abbreviate_line(line, width):
147887c131baSJan Høydahl    line = line.rstrip()
147987c131baSJan Høydahl    if len(line) > width:
148087c131baSJan Høydahl        line = "%s.....%s" % (line[:(width / 2 - 5)], line[-(width / 2):])
148187c131baSJan Høydahl    else:
148287c131baSJan Høydahl        line = "%s%s" % (line, " " * (width - len(line) + 2))
148387c131baSJan Høydahl    return line
148487c131baSJan Høydahl
148587c131baSJan Høydahl
148687c131baSJan Høydahldef print_line_cr(line, linenum, stdout=True, tee=False):
148787c131baSJan Høydahl    if not tee:
148887c131baSJan Høydahl        if not stdout:
148987c131baSJan Høydahl            print("[line %s] %s" % (linenum, abbreviate_line(line, 80)), end='\r')
149087c131baSJan Høydahl    else:
149187c131baSJan Høydahl        if line.endswith("\r"):
149287c131baSJan Høydahl            print(line.rstrip(), end='\r')
149387c131baSJan Høydahl        else:
149487c131baSJan Høydahl            print(line.rstrip())
149587c131baSJan Høydahl
149687c131baSJan Høydahl
149787c131baSJan Høydahldef run_follow(command, cwd=None, fh=sys.stdout, tee=False, live=False, shell=None):
149887c131baSJan Høydahl    doShell = '&&' in command or '&' in command or shell is not None
149987c131baSJan Høydahl    if not doShell and not isinstance(command, list):
150087c131baSJan Høydahl        command = shlex.split(command)
150187c131baSJan Høydahl    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd,
150287c131baSJan Høydahl                               universal_newlines=True, bufsize=0, close_fds=True, shell=doShell)
150387c131baSJan Høydahl    lines_written = 0
150487c131baSJan Høydahl
150587c131baSJan Høydahl    fl = fcntl.fcntl(process.stdout, fcntl.F_GETFL)
150687c131baSJan Høydahl    fcntl.fcntl(process.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK)
150787c131baSJan Høydahl
150887c131baSJan Høydahl    flerr = fcntl.fcntl(process.stderr, fcntl.F_GETFL)
150987c131baSJan Høydahl    fcntl.fcntl(process.stderr, fcntl.F_SETFL, flerr | os.O_NONBLOCK)
151087c131baSJan Høydahl
151187c131baSJan Høydahl    endstdout = endstderr = False
151287c131baSJan Høydahl    errlines = []
151387c131baSJan Høydahl    while not (endstderr and endstdout):
151487c131baSJan Høydahl        lines_before = lines_written
151587c131baSJan Høydahl        if not endstdout:
151687c131baSJan Høydahl            try:
151787c131baSJan Høydahl                if live:
151887c131baSJan Høydahl                    chars = process.stdout.read()
151987c131baSJan Høydahl                    if chars == '' and process.poll() is not None:
152087c131baSJan Høydahl                        endstdout = True
152187c131baSJan Høydahl                    else:
152287c131baSJan Høydahl                        fh.write(chars)
152387c131baSJan Høydahl                        fh.flush()
152487c131baSJan Høydahl                        if '\n' in chars:
152587c131baSJan Høydahl                            lines_written += 1
152687c131baSJan Høydahl                else:
152787c131baSJan Høydahl                    line = process.stdout.readline()
152887c131baSJan Høydahl                    if line == '' and process.poll() is not None:
152987c131baSJan Høydahl                        endstdout = True
153087c131baSJan Høydahl                    else:
153187c131baSJan Høydahl                        fh.write("%s\n" % line.rstrip())
153287c131baSJan Høydahl                        fh.flush()
153387c131baSJan Høydahl                        lines_written += 1
153487c131baSJan Høydahl                        print_line_cr(line, lines_written, stdout=(fh == sys.stdout), tee=tee)
153587c131baSJan Høydahl
153687c131baSJan Høydahl            except Exception as ioe:
153787c131baSJan Høydahl                pass
153887c131baSJan Høydahl        if not endstderr:
153987c131baSJan Høydahl            try:
154087c131baSJan Høydahl                if live:
154187c131baSJan Høydahl                    chars = process.stderr.read()
154287c131baSJan Høydahl                    if chars == '' and process.poll() is not None:
154387c131baSJan Høydahl                        endstderr = True
154487c131baSJan Høydahl                    else:
154587c131baSJan Høydahl                        fh.write(chars)
154687c131baSJan Høydahl                        fh.flush()
154787c131baSJan Høydahl                        if '\n' in chars:
154887c131baSJan Høydahl                            lines_written += 1
154987c131baSJan Høydahl                else:
155087c131baSJan Høydahl                    line = process.stderr.readline()
155187c131baSJan Høydahl                    if line == '' and process.poll() is not None:
155287c131baSJan Høydahl                        endstderr = True
155387c131baSJan Høydahl                    else:
155487c131baSJan Høydahl                        errlines.append("%s\n" % line.rstrip())
155587c131baSJan Høydahl                        lines_written += 1
155687c131baSJan Høydahl                        print_line_cr(line, lines_written, stdout=(fh == sys.stdout), tee=tee)
155787c131baSJan Høydahl            except Exception as e:
155887c131baSJan Høydahl                pass
155987c131baSJan Høydahl
156087c131baSJan Høydahl        if not lines_written > lines_before:
156187c131baSJan Høydahl            # if no output then sleep a bit before checking again
156287c131baSJan Høydahl            time.sleep(0.1)
156387c131baSJan Høydahl
156487c131baSJan Høydahl    print(" " * 80)
156587c131baSJan Høydahl    rc = process.poll()
156687c131baSJan Høydahl    if len(errlines) > 0:
156787c131baSJan Høydahl        for line in errlines:
156887c131baSJan Høydahl            fh.write("%s\n" % line.rstrip())
156987c131baSJan Høydahl            fh.flush()
157087c131baSJan Høydahl    return rc
157187c131baSJan Høydahl
157287c131baSJan Høydahl
157387c131baSJan Høydahldef is_windows():
157487c131baSJan Høydahl    return platform.system().startswith("Win")
157587c131baSJan Høydahl
1576329e7c7bSJan Høydahldef is_mac():
1577329e7c7bSJan Høydahl    return platform.system().startswith("Darwin")
1578329e7c7bSJan Høydahl
1579329e7c7bSJan Høydahldef is_linux():
1580329e7c7bSJan Høydahl    return platform.system().startswith("Linux")
158187c131baSJan Høydahl
158287c131baSJan Høydahlclass Commands(SecretYamlObject):
158387c131baSJan Høydahl    yaml_tag = u'!Commands'
158487c131baSJan Høydahl    hidden_fields = ['todo_id']
158587c131baSJan Høydahl    cmd_continuation_char = "^" if is_windows() else "\\"
158687c131baSJan Høydahl    def __init__(self, root_folder, commands_text=None, commands=None, logs_prefix=None, run_text=None, enable_execute=None,
158787c131baSJan Høydahl                 confirm_each_command=None, env=None, vars=None, todo_id=None, remove_files=None):
158887c131baSJan Høydahl        self.root_folder = root_folder
158987c131baSJan Høydahl        self.commands_text = commands_text
159087c131baSJan Høydahl        self.vars = vars
159187c131baSJan Høydahl        self.env = env
159287c131baSJan Høydahl        self.run_text = run_text
159387c131baSJan Høydahl        self.remove_files = remove_files
159487c131baSJan Høydahl        self.todo_id = todo_id
159587c131baSJan Høydahl        self.logs_prefix = logs_prefix
159687c131baSJan Høydahl        self.enable_execute = enable_execute
159787c131baSJan Høydahl        self.confirm_each_command = confirm_each_command
159887c131baSJan Høydahl        self.commands = commands
159987c131baSJan Høydahl        for c in self.commands:
160087c131baSJan Høydahl            c.todo_id = todo_id
160187c131baSJan Høydahl
160287c131baSJan Høydahl    @classmethod
160387c131baSJan Høydahl    def from_yaml(cls, loader, node):
160487c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
160587c131baSJan Høydahl        return Commands(**fields)
160687c131baSJan Høydahl
160787c131baSJan Høydahl    def run(self):
160887c131baSJan Høydahl        root = self.get_root_folder()
160987c131baSJan Høydahl
161087c131baSJan Høydahl        if self.commands_text:
161187c131baSJan Høydahl            print(self.get_commands_text())
161287c131baSJan Høydahl        if self.env:
161387c131baSJan Høydahl            for key in self.env:
161487c131baSJan Høydahl                val = self.jinjaify(self.env[key])
161587c131baSJan Høydahl                os.environ[key] = val
161687c131baSJan Høydahl                if is_windows():
161787c131baSJan Høydahl                    print("\n  SET %s=%s" % (key, val))
161887c131baSJan Høydahl                else:
161987c131baSJan Høydahl                    print("\n  export %s=%s" % (key, val))
162087c131baSJan Høydahl        print(abbreviate_homedir("\n  cd %s" % root))
162187c131baSJan Høydahl        commands = ensure_list(self.commands)
162287c131baSJan Høydahl        for cmd in commands:
162387c131baSJan Høydahl            for line in cmd.display_cmd():
162487c131baSJan Høydahl                print("  %s" % line)
162587c131baSJan Høydahl        print()
162687c131baSJan Høydahl        if not self.enable_execute is False:
162787c131baSJan Høydahl            if self.run_text:
162887c131baSJan Høydahl                print("\n%s\n" % self.get_run_text())
162987c131baSJan Høydahl            if len(commands) > 1:
163087c131baSJan Høydahl                if not self.confirm_each_command is False:
163187c131baSJan Høydahl                    print("You will get prompted before running each individual command.")
163287c131baSJan Høydahl                else:
163387c131baSJan Høydahl                    print(
163487c131baSJan Høydahl                        "You will not be prompted for each command but will see the ouput of each. If one command fails the execution will stop.")
163587c131baSJan Høydahl            success = True
163687c131baSJan Høydahl            if ask_yes_no("Do you want me to run these commands now?"):
163787c131baSJan Høydahl                if self.remove_files:
163887c131baSJan Høydahl                    for _f in ensure_list(self.get_remove_files()):
163987c131baSJan Høydahl                        f = os.path.join(root, _f)
164087c131baSJan Høydahl                        if os.path.exists(f):
164187c131baSJan Høydahl                            filefolder = "File" if os.path.isfile(f) else "Folder"
164287c131baSJan Høydahl                            if ask_yes_no("%s %s already exists. Shall I remove it now?" % (filefolder, f)) and not dry_run:
164387c131baSJan Høydahl                                if os.path.isdir(f):
164487c131baSJan Høydahl                                    shutil.rmtree(f)
164587c131baSJan Høydahl                                else:
164687c131baSJan Høydahl                                    os.remove(f)
164787c131baSJan Høydahl                index = 0
164887c131baSJan Høydahl                log_folder = self.logs_prefix if len(commands) > 1 else None
164987c131baSJan Høydahl                for cmd in commands:
165087c131baSJan Høydahl                    index += 1
165187c131baSJan Høydahl                    if len(commands) > 1:
165287c131baSJan Høydahl                        log_prefix = "%02d_" % index
165387c131baSJan Høydahl                    else:
165487c131baSJan Høydahl                        log_prefix = self.logs_prefix if self.logs_prefix else ''
165587c131baSJan Høydahl                    if not log_prefix[-1:] == '_':
165687c131baSJan Høydahl                        log_prefix += "_"
165787c131baSJan Høydahl                    cwd = root
165887c131baSJan Høydahl                    if cmd.cwd:
165987c131baSJan Høydahl                        cwd = os.path.join(root, cmd.cwd)
166087c131baSJan Høydahl                    folder_prefix = ''
166187c131baSJan Høydahl                    if cmd.cwd:
166287c131baSJan Høydahl                        folder_prefix = cmd.cwd + "_"
166387c131baSJan Høydahl                    if self.confirm_each_command is False or len(commands) == 1 or ask_yes_no("Shall I run '%s' in folder '%s'" % (cmd, cwd)):
166487c131baSJan Høydahl                        if self.confirm_each_command is False:
166587c131baSJan Høydahl                            print("------------\nRunning '%s' in folder '%s'" % (cmd, cwd))
166687c131baSJan Høydahl                        logfilename = cmd.logfile
166787c131baSJan Høydahl                        logfile = None
166887c131baSJan Høydahl                        cmd_to_run = "%s%s" % ("echo Dry run, command is: " if dry_run else "", cmd.get_cmd())
166987c131baSJan Høydahl                        if cmd.redirect:
167087c131baSJan Høydahl                            try:
167187c131baSJan Høydahl                                out = run(cmd_to_run, cwd=cwd)
167287c131baSJan Høydahl                                mode = 'a' if cmd.redirect_append is True else 'w'
167387c131baSJan Høydahl                                with open(os.path.join(root, cwd, cmd.get_redirect()), mode) as outfile:
167487c131baSJan Høydahl                                    outfile.write(out)
167587c131baSJan Høydahl                                    outfile.flush()
167687c131baSJan Høydahl                                print("Wrote %s bytes to redirect file %s" % (len(out), cmd.get_redirect()))
167787c131baSJan Høydahl                            except Exception as e:
167887c131baSJan Høydahl                                print("Command %s failed: %s" % (cmd_to_run, e))
167987c131baSJan Høydahl                                success = False
168087c131baSJan Høydahl                                break
168187c131baSJan Høydahl                        else:
168287c131baSJan Høydahl                            if not cmd.stdout:
168387c131baSJan Høydahl                                if not log_folder:
168487c131baSJan Høydahl                                    log_folder = os.path.join(state.get_rc_folder(), "logs")
168587c131baSJan Høydahl                                elif not os.path.isabs(log_folder):
168687c131baSJan Høydahl                                    log_folder = os.path.join(state.get_rc_folder(), "logs", log_folder)
168787c131baSJan Høydahl                                if not logfilename:
168887c131baSJan Høydahl                                    logfilename = "%s.log" % re.sub(r"\W", "_", cmd.get_cmd())
168987c131baSJan Høydahl                                logfile = os.path.join(log_folder, "%s%s%s" % (log_prefix, folder_prefix, logfilename))
169087c131baSJan Høydahl                                if cmd.tee:
169187c131baSJan Høydahl                                    print("Output of command will be printed (logfile=%s)" % logfile)
169287c131baSJan Høydahl                                elif cmd.live:
169387c131baSJan Høydahl                                    print("Output will be shown live byte by byte")
169487c131baSJan Høydahl                                    logfile = None
169587c131baSJan Høydahl                                else:
169687c131baSJan Høydahl                                    print("Wait until command completes... Full log in %s\n" % logfile)
169787c131baSJan Høydahl                                if cmd.comment:
169887c131baSJan Høydahl                                    print("# %s\n" % cmd.get_comment())
169987c131baSJan Høydahl                            start_time = time.time()
170087c131baSJan Høydahl                            returncode = run_with_log_tail(cmd_to_run, cwd, logfile=logfile, tee=cmd.tee, tail_lines=25,
170187c131baSJan Høydahl                                                           live=cmd.live, shell=cmd.shell)
170287c131baSJan Høydahl                            elapsed = time.time() - start_time
170387c131baSJan Høydahl                            if not returncode == 0:
170487c131baSJan Høydahl                                if cmd.should_fail:
170587c131baSJan Høydahl                                    print("Command failed, which was expected")
170687c131baSJan Høydahl                                    success = True
170787c131baSJan Høydahl                                else:
170887c131baSJan Høydahl                                    print("WARN: Command %s returned with error" % cmd.get_cmd())
170987c131baSJan Høydahl                                    success = False
171087c131baSJan Høydahl                                    break
171187c131baSJan Høydahl                            else:
171287c131baSJan Høydahl                                if cmd.should_fail and not dry_run:
171387c131baSJan Høydahl                                    print("Expected command to fail, but it succeeded.")
171487c131baSJan Høydahl                                    success = False
171587c131baSJan Høydahl                                    break
171687c131baSJan Høydahl                                else:
171787c131baSJan Høydahl                                    if elapsed > 30:
171887c131baSJan Høydahl                                        print("Command completed in %s seconds" % elapsed)
171987c131baSJan Høydahl            if not success:
172087c131baSJan Høydahl                print("WARNING: One or more commands failed, you may want to check the logs")
172187c131baSJan Høydahl            return success
172287c131baSJan Høydahl
172387c131baSJan Høydahl    def get_root_folder(self):
172487c131baSJan Høydahl        return self.jinjaify(self.root_folder)
172587c131baSJan Høydahl
172687c131baSJan Høydahl    def get_commands_text(self):
172787c131baSJan Høydahl        return self.jinjaify(self.commands_text)
172887c131baSJan Høydahl
172987c131baSJan Høydahl    def get_run_text(self):
173087c131baSJan Høydahl        return self.jinjaify(self.run_text)
173187c131baSJan Høydahl
173287c131baSJan Høydahl    def get_remove_files(self):
173387c131baSJan Høydahl        return self.jinjaify(self.remove_files)
173487c131baSJan Høydahl
173587c131baSJan Høydahl    def get_vars(self):
173687c131baSJan Høydahl        myvars = {}
173787c131baSJan Høydahl        if self.vars:
173887c131baSJan Høydahl            for k in self.vars:
173987c131baSJan Høydahl                val = self.vars[k]
174087c131baSJan Høydahl                if callable(val):
174187c131baSJan Høydahl                    myvars[k] = expand_jinja(val(), vars=myvars)
174287c131baSJan Høydahl                else:
174387c131baSJan Høydahl                    myvars[k] = expand_jinja(val, vars=myvars)
174487c131baSJan Høydahl        return myvars
174587c131baSJan Høydahl
174687c131baSJan Høydahl    def jinjaify(self, data, join=False):
174787c131baSJan Høydahl        if not data:
174887c131baSJan Høydahl            return None
174987c131baSJan Høydahl        v = self.get_vars()
175087c131baSJan Høydahl        if self.todo_id:
175187c131baSJan Høydahl            v.update(state.get_todo_by_id(self.todo_id).get_vars())
175287c131baSJan Høydahl        if isinstance(data, list):
175387c131baSJan Høydahl            if join:
175487c131baSJan Høydahl                return expand_jinja(" ".join(data), v)
175587c131baSJan Høydahl            else:
175687c131baSJan Høydahl                res = []
175787c131baSJan Høydahl                for rf in data:
175887c131baSJan Høydahl                    res.append(expand_jinja(rf, v))
175987c131baSJan Høydahl                return res
176087c131baSJan Høydahl        else:
176187c131baSJan Høydahl            return expand_jinja(data, v)
176287c131baSJan Høydahl
176387c131baSJan Høydahl
176487c131baSJan Høydahldef abbreviate_homedir(line):
176587c131baSJan Høydahl    if is_windows():
176687c131baSJan Høydahl        if 'HOME' in os.environ:
176787c131baSJan Høydahl            return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1%HOME%", line)
176887c131baSJan Høydahl        elif 'USERPROFILE' in os.environ:
176987c131baSJan Høydahl            return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1%USERPROFILE%", line)
177087c131baSJan Høydahl    else:
177187c131baSJan Høydahl        return re.sub(r'([^/]|\b)%s' % os.path.expanduser('~'), "\\1~", line)
177287c131baSJan Høydahl
177387c131baSJan Høydahl
177487c131baSJan Høydahlclass Command(SecretYamlObject):
177587c131baSJan Høydahl    yaml_tag = u'!Command'
177687c131baSJan Høydahl    hidden_fields = ['todo_id']
177787c131baSJan Høydahl    def __init__(self, cmd, cwd=None, stdout=None, logfile=None, tee=None, live=None, comment=None, vars=None,
177887c131baSJan Høydahl                 todo_id=None, should_fail=None, redirect=None, redirect_append=None, shell=None):
177987c131baSJan Høydahl        self.cmd = cmd
178087c131baSJan Høydahl        self.cwd = cwd
178187c131baSJan Høydahl        self.comment = comment
178287c131baSJan Høydahl        self.logfile = logfile
178387c131baSJan Høydahl        self.vars = vars
178487c131baSJan Høydahl        self.tee = tee
178587c131baSJan Høydahl        self.live = live
178687c131baSJan Høydahl        self.stdout = stdout
178787c131baSJan Høydahl        self.should_fail = should_fail
178887c131baSJan Høydahl        self.shell = shell
178987c131baSJan Høydahl        self.todo_id = todo_id
179087c131baSJan Høydahl        self.redirect_append = redirect_append
179187c131baSJan Høydahl        self.redirect = redirect
179287c131baSJan Høydahl        if tee and stdout:
179387c131baSJan Høydahl            self.stdout = None
179487c131baSJan Høydahl            print("Command %s specifies 'tee' and 'stdout', using only 'tee'" % self.cmd)
179587c131baSJan Høydahl        if live and stdout:
179687c131baSJan Høydahl            self.stdout = None
179787c131baSJan Høydahl            print("Command %s specifies 'live' and 'stdout', using only 'live'" % self.cmd)
179887c131baSJan Høydahl        if live and tee:
179987c131baSJan Høydahl            self.tee = None
180087c131baSJan Høydahl            print("Command %s specifies 'tee' and 'live', using only 'live'" % self.cmd)
180187c131baSJan Høydahl        if redirect and (tee or stdout or live):
180287c131baSJan Høydahl            self.tee = self.stdout = self.live = None
180387c131baSJan Høydahl            print("Command %s specifies 'redirect' and other out options at the same time. Using redirect only" % self.cmd)
180487c131baSJan Høydahl
180587c131baSJan Høydahl    @classmethod
180687c131baSJan Høydahl    def from_yaml(cls, loader, node):
180787c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
180887c131baSJan Høydahl        return Command(**fields)
180987c131baSJan Høydahl
181087c131baSJan Høydahl    def get_comment(self):
181187c131baSJan Høydahl        return self.jinjaify(self.comment)
181287c131baSJan Høydahl
181387c131baSJan Høydahl    def get_redirect(self):
181487c131baSJan Høydahl        return self.jinjaify(self.redirect)
181587c131baSJan Høydahl
181687c131baSJan Høydahl    def get_cmd(self):
181787c131baSJan Høydahl        return self.jinjaify(self.cmd, join=True)
181887c131baSJan Høydahl
181987c131baSJan Høydahl    def get_vars(self):
182087c131baSJan Høydahl        myvars = {}
182187c131baSJan Høydahl        if self.vars:
182287c131baSJan Høydahl            for k in self.vars:
182387c131baSJan Høydahl                val = self.vars[k]
182487c131baSJan Høydahl                if callable(val):
182587c131baSJan Høydahl                    myvars[k] = expand_jinja(val(), vars=myvars)
182687c131baSJan Høydahl                else:
182787c131baSJan Høydahl                    myvars[k] = expand_jinja(val, vars=myvars)
182887c131baSJan Høydahl        return myvars
182987c131baSJan Høydahl
183087c131baSJan Høydahl    def __str__(self):
183187c131baSJan Høydahl        return self.get_cmd()
183287c131baSJan Høydahl
183387c131baSJan Høydahl    def jinjaify(self, data, join=False):
183487c131baSJan Høydahl        v = self.get_vars()
183587c131baSJan Høydahl        if self.todo_id:
183687c131baSJan Høydahl            v.update(state.get_todo_by_id(self.todo_id).get_vars())
183787c131baSJan Høydahl        if isinstance(data, list):
183887c131baSJan Høydahl            if join:
183987c131baSJan Høydahl                return expand_jinja(" ".join(data), v)
184087c131baSJan Høydahl            else:
184187c131baSJan Høydahl                res = []
184287c131baSJan Høydahl                for rf in data:
184387c131baSJan Høydahl                    res.append(expand_jinja(rf, v))
184487c131baSJan Høydahl                return res
184587c131baSJan Høydahl        else:
184687c131baSJan Høydahl            return expand_jinja(data, v)
184787c131baSJan Høydahl
184887c131baSJan Høydahl    def display_cmd(self):
184987c131baSJan Høydahl        lines = []
185087c131baSJan Høydahl        pre = post = ''
185187c131baSJan Høydahl        if self.comment:
185287c131baSJan Høydahl            if is_windows():
185387c131baSJan Høydahl                lines.append("REM %s" % self.get_comment())
185487c131baSJan Høydahl            else:
185587c131baSJan Høydahl                lines.append("# %s" % self.get_comment())
185687c131baSJan Høydahl        if self.cwd:
185787c131baSJan Høydahl            lines.append("pushd %s" % self.cwd)
185887c131baSJan Høydahl        redir = "" if self.redirect is None else " %s %s" % (">" if self.redirect_append is None else ">>" , self.get_redirect())
185987c131baSJan Høydahl        line = "%s%s" % (expand_multiline(self.get_cmd(), indent=2), redir)
186087c131baSJan Høydahl        # Print ~ or %HOME% rather than the full expanded homedir path
186187c131baSJan Høydahl        line = abbreviate_homedir(line)
186287c131baSJan Høydahl        lines.append(line)
186387c131baSJan Høydahl        if self.cwd:
186487c131baSJan Høydahl            lines.append("popd")
186587c131baSJan Høydahl        return lines
186687c131baSJan Høydahl
186787c131baSJan Høydahlclass UserInput(SecretYamlObject):
186887c131baSJan Høydahl    yaml_tag = u'!UserInput'
186987c131baSJan Høydahl
187087c131baSJan Høydahl    def __init__(self, name, prompt, type=None):
187187c131baSJan Høydahl        self.type = type
187287c131baSJan Høydahl        self.prompt = prompt
187387c131baSJan Høydahl        self.name = name
187487c131baSJan Høydahl
187587c131baSJan Høydahl    @classmethod
187687c131baSJan Høydahl    def from_yaml(cls, loader, node):
187787c131baSJan Høydahl        fields = loader.construct_mapping(node, deep = True)
187887c131baSJan Høydahl        return UserInput(**fields)
187987c131baSJan Høydahl
188087c131baSJan Høydahl    def run(self, dict=None):
188187c131baSJan Høydahl        correct = False
188287c131baSJan Høydahl        while not correct:
188387c131baSJan Høydahl            try:
188487c131baSJan Høydahl                result = str(input("%s : " % self.prompt))
188587c131baSJan Høydahl                if self.type and self.type == 'int':
188687c131baSJan Høydahl                    result = int(result)
188787c131baSJan Høydahl                correct = True
188887c131baSJan Høydahl            except Exception as e:
188987c131baSJan Høydahl                print("Incorrect input: %s, try again" % e)
189087c131baSJan Høydahl                continue
189187c131baSJan Høydahl            if dict:
189287c131baSJan Høydahl                dict[self.name] = result
189387c131baSJan Høydahl            return result
189487c131baSJan Høydahl
189587c131baSJan Høydahl
189687c131baSJan Høydahldef create_ical(todo):
189787c131baSJan Høydahl    if ask_yes_no("Do you want to add a Calendar reminder for the close vote time?"):
189887c131baSJan Høydahl        c = Calendar()
189987c131baSJan Høydahl        e = Event()
1900674b66ddSJan Høydahl        e.name = "Lucene %s vote ends" % state.release_version
190187c131baSJan Høydahl        e.begin = vote_close_72h_date()
190287c131baSJan Høydahl        e.description = "Remember to sum up votes and continue release :)"
190387c131baSJan Høydahl        c.events.add(e)
190487c131baSJan Høydahl        ics_file = os.path.join(state.get_rc_folder(), 'vote_end.ics')
190587c131baSJan Høydahl        with open(ics_file, 'w') as my_file:
190687c131baSJan Høydahl            my_file.writelines(c)
190787c131baSJan Høydahl        open_file(ics_file)
190887c131baSJan Høydahl    return True
190987c131baSJan Høydahl
191087c131baSJan Høydahl
191187c131baSJan Høydahltoday = datetime.utcnow().date()
1912d86b473aSJan Høydahlsundays = {(today + timedelta(days=x)): 'Sunday' for x in range(10) if (today + timedelta(days=x)).weekday() == 6}
191387c131baSJan Høydahly = datetime.utcnow().year
191487c131baSJan Høydahlyears = [y, y+1]
191587c131baSJan Høydahlnon_working = holidays.CA(years=years) + holidays.US(years=years) + holidays.England(years=years) \
1916d86b473aSJan Høydahl              + holidays.DE(years=years) + holidays.NO(years=years) + holidays.IND(years=years) + holidays.RU(years=years)
191787c131baSJan Høydahl
191887c131baSJan Høydahl
191987c131baSJan Høydahldef vote_close_72h_date():
1920d86b473aSJan Høydahl    # Voting open at least 72 hours according to ASF policy
1921d86b473aSJan Høydahl    return datetime.utcnow() + timedelta(hours=73)
1922d86b473aSJan Høydahl
1923d86b473aSJan Høydahl
1924d86b473aSJan Høydahldef vote_close_72h_holidays():
1925d86b473aSJan Høydahl    days = 0
192687c131baSJan Høydahl    day_offset = -1
1927d86b473aSJan Høydahl    holidays = []
1928d86b473aSJan Høydahl    # Warn RM about major holidays coming up that should perhaps extend the voting deadline
1929d86b473aSJan Høydahl    # Warning will be given for Sunday or a public holiday observed by 3 or more [CA, US, EN, DE, NO, IND, RU]
1930d86b473aSJan Høydahl    while days < 3:
193187c131baSJan Høydahl        day_offset += 1
193287c131baSJan Høydahl        d = today + timedelta(days=day_offset)
1933d86b473aSJan Høydahl        if not (d in sundays or (d in non_working and len(non_working[d]) >= 2)):
1934d86b473aSJan Høydahl            days += 1
1935d86b473aSJan Høydahl        else:
1936d86b473aSJan Høydahl            if d in sundays:
1937d86b473aSJan Høydahl                holidays.append("%s (Sunday)" % d)
1938d86b473aSJan Høydahl            else:
1939d86b473aSJan Høydahl                holidays.append("%s (%s)" % (d, non_working[d]))
1940d86b473aSJan Høydahl    return holidays if len(holidays) > 0 else None
194187c131baSJan Høydahl
194287c131baSJan Høydahl
1943329e7c7bSJan Høydahldef prepare_announce_lucene(todo):
1944329e7c7bSJan Høydahl    if not os.path.exists(lucene_news_file):
194587c131baSJan Høydahl        lucene_text = expand_jinja("(( template=announce_lucene ))")
1946329e7c7bSJan Høydahl        with open(lucene_news_file, 'w') as fp:
194787c131baSJan Høydahl            fp.write(lucene_text)
194887c131baSJan Høydahl        # print("Wrote Lucene announce draft to %s" % lucene_news_file)
1949329e7c7bSJan Høydahl    else:
1950329e7c7bSJan Høydahl        print("Draft already exist, not re-generating")
1951329e7c7bSJan Høydahl    return True
195287c131baSJan Høydahl
195387c131baSJan Høydahl
1954f38c4012SJan Høydahldef check_artifacts_available(todo):
1955f38c4012SJan Høydahl  try:
1956f38c4012SJan Høydahl    cdnUrl = expand_jinja("https://dlcdn.apache.org/lucene/java/{{ release_version }}/lucene-{{ release_version }}-src.tgz.asc")
1957f38c4012SJan Høydahl    load(cdnUrl)
1958f38c4012SJan Høydahl    print("Found %s" % cdnUrl)
1959f38c4012SJan Høydahl  except Exception as e:
1960f38c4012SJan Høydahl    print("Could not fetch %s (%s)" % (cdnUrl, e))
1961f38c4012SJan Høydahl    return False
1962f38c4012SJan Høydahl
1963f38c4012SJan Høydahl  try:
1964f38c4012SJan Høydahl    mavenUrl = expand_jinja("https://repo1.maven.org/maven2/org/apache/lucene/lucene-core/{{ release_version }}/lucene-core-{{ release_version }}.pom.asc")
1965f38c4012SJan Høydahl    load(mavenUrl)
1966f38c4012SJan Høydahl    print("Found %s" % mavenUrl)
1967f38c4012SJan Høydahl  except Exception as e:
1968f38c4012SJan Høydahl    print("Could not fetch %s (%s)" % (mavenUrl, e))
1969f38c4012SJan Høydahl    return False
1970f38c4012SJan Høydahl
1971f38c4012SJan Høydahl  return True
1972f38c4012SJan Høydahl
197387c131baSJan Høydahldef set_java_home(version):
197487c131baSJan Høydahl    os.environ['JAVA_HOME'] = state.get_java_home_for_version(version)
197587c131baSJan Høydahl    os.environ['JAVACMD'] = state.get_java_cmd_for_version(version)
197687c131baSJan Høydahl
197787c131baSJan Høydahl
1978329e7c7bSJan Høydahldef load_lines(file, from_line=0):
197987c131baSJan Høydahl    if os.path.exists(file):
198087c131baSJan Høydahl        with open(file, 'r') as fp:
1981329e7c7bSJan Høydahl            return fp.readlines()[from_line:]
198287c131baSJan Høydahl    else:
1983329e7c7bSJan Høydahl        return ["<Please paste the announcement text here>\n"]
198487c131baSJan Høydahl
198587c131baSJan Høydahl
198687c131baSJan Høydahlif __name__ == '__main__':
198787c131baSJan Høydahl    try:
198887c131baSJan Høydahl        main()
198987c131baSJan Høydahl    except KeyboardInterrupt:
199087c131baSJan Høydahl        print('Keyboard interrupt...exiting')
1991