sign-tag.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env python3
  2. # Copyright (C) 2020 The Android Open Source Project
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Helper tool for signing repo release tags correctly.
  16. This is intended to be run only by the official Repo release managers, but it
  17. could be run by people maintaining their own fork of the project.
  18. NB: Check docs/release-process.md for production freeze information.
  19. """
  20. import argparse
  21. import os
  22. import re
  23. import subprocess
  24. import sys
  25. import util
  26. # We currently sign with the old DSA key as it's been around the longest.
  27. # We should transition to RSA by Jun 2020, and ECC by Jun 2021.
  28. KEYID = util.KEYID_DSA
  29. # Regular expression to validate tag names.
  30. RE_VALID_TAG = r'^v([0-9]+[.])+[0-9]+$'
  31. def sign(opts):
  32. """Tag the commit & sign it!"""
  33. # We use ! at the end of the key so that gpg uses this specific key.
  34. # Otherwise it uses the key as a lookup into the overall key and uses the
  35. # default signing key. i.e. It will see that KEYID_RSA is a subkey of
  36. # another key, and use the primary key to sign instead of the subkey.
  37. cmd = ['git', 'tag', '-s', opts.tag, '-u', f'{opts.key}!',
  38. '-m', f'repo {opts.tag}', opts.commit]
  39. key = 'GNUPGHOME'
  40. print('+', f'export {key}="{opts.gpgdir}"')
  41. oldvalue = os.getenv(key)
  42. os.putenv(key, opts.gpgdir)
  43. util.run(opts, cmd)
  44. if oldvalue is None:
  45. os.unsetenv(key)
  46. else:
  47. os.putenv(key, oldvalue)
  48. def check(opts):
  49. """Check the signature."""
  50. util.run(opts, ['git', 'tag', '--verify', opts.tag])
  51. def postmsg(opts):
  52. """Helpful info to show at the end for release manager."""
  53. cmd = ['git', 'rev-parse', 'remotes/origin/stable']
  54. ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
  55. current_release = ret.stdout.strip()
  56. cmd = ['git', 'log', '--format=%h (%aN) %s', '--no-merges',
  57. f'remotes/origin/stable..{opts.tag}']
  58. ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
  59. shortlog = ret.stdout.strip()
  60. print(f"""
  61. Here's the short log since the last release.
  62. {shortlog}
  63. To push release to the public:
  64. git push origin {opts.commit}:stable {opts.tag} -n
  65. NB: People will start upgrading to this version immediately.
  66. To roll back a release:
  67. git push origin --force {current_release}:stable -n
  68. """)
  69. def get_parser():
  70. """Get a CLI parser."""
  71. parser = argparse.ArgumentParser(
  72. description=__doc__,
  73. formatter_class=argparse.RawDescriptionHelpFormatter)
  74. parser.add_argument('-n', '--dry-run',
  75. dest='dryrun', action='store_true',
  76. help='show everything that would be done')
  77. parser.add_argument('--gpgdir',
  78. default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
  79. help='path to dedicated gpg dir with release keys '
  80. '(default: ~/.gnupg/repo/)')
  81. parser.add_argument('-f', '--force', action='store_true',
  82. help='force signing of any tag')
  83. parser.add_argument('--keyid', dest='key',
  84. help='alternative signing key to use')
  85. parser.add_argument('tag',
  86. help='the tag to create (e.g. "v2.0")')
  87. parser.add_argument('commit', default='HEAD', nargs='?',
  88. help='the commit to tag')
  89. return parser
  90. def main(argv):
  91. """The main func!"""
  92. parser = get_parser()
  93. opts = parser.parse_args(argv)
  94. if not os.path.exists(opts.gpgdir):
  95. parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
  96. if not opts.force and not re.match(RE_VALID_TAG, opts.tag):
  97. parser.error(f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; '
  98. 'use --force to sign anyways')
  99. if opts.key:
  100. print(f'Using custom key to sign: {opts.key}')
  101. else:
  102. print('Using official Repo release key to sign')
  103. opts.key = KEYID
  104. util.import_release_key(opts)
  105. sign(opts)
  106. check(opts)
  107. postmsg(opts)
  108. return 0
  109. if __name__ == '__main__':
  110. sys.exit(main(sys.argv[1:]))