summaryrefslogtreecommitdiffstats
path: root/taskcluster/gecko_taskgraph/actions/release_promotion.py
blob: 9d6b7ad0b785944b83c26778b0c36f142b6fd2e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.


import json
import os

import requests
from taskgraph.parameters import Parameters
from taskgraph.taskgraph import TaskGraph
from taskgraph.util.taskcluster import get_artifact, list_task_group_incomplete_tasks

from gecko_taskgraph.actions.registry import register_callback_action
from gecko_taskgraph.decision import taskgraph_decision
from gecko_taskgraph.util.attributes import RELEASE_PROMOTION_PROJECTS, release_level
from gecko_taskgraph.util.partials import populate_release_history
from gecko_taskgraph.util.partners import (
    fix_partner_config,
    get_partner_config_by_url,
    get_partner_url_config,
    get_token,
)
from gecko_taskgraph.util.taskgraph import (
    find_decision_task,
    find_existing_tasks_from_previous_kinds,
)

RELEASE_PROMOTION_SIGNOFFS = ("mar-signing",)


def is_release_promotion_available(parameters):
    return parameters["project"] in RELEASE_PROMOTION_PROJECTS


def get_partner_config(partner_url_config, github_token):
    partner_config = {}
    for kind, url in partner_url_config.items():
        if url:
            partner_config[kind] = get_partner_config_by_url(url, kind, github_token)
    return partner_config


def get_signoff_properties():
    props = {}
    for signoff in RELEASE_PROMOTION_SIGNOFFS:
        props[signoff] = {
            "type": "string",
        }
    return props


def get_required_signoffs(input, parameters):
    input_signoffs = set(input.get("required_signoffs", []))
    params_signoffs = set(parameters["required_signoffs"] or [])
    return sorted(list(input_signoffs | params_signoffs))


def get_signoff_urls(input, parameters):
    signoff_urls = parameters["signoff_urls"]
    signoff_urls.update(input.get("signoff_urls", {}))
    return signoff_urls


def get_flavors(graph_config, param):
    """
    Get all flavors with the given parameter enabled.
    """
    promotion_flavors = graph_config["release-promotion"]["flavors"]
    return sorted(
        flavor
        for (flavor, config) in promotion_flavors.items()
        if config.get(param, False)
    )


