Skip to content

Commit a586c19

Browse files
authored
Merge pull request #471 from depot/luke/jj-tspmvvlxqptp
feat: support bulk KEY=VALUE pairs for secrets and variables
2 parents 876fc62 + ae1213d commit a586c19

3 files changed

Lines changed: 165 additions & 32 deletions

File tree

pkg/api/ci.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,17 @@ func CIListSecrets(ctx context.Context, token, orgID, repo string) ([]CISecret,
179179
return secrets, nil
180180
}
181181

182+
// CIBatchAddSecrets adds multiple CI secrets in a single request, optionally scoped to a repo.
183+
func CIBatchAddSecrets(ctx context.Context, token, orgID string, secrets []*civ2.SecretInput, repo string) error {
184+
client := newCISecretServiceV2Client()
185+
if repo != "" {
186+
_, err := client.BatchAddRepoSecrets(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ2.BatchAddRepoSecretsRequest{Repo: repo, Secrets: secrets}), token, orgID))
187+
return err
188+
}
189+
_, err := client.BatchAddOrgSecrets(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ2.BatchAddOrgSecretsRequest{Secrets: secrets}), token, orgID))
190+
return err
191+
}
192+
182193
// CIDeleteSecret deletes a CI secret, optionally scoped to a repo.
183194
func CIDeleteSecret(ctx context.Context, token, orgID, name, repo string) error {
184195
client := newCISecretServiceV2Client()
@@ -253,6 +264,17 @@ func CIListVariables(ctx context.Context, token, orgID, repo string) ([]CIVariab
253264
return variables, nil
254265
}
255266

267+
// CIBatchAddVariables adds multiple CI variables in a single request, optionally scoped to a repo.
268+
func CIBatchAddVariables(ctx context.Context, token, orgID string, variables []*civ2.VariableInput, repo string) error {
269+
client := newCIVariableServiceV2Client()
270+
if repo != "" {
271+
_, err := client.BatchAddRepoVariables(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ2.BatchAddRepoVariablesRequest{Repo: repo, Variables: variables}), token, orgID))
272+
return err
273+
}
274+
_, err := client.BatchAddOrgVariables(ctx, WithAuthenticationAndOrg(connect.NewRequest(&civ2.BatchAddOrgVariablesRequest{Variables: variables}), token, orgID))
275+
return err
276+
}
277+
256278
// CIDeleteVariable deletes a CI variable, optionally scoped to a repo.
257279
func CIDeleteVariable(ctx context.Context, token, orgID, name, repo string) error {
258280
client := newCIVariableServiceV2Client()

pkg/cmd/ci/secrets.go

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/depot/cli/pkg/api"
1010
"github.com/depot/cli/pkg/config"
1111
"github.com/depot/cli/pkg/helpers"
12+
civ2 "github.com/depot/cli/pkg/proto/depot/ci/v2"
1213
"github.com/spf13/cobra"
1314
)
1415

