From f441d3200c0c3bb89334903d3c2735a225d278cc Mon Sep 17 00:00:00 2001 From: supernova Date: Fri, 30 May 2025 21:21:40 +0530 Subject: [PATCH 1/3] command guardwails initial --- cmd/devkit/main.go | 1 + pkg/common/config.go | 1 + pkg/hooks/command_guardrails.go | 280 +++++++++++++++++++++++++++ pkg/hooks/command_guardrails_test.go | 226 +++++++++++++++++++++ 4 files changed, 508 insertions(+) create mode 100644 pkg/hooks/command_guardrails.go create mode 100644 pkg/hooks/command_guardrails_test.go diff --git a/cmd/devkit/main.go b/cmd/devkit/main.go index cb16d1e6..1449fd59 100644 --- a/cmd/devkit/main.go +++ b/cmd/devkit/main.go @@ -46,6 +46,7 @@ func main() { } actionChain := hooks.NewActionChain() + actionChain.Use(hooks.WithCommandDependencyCheck) actionChain.Use(hooks.WithMetricEmission) hooks.ApplyMiddleware(app.Commands, actionChain) diff --git a/pkg/common/config.go b/pkg/common/config.go index d1f1066e..a003ab42 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -95,6 +95,7 @@ type OperatorRegistration struct { type ChainContextConfig struct { Name string `json:"name" yaml:"name"` + Stage string `json:"stage,omitempty" yaml:"stage,omitempty"` Chains map[string]ChainConfig `json:"chains" yaml:"chains"` DeployerPrivateKey string `json:"deployer_private_key" yaml:"deployer_private_key"` AppDeployerPrivateKey string `json:"app_private_key" yaml:"app_private_key"` diff --git a/pkg/hooks/command_guardrails.go b/pkg/hooks/command_guardrails.go new file mode 100644 index 00000000..7fe4dd6e --- /dev/null +++ b/pkg/hooks/command_guardrails.go @@ -0,0 +1,280 @@ +package hooks + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Layr-Labs/devkit-cli/pkg/common" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v3" +) + +// ProjectStage represents the current stage of project development +type ProjectStage string + +const ( + StageUninitialized ProjectStage = "uninitialized" // No project created yet + StageCreated ProjectStage = "created" // Project scaffolded but not built + StageBuilt ProjectStage = "built" // Contracts compiled + StageDevnetReady ProjectStage = "devnet_ready" // Devnet started and contracts deployed + StageRunning ProjectStage = "running" // AVS components running +) + +// CommandDependency defines a command dependency relationship +type CommandDependency struct { + Command string // Command that has dependencies + RequiredStage ProjectStage // Minimum stage required to run this command + ErrorMessage string // Helpful message when dependency isn't met + PromotesToStage ProjectStage // Stage this command promotes to on success (optional) + ConditionalPromotion func(*cli.Context) ProjectStage // Function to determine stage promotion based on context +} + +// CommandFlowDependencies defines the command flow dependencies +var CommandFlowDependencies = []CommandDependency{ + { + Command: "create", + RequiredStage: StageUninitialized, + PromotesToStage: StageCreated, + ErrorMessage: "Project already exists. Run commands from within the project directory.", + }, + { + Command: "build", + RequiredStage: StageCreated, + PromotesToStage: StageBuilt, + ErrorMessage: "The 'build' command requires a project to be created first. Please run 'devkit avs create ' first.", + }, + { + Command: "start", // This is the devnet start subcommand + RequiredStage: StageCreated, + PromotesToStage: StageDevnetReady, // Default promotion + ConditionalPromotion: func(cCtx *cli.Context) ProjectStage { + // If devnet start is called without --skip-deploy-contracts and --skip-avs-run, + // it automatically runs AVS components, so promote to StageRunning + skipDeployContracts := cCtx.Bool("skip-deploy-contracts") + skipAvsRun := cCtx.Bool("skip-avs-run") + if !skipDeployContracts && !skipAvsRun { + return StageRunning + } + return StageDevnetReady + }, + ErrorMessage: "The 'start' command requires a project to be created first. Please run 'devkit avs create ' first.", + }, + { + Command: "deploy-contracts", // This is the devnet deploy-contracts subcommand + RequiredStage: StageCreated, + PromotesToStage: StageDevnetReady, + ErrorMessage: "The 'deploy-contracts' command requires a project to be created first. Please run 'devkit avs create ' first.", + }, + { + Command: "call", + RequiredStage: StageRunning, + ErrorMessage: "The 'call' command requires AVS components to be running. Please run 'devkit avs run' or 'devkit avs devnet start' first to start the offchain components.", + }, + { + Command: "run", + RequiredStage: StageDevnetReady, + PromotesToStage: StageRunning, + ErrorMessage: "The 'run' command requires contracts to be deployed. Please run 'devkit avs devnet start' or 'devkit avs devnet deploy-contracts' first.", + }, +} + +// WithCommandDependencyCheck creates middleware that enforces command dependencies +func WithCommandDependencyCheck(action cli.ActionFunc) cli.ActionFunc { + return func(cCtx *cli.Context) error { + cmdName := cCtx.Command.Name + + // Get current project stage + currentStage, err := getCurrentProjectStage() + if err != nil { + // If we can't determine stage, only allow create command + if cmdName != "create" { + return fmt.Errorf("unable to determine project stage. Are you in a devkit project directory? Try running 'devkit avs create ' first") + } + } + + // Check dependencies for this command + dep := findCommandDependency(cmdName) + if dep != nil { + if !isStageAllowed(currentStage, dep.RequiredStage) { + return fmt.Errorf("%s\n\nCurrent stage: %s, Required stage: %s", + dep.ErrorMessage, currentStage, dep.RequiredStage) + } + } + + // Execute the command + result := action(cCtx) + + // If command succeeded and promotes to a new stage, update the stage + if result == nil && dep != nil { + var newStage ProjectStage + // Use conditional promotion if available, otherwise use default PromotesToStage + if dep.ConditionalPromotion != nil { + newStage = dep.ConditionalPromotion(cCtx) + } else if dep.PromotesToStage != "" { + newStage = dep.PromotesToStage + } + + if newStage != "" { + if err := updateProjectStage(newStage); err != nil { + // Log but don't fail the command if we can't update stage + logger := common.LoggerFromContext(cCtx.Context) + logger.Warn("Failed to update project stage: %v", err) + } + } + } + + return result + } +} + +// findCommandDependency finds the dependency rule for a command +func findCommandDependency(cmdName string) *CommandDependency { + for _, dep := range CommandFlowDependencies { + if dep.Command == cmdName { + return &dep + } + } + return nil +} + +// isStageAllowed checks if the current stage meets the required stage +func isStageAllowed(current, required ProjectStage) bool { + stageOrder := map[ProjectStage]int{ + StageUninitialized: 0, + StageCreated: 1, + StageBuilt: 2, + StageDevnetReady: 3, + StageRunning: 4, + } + + currentLevel, currentOk := stageOrder[current] + requiredLevel, requiredOk := stageOrder[required] + + if !currentOk || !requiredOk { + return false + } + + return currentLevel >= requiredLevel +} + +// getCurrentProjectStage determines the current project stage +func getCurrentProjectStage() (ProjectStage, error) { + // Check if we're in a project directory by looking for config/config.yaml + configPath := filepath.Join("config", "config.yaml") + if _, err := os.Stat(configPath); os.IsNotExist(err) { + return StageUninitialized, nil + } + + // Load the base config to get the current context + cfg, err := common.LoadBaseConfigYaml() + if err != nil { + return StageUninitialized, fmt.Errorf("failed to load project config: %w", err) + } + + // Load the context config to check for stage + contextPath := filepath.Join("config", "contexts", cfg.Config.Project.Context+".yaml") + if _, err := os.Stat(contextPath); os.IsNotExist(err) { + return StageCreated, nil // Project created but context not fully configured + } + + // Read the context file and check for stage + data, err := os.ReadFile(contextPath) + if err != nil { + return StageCreated, nil + } + + var contextWrapper struct { + Context struct { + Stage ProjectStage `yaml:"stage,omitempty"` + DeployedContracts []struct { + Name string `yaml:"name"` + Address string `yaml:"address"` + } `yaml:"deployed_contracts,omitempty"` + } `yaml:"context"` + } + + if err := yaml.Unmarshal(data, &contextWrapper); err != nil { + return StageCreated, nil + } + + // If stage is explicitly set, use it + if contextWrapper.Context.Stage != "" { + return contextWrapper.Context.Stage, nil + } + + // Otherwise, infer stage from project state + return inferStageFromProjectState(&contextWrapper) +} + +// inferStageFromProjectState infers the stage based on project artifacts +func inferStageFromProjectState(contextWrapper *struct { + Context struct { + Stage ProjectStage `yaml:"stage,omitempty"` + DeployedContracts []struct { + Name string `yaml:"name"` + Address string `yaml:"address"` + } `yaml:"deployed_contracts,omitempty"` + } `yaml:"context"` +}) (ProjectStage, error) { + // Check if contracts are deployed + if len(contextWrapper.Context.DeployedContracts) > 0 { + // Check if any contract has a valid address + for _, contract := range contextWrapper.Context.DeployedContracts { + if contract.Address != "" && strings.HasPrefix(contract.Address, "0x") { + return StageDevnetReady, nil + } + } + } + + // Check if build artifacts exist + if _, err := os.Stat("contracts/out"); err == nil { + return StageBuilt, nil + } + + // Default to created stage + return StageCreated, nil +} + +// updateProjectStage updates the project stage in the context file +func updateProjectStage(newStage ProjectStage) error { + // Load the base config to get the current context + cfg, err := common.LoadBaseConfigYaml() + if err != nil { + return fmt.Errorf("failed to load project config: %w", err) + } + + contextPath := filepath.Join("config", "contexts", cfg.Config.Project.Context+".yaml") + + // Load the existing context as YAML nodes to preserve formatting + rootNode, err := common.LoadYAML(contextPath) + if err != nil { + return fmt.Errorf("failed to load context YAML: %w", err) + } + + if len(rootNode.Content) == 0 { + return fmt.Errorf("empty YAML root node") + } + + // Navigate to the context node + contextNode := common.GetChildByKey(rootNode.Content[0], "context") + if contextNode == nil { + return fmt.Errorf("missing 'context' key in context file") + } + + // Update or add the stage field + stageNode := common.GetChildByKey(contextNode, "stage") + if stageNode != nil { + // Update existing stage + stageNode.Value = string(newStage) + } else { + // Add new stage field + stageKey := &yaml.Node{Kind: yaml.ScalarNode, Value: "stage"} + stageValue := &yaml.Node{Kind: yaml.ScalarNode, Value: string(newStage)} + contextNode.Content = append(contextNode.Content, stageKey, stageValue) + } + + // Write the updated YAML back + return common.WriteYAML(contextPath, rootNode) +} diff --git a/pkg/hooks/command_guardrails_test.go b/pkg/hooks/command_guardrails_test.go new file mode 100644 index 00000000..0ee68967 --- /dev/null +++ b/pkg/hooks/command_guardrails_test.go @@ -0,0 +1,226 @@ +package hooks + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProjectStageOrdering(t *testing.T) { + tests := []struct { + current ProjectStage + required ProjectStage + allowed bool + }{ + {StageUninitialized, StageUninitialized, true}, + {StageCreated, StageUninitialized, true}, + {StageCreated, StageCreated, true}, + {StageBuilt, StageCreated, true}, + {StageDevnetReady, StageBuilt, true}, + {StageRunning, StageDevnetReady, true}, + {StageCreated, StageBuilt, false}, + {StageUninitialized, StageCreated, false}, + {StageBuilt, StageDevnetReady, false}, + } + + for _, tt := range tests { + t.Run(string(tt.current)+"_to_"+string(tt.required), func(t *testing.T) { + result := isStageAllowed(tt.current, tt.required) + assert.Equal(t, tt.allowed, result) + }) + } +} + +func TestFindCommandDependency(t *testing.T) { + tests := []struct { + command string + shouldFind bool + requiredStage ProjectStage + }{ + {"create", true, StageUninitialized}, + {"build", true, StageCreated}, + {"start", true, StageCreated}, + {"deploy-contracts", true, StageCreated}, + {"call", true, StageRunning}, + {"run", true, StageDevnetReady}, + {"nonexistent", false, ""}, + } + + for _, tt := range tests { + t.Run(tt.command, func(t *testing.T) { + dep := findCommandDependency(tt.command) + if tt.shouldFind { + require.NotNil(t, dep) + assert.Equal(t, tt.requiredStage, dep.RequiredStage) + } else { + assert.Nil(t, dep) + } + }) + } +} + +func TestGetCurrentProjectStage(t *testing.T) { + // Test uninitialized state + t.Run("uninitialized", func(t *testing.T) { + tmpDir := t.TempDir() + origDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(origDir) + + require.NoError(t, os.Chdir(tmpDir)) + + stage, err := getCurrentProjectStage() + require.NoError(t, err) + assert.Equal(t, StageUninitialized, stage) + }) + + // Test created state (config exists but no context) + t.Run("created", func(t *testing.T) { + tmpDir := t.TempDir() + origDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(origDir) + + // Create basic project structure + configDir := filepath.Join(tmpDir, "config") + require.NoError(t, os.MkdirAll(configDir, 0755)) + + configContent := `version: "0.1.0" +config: + project: + name: "test-project" + version: "0.1.0" + context: "devnet" +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configContent), 0644)) + + require.NoError(t, os.Chdir(tmpDir)) + + stage, err := getCurrentProjectStage() + require.NoError(t, err) + assert.Equal(t, StageCreated, stage) + }) + + // Test built state (build artifacts exist) + t.Run("built", func(t *testing.T) { + tmpDir := t.TempDir() + origDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(origDir) + + // Create project structure with build artifacts + configDir := filepath.Join(tmpDir, "config") + contextsDir := filepath.Join(configDir, "contexts") + contractsDir := filepath.Join(tmpDir, "contracts", "out") + require.NoError(t, os.MkdirAll(contextsDir, 0755)) + require.NoError(t, os.MkdirAll(contractsDir, 0755)) + + configContent := `version: "0.1.0" +config: + project: + name: "test-project" + version: "0.1.0" + context: "devnet" +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configContent), 0644)) + + contextContent := `version: "0.1.0" +context: + name: "devnet" + chains: + l1: + chain_id: 1 + rpc_url: "http://localhost:8545" +` + require.NoError(t, os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contextContent), 0644)) + + require.NoError(t, os.Chdir(tmpDir)) + + stage, err := getCurrentProjectStage() + require.NoError(t, err) + assert.Equal(t, StageBuilt, stage) + }) + + // Test devnet ready state (deployed contracts exist) + t.Run("devnet_ready", func(t *testing.T) { + tmpDir := t.TempDir() + origDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(origDir) + + // Create project structure with deployed contracts + configDir := filepath.Join(tmpDir, "config") + contextsDir := filepath.Join(configDir, "contexts") + require.NoError(t, os.MkdirAll(contextsDir, 0755)) + + configContent := `version: "0.1.0" +config: + project: + name: "test-project" + version: "0.1.0" + context: "devnet" +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configContent), 0644)) + + contextContent := `version: "0.1.0" +context: + name: "devnet" + chains: + l1: + chain_id: 1 + rpc_url: "http://localhost:8545" + deployed_contracts: + - name: "TestContract" + address: "0x1234567890123456789012345678901234567890" +` + require.NoError(t, os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contextContent), 0644)) + + require.NoError(t, os.Chdir(tmpDir)) + + stage, err := getCurrentProjectStage() + require.NoError(t, err) + assert.Equal(t, StageDevnetReady, stage) + }) + + // Test explicit stage setting + t.Run("explicit_stage", func(t *testing.T) { + tmpDir := t.TempDir() + origDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(origDir) + + // Create project structure with explicit stage + configDir := filepath.Join(tmpDir, "config") + contextsDir := filepath.Join(configDir, "contexts") + require.NoError(t, os.MkdirAll(contextsDir, 0755)) + + configContent := `version: "0.1.0" +config: + project: + name: "test-project" + version: "0.1.0" + context: "devnet" +` + require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.yaml"), []byte(configContent), 0644)) + + contextContent := `version: "0.1.0" +context: + name: "devnet" + stage: "running" + chains: + l1: + chain_id: 1 + rpc_url: "http://localhost:8545" +` + require.NoError(t, os.WriteFile(filepath.Join(contextsDir, "devnet.yaml"), []byte(contextContent), 0644)) + + require.NoError(t, os.Chdir(tmpDir)) + + stage, err := getCurrentProjectStage() + require.NoError(t, err) + assert.Equal(t, StageRunning, stage) + }) +} From ac2ece13221551d582a74f6b853c05bd0c881d25 Mon Sep 17 00:00:00 2001 From: supernova Date: Mon, 2 Jun 2025 13:51:50 +0530 Subject: [PATCH 2/3] fix edge case for create command, and add devnet stop stage --- pkg/commands/devnet_actions.go | 7 ++ pkg/hooks/command_guardrails.go | 122 +++++++++++++++++++++++++++----- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/pkg/commands/devnet_actions.go b/pkg/commands/devnet_actions.go index 746ab478..61895766 100644 --- a/pkg/commands/devnet_actions.go +++ b/pkg/commands/devnet_actions.go @@ -27,6 +27,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/urfave/cli/v2" + "github.com/Layr-Labs/devkit-cli/pkg/hooks" allocationmanager "github.com/Layr-Labs/eigenlayer-contracts/pkg/bindings/AllocationManager" ) @@ -276,6 +277,12 @@ func StartDevnetAction(cCtx *cli.Context) error { // Start offchain AVS components after starting devnet and deploying contracts unless skipped if !skipDeployContracts && !skipAvsRun { + // Update project stage to running before starting AVS components (since AVSRun is long-running) + logger.Info("Updating project stage to running...") + if err := hooks.UpdateProjectStage(hooks.StageRunning, logger); err != nil { + logger.Warn("Failed to update project stage: %v", err) + } + if err := AVSRun(cCtx); err != nil && !errors.Is(err, context.Canceled) { return fmt.Errorf("avs run failed: %w", err) } diff --git a/pkg/hooks/command_guardrails.go b/pkg/hooks/command_guardrails.go index 7fe4dd6e..faafd7f8 100644 --- a/pkg/hooks/command_guardrails.go +++ b/pkg/hooks/command_guardrails.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/Layr-Labs/devkit-cli/pkg/common" + "github.com/Layr-Labs/devkit-cli/pkg/common/iface" "github.com/urfave/cli/v2" "gopkg.in/yaml.v3" ) @@ -48,18 +49,8 @@ var CommandFlowDependencies = []CommandDependency{ { Command: "start", // This is the devnet start subcommand RequiredStage: StageCreated, - PromotesToStage: StageDevnetReady, // Default promotion - ConditionalPromotion: func(cCtx *cli.Context) ProjectStage { - // If devnet start is called without --skip-deploy-contracts and --skip-avs-run, - // it automatically runs AVS components, so promote to StageRunning - skipDeployContracts := cCtx.Bool("skip-deploy-contracts") - skipAvsRun := cCtx.Bool("skip-avs-run") - if !skipDeployContracts && !skipAvsRun { - return StageRunning - } - return StageDevnetReady - }, - ErrorMessage: "The 'start' command requires a project to be created first. Please run 'devkit avs create ' first.", + PromotesToStage: StageDevnetReady, // Default promotion (will be overridden manually if AVS runs) + ErrorMessage: "The 'start' command requires a project to be created first. Please run 'devkit avs create ' first.", }, { Command: "deploy-contracts", // This is the devnet deploy-contracts subcommand @@ -67,6 +58,12 @@ var CommandFlowDependencies = []CommandDependency{ PromotesToStage: StageDevnetReady, ErrorMessage: "The 'deploy-contracts' command requires a project to be created first. Please run 'devkit avs create ' first.", }, + { + Command: "stop", // This is the devnet stop subcommand + RequiredStage: StageCreated, + PromotesToStage: StageCreated, // Reset back to created when devnet is stopped + ErrorMessage: "The 'stop' command requires a project to be created first.", + }, { Command: "call", RequiredStage: StageRunning, @@ -117,10 +114,18 @@ func WithCommandDependencyCheck(action cli.ActionFunc) cli.ActionFunc { } if newStage != "" { - if err := updateProjectStage(newStage); err != nil { - // Log but don't fail the command if we can't update stage - logger := common.LoggerFromContext(cCtx.Context) - logger.Warn("Failed to update project stage: %v", err) + logger := common.LoggerFromContext(cCtx.Context) + + if cmdName == "create" { + // For create command, update stage in the newly created project directory + if err := updateProjectStageForCreate(cCtx, newStage, logger); err != nil { + logger.Warn("Failed to update project stage for new project: %v", err) + } + } else { + // For other commands, update stage in current directory + if err := updateProjectStage(newStage, logger); err != nil { + logger.Warn("Failed to update project stage: %v", err) + } } } } @@ -238,7 +243,7 @@ func inferStageFromProjectState(contextWrapper *struct { } // updateProjectStage updates the project stage in the context file -func updateProjectStage(newStage ProjectStage) error { +func updateProjectStage(newStage ProjectStage, logger iface.Logger) error { // Load the base config to get the current context cfg, err := common.LoadBaseConfigYaml() if err != nil { @@ -278,3 +283,86 @@ func updateProjectStage(newStage ProjectStage) error { // Write the updated YAML back return common.WriteYAML(contextPath, rootNode) } + +// updateProjectStageForCreate updates the project stage in the context file for a newly created project +func updateProjectStageForCreate(cCtx *cli.Context, newStage ProjectStage, logger iface.Logger) error { + // Get the project name and target directory from create command arguments + if cCtx.NArg() == 0 { + return fmt.Errorf("project name is required for create command") + } + + projectName := cCtx.Args().First() + dest := cCtx.Args().Get(1) + + // Use dest from dir flag or positional + var targetDir string + if dest != "" { + targetDir = dest + } else { + targetDir = cCtx.String("dir") + } + + // Ensure provided dir is absolute + targetDir, err := filepath.Abs(filepath.Join(targetDir, projectName)) + if err != nil { + return fmt.Errorf("failed to resolve absolute path for target directory: %w", err) + } + + // Load the base config from the target project directory + configPath := filepath.Join(targetDir, "config", "config.yaml") + data, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read project config: %w", err) + } + + var cfg struct { + Config struct { + Project struct { + Context string `yaml:"context"` + } `yaml:"project"` + } `yaml:"config"` + } + + if err := yaml.Unmarshal(data, &cfg); err != nil { + return fmt.Errorf("failed to parse project config: %w", err) + } + + // Build context path in the target directory + contextPath := filepath.Join(targetDir, "config", "contexts", cfg.Config.Project.Context+".yaml") + + // Load the existing context as YAML nodes to preserve formatting + rootNode, err := common.LoadYAML(contextPath) + if err != nil { + return fmt.Errorf("failed to load context YAML: %w", err) + } + + if len(rootNode.Content) == 0 { + return fmt.Errorf("empty YAML root node") + } + + // Navigate to the context node + contextNode := common.GetChildByKey(rootNode.Content[0], "context") + if contextNode == nil { + return fmt.Errorf("missing 'context' key in context file") + } + + // Update or add the stage field + stageNode := common.GetChildByKey(contextNode, "stage") + if stageNode != nil { + // Update existing stage + stageNode.Value = string(newStage) + } else { + // Add new stage field + stageKey := &yaml.Node{Kind: yaml.ScalarNode, Value: "stage"} + stageValue := &yaml.Node{Kind: yaml.ScalarNode, Value: string(newStage)} + contextNode.Content = append(contextNode.Content, stageKey, stageValue) + } + + // Write the updated YAML back + return common.WriteYAML(contextPath, rootNode) +} + +// UpdateProjectStage manually updates the project stage - can be called from command implementations +func UpdateProjectStage(newStage ProjectStage, logger iface.Logger) error { + return updateProjectStage(newStage, logger) +} From cb8938a1ac821b3436476beed86905f862e2065d Mon Sep 17 00:00:00 2001 From: supernova Date: Mon, 2 Jun 2025 14:58:33 +0530 Subject: [PATCH 3/3] lint --- pkg/hooks/command_guardrails_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/hooks/command_guardrails_test.go b/pkg/hooks/command_guardrails_test.go index 0ee68967..ae5a78aa 100644 --- a/pkg/hooks/command_guardrails_test.go +++ b/pkg/hooks/command_guardrails_test.go @@ -44,6 +44,7 @@ func TestFindCommandDependency(t *testing.T) { {"build", true, StageCreated}, {"start", true, StageCreated}, {"deploy-contracts", true, StageCreated}, + {"stop", true, StageCreated}, {"call", true, StageRunning}, {"run", true, StageDevnetReady}, {"nonexistent", false, ""}, @@ -68,7 +69,7 @@ func TestGetCurrentProjectStage(t *testing.T) { tmpDir := t.TempDir() origDir, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(origDir) + defer func() { _ = os.Chdir(origDir) }() require.NoError(t, os.Chdir(tmpDir)) @@ -82,7 +83,7 @@ func TestGetCurrentProjectStage(t *testing.T) { tmpDir := t.TempDir() origDir, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(origDir) + defer func() { _ = os.Chdir(origDir) }() // Create basic project structure configDir := filepath.Join(tmpDir, "config") @@ -109,7 +110,7 @@ config: tmpDir := t.TempDir() origDir, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(origDir) + defer func() { _ = os.Chdir(origDir) }() // Create project structure with build artifacts configDir := filepath.Join(tmpDir, "config") @@ -149,7 +150,7 @@ context: tmpDir := t.TempDir() origDir, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(origDir) + defer func() { _ = os.Chdir(origDir) }() // Create project structure with deployed contracts configDir := filepath.Join(tmpDir, "config") @@ -190,7 +191,7 @@ context: tmpDir := t.TempDir() origDir, err := os.Getwd() require.NoError(t, err) - defer os.Chdir(origDir) + defer func() { _ = os.Chdir(origDir) }() // Create project structure with explicit stage configDir := filepath.Join(tmpDir, "config")