Adding set secrets from env files

Allows to set multiple secrets from an env file.

    gh secret set -f secrets.env

The env file follows a simple format as defined in https://github.com/joho/godotenv

    SPAM=eggs
    FOO="bar"
This commit is contained in:
lpessoa 2021-10-15 13:38:41 +00:00 committed by Mislav Marohnić
parent ca25026613
commit 5589583e4d
4 changed files with 85 additions and 38 deletions

1
go.mod
View file

@ -20,6 +20,7 @@ require (
github.com/hashicorp/go-version v1.3.0
github.com/henvic/httpretty v0.0.6
github.com/itchyny/gojq v0.12.6
github.com/joho/godotenv v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.12
github.com/mattn/go-isatty v0.0.14

2
go.sum
View file

@ -286,6 +286,8 @@ github.com/itchyny/gojq v0.12.6 h1:VjaFn59Em2wTxDNGcrRkDK9ZHMNa8IksOgL13sLL4d0=
github.com/itchyny/gojq v0.12.6/go.mod h1:ZHrkfu7A+RbZLy5J1/JKpS4poEqrzItSTGDItqsfP0A=
github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

View file

@ -82,8 +82,7 @@ func putSecret(client *api.Client, host, path string, payload interface{}) error
return client.REST(host, "PUT", path, requestBody, nil)
}
func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, eValue string) error {
secretName := opts.SecretName
func putOrgSecret(client *api.Client, host string, pk *PubKey, opts SetOptions, secretName string, eValue string) error {
orgName := opts.OrgName
visibility := opts.Visibility

View file

@ -16,6 +16,7 @@ import (
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/prompt"
"github.com/joho/godotenv"
"github.com/spf13/cobra"
"golang.org/x/crypto/nacl/box"
)
@ -36,6 +37,7 @@ type SetOptions struct {
DoNotStore bool
Visibility string
RepositoryNames []string
EnvFile string
}
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
@ -81,6 +83,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
# Set user-level secret for Codespaces
$ gh secret set MYSECRET --user
# Set multiple secrets imported from the ".env" file
$ gh secret set -f .env
`),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
@ -91,8 +96,12 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
return err
}
if err := cmdutil.MutuallyExclusive("specify only one of `--body` or `--env-file`", opts.Body != "", opts.EnvFile != ""); err != nil {
return err
}
if len(args) == 0 {
if !opts.DoNotStore {
if !opts.DoNotStore && opts.EnvFile == "" {
return cmdutil.FlagErrorf("must pass name argument")
}
} else {
@ -136,14 +145,15 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of `repositories` that can access an organization or user secret")
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "The value for the secret (reads from standard input if not specified)")
cmd.Flags().BoolVar(&opts.DoNotStore, "no-store", false, "Print the encrypted, base64-encoded value instead of storing it on Github")
cmd.Flags().StringVarP(&opts.EnvFile, "env-file", "f", "", "Load secret names and values from a dotenv-formatted `file`")
return cmd
}
func setRun(opts *SetOptions) error {
body, err := getBody(opts)
secrets, err := getSecretsFromOptions(opts)
if err != nil {
return fmt.Errorf("did not understand secret body: %w", err)
return err
}
c, err := opts.HttpClient()
@ -187,42 +197,77 @@ func setRun(opts *SetOptions) error {
return fmt.Errorf("failed to fetch public key: %w", err)
}
eBody, err := box.SealAnonymous(nil, body, &pk.Raw, opts.RandomOverride)
if err != nil {
return fmt.Errorf("failed to encrypt body: %w", err)
}
encoded := base64.StdEncoding.EncodeToString(eBody)
if opts.DoNotStore {
_, err := fmt.Fprintln(opts.IO.Out, encoded)
return err
}
if orgName != "" {
err = putOrgSecret(client, host, pk, *opts, encoded)
} else if envName != "" {
err = putEnvSecret(client, pk, baseRepo, envName, opts.SecretName, encoded)
} else if opts.UserSecrets {
err = putUserSecret(client, host, pk, *opts, encoded)
} else {
err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded)
}
if err != nil {
return fmt.Errorf("failed to set secret: %w", err)
}
if opts.IO.IsStdoutTTY() {
target := orgName
if opts.UserSecrets {
target = "your user"
} else if orgName == "" {
target = ghrepo.FullName(baseRepo)
for secretKey, secret := range secrets {
// for env files the randomoverride used in tests needs to be reset on every iteration
if len(opts.EnvFile) > 0 {
if rd, ok := opts.RandomOverride.(io.Seeker); ok {
if _, err := rd.Seek(0, 0); err != nil {
return fmt.Errorf("internal error: %w", err)
}
}
}
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIconWithColor(cs.Green), opts.SecretName, target)
eBody, err := box.SealAnonymous(nil, secret[:], &pk.Raw, opts.RandomOverride)
if err != nil {
return fmt.Errorf("failed to encrypt body: %w", err)
}
encoded := base64.StdEncoding.EncodeToString(eBody)
if opts.DoNotStore {
_, err := fmt.Fprintln(opts.IO.Out, encoded)
return err
}
if orgName != "" {
err = putOrgSecret(client, host, pk, *opts, secretKey, encoded)
} else if envName != "" {
err = putEnvSecret(client, pk, baseRepo, envName, secretKey, encoded)
} else if opts.UserSecrets {
err = putUserSecret(client, host, pk, *opts, encoded)
} else {
err = putRepoSecret(client, pk, baseRepo, secretKey, encoded)
}
if err != nil {
return fmt.Errorf("failed to set secret: %w", err)
}
if opts.IO.IsStdoutTTY() {
target := orgName
if opts.UserSecrets {
target = "your user"
} else if orgName == "" {
target = ghrepo.FullName(baseRepo)
}
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out, "%s Set secret %s for %s\n", cs.SuccessIconWithColor(cs.Green), secretKey, target)
}
}
return nil
}
func getSecretsFromOptions(opts *SetOptions) (map[string][]byte, error) {
secrets := make(map[string][]byte)
if len(opts.EnvFile) > 0 {
envs, err := godotenv.Read(opts.EnvFile)
if err != nil {
return nil, fmt.Errorf("could no open env file: %w", err)
}
for key, value := range envs {
secrets[key] = []byte(value)
}
} else {
body, err := getBody(opts)
if err != nil {
return nil, fmt.Errorf("did not understand secret body: %w", err)
}
secrets[opts.SecretName] = body
}
return nil
if len(secrets) == 0 {
return nil, fmt.Errorf("no secrets defined")
}
return secrets, nil
}
func getBody(opts *SetOptions) ([]byte, error) {