@@ -21,6 +22,9 @@ func NewCmdSecrets() *cobra.Command {
2122
depot ci secrets add GITHUB_TOKEN
2223
depot ci secrets add MY_API_KEY --value "secret-value"
2324
25+
# Add multiple secrets at once
26+
depot ci secrets add FOO=bar BAZ=qux
27+
2428
# Add a repo-specific secret
2529
depot ci secrets add MY_API_KEY --repo owner/repo --value "secret-value"
2630
@@ -54,31 +58,35 @@ func NewCmdSecretsAdd() *cobra.Command {
5458
)
5559

5660
cmd := &cobra.Command{
57-
Use: "add SECRET_NAME",
58-
Short: "Add a new CI secret",
59-
Long: `Add a new secret that can be used in Depot CI workflows.
60-
If --value is not provided, you will be prompted to enter the secret value securely.
61-
Use --repo to scope the secret to a specific repository. Without --repo, the secret
62-
applies to all repositories in the organization.`,
61+
Use: "add [SECRET_NAME | KEY=VALUE ...]",
62+
Short: "Add one or more CI secrets",
63+
Long: `Add secrets that can be used in Depot CI workflows.
64+
65+
Supports three modes:
66+
1. Single secret with --value flag: depot ci secrets add SECRET_NAME --value "val"
67+
2. Single secret with interactive prompt: depot ci secrets add SECRET_NAME
68+
3. Bulk KEY=VALUE pairs: depot ci secrets add FOO=bar BAZ=qux
69+
70+
The --value and --description flags cannot be used with KEY=VALUE pairs.
71+
Use --repo to scope secrets to a specific repository. Without --repo, secrets
72+
apply to all repositories in the organization.`,
6373
Example: ` # Add an org-wide secret with interactive prompt
6474
depot ci secrets add GITHUB_TOKEN
6575
6676
# Add an org-wide secret with value from command line
6777
depot ci secrets add MY_API_KEY --value "secret-value"
6878
79+
# Add multiple secrets at once
80+
depot ci secrets add FOO=bar BAZ=qux
81+
6982
# Add a repo-specific secret
7083
depot ci secrets add DATABASE_URL --repo owner/repo --value "prod-db-url"
7184
7285
# Add a secret with description
7386
depot ci secrets add DATABASE_URL --description "Production database connection string"`,
74-
Args: cobra.ExactArgs(1),
87+
Args: cobra.MinimumNArgs(1),
7588
RunE: func(cmd *cobra.Command, args []string) error {
7689
ctx := cmd.Context()
77-
secretName := args[0]
78-
79-
if secretName == "" {
80-
return fmt.Errorf("secret name cannot be empty")
81-
}
8290

8391
if orgID == "" {
8492
orgID = config.GetCurrentOrganization()
@@ -93,6 +101,59 @@ applies to all repositories in the organization.`,
93101
return fmt.Errorf("missing API token, please run `depot login`")
94102
}
95103

104+
scope := "org-wide"
105+
if repo != "" {
106+
scope = repo
107+
}
108+
109+
// Detect KEY=VALUE pairs
110+
hasKVPairs := false
111+
for _, arg := range args {
112+
if strings.Contains(arg, "=") {
113+
hasKVPairs = true
114+
break
115+
}
116+
}
117+
118+
if hasKVPairs {
119+
// Bulk mode: all args must be KEY=VALUE
120+
if value != "" {
121+
return fmt.Errorf("cannot use --value with KEY=VALUE arguments")
122+
}
123+
if description != "" {
124+
return fmt.Errorf("cannot use --description with KEY=VALUE arguments")
125+
}
126+
127+
var secrets []*civ2.SecretInput
128+
for _, arg := range args {
129+
parts := strings.SplitN(arg, "=", 2)
130+
if len(parts) != 2 || parts[0] == "" {
131+
return fmt.Errorf("invalid argument %q — expected KEY=VALUE format", arg)
132+
}
133+
secrets = append(secrets, &civ2.SecretInput{Name: parts[0], Value: parts[1]})
134+
}
135+
136+
err := api.CIBatchAddSecrets(ctx, tokenVal, orgID, secrets, repo)
137+
if err != nil {
138+
return fmt.Errorf("failed to add secrets: %w", err)
139+
}
140+
141+
for _, s := range secrets {
142+
fmt.Printf("Successfully added CI secret '%s' (%s)\n", s.Name, scope)
143+
}
144+
return nil
145+
}
146+
147+
// Single mode: first arg is secret name
148+
if len(args) > 1 {
149+
return fmt.Errorf("too many arguments — did you mean to use KEY=VALUE format?")
150+
}
151+
152+
secretName := args[0]
153+
if secretName == "" {
154+
return fmt.Errorf("secret name cannot be empty")
155+
}
156+
96157
secretValue := value
97158
if secretValue == "" {
98159
secretValue, err = helpers.PromptForSecret(fmt.Sprintf("Enter value for secret '%s': ", secretName))
@@ -106,10 +167,6 @@ applies to all repositories in the organization.`,
106167
return fmt.Errorf("failed to add secret: %w", err)
107168
}
108169

109-
scope := "org-wide"
110-
if repo != "" {
111-
scope = repo
112-
}
113170
fmt.Printf("Successfully added CI secret '%s' (%s)\n", secretName, scope)
114171
return nil
115172
},

pkg/cmd/ci/vars.go

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/depot/cli/pkg/api"
1010
"github.com/depot/cli/pkg/config"
1111
"github.com/depot/cli/pkg/helpers"
12+
civ2 "github.com/depot/cli/pkg/proto/depot/ci/v2"
1213
"github.com/spf13/cobra"
1314
)
1415

@@ -21,6 +22,9 @@ func NewCmdVars() *cobra.Command {
2122
depot ci vars add GITHUB_REPO
2223
depot ci vars add MY_SERVICE_NAME --value "my_service"
2324
25+
# Add multiple variables at once
26+
depot ci vars add REGION=us-east-1 ENV=prod
27+
2428
# Add a repo-specific variable
2529
depot ci vars add MY_SERVICE_NAME --repo owner/repo --value "my_service"
2630
@@ -51,28 +55,32 @@ func NewCmdVarsAdd() *cobra.Command {
5155
)
5256

5357
cmd := &cobra.Command{
54-
Use: "add VAR_NAME",
55-
Short: "Add a new CI variable",
56-
Long: `Add a new variable that can be used in Depot CI workflows.
57-
If --value is not provided, you will be prompted to enter the variable value.
58-
Use --repo to scope the variable to a specific repository. Without --repo, the
59-
variable applies to all repositories in the organization.`,
58+
Use: "add [VAR_NAME | KEY=VALUE ...]",
59+
Short: "Add one or more CI variables",
60+
Long: `Add variables that can be used in Depot CI workflows.
61+
62+
Supports three modes:
63+
1. Single variable with --value flag: depot ci vars add VAR_NAME --value "val"
64+
2. Single variable with interactive prompt: depot ci vars add VAR_NAME
65+
3. Bulk KEY=VALUE pairs: depot ci vars add FOO=bar BAZ=qux
66+
67+
The --value flag cannot be used with KEY=VALUE pairs.
68+
Use --repo to scope variables to a specific repository. Without --repo, variables
69+
apply to all repositories in the organization.`,
6070
Example: ` # Add an org-wide variable with interactive prompt
6171
depot ci vars add GITHUB_REPO
6272
6373
# Add an org-wide variable with value from command line
6474
depot ci vars add MY_SERVICE_NAME --value "my_service"
6575
76+
# Add multiple variables at once
77+
depot ci vars add REGION=us-east-1 ENV=prod
78+
6679
# Add a repo-specific variable
6780
depot ci vars add DEPLOY_ENV --repo owner/repo --value "production"`,
68-
Args: cobra.ExactArgs(1),
81+
Args: cobra.MinimumNArgs(1),
6982
RunE: func(cmd *cobra.Command, args []string) error {
7083
ctx := cmd.Context()
71-
varName := args[0]
72-
73-
if varName == "" {
74-
return fmt.Errorf("variable name cannot be empty")
75-
}
7684

7785
if orgID == "" {
7886
orgID = config.GetCurrentOrganization()
@@ -87,6 +95,56 @@ variable applies to all repositories in the organization.`,
8795
return fmt.Errorf("missing API token, please run `depot login`")
8896
}
8997

98+
scope := "org-wide"
99+
if repo != "" {
100+
scope = repo
101+
}
102+
103+
// Detect KEY=VALUE pairs
104+
hasKVPairs := false
105+
for _, arg := range args {
106+
if strings.Contains(arg, "=") {
107+
hasKVPairs = true
108+
break
109+
}
110+
}
111+
112+
if hasKVPairs {
113+
// Bulk mode: all args must be KEY=VALUE
114+
if value != "" {
115+
return fmt.Errorf("cannot use --value with KEY=VALUE arguments")
116+
}
117+
118+
var variables []*civ2.VariableInput
119+
for _, arg := range args {
120+
parts := strings.SplitN(arg, "=", 2)
121+
if len(parts) != 2 || parts[0] == "" {
122+
return fmt.Errorf("invalid argument %q — expected KEY=VALUE format", arg)
123+
}
124+
variables = append(variables, &civ2.VariableInput{Name: parts[0], Value: parts[1]})
125+
}
126+
127+
err := api.CIBatchAddVariables(ctx, tokenVal, orgID, variables, repo)
128+
if err != nil {
129+
return fmt.Errorf("failed to add variables: %w", err)
130+
}
131+
132+
for _, v := range variables {
133+
fmt.Printf("Successfully added CI variable '%s' (%s)\n", v.Name, scope)
134+
}
135+
return nil
136+
}
137+
138+
// Single mode: first arg is variable name
139+
if len(args) > 1 {
140+
return fmt.Errorf("too many arguments — did you mean to use KEY=VALUE format?")
141+
}
142+
143+
varName := args[0]
144+
if varName == "" {
145+
return fmt.Errorf("variable name cannot be empty")
146+
}
147+
90148
varValue := value
91149
if varValue == "" {
92150
varValue, err = helpers.PromptForValue(fmt.Sprintf("Enter value for variable '%s': ", varName))
@@ -100,10 +158,6 @@ variable applies to all repositories in the organization.`,
100158
return fmt.Errorf("failed to add CI variable: %w", err)
101159
}
102160

103-
scope := "org-wide"
104-
if repo != "" {
105-
scope = repo
106-
}
107161
fmt.Printf("Successfully added CI variable '%s' (%s)\n", varName, scope)
108162
return nil
109163
},

0 commit comments

Comments
 (0)