Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: releaseAutomation.py

Issue 29611593: Issue 5996 - Release consistent versions across WebExtensions (Closed) Base URL: https://codereview.adblockplus.org/29609559/
Patch Set: Refactoring release decision Created Nov. 23, 2017, 11:57 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « packagerChrome.py ('k') | tox.ini » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # This Source Code Form is subject to the terms of the Mozilla Public 1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this 2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 4
5 from __future__ import print_function 5 from __future__ import print_function
6 6
7 import os 7 import os
8 import operator
8 import re 9 import re
9 import codecs 10 import codecs
11 import logging
10 import subprocess 12 import subprocess
13 import sys
11 import tarfile 14 import tarfile
12 import json 15 import json
13 16
14 from packager import readMetadata, getDefaultFileName 17 from packager import readMetadata, getDefaultFileName, get_extension
18 from localeTools import read_locale_config
19
20 SOURCE_ARCHIVE = 'adblockplus-{}-source.tgz'
15 21
16 22
17 def get_dependencies(prefix, repos): 23 def get_dependencies(prefix, repos):
18 from ensure_dependencies import read_deps, safe_join 24 from ensure_dependencies import read_deps, safe_join
19 repo = repos[prefix] 25 repo = repos[prefix]
20 deps = read_deps(repo) 26 deps = read_deps(repo)
21 if deps: 27 if deps:
22 for subpath in deps: 28 for subpath in deps:
23 if subpath.startswith('_'): 29 if subpath.startswith('_'):
24 continue 30 continue
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after
106 """Run repository-checks in order to bail out early if necessary""" 112 """Run repository-checks in order to bail out early if necessary"""
107 if repo_has_uncommitted(): 113 if repo_has_uncommitted():
108 return False 114 return False
109 if repo_has_incoming(*repo_paths): 115 if repo_has_incoming(*repo_paths):
110 return False 116 return False
111 if repo_has_outgoing(): 117 if repo_has_outgoing():
112 return continue_with_outgoing() 118 return continue_with_outgoing()
113 return True 119 return True
114 120
115 121
116 def run(baseDir, type, version, keyFile, downloadsRepo): 122 def version_is_op_to(value, orig, op):
Wladimir Palant 2017/11/28 11:01:16 For reference, sitescripts.extensions.utils has an
tlucas 2017/11/28 13:28:43 Done.
117 if not can_safely_release(baseDir, downloadsRepo): 123 """Compare two version numbers (left to right) with a given operator.
118 print('Aborting release.')
119 return 1
120 124
121 if type == 'edge': 125 If a passed version contains non-numeric values, the comparison is always
122 import buildtools.packagerEdge as packager 126 False.
123 elif type == 'chrome': 127 """
124 import buildtools.packagerChrome as packager 128 def prefix_0(a, b):
129 target_len = max((len(a), len(b)))
130 a = '0' * (target_len - len(a)) + a
131 b = '0' * (target_len - len(b)) + b
132 return a, b
125 133
126 # Replace version number in metadata file "manually", ConfigParser will mess 134 try:
127 # up the order of lines. 135 value_digits = [int(v) for v in value.split('.')]
128 metadata = readMetadata(baseDir, type) 136 orig_digits = [int(v) for v in orig.split('.')]
129 with open(metadata.option_source('general', 'version'), 'r+b') as file: 137
130 rawMetadata = file.read() 138 value_dec = ''
139 orig_dec = ''
140
141 if len(value_digits) < len(orig_digits):
142 value_digits += [0] * (len(orig_digits) - len(value_digits))
143 elif len(orig_digits) < len(value_digits):
144 orig_digits += [0] * (len(value_digits) - len(orig_digits))
145
146 for i in range(len(value_digits)):
147 val, orig = prefix_0(str(value_digits[i]), str(orig_digits[i]))
148 value_dec += val
149 orig_dec += orig
150
151 return op(int(value_dec), int(orig_dec))
152
153 except ValueError:
154 pass
Wladimir Palant 2017/11/28 11:01:16 How about only passing in tags that match r'^\d+(\
tlucas 2017/11/28 13:28:43 Done, respectively only passed tags of the form "1
155
156 return False
157
158
159 def release_combination_is_possible(version, platforms, base_dir):
tlucas 2017/11/23 12:05:52 This docstring reflects what was discussed in IRC.
160 """Determine whether a release for the given parameters is possible.
161
162 Examine existing tags in order to find either higher or matching versions.
163 The release is impossible if a) a higher version for a requested platform
164 exists, or if b) a matching version exists and the requested set of
165 platforms differs from what was already released.
166 """
167 considered_tags = [
168 c for c in [
169 t.split('-') for t in
170 subprocess.check_output(
171 ['hg', 'tags', '-R', base_dir, '-q']).split()
172 ] if version_is_op_to(c[0], version, operator.ge)
173 ]
174
175 for tag in considered_tags:
176 if version_is_op_to(tag[0], version, operator.eq):
177 if set(tag[1:]) != set(platforms):
178 return False, ('You have to re-release version {} for exactly '
179 'all of {}').format(version, ', '.join(tag[1:]))
180 if version_is_op_to(tag[0], version, operator.gt):
181 intersect = set(tag[1:]).intersection(platforms)
182 if intersect:
183 return False, ('The higher version {} has already been '
184 'released for {}').format(
185 tag[0], ', '.join(intersect))
186
187 return True, None
188
189
190 def update_metadata(metadata, version):
191 """Replace version number in metadata file "manually".
192
193 The ConfigParser would mess up the order of lines.
194 """
195 with open(metadata.option_source('general', 'version'), 'r+b') as fp:
196 rawMetadata = fp.read()
131 rawMetadata = re.sub( 197 rawMetadata = re.sub(
132 r'^(\s*version\s*=\s*).*', r'\g<1>%s' % version, 198 r'^(\s*version\s*=\s*).*', r'\g<1>%s' % version,
133 rawMetadata, flags=re.I | re.M 199 rawMetadata, flags=re.I | re.M
134 ) 200 )
135 201
136 file.seek(0) 202 fp.seek(0)
137 file.write(rawMetadata) 203 fp.write(rawMetadata)
138 file.truncate() 204 fp.truncate()
139 205
140 # Read extension name from locale data 206
141 default_locale_path = os.path.join('_locales', packager.defaultLocale, 207 def create_build(platform, base_dir, target_path, version, key_file=None):
208 """Create a build for the target platform and version."""
209 if platform == 'edge':
210 import buildtools.packagerEdge as packager
211 else:
212 import buildtools.packagerChrome as packager
213
214 metadata = readMetadata(base_dir, platform)
215 update_metadata(metadata, version)
216
217 build_path = os.path.join(
218 target_path,
219 getDefaultFileName(metadata, version,
220 get_extension(platform, key_file is not None))
221 )
222
223 packager.createBuild(base_dir, type=platform, outFile=build_path,
224 releaseBuild=True, keyFile=key_file)
225
226 return build_path
227
228
229 def release_commit(base_dir, extension_name, version, platforms):
230 """Create a release commit with a representative message."""
231 subprocess.check_call([
232 'hg', 'commit', '-R', base_dir, '-m',
233 'Noissue - Releasing {} {} for {}'.format(
234 extension_name, version,
235 ', '.join([p.capitalize() for p in platforms]))])
236
237
238 def release_tag(base_dir, tag_name, extension_name):
239 """Create a tag, along with a commit message for that tag."""
240 subprocess.check_call([
241 'hg', 'tag', '-R', base_dir, '-f', tag_name,
242 '-m', 'Noissue - Adding release tag for {} {}'.format(
243 extension_name, tag_name)])
244
245
246 def run(baseDir, platforms, version, keyFile, downloads_repo):
247 if not can_safely_release(baseDir, downloads_repo):
248 print('Aborting release.')
249 return 1
250
251 target_platforms = sorted(platforms)
252 release_identifier = '-'.join([version] + [p for p in target_platforms])
253
254 release_possible, reason = release_combination_is_possible(
255 version, platforms, baseDir)
256
257 if not release_possible:
258 logging.error(reason)
259 return 2
260
261 downloads = []
262 # Read extension name from first provided platform
263 locale_config = read_locale_config(
264 baseDir, target_platforms[0],
265 readMetadata(baseDir, target_platforms[0]))
266 default_locale_path = os.path.join(locale_config['base_path'],
267 locale_config['default_locale'],
142 'messages.json') 268 'messages.json')
143 with open(default_locale_path, 'r') as fp: 269 with open(default_locale_path, 'r') as fp:
144 extensionName = json.load(fp)['name']['message'] 270 extension_name = json.load(fp)['name']['message']
145 271
146 # Now commit the change and tag it 272 for platform in target_platforms:
147 subprocess.check_call(['hg', 'commit', '-R', baseDir, '-m', 'Releasing %s %s ' % (extensionName, version)]) 273 used_key_file = None
148 tag_name = version 274 if platform == 'chrome':
149 if type == 'edge': 275 # Currently, only chrome builds are provided by us as signed
150 tag_name = '{}-{}'.format(tag_name, type) 276 # packages. Create an unsigned package in base_dir which should be
151 subprocess.check_call(['hg', 'tag', '-R', baseDir, '-f', tag_name]) 277 # uploaded to the Chrome Web Store
278 create_build(platform, baseDir, baseDir, version)
279 used_key_file = keyFile
152 280
153 # Create a release build 281 downloads.append(
154 downloads = [] 282 create_build(platform, baseDir, downloads_repo, version,
155 if type == 'chrome': 283 used_key_file)
156 # Create both signed and unsigned Chrome builds (the latter for Chrome W eb Store). 284 )
157 buildPath = os.path.join(downloadsRepo, getDefaultFileName(metadata, ver sion, 'crx'))
158 packager.createBuild(baseDir, type=type, outFile=buildPath, releaseBuild =True, keyFile=keyFile)
159 downloads.append(buildPath)
160 285
161 buildPathUnsigned = os.path.join(baseDir, getDefaultFileName(metadata, v ersion, 'zip')) 286 # Only create one commit, one tag and one source archive for all
162 packager.createBuild(baseDir, type=type, outFile=buildPathUnsigned, rele aseBuild=True, keyFile=None) 287 # platforms
163 elif type == 'edge': 288 archive_path = os.path.join(
164 # We only offer the Edge extension for use through the Windows Store 289 downloads_repo,
165 buildPath = os.path.join(downloadsRepo, getDefaultFileName(metadata, ver sion, 'appx')) 290 'adblockplus-{}-source.tgz'.format(release_identifier),
166 packager.createBuild(baseDir, type=type, outFile=buildPath, releaseBuild =True) 291 )
167 downloads.append(buildPath) 292 create_sourcearchive(baseDir, archive_path)
293 downloads.append(archive_path)
168 294
169 # Create source archive 295 release_commit(baseDir, extension_name, version, target_platforms)
170 archivePath = os.path.splitext(buildPath)[0] + '-source.tgz' 296 release_tag(baseDir, release_identifier, extension_name)
171 create_sourcearchive(baseDir, archivePath)
172 downloads.append(archivePath)
173 297
174 # Now add the downloads and commit 298 # Now add the downloads and commit
175 subprocess.check_call(['hg', 'add', '-R', downloadsRepo] + downloads) 299 subprocess.check_call(['hg', 'add', '-R', downloads_repo] + downloads)
176 subprocess.check_call(['hg', 'commit', '-R', downloadsRepo, '-m', 'Releasing %s %s' % (extensionName, version)]) 300 release_commit(downloads_repo, extension_name, version, target_platforms)
177 301
178 # Push all changes 302 # Push all changes
179 subprocess.check_call(['hg', 'push', '-R', baseDir]) 303 subprocess.check_call(['hg', 'push', '-R', baseDir])
180 subprocess.check_call(['hg', 'push', '-R', downloadsRepo]) 304 subprocess.check_call(['hg', 'push', '-R', downloads_repo])
OLDNEW
« no previous file with comments | « packagerChrome.py ('k') | tox.ini » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld