Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions src/scriptworker/cot/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import logging
import os
import pprint
import re
import sys
import tempfile
from copy import deepcopy
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should crash if a default_branch is not set on the repo.
No need to fix this here, we can fix this in the future.

for b in config["branches"]:
if fnmatch.fnmatch(branch, b["name"]):
return str(b["level"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I got a bit confused at first glance here. b variable (b["foo"]) vs byte-string (b"foo")


raise ValueError("Can't find level for project {}".format(project))

Expand Down Expand Up @@ -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},
}

Expand Down Expand Up @@ -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": {
Expand All @@ -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}",
},
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/data/cotv4/action_relpro.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion tests/data/cotv4/retrigger_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
36 changes: 36 additions & 0 deletions tests/test_cot_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down