From 40575de937f2e333cb8847aac6237cd33aab6f7b Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Thu, 28 May 2026 18:39:23 -0300 Subject: [PATCH 1/3] allow `--tag` in `infisical secrets set` for both creation and update --- packages/api/api.go | 39 +++++++++++++++++++++ packages/api/model.go | 60 ++++++++++++++++++++++---------- packages/cmd/secrets.go | 10 ++++-- packages/models/cli.go | 2 +- packages/util/secrets.go | 75 +++++++++++++++++++++++++++++++++------- 5 files changed, 151 insertions(+), 35 deletions(-) diff --git a/packages/api/api.go b/packages/api/api.go index 30d6e0d7..4ca651e9 100644 --- a/packages/api/api.go +++ b/packages/api/api.go @@ -1364,3 +1364,42 @@ func CallGetCertificateRequest(httpClient *resty.Client, certificateRequestId st return &resBody, nil } + +func GetTagBySlug(httpClient *resty.Client, projectId string, tagSlug string) (SecretTag, error) { + var resBody GetTagBySlugResponse + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + Get(fmt.Sprintf("%v/v1/projects/%s/tags/slug/%s", config.INFISICAL_URL, url.PathEscape(projectId), url.PathEscape(tagSlug))) + + if err != nil { + return SecretTag{}, NewGenericRequestError("GetTagBySlug", err) + } + + if response.IsError() { + return SecretTag{}, NewAPIErrorWithResponse("GetTagBySlug", response, nil) + } + + return resBody.Tag, nil +} + +func CreateTag(httpClient *resty.Client, projectId string, request CreateTagRequest) (SecretTag, error) { + var resBody CreateTagResponse + response, err := httpClient. + R(). + SetResult(&resBody). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v1/projects/%s/tags", config.INFISICAL_URL, url.PathEscape(projectId))) + + if err != nil { + return SecretTag{}, NewGenericRequestError("CreateTag", err) + } + + if response.IsError() { + return SecretTag{}, NewAPIErrorWithResponse("CreateTag", response, nil) + } + + return resBody.Tag, nil +} diff --git a/packages/api/model.go b/packages/api/model.go index cfebc509..fccff083 100644 --- a/packages/api/model.go +++ b/packages/api/model.go @@ -229,11 +229,12 @@ type Project struct { } type RawSecret struct { - SecretKey string `json:"secretKey,omitempty"` - SecretValue string `json:"secretValue,omitempty"` - Type string `json:"type,omitempty"` - SecretComment string `json:"secretComment,omitempty"` - ID string `json:"id,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + SecretValue string `json:"secretValue,omitempty"` + Type string `json:"type,omitempty"` + SecretComment string `json:"secretComment,omitempty"` + ID string `json:"id,omitempty"` + TagIDs []string `json:"tagIds,omitempty"` } type GetEncryptedWorkspaceKeyRequest struct { @@ -487,14 +488,15 @@ type CreateSecretV3Request struct { } type CreateRawSecretV3Request struct { - SecretName string `json:"-"` - WorkspaceID string `json:"workspaceId"` - Type string `json:"type,omitempty"` - Environment string `json:"environment"` - SecretPath string `json:"secretPath,omitempty"` - SecretValue string `json:"secretValue"` - SecretComment string `json:"secretComment,omitempty"` - SkipMultilineEncoding bool `json:"skipMultilineEncoding,omitempty"` + SecretName string `json:"-"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type,omitempty"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment,omitempty"` + SkipMultilineEncoding bool `json:"skipMultilineEncoding,omitempty"` + TagIDs []string `json:"tagIds,omitempty"` } type DeleteSecretV3Request struct { @@ -516,12 +518,13 @@ type UpdateSecretByNameV3Request struct { } type UpdateRawSecretByNameV3Request struct { - SecretName string `json:"-"` - WorkspaceID string `json:"workspaceId"` - Environment string `json:"environment"` - SecretPath string `json:"secretPath,omitempty"` - SecretValue string `json:"secretValue"` - Type string `json:"type,omitempty"` + SecretName string `json:"-"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` + SecretValue string `json:"secretValue"` + Type string `json:"type,omitempty"` + TagIDs []string `json:"tagIds,omitempty"` } type GetSingleSecretByNameV3Request struct { @@ -1124,3 +1127,22 @@ type GetCertificateRequestResponse struct { CertificateID *string `json:"certificateId,omitempty"` ErrorMessage *string `json:"errorMessage,omitempty"` } + +type SecretTag struct { + ID string `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` +} + +type GetTagBySlugResponse struct { + Tag SecretTag `json:"tag"` +} + +type CreateTagRequest struct { + Slug string `json:"slug"` + Color string `json:"color"` +} + +type CreateTagResponse struct { + Tag SecretTag `json:"tag"` +} diff --git a/packages/cmd/secrets.go b/packages/cmd/secrets.go index 904db0d8..a8aeaae4 100644 --- a/packages/cmd/secrets.go +++ b/packages/cmd/secrets.go @@ -218,6 +218,11 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + tags, err := cmd.Flags().GetStringArray("tag") + if err != nil { + util.HandleError(err, `Unable to parse "tag" flag`) + } + processedArgs := []string{} for _, arg := range args { splitKeyValue := strings.SplitN(arg, "=", 2) @@ -253,7 +258,7 @@ var secretsSetCmd = &cobra.Command{ util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag") } - secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file) + secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file, tags) if err != nil { util.HandleError(err, "Unable to set secrets") @@ -280,7 +285,7 @@ var secretsSetCmd = &cobra.Command{ secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{ Type: "", Token: loggedInUserDetails.UserCredentials.JTWToken, - }, file) + }, file, tags) if err != nil { util.HandleError(err, "Unable to set secrets") @@ -835,6 +840,7 @@ func init() { secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.") + secretsSetCmd.Flags().StringArray("tag", []string{}, "Tags to associate with the secret. Can be specified multiple times (e.g. --tag backend --tag production). When updating an existing secret, the provided tags will replace any existing tags") util.AddOutputFlagsToCmd(secretsSetCmd, "The output to format the secrets in.") secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") diff --git a/packages/models/cli.go b/packages/models/cli.go index 0a427f73..ea874088 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -32,7 +32,7 @@ type LoggedInUser struct { } type Tag struct { - ID string `json:"_id"` + ID string `json:"id"` Name string `json:"name"` Slug string `json:"slug"` Color string `json:"color"` diff --git a/packages/util/secrets.go b/packages/util/secrets.go index 0dd69c05..a24aa1b8 100644 --- a/packages/util/secrets.go +++ b/packages/util/secrets.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "os" "strings" "unicode" @@ -13,6 +14,7 @@ import ( "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/models" + "github.com/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" "gopkg.in/yaml.v3" @@ -376,16 +378,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo return secretsToReturn, errorToReturn } -func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { - secretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - - for _, secret := range secrets { - secretMapByName[secret.Key] = secret - } - - return secretMapByName -} - func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable { personalSecrets := make(map[string]models.SingleEnvironmentVariable) sharedSecrets := make(map[string]models.SingleEnvironmentVariable) @@ -631,7 +623,7 @@ func validateSecretKey(key string) error { return nil } -func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) { +func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string, tagSlugs []string) ([]models.SecretSetOperation, error) { if file != "" { content, err := os.ReadFile(file) if err != nil { @@ -688,6 +680,15 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) } + var cliProvidedTagIds []string + for _, slug := range tagSlugs { + tag, err := GetOrCreateTag(httpClient, projectId, slug) + if err != nil { + return nil, err + } + cliProvidedTagIds = append(cliProvidedTagIds, tag.ID) + } + secretsToCreate := []api.RawSecret{} secretsToModify := []api.RawSecret{} secretOperations := []models.SecretSetOperation{} @@ -733,15 +734,36 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin SecretValue: value, SecretKey: key, Type: existingSecret.Type, + TagIDs: cliProvidedTagIds, + } + + existingTags := make(map[string]struct{}, 0) + for _, tag := range existingSecret.Tags { + existingTags[tag.ID] = struct{}{} + } + + newTagProvided := false + for _, cliProvidedTagId := range cliProvidedTagIds { + if _, found := existingTags[cliProvidedTagId]; !found { + newTagProvided = true + } } + tagsChanged := newTagProvided || len(existingTags) != len(cliProvidedTagIds) + // Only add to modifications if the value is different - if existingSecret.Value != value { + if existingSecret.Value != value || tagsChanged { secretsToModify = append(secretsToModify, encryptedSecretDetails) + message := "SECRET VALUE MODIFIED" + if existingSecret.Value == value { + // We only display SECRET TAGS UPDATED if the value has not changed + // otherwise value changes should take precedence + message = "SECRET TAGS MODIFIED" + } secretOperations = append(secretOperations, models.SecretSetOperation{ SecretKey: key, SecretValue: value, - SecretOperation: "SECRET VALUE MODIFIED", + SecretOperation: message, }) } else { // Current value is same as existing so no change @@ -758,6 +780,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin SecretKey: key, SecretValue: value, Type: secretType, + TagIDs: cliProvidedTagIds, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) secretOperations = append(secretOperations, models.SecretSetOperation{ @@ -776,6 +799,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin SecretPath: secretsPath, WorkspaceID: projectId, Environment: environmentName, + TagIDs: secret.TagIDs, } err = api.CallCreateRawSecretsV3(httpClient, createSecretRequest) @@ -792,6 +816,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin WorkspaceID: projectId, Environment: environmentName, Type: secret.Type, + TagIDs: secret.TagIDs, } err = api.CallUpdateRawSecretsV3(httpClient, updateSecretRequest) @@ -803,3 +828,27 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin return secretOperations, nil } + +func GetOrCreateTag(client *resty.Client, projectId string, slug string) (api.SecretTag, error) { + tag, err := api.GetTagBySlug(client, projectId, slug) + if err == nil { + return tag, nil + } + + var apiErr *api.APIError + if errors.As(err, &apiErr) { + if apiErr.StatusCode == http.StatusNotFound { + newTag, createErr := api.CreateTag(client, projectId, api.CreateTagRequest{ + Slug: slug, + Color: "", + }) + if createErr != nil { + return api.SecretTag{}, fmt.Errorf("could not create tag %q: [err=%v]", slug, createErr) + } + + return newTag, nil + } + } + + return api.SecretTag{}, fmt.Errorf("unable to resolve tag slug %q [err=%v]", slug, err) +} From 27735f631ddde25b28c6a637f329f89e8fc6a180 Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Thu, 28 May 2026 18:49:20 -0300 Subject: [PATCH 2/3] fix tag changed condition --- packages/util/secrets.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/util/secrets.go b/packages/util/secrets.go index a24aa1b8..dc2a2140 100644 --- a/packages/util/secrets.go +++ b/packages/util/secrets.go @@ -737,20 +737,21 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin TagIDs: cliProvidedTagIds, } - existingTags := make(map[string]struct{}, 0) + existingTagIds := make(map[string]struct{}, len(existingSecret.Tags)) for _, tag := range existingSecret.Tags { - existingTags[tag.ID] = struct{}{} + existingTagIds[tag.ID] = struct{}{} } - newTagProvided := false - for _, cliProvidedTagId := range cliProvidedTagIds { - if _, found := existingTags[cliProvidedTagId]; !found { - newTagProvided = true + tagsChanged := len(cliProvidedTagIds) > 0 && len(cliProvidedTagIds) != len(existingTagIds) + if !tagsChanged { + for _, id := range cliProvidedTagIds { + if _, found := existingTagIds[id]; !found { + tagsChanged = true + break + } } } - tagsChanged := newTagProvided || len(existingTags) != len(cliProvidedTagIds) - // Only add to modifications if the value is different if existingSecret.Value != value || tagsChanged { secretsToModify = append(secretsToModify, encryptedSecretDetails) From d04d1af8fd8f467a6a4ab977fdc71b9b0d344cc9 Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Thu, 28 May 2026 18:57:53 -0300 Subject: [PATCH 3/3] dedupe tag slugs --- packages/util/secrets.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/util/secrets.go b/packages/util/secrets.go index dc2a2140..417f2732 100644 --- a/packages/util/secrets.go +++ b/packages/util/secrets.go @@ -680,8 +680,13 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) } + uniqueSlugs := make(map[string]struct{}, len(tagSlugs)) var cliProvidedTagIds []string for _, slug := range tagSlugs { + if _, seen := uniqueSlugs[slug]; seen { + continue + } + uniqueSlugs[slug] = struct{}{} tag, err := GetOrCreateTag(httpClient, projectId, slug) if err != nil { return nil, err