Skip to content

Commit

Permalink
Merge pull request #4 from shogo82148/implement-minimum-github-api-cl…
Browse files Browse the repository at this point in the history
…ient

implement very light weight GitHub API Client
  • Loading branch information
shogo82148 authored Aug 31, 2021
2 parents 4242d56 + f8b0c6e commit 0f8e530
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 0 deletions.
73 changes: 73 additions & 0 deletions provider/github-app-token/github/create_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package github

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
)

type CommitState string

const (
CommitStateError CommitState = "error"
CommitStateFailure CommitState = "failure"
CommitStatePending CommitState = "pending"
CommitStateSuccess CommitState = "success"
)

type CreateStatusRequest struct {
State CommitState `json:"state"`
TargetURL string `json:"target_url,omitempty"`
Description string `json:"description,omitempty"`
Context string `json:"context,omitempty"`
}

type CreateStatusResponse struct {
Creator *CreateStatusResponseCreator `json:"creator"`
// omit other fields, we don't use them.
}

type CreateStatusResponseCreator struct {
Login string `json:"login"`
ID int64 `json:"id"`
Type string `json:"type"`
// omit other fields, we don't use them.
}

// CreateStatus creates a commit status.
// https://docs.github.com/en/rest/reference/repos#create-a-commit-status
func (c *Client) CreateStatus(ctx context.Context, token, owner, repo, ref string, status *CreateStatusRequest) (*CreateStatusResponse, error) {
// build the request
u := fmt.Sprintf("%s/repos/%s/%s/statuses/%s", c.baseURL, owner, repo, ref)
body, err := json.Marshal(status)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("User-Agent", githubUserAgent)
req.Header.Set("Authorization", "token "+token)

// send the request
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// parse the response
if resp.StatusCode != http.StatusCreated {
return nil, &UnexpectedStatusCodeError{StatusCode: resp.StatusCode}
}

var ret *CreateStatusResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return ret, nil
}
51 changes: 51 additions & 0 deletions provider/github-app-token/github/create_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package github

import (
"context"
"net/http"
"net/http/httptest"
"os"
"strconv"
"testing"
)

func TestCreateStatus(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("unexpected method: want POST, got %s", r.Method)
}
path := "/repos/shogo82148/actions-aws-assume-role/statuses/496f02e29cc5760443becd7007049c1a2a502b6f"
if r.URL.Path != path {
t.Errorf("unexpected path: want %q, got %q", path, r.URL.Path)
}

data, err := os.ReadFile("testdata/status-created.json")
if err != nil {
panic(err)
}
rw.Header().Set("Content-Type", "application/json")
rw.Header().Set("Content-Length", strconv.Itoa(len(data)))
rw.WriteHeader(http.StatusCreated)
rw.Write(data)
}))
defer ts.Close()
c := NewClient(nil)
c.baseURL = ts.URL

resp, err := c.CreateStatus(context.Background(), "dummy-auth-token", "shogo82148", "actions-aws-assume-role", "496f02e29cc5760443becd7007049c1a2a502b6f", &CreateStatusRequest{
State: CommitStateSuccess,
Context: "actions-aws-assume-role",
})
if err != nil {
t.Fatal(err)
}
if resp.Creator.ID != 1157344 {
t.Errorf("unexpected creator id: want %d, got %d", 1157344, resp.Creator.ID)
}
if resp.Creator.Login != "shogo82148" {
t.Errorf("unexpected creator login: want %q, got %q", "shogo82148", resp.Creator.Login)
}
if resp.Creator.Type != "User" {
t.Errorf("unexpected creator type: want %q, got %q", "User", resp.Creator.Type)
}
}
97 changes: 97 additions & 0 deletions provider/github-app-token/github/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package github

import (
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
)

const (
githubUserAgent = "actions-aws-assume-role/1.0"
defaultAPIBaseURL = "https://api.github.com"
)

var apiBaseURL string

func init() {
u := os.Getenv("GITHUB_API_URL")
if u == "" {
u = defaultAPIBaseURL
}

var err error
apiBaseURL, err = canonicalURL(u)
if err != nil {
panic(err)
}
}

// Client is a very light weight GitHub API Client.
type Client struct {
baseURL string
httpClient *http.Client
}

