diff --git a/src/scriptworker/cot/verify.py b/src/scriptworker/cot/verify.py index 1a33481f..78c841b9 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 @@ -1022,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 @@ -1034,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. @@ -1044,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)) @@ -1181,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}, } @@ -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 59875172..e01297dd 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 a8a80919..8a4dbf4c 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 c5e1dd74..937bd35d 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", @@ -2371,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",