compose/script/test/versions.py

163 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python
"""
Query the github API for the git tags of a project, and return a list of
version tags for recent releases, or the default release.
The default release is the most recent non-RC version.
Recent is a list of unique major.minor versions, where each is the most
recent version in the series.
For example, if the list of versions is:
1.8.0-rc2
1.8.0-rc1
1.7.1
1.7.0
1.7.0-rc1
1.6.2
1.6.1
`default` would return `1.7.1` and
`recent -n 3` would return `1.8.0-rc2 1.7.1 1.6.2`
"""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
import argparse
import itertools
import operator
import sys
from collections import namedtuple
import requests
GITHUB_API = 'https://api.github.com/repos'
class Version(namedtuple('_Version', 'major minor patch rc edition')):
@classmethod
def parse(cls, version):
edition = None
version = version.lstrip('v')
version, _, rc = version.partition('-')
if rc:
if 'rc' not in rc:
edition = rc
rc = None
elif '-' in rc:
edition, rc = rc.split('-')
major, minor, patch = version.split('.', 3)
return cls(major, minor, patch, rc, edition)
@property
def major_minor(self):
return self.major, self.minor
@property
def order(self):
"""Return a representation that allows this object to be sorted
correctly with the default comparator.
"""
# rc releases should appear before official releases
rc = (0, self.rc) if self.rc else (1, )
return (int(self.major), int(self.minor), int(self.patch)) + rc
def __str__(self):
rc = '-{}'.format(self.rc) if self.rc else ''
edition = '-{}'.format(self.edition) if self.edition else ''
return '.'.join(map(str, self[:3])) + edition + rc
def group_versions(versions):
"""Group versions by `major.minor` releases.
Example:
>>> group_versions([
Version(1, 0, 0),
Version(2, 0, 0, 'rc1'),
Version(2, 0, 0),
Version(2, 1, 0),
])
[
[Version(1, 0, 0)],
[Version(2, 0, 0), Version(2, 0, 0, 'rc1')],
[Version(2, 1, 0)],
]
"""
return list(
list(releases)
for _, releases
in itertools.groupby(versions, operator.attrgetter('major_minor'))
)
def get_latest_versions(versions, num=1):
"""Return a list of the most recent versions for each major.minor version
group.
"""
versions = group_versions(versions)
num = min(len(versions), num)
return [versions[index][0] for index in range(num)]
def get_default(versions):
"""Return a :class:`Version` for the latest non-rc version."""
for version in versions:
if not version.rc:
return version
def get_versions(tags):
for tag in tags:
try:
yield Version.parse(tag['name'])
except ValueError:
print("Skipping invalid tag: {name}".format(**tag), file=sys.stderr)
def get_github_releases(projects):
"""Query the Github API for a list of version tags and return them in
sorted order.
See https://developer.github.com/v3/repos/#list-tags
"""
versions = []
for project in projects:
url = '{}/{}/tags'.format(GITHUB_API, project)
response = requests.get(url)
response.raise_for_status()
versions.extend(get_versions(response.json()))
return sorted(versions, reverse=True, key=operator.attrgetter('order'))
def parse_args(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('project', help="Github project name (ex: docker/docker)")
parser.add_argument('command', choices=['recent', 'default'])
parser.add_argument('-n', '--num', type=int, default=2,
help="Number of versions to return from `recent`")
return parser.parse_args(argv)
def main(argv=None):
args = parse_args(argv)
versions = get_github_releases(args.project.split(','))
if args.command == 'recent':
print(' '.join(map(str, get_latest_versions(versions, args.num))))
elif args.command == 'default':
print(get_default(versions))
else:
raise ValueError("Unknown command {}".format(args.command))
if __name__ == "__main__":
main()