Skip to content

Commit 3e5cab8

Browse files
committed
feat: Make taskgraph artifact prefix configurable (bug 1989274)
1 parent 993ab70 commit 3e5cab8

File tree

8 files changed

+188
-27
lines changed

8 files changed

+188
-27
lines changed

src/taskgraph/actions/util.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from taskgraph.util.taskcluster import (
2020
CONCURRENCY,
2121
get_artifact,
22+
get_artifact_with_prefix,
2223
get_session,
2324
list_tasks,
2425
parse_time,
@@ -28,8 +29,8 @@
2829
logger = logging.getLogger(__name__)
2930

3031

31-
def get_parameters(decision_task_id):
32-
return get_artifact(decision_task_id, "public/parameters.yml")
32+
def get_parameters(decision_task_id, artifact_prefix="public"):
33+
return get_artifact(decision_task_id, f"{artifact_prefix}/parameters.yml")
3334

3435

3536
def fetch_graph_and_labels(parameters, graph_config, task_group_id=None):
@@ -43,9 +44,13 @@ def fetch_graph_and_labels(parameters, graph_config, task_group_id=None):
4344
decision_task_id = task_group_id
4445

4546
# First grab the graph and labels generated during the initial decision task
46-
full_task_graph = get_artifact(decision_task_id, "public/full-task-graph.json")
47+
full_task_graph = get_artifact_with_prefix(
48+
decision_task_id, "full-task-graph.json", parameters
49+
)
4750
_, full_task_graph = TaskGraph.from_json(full_task_graph)
48-
label_to_taskid = get_artifact(decision_task_id, "public/label-to-taskid.json")
51+
label_to_taskid = get_artifact_with_prefix(
52+
decision_task_id, "label-to-taskid.json", parameters
53+
)
4954

5055
# fetch everything in parallel; this avoids serializing any delay in downloading
5156
# each artifact (such as waiting for the artifact to be mirrored locally)
@@ -57,7 +62,9 @@ def fetch_graph_and_labels(parameters, graph_config, task_group_id=None):
5762
def fetch_action(task_id):
5863
logger.info(f"fetching label-to-taskid.json for action task {task_id}")
5964
try:
60-
run_label_to_id = get_artifact(task_id, "public/label-to-taskid.json")
65+
run_label_to_id = get_artifact_with_prefix(
66+
task_id, "label-to-taskid.json", parameters
67+
)
6168
label_to_taskid.update(run_label_to_id)
6269
except HTTPError as e:
6370
if e.response.status_code != 404:
@@ -83,7 +90,9 @@ def fetch_action(task_id):
8390
def fetch_cron(task_id):
8491
logger.info(f"fetching label-to-taskid.json for cron task {task_id}")
8592
try:
86-
run_label_to_id = get_artifact(task_id, "public/label-to-taskid.json")
93+
run_label_to_id = get_artifact_with_prefix(
94+
task_id, "label-to-taskid.json", parameters
95+
)
8796
label_to_taskid.update(run_label_to_id)
8897
except HTTPError as e:
8998
if e.response.status_code != 404:

src/taskgraph/decision.py

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,34 +113,42 @@ def taskgraph_decision(options, parameters=None):
113113
enable_verifications=options.get("verify", True),
114114
)
115115

116+
# Get the artifact prefix from parameters
117+
artifact_prefix = tgg.parameters.get("artifact_prefix", "public")
118+
116119
# write out the parameters used to generate this graph
117-
write_artifact("parameters.yml", dict(**tgg.parameters))
120+
write_artifact("parameters.yml", dict(**tgg.parameters), artifact_prefix)
118121

119-
# write out the public/actions.json file
122+
# write out the actions.json file
120123
write_artifact(
121124
"actions.json",
122125
render_actions_json(tgg.parameters, tgg.graph_config, decision_task_id),
126+
artifact_prefix,
123127
)
124128

125129
# write out the full graph for reference
126130
full_task_json = tgg.full_task_graph.to_json()
127-
write_artifact("full-task-graph.json", full_task_json)
131+
write_artifact("full-task-graph.json", full_task_json, artifact_prefix)
128132

