From 0c6811722f2bff7ff867ef6b9252256c4f08a2cd Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Thu, 20 Aug 2026 12:02:21 -0400 Subject: [PATCH 1/2] Bug 2062156 - Re-create level in action roles with Chain of Trust The action repo roles are changing to include the level. We need to account for that in CoT. --- src/scriptworker/cot/verify.py | 20 ++++++++++++++++---- tests/data/cotv4/action_relpro.json | 2 +- tests/data/cotv4/retrigger_template.json | 2 +- tests/test_cot_verify.py | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/scriptworker/cot/verify.py b/src/scriptworker/cot/verify.py index 1a33481fa..bb9818b28 100644 --- a/src/scriptworker/cot/verify.py +++ b/src/scriptworker/cot/verify.py @@ -15,6 +15,7 @@ import logging import os import pprint +import re import sys import tempfile from copy import deepcopy @@ -1457,14 +1458,16 @@ def _get_action_from_actions_json(all_actions, callback_name): raise CoTError("No action with {} callback found.".format(callback_name)) -def _wrap_action_hook_with_let(tmpl, action_perm, tasks_for): +def _wrap_action_hook_with_let(tmpl, action_perm, tasks_for, level): """Construct the hook task template body. Given the content of .taskcluster.yml, construct the task template that would appear in the corresponding hook definition. This is an attempt to duplicate the logic here: - https://hg.mozilla.org/ci/ci-admin/file/edad9f8/ciadmin/generate/in_tree_actions.py#l154 + https://github.com/mozilla-releng/fxci-config/blob/main/src/ciadmin/generate/in_tree_actions.py + `level` must be derived from the hook that produced this action task, not + the triggering push """ return { "$let": { @@ -1475,7 +1478,7 @@ def _wrap_action_hook_with_let(tmpl, action_perm, tasks_for): "description": "${payload.decision.action.description}", "taskGroupId": "${payload.decision.action.taskGroupId}", "symbol": "${payload.decision.action.symbol}", - "repo_scope": "assume:repo:${payload.decision.repository.url[8:]}:" + tasks_for + ":" + action_perm, + "repo_scope": "assume:repo:${payload.decision.repository.url[8:]}:" + tasks_for + "-" + str(level) + ":" + action_perm, "action_perm": action_perm, "cb_name": "${payload.decision.action.cb_name}", }, @@ -1513,6 +1516,14 @@ def _get_action_perm(action_defn): return action_perm +def _get_action_level(action_defn): + hook_id = action_defn.get("hookId", "") + match = re.match(r"in-tree-(?:pr-)?action-(\d+)-", hook_id) + if not match: + raise CoTError("Can't find level in action hookId `{}`.".format(hook_id)) + return int(match.group(1)) + + async def get_action_context_and_template(chain, parent_link, decision_link, tasks_for): """Get the appropriate json-e context and template for an action task. @@ -1540,7 +1551,8 @@ async def get_action_context_and_template(chain, parent_link, decision_link, tas # action-hook. in_tree_tmpl = await get_in_tree_template(decision_link) action_perm = _get_action_perm(action_defn) - tmpl = _wrap_action_hook_with_let(in_tree_tmpl, action_perm, tasks_for) + action_level = _get_action_level(action_defn) + tmpl = _wrap_action_hook_with_let(in_tree_tmpl, action_perm, tasks_for, action_level) # define the JSON-e context with which the hook's task template was # rendered, defined at diff --git a/tests/data/cotv4/action_relpro.json b/tests/data/cotv4/action_relpro.json index 59875172f..e01297dd4 100644 --- a/tests/data/cotv4/action_relpro.json +++ b/tests/data/cotv4/action_relpro.json @@ -17,7 +17,7 @@ "deadline": "2018-12-20T19:30:10.266Z", "expires": "2019-12-19T19:30:11.266Z", "scopes": [ - "assume:repo:hg.mozilla.org/try:action:release-promotion" + "assume:repo:hg.mozilla.org/try:action-1:release-promotion" ], "payload": { "env": { diff --git a/tests/data/cotv4/retrigger_template.json b/tests/data/cotv4/retrigger_template.json index a8a809193..8a4dbf4cc 100644 --- a/tests/data/cotv4/retrigger_template.json +++ b/tests/data/cotv4/retrigger_template.json @@ -5,7 +5,7 @@ "cb_name": "${payload.decision.action.cb_name}", "description": "${payload.decision.action.description}", "name": "${payload.decision.action.name}", - "repo_scope": "assume:repo:${payload.decision.repository.url[8:]}:action:generic", + "repo_scope": "assume:repo:${payload.decision.repository.url[8:]}:action-1:generic", "symbol": "${payload.decision.action.symbol}", "taskGroupId": "${payload.decision.action.taskGroupId}", "title": "${payload.decision.action.title}" diff --git a/tests/test_cot_verify.py b/tests/test_cot_verify.py index c5e1dd744..fdf9c83f5 100644 --- a/tests/test_cot_verify.py +++ b/tests/test_cot_verify.py @@ -1412,6 +1412,24 @@ def test_get_action_perm(defn, expected): assert cotverify._get_action_perm(defn) == expected +@pytest.mark.parametrize( + "defn,expected", + ( + ({"hookId": "in-tree-action-1-generic/bb2dee27f9"}, 1), + ({"hookId": "in-tree-action-3-release-promotion/bb2dee27f9"}, 3), + ({"hookId": "in-tree-pr-action-1-generic/bb2dee27f9"}, 1), + ), +) +def test_get_action_level(defn, expected): + assert cotverify._get_action_level(defn) == expected + + +@pytest.mark.parametrize("defn", ({}, {"hookId": "blah/generic/"})) +def test_get_action_level_missing(defn): + with pytest.raises(CoTError): + cotverify._get_action_level(defn) + + @pytest.mark.asyncio @pytest.mark.parametrize( "name,task_id,path,decision_task_id,decision_path,expected_template_path,expected_context_path", From d2f0468d61572d035d3179ca360f84b250b52b43 Mon Sep 17 00:00:00 2001 From: Andrew Halberstadt Date: Thu, 20 Aug 2026 14:47:12 -0400 Subject: [PATCH 2/2] Bug 2062156 - Re-create cron tasks with proper branch in Chain of Trust Cron tasks can now run against specific Git branches, so we can't just assume they're running on the default branch anymore. --- src/scriptworker/cot/verify.py | 16 ++++++++-------- tests/test_cot_verify.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/scriptworker/cot/verify.py b/src/scriptworker/cot/verify.py index bb9818b28..78c841b98 100644 --- a/src/scriptworker/cot/verify.py +++ b/src/scriptworker/cot/verify.py @@ -1023,7 +1023,7 @@ async def get_pushlog_info(decision_link): # get_scm_level {{{1 -async def get_scm_level(context, project): +async def get_scm_level(context, project, branch=None): """Get the scm level for a project from ``projects.yml``. We define all known projects in ``projects.yml``. Let's make sure we have @@ -1035,6 +1035,7 @@ async def get_scm_level(context, project): Args: context (scriptworker.context.Context): the scriptworker context project (str): the project to get the scm level for. + branch (str, optional): the branch to get the level for (Git only) Returns: str: the level of the project, as a string. @@ -1045,12 +1046,11 @@ async def get_scm_level(context, project): if config["repo_type"] == "hg": return config["access"].replace("scm_level_", "") elif config["repo_type"] == "git": - # TODO: we should be using the branch that the task is actually - # being run on - default_branch = config.get("default_branch", "main") - for branch in config["branches"]: - if branch["name"] == default_branch: - return str(branch["level"]) + if branch is None: + branch = config.get("default_branch", "main") + for b in config["branches"]: + if fnmatch.fnmatch(branch, b["name"]): + return str(b["level"]) raise ValueError("Can't find level for project {}".format(project)) @@ -1182,7 +1182,7 @@ async def _get_additional_git_cron_jsone_context(decision_link): "sender": {"login": user}, }, # Taskgraph cron contexts mirror hg-push contexts - "repository": {"url": repo, "project": repo_name, "level": await get_scm_level(decision_link.context, repo_name), "type": "git"}, + "repository": {"url": repo, "project": repo_name, "level": await get_scm_level(decision_link.context, repo_name, branch), "type": "git"}, "push": {"revision": revision, "branch": branch}, } diff --git a/tests/test_cot_verify.py b/tests/test_cot_verify.py index fdf9c83f5..937bd35d0 100644 --- a/tests/test_cot_verify.py +++ b/tests/test_cot_verify.py @@ -2389,6 +2389,24 @@ async def test_get_scm_level(rw_context, project, level, raises): assert await cotverify.get_scm_level(rw_context, project) == level +@pytest.mark.asyncio +async def test_get_scm_level_with_branch(rw_context): + rw_context.projects = { + "multi-branch": { + "branches": [{"name": "main", "level": 3}, {"name": "*", "level": 1}], + "default_branch": "main", + "repo_type": "git", + }, + } + rw_context._projects_timestamp = time.time() + + # No branch given -> falls back to the default branch's level. + assert await cotverify.get_scm_level(rw_context, "multi-branch") == "3" + assert await cotverify.get_scm_level(rw_context, "multi-branch", branch="main") == "3" + # A non-default branch not explicitly listed matches the "*" catch-all. + assert await cotverify.get_scm_level(rw_context, "multi-branch", branch="staging") == "1" + + # tests for matching scopes with a partial match, implemented for xpi @pytest.mark.parametrize( "scope, restricted_scopes, expected",