Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4f06bee
arg replacement for custom recipes
canercidam Feb 9, 2026
514ce99
Merge branch 'caner/buildernet-recipe-support' into caner/buildernet-…
canercidam Feb 9, 2026
a3d0661
use the directory hierarchy for including custom recipes in the CLI
canercidam Feb 10, 2026
30eca91
fix lint error
canercidam Feb 10, 2026
f839a13
Merge branch 'caner/buildernet-recipe-support-2' into caner/builderne…
canercidam Feb 10, 2026
e423bd7
Merge branch 'caner/buildernet-recipe-support' into caner/buildernet-…
canercidam Feb 10, 2026
f27caef
move flags to the custom recipe file
canercidam Feb 10, 2026
d344f1f
internalize buildernet recipe overrides and move hook execution
canercidam Feb 10, 2026
1f0ee2d
Update CODEOWNERS (#361)
canercidam Feb 10, 2026
a8a3d07
detect ovmf files dynamically
canercidam Feb 10, 2026
98b8eb0
Use the directory hierarchy for including custom recipes in the CLI (…
canercidam Feb 10, 2026
11e8c7e
Update rbuilder custom recipes (#359)
canercidam Feb 10, 2026
e67dc87
Definining lifecycle of custom services in recipes (#363)
canercidam Feb 11, 2026
2d00abc
fix startup issues
canercidam Feb 11, 2026
ef272eb
fix integration tests
canercidam Feb 11, 2026
f969390
Start docker and host services concurrently (#364)
canercidam Feb 11, 2026
53f9f7a
run lifecycle hooks separately
canercidam Feb 11, 2026
6f4ca9b
Merge branch 'main' into caner/buildernet-recipe
canercidam Feb 11, 2026
75cd558
Flag replacement for custom recipes (#357)
canercidam Feb 11, 2026
8c74579
Merge branch 'main' into caner/buildernet-recipe
canercidam Feb 11, 2026
d701e8a
add missing test case
canercidam Feb 11, 2026
e36d0ef
unify recipe and make fixes
canercidam Feb 11, 2026
9b38545
generate docs
canercidam Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions custom-recipes/buildernet/mkosi/playground.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
base: buildernet
description: Deploy the stack with the BuilderNet mkosi image (QEMU)
53 changes: 53 additions & 0 deletions custom-recipes/rbuilder/custom/rbuilder.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
log_json = false
log_level = "info,rbuilder=debug"
redacted_telemetry_server_port = 6061
redacted_telemetry_server_ip = "0.0.0.0"
full_telemetry_server_port = 6060
full_telemetry_server_ip = "0.0.0.0"

# Paths relative to artifacts directory
chain = "genesis.json"
reth_datadir = "volume-el-data"
el_node_ipc_path = "volume-el-data/reth.ipc"

# Ethereum private key for coinbase (receives builder fees)
# This is the first prefunded account (0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266)
coinbase_secret_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"

# BLS secret key for signing relay submissions
relay_secret_key = "0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866"

cl_node_url = ["http://localhost:3500"]
jsonrpc_server_port = 8645
jsonrpc_server_ip = "0.0.0.0"
extra_data = "Playground Builder ⚡🤖"

ignore_cancellable_orders = true

# Required sparse trie settings
root_hash_use_sparse_trie = true
root_hash_compare_sparse_trie = false

# Start bidding immediately
slot_delta_to_start_bidding_ms = -20000

live_builders = ["mp-ordering"]

enabled_relays = ["playground"]

# Local mev-boost-relay for slot info and block submission
[[relays]]
name = "playground"
url = "http://localhost:5555"
priority = 0
use_ssz_for_submit = false
use_gzip_for_submit = false
mode = "full"

[[builders]]
name = "mp-ordering"
algo = "ordering-builder"
discard_txs = true
sorting = "max-profit"
failed_order_retries = 1
drop_failed_orders = true
26 changes: 12 additions & 14 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,18 @@ func runIt(recipe playground.Recipe) error {
return fmt.Errorf("failed to run docker: %w", err)
}

slog.Info("Waiting for services to get healthy... ⏳")
waitCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
if err := dockerRunner.WaitForReady(waitCtx); err != nil {
return fmt.Errorf("failed to wait for service readiness: %w", err)
}

// run post hook operations
if err := svcManager.ExecutePostHookActions(ctx); err != nil {
return fmt.Errorf("failed to execute post-hook operations: %w", err)
}

if !interactive {
log.Println()
log.Println("All services started! ✅")
Expand Down Expand Up @@ -789,20 +801,6 @@ func runIt(recipe playground.Recipe) error {
log.Println()
}

log.Println("Waiting for services to get healthy... ⏳")
waitCtx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel()
if err := dockerRunner.WaitForReady(waitCtx); err != nil {
dockerRunner.Stop(keepFlag)
return fmt.Errorf("failed to wait for service readiness: %w", err)
}

// run post hook operations
if err := svcManager.ExecutePostHookActions(); err != nil {
dockerRunner.Stop(keepFlag)
return fmt.Errorf("failed to execute post-hook operations: %w", err)
}

slog.Info("All services are healthy! Ready to accept transactions. 🚀", "session-id", svcManager.ID)

// get the output from the recipe
Expand Down
33 changes: 33 additions & 0 deletions playground/cmd_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,45 @@ func validateYAMLRecipe(recipe *YAMLRecipe, baseRecipes []Recipe, result *Valida
if serviceConfig.Remove {
result.AddWarning("removing service '%s' from component '%s' - verify names match base recipe", serviceName, componentName)
}

// Validate lifecycle cannot be used with host_path, release, or args
validateLifecycleConfig(serviceName, componentName, serviceConfig, result)
}
}
}
}
}

// validateLifecycleConfig checks that lifecycle_hooks is not used with incompatible options
// and that init/start/stop are only used when lifecycle_hooks is true
func validateLifecycleConfig(serviceName, componentName string, config *YAMLServiceConfig, result *ValidationResult) {
hasLifecycleFields := len(config.Init) > 0 || config.Start != "" || len(config.Stop) > 0

// If lifecycle_hooks is not set but lifecycle fields are used, that's an error
if !config.LifecycleHooks {
if hasLifecycleFields {
result.AddError("service '%s' in component '%s': init, start, and stop require lifecycle_hooks: true", serviceName, componentName)
}
return
}

// lifecycle_hooks is true - check for incompatible options
if config.HostPath != "" {
result.AddError("service '%s' in component '%s': lifecycle_hooks cannot be used with host_path", serviceName, componentName)
}
if config.Release != nil {
result.AddError("service '%s' in component '%s': lifecycle_hooks cannot be used with release", serviceName, componentName)
}
if len(config.Args) > 0 {
result.AddError("service '%s' in component '%s': lifecycle_hooks cannot be used with args", serviceName, componentName)
}

// Validate that at least one of init or start is specified
if len(config.Init) == 0 && config.Start == "" {
result.AddError("service '%s' in component '%s': lifecycle_hooks requires at least one of init or start", serviceName, componentName)
}
}

func validateUniqueServiceNames(manifest *Manifest, result *ValidationResult) {
seen := make(map[string]bool)
for _, svc := range manifest.Services {
Expand Down
245 changes: 245 additions & 0 deletions playground/cmd_validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package playground

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestValidateLifecycleConfig(t *testing.T) {
tests := []struct {
name string
config *YAMLServiceConfig
expectedErrors int
errorContains []string
}{
{
name: "valid lifecycle config with init start and stop",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Init: []string{"echo init"},
Start: "./start.sh",
Stop: []string{"echo stop"},
},
expectedErrors: 0,
},
{
name: "valid lifecycle config with only init",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Init: []string{"echo init"},
},
expectedErrors: 0,
},
{
name: "valid lifecycle config with init and stop",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Init: []string{"echo init"},
Stop: []string{"echo stop"},
},
expectedErrors: 0,
},
{
name: "valid lifecycle config with only start",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Start: "./start.sh",
},
expectedErrors: 0,
},
{
name: "lifecycle_hooks with host_path",
config: &YAMLServiceConfig{
LifecycleHooks: true,
HostPath: "/usr/bin/app",
Start: "./start.sh",
},
expectedErrors: 1,
errorContains: []string{"lifecycle_hooks cannot be used with host_path"},
},
{
name: "lifecycle_hooks with release",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Release: &YAMLReleaseConfig{
Name: "app",
Org: "org",
Version: "v1.0.0",
},
Start: "./start.sh",
},
expectedErrors: 1,
errorContains: []string{"lifecycle_hooks cannot be used with release"},
},
{
name: "lifecycle_hooks with args",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Args: []string{"--port", "8080"},
Start: "./start.sh",
},
expectedErrors: 1,
errorContains: []string{"lifecycle_hooks cannot be used with args"},
},
{
name: "lifecycle_hooks with only stop - invalid",
config: &YAMLServiceConfig{
LifecycleHooks: true,
Stop: []string{"echo stop"},
},
expectedErrors: 1,
errorContains: []string{"lifecycle_hooks requires at least one of init or start"},
},
{
name: "lifecycle_hooks with nothing - invalid",
config: &YAMLServiceConfig{
LifecycleHooks: true,
},
expectedErrors: 1,
errorContains: []string{"lifecycle_hooks requires at least one of init or start"},
},
{
name: "lifecycle_hooks with all incompatible options",
config: &YAMLServiceConfig{
LifecycleHooks: true,
HostPath: "/usr/bin/app",
Release: &YAMLReleaseConfig{
Name: "app",
Org: "org",
Version: "v1.0.0",
},
Args: []string{"--port", "8080"},
Start: "./start.sh",
},
expectedErrors: 3,
errorContains: []string{
"lifecycle_hooks cannot be used with host_path",
"lifecycle_hooks cannot be used with release",
"lifecycle_hooks cannot be used with args",
},
},
{
name: "no lifecycle_hooks - no errors",
config: &YAMLServiceConfig{
HostPath: "/usr/bin/app",
Args: []string{"--port", "8080"},
},
expectedErrors: 0,
},
{
name: "init without lifecycle_hooks - invalid",
config: &YAMLServiceConfig{
HostPath: "/usr/bin/app",
Init: []string{"echo init"},
},
expectedErrors: 1,
errorContains: []string{"init, start, and stop require lifecycle_hooks: true"},
},
{
name: "start without lifecycle_hooks - invalid",
config: &YAMLServiceConfig{
HostPath: "/usr/bin/app",
Start: "./start.sh",
},
expectedErrors: 1,
errorContains: []string{"init, start, and stop require lifecycle_hooks: true"},
},
{
name: "stop without lifecycle_hooks - invalid",
config: &YAMLServiceConfig{
HostPath: "/usr/bin/app",
Stop: []string{"echo stop"},
},
expectedErrors: 1,
errorContains: []string{"init, start, and stop require lifecycle_hooks: true"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := &ValidationResult{}
validateLifecycleConfig("test-svc", "test-component", tt.config, result)

require.Len(t, result.Errors, tt.expectedErrors)
for _, expected := range tt.errorContains {
found := false
for _, err := range result.Errors {
if strings.Contains(err, expected) {
found = true
break
}
}
require.True(t, found, "expected error containing '%s' not found in %v", expected, result.Errors)
}
})
}
}

