Skip to content

Commit e3203f9

Browse files
committed
feat(decision): support extracting parameters from Git note
Bug: 2028208
1 parent 5cf320c commit e3203f9

6 files changed

Lines changed: 130 additions & 1 deletion

File tree

.taskcluster.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ tasks:
226226
- $if: 'isPullRequest'
227227
then:
228228
TASKGRAPH_PULL_REQUEST_NUMBER: '${event.pull_request.number}'
229+
TASKGRAPH_EXTRA_REFS: {$json: ["refs/notes/decision-parameters"]}
229230
- $if: 'tasks_for == "action" || tasks_for == "pr-action"'
230231
then:
231232
ACTION_TASK_GROUP_ID: '${action.taskGroupId}' # taskGroupId of the target task
@@ -251,7 +252,11 @@ tasks:
251252
- bash
252253
- -cx
253254
- $let:
254-
extraArgs: {$if: 'tasks_for == "cron"', then: '${cron.quoted_args}', else: ''}
255+
extraArgs:
256+
$switch:
257+
'tasks_for == "cron"': '${cron.quoted_args}'
258+
'tasks_for == "github-pull-request"': '--allow-parameter-override'
259+
$default: ''
255260
in:
256261
$if: 'tasks_for == "action" || tasks_for == "pr-action"'
257262
then: >

src/taskgraph/decision.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,20 @@ def get_decision_parameters(graph_config, options):
264264
] == "github-pull-request":
265265
set_try_config(parameters, task_config_file)
266266

267+
# load extra parameters from vcs note if able
268+
note_ref = "refs/notes/decision-parameters"
269+
if options.get("allow_parameter_override") and (
270+
note_params := repo.get_note(note_ref)
271+
):
272+
try:
273+
note_params = json.loads(note_params)
274+
logger.info(f"Overriding parameters from {note_ref}:\n{json.dumps(note_params, indent=2)}")
275+
parameters.update(note_params)
276+
except ValueError as e:
277+
raise Exception(
278+
f"Failed to parse {note_ref} as JSON: {e}"
279+
) from e
280+
267281
result = Parameters(**parameters)
268282
result.check()
269283
return result

src/taskgraph/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,12 @@ def load_task(args):
933933
@argument(
934934
"--verbose", "-v", action="store_true", help="include debug-level logging output"
935935
)
936+
@argument(
937+
"--allow-parameter-override",
938+
default=False,
939+
action="store_true",
940+
help="Allow user to override computed decision task parameters.",
941+
)
936942
def decision(options):
937943
from taskgraph.decision import taskgraph_decision # noqa: PLC0415
938944

src/taskgraph/util/vcs.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,14 @@ def does_revision_exist_locally(self, revision: str) -> bool:
215215
If this function returns an unexpected value, then make sure
216216
the revision was fetched from the remote repository."""
217217

218+
def get_note(self, note: str, revision: Optional[str] = None) -> Optional[str]:
219+
"""Read a note attached to the given revision (defaults to HEAD).
220+
221+
Returns the note content as a string, or ``None`` if no note exists.
222+
Only supported by Git; returns ``None`` for all other VCS types.
223+
"""
224+
return None
225+
218226

219227
class HgRepository(Repository):
220228
@property
@@ -586,6 +594,20 @@ def does_revision_exist_locally(self, revision):
586594
return False
587595
raise
588596

597+
def get_note(self, note: str, revision: Optional[str] = None) -> Optional[str]:
598+
if not note.startswith("refs/notes/"):
599+
note = f"refs/notes/{note}"
600+
601+
revision = revision or "HEAD"
602+
try:
603+
return self.run(
604+
"notes", f"--ref={note}", "show", revision
605+
).strip()
606+
except subprocess.CalledProcessError as e:
607+
if e.returncode == 1:
608+
return None
609+
raise
610+
589611

590612
def get_repository(path: str):
591613
"""Get a repository object for the repository at `path`.

test/test_decision.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import unittest
1111
from pathlib import Path
1212

