Source code

Revision control

Other Tools

1
# -*- coding: utf-8 -*-
2
3
# This Source Code Form is subject to the terms of the Mozilla Public
4
# License, v. 2.0. If a copy of the MPL was not distributed with this
5
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7
from __future__ import absolute_import, print_function, unicode_literals
8
9
import json
10
import os
11
12
from .registry import register_callback_action
13
14
from taskgraph.util.hg import find_hg_revision_push_info
15
from taskgraph.util.taskcluster import get_artifact
16
from taskgraph.util.taskgraph import find_decision_task, find_existing_tasks_from_previous_kinds
17
from taskgraph.util.partials import populate_release_history
18
from taskgraph.util.partners import (
19
fix_partner_config,
20
get_partner_config_by_url,
21
get_partner_url_config,
22
get_token
23
)
24
from taskgraph.taskgraph import TaskGraph
25
from taskgraph.decision import taskgraph_decision
26
from taskgraph.parameters import Parameters
27
from taskgraph.util.attributes import RELEASE_PROMOTION_PROJECTS, release_level
28
29
30
RELEASE_PROMOTION_SIGNOFFS = ('mar-signing', )
31
32
33
def is_release_promotion_available(parameters):
34
return parameters['project'] in RELEASE_PROMOTION_PROJECTS
35
36
37
def get_partner_config(partner_url_config, github_token):
38
partner_config = {}
39
for kind, url in partner_url_config.items():
40
if url:
41
partner_config[kind] = get_partner_config_by_url(url, kind, github_token)
42
return partner_config
43
44
45
def get_signoff_properties():
46
props = {}
47
for signoff in RELEASE_PROMOTION_SIGNOFFS:
48
props[signoff] = {
49
'type': 'string',
50
}
51
return props
52
53
54
def get_required_signoffs(input, parameters):
55
input_signoffs = set(input.get('required_signoffs', []))
56
params_signoffs = set(parameters['required_signoffs'] or [])
57
return sorted(list(input_signoffs | params_signoffs))
58
59
60
def get_signoff_urls(input, parameters):
61
signoff_urls = parameters['signoff_urls']
62
signoff_urls.update(input.get('signoff_urls', {}))
63
return signoff_urls
64
65
66
def get_flavors(graph_config, param):
67
"""
68
Get all flavors with the given parameter enabled.
69
"""
70
promotion_flavors = graph_config['release-promotion']['flavors']
71
return sorted([
72
flavor for (flavor, config) in promotion_flavors.items()
73
if config.get(param, False)
74
])
75
76
77
@register_callback_action(
78
name='release-promotion',
79
title='Release Promotion',
80
symbol='${input.release_promotion_flavor}',
81
description="Promote a release.",
82
generic=False,
83
order=500,
84
context=[],
85
available=is_release_promotion_available,
86
schema=lambda graph_config: {
87
'type': 'object',
88
'properties': {
89
'build_number': {
90
'type': 'integer',
91
'default': 1,
92
'minimum': 1,
93
'title': 'The release build number',
94
'description': ('The release build number. Starts at 1 per '
95
'release version, and increments on rebuild.'),
96
},
97
'do_not_optimize': {
98
'type': 'array',
99
'description': ('Optional: a list of labels to avoid optimizing out '
100
'of the graph (to force a rerun of, say, '
101
'funsize docker-image tasks).'),
102
'items': {
103
'type': 'string',
104
},
105
},
106
'revision': {
107
'type': 'string',
108
'title': 'Optional: revision to promote',
109
'description': ('Optional: the revision to promote. If specified, '
110
'and if neither `pushlog_id` nor `previous_graph_kinds` '
111
'is specified, find the `pushlog_id using the '
112
'revision.'),
113
},
114
'release_promotion_flavor': {
115
'type': 'string',
116
'description': 'The flavor of release promotion to perform.',
117
'enum': sorted(graph_config['release-promotion']['flavors'].keys()),
118
},
119
'rebuild_kinds': {
120
'type': 'array',
121
'description': ('Optional: an array of kinds to ignore from the previous '
122
'graph(s).'),
123
'items': {
124
'type': 'string',
125
},
126
},
127
'previous_graph_ids': {
128
'type': 'array',
129
'description': ('Optional: an array of taskIds of decision or action '
130
'tasks from the previous graph(s) to use to populate '
131
'our `previous_graph_kinds`.'),
132
'items': {
133
'type': 'string',
134
},
135
},
136
'version': {
137
'type': 'string',
138
'description': ('Optional: override the version for release promotion. '
139
"Occasionally we'll land a taskgraph fix in a later "
140
'commit, but want to act on a build from a previous '
141
'commit. If a version bump has landed in the meantime, '
142
'relying on the in-tree version will break things.'),
143
'default': '',
144
},
145
'next_version': {
146
'type': 'string',
147
'description': ('Next version. Required in the following flavors: '
148
'{}'.format(get_flavors(graph_config, 'version-bump'))),
149
'default': '',
150
},
151
152
# Example:
153
# 'partial_updates': {
154
# '38.0': {
155
# 'buildNumber': 1,
156
# 'locales': ['de', 'en-GB', 'ru', 'uk', 'zh-TW']
157
# },
158
# '37.0': {
159
# 'buildNumber': 2,
160
# 'locales': ['de', 'en-GB', 'ru', 'uk']
161
# }
162
# }
163
'partial_updates': {
164
'type': 'object',
165
'description': ('Partial updates. Required in the following flavors: '
166
'{}'.format(get_flavors(graph_config, 'partial-updates'))),
167
'default': {},
168
'additionalProperties': {
169
'type': 'object',
170
'properties': {
171
'buildNumber': {
172
'type': 'number',
173
},
174
'locales': {
175
'type': 'array',
176
'items': {
177
'type': 'string',
178
},
179
},
180
},
181
'required': [
182
'buildNumber',
183
'locales',
184
],
185
'additionalProperties': False,
186
}
187
},
188
'release_eta': {
189
'type': 'string',
190
'default': '',
191
},
192
'release_enable_partners': {
193
'type': 'boolean',
194
'description': 'Toggle for creating partner repacks',
195
},
196
'release_partner_build_number': {
197
'type': 'integer',
198
'default': 1,
199
'minimum': 1,
200
'description': ('The partner build number. This translates to, e.g. '
201
'`v1` in the path. We generally only have to '
202
'bump this on off-cycle partner rebuilds.'),
203
},
204
'release_partners': {
205
'type': 'array',
206
'description': ('A list of partners to repack, or if null or empty then use '
207
'the current full set'),
208
'items': {
209
'type': 'string',
210
}
211
},
212
'release_partner_config': {
213
'type': 'object',
214
'description': 'Partner configuration to use for partner repacks.',
215
'properties': {},
216
'additionalProperties': True,
217
},
218
'release_enable_emefree': {
219
'type': 'boolean',
220
'description': 'Toggle for creating EME-free repacks',
221
},
222
'required_signoffs': {
223
'type': 'array',
224
'description': ('The flavor of release promotion to perform.'),
225
'items': {
226
'enum': RELEASE_PROMOTION_SIGNOFFS,
227
}
228
},
229
'signoff_urls': {
230
'type': 'object',
231
'default': {},
232
'additionalProperties': False,
233
'properties': get_signoff_properties(),
234
},
235
},
236
"required": ['release_promotion_flavor', 'build_number'],
237
}
238
)
239
def release_promotion_action(parameters, graph_config, input, task_group_id, task_id):
240
release_promotion_flavor = input['release_promotion_flavor']
241
promotion_config = graph_config['release-promotion']['flavors'][release_promotion_flavor]
242
release_history = {}
243
product = promotion_config['product']
244
245
next_version = str(input.get('next_version') or '')
246
if promotion_config.get('version-bump', False):
247
# We force str() the input, hence the 'None'
248
if next_version in ['', 'None']:
249
raise Exception(
250
"`next_version` property needs to be provided for `{}` "
251
"target.".format(release_promotion_flavor)
252
)
253
254
if promotion_config.get('partial-updates', False):
255
partial_updates = input.get('partial_updates', {})
256
if not partial_updates and release_level(parameters['project']) == 'production':
257
raise Exception(
258
"`partial_updates` property needs to be provided for `{}`"
259
"target.".format(release_promotion_flavor)
260
)
261
balrog_prefix = product.title()
262
os.environ['PARTIAL_UPDATES'] = json.dumps(partial_updates)
263
release_history = populate_release_history(
264
balrog_prefix, parameters['project'],
265
partial_updates=partial_updates
266
)
267
268
target_tasks_method = promotion_config['target-tasks-method'].format(
269
project=parameters['project']
270
)
271
rebuild_kinds = input.get(
272
'rebuild_kinds', promotion_config.get('rebuild-kinds', [])
273
)
274
do_not_optimize = input.get(
275
'do_not_optimize', promotion_config.get('do-not-optimize', [])
276
)
277
278
# make parameters read-write
279
parameters = dict(parameters)
280
# Build previous_graph_ids from ``previous_graph_ids``, ``pushlog_id``,
281
# or ``revision``.
282
previous_graph_ids = input.get('previous_graph_ids')
283
if not previous_graph_ids:
284
revision = input.get('revision')
285
if not parameters['pushlog_id']:
286
repo_param = '{}head_repository'.format(graph_config['project-repo-param-prefix'])
287
push_info = find_hg_revision_push_info(
288
repository=parameters[repo_param], revision=revision)
289
parameters['pushlog_id'] = push_info['pushid']
290
previous_graph_ids = [find_decision_task(parameters, graph_config)]
291
292
# Download parameters from the first decision task
293
parameters = get_artifact(previous_graph_ids[0], "public/parameters.yml")
294
# Download and combine full task graphs from each of the previous_graph_ids.
295
# Sometimes previous relpro action tasks will add tasks, like partials,
296
# that didn't exist in the first full_task_graph, so combining them is
297
# important. The rightmost graph should take precedence in the case of
298
# conflicts.
299
combined_full_task_graph = {}
300
for graph_id in previous_graph_ids:
301
full_task_graph = get_artifact(graph_id, "public/full-task-graph.json")
302
combined_full_task_graph.update(full_task_graph)
303
_, combined_full_task_graph = TaskGraph.from_json(combined_full_task_graph)
304
parameters['existing_tasks'] = find_existing_tasks_from_previous_kinds(
305
combined_full_task_graph, previous_graph_ids, rebuild_kinds
306
)
307
parameters['do_not_optimize'] = do_not_optimize
308
parameters['target_tasks_method'] = target_tasks_method
309
parameters['build_number'] = int(input['build_number'])
310
parameters['next_version'] = next_version
311
parameters['release_history'] = release_history
312
if promotion_config.get('is-rc'):
313
parameters['release_type'] += '-rc'
314
parameters['release_eta'] = input.get('release_eta', '')
315
parameters['release_product'] = product
316
# When doing staging releases on try, we still want to re-use tasks from
317
# previous graphs.
318
parameters['optimize_target_tasks'] = True
319
320
# Partner/EMEfree are enabled by default when get_partner_url_config() returns a non-null url
321
# The action input may override by sending False. It's an error to send True with no url found
322
partner_url_config = get_partner_url_config(parameters, graph_config)
323
release_enable_partners = partner_url_config['release-partner-repack'] is not None
324
release_enable_emefree = partner_url_config['release-eme-free-repack'] is not None
325
if input.get('release_enable_partners') is False:
326
release_enable_partners = False
327
elif input.get('release_enable_partners') is True and not release_enable_partners:
328
raise Exception("Can't enable partner repacks when no config url found")
329
if input.get('release_enable_emefree') is False:
330
release_enable_emefree = False
331
elif input.get('release_enable_emefree') is True and not release_enable_emefree:
332
raise Exception("Can't enable EMEfree when no config url found")
333
parameters['release_enable_partners'] = release_enable_partners
334
parameters['release_enable_emefree'] = release_enable_emefree
335
336
partner_config = input.get('release_partner_config')
337
if not partner_config and (release_enable_emefree or release_enable_partners):
338
github_token = get_token(parameters)
339
partner_config = get_partner_config(partner_url_config, github_token)
340
if partner_config:
341
parameters['release_partner_config'] = fix_partner_config(partner_config)
342
parameters['release_partners'] = input.get('release_partners')
343
if input.get('release_partner_build_number'):
344
parameters['release_partner_build_number'] = input['release_partner_build_number']
345
346
if input['version']:
347
parameters['version'] = input['version']
348
349
parameters['required_signoffs'] = get_required_signoffs(input, parameters)
350
parameters['signoff_urls'] = get_signoff_urls(input, parameters)
351
352
# make parameters read-only
353
parameters = Parameters(**parameters)
354
355
taskgraph_decision({'root': graph_config.root_dir}, parameters=parameters)