diff --git a/config/contexts/registry.go b/config/contexts/registry.go index b73db499..660212ae 100644 --- a/config/contexts/registry.go +++ b/config/contexts/registry.go @@ -4,12 +4,12 @@ import ( _ "embed" "github.com/Layr-Labs/devkit-cli/pkg/migration" - + "github.com/Layr-Labs/devkit-cli/pkg/common" contextMigrations "github.com/Layr-Labs/devkit-cli/config/contexts/migrations" ) // Set the latest version -const LatestVersion = "0.0.5" +const LatestVersion =common.DevkitLatestContextVersion // Array of default contexts to create in project var DefaultContexts = [...]string{ diff --git a/pkg/common/config.go b/pkg/common/config.go index d1f1066e..f242a7f2 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -6,8 +6,11 @@ import ( "os" "path/filepath" "reflect" + "strconv" "strings" + "github.com/Layr-Labs/devkit-cli/internal/version" + "github.com/Layr-Labs/devkit-cli/pkg/common/iface" "gopkg.in/yaml.v3" ) @@ -106,6 +109,138 @@ type ChainContextConfig struct { OperatorRegistrations []OperatorRegistration `json:"operator_registrations" yaml:"operator_registrations"` } +// VersionCompatibilityError represents a version mismatch error in migration +type VersionCompatibilityError struct { + ContextVersion string + CLIVersion string + LatestSupported string + ContextFile string +} + +func (e *VersionCompatibilityError) Error() string { + return fmt.Sprintf(` +⚠️ VERSION COMPATIBILITY WARNING ⚠️ + +Your context file version is newer than what this devkit +CLI version supports: + + Current Context file: %s + Current Context version: %s + Current CLI version: %s + Latest supported context version: %s + +This can cause context corruption if you proceed. Please update your devkit CLI first: + + # Update devkit CLI to latest version + +VERSION=%s +ARCH=$(uname -m | tr '[:upper:]' '[:lower:]') +DISTRO=$(uname -s | tr '[:upper:]' '[:lower:]') + +mkdir -p $HOME/bin +curl -sL "https://s3.amazonaws.com/eigenlayer-devkit-releases/${VERSION}/devkit-${DISTRO}-${ARCH}-${VERSION}.tar.gz" | tar xv -C "$HOME/bin" + + # Or build from source + git pull origin main && make install + +After updating, verify the CLI version supports your context: + devkit --version + +DO NOT edit the context file until you update the CLI version. +`, e.ContextFile, e.ContextVersion, e.CLIVersion, e.LatestSupported, embeddedDevkitReleaseVersion) +} + +// parseVersion converts version string like "0.0.5" to comparable integers +func parseVersion(v string) (major, minor, patch int, err error) { + parts := strings.Split(v, ".") + if len(parts) != 3 { + return 0, 0, 0, fmt.Errorf("invalid version format: %s", v) + } + + major, err = strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, 0, fmt.Errorf("invalid major version: %s", parts[0]) + } + + minor, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, 0, fmt.Errorf("invalid minor version: %s", parts[1]) + } + + patch, err = strconv.Atoi(parts[2]) + if err != nil { + return 0, 0, 0, fmt.Errorf("invalid patch version: %s", parts[2]) + } + + return major, minor, patch, nil +} + +// compareVersions returns true if v1 > v2 +func compareVersions(v1, v2 string) (bool, error) { + major1, minor1, patch1, err := parseVersion(v1) + if err != nil { + return false, fmt.Errorf("parse version %s: %w", v1, err) + } + + major2, minor2, patch2, err := parseVersion(v2) + if err != nil { + return false, fmt.Errorf("parse version %s: %w", v2, err) + } + + if major1 > major2 { + return true, nil + } + if major1 < major2 { + return false, nil + } + + if minor1 > minor2 { + return true, nil + } + if minor1 < minor2 { + return false, nil + } + + return patch1 > patch2, nil +} + +// checkVersionCompatibility validates that the context version is supported by the current CLI +// Logs a warning if there's a version mismatch, but allows execution to continue +func checkVersionCompatibility(contextVersion, contextFile string, logger iface.Logger) { + if contextVersion == "" { + // Missing version - could be very old context, warn but allow + if logger != nil { + logger.Info("⚠️ Context file %s is missing version field - this may be an old context that needs migration", contextFile) + } + return + } + + // Get the latest version supported by this CLI + latestSupported := DevkitLatestContextVersion // This should match contexts.LatestVersion , but cannot check due to import cycle error + + // Compare versions + isNewer, err := compareVersions(contextVersion, latestSupported) + if err != nil { + if logger != nil { + logger.Info("⚠️ Failed to compare versions: %v", err) + } + return + } + + // If context version is newer than what we support, log compatibility warning + if isNewer { + compatError := &VersionCompatibilityError{ + ContextVersion: contextVersion, + CLIVersion: version.GetVersion(), + LatestSupported: latestSupported, + ContextFile: contextFile, + } + if logger != nil { + logger.Info("%s", compatError.Error()) + } + } +} + func LoadBaseConfig() (map[string]interface{}, error) { path := filepath.Join(DefaultConfigWithContextConfigPath, "config.yaml") data, err := os.ReadFile(path) @@ -120,6 +255,10 @@ func LoadBaseConfig() (map[string]interface{}, error) { } func LoadContextConfig(ctxName string) (map[string]interface{}, error) { + return LoadContextConfigWithLogger(ctxName, nil) +} + +func LoadContextConfigWithLogger(ctxName string, logger iface.Logger) (map[string]interface{}, error) { // Default to devnet if ctxName == "" { ctxName = "devnet" @@ -133,6 +272,12 @@ func LoadContextConfig(ctxName string) (map[string]interface{}, error) { if err := yaml.Unmarshal(data, &ctx); err != nil { return nil, fmt.Errorf("parse context %q: %w", ctxName, err) } + + // Check version compatibility + if version, ok := ctx["version"].(string); ok { + checkVersionCompatibility(version, path, logger) + } + return ctx, nil } @@ -150,6 +295,10 @@ func LoadBaseConfigYaml() (*Config, error) { } func LoadConfigWithContextConfig(ctxName string) (*ConfigWithContextConfig, error) { + return LoadConfigWithContextConfigAndLogger(ctxName, nil) +} + +func LoadConfigWithContextConfigAndLogger(ctxName string, logger iface.Logger) (*ConfigWithContextConfig, error) { // Default to devnet if ctxName == "" { ctxName = "devnet" @@ -183,6 +332,9 @@ func LoadConfigWithContextConfig(ctxName string) (*ConfigWithContextConfig, erro return nil, fmt.Errorf("failed to parse context file %q: %w", contextFile, err) } + // Check version compatibility before proceeding + checkVersionCompatibility(wrapper.Version, contextFile, logger) + cfg.Context = map[string]ChainContextConfig{ ctxName: wrapper.Context, } diff --git a/pkg/common/constants.go b/pkg/common/constants.go index 0a5bd630..e6b8aba9 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -28,4 +28,7 @@ const ( // Default chainId for Anvil DefaultAnvilChainId = 31337 + + // DevkitLatestContextVersion is the latest version of the context file + DevkitLatestContextVersion = "0.0.5" ) diff --git a/pkg/migration/migrator.go b/pkg/migration/migrator.go index fb5193ec..2af899c9 100644 --- a/pkg/migration/migrator.go +++ b/pkg/migration/migrator.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/Layr-Labs/devkit-cli/internal/version" "github.com/Layr-Labs/devkit-cli/pkg/common" "github.com/Layr-Labs/devkit-cli/pkg/common/iface" "gopkg.in/yaml.v3" @@ -67,6 +68,28 @@ type VersionComparator func(string, string) bool // Known errors which we can ignore var ErrAlreadyUpToDate = errors.New("already up to date") +// checkVersionCompatibility validates that the context version is supported by the current CLI +func checkVersionCompatibility(contextVersion, contextFile string) error { + if contextVersion == "" { + // Missing version - could be very old context, warn but allow + return fmt.Errorf("context file %s is missing version field - this may be an old context that needs migration", contextFile) + } + + cliVersion := version.GetVersion() + + // If context version is newer than what we support, return compatibility error + if versionGreaterThan(contextVersion, common.DevkitLatestContextVersion) { + return &common.VersionCompatibilityError{ + ContextVersion: contextVersion, + CLIVersion: cliVersion, + LatestSupported: common.DevkitLatestContextVersion, + ContextFile: contextFile, + } + } + + return nil +} + // Apply walks each rule, and when Condition is met, either removes the node or replaces it with a (transformed) copy func (e *PatchEngine) Apply() error { for _, rule := range e.Rules { @@ -113,6 +136,11 @@ func MigrateYaml(logger iface.Logger, path string, latestVersion string, migrati from := verNode.Value to := latestVersion + // Check version compatibility BEFORE attempting migration + if err := checkVersionCompatibility(from, path); err != nil { + return err + } + // Continue and don't say anything if the user version is latest if from == to { return ErrAlreadyUpToDate @@ -148,6 +176,7 @@ func MigrateNode( if step.From != current { continue } + if versionGreaterThan(step.To, to) { break }