func NewClient(httpClient *http.Client) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{
baseURL: apiBaseURL,
httpClient: httpClient,
}
}

type UnexpectedStatusCodeError struct {
StatusCode int
}

func (err *UnexpectedStatusCodeError) Error() string {
return fmt.Sprintf("unexpected status code: %d", err.StatusCode)
}

func canonicalURL(rawurl string) (string, error) {
u, err := url.Parse(rawurl)
if err != nil {
return "", err
}

host := u.Hostname()
port := u.Port()

// host is case insensitive.
host = strings.ToLower(host)

// remove trailing slashes.
u.Path = strings.TrimRight(u.Path, "/")

// omit the default port number.
defaultPort := "80"
switch u.Scheme {
case "http":
case "https":
defaultPort = "443"
case "":
u.Scheme = "http"
default:
return "", fmt.Errorf("unknown scheme: %s", u.Scheme)
}
if port == defaultPort {
port = ""
}

if port == "" {
u.Host = host
} else {
u.Host = net.JoinHostPort(host, port)
}

// we don't use query and fragment, so drop them.
u.RawFragment = ""
u.RawQuery = ""

return u.String(), nil
}
65 changes: 65 additions & 0 deletions provider/github-app-token/github/github_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package github

import "testing"

func TestCanonicalURL(t *testing.T) {
cases := []struct {
input string
want string
}{
{
input: "https://api.github.com",
want: "https://api.github.com",
},
{
input: "https://API.GITHUB.COM",
want: "https://api.github.com",
},
{
input: "https://api.github.com/",
want: "https://api.github.com",
},
{
input: "http://example.com/API",
want: "http://example.com/API",
},
{
input: "http://example.com/api/",
want: "http://example.com/api",
},
{
input: "example.com/api",
want: "http://example.com/api",
},
{
input: "http://example.com:80/api",
want: "http://example.com/api",
},
{
input: "https://example.com:443/api",
want: "https://example.com/api",
},
{
input: "http://example.com:443/api",
want: "http://example.com:443/api",
},
{
input: "https://example.com:80/api",
want: "https://example.com:80/api",
},
{
input: "https://[::1]:8080/api",
want: "https://[::1]:8080/api",
},
}
for i, c := range cases {
got, err := canonicalURL(c.input)
if err != nil {
t.Errorf("%d: canonicalURL(%q) returns error: %v", i, c.input, err)
continue
}
if got != c.want {
t.Errorf("%d: canonicalURL(%q) should be %q, but got %q", i, c.input, c.want, got)
}
}
}
32 changes: 32 additions & 0 deletions provider/github-app-token/github/testdata/status-created.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"url": "https://api.github.com/repos/shogo82148/actions-aws-assume-role/statuses/496f02e29cc5760443becd7007049c1a2a502b6f",
"avatar_url": "https://avatars.githubusercontent.com/u/1157344?v=4",
"id": 12538509808,
"node_id": "MDEzOlN0YXR1c0NvbnRleHQxMjUzODUwOTgwOA==",
"state": "success",
"description": null,
"target_url": null,
"context": "actions-aws-assume-role",
"created_at": "2021-03-19T13:16:55Z",
"updated_at": "2021-03-19T13:16:55Z",
"creator": {
"login": "shogo82148",
"id": 1157344,
"node_id": "MDQ6VXNlcjExNTczNDQ=",
"avatar_url": "https://avatars.githubusercontent.com/u/1157344?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/shogo82148",
"html_url": "https://github.com/shogo82148",
"followers_url": "https://api.github.com/users/shogo82148/followers",
"following_url": "https://api.github.com/users/shogo82148/following{/other_user}",
"gists_url": "https://api.github.com/users/shogo82148/gists{/gist_id}",
"starred_url": "https://api.github.com/users/shogo82148/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/shogo82148/subscriptions",
"organizations_url": "https://api.github.com/users/shogo82148/orgs",
"repos_url": "https://api.github.com/users/shogo82148/repos",
"events_url": "https://api.github.com/users/shogo82148/events{/privacy}",
"received_events_url": "https://api.github.com/users/shogo82148/received_events",
"type": "User",
"site_admin": false
}
}

0 comments on commit 0f8e530

Please sign in to comment.