Skip to content

Commit afbe4c3

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

File tree

6 files changed

+126
-1
lines changed

6 files changed

+126
-1
lines changed

.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(
275+
f"Overriding parameters from {note_ref}:\n{json.dumps(note_params, indent=2)}"
276+
)
277+
parameters.update(note_params)
278+
except ValueError as e:
279+
raise Exception(f"Failed to parse {note_ref} as JSON: {e}") 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: 20 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,18 @@ 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("notes", f"--ref={note}", "show", revision).strip()
604+
except subprocess.CalledProcessError as e:
605+
if e.returncode == 1:
606+
return None
607+
raise
608+
589609

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

test/test_decision.py

Lines changed: 59 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,60 @@ 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(mock_files_changed, mock_get_note):
181+
options = {**_BASE_OPTIONS, "allow_parameter_override": False}
182+
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options)
183+
mock_get_note.assert_not_called()
184+
assert params["build_number"] == 1
185+
186+
187+
@unittest.mock.patch.object(
188+
GitRepository,
189+
"get_note",
190+
return_value="not valid json {",
191+
)
192+
@unittest.mock.patch.object(GitRepository, "get_changed_files", return_value=[])
193+
def test_decision_parameters_note_invalid_json(mock_files_changed, mock_get_note):
194+
options = {**_BASE_OPTIONS, "allow_parameter_override": True}
195+
with pytest.raises(
196+
Exception, match="Failed to parse refs/notes/decision-parameters as JSON"
197+
):
198+
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)