129-
# write out the public/runnable-jobs.json file
133+
# write out the runnable-jobs.json file
130134
write_artifact(
131-
"runnable-jobs.json", full_task_graph_to_runnable_tasks(full_task_json)
135+
"runnable-jobs.json",
136+
full_task_graph_to_runnable_tasks(full_task_json),
137+
artifact_prefix,
132138
)
133139

134140
# this is just a test to check whether the from_json() function is working
135141
_, _ = TaskGraph.from_json(full_task_json)
136142

137143
# write out the target task set to allow reproducing this as input
138-
write_artifact("target-tasks.json", list(tgg.target_task_set.tasks.keys()))
144+
write_artifact(
145+
"target-tasks.json", list(tgg.target_task_set.tasks.keys()), artifact_prefix
146+
)
139147

140148
# write out the optimized task graph to describe what will actually happen,
141149
# and the map of labels to taskids
142-
write_artifact("task-graph.json", tgg.morphed_task_graph.to_json())
143-
write_artifact("label-to-taskid.json", tgg.label_to_taskid)
150+
write_artifact("task-graph.json", tgg.morphed_task_graph.to_json(), artifact_prefix)
151+
write_artifact("label-to-taskid.json", tgg.label_to_taskid, artifact_prefix)
144152

145153
# write out current run-task and fetch-content scripts
146154
RUN_TASK_DIR = pathlib.Path(__file__).parent / "run-task"
@@ -181,6 +189,7 @@ def get_decision_parameters(graph_config, options):
181189
"level",
182190
"target_tasks_method",
183191
"tasks_for",
192+
"artifact_prefix",
184193
]
185194
if n in options
186195
}
@@ -366,11 +375,14 @@ def set_try_config(parameters, task_config_file):
366375
)
367376

368377

369-
def write_artifact(filename, data):
370-
logger.info(f"writing artifact file `{filename}`")
378+
def write_artifact(filename, data, artifact_prefix="public"):
379+
prefixed_filename = f"{artifact_prefix}/{filename}"
380+
logger.info(f"writing artifact file `{prefixed_filename}`")
371381
if not os.path.isdir(ARTIFACTS_DIR):
372382
os.mkdir(ARTIFACTS_DIR)
373-
path = ARTIFACTS_DIR / filename
383+
prefix_dir = ARTIFACTS_DIR / artifact_prefix
384+
os.makedirs(prefix_dir, exist_ok=True)
385+
path = prefix_dir / filename
374386
if filename.endswith(".yml"):
375387
with open(path, "w") as f:
376388
yaml.safe_dump(data, f, allow_unicode=True, default_flow_style=False)
@@ -386,10 +398,11 @@ def write_artifact(filename, data):
386398
raise TypeError(f"Don't know how to write to {filename}")
387399

388400

389-
def read_artifact(filename):
390-
path = ARTIFACTS_DIR / filename
401+
def read_artifact(filename, artifact_prefix="public"):
402+
prefix_dir = ARTIFACTS_DIR / artifact_prefix
403+
path = prefix_dir / filename
391404
if filename.endswith(".yml"):
392-
return load_yaml(path, filename)
405+
return load_yaml(prefix_dir, filename)
393406
elif filename.endswith(".json"):
394407
with open(path) as f:
395408
return json.load(f)
@@ -402,5 +415,6 @@ def read_artifact(filename):
402415
raise TypeError(f"Don't know how to read {filename}")
403416

404417

405-
def rename_artifact(src, dest):
406-
os.rename(ARTIFACTS_DIR / src, ARTIFACTS_DIR / dest)
418+
def rename_artifact(src, dest, artifact_prefix="public"):
419+
prefix_dir = ARTIFACTS_DIR / artifact_prefix
420+
os.rename(prefix_dir / src, prefix_dir / dest)

src/taskgraph/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,11 @@ def load_task(args):
750750
@argument(
751751
"--verbose", "-v", action="store_true", help="include debug-level logging output"
752752
)
753+
@argument(
754+
"--artifact-prefix",
755+
default="public",
756+
help="Prefix for artifact paths (default: public)",
757+
)
753758
def decision(options):
754759
from taskgraph.decision import taskgraph_decision # noqa: PLC0415
755760

