| #!/usr/bin/env python3 |
| # Copyright (C) 2020 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Helper tool for signing repo release tags correctly. |
| |
| This is intended to be run only by the official Repo release managers, but it |
| could be run by people maintaining their own fork of the project. |
| |
| NB: Check docs/release-process.md for production freeze information. |
| """ |
| |
| import argparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| import util |
| |
| |
| # We currently sign with the old DSA key as it's been around the longest. |
| # We should transition to RSA by Jun 2020, and ECC by Jun 2021. |
| KEYID = util.KEYID_DSA |
| |
| # Regular expression to validate tag names. |
| RE_VALID_TAG = r'^v([0-9]+[.])+[0-9]+$' |
| |
| |
| def sign(opts): |
| """Tag the commit & sign it!""" |
| # We use ! at the end of the key so that gpg uses this specific key. |
| # Otherwise it uses the key as a lookup into the overall key and uses the |
| # default signing key. i.e. It will see that KEYID_RSA is a subkey of |
| # another key, and use the primary key to sign instead of the subkey. |
| cmd = ['git', 'tag', '-s', opts.tag, '-u', f'{opts.key}!', |
| '-m', f'repo {opts.tag}', opts.commit] |
| |
| key = 'GNUPGHOME' |
| print('+', f'export {key}="{opts.gpgdir}"') |
| oldvalue = os.getenv(key) |
| os.putenv(key, opts.gpgdir) |
| util.run(opts, cmd) |
| if oldvalue is None: |
| os.unsetenv(key) |
| else: |
| os.putenv(key, oldvalue) |
| |
| |
| def check(opts): |
| """Check the signature.""" |
| util.run(opts, ['git', 'tag', '--verify', opts.tag]) |
| |
| |
| def postmsg(opts): |
| """Helpful info to show at the end for release manager.""" |
| cmd = ['git', 'rev-parse', 'remotes/origin/stable'] |
| ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE) |
| current_release = ret.stdout.strip() |
| |
| cmd = ['git', 'log', '--format=%h (%aN) %s', '--no-merges', |
| f'remotes/origin/stable..{opts.tag}'] |
| ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE) |
| shortlog = ret.stdout.strip() |
| |
| print(f""" |
| Here's the short log since the last release. |
| {shortlog} |
| |
| To push release to the public: |
| git push origin {opts.commit}:stable {opts.tag} -n |
| NB: People will start upgrading to this version immediately. |
| |
| To roll back a release: |
| git push origin --force {current_release}:stable -n |
| """) |
| |
| |
| def get_parser(): |
| """Get a CLI parser.""" |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| parser.add_argument('-n', '--dry-run', |
| dest='dryrun', action='store_true', |
| help='show everything that would be done') |
| parser.add_argument('--gpgdir', |
| default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'), |
| help='path to dedicated gpg dir with release keys ' |
| '(default: ~/.gnupg/repo/)') |
| parser.add_argument('-f', '--force', action='store_true', |
| help='force signing of any tag') |
| parser.add_argument('--keyid', dest='key', |
| help='alternative signing key to use') |
| parser.add_argument('tag', |
| help='the tag to create (e.g. "v2.0")') |
| parser.add_argument('commit', default='HEAD', nargs='?', |
| help='the commit to tag') |
| return parser |
| |
| |
| def main(argv): |
| """The main func!""" |
| parser = get_parser() |
| opts = parser.parse_args(argv) |
| |
| if not os.path.exists(opts.gpgdir): |
| parser.error(f'--gpgdir does not exist: {opts.gpgdir}') |
| |
| if not opts.force and not re.match(RE_VALID_TAG, opts.tag): |
| parser.error(f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; ' |
| 'use --force to sign anyways') |
| |
| if opts.key: |
| print(f'Using custom key to sign: {opts.key}') |
| else: |
| print('Using official Repo release key to sign') |
| opts.key = KEYID |
| util.import_release_key(opts) |
| |
| sign(opts) |
| check(opts) |
| postmsg(opts) |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |