Skip to content

Commit 5a7d3f6

Browse files
pb8owearyzen
authored andcommitted
ci: revamp passing options to Buildkite steps
Until now, we could only pass top-level parameters to a Buildkite step. After this change we can pass nested parameters to the buildkite pipeline. For example: pipeline_pr.py --step-param agents/queue=q --step-param env/VAR=7 "agents": { "os": "al2", "queue": "q" }, "env": {"VAR": "7"}, It supersedes the previous `--step-env` workaround to pass environment variables. Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent 6dedfa5 commit 5a7d3f6

File tree

5 files changed

+82
-47
lines changed

5 files changed

+82
-47
lines changed

.buildkite/common.py

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,42 @@
2323
]
2424

2525

26+
def overlay_dict(base: dict, update: dict):
27+
"""Overlay a dict over a base one"""
28+
base = base.copy()
29+
for key, val in update.items():
30+
if key in base and isinstance(val, dict):
31+
base[key] = overlay_dict(base.get(key, {}), val)
32+
else:
33+
base[key] = val
34+
return base
35+
36+
2637
def field_fmt(field, args):
2738
"""If `field` is a string, interpolate variables in `args`"""
2839
if not isinstance(field, str):
2940
return field
3041
return field.format(**args)
3142

3243

33-
def group(label, command, instances, platforms, agent_tags=None, **kwargs):
44+
def dict_fmt(dict_tmpl, args):
45+
"""Apply field_fmt over a hole dict"""
46+
res = {}
47+
for key, val in dict_tmpl.items():
48+
if isinstance(val, dict):
49+
res[key] = dict_fmt(val, args)
50+
else:
51+
res[key] = field_fmt(val, args)
52+
return res
53+
54+
55+
def group(label, command, instances, platforms, **kwargs):
3456
"""
3557
Generate a group step with specified parameters, for each instance+kernel
3658
combination
3759
3860
https://buildkite.com/docs/pipelines/group-step
3961
"""
40-
if agent_tags is None:
41-
agent_tags = []
4262
# Use the 1st character of the group name (should be an emoji)
4363
label1 = label[0]
4464
steps = []
@@ -48,16 +68,14 @@ def group(label, command, instances, platforms, agent_tags=None, **kwargs):
4868
for instance in instances:
4969
for os, kv in platforms:
5070
# fill any templated variables
51-
args = {"os": os, "kv": kv, "instance": instance}
52-
step_commands = [cmd.format(**args) for cmd in commands]
53-
step_kwargs = {key: field_fmt(val, args) for key, val in kwargs.items()}
54-
agents = [f"instance={instance}", f"kv={kv}", f"os={os}"] + agent_tags
71+
args = {"instance": instance, "os": os, "kv": kv}
5572
step = {
56-
"command": step_commands,
73+
"command": [cmd.format(**args) for cmd in commands],
5774
"label": f"{label1} {instance} {os} {kv}",
58-
"agents": agents,
59-
**step_kwargs,
75+
"agents": args,
6076
}
77+
step_kwargs = dict_fmt(kwargs, args)
78+
step = overlay_dict(step_kwargs, step)
6179
steps.append(step)
6280

6381
return {"group": label, "steps": steps}
@@ -68,6 +86,31 @@ def pipeline_to_json(pipeline):
6886
return json.dumps(pipeline, indent=4, sort_keys=True, ensure_ascii=False)
6987

7088

89+
class DictAction(argparse.Action):
90+
"""An argparse action that can receive a nested dictionary
91+
92+
Examples:
93+
94+
--step-param a/b/c=3
95+
{"a": {"b": {"c": 3}}}
96+
"""
97+
98+
def __init__(self, option_strings, dest, nargs=None, **kwargs):
99+
if nargs is not None:
100+
raise ValueError("nargs not allowed")
101+
super().__init__(option_strings, dest, **kwargs)
102+
103+
def __call__(self, parser, namespace, value, option_string=None):
104+
res = getattr(namespace, self.dest, {})
105+
key_str, val = value.split("=", maxsplit=1)
106+
keys = key_str.split("/")
107+
update = {keys[-1]: val}
108+
for key in list(reversed(keys))[1:]:
109+
update = {key: update}
110+
res = overlay_dict(res, update)
111+
setattr(namespace, self.dest, res)
112+
113+
71114
COMMON_PARSER = argparse.ArgumentParser()
72115
COMMON_PARSER.add_argument(
73116
"--instances",
@@ -88,16 +131,7 @@ def pipeline_to_json(pipeline):
88131
metavar="PARAM=VALUE",
89132
help="parameters to add to each step",
90133
required=False,
91-
action="append",
92-
default=[],
93-
type=lambda arg: tuple(arg.split("=", maxsplit=1)),
94-
)
95-
COMMON_PARSER.add_argument(
96-
"--step-env",
97-
metavar="KEY=VALUE",
98-
help="environment to use in each step",
99-
required=False,
100-
action="append",
101-
default=[],
102-
type=lambda arg: tuple(arg.split("=", maxsplit=1)),
134+
action=DictAction,
135+
default={},
136+
type=str,
103137
)