src/taskgraph/parameters.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class ParameterMismatch(Exception):
6161
Required("target_tasks_method"): str,
6262
Required("tasks_for"): str,
6363
Required("version"): Any(str, None),
64+
Optional("artifact_prefix"): str,
6465
Optional("code-review"): {
6566
Required("phabricator-build-target"): str,
6667
},

src/taskgraph/util/taskcluster.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,16 @@ def get_artifact_path(task, path):
188188
return f"{get_artifact_prefix(task)}/{path}"
189189

190190

191+
def get_artifact_prefix_from_parameters(parameters):
192+
return parameters.get("artifact_prefix", "public")
193+
194+
195+
def get_artifact_with_prefix(task_id, path, parameters, use_proxy=False):
196+
prefix = get_artifact_prefix_from_parameters(parameters)
197+
prefixed_path = f"{prefix}/{path}"
198+
return get_artifact(task_id, prefixed_path, use_proxy)
199+
200+
191201
def get_index_url(index_path, use_proxy=False, multiple=False):
192202
index_tmpl = liburls.api(get_root_url(use_proxy), "index", "v1", "task{}/{}")
193203
return index_tmpl.format("s" if multiple else "", index_path)

src/taskgraph/util/taskgraph.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
Tools for interacting with existing taskgraphs.
77
"""
88

9-
from taskgraph.util.taskcluster import find_task_id, get_artifact
9+
from taskgraph.util.taskcluster import find_task_id, get_artifact_with_prefix
1010

1111

1212
def find_decision_task(parameters, graph_config):
@@ -35,14 +35,16 @@ def find_decision_task(parameters, graph_config):
3535

3636

3737
def find_existing_tasks_from_previous_kinds(
38-
full_task_graph, previous_graph_ids, rebuild_kinds
38+
full_task_graph, previous_graph_ids, rebuild_kinds, parameters=None
3939
):
4040
"""Given a list of previous decision/action taskIds and kinds to ignore
4141
from the previous graphs, return a dictionary of labels-to-taskids to use
4242
as ``existing_tasks`` in the optimization step."""
4343
existing_tasks = {}
4444
for previous_graph_id in previous_graph_ids:
45-
label_to_taskid = get_artifact(previous_graph_id, "public/label-to-taskid.json")
45+
label_to_taskid = get_artifact_with_prefix(
46+
previous_graph_id, "label-to-taskid.json", parameters
47+
)
4648
kind_labels = {
4749
t.label
4850
for t in full_task_graph.tasks.values()

test/test_actions_util.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import unittest
2+
from unittest import mock
3+
4+
from taskgraph.actions.util import get_parameters
5+
6+
7+
class TestActionsUtil(unittest.TestCase):
8+
@mock.patch("taskgraph.actions.util.get_artifact")
9+
def test_get_parameters_default_prefix(self, mock_get_artifact):
10+
mock_get_artifact.return_value = {"some": "parameters"}
11+
12+
result = get_parameters("task-id-123")
13+
14+
mock_get_artifact.assert_called_once_with(
15+
"task-id-123", "public/parameters.yml"
16+
)
17+
self.assertEqual(result, {"some": "parameters"})
18+
19+
@mock.patch("taskgraph.actions.util.get_artifact")
20+
def test_get_parameters_custom_prefix(self, mock_get_artifact):
21+
mock_get_artifact.return_value = {"some": "parameters"}
22+
23+
result = get_parameters("task-id-123", "custom-prefix")
24+
25+
mock_get_artifact.assert_called_once_with(
26+
"task-id-123", "custom-prefix/parameters.yml"
27+
)
28+
self.assertEqual(result, {"some": "parameters"})

test/test_decision.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def test_write_artifact_json(self):
2626
try:
2727
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
2828
decision.write_artifact("artifact.json", data)
29-
with open(os.path.join(decision.ARTIFACTS_DIR, "artifact.json")) as f:
29+
with open(
30+
os.path.join(decision.ARTIFACTS_DIR, "public", "artifact.json")
31+
) as f:
3032
self.assertEqual(json.load(f), data)
3133
finally:
3234
if os.path.exists(tmpdir):
@@ -39,7 +41,97 @@ def test_write_artifact_yml(self):
3941
try:
4042
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
4143
decision.write_artifact("artifact.yml", data)
42-
self.assertEqual(load_yaml(decision.ARTIFACTS_DIR, "artifact.yml"), data)
44+
self.assertEqual(
45+
load_yaml(decision.ARTIFACTS_DIR / "public", "artifact.yml"), data
46+
)
47+
finally:
48+
if os.path.exists(tmpdir):
49+
shutil.rmtree(tmpdir)
50+
decision.ARTIFACTS_DIR = Path("artifacts")
51+
52+
def test_write_artifact_custom_prefix(self):
53+
data = [{"some": "data"}]
54+
tmpdir = tempfile.mkdtemp()
55+
try:
56+
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
57+
decision.write_artifact("artifact.json", data, "custom-prefix")
58+
with open(
59+
os.path.join(decision.ARTIFACTS_DIR, "custom-prefix", "artifact.json")
60+
) as f:
61+
self.assertEqual(json.load(f), data)
62+
finally:
63+
if os.path.exists(tmpdir):
64+
shutil.rmtree(tmpdir)
65+
decision.ARTIFACTS_DIR = Path("artifacts")
66+
67+
def test_read_artifact_json(self):
68+
data = {"test": "data"}
69+
tmpdir = tempfile.mkdtemp()
70+
try:
71+
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
72+
decision.write_artifact("test.json", data)
73+
result = decision.read_artifact("test.json")
74+
self.assertEqual(result, data)
75+
finally:
76+
if os.path.exists(tmpdir):
77+
shutil.rmtree(tmpdir)
78+
decision.ARTIFACTS_DIR = Path("artifacts")
79+
80+
def test_read_artifact_yml(self):
81+
data = {"test": "data"}
82+
tmpdir = tempfile.mkdtemp()
83+
try:
84+
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
85+
decision.write_artifact("test.yml", data)
86+
result = decision.read_artifact("test.yml")
87+
self.assertEqual(result, data)
88+
finally:
89+
if os.path.exists(tmpdir):
90+
shutil.rmtree(tmpdir)
91+
decision.ARTIFACTS_DIR = Path("artifacts")
92+
93+
def test_read_artifact_custom_prefix(self):
94+
data = {"test": "data"}
95+
tmpdir = tempfile.mkdtemp()
96+
try:
97+
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
98+
decision.write_artifact("test.json", data, "custom")
99+
result = decision.read_artifact("test.json", "custom")
100+
self.assertEqual(result, data)
101+
finally:
102+
if os.path.exists(tmpdir):
103+
shutil.rmtree(tmpdir)
104+
decision.ARTIFACTS_DIR = Path("artifacts")
105+
106+
def test_rename_artifact(self):
107+
data = {"test": "data"}
108+
tmpdir = tempfile.mkdtemp()
109+
try:
110+
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
111+
decision.write_artifact("original.json", data)
112+
decision.rename_artifact("original.json", "renamed.json")
113+
result = decision.read_artifact("renamed.json")
114+
self.assertEqual(result, data)
115+
# Verify original is gone
116+
with self.assertRaises(FileNotFoundError):
117+
decision.read_artifact("original.json")
118+
finally:
119+
if os.path.exists(tmpdir):
120+
shutil.rmtree(tmpdir)
121+
decision.ARTIFACTS_DIR = Path("artifacts")
122+
123+
def test_rename_artifact_custom_prefix(self):
124+
data = {"test": "data"}
125+
tmpdir = tempfile.mkdtemp()
126+
try:
127+
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
128+
decision.write_artifact("original.json", data, "custom")
129+
decision.rename_artifact("original.json", "renamed.json", "custom")
130+
result = decision.read_artifact("renamed.json", "custom")
131+
self.assertEqual(result, data)
132+
# Verify original is gone
133+
with self.assertRaises(FileNotFoundError):
134+
decision.read_artifact("original.json", "custom")
43135
finally:
44136
if os.path.exists(tmpdir):
45137
shutil.rmtree(tmpdir)

0 commit comments

Comments
 (0)