diff --git a/pkg/cmd/secret/set/http.go b/pkg/cmd/secret/set/http.go index 8c589adec..3ecee37c0 100644 --- a/pkg/cmd/secret/set/http.go +++ b/pkg/cmd/secret/set/http.go @@ -90,6 +90,15 @@ func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, return putSecret(client, host, path, payload) } +func putEnvSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, envName string, secretName, eValue string) error { + payload := SecretPayload{ + EncryptedValue: eValue, + KeyID: pk.ID, + } + path := fmt.Sprintf("repos/%s/environments/%s/secrets/%s", ghrepo.FullName(repo), envName, secretName) + return putSecret(client, repo.RepoHost(), path, payload) +} + func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string) error { payload := SecretPayload{ EncryptedValue: eValue, diff --git a/pkg/cmd/secret/set/set.go b/pkg/cmd/secret/set/set.go index cb1975ad3..9bb664e5a 100644 --- a/pkg/cmd/secret/set/set.go +++ b/pkg/cmd/secret/set/set.go @@ -33,6 +33,7 @@ type SetOptions struct { SecretName string OrgName string + EnvName string Body string Visibility string RepositoryNames []string @@ -48,7 +49,7 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command cmd := &cobra.Command{ Use: "set ", Short: "Create or update secrets", - Long: "Locally encrypt a new or updated secret at either the repository or organization level and send it to GitHub for storage.", + Long: "Locally encrypt a new or updated secret at either the repository, environment, or organization level and send it to GitHub for storage.", Example: heredoc.Doc(` Paste secret in prompt $ gh secret set MYSECRET @@ -59,6 +60,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command Use file as secret value $ gh secret set MYSECRET < file.json + Set environment level secret + $ gh secret set MYSECRET -bval --env=anEnv + Set organization level secret visible to entire organization $ gh secret set MYSECRET -bval --org=anOrg --visibility=all @@ -75,6 +79,10 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command // support `-R, --repo` override opts.BaseRepo = f.BaseRepo + if err := cmdutil.MutuallyExclusive("specify only one of `--org` or `--env`", opts.OrgName != "", opts.EnvName != ""); err != nil { + return err + } + opts.SecretName = args[0] err := validSecretName(opts.SecretName) @@ -115,7 +123,8 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command return setRun(opts) }, } - cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "List secrets for an organization") + cmd.Flags().StringVarP(&opts.OrgName, "org", "o", "", "Set a secret for an organization") + cmd.Flags().StringVarP(&opts.EnvName, "env", "e", "", "Set a secret for an organization") cmd.Flags().StringVarP(&opts.Visibility, "visibility", "v", "private", "Set visibility for an organization secret: `all`, `private`, or `selected`") cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of repository names for `selected` visibility") cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "A value for the secret. Reads from STDIN if not specified.") @@ -136,6 +145,7 @@ func setRun(opts *SetOptions) error { client := api.NewClientFromHTTP(c) orgName := opts.OrgName + envName := opts.EnvName var baseRepo ghrepo.Interface if orgName == "" { @@ -175,7 +185,11 @@ func setRun(opts *SetOptions) error { if orgName != "" { err = putOrgSecret(client, host, pk, *opts, encoded) } else { - err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded) + if envName != "" { + err = putEnvSecret(client, pk, baseRepo, envName, opts.SecretName, encoded) + } else { + err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded) + } } if err != nil { return fmt.Errorf("failed to set secret: %w", err) diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go index 7f9273eae..f1c2b7d76 100644 --- a/pkg/cmd/secret/set/set_test.go +++ b/pkg/cmd/secret/set/set_test.go @@ -100,6 +100,17 @@ func TestNewCmdSet(t *testing.T) { OrgName: "", }, }, + { + name: "env", + cli: `cool_secret -b"a secret" -eRelease`, + wants: SetOptions{ + SecretName: "cool_secret", + Visibility: shared.Private, + Body: "a secret", + OrgName: "", + EnvName: "Release", + }, + }, { name: "vis all", cli: `cool_secret --org coolOrg -b"cool" -vall`, @@ -160,6 +171,7 @@ func TestNewCmdSet(t *testing.T) { assert.Equal(t, tt.wants.Body, gotOpts.Body) assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility) assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName) + assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName) assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames) }) } @@ -204,6 +216,46 @@ func Test_setRun_repo(t *testing.T) { assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=") } +func Test_setRun_env(t *testing.T) { + reg := &httpmock.Registry{} + + reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/secrets/public-key"), + httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="})) + + reg.Register(httpmock.REST("PUT", "repos/owner/repo/environments/development/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`)) + + io, _, _, _ := iostreams.Test() + + opts := &SetOptions{ + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + Config: func() (config.Config, error) { return config.NewBlankConfig(), nil }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("owner/repo") + }, + EnvName: "development", + IO: io, + SecretName: "cool_secret", + Body: "a secret", + // Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7 + RandomOverride: bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}), + } + + err := setRun(opts) + assert.NoError(t, err) + + reg.Verify(t) + + data, err := ioutil.ReadAll(reg.Requests[1].Body) + assert.NoError(t, err) + var payload SecretPayload + err = json.Unmarshal(data, &payload) + assert.NoError(t, err) + assert.Equal(t, payload.KeyID, "123") + assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=") +} + func Test_setRun_org(t *testing.T) { tests := []struct { name string