From 937d1f15f3682344b7a3dd68874cd1f2c71c40d4 Mon Sep 17 00:00:00 2001 From: kryoseu Date: Mon, 22 Jun 2026 16:21:19 -0700 Subject: [PATCH 1/3] feat: add config-driven release_level utility Add a generic `release_level` to `mozilla_taskgraph.util.attributes` so projects can determine whether a task is a "production" or "staging" release without hardcoding project-specific branch data in shared code. The set of release branches is project-specific, so it is sourced from a new `release-branches` mapping in the graph config rather than baked into mozilla-taskgraph. `release_level` reads the resolved value from the `release_branches` parameter (`True` means every branch of the project is a release branch, a list restricts releases to the named branches). It is read from params rather than the graph config so it is also available to consumers that only have a `Parameters` object, such as action tasks. A project turns this on by adding `release-branches` to its `config.yml` and calling `set_release_branches` from its decision-parameters function. Until a project does both, the `release_branches` parameter stays None and nothing uses the new utility, so adding this code has no effect on any existing project's builds. --- src/mozilla_taskgraph/__init__.py | 4 ++ src/mozilla_taskgraph/config.py | 16 ++++++- src/mozilla_taskgraph/parameters.py | 38 ++++++++++++++++ src/mozilla_taskgraph/util/attributes.py | 34 ++++++++++++++ test/test_parameters.py | 34 ++++++++++++++ test/util/test_attributes.py | 58 ++++++++++++++++++++++++ 6 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/mozilla_taskgraph/parameters.py create mode 100644 src/mozilla_taskgraph/util/attributes.py create mode 100644 test/test_parameters.py create mode 100644 test/util/test_attributes.py diff --git a/src/mozilla_taskgraph/__init__.py b/src/mozilla_taskgraph/__init__.py index 1ec45be..bb4a588 100644 --- a/src/mozilla_taskgraph/__init__.py +++ b/src/mozilla_taskgraph/__init__.py @@ -7,6 +7,8 @@ from taskgraph.config import validate_graph_config from taskgraph.util import schema +from mozilla_taskgraph.parameters import register_parameters + # Schemas for YAML files should use dashed identifiers by default. If there are # components of the schema for which there is a good reason to use another format, # exceptions can be added here. @@ -26,6 +28,8 @@ def register(graph_config): "worker_types", ] ) + + register_parameters() validate_graph_config(graph_config._config) diff --git a/src/mozilla_taskgraph/config.py b/src/mozilla_taskgraph/config.py index 3523070..318f84c 100644 --- a/src/mozilla_taskgraph/config.py +++ b/src/mozilla_taskgraph/config.py @@ -1,5 +1,5 @@ from textwrap import dedent -from typing import Optional +from typing import Optional, Union from taskgraph import config as tg from taskgraph.util.schema import Schema @@ -26,6 +26,11 @@ class MozillaGraphConfigSchema(tg.graph_config_schema): # string to use for release tasks. # Defaults to ``mozilla_taskgraph.version:default_parser``. version_parser: Optional[str] = None + # Mapping of project to the branches that should be considered + # "production" releases. A value of ``True`` means all branches of the + # project are release branches, while a list restricts releases to the + # named branches. Consumed by ``mozilla_taskgraph.parameters``. + release_branches: Optional[dict[str, Union[bool, list[str]]]] = None else: # Legacy voluptuous-based graph_config_schema (e.g. gecko_taskgraph override). @@ -48,6 +53,15 @@ class MozillaGraphConfigSchema(tg.graph_config_schema): Defaults to ``mozilla_taskgraph.version:default_parser``. """.lstrip()), ): str, + Vol_Optional( + "release-branches", + description=dedent(""" + Mapping of project to the branches that should be considered + "production" releases. A value of ``True`` means all branches + of the project are release branches, while a list restricts + releases to the named branches. + """.lstrip()), + ): {str: object}, } ) diff --git a/src/mozilla_taskgraph/parameters.py b/src/mozilla_taskgraph/parameters.py new file mode 100644 index 0000000..954a8a8 --- /dev/null +++ b/src/mozilla_taskgraph/parameters.py @@ -0,0 +1,38 @@ +# 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/. + +from typing import Optional, Union + +from taskgraph.parameters import extend_parameters_schema +from taskgraph.util.schema import Schema + + +class MozillaParametersSchema(Schema, kw_only=True): + # Branches of the current project that are considered "production" + # releases. Resolved from the graph config's `release-branches` mapping by + # `set_release_branches`. `True` means all branches of the project are + # release branches, a list restricts releases to the named branches, and + # `None` means the project has no release branches. + release_branches: Optional[Union[bool, list[str]]] = None + + +def get_defaults(repo_root=None): + return { + "release_branches": None, + } + + +def register_parameters(): + extend_parameters_schema(MozillaParametersSchema, defaults_fn=get_defaults) + + +def set_release_branches(graph_config, parameters): + """Resolve the current project's release branches from the graph config. + + Projects should call this from their `decision-parameters` function so that + `release_branches` is persisted into `parameters.yml` and available to + consumers that only have a `Parameters` object (e.g. action tasks). + """ + mapping = graph_config._config.get("release-branches") or {} + parameters["release_branches"] = mapping.get(parameters["project"]) diff --git a/src/mozilla_taskgraph/util/attributes.py b/src/mozilla_taskgraph/util/attributes.py new file mode 100644 index 0000000..465b7a1 --- /dev/null +++ b/src/mozilla_taskgraph/util/attributes.py @@ -0,0 +1,34 @@ +# 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 re + + +def release_level(params): + """Whether this is a production release or not. + + The set of branches considered "production" is project specific and comes + from the ``release_branches`` parameter, which is resolved at decision time + from the graph config's ``release-branches`` mapping. A value of ``True`` + means every branch of the project is a release branch (the model used by + Mercurial based projects), while a list restricts releases to the named + branches. + + :return str: One of "production" or "staging". + """ + if params["level"] != "3": + return "staging" + + branches = params.get("release_branches") + if not branches: + return "staging" + + if branches is True: + return "production" + + m = re.match(r"refs/heads/(\S+)$", params["head_ref"]) + if m is not None and m.group(1) in branches: + return "production" + + return "staging" diff --git a/test/test_parameters.py b/test/test_parameters.py new file mode 100644 index 0000000..ddf2177 --- /dev/null +++ b/test/test_parameters.py @@ -0,0 +1,34 @@ +# 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 pytest + +from mozilla_taskgraph.parameters import set_release_branches + + +@pytest.mark.parametrize( + "release_branches,project,expected", + ( + # No `release-branches` config at all. + (None, "some-project", None), + # Project not listed. + ({"firefox": ["main"]}, "autoland", None), + # Whole project is a release project. + ({"mozilla-central": True}, "mozilla-central", True), + # Project mapped to a list of branches. + ({"firefox": ["main", "beta"]}, "firefox", ["main", "beta"]), + ), +) +def test_set_release_branches( + make_graph_config, parameters, release_branches, project, expected +): + extra_config = {} + if release_branches is not None: + extra_config["release-branches"] = release_branches + + graph_config = make_graph_config(extra_config=extra_config) + parameters["project"] = project + + set_release_branches(graph_config, parameters) + assert parameters["release_branches"] == expected diff --git a/test/util/test_attributes.py b/test/util/test_attributes.py new file mode 100644 index 0000000..badf383 --- /dev/null +++ b/test/util/test_attributes.py @@ -0,0 +1,58 @@ +# 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 pytest + +from mozilla_taskgraph.util.attributes import release_level + +FIREFOX_BRANCHES = ["main", "beta", "release", "esr140"] + + +@pytest.mark.parametrize( + "params,expected", + ( + # Not level 3 -> always staging, regardless of branch. + ({"level": "1", "release_branches": True}, "staging"), + ( + { + "level": "1", + "release_branches": FIREFOX_BRANCHES, + "head_ref": "refs/heads/beta", + }, + "staging", + ), + # No release branches for the project -> staging (e.g. autoland). + ({"level": "3", "release_branches": None}, "staging"), + # Whole project is a release project (Mercurial model). + ({"level": "3", "release_branches": True}, "production"), + # Git monorepo model: only listed branches are production. + ( + { + "level": "3", + "release_branches": FIREFOX_BRANCHES, + "head_ref": "refs/heads/beta", + }, + "production", + ), + ( + { + "level": "3", + "release_branches": FIREFOX_BRANCHES, + "head_ref": "refs/heads/test", + }, + "staging", + ), + # Only refs/heads/* match, not tags. + ( + { + "level": "3", + "release_branches": FIREFOX_BRANCHES, + "head_ref": "refs/tags/beta", + }, + "staging", + ), + ), +) +def test_release_level(params, expected): + assert release_level(params) == expected From 8e27e5d115447ead61baa243519a6a2422a5ce28 Mon Sep 17 00:00:00 2001 From: kryoseu Date: Tue, 23 Jun 2026 11:39:03 -0700 Subject: [PATCH 2/3] refactor: pass release-branches mapping to release_level directly Instead of populating a `release_branches` decision parameter at decision time, pass the graph config's `release-branches` mapping to release_level directly. - `release_level` now takes `(release_branches, params)` - drop `MozillaParametersSchema`, `register_parameters` and `set_release_branches` - update tests accordingly --- src/mozilla_taskgraph/__init__.py | 3 -- src/mozilla_taskgraph/config.py | 3 +- src/mozilla_taskgraph/parameters.py | 38 ----------------- src/mozilla_taskgraph/util/attributes.py | 33 +++++++-------- test/test_parameters.py | 34 ---------------- test/util/test_attributes.py | 52 ++++++++++++------------ 6 files changed, 41 insertions(+), 122 deletions(-) delete mode 100644 src/mozilla_taskgraph/parameters.py delete mode 100644 test/test_parameters.py diff --git a/src/mozilla_taskgraph/__init__.py b/src/mozilla_taskgraph/__init__.py index bb4a588..4b5258e 100644 --- a/src/mozilla_taskgraph/__init__.py +++ b/src/mozilla_taskgraph/__init__.py @@ -7,8 +7,6 @@ from taskgraph.config import validate_graph_config from taskgraph.util import schema -from mozilla_taskgraph.parameters import register_parameters - # Schemas for YAML files should use dashed identifiers by default. If there are # components of the schema for which there is a good reason to use another format, # exceptions can be added here. @@ -29,7 +27,6 @@ def register(graph_config): ] ) - register_parameters() validate_graph_config(graph_config._config) diff --git a/src/mozilla_taskgraph/config.py b/src/mozilla_taskgraph/config.py index 318f84c..4ac6ae7 100644 --- a/src/mozilla_taskgraph/config.py +++ b/src/mozilla_taskgraph/config.py @@ -29,7 +29,8 @@ class MozillaGraphConfigSchema(tg.graph_config_schema): # Mapping of project to the branches that should be considered # "production" releases. A value of ``True`` means all branches of the # project are release branches, while a list restricts releases to the - # named branches. Consumed by ``mozilla_taskgraph.parameters``. + # named branches. Consumed by + # ``mozilla_taskgraph.util.attributes:release_level``. release_branches: Optional[dict[str, Union[bool, list[str]]]] = None else: diff --git a/src/mozilla_taskgraph/parameters.py b/src/mozilla_taskgraph/parameters.py deleted file mode 100644 index 954a8a8..0000000 --- a/src/mozilla_taskgraph/parameters.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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/. - -from typing import Optional, Union - -from taskgraph.parameters import extend_parameters_schema -from taskgraph.util.schema import Schema - - -class MozillaParametersSchema(Schema, kw_only=True): - # Branches of the current project that are considered "production" - # releases. Resolved from the graph config's `release-branches` mapping by - # `set_release_branches`. `True` means all branches of the project are - # release branches, a list restricts releases to the named branches, and - # `None` means the project has no release branches. - release_branches: Optional[Union[bool, list[str]]] = None - - -def get_defaults(repo_root=None): - return { - "release_branches": None, - } - - -def register_parameters(): - extend_parameters_schema(MozillaParametersSchema, defaults_fn=get_defaults) - - -def set_release_branches(graph_config, parameters): - """Resolve the current project's release branches from the graph config. - - Projects should call this from their `decision-parameters` function so that - `release_branches` is persisted into `parameters.yml` and available to - consumers that only have a `Parameters` object (e.g. action tasks). - """ - mapping = graph_config._config.get("release-branches") or {} - parameters["release_branches"] = mapping.get(parameters["project"]) diff --git a/src/mozilla_taskgraph/util/attributes.py b/src/mozilla_taskgraph/util/attributes.py index 465b7a1..0d4d7ff 100644 --- a/src/mozilla_taskgraph/util/attributes.py +++ b/src/mozilla_taskgraph/util/attributes.py @@ -5,30 +5,25 @@ import re -def release_level(params): +def release_level(release_branches, params): """Whether this is a production release or not. - The set of branches considered "production" is project specific and comes - from the ``release_branches`` parameter, which is resolved at decision time - from the graph config's ``release-branches`` mapping. A value of ``True`` - means every branch of the project is a release branch (the model used by - Mercurial based projects), while a list restricts releases to the named - branches. + ``release_branches`` is the graph config's ``release-branches`` mapping of + project to the branches considered "production" for it. A value of ``True`` + for a project means every branch of that project is a release branch (the + model used by Mercurial based projects), while a list restricts releases to + the named branches. :return str: One of "production" or "staging". """ - if params["level"] != "3": - return "staging" - branches = params.get("release_branches") - if not branches: - return "staging" - - if branches is True: - return "production" - - m = re.match(r"refs/heads/(\S+)$", params["head_ref"]) - if m is not None and m.group(1) in branches: - return "production" + if params["level"] == "3": + branches = (release_branches or {}).get(params["project"]) + if branches is True or ( + branches + and (m := re.match(r"refs/heads/(\S+)$", params["head_ref"])) + and m.group(1) in branches + ): + return "production" return "staging" diff --git a/test/test_parameters.py b/test/test_parameters.py deleted file mode 100644 index ddf2177..0000000 --- a/test/test_parameters.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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 pytest - -from mozilla_taskgraph.parameters import set_release_branches - - -@pytest.mark.parametrize( - "release_branches,project,expected", - ( - # No `release-branches` config at all. - (None, "some-project", None), - # Project not listed. - ({"firefox": ["main"]}, "autoland", None), - # Whole project is a release project. - ({"mozilla-central": True}, "mozilla-central", True), - # Project mapped to a list of branches. - ({"firefox": ["main", "beta"]}, "firefox", ["main", "beta"]), - ), -) -def test_set_release_branches( - make_graph_config, parameters, release_branches, project, expected -): - extra_config = {} - if release_branches is not None: - extra_config["release-branches"] = release_branches - - graph_config = make_graph_config(extra_config=extra_config) - parameters["project"] = project - - set_release_branches(graph_config, parameters) - assert parameters["release_branches"] == expected diff --git a/test/util/test_attributes.py b/test/util/test_attributes.py index badf383..53a80ee 100644 --- a/test/util/test_attributes.py +++ b/test/util/test_attributes.py @@ -7,52 +7,50 @@ from mozilla_taskgraph.util.attributes import release_level FIREFOX_BRANCHES = ["main", "beta", "release", "esr140"] +RELEASE_BRANCHES = { + "firefox": FIREFOX_BRANCHES, + "mozilla-central": True, +} @pytest.mark.parametrize( - "params,expected", + "release_branches,params,expected", ( # Not level 3 -> always staging, regardless of branch. - ({"level": "1", "release_branches": True}, "staging"), + (RELEASE_BRANCHES, {"level": "1", "project": "mozilla-central"}, "staging"), ( - { - "level": "1", - "release_branches": FIREFOX_BRANCHES, - "head_ref": "refs/heads/beta", - }, + RELEASE_BRANCHES, + {"level": "1", "project": "firefox", "head_ref": "refs/heads/beta"}, "staging", ), - # No release branches for the project -> staging (e.g. autoland). - ({"level": "3", "release_branches": None}, "staging"), + # No `release-branches` mapping at all -> staging. + ( + None, + {"level": "3", "project": "firefox", "head_ref": "refs/heads/beta"}, + "staging", + ), + # Project not listed in the mapping -> staging (e.g. autoland). + (RELEASE_BRANCHES, {"level": "3", "project": "autoland"}, "staging"), # Whole project is a release project (Mercurial model). - ({"level": "3", "release_branches": True}, "production"), + (RELEASE_BRANCHES, {"level": "3", "project": "mozilla-central"}, "production"), # Git monorepo model: only listed branches are production. ( - { - "level": "3", - "release_branches": FIREFOX_BRANCHES, - "head_ref": "refs/heads/beta", - }, + RELEASE_BRANCHES, + {"level": "3", "project": "firefox", "head_ref": "refs/heads/beta"}, "production", ), ( - { - "level": "3", - "release_branches": FIREFOX_BRANCHES, - "head_ref": "refs/heads/test", - }, + RELEASE_BRANCHES, + {"level": "3", "project": "firefox", "head_ref": "refs/heads/test"}, "staging", ), # Only refs/heads/* match, not tags. ( - { - "level": "3", - "release_branches": FIREFOX_BRANCHES, - "head_ref": "refs/tags/beta", - }, + RELEASE_BRANCHES, + {"level": "3", "project": "firefox", "head_ref": "refs/tags/beta"}, "staging", ), ), ) -def test_release_level(params, expected): - assert release_level(params) == expected +def test_release_level(release_branches, params, expected): + assert release_level(release_branches, params) == expected From 34a7068e048be9fd9df512ac2411b167d36529cd Mon Sep 17 00:00:00 2001 From: kryoseu Date: Tue, 23 Jun 2026 13:53:11 -0700 Subject: [PATCH 3/3] refactor: tighten release_level signature and control flow - annotate both args as dicts and require release_branches to be a mapping (drop the `or {}` fallback; callers pass {} when unconfigured) - restructure the branch checks --- src/mozilla_taskgraph/util/attributes.py | 21 ++++++++++++++------- test/util/test_attributes.py | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/mozilla_taskgraph/util/attributes.py b/src/mozilla_taskgraph/util/attributes.py index 0d4d7ff..59ecb9b 100644 --- a/src/mozilla_taskgraph/util/attributes.py +++ b/src/mozilla_taskgraph/util/attributes.py @@ -5,7 +5,7 @@ import re -def release_level(release_branches, params): +def release_level(release_branches: dict, params: dict): """Whether this is a production release or not. ``release_branches`` is the graph config's ``release-branches`` mapping of @@ -14,16 +14,23 @@ def release_level(release_branches, params): model used by Mercurial based projects), while a list restricts releases to the named branches. + A build is only ever "production" at level 3. ``params`` provides ``level`` + and ``project``, plus ``head_ref`` for projects configured with a branch + list. + :return str: One of "production" or "staging". """ if params["level"] == "3": - branches = (release_branches or {}).get(params["project"]) - if branches is True or ( - branches - and (m := re.match(r"refs/heads/(\S+)$", params["head_ref"])) - and m.group(1) in branches - ): + branches = release_branches.get(params["project"]) + + if branches is True: return "production" + if isinstance(branches, list): + match = re.match(r"refs/heads/(\S+)$", params["head_ref"]) + + if match and match.group(1) in branches: + return "production" + return "staging" diff --git a/test/util/test_attributes.py b/test/util/test_attributes.py index 53a80ee..71d78e3 100644 --- a/test/util/test_attributes.py +++ b/test/util/test_attributes.py @@ -23,9 +23,9 @@ {"level": "1", "project": "firefox", "head_ref": "refs/heads/beta"}, "staging", ), - # No `release-branches` mapping at all -> staging. + # Empty `release-branches` mapping -> staging. ( - None, + {}, {"level": "3", "project": "firefox", "head_ref": "refs/heads/beta"}, "staging", ),