From 7d28d53999240c3f92dfbc40651e970301b9989a Mon Sep 17 00:00:00 2001 From: Martins Sipenko Date: Mon, 10 Feb 2020 05:21:44 +0200 Subject: [PATCH] Implement support for action secrets (#1402) Relates to: #1399. --- github/actions.go | 12 +++ github/actions_secrets.go | 132 +++++++++++++++++++++++++++++++++ github/actions_secrets_test.go | 123 ++++++++++++++++++++++++++++++ github/github-accessors.go | 16 ++++ github/github.go | 2 + 5 files changed, 285 insertions(+) create mode 100644 github/actions.go create mode 100644 github/actions_secrets.go create mode 100644 github/actions_secrets_test.go diff --git a/github/actions.go b/github/actions.go new file mode 100644 index 00000000000..f9f7f8ee867 --- /dev/null +++ b/github/actions.go @@ -0,0 +1,12 @@ +// Copyright 2020 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +// ActionsService handles communication with the actions related +// methods of the GitHub API. +// +// GitHub API docs: https://developer.github.com/v3/actions/ +type ActionsService service diff --git a/github/actions_secrets.go b/github/actions_secrets.go new file mode 100644 index 00000000000..d660663e139 --- /dev/null +++ b/github/actions_secrets.go @@ -0,0 +1,132 @@ +// Copyright 2020 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" +) + +// PublicKey represents the public key that should be used to encrypt secrets. +type PublicKey struct { + KeyID *string `json:"key_id"` + Key *string `json:"key"` +} + +// GetPublicKey gets a public key that should be used for secret encryption. +// +// GitHub API docs: https://developer.github.com/v3/actions/secrets/#get-your-public-key +func (s *ActionsService) GetPublicKey(ctx context.Context, owner, repo string) (*PublicKey, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/actions/secrets/public-key", owner, repo) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + pubKey := new(PublicKey) + resp, err := s.client.Do(ctx, req, pubKey) + if err != nil { + return nil, resp, err + } + + return pubKey, resp, nil +} + +// Secret represents a repository action secret. +type Secret struct { + Name string `json:"name"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` +} + +// Secrets represents one item from the ListSecrets response. +type Secrets struct { + TotalCount int `json:"total_count"` + Secrets []*Secret `json:"secrets"` +} + +// ListSecrets lists all secrets available in a repository +// without revealing their encrypted values. +// +// GitHub API docs: https://developer.github.com/v3/actions/secrets/#list-secrets-for-a-repository +func (s *ActionsService) ListSecrets(ctx context.Context, owner, repo string, opt *ListOptions) (*Secrets, *Response, error) { + u := fmt.Sprintf("repos/%s/%s/actions/secrets", owner, repo) + u, err := addOptions(u, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + secrets := new(Secrets) + resp, err := s.client.Do(ctx, req, &secrets) + if err != nil { + return nil, resp, err + } + + return secrets, resp, nil +} + +// GetSecret gets a single secret without revealing its encrypted value. +// +// GitHub API docs: https://developer.github.com/v3/actions/secrets/#get-a-secret +func (s *ActionsService) GetSecret(ctx context.Context, owner, repo, name string) (*Secret, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/actions/secrets/%v", owner, repo, name) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + secret := new(Secret) + resp, err := s.client.Do(ctx, req, secret) + if err != nil { + return nil, resp, err + } + + return secret, resp, nil +} + +// EncryptedSecret represents a secret that is encrypted using a public key. +// +// The value of EncryptedValue must be your secret, encrypted with +// LibSodium (see documentation here: https://libsodium.gitbook.io/doc/bindings_for_other_languages) +// using the public key retrieved using the GetPublicKey method. +type EncryptedSecret struct { + Name string `json:"-"` + KeyID string `json:"key_id"` + EncryptedValue string `json:"encrypted_value"` +} + +// CreateOrUpdateSecret creates or updates a secret with an encrypted value. +// +// GitHub API docs: https://developer.github.com/v3/actions/secrets/#create-or-update-a-secret-for-a-repository +func (s *ActionsService) CreateOrUpdateSecret(ctx context.Context, owner, repo string, eSecret *EncryptedSecret) (*Response, error) { + u := fmt.Sprintf("repos/%v/%v/actions/secrets/%v", owner, repo, eSecret.Name) + + req, err := s.client.NewRequest("PUT", u, eSecret) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// DeleteSecret deletes a secret in a repository using the secret name. +// +// GitHub API docs: https://developer.github.com/v3/actions/secrets/#delete-a-secret-from-a-repository +func (s *ActionsService) DeleteSecret(ctx context.Context, owner, repo, name string) (*Response, error) { + u := fmt.Sprintf("repos/%v/%v/actions/secrets/%v", owner, repo, name) + + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/github/actions_secrets_test.go b/github/actions_secrets_test.go new file mode 100644 index 00000000000..07f911fd7d8 --- /dev/null +++ b/github/actions_secrets_test.go @@ -0,0 +1,123 @@ +// Copyright 2020 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestActionsService_GetPublicKey(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/secrets/public-key", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"key_id":"1234","key":"2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234"}`) + }) + + key, _, err := client.Actions.GetPublicKey(context.Background(), "o", "r") + if err != nil { + t.Errorf("Actions.GetPublicKey returned error: %v", err) + } + + want := &PublicKey{KeyID: String("1234"), Key: String("2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234")} + if !reflect.DeepEqual(key, want) { + t.Errorf("Actions.GetPublicKey returned %+v, want %+v", key, want) + } +} + +func TestActionsService_ListSecrets(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/secrets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `{"total_count":4,"secrets":[{"name":"A","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"},{"name":"B","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}]}`) + }) + + opt := &ListOptions{Page: 2, PerPage: 2} + secrets, _, err := client.Actions.ListSecrets(context.Background(), "o", "r", opt) + if err != nil { + t.Errorf("Actions.ListSecrets returned error: %v", err) + } + + want := &Secrets{ + TotalCount: 4, + Secrets: []*Secret{ + {Name: "A", CreatedAt: Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + {Name: "B", CreatedAt: Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + }, + } + if !reflect.DeepEqual(secrets, want) { + t.Errorf("Actions.ListSecrets returned %+v, want %+v", secrets, want) + } +} + +func TestActionsService_GetSecret(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/secrets/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"name":"NAME","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}`) + }) + + secret, _, err := client.Actions.GetSecret(context.Background(), "o", "r", "NAME") + if err != nil { + t.Errorf("Actions.GetSecret returned error: %v", err) + } + + want := &Secret{ + Name: "NAME", + CreatedAt: Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, + UpdatedAt: Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}, + } + if !reflect.DeepEqual(secret, want) { + t.Errorf("Actions.GetSecret returned %+v, want %+v", secret, want) + } +} + +func TestActionsService_CreateOrUpdateSecret(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/secrets/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"key_id":"1234","encrypted_value":"QIv="}`+"\n") + w.WriteHeader(http.StatusCreated) + }) + + input := &EncryptedSecret{ + Name: "NAME", + EncryptedValue: "QIv=", + KeyID: "1234", + } + _, err := client.Actions.CreateOrUpdateSecret(context.Background(), "o", "r", input) + if err != nil { + t.Errorf("Actions.CreateOrUpdateSecret returned error: %v", err) + } +} + +func TestActionsService_DeleteSecret(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/secrets/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + _, err := client.Actions.DeleteSecret(context.Background(), "o", "r", "NAME") + if err != nil { + t.Errorf("Actions.DeleteSecret returned error: %v", err) + } +} diff --git a/github/github-accessors.go b/github/github-accessors.go index a17114ec914..212e334b460 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -7932,6 +7932,22 @@ func (p *PublicEvent) GetSender() *User { return p.Sender } +// GetKey returns the Key field if it's non-nil, zero value otherwise. +func (p *PublicKey) GetKey() string { + if p == nil || p.Key == nil { + return "" + } + return *p.Key +} + +// GetKeyID returns the KeyID field if it's non-nil, zero value otherwise. +func (p *PublicKey) GetKeyID() string { + if p == nil || p.KeyID == nil { + return "" + } + return *p.KeyID +} + // GetActiveLockReason returns the ActiveLockReason field if it's non-nil, zero value otherwise. func (p *PullRequest) GetActiveLockReason() string { if p == nil || p.ActiveLockReason == nil { diff --git a/github/github.go b/github/github.go index b1b9afa2ff9..696684b931f 100644 --- a/github/github.go +++ b/github/github.go @@ -159,6 +159,7 @@ type Client struct { common service // Reuse a single struct instead of allocating one for each service on the heap. // Services used for talking to different parts of the GitHub API. + Actions *ActionsService Activity *ActivityService Admin *AdminService Apps *AppsService @@ -264,6 +265,7 @@ func NewClient(httpClient *http.Client) *Client { c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL} c.common.client = c + c.Actions = (*ActionsService)(&c.common) c.Activity = (*ActivityService)(&c.common) c.Admin = (*AdminService)(&c.common) c.Apps = (*AppsService)(&c.common)