implement gh secret create and gh secret list

This commit is contained in:
vilmibm 2020-11-24 12:07:54 -08:00
parent f4152454f2
commit 5309a2089a
9 changed files with 1077 additions and 2 deletions

View file

@ -227,8 +227,8 @@ func createGist(client *http.Client, hostname, description string, public bool,
}
requestBody := bytes.NewReader(requestByte)
apliClient := api.NewClientFromHTTP(client)
err = apliClient.REST(hostname, "POST", path, requestBody, &result)
apiClient := api.NewClientFromHTTP(client)
err = apiClient.REST(hostname, "POST", path, requestBody, &result)
if err != nil {
return nil, err
}

View file

@ -19,6 +19,7 @@ import (
releaseCmd "github.com/cli/cli/pkg/cmd/release"
repoCmd "github.com/cli/cli/pkg/cmd/repo"
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
secretCmd "github.com/cli/cli/pkg/cmd/secret"
versionCmd "github.com/cli/cli/pkg/cmd/version"
"github.com/cli/cli/pkg/cmdutil"
"github.com/spf13/cobra"
@ -74,6 +75,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
cmd.AddCommand(gistCmd.NewCmdGist(f))
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
cmd.AddCommand(secretCmd.NewCmdSecret(f))
// the `api` command should not inherit any extra HTTP headers
bareHTTPCmdFactory := *f

View file

@ -0,0 +1,183 @@
package create
import (
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/secret/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/spf13/cobra"
"golang.org/x/crypto/nacl/box"
)
type CreateOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
RandomOverride io.Reader
SecretName string
OrgName string
Body string
Visibility string
RepositoryNames []string
}
func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
opts := &CreateOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
}
cmd := &cobra.Command{
Use: "create <secret name>",
Short: "Create secrets",
Long: "Locally encrypt a new secret and send it to GitHub for storage.",
Example: heredoc.Doc(`
$ cat SECRET.txt | gh secret create NEW_SECRET
$ gh secret create NEW_SECRET -b"some literal value"
$ gh secret create NEW_SECRET -b"@file.json"
$ gh secret create ORG_SECRET --org
$ gh secret create ORG_SECRET --org=anotherOrg --visibility=selected -r="repo1,repo2,repo3"
$ gh secret create ORG_SECRET --org=anotherOrg --visibility="all"
`),
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return &cmdutil.FlagError{Err: errors.New("must pass single secret name")}
}
if !cmd.Flags().Changed("body") && opts.IO.IsStdinTTY() {
return &cmdutil.FlagError{Err: errors.New("no --body specified but nothing on STIDN")}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
opts.SecretName = args[0]
if cmd.Flags().Changed("visibility") {
if opts.OrgName == "" {
return &cmdutil.FlagError{Err: errors.New(
"--visibility not supported for repository secrets; did you mean to pass --org?")}
}
if opts.Visibility != shared.VisAll && opts.Visibility != shared.VisPrivate && opts.Visibility != shared.VisSelected {
return &cmdutil.FlagError{Err: errors.New(
"--visibility must be one of `all`, `private`, or `selected`")}
}
}
if cmd.Flags().Changed("repos") && opts.Visibility != shared.VisSelected {
return &cmdutil.FlagError{Err: errors.New(
"--repos only supported when --visibility='selected'")}
}
if opts.Visibility == shared.VisSelected && len(opts.RepositoryNames) == 0 {
return &cmdutil.FlagError{Err: errors.New(
"--repos flag required when --visibility='selected'")}
}
if runF != nil {
return runF(opts)
}
return createRun(opts)
},
}
cmd.Flags().StringVar(&opts.OrgName, "org", "", "List secrets for an organization")
cmd.Flags().Lookup("org").NoOptDefVal = "@owner"
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", "-", "Provide either a literal string or a file path; prepend file paths with an @. Reads from STDIN if not provided.")
return cmd
}
func createRun(opts *CreateOptions) error {
body, err := getBody(opts)
if err != nil {
return fmt.Errorf("did not understand secret body: %w", err)
}
c, err := opts.HttpClient()
if err != nil {
return fmt.Errorf("could not create http client: %w", err)
}
client := api.NewClientFromHTTP(c)
var baseRepo ghrepo.Interface
if opts.OrgName == "" || opts.OrgName == "@owner" {
baseRepo, err = opts.BaseRepo()
if err != nil {
return fmt.Errorf("could not determine base repo: %w", err)
}
}
host := ghinstance.OverridableDefault()
if opts.OrgName == "@owner" {
opts.OrgName = baseRepo.RepoOwner()
host = baseRepo.RepoHost()
}
var pk *PubKey
if opts.OrgName != "" {
pk, err = getOrgPublicKey(client, host, opts.OrgName)
} else {
pk, err = getRepoPubKey(client, baseRepo)
}
if err != nil {
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.OrgName != "" {
err = putOrgSecret(client, pk, host, *opts, encoded)
} else {
err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded)
}
if err != nil {
return fmt.Errorf("failed to create secret: %w", err)
}
return nil
}
func getBody(opts *CreateOptions) (body []byte, err error) {
if opts.Body == "-" {
body, err = ioutil.ReadAll(opts.IO.In)
if err != nil {
return nil, fmt.Errorf("failed to read from STDIN: %w", err)
}
return
}
if strings.HasPrefix(opts.Body, "@") {
body, err = opts.IO.ReadUserFile(opts.Body[1:])
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", opts.Body[1:], err)
}
return
}
return []byte(opts.Body), nil
}

