Created
July 12, 2025 03:41
-
-
Save cds-amal/13fd5ff8cc3a1e67a231b6e58807a5e1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package cmd | |
import ( | |
"bufio" | |
"context" | |
"encoding/json" | |
"fmt" | |
"io" | |
"os" | |
"os/exec" | |
"path/filepath" | |
"strings" | |
"time" | |
"github.com/briandowns/spinner" | |
"github.com/fatih/color" | |
"github.com/spf13/cobra" | |
) | |
var deployCmd = &cobra.Command{ | |
Use: "deploy [profile]", | |
Short: "Deploy DIN-AVS with a single command", | |
Long: `Deploy DIN-AVS infrastructure with intelligent defaults and automatic setup. | |
Examples: | |
dorch deploy # Auto-detect profile and deploy | |
dorch deploy devnet # Deploy devnet profile | |
dorch deploy testnet02 # Deploy testnet02 profile`, | |
Args: cobra.MaximumNArgs(1), | |
RunE: runDeploy, | |
} | |
var ( | |
deployTarget string | |
skipChecks bool | |
skipValidation bool | |
forceRebuild bool | |
noBrowser bool | |
deployVerbose bool | |
deployDryRun bool | |
) | |
func init() { | |
deployCmd.Flags().StringVar(&deployTarget, "target", "", "Deployment target (default: profile default)") | |
deployCmd.Flags().BoolVar(&skipChecks, "skip-checks", false, "Skip prerequisite checks") | |
deployCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "Skip profile validation") | |
deployCmd.Flags().BoolVar(&forceRebuild, "force-rebuild", false, "Force container rebuild") | |
deployCmd.Flags().BoolVar(&noBrowser, "no-browser", false, "Don't open browser after deployment") | |
deployCmd.Flags().BoolVarP(&deployVerbose, "verbose", "v", false, "Enable verbose output") | |
deployCmd.Flags().BoolVar(&deployDryRun, "dry-run", false, "Show what would be deployed without executing") | |
rootCmd.AddCommand(deployCmd) | |
} | |
type DeploymentStep struct { | |
Name string | |
Description string | |
Execute func(ctx *DeploymentContext) error | |
CanFail bool | |
} | |
type DeploymentContext struct { | |
Profile string | |
Target string | |
DinDockerDir string | |
DinAvsDir string | |
ChainRegistryCmd string | |
StartTime time.Time | |
Spinner *spinner.Spinner | |
Verbose bool | |
} | |
func runDeploy(cmd *cobra.Command, args []string) error { | |
// Determine profile | |
var profile string | |
if len(args) > 0 { | |
profile = args[0] | |
} else { | |
// Auto-detect profile | |
profile = detectDeploymentProfile() | |
color.Cyan("Auto-detected profile: %s\n", profile) | |
} | |
// Initialize deployment context | |
ctx := &DeploymentContext{ | |
Profile: profile, | |
Target: deployTarget, | |
DinDockerDir: getDinDockerDir(), | |
StartTime: time.Now(), | |
Verbose: deployVerbose, | |
} | |
// Set up paths | |
ctx.DinAvsDir = getEnvOrDefault("DIN_AVS_DIR", filepath.Join(os.Getenv("HOME"), "work/din-workspace/din-avs")) | |
ctx.ChainRegistryCmd = filepath.Join(ctx.DinDockerDir, "chain-registry/chain-registry") | |
// Dry run mode | |
if deployDryRun { | |
return showDryRun(ctx) | |
} | |
// Display deployment header | |
fmt.Println() | |
color.Green("🚀 DIN-AVS Deployment Orchestrator") | |
fmt.Println(strings.Repeat("=", 40)) | |
fmt.Printf("Profile: %s\n", color.YellowString(ctx.Profile)) | |
if ctx.Target != "" { | |
fmt.Printf("Target: %s\n", color.YellowString(ctx.Target)) | |
} | |
fmt.Printf("Time: %s\n", ctx.StartTime.Format("2006-01-02 15:04:05")) | |
fmt.Println(strings.Repeat("=", 40)) | |
fmt.Println() | |
// Define deployment steps | |
steps := []DeploymentStep{ | |
{ | |
Name: "Prerequisites", | |
Description: "Checking system requirements", | |
Execute: checkPrerequisites, | |
CanFail: false, | |
}, | |
{ | |
Name: "Profile Validation", | |
Description: "Validating deployment profile", | |
Execute: validateProfile, | |
CanFail: skipValidation, | |
}, | |
{ | |
Name: "Anvil Setup", | |
Description: "Starting local Ethereum node", | |
Execute: setupAnvil, | |
CanFail: false, | |
}, | |
{ | |
Name: "Container Check", | |
Description: "Checking container rebuild requirements", | |
Execute: checkContainerRebuild, | |
CanFail: false, | |
}, | |
{ | |
Name: "Contract Deployment", | |
Description: "Deploying smart contracts", | |
Execute: deployContracts, | |
CanFail: false, | |
}, | |
{ | |
Name: "Environment Setup", | |
Description: "Loading contract addresses", | |
Execute: loadContractAddresses, | |
CanFail: false, | |
}, | |
{ | |
Name: "Docker Configuration", | |
Description: "Generating Docker configuration", | |
Execute: generateDockerConfig, | |
CanFail: false, | |
}, | |
{ | |
Name: "Service Startup", | |
Description: "Building and starting services", | |
Execute: startServices, | |
CanFail: false, | |
}, | |
{ | |
Name: "Health Checks", | |
Description: "Verifying service health", | |
Execute: performHealthChecks, | |
CanFail: false, | |
}, | |
{ | |
Name: "Deployment Verification", | |
Description: "Finalizing deployment", | |
Execute: verifyDeployment, | |
CanFail: false, | |
}, | |
} | |
// Execute deployment steps | |
for i, step := range steps { | |
if skipChecks && step.Name == "Prerequisites" { | |
continue | |
} | |
if skipValidation && step.Name == "Profile Validation" { | |
continue | |
} | |
if err := executeStep(ctx, step, i+1, len(steps)); err != nil { | |
if !step.CanFail { | |
color.Red("\n❌ Deployment failed at step: %s", step.Name) | |
return err | |
} | |
color.Yellow("⚠️ Warning: %s (continuing)", err.Error()) | |
} | |
} | |
// Deployment complete | |
duration := time.Since(ctx.StartTime) | |
fmt.Println() | |
color.Green("✅ Deployment completed successfully!") | |
fmt.Printf("Total time: %s\n", duration.Round(time.Second)) | |
// Open browser if requested | |
if !noBrowser { | |
openBrowser("http://localhost:8080") | |
} | |
// Show helpful commands | |
showPostDeploymentInfo(ctx) | |
return nil | |
} | |
func executeStep(ctx *DeploymentContext, step DeploymentStep, current, total int) error { | |
// Progress indicator | |
progress := fmt.Sprintf("[%d/%d]", current, total) | |
color.Blue("%s %s", progress, step.Description) | |
// Create spinner for visual feedback | |
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) | |
s.Suffix = " " | |
ctx.Spinner = s | |
if !ctx.Verbose { | |
s.Start() | |
} | |
// Execute step | |
err := step.Execute(ctx) | |
if !ctx.Verbose { | |
s.Stop() | |
} | |
if err != nil { | |
color.Red(" ❌") | |
return err | |
} | |
color.Green(" ✓") | |
return nil | |
} | |
func detectDeploymentProfile() string { | |
// Use chain-registry detect command | |
cmd := exec.Command("chain-registry", "current", "detect") | |
output, err := cmd.Output() | |
if err == nil { | |
profile := strings.TrimSpace(string(output)) | |
if profile != "" { | |
return profile | |
} | |
} | |
// Fallback to devnet | |
return "devnet" | |
} | |
func getDinDockerDir() string { | |
// Try to find din-docker directory | |
cwd, err := os.Getwd() | |
if err == nil { | |
// Check if we're already in din-docker | |
if strings.Contains(cwd, "din-docker") { | |
return cwd | |
} | |
// Check parent directories | |
dir := cwd | |
for i := 0; i < 5; i++ { | |
if filepath.Base(dir) == "din-docker" { | |
return dir | |
} | |
parent := filepath.Dir(dir) | |
if parent == dir { | |
break | |
} | |
dir = parent | |
} | |
} | |
// Default | |
return filepath.Join(os.Getenv("HOME"), "work/din-workspace/din-docker") | |
} | |
func getEnvOrDefault(key, defaultValue string) string { | |
if value := os.Getenv(key); value != "" { | |
return value | |
} | |
return defaultValue | |
} | |
func showDryRun(ctx *DeploymentContext) error { | |
fmt.Println("🔍 Dry Run Mode - Showing deployment plan") | |
fmt.Println() | |
fmt.Printf("Profile: %s\n", ctx.Profile) | |
fmt.Printf("Target: %s\n", ctx.Target) | |
fmt.Printf("DIN Docker Dir: %s\n", ctx.DinDockerDir) | |
fmt.Printf("DIN AVS Dir: %s\n", ctx.DinAvsDir) | |
fmt.Println() | |
fmt.Println("Steps that would be executed:") | |
fmt.Println("1. Check prerequisites (Docker, tools)") | |
fmt.Println("2. Validate profile configuration") | |
fmt.Println("3. Start Anvil if not running") | |
fmt.Println("4. Check and rebuild containers if needed") | |
fmt.Println("5. Deploy smart contracts") | |
fmt.Println("6. Load contract addresses") | |
fmt.Println("7. Generate Docker configuration") | |
fmt.Println("8. Build and start services") | |
fmt.Println("9. Perform health checks") | |
fmt.Println("10. Verify deployment") | |
return nil | |
} | |
func checkPrerequisites(ctx *DeploymentContext) error { | |
// Check required commands | |
requiredCmds := []string{"docker", "docker-compose", "yq", "jq", "txtx"} | |
for _, cmd := range requiredCmds { | |
if _, err := exec.LookPath(cmd); err != nil { | |
return fmt.Errorf("%s not found in PATH", cmd) | |
} | |
} | |
// Check Docker daemon | |
cmd := exec.Command("docker", "info") | |
if err := cmd.Run(); err != nil { | |
return fmt.Errorf("Docker daemon not running") | |
} | |
// Check chain-registry | |
if _, err := os.Stat(ctx.ChainRegistryCmd); err != nil { | |
// Build it | |
buildCmd := exec.Command("make", "build") | |
buildCmd.Dir = filepath.Dir(ctx.ChainRegistryCmd) | |
if err := buildCmd.Run(); err != nil { | |
return fmt.Errorf("failed to build chain-registry: %w", err) | |
} | |
} | |
// Check din-avs directory | |
if _, err := os.Stat(ctx.DinAvsDir); err != nil { | |
return fmt.Errorf("din-avs directory not found at %s", ctx.DinAvsDir) | |
} | |
return nil | |
} | |
func validateProfile(ctx *DeploymentContext) error { | |
validateScript := filepath.Join(ctx.DinDockerDir, "scripts/validate-profile.sh") | |
if _, err := os.Stat(validateScript); err != nil { | |
// Script doesn't exist, skip validation | |
return nil | |
} | |
cmd := exec.Command(validateScript, ctx.Profile) | |
cmd.Dir = ctx.DinDockerDir | |
if ctx.Verbose { | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
} | |
return cmd.Run() | |
} | |
func setupAnvil(ctx *DeploymentContext) error { | |
// Check if Anvil is already running | |
checkCmd := exec.Command("lsof", "-i", ":8545") | |
if err := checkCmd.Run(); err == nil { | |
// Anvil already running | |
return nil | |
} | |
// Start Anvil | |
envFile := filepath.Join(ctx.DinDockerDir, ".env.anvil") | |
mnemonic := "chalk floor identify cruise endless truck gauge swing noble slow swing stock" | |
forkURL := "https://sepolia.infura.io/v3/cc320ed2842746fdb056e717ad8fff7b" | |
// Load from env file if exists | |
if data, err := os.ReadFile(envFile); err == nil { | |
lines := strings.Split(string(data), "\n") | |
for _, line := range lines { | |
if strings.HasPrefix(line, "MNEMONIC=") { | |
mnemonic = strings.TrimPrefix(line, "MNEMONIC=") | |
} else if strings.HasPrefix(line, "FORK_URL=") { | |
forkURL = strings.TrimPrefix(line, "FORK_URL=") | |
} | |
} | |
} | |
// Start Anvil in background | |
anvilCmd := exec.Command("anvil", | |
"--host", "0.0.0.0", | |
"--port", "8545", | |
"-a", "26", | |
"--balance", "1000", | |
"--fork-url", forkURL, | |
"-m", mnemonic, | |
) | |
if err := anvilCmd.Start(); err != nil { | |
return fmt.Errorf("failed to start Anvil: %w", err) | |
} | |
// Save PID | |
pidFile := filepath.Join(ctx.DinDockerDir, ".anvil.pid") | |
os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", anvilCmd.Process.Pid)), 0644) | |
// Wait for Anvil to be ready | |
time.Sleep(5 * time.Second) | |
return nil | |
} | |
func checkContainerRebuild(ctx *DeploymentContext) error { | |
if forceRebuild { | |
return cleanupContainers(ctx) | |
} | |
// Check using chain-registry manifest | |
cmd := exec.Command(ctx.ChainRegistryCmd, "manifest", "check", | |
"--env", ctx.Profile, | |
"--profile", ctx.Profile, | |
"--target", ctx.Target, | |
"--profile-dir", filepath.Join(ctx.DinDockerDir, "op-config", ctx.Profile), | |
) | |
output, err := cmd.Output() | |
if err != nil || strings.TrimSpace(string(output)) == "REBUILD_REQUIRED" { | |
return cleanupContainers(ctx) | |
} | |
return nil | |
} | |
func cleanupContainers(ctx *DeploymentContext) error { | |
// Stop containers | |
stopCmd := exec.Command("docker", "compose", "down", "--remove-orphans") | |
stopCmd.Dir = ctx.DinDockerDir | |
stopCmd.Run() // Ignore errors | |
if forceRebuild { | |
// Remove images | |
removeCmd := exec.Command("docker", "compose", "down", "--rmi", "local") | |
removeCmd.Dir = ctx.DinDockerDir | |
removeCmd.Run() // Ignore errors | |
} | |
return nil | |
} | |
func deployContracts(ctx *DeploymentContext) error { | |
// Update state | |
updateDeploymentState(ctx, "contracts", "deploying") | |
// Determine target if not specified | |
if ctx.Target == "" { | |
ctx.Target = getDefaultTarget(ctx.Profile) | |
} | |
// Run deployment | |
deployScript := filepath.Join(ctx.DinDockerDir, "Justfile") | |
cmd := exec.Command("just", "deploy", ctx.Profile, ctx.Target) | |
cmd.Dir = ctx.DinDockerDir | |
if ctx.Verbose { | |
cmd.Stdout = os.Stdout | |
cmd.Stderr = os.Stderr | |
return cmd.Run() | |
} | |
// Capture output for error reporting | |
output, err := cmd.CombinedOutput() | |
if err != nil { | |
updateDeploymentState(ctx, "contracts", "failed") | |
// Show last 20 lines on error | |
lines := strings.Split(string(output), "\n") | |
start := len(lines) - 20 | |
if start < 0 { | |
start = 0 | |
} | |
for i := start; i < len(lines); i++ { | |
fmt.Fprintln(os.Stderr, lines[i]) | |
} | |
return fmt.Errorf("contract deployment failed") | |
} | |
updateDeploymentState(ctx, "contracts", "deployed") | |
return nil | |
} | |
func loadContractAddresses(ctx *DeploymentContext) error { | |
// Run load script | |
loadScript := filepath.Join(ctx.DinDockerDir, "scripts/load-contracts-env.sh") | |
cmd := exec.Command(loadScript) | |
cmd.Dir = ctx.DinDockerDir | |
cmd.Env = append(os.Environ(), fmt.Sprintf("REGISTRY_ENV=%s", ctx.Profile)) | |
if err := cmd.Run(); err != nil { | |
return fmt.Errorf("failed to load contract addresses: %w", err) | |
} | |
// Verify .env.contracts exists | |
envFile := filepath.Join(ctx.DinDockerDir, ".env.contracts") | |
if _, err := os.Stat(envFile); err != nil { | |
return fmt.Errorf(".env.contracts not generated") | |
} | |
return nil | |
} | |
func generateDockerConfig(ctx *DeploymentContext) error { | |
updateDeploymentState(ctx, "docker-config", "generating") | |
generateScript := filepath.Join(ctx.DinDockerDir, "scripts/generate-from-profile.sh") | |
cmd := exec.Command(generateScript, ctx.Profile) | |
cmd.Dir = ctx.DinDockerDir | |
if err := cmd.Run(); err != nil { | |
updateDeploymentState(ctx, "docker-config", "failed") | |
return fmt.Errorf("failed to generate Docker configuration") | |
} | |
// Verify docker-compose.yml exists | |
composeFile := filepath.Join(ctx.DinDockerDir, "docker-compose.yml") | |
if _, err := os.Stat(composeFile); err != nil { | |
return fmt.Errorf("docker-compose.yml not generated") | |
} | |
updateDeploymentState(ctx, "docker-config", "generated") | |
return nil | |
} | |
func startServices(ctx *DeploymentContext) error { | |
updateDeploymentState(ctx, "services", "building") | |
// Build bn254-rs image if needed | |
if err := buildBN254Image(ctx); err != nil { | |
return err | |
} | |
// Load contract addresses for build | |
envFile := filepath.Join(ctx.DinDockerDir, ".env.contracts") | |
if data, err := os.ReadFile(envFile); err == nil { | |
lines := strings.Split(string(data), "\n") | |
for _, line := range lines { | |
if parts := strings.SplitN(line, "=", 2); len(parts) == 2 { | |
os.Setenv(parts[0], parts[1]) | |
} | |
} | |
} | |
// Build services | |
buildCmd := exec.Command("docker", "compose", "--profile", "ui", "build") | |
buildCmd.Dir = ctx.DinDockerDir | |
if ctx.Verbose { | |
buildCmd.Stdout = os.Stdout | |
buildCmd.Stderr = os.Stderr | |
} | |
if err := buildCmd.Run(); err != nil { | |
updateDeploymentState(ctx, "services", "failed") | |
return fmt.Errorf("failed to build services") | |
} | |
// Start services | |
upCmd := exec.Command("docker", "compose", "--profile", "ui", "up", "-d") | |
upCmd.Dir = ctx.DinDockerDir | |
if err := upCmd.Run(); err != nil { | |
updateDeploymentState(ctx, "services", "failed") | |
return fmt.Errorf("failed to start services") | |
} | |
updateDeploymentState(ctx, "services", "running") | |
return nil | |
} | |
func buildBN254Image(ctx *DeploymentContext) error { | |
// Check if image exists | |
checkCmd := exec.Command("docker", "image", "inspect", "bn254-rs:latest") | |
if err := checkCmd.Run(); err == nil { | |
return nil // Image exists | |
} | |
// Build image | |
buildScript := filepath.Join(ctx.DinDockerDir, "scripts/build-bn254-image.sh") | |
if _, err := os.Stat(buildScript); err == nil { | |
cmd := exec.Command(buildScript) | |
cmd.Dir = ctx.DinDockerDir | |
return cmd.Run() | |
} | |
return fmt.Errorf("bn254-rs image not found and build script missing") | |
} | |
func performHealthChecks(ctx *DeploymentContext) error { | |
services := []struct { | |
Name string | |
URL string | |
}{ | |
{"gateway", "http://localhost:8080/health"}, | |
{"kms", "http://localhost:3000/health"}, | |
{"frontend", "http://localhost:8080"}, | |
} | |
// Wait a bit for services to start | |
time.Sleep(5 * time.Second) | |
for _, service := range services { | |
// Simple HTTP check | |
cmd := exec.Command("curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", service.URL) | |
output, err := cmd.Output() | |
if err != nil || string(output) != "200" { | |
// Try docker ps to see if container is running | |
psCmd := exec.Command("docker", "ps", "--filter", fmt.Sprintf("name=%s", service.Name), "--format", "{{.Names}}") | |
psOutput, _ := psCmd.Output() | |
if strings.TrimSpace(string(psOutput)) == "" { | |
return fmt.Errorf("service %s not running", service.Name) | |
} | |
} | |
// Update container status | |
updateContainerStatus(ctx, service.Name, true) | |
} | |
return nil | |
} | |
func verifyDeployment(ctx *DeploymentContext) error { | |
updateDeploymentState(ctx, "deployment", "completed") | |
// Save manifest | |
saveCmd := exec.Command(ctx.ChainRegistryCmd, "manifest", "save", | |
"--env", ctx.Profile, | |
"--profile", ctx.Profile, | |
"--target", ctx.Target, | |
"--profile-dir", filepath.Join(ctx.DinDockerDir, "op-config", ctx.Profile), | |
) | |
saveCmd.Run() // Ignore errors | |
// Update current deployment | |
setCurrentCmd := exec.Command(ctx.ChainRegistryCmd, "current", "set", | |
"--profile", ctx.Profile, | |
"--status", "deployed", | |
) | |
setCurrentCmd.Run() | |
return nil | |
} | |
func updateDeploymentState(ctx *DeploymentContext, phase, status string) { | |
cmd := exec.Command(ctx.ChainRegistryCmd, "state", "update", | |
"--env", ctx.Profile, | |
"--phase", phase, | |
"--status", status, | |
) | |
cmd.Run() // Ignore errors | |
} | |
func updateContainerStatus(ctx *DeploymentContext, container string, running bool) { | |
args := []string{"state", "update", "--env", ctx.Profile, "--container", container} | |
if running { | |
args = append(args, "--running") | |
} | |
cmd := exec.Command(ctx.ChainRegistryCmd, args...) | |
cmd.Run() // Ignore errors | |
} | |
func getDefaultTarget(profile string) string { | |
// Read deploy-targets.yaml | |
targetsFile := filepath.Join(getDinDockerDir(), "deploy-targets.yaml") | |
if _, err := os.Stat(targetsFile); err == nil { | |
cmd := exec.Command("yq", "-r", fmt.Sprintf(".profile_defaults.%s // \"local\"", profile), targetsFile) | |
if output, err := cmd.Output(); err == nil { | |
return strings.TrimSpace(string(output)) | |
} | |
} | |
return "local" | |
} | |
func openBrowser(url string) { | |
time.Sleep(2 * time.Second) // Give services time to fully start | |
var cmd *exec.Cmd | |
switch { | |
case isWSL(): | |
cmd = exec.Command("cmd.exe", "/c", "start", url) | |
case isMac(): | |
cmd = exec.Command("open", url) | |
default: | |
cmd = exec.Command("xdg-open", url) | |
} | |
cmd.Run() // Ignore errors | |
} | |
func isWSL() bool { | |
if data, err := os.ReadFile("/proc/version"); err == nil { | |
return strings.Contains(strings.ToLower(string(data)), "microsoft") | |
} | |
return false | |
} | |
func isMac() bool { | |
return exec.Command("uname", "-s").Output()[0] == 'D' // Darwin | |
} | |
func showPostDeploymentInfo(ctx *DeploymentContext) { | |
fmt.Println() | |
color.Cyan("📋 Next Steps:") | |
fmt.Println("1. Navigate to http://localhost:8080") | |
fmt.Println("2. Connect your wallet using 'Sign In'") | |
fmt.Println("3. Follow the operator onboarding flow") | |
fmt.Println() | |
color.Cyan("🛠️ Useful Commands:") | |
fmt.Printf(" %s - Check deployment status\n", color.YellowString("just s")) | |
fmt.Printf(" %s - View service logs\n", color.YellowString("just l")) | |
fmt.Printf(" %s - Monitor transactions\n", color.YellowString("just m")) | |
fmt.Printf(" %s - Stop all services\n", color.YellowString("just stop")) | |
fmt.Printf(" %s - View state\n", color.YellowString("chain-registry state get --env %s", ctx.Profile)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment