Skip to content

Commit 8a3e173

Browse files
mahzounbdchatham
andauthored
feat: add override support for devkit avs build target (#292)
<!-- 🚨 ATTENTION! 🚨 This PR template is REQUIRED. PRs not following this format will be closed without review. Requirements: - PR title must follow commit conventions: https://www.conventionalcommits.org/en/v1.0.0/ - Label your PR with the correct type (e.g., 🐛 Bug, ✨ Enhancement, 🧪 Test, etc.) - Provide clear and specific details in each section --> **Motivation:** Enable AVS developers whose performer code lives outside the generated Go project to plug in their own build orchestration without modifying `.devkit/scripts/build` or the root Dockerfile. **Modifications:** - Added a `--target` flag to `devkit avs build` so callers can invoke an alternate build script while keeping the existing default. - Forwarded positional CLI arguments to the selected script, preserving artifact metadata updates. - Introduced unit tests covering custom targets, argument forwarding, and missing-script validation. **Result:** Developers can run `devkit avs build --target <path> -- ...` to execute bespoke build logic (e.g., alternate Dockerfiles or external repos) while downstream tooling still gets the artifact details it expects. Closes #291. **Testing:** - `GOCACHE=$(mktemp -d) go test ./pkg/commands` **Open questions:** Should the chosen build target be persisted per context/project to avoid re-specifying it for every build? --------- Co-authored-by: FromTheRain <[email protected]> Co-authored-by: bdchatham <[email protected]>
1 parent 7ac3227 commit 8a3e173

File tree

4 files changed

+179
-6
lines changed

4 files changed

+179
-6
lines changed

.github/workflows/devnet.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ env:
1616

1717
jobs:
1818
devnet-test:
19+
if: github.event.pull_request.head.repo.full_name == github.repository
1920
strategy:
2021
fail-fast: true
2122
runs-on: ubuntu-latest

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ env:
1717
jobs:
1818
e2e:
1919
runs-on: ubuntu-latest
20-
if: github.repository == 'Layr-Labs/devkit-cli'
20+
if: github.event.pull_request.head.repo.full_name == github.repository
2121
steps:
2222
- name: Checkout repository
2323
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

pkg/commands/build.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package commands
22

33
import (
44
"fmt"
5+
"os"
56
"path/filepath"
67

78
"github.com/Layr-Labs/devkit-cli/config/configs"
@@ -22,6 +23,10 @@ var BuildCommand = &cli.Command{
2223
Name: "context",
2324
Usage: "Select the context to use in this command (devnet, testnet or mainnet)",
2425
},
26+
&cli.StringFlag{
27+
Name: "target",
28+
Usage: "Override the build script target (relative or absolute path)",
29+
},
2530
}, common.GlobalFlags...),
2631
Action: func(cCtx *cli.Context) error {
2732
logger := common.LoggerFromContext(cCtx)
@@ -81,18 +86,43 @@ var BuildCommand = &cli.Command{
8186
logger.Debug("Project Name: %s", cfg.Config.Project.Name)
8287
logger.Debug("Building AVS components...")
8388

84-
// All scripts contained here
85-
scriptsDir := filepath.Join(".devkit", "scripts")
89+
// Resolve target script (defaults to .devkit/scripts/build)
90+
defaultScript := filepath.Join(".devkit", "scripts", "build")
91+
scriptPath := defaultScript
92+
93+
if override := cCtx.String("target"); override != "" {
94+
if filepath.IsAbs(override) {
95+
scriptPath = override
96+
} else {
97+
scriptPath = filepath.Join(".", filepath.Clean(override))
98+
}
99+
100+
if _, statErr := os.Stat(scriptPath); statErr != nil {
101+
if os.IsNotExist(statErr) {
102+
return fmt.Errorf("custom build target not found: %s", scriptPath)
103+
}
104+
return fmt.Errorf("failed to access custom build target %s: %w", scriptPath, statErr)
105+
}
106+
107+
logger.Info("Using custom build target: %s", scriptPath)
108+
}
86109

87-
// Execute build via .devkit scripts with project name
88-
output, err := common.CallTemplateScript(cCtx.Context, logger, dir, filepath.Join(scriptsDir, "build"), common.ExpectJSONResponse,
110+
// Execute build via script with project name
111+
params := [][]byte{
89112
[]byte("--image"),
90113
[]byte(cfg.Config.Project.Name),
91114
[]byte("--tag"),
92115
[]byte(version),
93116
[]byte("--lang"),
94117
[]byte(language),
95-
)
118+
}
119+
120+
// Forward any additional positional arguments to the build script
121+
for _, arg := range cCtx.Args().Slice() {
122+
params = append(params, []byte(arg))
123+
}
124+
125+
output, err := common.CallTemplateScript(cCtx.Context, logger, dir, scriptPath, common.ExpectJSONResponse, params...)
96126
if err != nil {
97127
logger.Error("Build script failed with error: %v", err)
98128
return fmt.Errorf("build failed: %w", err)

pkg/commands/build_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package commands
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"os"
78
"path/filepath"
9+
"strings"
810
"testing"
911
"time"
1012

@@ -204,3 +206,143 @@ echo "Mock build executed"`
204206
t.Error("Build command did not exit after context cancellation")
205207
}
206208
}
209+
210+
func TestBuildCommand_CustomTarget(t *testing.T) {
211+
tmpDir := t.TempDir()
212+
213+
// Create config directory and devnet.yaml
214+
configDir := filepath.Join(tmpDir, "config")
215+
if err := os.MkdirAll(configDir, 0755); err != nil {
216+
t.Fatal(err)
217+
}
218+
contextsDir := filepath.Join(configDir, "contexts")
219+
if err := os.MkdirAll(contextsDir, 0755); err != nil {
220+
t.Fatal(err)
221+
}
222+
if err := os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configs.ConfigYamls[configs.LatestVersion]), 0644); err != nil {
223+
t.Fatal(err)
224+
}
225+
if err := os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contexts.ContextYamls[contexts.LatestVersion]), 0644); err != nil {
226+
t.Fatal(err)
227+
}
228+
229+
// Create custom userland build script
230+
userlandDir := filepath.Join(tmpDir, "userland")
231+
if err := os.MkdirAll(userlandDir, 0755); err != nil {
232+
t.Fatal(err)
233+
}
234+
capturePath := filepath.Join(tmpDir, "captured_args.txt")
235+
customScript := fmt.Sprintf(`#!/bin/bash
236+
echo "$@" > "%s"
237+
cat <<'EOF'
238+
{"artifact":{"artifactId":"custom-artifact","component":"custom-component"}}
239+
EOF
240+
`, capturePath)
241+
customScriptPath := filepath.Join(userlandDir, "build")
242+
if err := os.WriteFile(customScriptPath, []byte(customScript), 0755); err != nil {
243+
t.Fatal(err)
244+
}
245+
246+
oldWd, err := os.Getwd()
247+
if err != nil {
248+
t.Fatal(err)
249+
}
250+
if err := os.Chdir(tmpDir); err != nil {
251+
t.Fatal(err)
252+
}
253+
defer func() {
254+
if err := os.Chdir(oldWd); err != nil {
255+
t.Logf("Failed to restore original directory: %v", err)
256+
}
257+
}()
258+
259+
app := &cli.App{
260+
Name: "test",
261+
Commands: []*cli.Command{testutils.WithTestConfigAndNoopLogger(BuildCommand)},
262+
}
263+
264+
args := []string{
265+
"app", "build",
266+
"--context", devnet.DEVNET_CONTEXT,
267+
"--target", "userland/build",
268+
"--",
269+
"--dockerfile", "path/to/Dockerfile",
270+
"--context", "custom-performer",
271+
}
272+
273+
if err := app.Run(args); err != nil {
274+
t.Fatalf("Failed to execute build command with custom target: %v", err)
275+
}
276+
277+
// Ensure custom script captured the forwarded arguments
278+
captured, err := os.ReadFile(capturePath)
279+
if err != nil {
280+
t.Fatalf("Failed to read captured args: %v", err)
281+
}
282+
capturedArgs := strings.TrimSpace(string(captured))
283+
if !strings.Contains(capturedArgs, "--dockerfile path/to/Dockerfile") {
284+
t.Fatalf("Expected forwarded args to include dockerfile flag, got: %s", capturedArgs)
285+
}
286+
if !strings.Contains(capturedArgs, "--context custom-performer") {
287+
t.Fatalf("Expected forwarded args to include performer context flag, got: %s", capturedArgs)
288+
}
289+
290+
// Context file should have been updated with custom artifact info
291+
contextBytes, err := os.ReadFile(filepath.Join(configDir, "contexts", "devnet.yaml"))
292+
if err != nil {
293+
t.Fatalf("Failed to read context file: %v", err)
294+
}
295+
contextStr := string(contextBytes)
296+
if !strings.Contains(contextStr, "artifactId: custom-artifact") {
297+
t.Fatalf("Expected context to include custom artifactId, got: %s", contextStr)
298+
}
299+
if !strings.Contains(contextStr, "component: custom-component") {
300+
t.Fatalf("Expected context to include custom component, got: %s", contextStr)
301+
}
302+
}
303+
304+
func TestBuildCommand_CustomTargetMissing(t *testing.T) {
305+
tmpDir := t.TempDir()
306+
307+
// Create config directory and devnet.yaml
308+
configDir := filepath.Join(tmpDir, "config")
309+
if err := os.MkdirAll(configDir, 0755); err != nil {
310+
t.Fatal(err)
311+
}
312+
contextsDir := filepath.Join(configDir, "contexts")
313+
if err := os.MkdirAll(contextsDir, 0755); err != nil {
314+
t.Fatal(err)
315+
}
316+
if err := os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configs.ConfigYamls[configs.LatestVersion]), 0644); err != nil {
317+
t.Fatal(err)
318+
}
319+
if err := os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contexts.ContextYamls[contexts.LatestVersion]), 0644); err != nil {
320+
t.Fatal(err)
321+
}
322+
323+
oldWd, err := os.Getwd()
324+
if err != nil {
325+
t.Fatal(err)
326+
}
327+
if err := os.Chdir(tmpDir); err != nil {
328+
t.Fatal(err)
329+
}
330+
defer func() {
331+
if err := os.Chdir(oldWd); err != nil {
332+
t.Logf("Failed to restore original directory: %v", err)
333+
}
334+
}()
335+
336+
app := &cli.App{
337+
Name: "test",
338+
Commands: []*cli.Command{testutils.WithTestConfigAndNoopLogger(BuildCommand)},
339+
}
340+
341+
err = app.Run([]string{"app", "build", "--context", devnet.DEVNET_CONTEXT, "--target", "userland/build"})
342+
if err == nil {
343+
t.Fatal("Expected error due to missing custom target, got nil")
344+
}
345+
if !strings.Contains(err.Error(), "custom build target not found") {
346+
t.Fatalf("Expected missing target error, got: %v", err)
347+
}
348+
}

0 commit comments

Comments
 (0)