-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from shogo82148/implement-minimum-github-api-cl…
…ient implement very light weight GitHub API Client
- Loading branch information
Showing
5 changed files
with
318 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
32
provider/github-app-token/github/testdata/status-created.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |