summaryrefslogtreecommitdiffstats
path: root/release
diff options
context:
space:
mode:
Diffstat (limited to 'release')
-rw-r--r--release/README.md2
-rwxr-xr-xrelease/sign-launcher.py114
-rwxr-xr-xrelease/sign-tag.py140
-rwxr-xr-xrelease/update-manpages102
-rw-r--r--release/util.py73
5 files changed, 431 insertions, 0 deletions
diff --git a/release/README.md b/release/README.md
new file mode 100644
index 00000000..3b81d532
--- /dev/null
+++ b/release/README.md
@@ -0,0 +1,2 @@
1These are helper tools for managing official releases.
2See the [release process](../docs/release-process.md) document for more details.
diff --git a/release/sign-launcher.py b/release/sign-launcher.py
new file mode 100755
index 00000000..ba5e490c
--- /dev/null
+++ b/release/sign-launcher.py
@@ -0,0 +1,114 @@
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
16"""Helper tool for signing repo launcher scripts correctly.
17
18This is intended to be run only by the official Repo release managers.
19"""
20
21import argparse
22import os
23import subprocess
24import sys
25
26import util
27
28
29def sign(opts):
30 """Sign the launcher!"""
31 output = ''
32 for key in opts.keys:
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 = ['gpg', '--homedir', opts.gpgdir, '-u', f'{key}!', '--batch', '--yes',
38 '--armor', '--detach-sign', '--output', '-', opts.launcher]
39 ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
40 output += ret.stdout
41
42 # Save the combined signatures into one file.
43 with open(f'{opts.launcher}.asc', 'w', encoding='utf-8') as fp:
44 fp.write(output)
45
46
47def check(opts):
48 """Check the signature."""
49 util.run(opts, ['gpg', '--verify', f'{opts.launcher}.asc'])
50
51
52def postmsg(opts):
53 """Helpful info to show at the end for release manager."""
54 print(f"""
55Repo launcher bucket:
56 gs://git-repo-downloads/
57
58To upload this launcher directly:
59 gsutil cp -a public-read {opts.launcher} {opts.launcher}.asc gs://git-repo-downloads/
60
61NB: You probably want to upload it with a specific version first, e.g.:
62 gsutil cp -a public-read {opts.launcher} gs://git-repo-downloads/repo-3.0
63 gsutil cp -a public-read {opts.launcher}.asc gs://git-repo-downloads/repo-3.0.asc
64""")
65
66
67def get_parser():
68 """Get a CLI parser."""
69 parser = argparse.ArgumentParser(description=__doc__)
70 parser.add_argument('-n', '--dry-run',
71 dest='dryrun', action='store_true',
72 help='show everything that would be done')
73 parser.add_argument('--gpgdir',
74 default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
75 help='path to dedicated gpg dir with release keys '
76 '(default: ~/.gnupg/repo/)')
77 parser.add_argument('--keyid', dest='keys', default=[], action='append',
78 help='alternative signing keys to use')
79 parser.add_argument('launcher',
80 default=os.path.join(util.TOPDIR, 'repo'), nargs='?',
81 help='the launcher script to sign')
82 return parser
83
84
85def main(argv):
86 """The main func!"""
87 parser = get_parser()
88 opts = parser.parse_args(argv)
89
90 if not os.path.exists(opts.gpgdir):
91 parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
92 if not os.path.exists(opts.launcher):
93 parser.error(f'launcher does not exist: {opts.launcher}')
94
95 opts.launcher = os.path.relpath(opts.launcher)
96 print(f'Signing "{opts.launcher}" launcher script and saving to '
97 f'"{opts.launcher}.asc"')
98
99 if opts.keys:
100 print(f'Using custom keys to sign: {" ".join(opts.keys)}')
101 else:
102 print('Using official Repo release keys to sign')
103 opts.keys = [util.KEYID_DSA, util.KEYID_RSA, util.KEYID_ECC]
104 util.import_release_key(opts)
105
106 sign(opts)
107 check(opts)
108 postmsg(opts)
109
110 return 0
111
112
113if __name__ == '__main__':
114 sys.exit(main(sys.argv[1:]))
diff --git a/release/sign-tag.py b/release/sign-tag.py
new file mode 100755
index 00000000..605437c9
--- /dev/null
+++ b/release/sign-tag.py
@@ -0,0 +1,140 @@
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
16"""Helper tool for signing repo release tags correctly.
17
18This is intended to be run only by the official Repo release managers, but it
19could be run by people maintaining their own fork of the project.
20
21NB: Check docs/release-process.md for production freeze information.
22"""
23
24import argparse
25import os
26import re
27import subprocess
28import sys
29
30import util
31
32
33# We currently sign with the old DSA key as it's been around the longest.
34# We should transition to RSA by Jun 2020, and ECC by Jun 2021.
35KEYID = util.KEYID_DSA
36
37# Regular expression to validate tag names.
38RE_VALID_TAG = r'^v([0-9]+[.])+[0-9]+$'
39
40
41def sign(opts):
42 """Tag the commit & sign it!"""
43 # We use ! at the end of the key so that gpg uses this specific key.
44 # Otherwise it uses the key as a lookup into the overall key and uses the
45 # default signing key. i.e. It will see that KEYID_RSA is a subkey of
46 # another key, and use the primary key to sign instead of the subkey.
47 cmd = ['git', 'tag', '-s', opts.tag, '-u', f'{opts.key}!',
48 '-m', f'repo {opts.tag}', opts.commit]
49
50 key = 'GNUPGHOME'
51 print('+', f'export {key}="{opts.gpgdir}"')
52 oldvalue = os.getenv(key)
53 os.putenv(key, opts.gpgdir)
54 util.run(opts, cmd)
55 if oldvalue is None:
56 os.unsetenv(key)
57 else:
58 os.putenv(key, oldvalue)
59
60
61def check(opts):
62 """Check the signature."""
63 util.run(opts, ['git', 'tag', '--verify', opts.tag])
64
65
66def postmsg(opts):
67 """Helpful info to show at the end for release manager."""
68 cmd = ['git', 'rev-parse', 'remotes/origin/stable']
69 ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
70 current_release = ret.stdout.strip()
71
72 cmd = ['git', 'log', '--format=%h (%aN) %s', '--no-merges',
73 f'remotes/origin/stable..{opts.tag}']
74 ret = util.run(opts, cmd, encoding='utf-8', stdout=subprocess.PIPE)
75 shortlog = ret.stdout.strip()
76
77 print(f"""
78Here's the short log since the last release.
79{shortlog}
80
81To push release to the public:
82 git push origin {opts.commit}:stable {opts.tag} -n
83NB: People will start upgrading to this version immediately.
84
85To roll back a release:
86 git push origin --force {current_release}:stable -n
87""")
88
89
90def get_parser():
91 """Get a CLI parser."""
92 parser = argparse.ArgumentParser(
93 description=__doc__,
94 formatter_class=argparse.RawDescriptionHelpFormatter)
95 parser.add_argument('-n', '--dry-run',
96 dest='dryrun', action='store_true',
97 help='show everything that would be done')
98 parser.add_argument('--gpgdir',
99 default=os.path.join(util.HOMEDIR, '.gnupg', 'repo'),
100 help='path to dedicated gpg dir with release keys '
101 '(default: ~/.gnupg/repo/)')
102 parser.add_argument('-f', '--force', action='store_true',
103 help='force signing of any tag')
104 parser.add_argument('--keyid', dest='key',
105 help='alternative signing key to use')
106 parser.add_argument('tag',
107 help='the tag to create (e.g. "v2.0")')
108 parser.add_argument('commit', default='HEAD', nargs='?',
109 help='the commit to tag')
110 return parser
111
112
113def main(argv):
114 """The main func!"""
115 parser = get_parser()
116 opts = parser.parse_args(argv)
117
118 if not os.path.exists(opts.gpgdir):
119 parser.error(f'--gpgdir does not exist: {opts.gpgdir}')
120
121 if not opts.force and not re.match(RE_VALID_TAG, opts.tag):
122 parser.error(f'tag "{opts.tag}" does not match regex "{RE_VALID_TAG}"; '
123 'use --force to sign anyways')
124
125 if opts.key:
126 print(f'Using custom key to sign: {opts.key}')
127 else:
128 print('Using official Repo release key to sign')
129 opts.key = KEYID
130 util.import_release_key(opts)
131
132 sign(opts)
133 check(opts)
134 postmsg(opts)
135
136 return 0
137
138
139if __name__ == '__main__':
140 sys.exit(main(sys.argv[1:]))
diff --git a/release/update-manpages b/release/update-manpages
new file mode 100755
index 00000000..ddbce0cc
--- /dev/null
+++ b/release/update-manpages
@@ -0,0 +1,102 @@
1#!/usr/bin/env python3
2# Copyright (C) 2021 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
16"""Helper tool for generating manual page for all repo commands.
17
18This is intended to be run before every official Repo release.
19"""
20
21from pathlib import Path
22from functools import partial
23import argparse
24import multiprocessing
25import os
26import re
27import shutil
28import subprocess
29import sys
30import tempfile
31
32TOPDIR = Path(__file__).resolve().parent.parent
33MANDIR = TOPDIR.joinpath('man')
34
35# Load repo local modules.
36sys.path.insert(0, str(TOPDIR))
37from git_command import RepoSourceVersion
38import subcmds
39
40def worker(cmd, **kwargs):
41 subprocess.run(cmd, **kwargs)
42
43def main(argv):
44 parser = argparse.ArgumentParser(description=__doc__)
45 opts = parser.parse_args(argv)
46
47 if not shutil.which('help2man'):
48 sys.exit('Please install help2man to continue.')
49
50 # Let repo know we're generating man pages so it can avoid some dynamic
51 # behavior (like probing active number of CPUs). We use a weird name &
52 # value to make it less likely for users to set this var themselves.
53 os.environ['_REPO_GENERATE_MANPAGES_'] = ' indeed! '
54
55 # "repo branch" is an alias for "repo branches".
56 del subcmds.all_commands['branch']
57 (MANDIR / 'repo-branch.1').write_text('.so man1/repo-branches.1')
58
59 version = RepoSourceVersion()
60 cmdlist = [['help2man', '-N', '-n', f'repo {cmd} - manual page for repo {cmd}',
61 '-S', f'repo {cmd}', '-m', 'Repo Manual', f'--version-string={version}',
62 '-o', MANDIR.joinpath(f'repo-{cmd}.1.tmp'), TOPDIR.joinpath('repo'),
63 '-h', f'help {cmd}'] for cmd in subcmds.all_commands]
64 cmdlist.append(['help2man', '-N', '-n', 'repository management tool built on top of git',
65 '-S', 'repo', '-m', 'Repo Manual', f'--version-string={version}',
66 '-o', MANDIR.joinpath('repo.1.tmp'), TOPDIR.joinpath('repo'),
67 '-h', '--help-all'])
68
69 with tempfile.TemporaryDirectory() as tempdir:
70 repo_dir = Path(tempdir) / '.repo'
71 repo_dir.mkdir()
72 (repo_dir / 'repo').symlink_to(TOPDIR)
73
74 # Run all cmd in parallel, and wait for them to finish.
75 with multiprocessing.Pool() as pool:
76 pool.map(partial(worker, cwd=tempdir, check=True), cmdlist)
77
78 regex = (
79 (r'(It was generated by help2man) [0-9.]+', '\g<1>.'),
80 (r'^\.IP\n(.*:)\n', '.SS \g<1>\n'),
81 (r'^\.PP\nDescription', '.SH DETAILS'),
82 )
83 for tmp_path in MANDIR.glob('*.1.tmp'):
84 path = tmp_path.parent / tmp_path.stem
85 old_data = path.read_text() if path.exists() else ''
86
87 data = tmp_path.read_text()
88 tmp_path.unlink()
89
90 for pattern, replacement in regex:
91 data = re.sub(pattern, replacement, data, flags=re.M)
92
93 # If the only thing that changed was the date, don't refresh. This avoids
94 # a lot of noise when only one file actually updates.
95 old_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', old_data, flags=re.M)
96 new_data = re.sub(r'^(\.TH REPO "1" ")([^"]+)', r'\1', data, flags=re.M)
97 if old_data != new_data:
98 path.write_text(data)
99
100
101if __name__ == '__main__':
102 sys.exit(main(sys.argv[1:]))
diff --git a/release/util.py b/release/util.py
new file mode 100644
index 00000000..9d0eb1dc
--- /dev/null
+++ b/release/util.py
@@ -0,0 +1,73 @@
1# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Random utility code for release tools."""
16
17import os
18import re
19import subprocess
20import sys
21
22
23assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
24
25
26TOPDIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
27HOMEDIR = os.path.expanduser('~')
28
29
30# These are the release keys we sign with.
31KEYID_DSA = '8BB9AD793E8E6153AF0F9A4416530D5E920F5C65'
32KEYID_RSA = 'A34A13BE8E76BFF46A0C022DA2E75A824AAB9624'
33KEYID_ECC = 'E1F9040D7A3F6DAFAC897CD3D3B95DA243E48A39'
34
35
36def cmdstr(cmd):
37 """Get a nicely quoted shell command."""
38 ret = []
39 for arg in cmd:
40 if not re.match(r'^[a-zA-Z0-9/_.=-]+$', arg):
41 arg = f'"{arg}"'
42 ret.append(arg)
43 return ' '.join(ret)
44
45
46def run(opts, cmd, check=True, **kwargs):
47 """Helper around subprocess.run to include logging."""
48 print('+', cmdstr(cmd))
49 if opts.dryrun:
50 cmd = ['true', '--'] + cmd
51 try:
52 return subprocess.run(cmd, check=check, **kwargs)
53 except subprocess.CalledProcessError as e:
54 print(f'aborting: {e}', file=sys.stderr)
55 sys.exit(1)
56
57
58def import_release_key(opts):
59 """Import the public key of the official release repo signing key."""
60 # Extract the key from our repo launcher.
61 launcher = getattr(opts, 'launcher', os.path.join(TOPDIR, 'repo'))
62 print(f'Importing keys from "{launcher}" launcher script')
63 with open(launcher, encoding='utf-8') as fp:
64 data = fp.read()
65
66 keys = re.findall(
67 r'\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n[^-]*'
68 r'\n-----END PGP PUBLIC KEY BLOCK-----\n', data, flags=re.M)
69 run(opts, ['gpg', '--import'], input='\n'.join(keys).encode('utf-8'))
70
71 print('Marking keys as fully trusted')
72 run(opts, ['gpg', '--import-ownertrust'],
73 input=f'{KEYID_DSA}:6:\n'.encode('utf-8'))