func TestValidateLifecycleConfig_InYAMLRecipe(t *testing.T) {
// Test that lifecycle validation is called during YAML recipe validation
recipe := &YAMLRecipe{
config: &YAMLRecipeConfig{
Base: "l1",
Recipe: map[string]*YAMLComponentConfig{
"test-component": {
Services: map[string]*YAMLServiceConfig{
"test-svc": {
LifecycleHooks: true,
HostPath: "/usr/bin/app",
Start: "./start.sh",
},
},
},
},
},
}

baseRecipes := []Recipe{&L1Recipe{}}
result := &ValidationResult{}
validateYAMLRecipe(recipe, baseRecipes, result)

require.NotEmpty(t, result.Errors)
found := false
for _, err := range result.Errors {
if strings.Contains(err, "lifecycle_hooks cannot be used with host_path") {
found = true
break
}
}
require.True(t, found, "expected lifecycle validation error not found")
}

func TestValidateLifecycleConfig_InYAMLRecipe_WithoutLifecycleHooks(t *testing.T) {
// Test that init/start/stop without lifecycle_hooks is caught during YAML recipe validation
recipe := &YAMLRecipe{
config: &YAMLRecipeConfig{
Base: "l1",
Recipe: map[string]*YAMLComponentConfig{
"test-component": {
Services: map[string]*YAMLServiceConfig{
"test-svc": {
HostPath: "/usr/bin/app",
Start: "./start.sh",
},
},
},
},
},
}

baseRecipes := []Recipe{&L1Recipe{}}
result := &ValidationResult{}
validateYAMLRecipe(recipe, baseRecipes, result)

require.NotEmpty(t, result.Errors)
found := false
for _, err := range result.Errors {
if strings.Contains(err, "init, start, and stop require lifecycle_hooks: true") {
found = true
break
}
}
require.True(t, found, "expected lifecycle validation error not found in %v", result.Errors)
}
Loading
Loading