View file

@ -0,0 +1,334 @@
package create
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/secret/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdCreate(t *testing.T) {
tests := []struct {
name string
cli string
wants CreateOptions
stdinTTY bool
wantsErr bool
}{
{
name: "invalid visibility",
cli: "cool_secret --org -v'mistyVeil'",
wantsErr: true,
},
{
name: "invalid visibility",
cli: "cool_secret --org -v'selected'",
wantsErr: true,
},
{
name: "no name",
cli: "",
wantsErr: true,
},
{
name: "multiple names",
cli: "cool_secret good_secret",
wantsErr: true,
},
{
name: "no body, stdin is terminal",
cli: "cool_secret",
stdinTTY: true,
wantsErr: true,
},
{
name: "visibility without org",
cli: "cool_secret -vall",
wantsErr: true,
},
{
name: "explicit org with selected repo",
cli: "--org=coolOrg -vselected -rcoolRepo cool_secret",
wants: CreateOptions{
SecretName: "cool_secret",
Visibility: shared.VisSelected,
RepositoryNames: []string{"coolRepo"},
Body: "-",
OrgName: "coolOrg",
},
},
{
name: "explicit org with selected repos",
cli: `--org=coolOrg -vselected -r="coolRepo,radRepo,goodRepo" cool_secret`,
wants: CreateOptions{
SecretName: "cool_secret",
Visibility: shared.VisSelected,
RepositoryNames: []string{"coolRepo", "goodRepo", "radRepo"},
Body: "-",
OrgName: "coolOrg",
},
},
{
name: "repo",
cli: `cool_secret -b"a secret"`,
wants: CreateOptions{
SecretName: "cool_secret",
Visibility: shared.VisPrivate,
Body: "a secret",
OrgName: "",
},
},
{
name: "implicit org",
cli: `cool_secret --org -b"@cool.json"`,
wants: CreateOptions{
SecretName: "cool_secret",
Visibility: shared.VisPrivate,
Body: "@cool.json",
OrgName: "@owner",
},
},
{
name: "vis all",
cli: `cool_secret --org -b"@cool.json" -vall`,
wants: CreateOptions{
SecretName: "cool_secret",
Visibility: shared.VisAll,
Body: "@cool.json",
OrgName: "@owner",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
}
io.SetStdinTTY(tt.stdinTTY)
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
var gotOpts *CreateOptions
cmd := NewCmdCreate(f, func(opts *CreateOptions) error {
gotOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wants.SecretName, gotOpts.SecretName)
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.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames)
})
}
}
func Test_createRun_repo(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/actions/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`))
mockClient := func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
io, _, _, _ := iostreams.Test()
opts := &CreateOptions{
BaseRepo: func() (ghrepo.Interface, error) {
return ghrepo.FromFullName("owner/repo")
},
HttpClient: mockClient,
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 := createRun(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_createRun_org(t *testing.T) {
tests := []struct {
name string
opts *CreateOptions
wantVisibility string
wantRepositories []int
}{
{
name: "explicit org name",
opts: &CreateOptions{
OrgName: "UmbrellaCorporation",
Visibility: shared.VisAll,
},
},
{
name: "implicit org name",
opts: &CreateOptions{
OrgName: "@owner",
Visibility: shared.VisPrivate,
},
},
{
name: "selected visibility",
opts: &CreateOptions{
OrgName: "UmbrellaCorporation",
Visibility: shared.VisSelected,
RepositoryNames: []string{"birkin", "wesker"},
},
wantRepositories: []int{1, 2},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
orgName := tt.opts.OrgName
if orgName == "@owner" {
orgName = "NeoUmbrella"
}
reg.Register(httpmock.REST("GET",
fmt.Sprintf("orgs/%s/actions/secrets/public-key", orgName)),
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
reg.Register(httpmock.REST("PUT",
fmt.Sprintf("orgs/%s/actions/secrets/cool_secret", orgName)),
httpmock.StatusStringResponse(201, `{}`))
if len(tt.opts.RepositoryNames) > 0 {
reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
httpmock.StringResponse(`{"data":{"birkin":{"databaseId":1},"wesker":{"databaseId":2}}}`))
}
io, _, _, _ := iostreams.Test()
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.FromFullName("NeoUmbrella/repo")
}
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.IO = io
tt.opts.SecretName = "cool_secret"
tt.opts.Body = "a secret"
// Cribbed from https://github.com/golang/crypto/commit/becbf705a91575484002d598f87d74f0002801e7
tt.opts.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 := createRun(tt.opts)
assert.NoError(t, err)
reg.Verify(t)
data, err := ioutil.ReadAll(reg.Requests[len(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=")
assert.Equal(t, payload.Visibility, tt.opts.Visibility)
assert.ElementsMatch(t, payload.Repositories, tt.wantRepositories)
})
}
}
func Test_getBody(t *testing.T) {
tests := []struct {
name string
bodyArg string
want string
stdin string
fromFile bool
}{
{
name: "literal value",
bodyArg: "a secret",
want: "a secret",
},
{
name: "from stdin",
bodyArg: "-",
want: "a secret",
stdin: "a secret",
},
{
name: "from file",
fromFile: true,
want: "a secret from a file",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, stdin, _, _ := iostreams.Test()
io.SetStdinTTY(false)
_, err := stdin.WriteString(tt.stdin)
assert.NoError(t, err)
if tt.fromFile {
dir := os.TempDir()
tmpfile, err := ioutil.TempFile(dir, "testfile*")
assert.NoError(t, err)
_, err = tmpfile.WriteString(tt.want)
assert.NoError(t, err)
tt.bodyArg = fmt.Sprintf("@%s", tmpfile.Name())
}
body, err := getBody(&CreateOptions{
Body: tt.bodyArg,
IO: io,
})
assert.NoError(t, err)
assert.Equal(t, string(body), tt.want)
})
}
}

View file

@ -0,0 +1,138 @@
package create
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/secret/shared"
)
type SecretPayload struct {
EncryptedValue string `json:"encrypted_value"`
Visibility string `json:"visibility,omitempty"`
Repositories []int `json:"selected_repository_ids,omitempty"`
KeyID string `json:"key_id"`
}
type PubKey struct {
Raw [32]byte
ID string `json:"key_id"`
Key string
}
func getPubKey(client *api.Client, host, path string) (*PubKey, error) {
pk := PubKey{}
err := client.REST(host, "GET", path, nil, &pk)
if err != nil {
return nil, err
}
if pk.Key == "" {
return nil, fmt.Errorf("failed to find public key at %s/%s", host, path)
}
decoded, err := base64.StdEncoding.DecodeString(pk.Key)
if err != nil {
return nil, fmt.Errorf("failed to decode public key: %w", err)
}
copy(pk.Raw[:], decoded[0:32])
return &pk, nil
}
func getOrgPublicKey(client *api.Client, host, orgName string) (*PubKey, error) {
return getPubKey(client, host, fmt.Sprintf("orgs/%s/actions/secrets/public-key", orgName))
}
func getRepoPubKey(client *api.Client, repo ghrepo.Interface) (*PubKey, error) {
return getPubKey(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets/public-key",
ghrepo.FullName(repo)))
}
func putSecret(client *api.Client, host, path string, payload SecretPayload) error {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed to serialize: %w", err)
}
requestBody := bytes.NewReader(payloadBytes)
return client.REST(host, "PUT", path, requestBody, nil)
}
func putOrgSecret(client *api.Client, pk *PubKey, host string, opts CreateOptions, eValue string) error {
secretName := opts.SecretName
orgName := opts.OrgName
visibility := opts.Visibility
var repositoryIDs []int
var err error
if orgName != "" && visibility == shared.VisSelected {
repositoryIDs, err = mapRepoNameToID(client, host, orgName, opts.RepositoryNames)
if err != nil {
return fmt.Errorf("failed to look up IDs for repositories %v: %w", opts.RepositoryNames, err)
}
}
payload := SecretPayload{
EncryptedValue: eValue,
KeyID: pk.ID,
Repositories: repositoryIDs,
Visibility: visibility,
}
path := fmt.Sprintf("orgs/%s/actions/secrets/%s", orgName, secretName)
return putSecret(client, host, path, payload)
}
func putRepoSecret(client *api.Client, pk *PubKey, repo ghrepo.Interface, secretName, eValue string) error {
payload := SecretPayload{
EncryptedValue: eValue,
KeyID: pk.ID,
}
path := fmt.Sprintf("repos/%s/actions/secrets/%s", ghrepo.FullName(repo), secretName)
return putSecret(client, repo.RepoHost(), path, payload)
}
func mapRepoNameToID(client *api.Client, host, orgName string, repositoryNames []string) ([]int, error) {
queries := make([]string, 0, len(repositoryNames))
for _, repoName := range repositoryNames {
queries = append(queries, fmt.Sprintf(`
%s: repository(owner: %q, name :%q) {
databaseId
}
`, repoName, orgName, repoName))
}
query := fmt.Sprintf(`query MapRepositoryNames { %s }`, strings.Join(queries, ""))
graphqlResult := make(map[string]*struct {
DatabaseID int `json:"databaseId"`
})
err := client.GraphQL(host, query, nil, &graphqlResult)
gqlErr, isGqlErr := err.(*api.GraphQLErrorResponse)
if isGqlErr {
for _, ge := range gqlErr.Errors {
if ge.Type == "NOT_FOUND" {
return nil, fmt.Errorf("could not find %s/%s", orgName, ge.Path[0])
}
}
}
if err != nil {
return nil, fmt.Errorf("failed to look up repositories: %w", err)
}
result := make([]int, 0, len(repositoryNames))
for _, repoName := range repositoryNames {
result = append(result, graphqlResult[repoName].DatabaseID)
}
return result, nil
}

156
pkg/cmd/secret/list/list.go Normal file
View file

@ -0,0 +1,156 @@
package list
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/secret/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)
type ListOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
OrgName string
}
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
opts := &ListOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
}
cmd := &cobra.Command{
Use: "list",
Short: "List secrets",
Long: "List secrets for a repository or organization",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo
if runF != nil {
return runF(opts)
}
return listRun(opts)
},
}
cmd.Flags().StringVar(&opts.OrgName, "org", "", "List secrets for an organization")
cmd.Flags().Lookup("org").NoOptDefVal = "@owner"
return cmd
}
func listRun(opts *ListOptions) error {
c, err := opts.HttpClient()
if err != nil {
return fmt.Errorf("could not create http client: %w", err)
}
client := api.NewClientFromHTTP(c)
var baseRepo ghrepo.Interface
if opts.OrgName == "" || opts.OrgName == "@owner" {
baseRepo, err = opts.BaseRepo()
if err != nil {
return fmt.Errorf("could not determine base repo: %w", err)
}
}
orgName := opts.OrgName
host := ghinstance.OverridableDefault()
if orgName == "@owner" {
orgName = baseRepo.RepoOwner()
host = baseRepo.RepoHost()
}
var secrets []Secret
if orgName != "" {
secrets, err = getOrgSecrets(client, host, orgName)
} else {
secrets, err = getRepoSecrets(client, baseRepo)
}
if err != nil {
return fmt.Errorf("failed to get secrets: %w", err)
}
tp := utils.NewTablePrinter(opts.IO)
for _, secret := range secrets {
tp.AddField(secret.Name, nil, nil)
updatedAt := secret.UpdatedAt.Format("2006-01-02")
if opts.IO.IsStdoutTTY() {
updatedAt = fmt.Sprintf("Updated %s", updatedAt)
}
tp.AddField(updatedAt, nil, nil)
if secret.Visibility != "" {
if opts.IO.IsStdoutTTY() {
tp.AddField(fmtVisibility(secret), nil, nil)
} else {
tp.AddField(strings.ToUpper(secret.Visibility), nil, nil)
}
}
tp.EndRow()
}
err = tp.Render()
if err != nil {
return err
}
return nil
}
type Secret struct {
Name string
UpdatedAt time.Time `json:"updated_at"`
Visibility string
}
func fmtVisibility(s Secret) string {
switch s.Visibility {
case shared.VisAll:
return "Visible to all repositories"
case shared.VisPrivate:
return "Visible to private repositories"
case shared.VisSelected:
// TODO print how many? print which ones?
return "Visible to selected repositories"
}
return ""
}
func getOrgSecrets(client *api.Client, host, orgName string) ([]Secret, error) {
return getSecrets(client, host, fmt.Sprintf("orgs/%s/actions/secrets", orgName))
}
func getRepoSecrets(client *api.Client, repo ghrepo.Interface) ([]Secret, error) {
return getSecrets(client, repo.RepoHost(), fmt.Sprintf("repos/%s/actions/secrets",
ghrepo.FullName(repo)))
}
type secretsPayload struct {
Secrets []Secret
}
func getSecrets(client *api.Client, host, path string) ([]Secret, error) {
result := secretsPayload{}
err := client.REST(host, "GET", path, nil, &result)
if err != nil {
return nil, err
}
return result.Secrets, nil
}

View file

@ -0,0 +1,227 @@
package list
import (
"bytes"
"fmt"
"net/http"
"testing"
"time"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/secret/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/httpmock"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/test"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func Test_NewCmdList(t *testing.T) {
tests := []struct {
name string
cli string
wants ListOptions
}{
{
name: "repo",
cli: "",
wants: ListOptions{
OrgName: "",
},
},
{
name: "implicit org",
cli: "--org",
wants: ListOptions{
OrgName: "@owner",
},
},
{
name: "explicit org",
cli: "--org=UmbrellaCorporation",
wants: ListOptions{
OrgName: "UmbrellaCorporation",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
}
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
var gotOpts *ListOptions
cmd := NewCmdList(f, func(opts *ListOptions) error {
gotOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
assert.NoError(t, err)
assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
})
}
}
// TODO run tests
func Test_listRun(t *testing.T) {
tests := []struct {
name string
tty bool
opts *ListOptions
wantOut []string
}{
{
name: "repo tty",
tty: true,
opts: &ListOptions{},
wantOut: []string{
"SECRET_ONE.*Updated 1988-10-11",
"SECRET_TWO.*Updated 2020-12-04",
"SECRET_THREE.*Updated 1975-11-30",
},
},
{
name: "repo not tty",
tty: false,
opts: &ListOptions{},
wantOut: []string{
"SECRET_ONE\t1988-10-11",
"SECRET_TWO\t2020-12-04",
"SECRET_THREE\t1975-11-30",
},
},
{
name: "explicit org tty",
tty: true,
opts: &ListOptions{
OrgName: "UmbrellaCorporation",
},
wantOut: []string{
"SECRET_ONE.*Updated 1988-10-11.*Visible to all repositories",
"SECRET_TWO.*Updated 2020-12-04.*Visible to private repositories",
"SECRET_THREE.*Updated 1975-11-30.*Visible to selected repositories",
},
},
{
name: "explicit org not tty",
tty: false,
opts: &ListOptions{
OrgName: "UmbrellaCorporation",
},
wantOut: []string{
"SECRET_ONE\t1988-10-11\tALL",
"SECRET_TWO\t2020-12-04\tPRIVATE",
"SECRET_THREE\t1975-11-30\tSELECTED",
},
},
{
name: "implicit org not tty",
tty: false,
opts: &ListOptions{
OrgName: "@owner",
},
wantOut: []string{
"SECRET_ONE\t1988-10-11\tALL",
"SECRET_TWO\t2020-12-04\tPRIVATE",
"SECRET_THREE\t1975-11-30\tSELECTED",
},
},
{
name: "implicit org not tty",
tty: true,
opts: &ListOptions{
OrgName: "@owner",
},
wantOut: []string{
"SECRET_ONE.*Updated 1988-10-11.*Visible to all repositories",
"SECRET_TWO.*Updated 2020-12-04.*Visible to private repositories",
"SECRET_THREE.*Updated 1975-11-30.*Visible to selected repositories",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
t0, _ := time.Parse("2006-01-02", "1988-10-11")
t1, _ := time.Parse("2006-01-02", "2020-12-04")
t2, _ := time.Parse("2006-01-02", "1975-11-30")
path := "repos/owner/repo/actions/secrets"
payload := secretsPayload{}
payload.Secrets = []Secret{
{
Name: "SECRET_ONE",
UpdatedAt: t0,
},
{
Name: "SECRET_TWO",
UpdatedAt: t1,
},
{
Name: "SECRET_THREE",
UpdatedAt: t2,
},
}
if tt.opts.OrgName != "" {
payload.Secrets = []Secret{
{
Name: "SECRET_ONE",
UpdatedAt: t0,
Visibility: shared.VisAll,
},
{
Name: "SECRET_TWO",
UpdatedAt: t1,
Visibility: shared.VisPrivate,
},
{
Name: "SECRET_THREE",
UpdatedAt: t2,
Visibility: shared.VisSelected,
},
}
if tt.opts.OrgName == "@owner" {
path = "orgs/owner/actions/secrets"
} else {
path = fmt.Sprintf("orgs/%s/actions/secrets", tt.opts.OrgName)
}
}
reg.Register(httpmock.REST("GET", path), httpmock.JSONResponse(payload))
io, _, stdout, _ := iostreams.Test()
io.SetStdoutTTY(tt.tty)
tt.opts.IO = io
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.FromFullName("owner/repo")
}
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
err := listRun(tt.opts)
assert.NoError(t, err)
reg.Verify(t)
test.ExpectLines(t, stdout.String(), tt.wantOut...)
})
}
}

28
pkg/cmd/secret/secret.go Normal file
View file

@ -0,0 +1,28 @@
package secret
import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/pkg/cmdutil"
"github.com/spf13/cobra"
cmdCreate "github.com/cli/cli/pkg/cmd/secret/create"
cmdList "github.com/cli/cli/pkg/cmd/secret/list"
)
func NewCmdSecret(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "secret <command>",
Short: "Manage GitHub secrets",
Long: heredoc.Doc(`
Secrets can be set at the repository or organization level for use in GitHub Actions.
Run "gh help secret add" to learn how to get started.
`),
}
cmdutil.EnableRepoOverride(cmd, f)
cmd.AddCommand(cmdList.NewCmdList(f, nil))
cmd.AddCommand(cmdCreate.NewCmdCreate(f, nil))
return cmd
}

View file

@ -0,0 +1,7 @@
package shared
const (
VisAll = "all"
VisPrivate = "private"
VisSelected = "selected"
)