8
8
import argparse
9
9
import json
10
10
import os
11
+ import random
12
+ import string
11
13
import subprocess
12
14
from pathlib import Path
13
15
21
23
]
22
24
23
25
DEFAULT_PLATFORMS = [
24
- ("al2" , "linux_4.14" ),
25
26
("al2" , "linux_5.10" ),
26
27
("al2023" , "linux_6.1" ),
27
28
]
@@ -70,12 +71,12 @@ def group(label, command, instances, platforms, **kwargs):
70
71
if isinstance (command , str ):
71
72
commands = [command ]
72
73
for instance in instances :
73
- for os , kv in platforms :
74
+ for os_ , kv in platforms :
74
75
# fill any templated variables
75
- args = {"instance" : instance , "os" : os , "kv" : kv }
76
+ args = {"instance" : instance , "os" : os_ , "kv" : kv }
76
77
step = {
77
78
"command" : [cmd .format (** args ) for cmd in commands ],
78
- "label" : f"{ label1 } { instance } { os } { kv } " ,
79
+ "label" : f"{ label1 } { instance } { os_ } { kv } " ,
79
80
"agents" : args ,
80
81
}
81
82
step_kwargs = dict_fmt (kwargs , args )
@@ -85,11 +86,6 @@ def group(label, command, instances, platforms, **kwargs):
85
86
return {"group" : label , "steps" : steps }
86
87
87
88
88
- def pipeline_to_json (pipeline ):
89
- """Serialize a pipeline dictionary to JSON"""
90
- return json .dumps (pipeline , indent = 4 , sort_keys = True , ensure_ascii = False )
91
-
92
-
93
89
def get_changed_files ():
94
90
"""
95
91
Get all files changed since `branch`
@@ -119,23 +115,6 @@ def run_all_tests(changed_files):
119
115
)
120
116
121
117
122
- def devtool_test (devtool_opts = None , pytest_opts = None , binary_dir = None ):
123
- """Generate a `devtool test` command"""
124
- cmds = []
125
- parts = ["./tools/devtool -y test" ]
126
- if devtool_opts :
127
- parts .append (devtool_opts )
128
- parts .append ("--" )
129
- if binary_dir is not None :
130
- cmds .append (f'buildkite-agent artifact download "{ binary_dir } /$(uname -m)/*" .' )
131
- cmds .append (f"chmod -v a+x { binary_dir } /**/*" )
132
- parts .append (f"--binary-dir=../{ binary_dir } /$(uname -m)" )
133
- if pytest_opts :
134
- parts .append (pytest_opts )
135
- cmds .append (" " .join (parts ))
136
- return cmds
137
-
138
-
139
118
class DictAction (argparse .Action ):
140
119
"""An argparse action that can receive a nested dictionary
141
120
@@ -192,3 +171,169 @@ def __call__(self, parser, namespace, value, option_string=None):
192
171
default = None ,
193
172
type = str ,
194
173
)
174
+
175
+
176
+ def random_str (k : int ):
177
+ """Generate a random string of hex characters."""
178
+ return "" .join (random .choices (string .hexdigits , k = k ))
179
+
180
+
181
+ def ab_revision_build (revision ):
182
+ """Generate steps for building an A/B-test revision"""
183
+ # Copied from framework/ab_test. Double dollar signs needed for Buildkite (otherwise it will try to interpolate itself)
184
+ return [
185
+ f"commitish={ revision } " ,
186
+ f"if ! git cat-file -t $$commitish; then commitish=origin/{ revision } ; fi" ,
187
+ "branch_name=tmp-$$commitish" ,
188
+ "git branch $$branch_name $$commitish" ,
189
+ f"git clone -b $$branch_name . build/{ revision } " ,
190
+ f"cd build/{ revision } && ./tools/devtool -y build --release && cd -" ,
191
+ ]
192
+
193
+
194
+ def shared_build ():
195
+ """Helper function to make it simple to share a compilation artifacts for a
196
+ whole Buildkite build
197
+ """
198
+
199
+ # We need to support 3 scenarios here:
200
+ # 1. We are running in the nightly pipeline - only compile the HEAD of main.
201
+ # 2. We are running in a PR pipeline - compile HEAD of main as revision A and HEAD of PR branch as revision B.
202
+ # 3. We are running in an A/B-test pipeline - compile what is passed via REVISION_{A,B} environment variables.
203
+ rev_a = os .environ .get ("REVISION_A" )
204
+ if rev_a is not None :
205
+ rev_b = os .environ .get ("REVISION_B" )
206
+ assert rev_b is not None , "REVISION_B environment variable not set"
207
+ build_cmds = ab_revision_build (rev_a ) + ab_revision_build (rev_b )
208
+ elif os .environ .get ("BUILDKITE_PULL_REQUEST" , "false" ) != "false" :
209
+ build_cmds = ab_revision_build (
210
+ os .environ .get ("BUILDKITE_PULL_REQUEST_BASE_BRANCH" , "main" )
211
+ ) + ["./tools/devtool -y build --release" ]
212
+ else :
213
+ build_cmds = ["./tools/devtool -y build --release" ]
214
+ binary_dir = f"build_$(uname -m)_{ random_str (k = 8 )} .tar.gz"
215
+ build_cmds += [
216
+ "du -sh build/*" ,
217
+ f"tar czf { binary_dir } build" ,
218
+ f"buildkite-agent artifact upload { binary_dir } " ,
219
+ ]
220
+ return build_cmds , binary_dir
221
+
222
+
223
+ class BKPipeline :
224
+ """
225
+ Buildkite Pipeline class abstraction
226
+
227
+ Helper class to easily construct pipelines.
228
+ """
229
+
230
+ parser = COMMON_PARSER
231
+
232
+ def __init__ (self , initial_steps = None , ** kwargs ):
233
+ self .steps = []
234
+ self .args = args = self .parser .parse_args ()
235
+ # Retry one time if agent was lost. This can happen if we terminate the
236
+ # instance or the agent gets disconnected for whatever reason
237
+ retry = {
238
+ "automatic" : [{"exit_status" : - 1 , "limit" : 1 }],
239
+ }
240
+ retry = overlay_dict (retry , kwargs .pop ("retry" , {}))
241
+ # Calculate step defaults with parameters and kwargs
242
+ per_instance = {
243
+ "instances" : args .instances ,
244
+ "platforms" : args .platforms ,
245
+ "artifact_paths" : ["./test_results/**/*" ],
246
+ "retry" : retry ,
247
+ ** kwargs ,
248
+ }
249
+ self .per_instance = overlay_dict (per_instance , args .step_param )
250
+ self .per_arch = self .per_instance .copy ()
251
+ self .per_arch ["instances" ] = ["m6i.metal" , "m7g.metal" ]
252
+ self .per_arch ["platforms" ] = [("al2" , "linux_5.10" )]
253
+ self .binary_dir = args .binary_dir
254
+ # Build sharing
255
+ build_cmds , self .shared_build = shared_build ()
256
+ step_build = group ("🏗️ Build" , build_cmds , ** self .per_arch )
257
+ self .steps += [step_build , "wait" ]
258
+
259
+ # If we run initial_steps before the "wait" step above, then a failure of the initial steps
260
+ # would result in the build not progressing past the "wait" step (as buildkite only proceeds past a wait step
261
+ # if everything before it passed). Thus put the initial steps after the "wait" step, but set `"depends_on": null`
262
+ # to start running them immediately (e.g. without waiting for the "wait" step to unblock).
263
+ #
264
+ # See also https://buildkite.com/docs/pipelines/dependencies#explicit-dependencies-in-uploaded-steps
265
+ if initial_steps :
266
+ for step in initial_steps :
267
+ step ["depends_on" ] = None
268
+
269
+ self .steps += initial_steps
270
+
271
+ def add_step (self , step , decorate = True ):
272
+ """
273
+ Add a step to the pipeline.
274
+
275
+ https://buildkite.com/docs/pipelines/step-reference
276
+
277
+ :param step: a Buildkite step
278
+ :param decorate: inject needed commands for sharing builds
279
+ """
280
+ if decorate and isinstance (step , dict ):
281
+ step = self ._adapt_group (step )
282
+ self .steps .append (step )
283
+ return step
284
+
285
+ def _adapt_group (self , group ):
286
+ """"""
287
+ prepend = [
288
+ f'buildkite-agent artifact download "{ self .shared_build } " .' ,
289
+ f"tar xzf { self .shared_build } " ,
290
+ ]
291
+ if self .binary_dir is not None :
292
+ prepend .extend (
293
+ [
294
+ f'buildkite-agent artifact download "{ self .binary_dir } /$(uname -m)/*" .' ,
295
+ f"chmod -v a+x { self .binary_dir } /**/*" ,
296
+ ]
297
+ )
298
+
299
+ for step in group ["steps" ]:
300
+ step ["command" ] = prepend + step ["command" ]
301
+ return group
302
+
303
+ def build_group (self , * args , ** kwargs ):
304
+ """
305
+ Build a group, parametrizing over the selected instances/platforms.
306
+
307
+ https://buildkite.com/docs/pipelines/group-step
308
+ """
309
+ combined = overlay_dict (self .per_instance , kwargs )
310
+ return self .add_step (group (* args , ** combined ))
311
+
312
+ def build_group_per_arch (self , * args , ** kwargs ):
313
+ """
314
+ Build a group, parametrizing over the architectures only.
315
+ """
316
+ combined = overlay_dict (self .per_arch , kwargs )
317
+ return self .add_step (group (* args , ** combined ))
318
+
319
+ def to_dict (self ):
320
+ """Render the pipeline as a dictionary."""
321
+ return {"steps" : self .steps }
322
+
323
+ def to_json (self ):
324
+ """Serialize the pipeline to JSON"""
325
+ return json .dumps (self .to_dict (), indent = 4 , sort_keys = True , ensure_ascii = False )
326
+
327
+ def devtool_test (self , devtool_opts = None , pytest_opts = None ):
328
+ """Generate a `devtool test` command"""
329
+ cmds = []
330
+ parts = ["./tools/devtool -y test" , "--no-build" ]
331
+ if devtool_opts :
332
+ parts .append (devtool_opts )
333
+ parts .append ("--" )
334
+ if self .binary_dir is not None :
335
+ parts .append (f"--binary-dir=../{ self .binary_dir } /$(uname -m)" )
336
+ if pytest_opts :
337
+ parts .append (pytest_opts )
338
+ cmds .append (" " .join (parts ))
339
+ return cmds
0 commit comments