Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for action secrets #1402

Merged
merged 7 commits into from
Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
martinssipenko marked this conversation as resolved.
Show resolved Hide resolved
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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you get a chance, could you please change opt to opts to be consistent with #1417?
(Here and anywhere else you find it in this PR, please.)

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please contact support@github.com and let them know that they shouldn't need pagination for this endpoint, yet their documentation shows that they do? (Or maybe just ask why they do... whichever you prefer.)
And then can you please report their answer back here? They are usually incredibly fast in responding.
Thank you!

Copy link
Contributor Author

@martinssipenko martinssipenko Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reached out to GitHub support, more details below in comment below.

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 {
martinssipenko marked this conversation as resolved.
Show resolved Hide resolved
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