PageRenderTime 70ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/scripts/release/tag-release.py

https://gitlab.com/astian/build-tools
Python | 278 lines | 213 code | 42 blank | 23 comment | 44 complexity | 350fd4de294d6936d32231a3a44a39d5 MD5 | raw file
  1. #!/usr/bin/env python
  2. import logging
  3. from os import path
  4. from traceback import format_exc
  5. import subprocess
  6. import sys
  7. sys.path.append(path.join(path.dirname(__file__), "../../lib/python"))
  8. logging.basicConfig(
  9. stream=sys.stdout, level=logging.INFO, format="%(message)s")
  10. log = logging.getLogger(__name__)
  11. from util.commands import run_cmd, get_output
  12. from util.hg import mercurial, apply_and_push, update, get_revision, \
  13. make_hg_url, out, BRANCH, get_branches, cleanOutgoingRevs
  14. from util.retry import retry
  15. from build.versions import bumpFile
  16. from release.info import readReleaseConfig, getTags, generateRelbranchName
  17. from release.l10n import getL10nRepositories
  18. HG = "hg.mozilla.org"
  19. DEFAULT_BUILDBOT_CONFIGS_REPO = make_hg_url(HG, 'build/buildbot-configs')
  20. DEFAULT_MAX_PUSH_ATTEMPTS = 10
  21. REQUIRED_CONFIG = ('version', 'appVersion', 'appName', 'productName',
  22. 'buildNumber', 'hgUsername', 'hgSshKey',
  23. 'baseTag', 'l10nRepoPath', 'sourceRepositories',
  24. 'l10nRevisionFile')
  25. REQUIRED_SOURCE_REPO_KEYS = ('path', 'revision')
  26. def getBumpCommitMessage(productName, version):
  27. return 'Automated checkin: version bump for ' + productName + ' ' + \
  28. version + ' release. DONTBUILD CLOSED TREE a=release'
  29. def getTagCommitMessage(revision, tags):
  30. return "Added " + " ".join(tags) + " tag(s) for changeset " + revision + \
  31. ". DONTBUILD CLOSED TREE a=release"
  32. def bump(repo, bumpFiles, versionKey):
  33. for f, info in bumpFiles.iteritems():
  34. fileToBump = path.join(repo, f)
  35. contents = open(fileToBump).read()
  36. # If info[versionKey] is a function, this function will do the bump.
  37. # It takes the old contents as its input to generate the new content.
  38. if callable(info[versionKey]):
  39. newContents = info[versionKey](contents)
  40. else:
  41. newContents = bumpFile(f, contents, info[versionKey])
  42. if contents != newContents:
  43. fh = open(fileToBump, "w")
  44. fh.write(newContents)
  45. fh.close()
  46. def tag(repo, revision, tags, username):
  47. cmd = ['hg', 'tag', '-u', username, '-r', revision,
  48. '-m', getTagCommitMessage(revision, tags), '-f']
  49. cmd.extend(tags)
  50. run_cmd(cmd, cwd=repo)
  51. def tagRepo(config, repo, reponame, revision, tags, bumpFiles, relbranch,
  52. pushAttempts):
  53. remote = make_hg_url(HG, repo)
  54. mercurial(remote, reponame)
  55. def bump_and_tag(repo, attempt, config, relbranch, revision, tags):
  56. # set relbranchChangesets=1 because tag() generates exactly 1 commit
  57. relbranchChangesets = 1
  58. if relbranch in get_branches(reponame):
  59. update(reponame, revision=relbranch)
  60. else:
  61. update(reponame, revision=revision)
  62. run_cmd(['hg', 'branch', relbranch], cwd=reponame)
  63. if len(bumpFiles) > 0:
  64. # Bump files on the relbranch, if necessary
  65. bump(reponame, bumpFiles, 'version')
  66. run_cmd(['hg', 'diff'], cwd=repo)
  67. try:
  68. get_output(['hg', 'commit', '-u', config['hgUsername'],
  69. '-m', getBumpCommitMessage(config['productName'], config['version'])],
  70. cwd=reponame)
  71. relbranchChangesets += 1
  72. except subprocess.CalledProcessError, e:
  73. # We only want to ignore exceptions caused by having nothing to
  74. # commit, which are OK. We still want to raise exceptions caused
  75. # by any other thing.
  76. if e.returncode != 1 or "nothing changed" not in e.output:
  77. raise
  78. # We always want our tags pointing at the tip of the relbranch
  79. # so we need to grab the current revision after we've switched
  80. # branches and bumped versions.
  81. revision = get_revision(reponame)
  82. # Create the desired tags on the relbranch
  83. tag(repo, revision, tags, config['hgUsername'])
  84. # Validate that the repository is only different from the remote in
  85. # ways we expect.
  86. outgoingRevs = out(src=reponame, remote=remote,
  87. ssh_username=config['hgUsername'],
  88. ssh_key=config['hgSshKey'])
  89. if len([r for r in outgoingRevs if r[BRANCH] == relbranch]) != relbranchChangesets:
  90. raise Exception("Incorrect number of revisions on %s" % relbranch)
  91. if len(outgoingRevs) != relbranchChangesets:
  92. raise Exception("Wrong number of outgoing revisions")
  93. pushRepo = make_hg_url(HG, repo, protocol='ssh')
  94. def bump_and_tag_wrapper(r, n):
  95. bump_and_tag(r, n, config, relbranch, revision, tags)
  96. def cleanup_wrapper():
  97. cleanOutgoingRevs(reponame, pushRepo, config['hgUsername'],
  98. config['hgSshKey'])
  99. retry(apply_and_push, cleanup=cleanup_wrapper,
  100. args=(reponame, pushRepo, bump_and_tag_wrapper, pushAttempts),
  101. kwargs=dict(ssh_username=config['hgUsername'],
  102. ssh_key=config['hgSshKey']))
  103. def tagOtherRepo(config, repo, reponame, revision, pushAttempts):
  104. remote = make_hg_url(HG, repo)
  105. mercurial(remote, reponame)
  106. def tagRepo(repo, attempt, config, revision, tags):
  107. # set totalChangesets=1 because tag() generates exactly 1 commit
  108. totalChangesets = 1
  109. # update to the desired revision first, then to the tip of revision's
  110. # branch to avoid new head creation
  111. update(repo, revision=revision)
  112. update(repo)
  113. tag(repo, revision, tags, config['hgUsername'])
  114. outgoingRevs = retry(out, kwargs=dict(src=reponame, remote=remote,
  115. ssh_username=config[
  116. 'hgUsername'],
  117. ssh_key=config['hgSshKey']))
  118. if len(outgoingRevs) != totalChangesets:
  119. raise Exception("Wrong number of outgoing revisions")
  120. pushRepo = make_hg_url(HG, repo, protocol='ssh')
  121. def tag_wrapper(r, n):
  122. tagRepo(r, n, config, revision, tags)
  123. def cleanup_wrapper():
  124. cleanOutgoingRevs(reponame, pushRepo, config['hgUsername'],
  125. config['hgSshKey'])
  126. retry(apply_and_push, cleanup=cleanup_wrapper,
  127. args=(reponame, pushRepo, tag_wrapper, pushAttempts),
  128. kwargs=dict(ssh_username=config['hgUsername'],
  129. ssh_key=config['hgSshKey']))
  130. def validate(options, args):
  131. err = False
  132. config = {}
  133. if not options.configfile:
  134. log.info("Must pass --configfile")
  135. sys.exit(1)
  136. elif not path.exists(path.join('buildbot-configs', options.configfile)):
  137. log.info("%s does not exist!" % options.configfile)
  138. sys.exit(1)
  139. config = readReleaseConfig(
  140. path.join('buildbot-configs', options.configfile))
  141. for key in REQUIRED_CONFIG:
  142. if key not in config:
  143. err = True
  144. log.info("Required item missing in config: %s" % key)
  145. for r in config['sourceRepositories'].values():
  146. for key in REQUIRED_SOURCE_REPO_KEYS:
  147. if key not in r:
  148. err = True
  149. log.info("Missing required key '%s' for '%s'" % (key, r))
  150. if 'otherReposToTag' in config:
  151. if not callable(getattr(config['otherReposToTag'], 'iteritems')):
  152. err = True
  153. log.info("otherReposToTag exists in config but is not a dict")
  154. if err:
  155. sys.exit(1)
  156. # Non-fatal warnings only after this point
  157. if not (options.tag_source or options.tag_l10n or options.tag_other):
  158. log.info("No tag directive specified, defaulting to all")
  159. options.tag_source = True
  160. options.tag_l10n = True
  161. options.tag_other = True
  162. return config
  163. if __name__ == '__main__':
  164. from optparse import OptionParser
  165. import os
  166. parser = OptionParser(__doc__)
  167. parser.set_defaults(
  168. attempts=os.environ.get(
  169. 'MAX_PUSH_ATTEMPTS', DEFAULT_MAX_PUSH_ATTEMPTS),
  170. buildbot_configs=os.environ.get('BUILDBOT_CONFIGS_REPO',
  171. DEFAULT_BUILDBOT_CONFIGS_REPO),
  172. )
  173. parser.add_option("-a", "--push-attempts", dest="attempts",
  174. help="Number of attempts before giving up on pushing")
  175. parser.add_option("-c", "--configfile", dest="configfile",
  176. help="The release config file to use.")
  177. parser.add_option("-b", "--buildbot-configs", dest="buildbot_configs",
  178. help="The place to clone buildbot-configs from")
  179. parser.add_option("-t", "--release-tag", dest="release_tag",
  180. help="Release tag to update buildbot-configs to")
  181. parser.add_option("--tag-source", dest="tag_source",
  182. action="store_true", default=False,
  183. help="Tag the source repo(s).")
  184. parser.add_option("--tag-l10n", dest="tag_l10n",
  185. action="store_true", default=False,
  186. help="Tag the L10n repo(s).")
  187. parser.add_option("--tag-other", dest="tag_other",
  188. action="store_true", default=False,
  189. help="Tag the other repo(s).")
  190. options, args = parser.parse_args()
  191. mercurial(options.buildbot_configs, 'buildbot-configs')
  192. update('buildbot-configs', revision=options.release_tag)
  193. config = validate(options, args)
  194. configDir = path.dirname(options.configfile)
  195. # We generate this upfront to ensure that it's consistent throughout all
  196. # repositories that use it. However, in cases where a relbranch is provided
  197. # for all repositories, it will not be used
  198. generatedRelbranch = generateRelbranchName(config['version'])
  199. if config.get('relbranchPrefix'):
  200. generatedRelbranch = generateRelbranchName(
  201. config['version'], prefix=config['relbranchPrefix'])
  202. tags = getTags(config['baseTag'], config['buildNumber'])
  203. l10nRevisionFile = path.join(
  204. 'buildbot-configs', configDir, config['l10nRevisionFile'])
  205. l10nRepos = getL10nRepositories(
  206. open(l10nRevisionFile).read(), config['l10nRepoPath'])
  207. if options.tag_source:
  208. for repo in config['sourceRepositories'].values():
  209. relbranch = repo['relbranch'] or generatedRelbranch
  210. tagRepo(config, repo['path'], repo['name'], repo['revision'], tags,
  211. repo['bumpFiles'], relbranch, options.attempts)
  212. failed = []
  213. if options.tag_l10n:
  214. for l in sorted(l10nRepos):
  215. info = l10nRepos[l]
  216. relbranch = config['l10nRelbranch'] or generatedRelbranch
  217. try:
  218. tagRepo(config, l, path.basename(l), info['revision'], tags,
  219. info['bumpFiles'], relbranch, options.attempts)
  220. # If en-US tags successfully we'll do our best to tag all of the l10n
  221. # repos, even if some have errors
  222. except:
  223. failed.append((l, format_exc()))
  224. if 'otherReposToTag' in config and options.tag_other:
  225. for repo, revision in config['otherReposToTag'].iteritems():
  226. try:
  227. tagOtherRepo(config, repo, path.basename(repo), revision,
  228. options.attempts)
  229. except:
  230. failed.append((repo, format_exc()))
  231. if len(failed) > 0:
  232. log.info("The following locales failed to tag:")
  233. for l, e in failed:
  234. log.info(" %s" % l)
  235. log.info("%s\n" % e)
  236. sys.exit(1)