From 9c1c7bc9c3ca8798930f48872faff657cc6e5f1a Mon Sep 17 00:00:00 2001 From: adilson Date: Mon, 19 Oct 2026 19:37:57 -0300 Subject: [PATCH 1/7] Revert "Merge pull request #444 from Infisical/auto-create-yaml" This reverts commit bf67dc45193fed7afa28ef330597fcbb3cd9438f, reversing changes made to 407a676c175377818785d06b0201db850d5e7cb3. Co-Authored-By: Claude Opus 5.5 --- packages/cmd/init.go | 5 +- packages/cmd/init_test.go | 3 - packages/cmd/migrate_config.go | 64 ++++++ packages/cmd/migrate_config_test.go | 102 +++++++++ packages/cmd/root.go | 13 +- packages/models/cli.go | 1 - packages/util/config.go | 60 +----- packages/util/config_test.go | 194 ++---------------- packages/util/constants.go | 1 - packages/util/migration_notice.go | 8 +- packages/util/migration_notice_test.go | 6 +- packages/util/resolve_test.go | 8 +- .../util/workspace_config_migration_notice.go | 25 +-- .../workspace_config_migration_notice_test.go | 49 +---- 14 files changed, 215 insertions(+), 324 deletions(-) create mode 100644 packages/cmd/migrate_config.go create mode 100644 packages/cmd/migrate_config_test.go diff --git a/packages/cmd/init.go b/packages/cmd/init.go index 851422e0..29cc53d7 100644 --- a/packages/cmd/init.go +++ b/packages/cmd/init.go @@ -337,7 +337,6 @@ func pickOrganization(httpClient *resty.Client, label string, username string) ( func writeWorkspaceFile(selectedWorkspace models.Workspace) error { workspaceFileToSave := models.WorkspaceConfigFileYaml{ - Version: util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION, SecretsManagement: models.SecretsManagementConfigSection{ ProjectID: selectedWorkspace.ID, }, @@ -354,7 +353,9 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace) error { } // the legacy file is left in place; .infisical.yaml takes precedence over it from now on - util.RemindToRemoveLegacyWorkspaceConfig(".") + if _, err := os.Stat(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil { + util.PrintWarning(fmt.Sprintf("The legacy %s in this directory is no longer used because %s takes precedence. You can delete it.", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) + } return nil } diff --git a/packages/cmd/init_test.go b/packages/cmd/init_test.go index a5a95682..6946c3aa 100644 --- a/packages/cmd/init_test.go +++ b/packages/cmd/init_test.go @@ -36,9 +36,6 @@ func TestWriteWorkspaceFile(t *testing.T) { if err := yaml.Unmarshal(raw, &written); err != nil { t.Fatalf("%s is not valid yaml: %v\n%s", yamlPath, err, raw) } - if written.Version != util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION { - t.Errorf("version = %d, want %d", written.Version, util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION) - } if written.SecretsManagement.ProjectID != "proj-new" { t.Errorf("secrets-management.project-id = %q, want proj-new", written.SecretsManagement.ProjectID) } diff --git a/packages/cmd/migrate_config.go b/packages/cmd/migrate_config.go new file mode 100644 index 00000000..bcb910c4 --- /dev/null +++ b/packages/cmd/migrate_config.go @@ -0,0 +1,64 @@ +/* +Copyright (c) 2026 Infisical Inc. +*/ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/Infisical/infisical-merge/packages/util" + "github.com/posthog/posthog-go" + "github.com/spf13/cobra" +) + +var migrateConfigCmd = &cobra.Command{ + Use: "migrate-config", + Short: "Convert this project's legacy .infisical.json into .infisical.yaml", + Example: "infisical migrate-config", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + if err := migrateLegacyWorkspaceConfig(); err != nil { + util.HandleError(err, fmt.Sprintf("Unable to migrate %s to %s", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) + } + + Telemetry.CaptureEvent("cli-command:migrate-config", posthog.NewProperties().Set("version", util.CLI_VERSION)) + }, +} + +func migrateLegacyWorkspaceConfig() error { + configFilePath, err := util.FindWorkspaceConfigFile() + if err != nil { + return fmt.Errorf("no %s found in this directory or any parent directory", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + } + configFileDir := filepath.Dir(configFilePath) + + if filepath.Base(configFilePath) == util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { + util.PrintlnStderr(fmt.Sprintf("This project already uses %s. Nothing to migrate.", configFilePath)) + if _, err := os.Stat(filepath.Join(configFileDir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)); err == nil { + util.PrintWarning(fmt.Sprintf("The legacy %s in %s is no longer used because %s takes precedence. You can delete it.", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) + } + return nil + } + + yamlConfigFilePath := filepath.Join(configFileDir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + if err := util.ConvertLegacyWorkspaceConfigToYaml(configFilePath, yamlConfigFilePath); err != nil { + return err + } + + if err := os.Remove(configFilePath); err != nil { + util.PrintWarning(fmt.Sprintf("Wrote %s but unable to remove the legacy %s; delete it manually [err=%s]", yamlConfigFilePath, configFilePath, err)) + return nil + } + + util.PrintSuccessMessage(fmt.Sprintf("Migrated %s to %s.", configFilePath, yamlConfigFilePath)) + util.PrintlnStderr(fmt.Sprintf("If this project is under version control, commit the new %s and the removal of %s.", util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) + + return nil +} + +func init() { + RootCmd.AddCommand(migrateConfigCmd) +} diff --git a/packages/cmd/migrate_config_test.go b/packages/cmd/migrate_config_test.go new file mode 100644 index 00000000..6172cf34 --- /dev/null +++ b/packages/cmd/migrate_config_test.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "os" + "path/filepath" + "testing" + + "github.com/Infisical/infisical-merge/packages/util" +) + +const migrateConfigTestJSON = `{ + "workspaceId": "proj-123", + "defaultEnvironment": "dev", + "gitBranchToEnvironmentMapping": {"main": "prod"}, + "defaultSecretPath": "/backend", + "domain": "https://eu.infisical.com" +}` + +func writeMigrateConfigTestFile(t *testing.T, path string, contents string) { + t.Helper() + if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { + t.Fatalf("write %s: %v", path, err) + } +} + +func assertMigratedWorkspaceConfig(t *testing.T, dir string) { + t.Helper() + if _, err := os.Stat(filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)); !os.IsNotExist(err) { + t.Errorf("legacy %s should be removed after migrating [err=%v]", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, err) + } + + cfg, err := util.GetWorkSpaceFromFilePath(dir) + if err != nil { + t.Fatalf("GetWorkSpaceFromFilePath: %v", err) + } + if cfg.WorkspaceId != "proj-123" || cfg.DefaultEnvironment != "dev" || cfg.DefaultSecretPath != "/backend" || + cfg.Domain != "https://eu.infisical.com" || cfg.GitBranchToEnvironmentMapping["main"] != "prod" { + t.Errorf("migrated config does not match the legacy json: %+v", cfg) + } +} + +func TestMigrateLegacyWorkspaceConfig(t *testing.T) { + t.Run("migrates the json in the current directory", func(t *testing.T) { + dir := isolateWorkspaceDir(t) + writeMigrateConfigTestFile(t, filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME), migrateConfigTestJSON) + + if err := migrateLegacyWorkspaceConfig(); err != nil { + t.Fatalf("migrateLegacyWorkspaceConfig: %v", err) + } + assertMigratedWorkspaceConfig(t, dir) + }) + + t.Run("migrates the json found in a parent directory", func(t *testing.T) { + dir := isolateWorkspaceDir(t) + writeMigrateConfigTestFile(t, filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME), migrateConfigTestJSON) + nested := filepath.Join(dir, "a", "b") + if err := os.MkdirAll(nested, 0o700); err != nil { + t.Fatalf("mkdir: %v", err) + } + t.Chdir(nested) + + if err := migrateLegacyWorkspaceConfig(); err != nil { + t.Fatalf("migrateLegacyWorkspaceConfig: %v", err) + } + assertMigratedWorkspaceConfig(t, dir) + if _, err := os.Stat(filepath.Join(nested, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)); !os.IsNotExist(err) { + t.Errorf("yaml should be written next to the json, not in the current directory [err=%v]", err) + } + }) + + t.Run("does nothing when the project already uses yaml", func(t *testing.T) { + dir := isolateWorkspaceDir(t) + jsonPath := filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + yamlPath := filepath.Join(dir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + const existingYAML = "secrets-management:\n project-id: proj-yaml\n" + writeMigrateConfigTestFile(t, jsonPath, migrateConfigTestJSON) + writeMigrateConfigTestFile(t, yamlPath, existingYAML) + + if err := migrateLegacyWorkspaceConfig(); err != nil { + t.Fatalf("migrateLegacyWorkspaceConfig: %v", err) + } + + raw, err := os.ReadFile(yamlPath) + if err != nil { + t.Fatalf("read %s: %v", yamlPath, err) + } + if string(raw) != existingYAML { + t.Errorf("existing yaml was modified:\n%s", raw) + } + if _, err := os.Stat(jsonPath); err != nil { + t.Errorf("leftover json should not be touched [err=%v]", err) + } + }) + + t.Run("errors when there is no workspace config", func(t *testing.T) { + isolateWorkspaceDir(t) + + if err := migrateLegacyWorkspaceConfig(); err == nil { + t.Fatal("expected an error when no workspace config exists") + } + }) +} diff --git a/packages/cmd/root.go b/packages/cmd/root.go index a14d4476..c421e081 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -162,9 +162,6 @@ func init() { if err != nil { util.HandleError(err) } - if !silent && !isStructuredOutputRequested(cmd) { - util.EnableWorkspaceConfigMessages() - } profileFlag, err := cmd.Flags().GetString("profile") if err != nil { @@ -197,11 +194,13 @@ func init() { if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) { util.CheckForUpdateWithWriter(cmd.ErrOrStderr()) - projectConfigDir := "" - if f := cmd.Flags().Lookup("project-config-dir"); f != nil { - projectConfigDir = f.Value.String() + if cmd != migrateConfigCmd { + projectConfigDir := "" + if f := cmd.Flags().Lookup("project-config-dir"); f != nil { + projectConfigDir = f.Value.String() + } + util.DisplayWorkspaceConfigMigrationNoticeWithWriter(silent, projectConfigDir, cmd.ErrOrStderr()) } - util.DisplayWorkspaceConfigMigrationNoticeWithWriter(silent, projectConfigDir, cmd.ErrOrStderr()) } loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) diff --git a/packages/models/cli.go b/packages/models/cli.go index a393fc6c..19063074 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -196,7 +196,6 @@ type SecretsManagementConfigSection struct { } type WorkspaceConfigFileYaml struct { - Version int `yaml:"version"` General GeneralConfigSection `yaml:"general"` SecretsManagement SecretsManagementConfigSection `yaml:"secrets-management"` } diff --git a/packages/util/config.go b/packages/util/config.go index 6bd7693f..cdc60c57 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -70,9 +70,6 @@ func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, // the yaml file takes precedence over the legacy json file when both exist if workspaceConfigFile, exists, err := readWorkspaceConfigYamlIfExists(yamlConfigFilePath); exists { - if err == nil { - RemindToRemoveLegacyWorkspaceConfig(configFileDir) - } return workspaceConfigFile, err } @@ -80,51 +77,7 @@ func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, return models.WorkspaceConfigFile{}, fmt.Errorf("no %s or %s found in %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir) } - workspaceConfigFile, err := GetWorkspaceConfigByPath(jsonConfigFilePath) - if err != nil { - return models.WorkspaceConfigFile{}, err - } - - createWorkspaceConfigYamlFromLegacy(jsonConfigFilePath, yamlConfigFilePath) - - return workspaceConfigFile, nil -} - -func createWorkspaceConfigYamlFromLegacy(jsonConfigFilePath string, yamlConfigFilePath string) bool { - if err := convertLegacyWorkspaceConfigToYaml(jsonConfigFilePath, yamlConfigFilePath); err != nil { - log.Debug().Err(err).Msgf("createWorkspaceConfigYamlFromLegacy: unable to create [path=%s], using the legacy [path=%s]", yamlConfigFilePath, jsonConfigFilePath) - return false - } - - if workspaceConfigMessagesEnabled { - PrintlnStderr(fmt.Sprintf("Created %s from the legacy %s (commit it if this project is under version control).", yamlConfigFilePath, jsonConfigFilePath)) - } - RemindToRemoveLegacyWorkspaceConfig(filepath.Dir(jsonConfigFilePath)) - return true -} - -var legacyWorkspaceConfigReminderShown bool - -var workspaceConfigMessagesEnabled bool - -func EnableWorkspaceConfigMessages() { - workspaceConfigMessagesEnabled = true -} - -func RemindToRemoveLegacyWorkspaceConfig(configFileDir string) { - if legacyWorkspaceConfigReminderShown || !workspaceConfigMessagesEnabled { - return - } - if absDir, err := filepath.Abs(configFileDir); err == nil { - configFileDir = absDir - } - jsonConfigFilePath := filepath.Join(configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - if _, err := os.Stat(jsonConfigFilePath); err != nil { - return - } - - legacyWorkspaceConfigReminderShown = true - PrintWarning(fmt.Sprintf("Both %s and %s exist in %s. %s takes precedence, so the legacy %s is no longer used and you can remove it.", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, jsonConfigFilePath)) + return GetWorkspaceConfigByPath(jsonConfigFilePath) } func readWorkspaceConfigYamlIfExists(path string) (workspaceConfigFile models.WorkspaceConfigFile, exists bool, err error) { @@ -154,11 +107,11 @@ func readWorkspaceConfigYaml(path string) (models.WorkspaceConfigFile, error) { return workspaceConfigFromYaml(workspaceConfigFileYaml), nil } -func convertLegacyWorkspaceConfigToYaml(jsonConfigFilePath string, yamlConfigFilePath string) error { +func ConvertLegacyWorkspaceConfigToYaml(jsonConfigFilePath string, yamlConfigFilePath string) error { if _, err := os.Stat(yamlConfigFilePath); err == nil { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: %s already exists", yamlConfigFilePath) + return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: %s already exists", yamlConfigFilePath) } else if !os.IsNotExist(err) { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: unable to check %s [err=%s]", yamlConfigFilePath, err) + return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: unable to check %s [err=%s]", yamlConfigFilePath, err) } legacyWorkspaceConfig, err := GetWorkspaceConfigByPath(jsonConfigFilePath) @@ -168,12 +121,12 @@ func convertLegacyWorkspaceConfigToYaml(jsonConfigFilePath string, yamlConfigFil yamlConfigFileAsBytes, err := yaml.Marshal(workspaceConfigToYaml(legacyWorkspaceConfig)) if err != nil { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: unable to marshal yaml [err=%s]", err) + return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: unable to marshal yaml [err=%s]", err) } // written atomically so a partially written .infisical.yaml is never left behind if err := WriteFileAtomic(yamlConfigFilePath, yamlConfigFileAsBytes, 0600); err != nil { - return fmt.Errorf("convertLegacyWorkspaceConfigToYaml: unable to create %s [err=%s]", yamlConfigFilePath, err) + return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: unable to create %s [err=%s]", yamlConfigFilePath, err) } if _, err := readWorkspaceConfigYaml(yamlConfigFilePath); err != nil { @@ -186,7 +139,6 @@ func convertLegacyWorkspaceConfigToYaml(jsonConfigFilePath string, yamlConfigFil func workspaceConfigToYaml(workspaceConfig models.WorkspaceConfigFile) models.WorkspaceConfigFileYaml { var workspaceConfigYaml models.WorkspaceConfigFileYaml - workspaceConfigYaml.Version = INFISICAL_WORKSPACE_CONFIG_YAML_VERSION workspaceConfigYaml.General.Domain = workspaceConfig.Domain workspaceConfigYaml.SecretsManagement.ProjectID = workspaceConfig.WorkspaceId workspaceConfigYaml.SecretsManagement.DefaultEnvironment = workspaceConfig.DefaultEnvironment diff --git a/packages/util/config_test.go b/packages/util/config_test.go index a7f8fcc4..a5fda686 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -1,12 +1,9 @@ package util import ( - "bytes" - "io" "os" "path/filepath" "runtime" - "strings" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -174,125 +171,6 @@ func fileExists(path string) bool { return err == nil } -// makeDirReadOnly blocks creating files in dir for the rest of the test, so a .infisical.yaml can't be written. -func makeDirReadOnly(t *testing.T, dir string) { - t.Helper() - if runtime.GOOS == "windows" || os.Geteuid() == 0 { - t.Skip("directory permissions don't block writes here") - } - if err := os.Chmod(dir, 0o500); err != nil { - t.Fatalf("chmod %s: %v", dir, err) - } - t.Cleanup(func() { _ = os.Chmod(dir, 0o700) }) -} - -// captureStderr sends what the CLI prints to stderr into the returned buffer for the rest of the test. -func captureStderr(t *testing.T) *bytes.Buffer { - t.Helper() - var buf bytes.Buffer - original := GetStderrWriter - GetStderrWriter = func() io.Writer { return &buf } - t.Cleanup(func() { GetStderrWriter = original }) - return &buf -} - -// enableWorkspaceConfigMessages turns the yaml messages on for the rest of the test, as the root command does -// outside --silent and structured output. -func enableWorkspaceConfigMessages(t *testing.T) { - t.Helper() - EnableWorkspaceConfigMessages() - t.Cleanup(func() { workspaceConfigMessagesEnabled = false }) -} - -// startNewRun forgets that the reminder to remove the legacy json was shown, as if the CLI was run again. -func startNewRun() { - legacyWorkspaceConfigReminderShown = false -} - -func TestLegacyWorkspaceConfigRemovalMessage(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - stderr := captureStderr(t) - enableWorkspaceConfigMessages(t) - startNewRun() - - readWorkspaceConfig := func(t *testing.T) string { - t.Helper() - stderr.Reset() - cfg, err := GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("GetWorkSpaceFromFilePath: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - return stderr.String() - } - assertContains := func(t *testing.T, out string, wants ...string) { - t.Helper() - for _, want := range wants { - if !strings.Contains(out, want) { - t.Errorf("output missing %q, got:\n%s", want, out) - } - } - } - - bothFilesExist := []string{"Both .infisical.yaml and .infisical.json exist in " + dir, "the legacy " + jsonPath + " is no longer used and you can remove it"} - - // the run that creates the yaml says both files exist - out := readWorkspaceConfig(t) - assertContains(t, out, append([]string{"Created " + yamlPath}, bothFilesExist...)...) - if out := readWorkspaceConfig(t); out != "" { - t.Errorf("expected the message once per run, got on the second read:\n%s", out) - } - - // later runs keep saying it while both files exist, without creating the yaml again - for run := 2; run <= 3; run++ { - startNewRun() - out := readWorkspaceConfig(t) - assertContains(t, out, bothFilesExist...) - if strings.Contains(out, "Created ") { - t.Errorf("run %d: the yaml already exists, so it should not be created again, got:\n%s", run, out) - } - if out := readWorkspaceConfig(t); out != "" { - t.Errorf("run %d: expected the message once per run, got on the second read:\n%s", run, out) - } - } - - // nothing to say once the json is gone - if err := os.Remove(jsonPath); err != nil { - t.Fatalf("remove %s: %v", jsonPath, err) - } - startNewRun() - if out := readWorkspaceConfig(t); out != "" { - t.Errorf("expected no message once the legacy json is removed, got:\n%s", out) - } -} - -func TestLegacyWorkspaceConfigMessagesSilenced(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - // --silent and structured output leave the messages off - stderr := captureStderr(t) - - // the run that creates the yaml, then a later run where both files exist - for run := 1; run <= 2; run++ { - startNewRun() - cfg, err := GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("run %d: GetWorkSpaceFromFilePath: %v", run, err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if !fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Fatalf("run %d: the yaml should still be created in silent mode", run) - } - if out := stderr.String(); out != "" { - t.Errorf("run %d: expected no output in silent mode, got:\n%s", run, out) - } - } -} - func TestGetWorkSpaceFromFilePath(t *testing.T) { t.Run("reads yaml when only yaml exists", func(t *testing.T) { dir := t.TempDir() @@ -305,10 +183,9 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { assertFullWorkspaceConfig(t, cfg, "proj-123") }) - t.Run("reads legacy json and creates a yaml beside it", func(t *testing.T) { + t.Run("reads legacy json when only json exists", func(t *testing.T) { dir := t.TempDir() jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, legacyWorkspaceJSON) cfg, err := GetWorkSpaceFromFilePath(dir) @@ -317,45 +194,12 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { } assertFullWorkspaceConfig(t, cfg, "proj-123") - yamlCfg, err := readWorkspaceConfigYaml(yamlPath) - if err != nil { - t.Fatalf("yaml should be created when reading the legacy json: %v", err) - } - assertFullWorkspaceConfig(t, yamlCfg, "proj-123") - - raw, err := os.ReadFile(jsonPath) - if err != nil { - t.Fatalf("legacy %s should not be removed: %v", jsonPath, err) - } - if string(raw) != legacyWorkspaceJSON { - t.Errorf("legacy json was modified:\n%s", raw) - } - - // later reads come from the yaml - cfg, err = GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("unexpected error on the second read: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - }) - - t.Run("falls back to the legacy json when the yaml can't be created", func(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - makeDirReadOnly(t, dir) - - cfg, err := GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("a failed yaml creation should not fail the read: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("yaml should not exist in a read-only directory") - } if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } + if fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + t.Error("yaml should not be created when reading the legacy json") + } }) t.Run("prefers yaml and leaves json untouched when both exist", func(t *testing.T) { @@ -415,20 +259,16 @@ func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } - yamlPath := filepath.Join(root, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - if !fileExists(yamlPath) { - t.Error("yaml should be created next to the legacy json, not in the current directory") - } - if fileExists(filepath.Join(nested, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("yaml should not be created in the current directory") + if fileExists(filepath.Join(root, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + t.Error("yaml should not be created when reading the legacy json") } found, err := FindWorkspaceConfigFile() if err != nil { t.Fatalf("FindWorkspaceConfigFile: %v", err) } - if filepath.Base(found) != INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("FindWorkspaceConfigFile = %q, want the created yaml file", found) + if filepath.Base(found) != INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + t.Errorf("FindWorkspaceConfigFile = %q, want the legacy json file", found) } } @@ -439,8 +279,8 @@ func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, legacyWorkspaceJSON) - if err := convertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err != nil { - t.Fatalf("convertLegacyWorkspaceConfigToYaml: %v", err) + if err := ConvertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err != nil { + t.Fatalf("ConvertLegacyWorkspaceConfigToYaml: %v", err) } cfg, err := readWorkspaceConfigYaml(yamlPath) @@ -449,14 +289,6 @@ func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { } assertFullWorkspaceConfig(t, cfg, "proj-123") - raw, err := os.ReadFile(yamlPath) - if err != nil { - t.Fatalf("read %s: %v", yamlPath, err) - } - if !strings.HasPrefix(string(raw), "version: 1\n") { - t.Errorf("yaml should start with the config version, got:\n%s", raw) - } - info, err := os.Stat(yamlPath) if err != nil { t.Fatalf("stat %s: %v", yamlPath, err) @@ -465,7 +297,7 @@ func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { t.Errorf("yaml perm = %o, want 600", info.Mode().Perm()) } if !fileExists(jsonPath) { - t.Error("the legacy json should be left in place") + t.Error("the legacy json should be left for the caller to remove") } }) @@ -477,7 +309,7 @@ func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { const existingYAML = "secrets-management:\n project-id: proj-existing\n" writeTestFile(t, yamlPath, existingYAML) - if err := convertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { + if err := ConvertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { t.Fatal("expected an error when the yaml already exists") } raw, err := os.ReadFile(yamlPath) @@ -495,7 +327,7 @@ func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, "{not json") - if err := convertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { + if err := ConvertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { t.Fatal("expected an error for invalid json") } if fileExists(yamlPath) { diff --git a/packages/util/constants.go b/packages/util/constants.go index c94a0eda..70846f66 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -13,7 +13,6 @@ const ( INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME = ".infisical.yaml" - INFISICAL_WORKSPACE_CONFIG_YAML_VERSION = 1 INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_PROJECT_ID_NAME = "INFISICAL_PROJECT_ID" INFISICAL_ENVIRONMENT_NAME = "INFISICAL_ENVIRONMENT" diff --git a/packages/util/migration_notice.go b/packages/util/migration_notice.go index 12823a04..70e2f6d2 100644 --- a/packages/util/migration_notice.go +++ b/packages/util/migration_notice.go @@ -15,8 +15,8 @@ import ( const migrationNoticeCacheTTL = 24 * time.Hour type MigrationNotice struct { - ID string - Title string + ID string + Title string Message string DocsURL string } @@ -25,7 +25,7 @@ type migrationNoticeCache struct { LastShownTimes map[string]time.Time `json:"lastShownTimes"` } -// migrationNoticeCacheSingleton is the one cache map for this process. +// migrationNoticeCacheSingleton is the one cache map for this process. var migrationNoticeCacheSingleton = migrationNoticeCache{LastShownTimes: map[string]time.Time{}} var migrationNoticeCachePath string @@ -104,7 +104,7 @@ func DisplayMigrationNoticeWithWriter(notice MigrationNotice, w io.Writer) { body := notice.Message if notice.DocsURL != "" { - body += "\n\nYou can read more here: " + notice.DocsURL + body += "\nMigration steps: " + notice.DocsURL } fmt.Fprintln(w, bold(notice.Title)) diff --git a/packages/util/migration_notice_test.go b/packages/util/migration_notice_test.go index 3c3519c6..fb0036b9 100644 --- a/packages/util/migration_notice_test.go +++ b/packages/util/migration_notice_test.go @@ -30,8 +30,8 @@ func TestDisplayMigrationNotice(t *testing.T) { }, &buf) want := "Important: X is deprecated.\n" + - "What's happening: X goes away.\nWhat to do: use Y.\n\n" + - "You can read more here: https://infisical.com/docs/x-to-y\n\n" + "What's happening: X goes away.\nWhat to do: use Y.\n" + + "Migration steps: https://infisical.com/docs/x-to-y\n\n" if buf.String() != want { t.Errorf("notice = %q, want %q", buf.String(), want) } @@ -46,7 +46,7 @@ func TestDisplayMigrationNotice(t *testing.T) { var buf bytes.Buffer DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "What to do: use Y."}, &buf) - if strings.Contains(buf.String(), "You can read more here:") { + if strings.Contains(buf.String(), "Migration steps:") { t.Errorf("expected no docs line, got:\n%s", buf.String()) } }) diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index 94eae2bb..f81e3243 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -142,7 +142,7 @@ func TestResolveEnvironmentName(t *testing.T) { } }) - t.Run("legacy json is read and a yaml is created beside it", func(t *testing.T) { + t.Run("legacy json is read without being migrated", func(t *testing.T) { writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" { @@ -151,10 +151,10 @@ func TestResolveEnvironmentName(t *testing.T) { if !fileExists(INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { t.Fatalf("legacy %s should not be removed", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } - if !fileExists(INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) { - t.Fatalf("%s should be created", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + if fileExists(INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) { + t.Fatalf("%s should not be created", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) } - // the second resolve reads the created yaml + // the second resolve still reads the legacy json if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" { t.Fatalf("got %q on the second resolve, want fromfile", got) } diff --git a/packages/util/workspace_config_migration_notice.go b/packages/util/workspace_config_migration_notice.go index 760f2eb6..15c0a1d8 100644 --- a/packages/util/workspace_config_migration_notice.go +++ b/packages/util/workspace_config_migration_notice.go @@ -12,9 +12,7 @@ const workspaceConfigMigrationNoticeID = "workspace-config-yaml" const workspaceConfigMigrationGuideURL = "https://infisical.com/docs/cli/project-config-migration" // DisplayWorkspaceConfigMigrationNoticeWithWriter prints a once-per-day notice when the -// workspace config in use is a legacy .infisical.json and the CLI can't create a .infisical.yaml -// next to it. It first tries to create the yaml itself, so the notice only shows when that fails -// (read-only directory, invalid json, ...). +// workspace config in use is a legacy .infisical.json rather than a .infisical.yaml. // Stays quiet in --silent mode, and can be disabled with INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE. // // projectConfigDir is the value of --project-config-dir when the command defines it; when set, @@ -33,26 +31,15 @@ func DisplayWorkspaceConfigMigrationNoticeWithWriter(isSilent bool, projectConfi return } - // nothing to tell the user once the yaml exists, whether created here or by another command - configFileDir := filepath.Dir(legacyConfigFilePath) - yamlConfigFilePath := filepath.Join(configFileDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - if createWorkspaceConfigYamlFromLegacy(legacyConfigFilePath, yamlConfigFilePath) { - return - } - if _, err := os.Stat(yamlConfigFilePath); err == nil { - return - } - DisplayMigrationNoticeWithWriter(MigrationNotice{ ID: workspaceConfigMigrationNoticeID, - Title: fmt.Sprintf("Important: unable to create %s. This project still uses the deprecated %s.", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), + Title: fmt.Sprintf("Important: %s is deprecated. Move this project to %s.", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), Message: fmt.Sprintf( - "\nThe Infisical CLI is reading the legacy %s config file, and support for it will be removed in the near future.\n"+ - "Falling back to the legacy config file \"%s\".\n\n"+ - "The CLI attempts to create the %s config file next to it automatically, but could not write it to \"%s\".\n\n"+ - "What to do: make sure that the directory is writable and your \"%s\" file is valid JSON, then run any Infisical command again, or create \"%s\" there yourself.", + "What's happening: the CLI is reading a legacy %s, and support for it will be removed\n"+ + "in a future release. File in use: %s\n"+ + "What to do: run `infisical migrate-config` in %s to convert it to %s.", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, legacyConfigFilePath, - INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, + filepath.Dir(legacyConfigFilePath), INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, ), DocsURL: workspaceConfigMigrationGuideURL, }, w) diff --git a/packages/util/workspace_config_migration_notice_test.go b/packages/util/workspace_config_migration_notice_test.go index dabc9ffe..fb184e93 100644 --- a/packages/util/workspace_config_migration_notice_test.go +++ b/packages/util/workspace_config_migration_notice_test.go @@ -42,16 +42,15 @@ func TestWorkspaceConfigMigrationNoticeShownForLegacyJSON(t *testing.T) { projectDir := setupWorkspaceConfigMigrationNoticeTest(t) jsonPath := filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, legacyWorkspaceJSON) - makeDirReadOnly(t, projectDir) var buf bytes.Buffer DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) out := buf.String() for _, want := range []string{ - "Important: unable to create .infisical.yaml", + "Important: .infisical.json is deprecated", jsonPath, - "could not write it in " + projectDir, + "infisical migrate-config", workspaceConfigMigrationGuideURL, } { if !strings.Contains(out, want) { @@ -77,7 +76,6 @@ func TestWorkspaceConfigMigrationNoticeShownForLegacyJSONInParentDir(t *testing. t.Fatalf("mkdir: %v", err) } t.Chdir(nested) - makeDirReadOnly(t, projectDir) var buf bytes.Buffer DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) @@ -86,38 +84,6 @@ func TestWorkspaceConfigMigrationNoticeShownForLegacyJSONInParentDir(t *testing. } } -func TestWorkspaceConfigMigrationNoticeCreatesYamlInsteadOfWarning(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - jsonPath := filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - stderr := captureStderr(t) - enableWorkspaceConfigMessages(t) - startNewRun() - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice once the yaml is created, got:\n%s", buf.String()) - } - - // the command reading the new yaml in the same run must not repeat that the json can be removed - if _, err := GetWorkSpaceFromFile(); err != nil { - t.Fatalf("GetWorkSpaceFromFile: %v", err) - } - if got := strings.Count(stderr.String(), "you can remove"); got != 1 { - t.Errorf("expected the removal message once, got it %d times:\n%s", got, stderr.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - if !fileExists(filepath.Join(projectDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("expected the yaml to be created next to the legacy json") - } - if !fileExists(jsonPath) { - t.Errorf("legacy %s should not be removed", jsonPath) - } -} - func TestWorkspaceConfigMigrationNoticeNotShown(t *testing.T) { cases := []struct { name string @@ -205,9 +171,6 @@ func TestWorkspaceConfigMigrationNoticeHonorsProjectConfigDir(t *testing.T) { if fileExists(migrationNoticeCachePathForTest(t)) { t.Error("expected no notice cache file to be written") } - if fileExists(filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("the repo root json is not the file the run reads, so no yaml should be created for it") - } }) t.Run("legacy json in project config dir, yaml above cwd", func(t *testing.T) { @@ -215,15 +178,14 @@ func TestWorkspaceConfigMigrationNoticeHonorsProjectConfigDir(t *testing.T) { writeTestFile(t, filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) apiJSONPath := filepath.Join(apiDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, apiJSONPath, legacyWorkspaceJSON) - makeDirReadOnly(t, apiDir) var buf bytes.Buffer DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) out := buf.String() for _, want := range []string{ - "Important: unable to create .infisical.yaml", + "Important: .infisical.json is deprecated", "File in use: " + apiJSONPath, - "could not write it in " + apiDir, + "run `infisical migrate-config` in " + apiDir, } { if !strings.Contains(out, want) { t.Errorf("notice missing %q, got:\n%s", want, out) @@ -243,8 +205,5 @@ func TestWorkspaceConfigMigrationNoticeHonorsProjectConfigDir(t *testing.T) { if buf.Len() != 0 { t.Errorf("expected no notice when the project config dir has no config, got:\n%s", buf.String()) } - if fileExists(filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("the repo root json is not the file the run reads, so no yaml should be created for it") - } }) } From d800d6c064908823e7591bfe86101075dd98530e Mon Sep 17 00:00:00 2001 From: adilson Date: Mon, 19 Oct 2026 20:09:02 -0300 Subject: [PATCH 2/7] remove the auto migration to .yaml file --- packages/cmd/init_test.go | 7 + packages/cmd/migrate_config.go | 64 ----- packages/cmd/migrate_config_test.go | 102 -------- packages/cmd/root.go | 7 - packages/models/cli.go | 28 ++- packages/util/config.go | 91 +++---- packages/util/config_test.go | 236 +++++++++--------- packages/util/constants.go | 3 +- packages/util/helper.go | 2 +- packages/util/migration_notice.go | 114 --------- packages/util/migration_notice_test.go | 153 ------------ packages/util/resolve_test.go | 40 +++ .../util/workspace_config_migration_notice.go | 46 ---- .../workspace_config_migration_notice_test.go | 209 ---------------- 14 files changed, 218 insertions(+), 884 deletions(-) delete mode 100644 packages/cmd/migrate_config.go delete mode 100644 packages/cmd/migrate_config_test.go delete mode 100644 packages/util/migration_notice.go delete mode 100644 packages/util/migration_notice_test.go delete mode 100644 packages/util/workspace_config_migration_notice.go delete mode 100644 packages/util/workspace_config_migration_notice_test.go diff --git a/packages/cmd/init_test.go b/packages/cmd/init_test.go index 6946c3aa..38e11a90 100644 --- a/packages/cmd/init_test.go +++ b/packages/cmd/init_test.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -39,6 +40,12 @@ func TestWriteWorkspaceFile(t *testing.T) { if written.SecretsManagement.ProjectID != "proj-new" { t.Errorf("secrets-management.project-id = %q, want proj-new", written.SecretsManagement.ProjectID) } + // a fresh config carries no environment settings, so neither the deprecated keys nor the environment section are written + for _, key := range []string{"default-environment", "mappings", "environment:"} { + if strings.Contains(string(raw), key) { + t.Errorf("a fresh config should not contain %q, got:\n%s", key, raw) + } + } cfg, err := util.GetWorkSpaceFromFile() if err != nil { diff --git a/packages/cmd/migrate_config.go b/packages/cmd/migrate_config.go deleted file mode 100644 index bcb910c4..00000000 --- a/packages/cmd/migrate_config.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright (c) 2026 Infisical Inc. -*/ -package cmd - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/Infisical/infisical-merge/packages/util" - "github.com/posthog/posthog-go" - "github.com/spf13/cobra" -) - -var migrateConfigCmd = &cobra.Command{ - Use: "migrate-config", - Short: "Convert this project's legacy .infisical.json into .infisical.yaml", - Example: "infisical migrate-config", - DisableFlagsInUseLine: true, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - if err := migrateLegacyWorkspaceConfig(); err != nil { - util.HandleError(err, fmt.Sprintf("Unable to migrate %s to %s", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) - } - - Telemetry.CaptureEvent("cli-command:migrate-config", posthog.NewProperties().Set("version", util.CLI_VERSION)) - }, -} - -func migrateLegacyWorkspaceConfig() error { - configFilePath, err := util.FindWorkspaceConfigFile() - if err != nil { - return fmt.Errorf("no %s found in this directory or any parent directory", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - } - configFileDir := filepath.Dir(configFilePath) - - if filepath.Base(configFilePath) == util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - util.PrintlnStderr(fmt.Sprintf("This project already uses %s. Nothing to migrate.", configFilePath)) - if _, err := os.Stat(filepath.Join(configFileDir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)); err == nil { - util.PrintWarning(fmt.Sprintf("The legacy %s in %s is no longer used because %s takes precedence. You can delete it.", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) - } - return nil - } - - yamlConfigFilePath := filepath.Join(configFileDir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - if err := util.ConvertLegacyWorkspaceConfigToYaml(configFilePath, yamlConfigFilePath); err != nil { - return err - } - - if err := os.Remove(configFilePath); err != nil { - util.PrintWarning(fmt.Sprintf("Wrote %s but unable to remove the legacy %s; delete it manually [err=%s]", yamlConfigFilePath, configFilePath, err)) - return nil - } - - util.PrintSuccessMessage(fmt.Sprintf("Migrated %s to %s.", configFilePath, yamlConfigFilePath)) - util.PrintlnStderr(fmt.Sprintf("If this project is under version control, commit the new %s and the removal of %s.", util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) - - return nil -} - -func init() { - RootCmd.AddCommand(migrateConfigCmd) -} diff --git a/packages/cmd/migrate_config_test.go b/packages/cmd/migrate_config_test.go deleted file mode 100644 index 6172cf34..00000000 --- a/packages/cmd/migrate_config_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package cmd - -import ( - "os" - "path/filepath" - "testing" - - "github.com/Infisical/infisical-merge/packages/util" -) - -const migrateConfigTestJSON = `{ - "workspaceId": "proj-123", - "defaultEnvironment": "dev", - "gitBranchToEnvironmentMapping": {"main": "prod"}, - "defaultSecretPath": "/backend", - "domain": "https://eu.infisical.com" -}` - -func writeMigrateConfigTestFile(t *testing.T, path string, contents string) { - t.Helper() - if err := os.WriteFile(path, []byte(contents), 0o600); err != nil { - t.Fatalf("write %s: %v", path, err) - } -} - -func assertMigratedWorkspaceConfig(t *testing.T, dir string) { - t.Helper() - if _, err := os.Stat(filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)); !os.IsNotExist(err) { - t.Errorf("legacy %s should be removed after migrating [err=%v]", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, err) - } - - cfg, err := util.GetWorkSpaceFromFilePath(dir) - if err != nil { - t.Fatalf("GetWorkSpaceFromFilePath: %v", err) - } - if cfg.WorkspaceId != "proj-123" || cfg.DefaultEnvironment != "dev" || cfg.DefaultSecretPath != "/backend" || - cfg.Domain != "https://eu.infisical.com" || cfg.GitBranchToEnvironmentMapping["main"] != "prod" { - t.Errorf("migrated config does not match the legacy json: %+v", cfg) - } -} - -func TestMigrateLegacyWorkspaceConfig(t *testing.T) { - t.Run("migrates the json in the current directory", func(t *testing.T) { - dir := isolateWorkspaceDir(t) - writeMigrateConfigTestFile(t, filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME), migrateConfigTestJSON) - - if err := migrateLegacyWorkspaceConfig(); err != nil { - t.Fatalf("migrateLegacyWorkspaceConfig: %v", err) - } - assertMigratedWorkspaceConfig(t, dir) - }) - - t.Run("migrates the json found in a parent directory", func(t *testing.T) { - dir := isolateWorkspaceDir(t) - writeMigrateConfigTestFile(t, filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME), migrateConfigTestJSON) - nested := filepath.Join(dir, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - - if err := migrateLegacyWorkspaceConfig(); err != nil { - t.Fatalf("migrateLegacyWorkspaceConfig: %v", err) - } - assertMigratedWorkspaceConfig(t, dir) - if _, err := os.Stat(filepath.Join(nested, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)); !os.IsNotExist(err) { - t.Errorf("yaml should be written next to the json, not in the current directory [err=%v]", err) - } - }) - - t.Run("does nothing when the project already uses yaml", func(t *testing.T) { - dir := isolateWorkspaceDir(t) - jsonPath := filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - const existingYAML = "secrets-management:\n project-id: proj-yaml\n" - writeMigrateConfigTestFile(t, jsonPath, migrateConfigTestJSON) - writeMigrateConfigTestFile(t, yamlPath, existingYAML) - - if err := migrateLegacyWorkspaceConfig(); err != nil { - t.Fatalf("migrateLegacyWorkspaceConfig: %v", err) - } - - raw, err := os.ReadFile(yamlPath) - if err != nil { - t.Fatalf("read %s: %v", yamlPath, err) - } - if string(raw) != existingYAML { - t.Errorf("existing yaml was modified:\n%s", raw) - } - if _, err := os.Stat(jsonPath); err != nil { - t.Errorf("leftover json should not be touched [err=%v]", err) - } - }) - - t.Run("errors when there is no workspace config", func(t *testing.T) { - isolateWorkspaceDir(t) - - if err := migrateLegacyWorkspaceConfig(); err == nil { - t.Fatal("expected an error when no workspace config exists") - } - }) -} diff --git a/packages/cmd/root.go b/packages/cmd/root.go index c421e081..8695c070 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -194,13 +194,6 @@ func init() { if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) { util.CheckForUpdateWithWriter(cmd.ErrOrStderr()) - if cmd != migrateConfigCmd { - projectConfigDir := "" - if f := cmd.Flags().Lookup("project-config-dir"); f != nil { - projectConfigDir = f.Value.String() - } - util.DisplayWorkspaceConfigMigrationNoticeWithWriter(silent, projectConfigDir, cmd.ErrOrStderr()) - } } loggedInDetails, err := util.GetCurrentLoggedInUserDetails(false) diff --git a/packages/models/cli.go b/packages/models/cli.go index 19063074..b6c3c2f5 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -186,13 +186,31 @@ type GeneralConfigSection struct { Domain string `yaml:"domain,omitempty"` } +// GitBranchEnvironmentOverride is one entry of the secrets-management.environment.overrides.git-branch list. +type GitBranchEnvironmentOverride struct { + Branch string `yaml:"branch"` + Environment string `yaml:"environment"` +} + +type EnvironmentOverridesConfigSection struct { + GitBranch []GitBranchEnvironmentOverride `yaml:"git-branch,omitempty"` +} + +type EnvironmentConfigSection struct { + Default string `yaml:"default,omitempty"` + Overrides EnvironmentOverridesConfigSection `yaml:"overrides,omitempty"` +} + type SecretsManagementConfigSection struct { - ProjectID string `yaml:"project-id"` - DefaultEnvironment string `yaml:"default-environment"` + ProjectID string `yaml:"project-id"` + // Deprecated: superseded by Environment; still read until support for it is removed. + DefaultEnvironment string `yaml:"default-environment,omitempty"` DefaultSecretPath string `yaml:"default-secret-path,omitempty"` - Mappings struct { - GitBranchToEnvironment map[string]string `yaml:"git-branch-to-environment"` - } `yaml:"mappings"` + // Deprecated: superseded by Environment; still read until support for it is removed. + Mappings struct { + GitBranchToEnvironment map[string]string `yaml:"git-branch-to-environment,omitempty"` + } `yaml:"mappings,omitempty"` + Environment *EnvironmentConfigSection `yaml:"environment,omitempty"` } type WorkspaceConfigFileYaml struct { diff --git a/packages/util/config.go b/packages/util/config.go index cdc60c57..75d0a691 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -107,54 +107,44 @@ func readWorkspaceConfigYaml(path string) (models.WorkspaceConfigFile, error) { return workspaceConfigFromYaml(workspaceConfigFileYaml), nil } -func ConvertLegacyWorkspaceConfigToYaml(jsonConfigFilePath string, yamlConfigFilePath string) error { - if _, err := os.Stat(yamlConfigFilePath); err == nil { - return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: %s already exists", yamlConfigFilePath) - } else if !os.IsNotExist(err) { - return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: unable to check %s [err=%s]", yamlConfigFilePath, err) - } - - legacyWorkspaceConfig, err := GetWorkspaceConfigByPath(jsonConfigFilePath) - if err != nil { - return err - } - - yamlConfigFileAsBytes, err := yaml.Marshal(workspaceConfigToYaml(legacyWorkspaceConfig)) - if err != nil { - return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: unable to marshal yaml [err=%s]", err) - } - - // written atomically so a partially written .infisical.yaml is never left behind - if err := WriteFileAtomic(yamlConfigFilePath, yamlConfigFileAsBytes, 0600); err != nil { - return fmt.Errorf("ConvertLegacyWorkspaceConfigToYaml: unable to create %s [err=%s]", yamlConfigFilePath, err) +// workspaceConfigFromYaml accepts both the environment section and the deprecated default-environment / +// mappings fields. When the environment section has any value it wins, and the deprecated fields are ignored. +func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) models.WorkspaceConfigFile { + secretsManagement := workspaceConfigYaml.SecretsManagement + workspaceConfigFile := models.WorkspaceConfigFile{ + WorkspaceId: secretsManagement.ProjectID, + DefaultEnvironment: secretsManagement.DefaultEnvironment, + GitBranchToEnvironmentMapping: secretsManagement.Mappings.GitBranchToEnvironment, + DefaultSecretPath: secretsManagement.DefaultSecretPath, + Domain: workspaceConfigYaml.General.Domain, } - if _, err := readWorkspaceConfigYaml(yamlConfigFilePath); err != nil { - os.Remove(yamlConfigFilePath) - return err + environment := secretsManagement.Environment + if environment != nil && (environment.Default != "" || len(environment.Overrides.GitBranch) > 0) { + workspaceConfigFile.DefaultEnvironment = environment.Default + workspaceConfigFile.GitBranchToEnvironmentMapping = gitBranchMapFromOverrides(environment.Overrides.GitBranch) } - return nil -} - -func workspaceConfigToYaml(workspaceConfig models.WorkspaceConfigFile) models.WorkspaceConfigFileYaml { - var workspaceConfigYaml models.WorkspaceConfigFileYaml - workspaceConfigYaml.General.Domain = workspaceConfig.Domain - workspaceConfigYaml.SecretsManagement.ProjectID = workspaceConfig.WorkspaceId - workspaceConfigYaml.SecretsManagement.DefaultEnvironment = workspaceConfig.DefaultEnvironment - workspaceConfigYaml.SecretsManagement.DefaultSecretPath = workspaceConfig.DefaultSecretPath - workspaceConfigYaml.SecretsManagement.Mappings.GitBranchToEnvironment = workspaceConfig.GitBranchToEnvironmentMapping - return workspaceConfigYaml + return workspaceConfigFile } -func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) models.WorkspaceConfigFile { - return models.WorkspaceConfigFile{ - WorkspaceId: workspaceConfigYaml.SecretsManagement.ProjectID, - DefaultEnvironment: workspaceConfigYaml.SecretsManagement.DefaultEnvironment, - GitBranchToEnvironmentMapping: workspaceConfigYaml.SecretsManagement.Mappings.GitBranchToEnvironment, - DefaultSecretPath: workspaceConfigYaml.SecretsManagement.DefaultSecretPath, - Domain: workspaceConfigYaml.General.Domain, +// gitBranchMapFromOverrides builds the in-memory branch map from the git-branch overrides list. Entries +// without a branch are skipped, and the first entry for a branch wins. +func gitBranchMapFromOverrides(gitBranchOverrides []models.GitBranchEnvironmentOverride) map[string]string { + if len(gitBranchOverrides) == 0 { + return nil + } + branchToEnvironment := make(map[string]string, len(gitBranchOverrides)) + for _, override := range gitBranchOverrides { + if override.Branch == "" { + continue + } + if _, exists := branchToEnvironment[override.Branch]; exists { + continue + } + branchToEnvironment[override.Branch] = override.Environment } + return branchToEnvironment } // findWorkspaceConfigFileInDir looks for a .infisical.yaml (or legacy .infisical.json) in exactly @@ -169,25 +159,6 @@ func findWorkspaceConfigFileInDir(dir string) (string, bool) { return "", false } -// ResolveWorkspaceConfigFile returns the path of the workspace config file a command will read. -// With an explicit projectConfigDir (the --project-config-dir flag) only that directory is -// considered, matching GetWorkSpaceFromFilePath. Otherwise it walks up from the current directory. -func ResolveWorkspaceConfigFile(projectConfigDir string) (string, error) { - if projectConfigDir == "" { - return FindWorkspaceConfigFile() - } - - path, found := findWorkspaceConfigFileInDir(projectConfigDir) - if !found { - return "", fmt.Errorf("no %s or %s found in %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, projectConfigDir) - } - - if absPath, err := filepath.Abs(path); err == nil { - path = absPath - } - return path, nil -} - // FindWorkspaceConfigFile searches for a .infisical.yaml (or legacy .infisical.json) file in the current directory // and all parent directories. In each directory the YAML file takes precedence. func FindWorkspaceConfigFile() (string, error) { diff --git a/packages/util/config_test.go b/packages/util/config_test.go index a5fda686..645f4e04 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -1,9 +1,9 @@ package util import ( + "maps" "os" "path/filepath" - "runtime" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -241,155 +241,149 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { }) } -func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { - root := t.TempDir() - jsonPath := filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - nested := filepath.Join(root, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - - cfg, err := GetWorkSpaceFromFile() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if !fileExists(jsonPath) { - t.Errorf("legacy %s should not be removed", jsonPath) - } - if fileExists(filepath.Join(root, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { - t.Error("yaml should not be created when reading the legacy json") - } - - found, err := FindWorkspaceConfigFile() - if err != nil { - t.Fatalf("FindWorkspaceConfigFile: %v", err) - } - if filepath.Base(found) != INFISICAL_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("FindWorkspaceConfigFile = %q, want the legacy json file", found) - } -} +const workspaceYAMLWithEnvironment = `general: + domain: https://eu.infisical.com +secrets-management: + project-id: proj-123 + default-secret-path: /backend + environment: + default: dev + overrides: + git-branch: + - branch: main + environment: prod + - branch: staging + environment: staging +` -func TestConvertLegacyWorkspaceConfigToYaml(t *testing.T) { - t.Run("carries every field over and leaves the json in place", func(t *testing.T) { +func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { + readYaml := func(t *testing.T, contents string) (models.WorkspaceConfigFile, error) { + t.Helper() dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) + writeTestFile(t, yamlPath, contents) + cfg, err := GetWorkSpaceFromFilePath(dir) - if err := ConvertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err != nil { - t.Fatalf("ConvertLegacyWorkspaceConfigToYaml: %v", err) + raw, readErr := os.ReadFile(yamlPath) + if readErr != nil { + t.Fatalf("read %s: %v", yamlPath, readErr) } - - cfg, err := readWorkspaceConfigYaml(yamlPath) - if err != nil { - t.Fatalf("readWorkspaceConfigYaml: %v", err) + if string(raw) != contents { + t.Errorf("%s should not be modified when read, got:\n%s", yamlPath, raw) } - assertFullWorkspaceConfig(t, cfg, "proj-123") + return cfg, err + } - info, err := os.Stat(yamlPath) + t.Run("reads the environment section", func(t *testing.T) { + cfg, err := readYaml(t, workspaceYAMLWithEnvironment) if err != nil { - t.Fatalf("stat %s: %v", yamlPath, err) - } - if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 { - t.Errorf("yaml perm = %o, want 600", info.Mode().Perm()) + t.Fatalf("unexpected error: %v", err) } - if !fileExists(jsonPath) { - t.Error("the legacy json should be left for the caller to remove") + assertFullWorkspaceConfig(t, cfg, "proj-123") + if got := cfg.GitBranchToEnvironmentMapping["staging"]; got != "staging" { + t.Errorf("GitBranchToEnvironmentMapping[staging] = %q, want staging", got) } }) - t.Run("refuses to overwrite an existing yaml", func(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - const existingYAML = "secrets-management:\n project-id: proj-existing\n" - writeTestFile(t, yamlPath, existingYAML) - - if err := ConvertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { - t.Fatal("expected an error when the yaml already exists") - } - raw, err := os.ReadFile(yamlPath) + t.Run("reads the deprecated default-environment and mappings", func(t *testing.T) { + cfg, err := readYaml(t, workspaceYAML) if err != nil { - t.Fatalf("read %s: %v", yamlPath, err) - } - if string(raw) != existingYAML { - t.Errorf("existing yaml was modified:\n%s", raw) - } - }) - - t.Run("invalid json leaves no yaml behind", func(t *testing.T) { - dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, "{not json") - - if err := ConvertLegacyWorkspaceConfigToYaml(jsonPath, yamlPath); err == nil { - t.Fatal("expected an error for invalid json") - } - if fileExists(yamlPath) { - t.Error("no yaml should be written when the json cannot be read") + t.Fatalf("unexpected error: %v", err) } + assertFullWorkspaceConfig(t, cfg, "proj-123") }) -} -func TestResolveWorkspaceConfigFile(t *testing.T) { - t.Run("empty dir walks up from cwd", func(t *testing.T) { - root := t.TempDir() - jsonPath := filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - nested := filepath.Join(root, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - - got, err := ResolveWorkspaceConfigFile("") + t.Run("the environment section wins over the deprecated fields", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: old-default + mappings: + git-branch-to-environment: + main: old-main + feature: old-feature + environment: + default: dev + overrides: + git-branch: + - branch: main + environment: prod +`) if err != nil { t.Fatalf("unexpected error: %v", err) } - if got != jsonPath { - t.Errorf("got %q, want %q", got, jsonPath) + if cfg.DefaultEnvironment != "dev" { + t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) + } + // the deprecated mappings are ignored, not merged with the overrides + want := map[string]string{"main": "prod"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) - t.Run("explicit dir does not walk up", func(t *testing.T) { - root := t.TempDir() - writeTestFile(t, filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - empty := filepath.Join(root, "empty") - if err := os.MkdirAll(empty, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) + t.Run("an empty environment section falls back to the deprecated fields", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: dev + environment: {} +`) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - t.Chdir(root) - - if _, err := ResolveWorkspaceConfigFile("empty"); err == nil { - t.Error("expected an error when the explicit dir has no config") + if cfg.DefaultEnvironment != "dev" { + t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) } }) - t.Run("explicit dir prefers yaml and returns an absolute path", func(t *testing.T) { - root := t.TempDir() - cfgDir := filepath.Join(root, "cfg") - if err := os.MkdirAll(cfgDir, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - writeTestFile(t, filepath.Join(cfgDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - yamlPath := filepath.Join(cfgDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, yamlPath, workspaceYAML) - t.Chdir(root) - - got, err := ResolveWorkspaceConfigFile("cfg") + t.Run("first override for a branch wins and entries without a branch are skipped", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + environment: + overrides: + git-branch: + - environment: orphan + - branch: main + environment: prod + - branch: main + environment: staging +`) if err != nil { t.Fatalf("unexpected error: %v", err) } - if !filepath.IsAbs(got) { - t.Errorf("expected an absolute path, got %q", got) - } - if filepath.Base(got) != INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("expected the yaml to win, got %q", got) + want := map[string]string{"main": "prod"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) } + +func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { + root := t.TempDir() + jsonPath := filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + writeTestFile(t, jsonPath, legacyWorkspaceJSON) + nested := filepath.Join(root, "a", "b") + if err := os.MkdirAll(nested, 0o700); err != nil { + t.Fatalf("mkdir: %v", err) + } + t.Chdir(nested) + + cfg, err := GetWorkSpaceFromFile() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertFullWorkspaceConfig(t, cfg, "proj-123") + if !fileExists(jsonPath) { + t.Errorf("legacy %s should not be removed", jsonPath) + } + if fileExists(filepath.Join(root, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + t.Error("yaml should not be created when reading the legacy json") + } + + found, err := FindWorkspaceConfigFile() + if err != nil { + t.Fatalf("FindWorkspaceConfigFile: %v", err) + } + if filepath.Base(found) != INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + t.Errorf("FindWorkspaceConfigFile = %q, want the legacy json file", found) + } +} diff --git a/packages/util/constants.go b/packages/util/constants.go index 70846f66..3baf4b57 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -91,8 +91,7 @@ const ( KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" - UPDATE_CHECK_CACHE_FILE_NAME = "update-check.json" - MIGRATION_NOTICE_CACHE_FILE_NAME = "migration-notices.json" + UPDATE_CHECK_CACHE_FILE_NAME = "update-check.json" ) var ( diff --git a/packages/util/helper.go b/packages/util/helper.go index b4c61e48..4d7bb2ac 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -557,7 +557,7 @@ func GetCmdFlagOrEnvWithDefaultValue(cmd *cobra.Command, flag string, envNames [ // ResolveEnvironmentName resolves the environment slug for a command, in order: // the --env flag (if explicitly set) > INFISICAL_ENVIRONMENT > .infisical.yaml -// (git-branch mapping, then default-environment) > the flag's own default value. +// (git-branch override or mapping, then the default environment) > the flag's own default value. // It keys off cmd.Flags().Changed rather than an empty-value check so env and workspace // are still consulted when --env is left at its default. func ResolveEnvironmentName(cmd *cobra.Command) string { diff --git a/packages/util/migration_notice.go b/packages/util/migration_notice.go deleted file mode 100644 index 70e2f6d2..00000000 --- a/packages/util/migration_notice.go +++ /dev/null @@ -1,114 +0,0 @@ -package util - -import ( - "encoding/json" - "fmt" - "io" - "maps" - "os" - "path/filepath" - "time" - - "github.com/fatih/color" -) - -const migrationNoticeCacheTTL = 24 * time.Hour - -type MigrationNotice struct { - ID string - Title string - Message string - DocsURL string -} - -type migrationNoticeCache struct { - LastShownTimes map[string]time.Time `json:"lastShownTimes"` -} - -// migrationNoticeCacheSingleton is the one cache map for this process. -var migrationNoticeCacheSingleton = migrationNoticeCache{LastShownTimes: map[string]time.Time{}} - -var migrationNoticeCachePath string - -func getMigrationNoticeCachePath() (string, error) { - homeDir, err := GetHomeDir() - if err != nil { - return "", err - } - return filepath.Join(homeDir, CONFIG_FOLDER_NAME, MIGRATION_NOTICE_CACHE_FILE_NAME), nil -} - -// readMigrationNoticeCache returns the process-wide cache. The LastShownTimes map is -// allocated once; a later read for the same path returns that map. A different cache -// path clears it and loads that file. A missing or unreadable file leaves the map empty. -func readMigrationNoticeCache() migrationNoticeCache { - path, err := getMigrationNoticeCachePath() - if err == nil && path == migrationNoticeCachePath { - return migrationNoticeCacheSingleton - } - - clear(migrationNoticeCacheSingleton.LastShownTimes) - migrationNoticeCachePath = path - if err != nil { - return migrationNoticeCacheSingleton - } - - data, readErr := os.ReadFile(path) - if readErr != nil { - return migrationNoticeCacheSingleton - } - - var decoded migrationNoticeCache - if json.Unmarshal(data, &decoded) != nil || decoded.LastShownTimes == nil { - return migrationNoticeCacheSingleton - } - maps.Copy(migrationNoticeCacheSingleton.LastShownTimes, decoded.LastShownTimes) - return migrationNoticeCacheSingleton -} - -// migrationNoticeRecentlyShown returns true if the notice with this ID was shown within the TTL. -func migrationNoticeRecentlyShown(id string) bool { - lastShownTime, ok := readMigrationNoticeCache().LastShownTimes[id] - return ok && time.Since(lastShownTime) < migrationNoticeCacheTTL -} - -// recordMigrationNoticeShown stamps the cache so the notice with this ID is throttled. -func recordMigrationNoticeShown(id string) { - path, err := getMigrationNoticeCachePath() - if err != nil { - return - } - if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return - } - cache := readMigrationNoticeCache() - cache.LastShownTimes[id] = time.Now() - data, err := json.Marshal(cache) - if err != nil { - return - } - // Best-effort write; a failure just means the notice may show again next run. - _ = WriteFileAtomic(path, data, 0o600) -} - -func DisplayMigrationNoticeWithWriter(notice MigrationNotice, w io.Writer) { - if os.Getenv("INFISICAL_DISABLE_MIGRATION_NOTICES") != "" { - return - } - if migrationNoticeRecentlyShown(notice.ID) { - return - } - - yellow := color.New(color.FgYellow).SprintFunc() - bold := color.New(color.FgYellow, color.Bold).SprintFunc() - - body := notice.Message - if notice.DocsURL != "" { - body += "\nMigration steps: " + notice.DocsURL - } - - fmt.Fprintln(w, bold(notice.Title)) - fmt.Fprintln(w, yellow(body+"\n")) - - recordMigrationNoticeShown(notice.ID) -} diff --git a/packages/util/migration_notice_test.go b/packages/util/migration_notice_test.go deleted file mode 100644 index fb0036b9..00000000 --- a/packages/util/migration_notice_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package util - -import ( - "bytes" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -// setupMigrationNoticeTest points the home dir at a temp dir so the notice cache is isolated. -func setupMigrationNoticeTest(t *testing.T) { - t.Helper() - isolateHomeDir(t) - t.Setenv("INFISICAL_DISABLE_MIGRATION_NOTICES", "") -} - -func TestDisplayMigrationNotice(t *testing.T) { - t.Run("prints the title, message and docs link", func(t *testing.T) { - setupMigrationNoticeTest(t) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ - ID: "test-notice", - Title: "Important: X is deprecated.", - Message: "What's happening: X goes away.\nWhat to do: use Y.", - DocsURL: "https://infisical.com/docs/x-to-y", - }, &buf) - - want := "Important: X is deprecated.\n" + - "What's happening: X goes away.\nWhat to do: use Y.\n" + - "Migration steps: https://infisical.com/docs/x-to-y\n\n" - if buf.String() != want { - t.Errorf("notice = %q, want %q", buf.String(), want) - } - if !fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected the notice cache file to be written") - } - }) - - t.Run("omits the docs line without a docs link", func(t *testing.T) { - setupMigrationNoticeTest(t) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "What to do: use Y."}, &buf) - - if strings.Contains(buf.String(), "Migration steps:") { - t.Errorf("expected no docs line, got:\n%s", buf.String()) - } - }) - - t.Run("shown once per day per notice ID", func(t *testing.T) { - setupMigrationNoticeTest(t) - notice := MigrationNotice{ID: "first", Title: "first notice", Message: "first"} - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(notice, &buf) - if buf.Len() == 0 { - t.Fatal("expected the notice on the first call") - } - - buf.Reset() - DisplayMigrationNoticeWithWriter(notice, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice within the TTL, got:\n%s", buf.String()) - } - - buf.Reset() - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "second", Title: "second notice", Message: "second"}, &buf) - if !strings.Contains(buf.String(), "second notice") { - t.Errorf("a different notice ID should not be throttled, got:\n%s", buf.String()) - } - - cache := readMigrationNoticeCache() - if _, ok := cache.LastShownTimes["first"]; !ok { - t.Error("recording the second notice should keep the first one's entry") - } - }) - - t.Run("keeps both notices in the same cache map", func(t *testing.T) { - setupMigrationNoticeTest(t) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "first", Title: "first notice", Message: "first"}, &buf) - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "second", Title: "second notice", Message: "second"}, &buf) - - cache := readMigrationNoticeCache() - if _, ok := cache.LastShownTimes["first"]; !ok { - t.Error("expected the first notice in the cache map") - } - if _, ok := cache.LastShownTimes["second"]; !ok { - t.Error("expected the second notice in the cache map") - } - if len(cache.LastShownTimes) != 2 { - t.Errorf("cache map length = %d, want 2", len(cache.LastShownTimes)) - } - }) - - t.Run("shown again after the TTL", func(t *testing.T) { - setupMigrationNoticeTest(t) - - cachePath := migrationNoticeCachePathForTest(t) - if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - data, err := json.Marshal(migrationNoticeCache{LastShownTimes: map[string]time.Time{"test-notice": time.Now().Add(-25 * time.Hour)}}) - if err != nil { - t.Fatalf("marshal: %v", err) - } - writeTestFile(t, cachePath, string(data)) - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "use Y"}, &buf) - if !strings.Contains(buf.String(), "Important:") { - t.Errorf("expected the notice after the TTL expired, got:\n%s", buf.String()) - } - }) - - t.Run("a corrupt cache doesn't block the notice", func(t *testing.T) { - setupMigrationNoticeTest(t) - - cachePath := migrationNoticeCachePathForTest(t) - if err := os.MkdirAll(filepath.Dir(cachePath), 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - writeTestFile(t, cachePath, "{not json") - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "use Y"}, &buf) - if buf.Len() == 0 { - t.Error("expected the notice when the cache can't be parsed") - } - if !migrationNoticeRecentlyShown("test-notice") { - t.Error("expected the cache to be rewritten after showing the notice") - } - }) - - t.Run("disabled by INFISICAL_DISABLE_MIGRATION_NOTICES", func(t *testing.T) { - setupMigrationNoticeTest(t) - t.Setenv("INFISICAL_DISABLE_MIGRATION_NOTICES", "1") - - var buf bytes.Buffer - DisplayMigrationNoticeWithWriter(MigrationNotice{ID: "test-notice", Title: "Important: X is deprecated.", Message: "use Y"}, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice, got:\n%s", buf.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - }) -} diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index f81e3243..01fa40be 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -142,6 +142,46 @@ func TestResolveEnvironmentName(t *testing.T) { } }) + t.Run("yaml environment.default used when flag and env unset", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n environment:\n default: fromyaml\n") + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("yaml git-branch override wins over environment.default", func(t *testing.T) { + mockCurrentBranch(t, "main") + writeWorkspaceYaml(t, `secrets-management: + environment: + default: fromyaml + overrides: + git-branch: + - branch: main + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("yaml environment.default used when the branch has no override", func(t *testing.T) { + mockCurrentBranch(t, "feature") + writeWorkspaceYaml(t, `secrets-management: + environment: + default: fromyaml + overrides: + git-branch: + - branch: main + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + t.Run("legacy json is read without being migrated", func(t *testing.T) { writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") diff --git a/packages/util/workspace_config_migration_notice.go b/packages/util/workspace_config_migration_notice.go deleted file mode 100644 index 15c0a1d8..00000000 --- a/packages/util/workspace_config_migration_notice.go +++ /dev/null @@ -1,46 +0,0 @@ -package util - -import ( - "fmt" - "io" - "os" - "path/filepath" -) - -const workspaceConfigMigrationNoticeID = "workspace-config-yaml" - -const workspaceConfigMigrationGuideURL = "https://infisical.com/docs/cli/project-config-migration" - -// DisplayWorkspaceConfigMigrationNoticeWithWriter prints a once-per-day notice when the -// workspace config in use is a legacy .infisical.json rather than a .infisical.yaml. -// Stays quiet in --silent mode, and can be disabled with INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE. -// -// projectConfigDir is the value of --project-config-dir when the command defines it; when set, -// only that directory is considered so the notice names the same file the command reads. -func DisplayWorkspaceConfigMigrationNoticeWithWriter(isSilent bool, projectConfigDir string, w io.Writer) { - if isSilent { - return - } - if os.Getenv("INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE") != "" { - return - } - - // if the workspace config is not a legacy .infisical.json, don't print the notice - legacyConfigFilePath, err := ResolveWorkspaceConfigFile(projectConfigDir) - if err != nil || filepath.Base(legacyConfigFilePath) != INFISICAL_WORKSPACE_CONFIG_FILE_NAME { - return - } - - DisplayMigrationNoticeWithWriter(MigrationNotice{ - ID: workspaceConfigMigrationNoticeID, - Title: fmt.Sprintf("Important: %s is deprecated. Move this project to %s.", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), - Message: fmt.Sprintf( - "What's happening: the CLI is reading a legacy %s, and support for it will be removed\n"+ - "in a future release. File in use: %s\n"+ - "What to do: run `infisical migrate-config` in %s to convert it to %s.", - INFISICAL_WORKSPACE_CONFIG_FILE_NAME, legacyConfigFilePath, - filepath.Dir(legacyConfigFilePath), INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, - ), - DocsURL: workspaceConfigMigrationGuideURL, - }, w) -} diff --git a/packages/util/workspace_config_migration_notice_test.go b/packages/util/workspace_config_migration_notice_test.go deleted file mode 100644 index fb184e93..00000000 --- a/packages/util/workspace_config_migration_notice_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package util - -import ( - "bytes" - "os" - "path/filepath" - "strings" - "testing" -) - -// isolateHomeDir points the home dir at a temp dir. os.UserHomeDir reads HOME on Unix -// and USERPROFILE on Windows, so both are set. -func isolateHomeDir(t *testing.T) { - t.Helper() - home := t.TempDir() - t.Setenv("HOME", home) - t.Setenv("USERPROFILE", home) -} - -// setupWorkspaceConfigMigrationNoticeTest points the home dir at a temp dir so the notice cache is isolated, -// chdirs into a fresh project dir, and returns the project dir. -func setupWorkspaceConfigMigrationNoticeTest(t *testing.T) string { - t.Helper() - isolateHomeDir(t) - t.Setenv("INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE", "") - t.Setenv("INFISICAL_DISABLE_MIGRATION_NOTICES", "") - projectDir := t.TempDir() - t.Chdir(projectDir) - return projectDir -} - -func migrationNoticeCachePathForTest(t *testing.T) string { - t.Helper() - path, err := getMigrationNoticeCachePath() - if err != nil { - t.Fatalf("cache path: %v", err) - } - return path -} - -func TestWorkspaceConfigMigrationNoticeShownForLegacyJSON(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - jsonPath := filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, jsonPath, legacyWorkspaceJSON) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - - out := buf.String() - for _, want := range []string{ - "Important: .infisical.json is deprecated", - jsonPath, - "infisical migrate-config", - workspaceConfigMigrationGuideURL, - } { - if !strings.Contains(out, want) { - t.Errorf("notice missing %q, got:\n%s", want, out) - } - } - if !fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected the notice cache file to be written") - } - - buf.Reset() - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice within the TTL, got:\n%s", buf.String()) - } -} - -func TestWorkspaceConfigMigrationNoticeShownForLegacyJSONInParentDir(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - nested := filepath.Join(projectDir, "a", "b") - if err := os.MkdirAll(nested, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - t.Chdir(nested) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, "", &buf) - if !strings.Contains(buf.String(), "Important:") { - t.Errorf("expected the notice for a legacy json in a parent dir, got:\n%s", buf.String()) - } -} - -func TestWorkspaceConfigMigrationNoticeNotShown(t *testing.T) { - cases := []struct { - name string - setup func(t *testing.T, projectDir string) - isSilent bool - }{ - { - name: "yaml only", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - }, - }, - { - name: "yaml takes precedence over json", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - }, - }, - { - name: "no workspace config", - setup: func(t *testing.T, projectDir string) {}, - }, - { - name: "silent", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - }, - isSilent: true, - }, - { - name: "disabled by env var", - setup: func(t *testing.T, projectDir string) { - writeTestFile(t, filepath.Join(projectDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - t.Setenv("INFISICAL_DISABLE_WORKSPACE_CONFIG_MIGRATION_NOTICE", "1") - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - projectDir := setupWorkspaceConfigMigrationNoticeTest(t) - tc.setup(t, projectDir) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(tc.isSilent, "", &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice, got:\n%s", buf.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - }) - } -} - -// Reproduces: cwd apps/web, --project-config-dir ../../services/api, legacy json at the repo root. -// The notice must describe the file the run reads (services/api), not the one found walking up from cwd. -func TestWorkspaceConfigMigrationNoticeHonorsProjectConfigDir(t *testing.T) { - layout := func(t *testing.T) (repoDir string, apiDir string) { - t.Helper() - repoDir = setupWorkspaceConfigMigrationNoticeTest(t) - apiDir = filepath.Join(repoDir, "services", "api") - webDir := filepath.Join(repoDir, "apps", "web") - for _, dir := range []string{apiDir, webDir} { - if err := os.MkdirAll(dir, 0o700); err != nil { - t.Fatalf("mkdir: %v", err) - } - } - t.Chdir(webDir) - return repoDir, apiDir - } - projectConfigDir := filepath.Join("..", "..", "services", "api") - - t.Run("yaml in project config dir, legacy json above cwd", func(t *testing.T) { - repoDir, apiDir := layout(t) - writeTestFile(t, filepath.Join(repoDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - writeTestFile(t, filepath.Join(apiDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice when the run uses a yaml, got:\n%s", buf.String()) - } - if fileExists(migrationNoticeCachePathForTest(t)) { - t.Error("expected no notice cache file to be written") - } - }) - - t.Run("legacy json in project config dir, yaml above cwd", func(t *testing.T) { - repoDir, apiDir := layout(t) - writeTestFile(t, filepath.Join(repoDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) - apiJSONPath := filepath.Join(apiDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) - writeTestFile(t, apiJSONPath, legacyWorkspaceJSON) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) - out := buf.String() - for _, want := range []string{ - "Important: .infisical.json is deprecated", - "File in use: " + apiJSONPath, - "run `infisical migrate-config` in " + apiDir, - } { - if !strings.Contains(out, want) { - t.Errorf("notice missing %q, got:\n%s", want, out) - } - } - if strings.Contains(out, filepath.Join(repoDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { - t.Errorf("notice must not name the repo root json, got:\n%s", out) - } - }) - - t.Run("empty project config dir, legacy json above cwd", func(t *testing.T) { - repoDir, _ := layout(t) - writeTestFile(t, filepath.Join(repoDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), legacyWorkspaceJSON) - - var buf bytes.Buffer - DisplayWorkspaceConfigMigrationNoticeWithWriter(false, projectConfigDir, &buf) - if buf.Len() != 0 { - t.Errorf("expected no notice when the project config dir has no config, got:\n%s", buf.String()) - } - }) -} From 4e97abd727b165127b92d0160266489aa58c4447 Mon Sep 17 00:00:00 2001 From: adilson Date: Mon, 19 Oct 2026 20:14:16 -0300 Subject: [PATCH 3/7] add yaml version again --- packages/cmd/init.go | 1 + packages/cmd/init_test.go | 10 +++------- packages/models/cli.go | 1 + packages/util/constants.go | 1 + 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/cmd/init.go b/packages/cmd/init.go index 29cc53d7..15d1fa7f 100644 --- a/packages/cmd/init.go +++ b/packages/cmd/init.go @@ -337,6 +337,7 @@ func pickOrganization(httpClient *resty.Client, label string, username string) ( func writeWorkspaceFile(selectedWorkspace models.Workspace) error { workspaceFileToSave := models.WorkspaceConfigFileYaml{ + Version: util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION, SecretsManagement: models.SecretsManagementConfigSection{ ProjectID: selectedWorkspace.ID, }, diff --git a/packages/cmd/init_test.go b/packages/cmd/init_test.go index 38e11a90..a5a95682 100644 --- a/packages/cmd/init_test.go +++ b/packages/cmd/init_test.go @@ -4,7 +4,6 @@ import ( "os" "path/filepath" "runtime" - "strings" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -37,15 +36,12 @@ func TestWriteWorkspaceFile(t *testing.T) { if err := yaml.Unmarshal(raw, &written); err != nil { t.Fatalf("%s is not valid yaml: %v\n%s", yamlPath, err, raw) } + if written.Version != util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION { + t.Errorf("version = %d, want %d", written.Version, util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION) + } if written.SecretsManagement.ProjectID != "proj-new" { t.Errorf("secrets-management.project-id = %q, want proj-new", written.SecretsManagement.ProjectID) } - // a fresh config carries no environment settings, so neither the deprecated keys nor the environment section are written - for _, key := range []string{"default-environment", "mappings", "environment:"} { - if strings.Contains(string(raw), key) { - t.Errorf("a fresh config should not contain %q, got:\n%s", key, raw) - } - } cfg, err := util.GetWorkSpaceFromFile() if err != nil { diff --git a/packages/models/cli.go b/packages/models/cli.go index b6c3c2f5..8edceba6 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -214,6 +214,7 @@ type SecretsManagementConfigSection struct { } type WorkspaceConfigFileYaml struct { + Version int `yaml:"version"` General GeneralConfigSection `yaml:"general"` SecretsManagement SecretsManagementConfigSection `yaml:"secrets-management"` } diff --git a/packages/util/constants.go b/packages/util/constants.go index 3baf4b57..39fdfa19 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -13,6 +13,7 @@ const ( INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME = ".infisical.yaml" + INFISICAL_WORKSPACE_CONFIG_YAML_VERSION = 1 INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_PROJECT_ID_NAME = "INFISICAL_PROJECT_ID" INFISICAL_ENVIRONMENT_NAME = "INFISICAL_ENVIRONMENT" From 2cd32a9f2ce88ec20cc06ef7010c3f8c39191744 Mon Sep 17 00:00:00 2001 From: adilson Date: Mon, 19 Oct 2026 20:30:17 -0300 Subject: [PATCH 4/7] fix: match the whole branch name on the new structure --- packages/models/cli.go | 3 ++ packages/util/config.go | 3 +- packages/util/config_test.go | 26 +++++++++++----- packages/util/helper.go | 3 +- packages/util/resolve_test.go | 57 +++++++++++++++++++++++++++++++++++ packages/util/secrets.go | 6 +++- 6 files changed, 87 insertions(+), 11 deletions(-) diff --git a/packages/models/cli.go b/packages/models/cli.go index 8edceba6..f4da54d1 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -180,6 +180,9 @@ type WorkspaceConfigFile struct { GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"` DefaultSecretPath string `json:"defaultSecretPath,omitempty"` Domain string `json:"domain,omitempty"` + // GitBranchOverrides comes from the .infisical.yaml environment.overrides.git-branch list and is matched + // against the full branch name, while GitBranchToEnvironmentMapping only matches its last segment. + GitBranchOverrides map[string]string `json:"-"` } type GeneralConfigSection struct { diff --git a/packages/util/config.go b/packages/util/config.go index 75d0a691..86845c93 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -122,7 +122,8 @@ func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) environment := secretsManagement.Environment if environment != nil && (environment.Default != "" || len(environment.Overrides.GitBranch) > 0) { workspaceConfigFile.DefaultEnvironment = environment.Default - workspaceConfigFile.GitBranchToEnvironmentMapping = gitBranchMapFromOverrides(environment.Overrides.GitBranch) + workspaceConfigFile.GitBranchToEnvironmentMapping = nil + workspaceConfigFile.GitBranchOverrides = gitBranchMapFromOverrides(environment.Overrides.GitBranch) } return workspaceConfigFile diff --git a/packages/util/config_test.go b/packages/util/config_test.go index 645f4e04..5c91426b 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -279,9 +279,15 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - assertFullWorkspaceConfig(t, cfg, "proj-123") - if got := cfg.GitBranchToEnvironmentMapping["staging"]; got != "staging" { - t.Errorf("GitBranchToEnvironmentMapping[staging] = %q, want staging", got) + if cfg.WorkspaceId != "proj-123" || cfg.DefaultEnvironment != "dev" || cfg.DefaultSecretPath != "/backend" || cfg.Domain != "https://eu.infisical.com" { + t.Errorf("unexpected config %+v", cfg) + } + want := map[string]string{"main": "prod", "staging": "staging"} + if !maps.Equal(cfg.GitBranchOverrides, want) { + t.Errorf("GitBranchOverrides = %v, want %v", cfg.GitBranchOverrides, want) + } + if cfg.GitBranchToEnvironmentMapping != nil { + t.Errorf("GitBranchToEnvironmentMapping = %v, want nil", cfg.GitBranchToEnvironmentMapping) } }) @@ -291,6 +297,9 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { t.Fatalf("unexpected error: %v", err) } assertFullWorkspaceConfig(t, cfg, "proj-123") + if cfg.GitBranchOverrides != nil { + t.Errorf("GitBranchOverrides = %v, want nil", cfg.GitBranchOverrides) + } }) t.Run("the environment section wins over the deprecated fields", func(t *testing.T) { @@ -316,8 +325,11 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { } // the deprecated mappings are ignored, not merged with the overrides want := map[string]string{"main": "prod"} - if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { - t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) + if !maps.Equal(cfg.GitBranchOverrides, want) { + t.Errorf("GitBranchOverrides = %v, want %v", cfg.GitBranchOverrides, want) + } + if cfg.GitBranchToEnvironmentMapping != nil { + t.Errorf("GitBranchToEnvironmentMapping = %v, want nil", cfg.GitBranchToEnvironmentMapping) } }) @@ -351,8 +363,8 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { t.Fatalf("unexpected error: %v", err) } want := map[string]string{"main": "prod"} - if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { - t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) + if !maps.Equal(cfg.GitBranchOverrides, want) { + t.Errorf("GitBranchOverrides = %v, want %v", cfg.GitBranchOverrides, want) } }) } diff --git a/packages/util/helper.go b/packages/util/helper.go index 4d7bb2ac..c809d2fa 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -10,7 +10,6 @@ import ( "net" "os" "os/exec" - "path" "runtime" "sort" "strconv" @@ -454,7 +453,7 @@ func getCurrentBranch() (string, error) { if err != nil { return "", err } - return path.Base(strings.TrimSpace(out.String())), nil + return strings.TrimSpace(out.String()), nil } // DomainEnvNames lists the env vars that configure the Infisical domain, in diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index 01fa40be..70ed219f 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -182,6 +182,63 @@ func TestResolveEnvironmentName(t *testing.T) { } }) + t.Run("yaml git-branch override matches the full branch name", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspaceYaml(t, `secrets-management: + environment: + default: fromyaml + overrides: + git-branch: + - branch: login + environment: fromlastsegment + - branch: feature/login + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("yaml git-branch override does not match only the last segment of the branch", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspaceYaml(t, `secrets-management: + environment: + default: fromyaml + overrides: + git-branch: + - branch: login + environment: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("deprecated yaml mapping still matches the last segment of the branch", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspaceYaml(t, `secrets-management: + default-environment: fromyaml + mappings: + git-branch-to-environment: + login: frombranch +`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + + t.Run("legacy json mapping still matches the last segment of the branch", func(t *testing.T) { + mockCurrentBranch(t, "feature/login") + writeWorkspace(t, `{"defaultEnvironment":"fromfile","gitBranchToEnvironmentMapping":{"login":"frombranch"}}`) + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { + t.Fatalf("got %q, want frombranch", got) + } + }) + t.Run("legacy json is read without being migrated", func(t *testing.T) { writeWorkspace(t, `{"defaultEnvironment":"fromfile"}`) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") diff --git a/packages/util/secrets.go b/packages/util/secrets.go index d094b2d3..17adf3a0 100644 --- a/packages/util/secrets.go +++ b/packages/util/secrets.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "os" + "path" "strings" "unicode" @@ -543,7 +544,10 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st log.Debug().Msgf("getEnvelopmentBasedOnGitBranch: [err=%s]", err) } - envBasedOnGitBranch, ok := workspaceFile.GitBranchToEnvironmentMapping[branch] + envBasedOnGitBranch, ok := workspaceFile.GitBranchOverrides[branch] + if workspaceFile.GitBranchOverrides == nil { + envBasedOnGitBranch, ok = workspaceFile.GitBranchToEnvironmentMapping[path.Base(branch)] + } log.Debug().Msgf("GetEnvelopmentBasedOnGitBranch: [envBasedOnGitBranch=%s] [ok=%t]", envBasedOnGitBranch, ok) From c997802d505822f11fe685d711a4c7b9fe081061 Mon Sep 17 00:00:00 2001 From: adilson Date: Mon, 19 Oct 2026 21:03:56 -0300 Subject: [PATCH 5/7] address ai comments --- packages/cmd/init.go | 10 +++-- packages/cmd/init_test.go | 31 ++++++++++++++- packages/models/cli.go | 3 -- packages/util/config.go | 16 ++++++-- packages/util/config_test.go | 71 ++++++++++++++++++++++++++--------- packages/util/helper.go | 3 +- packages/util/resolve_test.go | 12 +++--- packages/util/secrets.go | 6 +-- 8 files changed, 108 insertions(+), 44 deletions(-) diff --git a/packages/cmd/init.go b/packages/cmd/init.go index 15d1fa7f..33409c1b 100644 --- a/packages/cmd/init.go +++ b/packages/cmd/init.go @@ -36,6 +36,8 @@ infisical init --project-id `, }, Run: func(cmd *cobra.Command, args []string) { projectID, _ := cmd.Flags().GetString("project-id") + silent, _ := cmd.Flags().GetBool("silent") + showMessages := !silent && !isStructuredOutputRequested(cmd) if util.WorkspaceConfigFileExistsInCurrentPath() { if !isatty.IsTerminal(os.Stdin.Fd()) { @@ -77,7 +79,7 @@ infisical init --project-id `, if userCreds.OrganizationID != "" { rejectTransientOrgOverride(userCreds) } - if err := writeWorkspaceFile(models.Workspace{ID: projectID}); err != nil { + if err := writeWorkspaceFile(models.Workspace{ID: projectID}, showMessages); err != nil { util.HandleError(err) } hintDirectoryProfileBinding(userCreds) @@ -167,7 +169,7 @@ infisical init --project-id `, util.HandleError(err) } - err = writeWorkspaceFile(filteredWorkspaces[index]) + err = writeWorkspaceFile(filteredWorkspaces[index], showMessages) if err != nil { util.HandleError(err) } @@ -335,7 +337,7 @@ func pickOrganization(httpClient *resty.Client, label string, username string) ( return subItems[subIndex].ID, &selectedSubOrgName, nil } -func writeWorkspaceFile(selectedWorkspace models.Workspace) error { +func writeWorkspaceFile(selectedWorkspace models.Workspace, showMessages bool) error { workspaceFileToSave := models.WorkspaceConfigFileYaml{ Version: util.INFISICAL_WORKSPACE_CONFIG_YAML_VERSION, SecretsManagement: models.SecretsManagementConfigSection{ @@ -354,7 +356,7 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace) error { } // the legacy file is left in place; .infisical.yaml takes precedence over it from now on - if _, err := os.Stat(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil { + if _, err := os.Stat(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil && showMessages { util.PrintWarning(fmt.Sprintf("The legacy %s in this directory is no longer used because %s takes precedence. You can delete it.", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) } diff --git a/packages/cmd/init_test.go b/packages/cmd/init_test.go index a5a95682..3d17a503 100644 --- a/packages/cmd/init_test.go +++ b/packages/cmd/init_test.go @@ -1,9 +1,12 @@ package cmd import ( + "bytes" + "io" "os" "path/filepath" "runtime" + "strings" "testing" "github.com/Infisical/infisical-merge/packages/models" @@ -23,7 +26,7 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Run("fresh directory gets only a yaml config", func(t *testing.T) { dir := isolateWorkspaceDir(t) - if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}); err != nil { + if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}, true); err != nil { t.Fatalf("writeWorkspaceFile: %v", err) } @@ -76,7 +79,7 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Fatalf("write json: %v", err) } - if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}); err != nil { + if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}, true); err != nil { t.Fatalf("writeWorkspaceFile: %v", err) } @@ -97,4 +100,28 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Errorf("WorkspaceId = %q, want proj-new", cfg.WorkspaceId) } }) + + t.Run("legacy json warning is shown only when messages are enabled", func(t *testing.T) { + var stderr bytes.Buffer + originalStderrWriter := util.GetStderrWriter + util.GetStderrWriter = func() io.Writer { return &stderr } + t.Cleanup(func() { util.GetStderrWriter = originalStderrWriter }) + + for _, showMessages := range []bool{false, true} { + stderr.Reset() + dir := isolateWorkspaceDir(t) + if err := os.WriteFile(filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME), []byte(`{"workspaceId":"proj-old"}`), 0o600); err != nil { + t.Fatalf("write json: %v", err) + } + + if err := writeWorkspaceFile(models.Workspace{ID: "proj-new"}, showMessages); err != nil { + t.Fatalf("writeWorkspaceFile: %v", err) + } + + warned := strings.Contains(stderr.String(), util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + if warned != showMessages { + t.Errorf("showMessages=%t: warned = %t, stderr:\n%s", showMessages, warned, stderr.String()) + } + } + }) } diff --git a/packages/models/cli.go b/packages/models/cli.go index f4da54d1..8edceba6 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -180,9 +180,6 @@ type WorkspaceConfigFile struct { GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"` DefaultSecretPath string `json:"defaultSecretPath,omitempty"` Domain string `json:"domain,omitempty"` - // GitBranchOverrides comes from the .infisical.yaml environment.overrides.git-branch list and is matched - // against the full branch name, while GitBranchToEnvironmentMapping only matches its last segment. - GitBranchOverrides map[string]string `json:"-"` } type GeneralConfigSection struct { diff --git a/packages/util/config.go b/packages/util/config.go index 86845c93..3550bdc8 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -108,7 +108,9 @@ func readWorkspaceConfigYaml(path string) (models.WorkspaceConfigFile, error) { } // workspaceConfigFromYaml accepts both the environment section and the deprecated default-environment / -// mappings fields. When the environment section has any value it wins, and the deprecated fields are ignored. +// mappings fields. Each field is resolved on its own: environment.default wins over default-environment when +// set, and environment.overrides.git-branch wins over the mappings when present, so a partial migration keeps +// the deprecated value for whichever field has not been moved yet. func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) models.WorkspaceConfigFile { secretsManagement := workspaceConfigYaml.SecretsManagement workspaceConfigFile := models.WorkspaceConfigFile{ @@ -120,10 +122,16 @@ func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) } environment := secretsManagement.Environment - if environment != nil && (environment.Default != "" || len(environment.Overrides.GitBranch) > 0) { + if environment == nil { + return workspaceConfigFile + } + + if environment.Default != "" { workspaceConfigFile.DefaultEnvironment = environment.Default - workspaceConfigFile.GitBranchToEnvironmentMapping = nil - workspaceConfigFile.GitBranchOverrides = gitBranchMapFromOverrides(environment.Overrides.GitBranch) + } + + if len(environment.Overrides.GitBranch) > 0 { + workspaceConfigFile.GitBranchToEnvironmentMapping = gitBranchMapFromOverrides(environment.Overrides.GitBranch) } return workspaceConfigFile diff --git a/packages/util/config_test.go b/packages/util/config_test.go index 5c91426b..93b79d08 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -279,15 +279,9 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if cfg.WorkspaceId != "proj-123" || cfg.DefaultEnvironment != "dev" || cfg.DefaultSecretPath != "/backend" || cfg.Domain != "https://eu.infisical.com" { - t.Errorf("unexpected config %+v", cfg) - } - want := map[string]string{"main": "prod", "staging": "staging"} - if !maps.Equal(cfg.GitBranchOverrides, want) { - t.Errorf("GitBranchOverrides = %v, want %v", cfg.GitBranchOverrides, want) - } - if cfg.GitBranchToEnvironmentMapping != nil { - t.Errorf("GitBranchToEnvironmentMapping = %v, want nil", cfg.GitBranchToEnvironmentMapping) + assertFullWorkspaceConfig(t, cfg, "proj-123") + if got := cfg.GitBranchToEnvironmentMapping["staging"]; got != "staging" { + t.Errorf("GitBranchToEnvironmentMapping[staging] = %q, want staging", got) } }) @@ -297,9 +291,6 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { t.Fatalf("unexpected error: %v", err) } assertFullWorkspaceConfig(t, cfg, "proj-123") - if cfg.GitBranchOverrides != nil { - t.Errorf("GitBranchOverrides = %v, want nil", cfg.GitBranchOverrides) - } }) t.Run("the environment section wins over the deprecated fields", func(t *testing.T) { @@ -325,11 +316,55 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { } // the deprecated mappings are ignored, not merged with the overrides want := map[string]string{"main": "prod"} - if !maps.Equal(cfg.GitBranchOverrides, want) { - t.Errorf("GitBranchOverrides = %v, want %v", cfg.GitBranchOverrides, want) + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) + } + }) + + t.Run("environment.overrides alone keeps the deprecated default-environment", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: prod + mappings: + git-branch-to-environment: + main: old-main + environment: + overrides: + git-branch: + - branch: feature/x + environment: staging +`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.DefaultEnvironment != "prod" { + t.Errorf("DefaultEnvironment = %q, want prod", cfg.DefaultEnvironment) } - if cfg.GitBranchToEnvironmentMapping != nil { - t.Errorf("GitBranchToEnvironmentMapping = %v, want nil", cfg.GitBranchToEnvironmentMapping) + want := map[string]string{"feature/x": "staging"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) + } + }) + + t.Run("environment.default alone keeps the deprecated mappings", func(t *testing.T) { + cfg, err := readYaml(t, `secrets-management: + project-id: proj-123 + default-environment: old-default + mappings: + git-branch-to-environment: + main: prod + environment: + default: dev +`) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.DefaultEnvironment != "dev" { + t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) + } + want := map[string]string{"main": "prod"} + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) @@ -363,8 +398,8 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { t.Fatalf("unexpected error: %v", err) } want := map[string]string{"main": "prod"} - if !maps.Equal(cfg.GitBranchOverrides, want) { - t.Errorf("GitBranchOverrides = %v, want %v", cfg.GitBranchOverrides, want) + if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { + t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) } diff --git a/packages/util/helper.go b/packages/util/helper.go index c809d2fa..4d7bb2ac 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -10,6 +10,7 @@ import ( "net" "os" "os/exec" + "path" "runtime" "sort" "strconv" @@ -453,7 +454,7 @@ func getCurrentBranch() (string, error) { if err != nil { return "", err } - return strings.TrimSpace(out.String()), nil + return path.Base(strings.TrimSpace(out.String())), nil } // DomainEnvNames lists the env vars that configure the Infisical domain, in diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index 70ed219f..2761faed 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -182,7 +182,7 @@ func TestResolveEnvironmentName(t *testing.T) { } }) - t.Run("yaml git-branch override matches the full branch name", func(t *testing.T) { + t.Run("yaml git-branch override matches the last segment of the branch", func(t *testing.T) { mockCurrentBranch(t, "feature/login") writeWorkspaceYaml(t, `secrets-management: environment: @@ -190,8 +190,6 @@ func TestResolveEnvironmentName(t *testing.T) { overrides: git-branch: - branch: login - environment: fromlastsegment - - branch: feature/login environment: frombranch `) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") @@ -200,14 +198,14 @@ func TestResolveEnvironmentName(t *testing.T) { } }) - t.Run("yaml git-branch override does not match only the last segment of the branch", func(t *testing.T) { - mockCurrentBranch(t, "feature/login") + t.Run("yaml deprecated default-environment used when only overrides are set and the branch has no override", func(t *testing.T) { + mockCurrentBranch(t, "main") writeWorkspaceYaml(t, `secrets-management: + default-environment: fromyaml environment: - default: fromyaml overrides: git-branch: - - branch: login + - branch: feature/x environment: frombranch `) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") diff --git a/packages/util/secrets.go b/packages/util/secrets.go index 17adf3a0..d094b2d3 100644 --- a/packages/util/secrets.go +++ b/packages/util/secrets.go @@ -8,7 +8,6 @@ import ( "fmt" "net/http" "os" - "path" "strings" "unicode" @@ -544,10 +543,7 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st log.Debug().Msgf("getEnvelopmentBasedOnGitBranch: [err=%s]", err) } - envBasedOnGitBranch, ok := workspaceFile.GitBranchOverrides[branch] - if workspaceFile.GitBranchOverrides == nil { - envBasedOnGitBranch, ok = workspaceFile.GitBranchToEnvironmentMapping[path.Base(branch)] - } + envBasedOnGitBranch, ok := workspaceFile.GitBranchToEnvironmentMapping[branch] log.Debug().Msgf("GetEnvelopmentBasedOnGitBranch: [envBasedOnGitBranch=%s] [ok=%t]", envBasedOnGitBranch, ok) From 2ad34517f2271bdcdd05bca7d5b9ab894d9184b6 Mon Sep 17 00:00:00 2001 From: varonix <62331820+varonix0@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:57:41 +0400 Subject: [PATCH 6/7] rename --- packages/cmd/init.go | 6 +++--- packages/cmd/init_test.go | 12 ++++++------ packages/util/config.go | 12 ++++++------ packages/util/config_test.go | 22 +++++++++++----------- packages/util/constants.go | 15 +++++++++------ packages/util/resolve_test.go | 12 ++++++------ 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/packages/cmd/init.go b/packages/cmd/init.go index 33409c1b..784eca47 100644 --- a/packages/cmd/init.go +++ b/packages/cmd/init.go @@ -350,14 +350,14 @@ func writeWorkspaceFile(selectedWorkspace models.Workspace, showMessages bool) e return err } - err = util.WriteFileAtomic(util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0o600) + err = util.WriteFileAtomic(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, marshalledWorkspaceFile, 0o600) if err != nil { return err } // the legacy file is left in place; .infisical.yaml takes precedence over it from now on - if _, err := os.Stat(util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil && showMessages { - util.PrintWarning(fmt.Sprintf("The legacy %s in this directory is no longer used because %s takes precedence. You can delete it.", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) + if _, err := os.Stat(util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME); err == nil && showMessages { + util.PrintWarning(fmt.Sprintf("The legacy %s in this directory is no longer used because %s takes precedence. You can delete it.", util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) } return nil diff --git a/packages/cmd/init_test.go b/packages/cmd/init_test.go index 3d17a503..0cf1c0af 100644 --- a/packages/cmd/init_test.go +++ b/packages/cmd/init_test.go @@ -30,7 +30,7 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Fatalf("writeWorkspaceFile: %v", err) } - yamlPath := filepath.Join(dir, util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + yamlPath := filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) raw, err := os.ReadFile(yamlPath) if err != nil { t.Fatalf("read %s: %v", yamlPath, err) @@ -67,14 +67,14 @@ func TestWriteWorkspaceFile(t *testing.T) { if err != nil { t.Fatalf("read dir: %v", err) } - if len(entries) != 1 || entries[0].Name() != util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME { - t.Errorf("expected only %s in dir, got %v", util.INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, entries) + if len(entries) != 1 || entries[0].Name() != util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + t.Errorf("expected only %s in dir, got %v", util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME, entries) } }) t.Run("leaves a legacy json config in place", func(t *testing.T) { dir := isolateWorkspaceDir(t) - jsonPath := filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) if err := os.WriteFile(jsonPath, []byte(`{"workspaceId":"proj-old"}`), 0o600); err != nil { t.Fatalf("write json: %v", err) } @@ -110,7 +110,7 @@ func TestWriteWorkspaceFile(t *testing.T) { for _, showMessages := range []bool{false, true} { stderr.Reset() dir := isolateWorkspaceDir(t) - if err := os.WriteFile(filepath.Join(dir, util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME), []byte(`{"workspaceId":"proj-old"}`), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(dir, util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME), []byte(`{"workspaceId":"proj-old"}`), 0o600); err != nil { t.Fatalf("write json: %v", err) } @@ -118,7 +118,7 @@ func TestWriteWorkspaceFile(t *testing.T) { t.Fatalf("writeWorkspaceFile: %v", err) } - warned := strings.Contains(stderr.String(), util.INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + warned := strings.Contains(stderr.String(), util.LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) if warned != showMessages { t.Errorf("showMessages=%t: warned = %t, stderr:\n%s", showMessages, warned, stderr.String()) } diff --git a/packages/util/config.go b/packages/util/config.go index 3550bdc8..c6b488d6 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -30,7 +30,7 @@ func ConfigFileExists() bool { } func WorkspaceConfigFileExistsInCurrentPath() bool { - for _, fileName := range []string{INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { + for _, fileName := range []string{INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { if _, err := os.Stat(fileName); err == nil { return true } else { @@ -65,8 +65,8 @@ func GetDomainFromFile() (domain string, valid bool) { } func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, error) { - yamlConfigFilePath := filepath.Join(configFileDir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) - jsonConfigFilePath := filepath.Join(configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + yamlConfigFilePath := filepath.Join(configFileDir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonConfigFilePath := filepath.Join(configFileDir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) // the yaml file takes precedence over the legacy json file when both exist if workspaceConfigFile, exists, err := readWorkspaceConfigYamlIfExists(yamlConfigFilePath); exists { @@ -74,7 +74,7 @@ func GetWorkSpaceFromFilePath(configFileDir string) (models.WorkspaceConfigFile, } if _, err := os.Stat(jsonConfigFilePath); os.IsNotExist(err) { - return models.WorkspaceConfigFile{}, fmt.Errorf("no %s or %s found in %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir) + return models.WorkspaceConfigFile{}, fmt.Errorf("no %s or %s found in %s", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME, configFileDir) } return GetWorkspaceConfigByPath(jsonConfigFilePath) @@ -159,7 +159,7 @@ func gitBranchMapFromOverrides(gitBranchOverrides []models.GitBranchEnvironmentO // findWorkspaceConfigFileInDir looks for a .infisical.yaml (or legacy .infisical.json) in exactly // the given directory, without walking up. The YAML file takes precedence. func findWorkspaceConfigFileInDir(dir string) (string, bool) { - for _, fileName := range []string{INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { + for _, fileName := range []string{INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME} { path := filepath.Join(dir, fileName) if _, err := os.Stat(path); err == nil { return path, true @@ -192,7 +192,7 @@ func FindWorkspaceConfigFile() (string, error) { } // file not found - return "", fmt.Errorf("file not found: %s or %s", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + return "", fmt.Errorf("file not found: %s or %s", INFISICAL_WORKSPACE_CONFIG_FILE_NAME, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } diff --git a/packages/util/config_test.go b/packages/util/config_test.go index 93b79d08..16bdedb7 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -174,7 +174,7 @@ func fileExists(path string) bool { func TestGetWorkSpaceFromFilePath(t *testing.T) { t.Run("reads yaml when only yaml exists", func(t *testing.T) { dir := t.TempDir() - writeTestFile(t, filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) + writeTestFile(t, filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) cfg, err := GetWorkSpaceFromFilePath(dir) if err != nil { @@ -185,7 +185,7 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { t.Run("reads legacy json when only json exists", func(t *testing.T) { dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, legacyWorkspaceJSON) cfg, err := GetWorkSpaceFromFilePath(dir) @@ -197,16 +197,16 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } - if fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + if fileExists(filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { t.Error("yaml should not be created when reading the legacy json") } }) t.Run("prefers yaml and leaves json untouched when both exist", func(t *testing.T) { dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, `{"workspaceId":"from-json"}`) - writeTestFile(t, filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) + writeTestFile(t, filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), workspaceYAML) cfg, err := GetWorkSpaceFromFilePath(dir) if err != nil { @@ -220,7 +220,7 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { t.Run("malformed json returns an error and keeps the json", func(t *testing.T) { dir := t.TempDir() - jsonPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(dir, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, `{not json`) if _, err := GetWorkSpaceFromFilePath(dir); err == nil { @@ -229,7 +229,7 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } - if fileExists(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + if fileExists(filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { t.Error("yaml should not be created from malformed json") } }) @@ -260,7 +260,7 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { readYaml := func(t *testing.T, contents string) (models.WorkspaceConfigFile, error) { t.Helper() dir := t.TempDir() - yamlPath := filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + yamlPath := filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, yamlPath, contents) cfg, err := GetWorkSpaceFromFilePath(dir) @@ -406,7 +406,7 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { root := t.TempDir() - jsonPath := filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + jsonPath := filepath.Join(root, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) writeTestFile(t, jsonPath, legacyWorkspaceJSON) nested := filepath.Join(root, "a", "b") if err := os.MkdirAll(nested, 0o700); err != nil { @@ -422,7 +422,7 @@ func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { if !fileExists(jsonPath) { t.Errorf("legacy %s should not be removed", jsonPath) } - if fileExists(filepath.Join(root, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME)) { + if fileExists(filepath.Join(root, INFISICAL_WORKSPACE_CONFIG_FILE_NAME)) { t.Error("yaml should not be created when reading the legacy json") } @@ -430,7 +430,7 @@ func TestGetWorkSpaceFromFileReadsParentLegacyConfig(t *testing.T) { if err != nil { t.Fatalf("FindWorkspaceConfigFile: %v", err) } - if filepath.Base(found) != INFISICAL_WORKSPACE_CONFIG_FILE_NAME { + if filepath.Base(found) != LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME { t.Errorf("FindWorkspaceConfigFile = %q, want the legacy json file", found) } } diff --git a/packages/util/constants.go b/packages/util/constants.go index 39fdfa19..59bd4eee 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -7,12 +7,15 @@ import ( ) const ( - CONFIG_FILE_NAME = "infisical-config.json" - CONFIG_FOLDER_NAME = ".infisical" - INFISICAL_DEFAULT_US_URL = "https://app.infisical.com" - INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" - INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" - INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME = ".infisical.yaml" + CONFIG_FILE_NAME = "infisical-config.json" + CONFIG_FOLDER_NAME = ".infisical" + INFISICAL_DEFAULT_US_URL = "https://app.infisical.com" + INFISICAL_DEFAULT_EU_URL = "https://eu.infisical.com" + + // Legacy workspace config file name, still supported for backwards compatibility. + LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.json" + INFISICAL_WORKSPACE_CONFIG_FILE_NAME = ".infisical.yaml" + INFISICAL_WORKSPACE_CONFIG_YAML_VERSION = 1 INFISICAL_TOKEN_NAME = "INFISICAL_TOKEN" INFISICAL_PROJECT_ID_NAME = "INFISICAL_PROJECT_ID" diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index 2761faed..4244f8a2 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -37,7 +37,7 @@ func writeWorkspaceYaml(t *testing.T, contents string) { t.Helper() dir := t.TempDir() t.Chdir(dir) - if err := os.WriteFile(filepath.Join(dir, INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME), []byte(contents), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(dir, INFISICAL_WORKSPACE_CONFIG_FILE_NAME), []byte(contents), 0o600); err != nil { t.Fatalf("write workspace yaml: %v", err) } } @@ -243,11 +243,11 @@ func TestResolveEnvironmentName(t *testing.T) { if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" { t.Fatalf("got %q, want fromfile", got) } - if !fileExists(INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { - t.Fatalf("legacy %s should not be removed", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) + if !fileExists(LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { + t.Fatalf("legacy %s should not be removed", LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } - if fileExists(INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) { - t.Fatalf("%s should not be created", INFISICAL_NEW_WORKSPACE_CONFIG_FILE_NAME) + if fileExists(INFISICAL_WORKSPACE_CONFIG_FILE_NAME) { + t.Fatalf("%s should not be created", INFISICAL_WORKSPACE_CONFIG_FILE_NAME) } // the second resolve still reads the legacy json if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromfile" { @@ -257,7 +257,7 @@ func TestResolveEnvironmentName(t *testing.T) { t.Run("yaml file wins over legacy json in the same directory", func(t *testing.T) { writeWorkspaceYaml(t, "secrets-management:\n default-environment: fromyaml\n") - writeTestFile(t, INFISICAL_WORKSPACE_CONFIG_FILE_NAME, `{"defaultEnvironment":"fromjson"}`) + writeTestFile(t, LEGACY_INFISICAL_WORKSPACE_CONFIG_FILE_NAME, `{"defaultEnvironment":"fromjson"}`) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { t.Fatalf("got %q, want fromyaml", got) From 27e4909d0431ef2ebbbc80d8dcc7a69375f216d4 Mon Sep 17 00:00:00 2001 From: adilson Date: Tue, 20 Oct 2026 22:01:17 -0300 Subject: [PATCH 7/7] update yaml format --- packages/cmd/agent_proxy.go | 4 +- packages/cmd/agent_proxy_run.go | 4 +- packages/models/cli.go | 20 +++---- packages/util/config.go | 22 ++++---- packages/util/config_test.go | 99 ++++++++++++++++----------------- packages/util/helper.go | 4 +- packages/util/resolve_test.go | 76 +++++++++++++++---------- 7 files changed, 121 insertions(+), 108 deletions(-) diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index d880573a..7d500a8e 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -58,7 +58,7 @@ infisical secrets agent-proxy connect -- claude`, agentProxyConnectCmd.Flags().String("proxy", "", "address of the agent proxy as host:port (falls back to INFISICAL_AGENT_PROXY_ADDRESS)") agentProxyConnectCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from (falls back to INFISICAL_ENVIRONMENT, then .infisical.yaml or a legacy .infisical.json)") - agentProxyConnectCmd.Flags().String("path", "/", "secret path (folder) scope (falls back to INFISICAL_SECRET_PATH, then secrets-management.default-secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") + agentProxyConnectCmd.Flags().String("path", "/", "secret path (folder) scope (falls back to INFISICAL_SECRET_PATH, then secrets-management.secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") agentProxyConnectCmd.Flags().String("projectId", "", "project id (falls back to INFISICAL_PROJECT_ID, then .infisical.yaml or a legacy .infisical.json)") agentProxyConnectCmd.Flags().String("client-id", "", "universal auth client id for the agent machine identity") agentProxyConnectCmd.Flags().String("client-secret", "", "universal auth client secret for the agent machine identity") @@ -169,7 +169,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { environment := util.ResolveEnvironmentName(cmd) if environment == "" { - util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.default-environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) + util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) } secretPath := util.ResolveSecretPath(cmd) diff --git a/packages/cmd/agent_proxy_run.go b/packages/cmd/agent_proxy_run.go index f1c24662..3090ec71 100644 --- a/packages/cmd/agent_proxy_run.go +++ b/packages/cmd/agent_proxy_run.go @@ -44,7 +44,7 @@ func buildAgentProxyRunCmd() *cobra.Command { } agentProxyRunCmd.Flags().StringP("env", "e", "", "environment slug to fetch proxied services and secrets from (falls back to INFISICAL_ENVIRONMENT, then .infisical.yaml or a legacy .infisical.json)") - agentProxyRunCmd.Flags().String("path", "/", "secret path (folder) to fetch from (falls back to INFISICAL_SECRET_PATH, then secrets-management.default-secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") + agentProxyRunCmd.Flags().String("path", "/", "secret path (folder) to fetch from (falls back to INFISICAL_SECRET_PATH, then secrets-management.secret-path in .infisical.yaml or defaultSecretPath in a legacy .infisical.json)") agentProxyRunCmd.Flags().String("projectId", "", "project id (falls back to INFISICAL_PROJECT_ID, then .infisical.yaml or a legacy .infisical.json)") agentProxyRunCmd.Flags().String("token", "", "run using this token instead of your logged-in session") agentProxyRunCmd.Flags().Bool("sandbox", true, "run the agent inside the OS sandbox") @@ -78,7 +78,7 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { // Same resolution order as `connect`: flag, then env var, then .infisical.yaml. environment := util.ResolveEnvironmentName(cmd) if environment == "" { - util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.default-environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) + util.HandleError(fmt.Errorf("the environment is required; pass --env, set INFISICAL_ENVIRONMENT, or set secrets-management.environment in .infisical.yaml (or defaultEnvironment in a legacy .infisical.json)")) } secretPath := util.ResolveSecretPath(cmd) diff --git a/packages/models/cli.go b/packages/models/cli.go index 8edceba6..d48b5e40 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -186,31 +186,29 @@ type GeneralConfigSection struct { Domain string `yaml:"domain,omitempty"` } -// GitBranchEnvironmentOverride is one entry of the secrets-management.environment.overrides.git-branch list. +// GitBranchEnvironmentOverride is one entry of the secrets-management.overrides.git-branch list. type GitBranchEnvironmentOverride struct { Branch string `yaml:"branch"` Environment string `yaml:"environment"` } -type EnvironmentOverridesConfigSection struct { +type OverridesConfigSection struct { GitBranch []GitBranchEnvironmentOverride `yaml:"git-branch,omitempty"` } -type EnvironmentConfigSection struct { - Default string `yaml:"default,omitempty"` - Overrides EnvironmentOverridesConfigSection `yaml:"overrides,omitempty"` -} - type SecretsManagementConfigSection struct { - ProjectID string `yaml:"project-id"` + ProjectID string `yaml:"project-id"` + Environment string `yaml:"environment,omitempty"` + SecretPath string `yaml:"secret-path,omitempty"` + Overrides OverridesConfigSection `yaml:"overrides,omitempty"` // Deprecated: superseded by Environment; still read until support for it is removed. DefaultEnvironment string `yaml:"default-environment,omitempty"` - DefaultSecretPath string `yaml:"default-secret-path,omitempty"` - // Deprecated: superseded by Environment; still read until support for it is removed. + // Deprecated: superseded by SecretPath; still read until support for it is removed. + DefaultSecretPath string `yaml:"default-secret-path,omitempty"` + // Deprecated: superseded by Overrides.GitBranch; still read until support for it is removed. Mappings struct { GitBranchToEnvironment map[string]string `yaml:"git-branch-to-environment,omitempty"` } `yaml:"mappings,omitempty"` - Environment *EnvironmentConfigSection `yaml:"environment,omitempty"` } type WorkspaceConfigFileYaml struct { diff --git a/packages/util/config.go b/packages/util/config.go index c6b488d6..ffaa4814 100644 --- a/packages/util/config.go +++ b/packages/util/config.go @@ -107,10 +107,11 @@ func readWorkspaceConfigYaml(path string) (models.WorkspaceConfigFile, error) { return workspaceConfigFromYaml(workspaceConfigFileYaml), nil } -// workspaceConfigFromYaml accepts both the environment section and the deprecated default-environment / -// mappings fields. Each field is resolved on its own: environment.default wins over default-environment when -// set, and environment.overrides.git-branch wins over the mappings when present, so a partial migration keeps -// the deprecated value for whichever field has not been moved yet. +// workspaceConfigFromYaml accepts both the current keys and the deprecated default-environment / +// default-secret-path / mappings ones. Each field is resolved on its own: environment wins over +// default-environment and secret-path wins over default-secret-path when set, and overrides.git-branch replaces +// the mappings when present, so a partial migration keeps the deprecated value for whichever field has not been +// moved yet. func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) models.WorkspaceConfigFile { secretsManagement := workspaceConfigYaml.SecretsManagement workspaceConfigFile := models.WorkspaceConfigFile{ @@ -121,17 +122,16 @@ func workspaceConfigFromYaml(workspaceConfigYaml models.WorkspaceConfigFileYaml) Domain: workspaceConfigYaml.General.Domain, } - environment := secretsManagement.Environment - if environment == nil { - return workspaceConfigFile + if secretsManagement.Environment != "" { + workspaceConfigFile.DefaultEnvironment = secretsManagement.Environment } - if environment.Default != "" { - workspaceConfigFile.DefaultEnvironment = environment.Default + if secretsManagement.SecretPath != "" { + workspaceConfigFile.DefaultSecretPath = secretsManagement.SecretPath } - if len(environment.Overrides.GitBranch) > 0 { - workspaceConfigFile.GitBranchToEnvironmentMapping = gitBranchMapFromOverrides(environment.Overrides.GitBranch) + if len(secretsManagement.Overrides.GitBranch) > 0 { + workspaceConfigFile.GitBranchToEnvironmentMapping = gitBranchMapFromOverrides(secretsManagement.Overrides.GitBranch) } return workspaceConfigFile diff --git a/packages/util/config_test.go b/packages/util/config_test.go index 16bdedb7..28e5ab20 100644 --- a/packages/util/config_test.go +++ b/packages/util/config_test.go @@ -241,22 +241,21 @@ func TestGetWorkSpaceFromFilePath(t *testing.T) { }) } -const workspaceYAMLWithEnvironment = `general: +const workspaceYAMLCurrent = `general: domain: https://eu.infisical.com secrets-management: project-id: proj-123 - default-secret-path: /backend - environment: - default: dev - overrides: - git-branch: - - branch: main - environment: prod - - branch: staging - environment: staging + environment: dev + secret-path: /backend + overrides: + git-branch: + - branch: main + environment: prod + - branch: staging + environment: staging ` -func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { +func TestWorkspaceConfigYamlSchemas(t *testing.T) { readYaml := func(t *testing.T, contents string) (models.WorkspaceConfigFile, error) { t.Helper() dir := t.TempDir() @@ -274,8 +273,8 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { return cfg, err } - t.Run("reads the environment section", func(t *testing.T) { - cfg, err := readYaml(t, workspaceYAMLWithEnvironment) + t.Run("reads the current keys", func(t *testing.T) { + cfg, err := readYaml(t, workspaceYAMLCurrent) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -285,7 +284,7 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { } }) - t.Run("reads the deprecated default-environment and mappings", func(t *testing.T) { + t.Run("reads the deprecated default-environment, default-secret-path and mappings", func(t *testing.T) { cfg, err := readYaml(t, workspaceYAML) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -293,20 +292,21 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { assertFullWorkspaceConfig(t, cfg, "proj-123") }) - t.Run("the environment section wins over the deprecated fields", func(t *testing.T) { + t.Run("the current keys win over the deprecated ones", func(t *testing.T) { cfg, err := readYaml(t, `secrets-management: project-id: proj-123 default-environment: old-default + default-secret-path: /old mappings: git-branch-to-environment: main: old-main feature: old-feature - environment: - default: dev - overrides: - git-branch: - - branch: main - environment: prod + environment: dev + secret-path: /backend + overrides: + git-branch: + - branch: main + environment: prod `) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -314,6 +314,9 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { if cfg.DefaultEnvironment != "dev" { t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) } + if cfg.DefaultSecretPath != "/backend" { + t.Errorf("DefaultSecretPath = %q, want /backend", cfg.DefaultSecretPath) + } // the deprecated mappings are ignored, not merged with the overrides want := map[string]string{"main": "prod"} if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { @@ -321,18 +324,18 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { } }) - t.Run("environment.overrides alone keeps the deprecated default-environment", func(t *testing.T) { + t.Run("overrides alone keep the deprecated default-environment and default-secret-path", func(t *testing.T) { cfg, err := readYaml(t, `secrets-management: project-id: proj-123 default-environment: prod + default-secret-path: /old mappings: git-branch-to-environment: main: old-main - environment: - overrides: - git-branch: - - branch: feature/x - environment: staging + overrides: + git-branch: + - branch: feature/x + environment: staging `) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -340,21 +343,25 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { if cfg.DefaultEnvironment != "prod" { t.Errorf("DefaultEnvironment = %q, want prod", cfg.DefaultEnvironment) } + if cfg.DefaultSecretPath != "/old" { + t.Errorf("DefaultSecretPath = %q, want /old", cfg.DefaultSecretPath) + } want := map[string]string{"feature/x": "staging"} if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) - t.Run("environment.default alone keeps the deprecated mappings", func(t *testing.T) { + t.Run("environment and secret-path alone keep the deprecated mappings", func(t *testing.T) { cfg, err := readYaml(t, `secrets-management: project-id: proj-123 default-environment: old-default + default-secret-path: /old mappings: git-branch-to-environment: main: prod - environment: - default: dev + environment: dev + secret-path: /backend `) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -362,37 +369,25 @@ func TestWorkspaceConfigYamlEnvironmentSchemas(t *testing.T) { if cfg.DefaultEnvironment != "dev" { t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) } + if cfg.DefaultSecretPath != "/backend" { + t.Errorf("DefaultSecretPath = %q, want /backend", cfg.DefaultSecretPath) + } want := map[string]string{"main": "prod"} if !maps.Equal(cfg.GitBranchToEnvironmentMapping, want) { t.Errorf("GitBranchToEnvironmentMapping = %v, want %v", cfg.GitBranchToEnvironmentMapping, want) } }) - t.Run("an empty environment section falls back to the deprecated fields", func(t *testing.T) { - cfg, err := readYaml(t, `secrets-management: - project-id: proj-123 - default-environment: dev - environment: {} -`) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if cfg.DefaultEnvironment != "dev" { - t.Errorf("DefaultEnvironment = %q, want dev", cfg.DefaultEnvironment) - } - }) - t.Run("first override for a branch wins and entries without a branch are skipped", func(t *testing.T) { cfg, err := readYaml(t, `secrets-management: project-id: proj-123 - environment: - overrides: - git-branch: - - environment: orphan - - branch: main - environment: prod - - branch: main - environment: staging + overrides: + git-branch: + - environment: orphan + - branch: main + environment: prod + - branch: main + environment: staging `) if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/packages/util/helper.go b/packages/util/helper.go index 4d7bb2ac..d6a87bed 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -579,7 +579,7 @@ func ResolveEnvironmentName(cmd *cobra.Command) string { // ResolveSecretPath resolves the secret path (folder) for a command, in order: // the --path flag (if explicitly set) > INFISICAL_SECRET_PATH > .infisical.yaml -// default-secret-path > the flag's own default value ("/"). +// secret-path (or the deprecated default-secret-path) > the flag's own default value ("/"). func ResolveSecretPath(cmd *cobra.Command) string { if cmd.Flags().Changed("path") { value, _ := cmd.Flags().GetString("path") @@ -597,7 +597,7 @@ func ResolveSecretPath(cmd *cobra.Command) string { // ResolveSecretPaths is ResolveSecretPath for commands whose --path flag can be repeated // (`run`). Explicit flags are returned as given; otherwise the single path from -// INFISICAL_SECRET_PATH or .infisical.json defaultSecretPath, then the flag's default. +// INFISICAL_SECRET_PATH or .infisical.yaml secret-path, then the flag's default. func ResolveSecretPaths(cmd *cobra.Command) []string { if cmd.Flags().Changed("path") { values, _ := cmd.Flags().GetStringArray("path") diff --git a/packages/util/resolve_test.go b/packages/util/resolve_test.go index 4244f8a2..1d28c8e5 100644 --- a/packages/util/resolve_test.go +++ b/packages/util/resolve_test.go @@ -142,23 +142,30 @@ func TestResolveEnvironmentName(t *testing.T) { } }) - t.Run("yaml environment.default used when flag and env unset", func(t *testing.T) { - writeWorkspaceYaml(t, "secrets-management:\n environment:\n default: fromyaml\n") + t.Run("yaml environment used when flag and env unset", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n environment: fromyaml\n") t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { t.Fatalf("got %q, want fromyaml", got) } }) - t.Run("yaml git-branch override wins over environment.default", func(t *testing.T) { + t.Run("yaml environment wins over the deprecated default-environment", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n default-environment: fromdeprecated\n environment: fromyaml\n") + t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") + if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { + t.Fatalf("got %q, want fromyaml", got) + } + }) + + t.Run("yaml git-branch override wins over environment", func(t *testing.T) { mockCurrentBranch(t, "main") writeWorkspaceYaml(t, `secrets-management: - environment: - default: fromyaml - overrides: - git-branch: - - branch: main - environment: frombranch + environment: fromyaml + overrides: + git-branch: + - branch: main + environment: frombranch `) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { @@ -166,15 +173,14 @@ func TestResolveEnvironmentName(t *testing.T) { } }) - t.Run("yaml environment.default used when the branch has no override", func(t *testing.T) { + t.Run("yaml environment used when the branch has no override", func(t *testing.T) { mockCurrentBranch(t, "feature") writeWorkspaceYaml(t, `secrets-management: - environment: - default: fromyaml - overrides: - git-branch: - - branch: main - environment: frombranch + environment: fromyaml + overrides: + git-branch: + - branch: main + environment: frombranch `) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { @@ -185,12 +191,11 @@ func TestResolveEnvironmentName(t *testing.T) { t.Run("yaml git-branch override matches the last segment of the branch", func(t *testing.T) { mockCurrentBranch(t, "feature/login") writeWorkspaceYaml(t, `secrets-management: - environment: - default: fromyaml - overrides: - git-branch: - - branch: login - environment: frombranch + environment: fromyaml + overrides: + git-branch: + - branch: login + environment: frombranch `) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "frombranch" { @@ -202,11 +207,10 @@ func TestResolveEnvironmentName(t *testing.T) { mockCurrentBranch(t, "main") writeWorkspaceYaml(t, `secrets-management: default-environment: fromyaml - environment: - overrides: - git-branch: - - branch: feature/x - environment: frombranch + overrides: + git-branch: + - branch: feature/x + environment: frombranch `) t.Setenv(INFISICAL_ENVIRONMENT_NAME, "") if got := ResolveEnvironmentName(newResolveTestCmd(t)); got != "fromyaml" { @@ -326,7 +330,23 @@ func TestResolveSecretPath(t *testing.T) { } }) - t.Run("defaults to / when yaml file has no default-secret-path", func(t *testing.T) { + t.Run("yaml secret-path when flag and env unset", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n secret-path: /fromyaml\n") + t.Setenv(INFISICAL_SECRET_PATH_NAME, "") + if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/fromyaml" { + t.Fatalf("got %q, want /fromyaml", got) + } + }) + + t.Run("yaml secret-path wins over the deprecated default-secret-path", func(t *testing.T) { + writeWorkspaceYaml(t, "secrets-management:\n default-secret-path: /fromdeprecated\n secret-path: /fromyaml\n") + t.Setenv(INFISICAL_SECRET_PATH_NAME, "") + if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/fromyaml" { + t.Fatalf("got %q, want /fromyaml", got) + } + }) + + t.Run("defaults to / when yaml file has no secret path", func(t *testing.T) { writeWorkspaceYaml(t, "secrets-management:\n project-id: proj-123\n") t.Setenv(INFISICAL_SECRET_PATH_NAME, "") if got := ResolveSecretPath(newResolveTestCmd(t)); got != "/" {