#!/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()