| #!/usr/bin/env vpython3 |
| # Copyright 2020 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Script for updating the active milestones for the chromium project. |
| |
| To activate a new chromium branch, run the following from the root of |
| the repo (where MM is the milestone number and BBBB is the branch |
| number): |
| ``` |
| scripts/chromium/milestones.py activate --milestone MM --branch BBBB |
| ./main.star |
| ``` |
| |
| To deactivate a chromium branch, run the following from the root of the |
| repo (where MM is the milestone number): |
| ``` |
| scripts/chromium/milestones.py deactivate --milestone MM |
| ./main.star |
| ``` |
| |
| Usage: |
| milestones.py activate --milestone XX --branch YYYY |
| milestones.py deactivate --milestone XX |
| """ |
| |
| import argparse |
| import itertools |
| import json |
| import os |
| import re |
| import sys |
| |
| INFRA_CONFIG_DIR = os.path.abspath(os.path.join(__file__, '..', '..')) |
| |
| def parse_args(args=None, *, parser_type=None): |
| parser_type = parser_type or argparse.ArgumentParser |
| parser = parser_type( |
| description='Update the active milestones for the chromium project') |
| parser.set_defaults(func=None) |
| parser.add_argument('--milestones-json', |
| help='Path to the milestones.json file', |
| default=os.path.join(INFRA_CONFIG_DIR, 'milestones.json')) |
| |
| subparsers = parser.add_subparsers() |
| |
| activate_parser = subparsers.add_parser( |
| 'activate', help='Add an additional active milestone') |
| activate_parser.set_defaults(func=activate_cmd) |
| activate_parser.add_argument( |
| '--milestone', |
| required=True, |
| help=('The milestone identifier ' |
| '(e.g. the milestone number for standard release channel)')) |
| activate_parser.add_argument( |
| '--branch', |
| required=True, |
| help='The branch name, must correspond to a ref in refs/branch-heads') |
| |
| deactivate_parser = subparsers.add_parser( |
| 'deactivate', help='Remove an active milestone') |
| deactivate_parser.set_defaults(func=deactivate_cmd) |
| deactivate_parser.add_argument( |
| '--milestone', |
| required=True, |
| help=('The milestone identifier ' |
| '(e.g. the milestone number for standard release channel)')) |
| |
| args = parser.parse_args(args) |
| if args.func is None: |
| parser.error('no sub-command specified') |
| return args |
| |
| class MilestonesException(Exception): |
| pass |
| |
| _NUMBER_RE = re.compile('([0-9]+)') |
| |
| def numeric_sort_key(s): |
| # The capture group in the regex means that the numeric portions are returned, |
| # odd indices will be the numeric portions of the string (the 0th or last |
| # element will be empty if the string starts or ends with a number, |
| # respectively) |
| pieces = _NUMBER_RE.split(s) |
| return [ |
| (int(x), x) if is_numeric else x |
| for x, is_numeric |
| in zip(pieces, itertools.cycle([False, True])) |
| ] |
| |
| def add_milestone(milestones, milestone, branch): |
| if milestone in milestones: |
| raise MilestonesException( |
| f'there is already an active milestone with id {milestone!r}: ' |
| f'{milestones[milestone]}') |
| |
| milestones[milestone] = { |
| 'name': f'm{milestone}', |
| 'project': f'chromium-m{milestone}', |
| 'ref': f'refs/branch-heads/{branch}', |
| } |
| |
| milestones = { |
| k: milestones[k] for k in sorted(milestones, key=numeric_sort_key) |
| } |
| |
| return json.dumps(milestones, indent=4) + '\n' |
| |
| def activate_cmd(args): |
| with open(args.milestones_json) as f: |
| milestones = json.load(f) |
| |
| milestones = add_milestone(milestones, args.milestone, args.branch) |
| |
| with open(args.milestones_json, 'w') as f: |
| f.write(milestones) |
| |
| def remove_milestone(milestones, milestone): |
| if milestone not in milestones: |
| raise MilestonesException( |
| f'{milestone!r} does not refer to an active milestone: ' |
| f'{list(milestones.keys())}') |
| |
| del milestones[milestone] |
| |
| milestones = { |
| k: milestones[k] for k in sorted(milestones, key=numeric_sort_key) |
| } |
| |
| return json.dumps(milestones, indent=4) + '\n' |
| |
| def deactivate_cmd(args): |
| with open(args.milestones_json) as f: |
| milestones = json.load(f) |
| |
| milestones = remove_milestone(milestones, args.milestone) |
| |
| with open(args.milestones_json, 'w') as f: |
| f.write(milestones) |
| |
| def main(): |
| args = parse_args() |
| try: |
| args.func(args) |
| except MilestonesException as e: |
| print(str(e), file=sys.stderr) |
| sys.exit(1) |
| |
| if __name__ == '__main__': |
| main() |