13+
import pytest
14+
1315
from taskgraph import decision
1416
from taskgraph.util.vcs import GitRepository, HgRepository
1517
from taskgraph.util.yaml import load_yaml
@@ -137,3 +139,62 @@ def test_dontbuild_commit_message_yields_default_target_tasks_method(
137139
self.options["tasks_for"] = "hg-push"
138140
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options)
139141
self.assertEqual(params["target_tasks_method"], "nothing")
142+
143+
144+
_BASE_OPTIONS = {
145+
"base_repository": "https://hg.mozilla.org/mozilla-unified",
146+
"base_rev": "aaaa",
147+
"head_repository": "https://hg.mozilla.org/mozilla-central",
148+
"head_rev": "bbbb",
149+
"head_ref": "default",
150+
"head_tag": "",
151+
"project": "mozilla-central",
152+
"pushlog_id": "1",
153+
"pushdate": 0,
154+
"repository_type": "git",
155+
"owner": "nobody@mozilla.com",
156+
"tasks_for": "github-push",
157+
"level": "1",
158+
}
159+
160+
161+
@unittest.mock.patch.object(
162+
GitRepository,
163+
"get_note",
164+
return_value=json.dumps({"build_number": 99}),
165+
)
166+
@unittest.mock.patch.object(GitRepository, "get_changed_files", return_value=[])
167+
def test_decision_parameters_note(mock_files_changed, mock_get_note):
168+
options = {**_BASE_OPTIONS, "allow_parameter_override": True}
169+
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options)
170+
mock_get_note.assert_called_once_with("decision-parameters")
171+
assert params["build_number"] == 99
172+
173+
174+
@unittest.mock.patch.object(
175+
GitRepository,
176+
"get_note",
177+
return_value=json.dumps({"build_number": 99}),
178+
)
179+
@unittest.mock.patch.object(GitRepository, "get_changed_files", return_value=[])
180+
def test_decision_parameters_note_disallow_override(
181+
mock_files_changed, mock_get_note
182+
):
183+
options = {**_BASE_OPTIONS, "allow_parameter_override": False}
184+
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options)
185+
mock_get_note.assert_not_called()
186+
assert params["build_number"] == 1
187+
188+
189+
@unittest.mock.patch.object(
190+
GitRepository,
191+
"get_note",
192+
return_value="not valid json {",
193+
)
194+
@unittest.mock.patch.object(GitRepository, "get_changed_files", return_value=[])
195+
def test_decision_parameters_note_invalid_json(mock_files_changed, mock_get_note):
196+
options = {**_BASE_OPTIONS, "allow_parameter_override": True}
197+
with pytest.raises(
198+
Exception, match="Failed to parse refs/notes/decision-parameters as JSON"
199+
):
200+
decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options)

test/test_util_vcs.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

55
import os
6+
import shutil
67
import subprocess
78
from pathlib import Path
89
from textwrap import dedent
@@ -648,3 +649,23 @@ def test_get_changed_files_with_null_base_revision_shallow_clone(
648649
assert "first_file" in changed_files
649650
assert "file1.txt" in changed_files
650651
assert "file2.txt" in changed_files
652+
653+
654+
def test_get_note_git(git_repo, tmpdir):
655+
"""get_note returns note content when present, None otherwise."""
656+
repo_path = tmpdir.join("git")
657+
shutil.copytree(git_repo, repo_path)
658+
repo = get_repository(str(repo_path))
659+
660+
# No note yet
661+
assert repo.get_note("try-config") is None
662+
663+
rev = repo.head_rev
664+
subprocess.check_call(
665+
["git", "notes", "--ref=refs/notes/try-config", "add", "-m", "test note", rev],
666+
cwd=repo.path,
667+
)
668+
669+
assert repo.get_note("try-config") == "test note"
670+
assert repo.get_note("try-config", rev) == "test note"
671+
assert repo.get_note("other") is None

0 commit comments

Comments
 (0)