versions.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/env python
  2. """
  3. Query the github API for the git tags of a project, and return a list of
  4. version tags for recent releases, or the default release.
  5. The default release is the most recent non-RC version.
  6. Recent is a list of unique major.minor versions, where each is the most
  7. recent version in the series.
  8. For example, if the list of versions is:
  9. 1.8.0-rc2
  10. 1.8.0-rc1
  11. 1.7.1
  12. 1.7.0
  13. 1.7.0-rc1
  14. 1.6.2
  15. 1.6.1
  16. `default` would return `1.7.1` and
  17. `recent -n 3` would return `1.8.0-rc2 1.7.1 1.6.2`
  18. """
  19. import argparse
  20. import itertools
  21. import operator
  22. import sys
  23. from collections import namedtuple
  24. import requests
  25. GITHUB_API = 'https://api.github.com/repos'
  26. STAGES = ['tp', 'beta', 'rc']
  27. class Version(namedtuple('_Version', 'major minor patch stage edition')):
  28. @classmethod
  29. def parse(cls, version):
  30. edition = None
  31. version = version.lstrip('v')
  32. version, _, stage = version.partition('-')
  33. if stage:
  34. if not any(marker in stage for marker in STAGES):
  35. edition = stage
  36. stage = None
  37. elif '-' in stage:
  38. edition, stage = stage.split('-')
  39. major, minor, patch = version.split('.', 3)
  40. return cls(major, minor, patch, stage, edition)
  41. @property
  42. def major_minor(self):
  43. return self.major, self.minor
  44. @property
  45. def order(self):
  46. """Return a representation that allows this object to be sorted
  47. correctly with the default comparator.
  48. """
  49. # non-GA releases should appear before GA releases
  50. # Order: tp -> beta -> rc -> GA
  51. if self.stage:
  52. for st in STAGES:
  53. if st in self.stage:
  54. stage = (STAGES.index(st), self.stage)
  55. break
  56. else:
  57. stage = (len(STAGES),)
  58. return (int(self.major), int(self.minor), int(self.patch)) + stage
  59. def __str__(self):
  60. stage = '-{}'.format(self.stage) if self.stage else ''
  61. edition = '-{}'.format(self.edition) if self.edition else ''
  62. return '.'.join(map(str, self[:3])) + edition + stage
  63. BLACKLIST = [ # List of versions known to be broken and should not be used
  64. Version.parse('18.03.0-ce-rc2'),
  65. ]
  66. def group_versions(versions):
  67. """Group versions by `major.minor` releases.
  68. Example:
  69. >>> group_versions([
  70. Version(1, 0, 0),
  71. Version(2, 0, 0, 'rc1'),
  72. Version(2, 0, 0),
  73. Version(2, 1, 0),
  74. ])
  75. [
  76. [Version(1, 0, 0)],
  77. [Version(2, 0, 0), Version(2, 0, 0, 'rc1')],
  78. [Version(2, 1, 0)],
  79. ]
  80. """
  81. return list(
  82. list(releases)
  83. for _, releases
  84. in itertools.groupby(versions, operator.attrgetter('major_minor'))
  85. )
  86. def get_latest_versions(versions, num=1):
  87. """Return a list of the most recent versions for each major.minor version
  88. group.
  89. """
  90. versions = group_versions(versions)
  91. num = min(len(versions), num)
  92. return [versions[index][0] for index in range(num)]
  93. def get_default(versions):
  94. """Return a :class:`Version` for the latest GA version."""
  95. for version in versions:
  96. if not version.stage:
  97. return version
  98. def get_versions(tags):
  99. for tag in tags:
  100. try:
  101. v = Version.parse(tag['name'])
  102. if v in BLACKLIST:
  103. continue
  104. yield v
  105. except ValueError:
  106. print("Skipping invalid tag: {name}".format(**tag), file=sys.stderr)
  107. def get_github_releases(projects):
  108. """Query the Github API for a list of version tags and return them in
  109. sorted order.
  110. See https://developer.github.com/v3/repos/#list-tags
  111. """
  112. versions = []
  113. for project in projects:
  114. url = '{}/{}/tags'.format(GITHUB_API, project)
  115. response = requests.get(url)
  116. response.raise_for_status()
  117. versions.extend(get_versions(response.json()))
  118. return sorted(versions, reverse=True, key=operator.attrgetter('order'))
  119. def parse_args(argv):
  120. parser = argparse.ArgumentParser(description=__doc__)
  121. parser.add_argument('project', help="Github project name (ex: docker/docker)")
  122. parser.add_argument('command', choices=['recent', 'default'])
  123. parser.add_argument('-n', '--num', type=int, default=2,
  124. help="Number of versions to return from `recent`")
  125. return parser.parse_args(argv)
  126. def main(argv=None):
  127. args = parse_args(argv)
  128. versions = get_github_releases(args.project.split(','))
  129. if args.command == 'recent':
  130. print(' '.join(map(str, get_latest_versions(versions, args.num))))
  131. elif args.command == 'default':
  132. print(get_default(versions))
  133. else:
  134. raise ValueError("Unknown command {}".format(args.command))
  135. if __name__ == "__main__":
  136. main()