Skip to content

Commit

Permalink
Implement support for action secrets (#1402)
Browse files Browse the repository at this point in the history
Relates to: #1399.
  • Loading branch information
martinssipenko authored Feb 10, 2020
1 parent 6923131 commit 7d28d53
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 0 deletions.
12 changes: 12 additions & 0 deletions github/actions.go
Original file line number Diff line number Diff line change
@@ -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
132 changes: 132 additions & 0 deletions github/actions_secrets.go
Original file line number Diff line number Diff line change
@@ -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)
}
123 changes: 123 additions & 0 deletions github/actions_secrets_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
16 changes: 16 additions & 0 deletions github/github-accessors.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 7d28d53

Please sign in to comment.