@register_callback_action(
    name="release-promotion",
    title="Release Promotion",
    symbol="${input.release_promotion_flavor}",
    description="Promote a release.",
    permission="release-promotion",
    order=500,
    context=[],
    available=is_release_promotion_available,
    schema=lambda graph_config: {
        "type": "object",
        "properties": {
            "build_number": {
                "type": "integer",
                "default": 1,
                "minimum": 1,
                "title": "The release build number",
                "description": (
                    "The release build number. Starts at 1 per "
                    "release version, and increments on rebuild."
                ),
            },
            "do_not_optimize": {
                "type": "array",
                "description": (
                    "Optional: a list of labels to avoid optimizing out "
                    "of the graph (to force a rerun of, say, "
                    "funsize docker-image tasks)."
                ),
                "items": {
                    "type": "string",
                },
            },
            "revision": {
                "type": "string",
                "title": "Optional: revision to promote",
                "description": (
                    "Optional: the revision to promote. If specified, "
                    "and `previous_graph_kinds is not specified, find the "
                    "push graph to promote based on the revision."
                ),
            },
            "release_promotion_flavor": {
                "type": "string",
                "description": "The flavor of release promotion to perform.",
                "default": "FILL ME OUT",
                "enum": sorted(graph_config["release-promotion"]["flavors"].keys()),
            },
            "rebuild_kinds": {
                "type": "array",
                "description": (
                    "Optional: an array of kinds to ignore from the previous "
                    "graph(s)."
                ),
                "items": {
                    "type": "string",
                },
            },
            "previous_graph_ids": {
                "type": "array",
                "description": (
                    "Optional: an array of taskIds of decision or action "
                    "tasks from the previous graph(s) to use to populate "
                    "our `previous_graph_kinds`."
                ),
                "items": {
                    "type": "string",
                },
            },
            "version": {
                "type": "string",
                "description": (
                    "Optional: override the version for release promotion. "
                    "Occasionally we'll land a taskgraph fix in a later "
                    "commit, but want to act on a build from a previous "
                    "commit. If a version bump has landed in the meantime, "
                    "relying on the in-tree version will break things."
                ),
                "default": "",
            },
            "next_version": {
                "type": "string",
                "description": (
                    "Next version. Required in the following flavors: "
                    "{}".format(get_flavors(graph_config, "version-bump"))
                ),
                "default": "",
            },
            # Example:
            #   'partial_updates': {
            #       '38.0': {
            #           'buildNumber': 1,
            #           'locales': ['de', 'en-GB', 'ru', 'uk', 'zh-TW']
            #       },
            #       '37.0': {
            #           'buildNumber': 2,
            #           'locales': ['de', 'en-GB', 'ru', 'uk']
            #       }
            #   }
            "partial_updates": {
                "type": "object",
                "description": (
                    "Partial updates. Required in the following flavors: "
                    "{}".format(get_flavors(graph_config, "partial-updates"))
                ),
                "default": {},
                "additionalProperties": {
                    "type": "object",
                    "properties": {
                        "buildNumber": {
                            "type": "number",
                        },
                        "locales": {
                            "type": "array",
                            "items": {
                                "type": "string",
                            },
                        },
                    },
                    "required": [
                        "buildNumber",
                        "locales",
                    ],
                    "additionalProperties": False,
                },
            },
            "release_eta": {
                "type": "string",
                "default": "",
            },
            "release_enable_partner_repack": {
                "type": "boolean",
                "default": False,
                "description": "Toggle for creating partner repacks",
            },
            "release_enable_partner_attribution": {
                "type": "boolean",
                "default": False,
                "description": "Toggle for creating partner attribution",
            },
            "release_partner_build_number": {
                "type": "integer",
                "default": 1,
                "minimum": 1,
                "description": (
                    "The partner build number. This translates to, e.g. "
                    "`v1` in the path. We generally only have to "
                    "bump this on off-cycle partner rebuilds."
                ),
            },
            "release_partners": {
                "type": "array",
                "description": (
                    "A list of partners to repack, or if null or empty then use "
                    "the current full set"
                ),
                "items": {
                    "type": "string",
                },
            },
            "release_partner_config": {
                "type": "object",
                "description": "Partner configuration to use for partner repacks.",
                "properties": {},
                "additionalProperties": True,
            },
            "release_enable_emefree": {
                "type": "boolean",
                "default": False,
                "description": "Toggle for creating EME-free repacks",
            },
            "required_signoffs": {
                "type": "array",
                "description": ("The flavor of release promotion to perform."),
                "items": {
                    "enum": RELEASE_PROMOTION_SIGNOFFS,
                },
            },
            "signoff_urls": {
                "type": "object",
                "default": {},
                "additionalProperties": False,
                "properties": get_signoff_properties(),
            },
        },
        "required": ["release_promotion_flavor", "build_number"],
    },
)
def release_promotion_action(parameters, graph_config, input, task_group_id, task_id):
    release_promotion_flavor = input["release_promotion_flavor"]
    promotion_config = graph_config["release-promotion"]["flavors"][
        release_promotion_flavor
    ]
    release_history = {}
    product = promotion_config["product"]

    next_version = str(input.get("next_version") or "")
    if promotion_config.get("version-bump", False):
        # We force str() the input, hence the 'None'
        if next_version in ["", "None"]:
            raise Exception(
                "`next_version` property needs to be provided for `{}` "
                "target.".format(release_promotion_flavor)
            )

    if promotion_config.get("partial-updates", False):
        partial_updates = input.get("partial_updates", {})
        if not partial_updates and release_level(parameters["project"]) == "production":
            raise Exception(
                "`partial_updates` property needs to be provided for `{}`"
                "target.".format(release_promotion_flavor)
            )
        balrog_prefix = product.title()
        os.environ["PARTIAL_UPDATES"] = json.dumps(partial_updates, sort_keys=True)
        release_history = populate_release_history(
            balrog_prefix, parameters["project"], partial_updates=partial_updates
        )

    target_tasks_method = promotion_config["target-tasks-method"].format(
        project=parameters["project"]
    )
    rebuild_kinds = input.get(
        "rebuild_kinds", promotion_config.get("rebuild-kinds", [])
    )
    do_not_optimize = input.get(
        "do_not_optimize", promotion_config.get("do-not-optimize", [])
    )

    # Make sure no pending tasks remain from a previous run
    own_task_id = os.environ.get("TASK_ID", "")
    try:
        for t in list_task_group_incomplete_tasks(own_task_id):
            if t == own_task_id:
                continue
            raise Exception(
                "task group has unexpected pre-existing incomplete tasks (e.g. {})".format(
                    t
                )
            )
    except requests.exceptions.HTTPError as e:
        # 404 means the task group doesn't exist yet, and we're fine
        if e.response.status_code != 404:
            raise

    # Build previous_graph_ids from ``previous_graph_ids``, ``revision``,
    # or the action parameters.
    previous_graph_ids = input.get("previous_graph_ids")
    if not previous_graph_ids:
        revision = input.get("revision")
        if revision:
            head_rev_param = "{}head_rev".format(
                graph_config["project-repo-param-prefix"]
            )
            push_parameters = {
                head_rev_param: revision,
                "project": parameters["project"],
            }
        else:
            push_parameters = parameters
        previous_graph_ids = [find_decision_task(push_parameters, graph_config)]

    # Download parameters from the first decision task
    parameters = get_artifact(previous_graph_ids[0], "public/parameters.yml")
    # Download and combine full task graphs from each of the previous_graph_ids.
    # Sometimes previous relpro action tasks will add tasks, like partials,
    # that didn't exist in the first full_task_graph, so combining them is
    # important. The rightmost graph should take precedence in the case of
    # conflicts.
    combined_full_task_graph = {}
    for graph_id in previous_graph_ids:
        full_task_graph = get_artifact(graph_id, "public/full-task-graph.json")
        combined_full_task_graph.update(full_task_graph)
    _, combined_full_task_graph = TaskGraph.from_json(combined_full_task_graph)
    parameters["existing_tasks"] = find_existing_tasks_from_previous_kinds(
        combined_full_task_graph, previous_graph_ids, rebuild_kinds
    )
    parameters["do_not_optimize"] = do_not_optimize
    parameters["target_tasks_method"] = target_tasks_method
    parameters["build_number"] = int(input["build_number"])
    parameters["next_version"] = next_version
    parameters["release_history"] = release_history
    if promotion_config.get("is-rc"):
        parameters["release_type"] += "-rc"
    parameters["release_eta"] = input.get("release_eta", "")
    parameters["release_product"] = product
    # When doing staging releases on try, we still want to re-use tasks from
    # previous graphs.
    parameters["optimize_target_tasks"] = True

    if release_promotion_flavor == "promote_firefox_partner_repack":
        release_enable_partner_repack = True
        release_enable_partner_attribution = False
        release_enable_emefree = False
    elif release_promotion_flavor == "promote_firefox_partner_attribution":
        release_enable_partner_repack = False
        release_enable_partner_attribution = True
        release_enable_emefree = False
    else:
        # for promotion or ship phases, we use the action input to turn the repacks/attribution off
        release_enable_partner_repack = input["release_enable_partner_repack"]
        release_enable_partner_attribution = input["release_enable_partner_attribution"]
        release_enable_emefree = input["release_enable_emefree"]

    partner_url_config = get_partner_url_config(parameters, graph_config)
    if (
        release_enable_partner_repack
        and not partner_url_config["release-partner-repack"]
    ):
        raise Exception("Can't enable partner repacks when no config url found")
    if (
        release_enable_partner_attribution
        and not partner_url_config["release-partner-attribution"]
    ):
        raise Exception("Can't enable partner attribution when no config url found")
    if release_enable_emefree and not partner_url_config["release-eme-free-repack"]:
        raise Exception("Can't enable EMEfree repacks when no config url found")
    parameters["release_enable_partner_repack"] = release_enable_partner_repack
    parameters[
        "release_enable_partner_attribution"
    ] = release_enable_partner_attribution
    parameters["release_enable_emefree"] = release_enable_emefree

    partner_config = input.get("release_partner_config")
    if not partner_config and any(
        [
            release_enable_partner_repack,
            release_enable_partner_attribution,
            release_enable_emefree,
        ]
    ):
        github_token = get_token(parameters)
        partner_config = get_partner_config(partner_url_config, github_token)
    if partner_config:
        parameters["release_partner_config"] = fix_partner_config(partner_config)
    parameters["release_partners"] = input.get("release_partners")
    if input.get("release_partner_build_number"):
        parameters["release_partner_build_number"] = input[
            "release_partner_build_number"
        ]

    if input["version"]:
        parameters["version"] = input["version"]

    parameters["required_signoffs"] = get_required_signoffs(input, parameters)
    parameters["signoff_urls"] = get_signoff_urls(input, parameters)

    # make parameters read-only
    parameters = Parameters(**parameters)

    taskgraph_decision({"root": graph_config.root_dir}, parameters=parameters)