.buildkite/pipeline_cross.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def restore_step(label, src_instance, src_kv, dst_instance, dst_os, dst_kv):
3030
],
3131
"label": label,
3232
"timeout": 30,
33-
"agents": [f"instance={dst_instance}", f"kv={dst_kv}", f"os={dst_os}"],
33+
"agents": {"instance": dst_instance, "kv": dst_kv, "os": dst_os},
3434
}
3535

3636

.buildkite/pipeline_perf.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
"""Generate Buildkite performance pipelines dynamically"""
66

7-
from common import COMMON_PARSER, group, pipeline_to_json
7+
from common import COMMON_PARSER, group, overlay_dict, pipeline_to_json
88

99
perf_test = {
1010
"block": {
@@ -48,7 +48,6 @@ def build_group(test):
4848
return group(
4949
label=test.pop("label"),
5050
command=f"./tools/devtool -y test {devtool_opts} -- -m nonci --reruns {retries} {test_path}",
51-
agent_tags=["ag=1"],
5251
artifacts=["./test_results/*"],
5352
instances=test.pop("instances"),
5453
platforms=test.pop("platforms"),
@@ -72,10 +71,10 @@ def build_group(test):
7271
for test_data in tests:
7372
test_data.setdefault("platforms", args.platforms)
7473
test_data.setdefault("instances", args.instances)
75-
test_data["env"] = dict(args.step_env)
74+
test_data.setdefault("agents", {"ag": 1})
7675
test_data["retries"] = args.retries
7776
test_data["timeout_in_minutes"] *= args.retries + 1
78-
test_data.update(args.step_param)
77+
test_data = overlay_dict(test_data, args.step_param)
7978
test_data["retry"] = {
8079
"automatic": [
8180
# Agent was lost, retry one time

.buildkite/pipeline_pr.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import subprocess
88
from pathlib import Path
99

10-
from common import COMMON_PARSER, group, pipeline_to_json
10+
from common import COMMON_PARSER, group, overlay_dict, pipeline_to_json
1111

1212
# Buildkite default job priority is 0. Setting this to 1 prioritizes PRs over
1313
# scheduled jobs and other batch jobs.
@@ -36,10 +36,9 @@ def get_changed_files(branch):
3636
# buildkite step parameters
3737
"priority": DEFAULT_PRIORITY,
3838
"timeout_in_minutes": 45,
39-
"env": dict(args.step_env),
4039
"artifacts": ["./test_results/**/*"],
4140
}
42-
defaults.update(args.step_param)
41+
defaults = overlay_dict(defaults, args.step_param)
4342

4443
build_grp = group(
4544
"📦 Build",
@@ -65,12 +64,14 @@ def get_changed_files(branch):
6564
**defaults,
6665
)
6766

68-
defaults_for_performance = defaults.copy()
69-
defaults_for_performance.update(
70-
# We specify higher priority so the ag=1 jobs get picked up before the ag=n
71-
# jobs in ag=1 agents
72-
priority=DEFAULT_PRIORITY + 1,
73-
agent_tags=["ag=1"],
67+
defaults_for_performance = overlay_dict(
68+
defaults,
69+
{
70+
# We specify higher priority so the ag=1 jobs get picked up before the ag=n
71+
# jobs in ag=1 agents
72+
"priority": DEFAULT_PRIORITY + 1,
73+
"agents": {"ag": 1},
74+
},
7475
)
7576

7677
performance_grp = group(
@@ -79,13 +80,14 @@ def get_changed_files(branch):
7980
**defaults_for_performance,
8081
)
8182

82-
defaults_for_kani = defaults.copy()
83-
defaults_for_kani.update(
84-
# Kani runs fastest on m6i.metal
85-
instances=["m6i.metal"],
86-
platforms=[("al2", "linux_5.10")],
87-
timeout_in_minutes=300,
88-
agent_tags=["ag=1"],
83+
defaults_for_kani = overlay_dict(
84+
defaults_for_performance,
85+
{
86+
# Kani runs fastest on m6i.metal
87+
"instances": ["m6i.metal"],
88+
"platforms": [("al2", "linux_5.10")],
89+
"timeout_in_minutes": 300,
90+
},
8991
)
9092

9193
kani_grp = group(

.buildkite/pipeline_pr_no_block.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
"""Generate Buildkite pipelines dynamically"""
66

7-
from common import COMMON_PARSER, group, pipeline_to_json
7+
from common import COMMON_PARSER, group, overlay_dict, pipeline_to_json
88

99
# Buildkite default job priority is 0. Setting this to 1 prioritizes PRs over
1010
# scheduled jobs and other batch jobs.
@@ -17,12 +17,12 @@
1717
"platforms": args.platforms,
1818
# buildkite step parameters
1919
"timeout_in_minutes": 45,
20-
"env": dict(args.step_env),
2120
# some non-blocking tests are performance, so make sure they get ag=1 instances
2221
"priority": DEFAULT_PRIORITY + 1,
23-
"agent_tags": ["ag=1"],
22+
"agents": {"ag": 1},
2423
}
25-
defaults.update(args.step_param)
24+
defaults = overlay_dict(defaults, args.step_param)
25+
2626

2727
optional_grp = group(
2828
"❓ Optional",

0 commit comments

Comments
 (0)