Skip to content

Commit

Permalink
Add GitHub status updates to Pull Request CRD.
Browse files Browse the repository at this point in the history
This adds support for the GitHub Status API
(https://developer.github.com/v3/repos/statuses/).

This accompanies #778 and #895 to complete initial Pull Request support
support for GitHub OAuth.
  • Loading branch information
wlynch committed Jul 2, 2019
1 parent 723b9a9 commit 2a8ec17
Show file tree
Hide file tree
Showing 7 changed files with 451 additions and 42 deletions.
29 changes: 28 additions & 1 deletion cmd/pullrequest-init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ like:
"Text": "my-label"
}
],
"Raw": "/tmp/prtest/github/pr.json"
"Statuses": [
{
"Code": "success",
"Description": "Job succeeded.",
"ID": "pull-tekton-pipeline-go-coverage",
"URL": "https://tekton-releases.appspot.com/build/tekton-prow/pr-logs/pull/tektoncd_pipeline/895/pull-tekton-pipeline-go-coverage/1141483806818045953/"
},
],
"Raw": "/tmp/prtest/github/pr.json",
"RawStatus": "/tmp/pr/github/status.json"
}
```

Expand All @@ -54,6 +63,9 @@ GitHub pull requests will output these additional files:

* `$PATH/github/pr.json`: The raw GitHub payload as specified by
https://developer.github.com/v3/pulls/#get-a-single-pull-request
* `$PATH/github/status.json`: The raw GitHub combined status payload as
specified by
https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
* `$PATH/github/comments/#.json`: Comments associated to the PR as specified
by https://developer.github.com/v3/issues/comments/#get-a-single-comment

Expand All @@ -62,3 +74,18 @@ For now, these files are *read-only*.
The binary will look for GitHub credentials in the `${GITHUBTOKEN}` environment
variable. This should generally be specified as a secret with the field name
`githubToken` in the `PullRequestResource` definition.

### Status code conversion

Tekton Status Code | GitHub Status State
------------------ | -------------------
success | success
neutral | success
queued | pending
in_progress | pending
failure | failure
unknown | error
error | error
timeout | error
canceled | error
action_required | error
51 changes: 51 additions & 0 deletions cmd/pullrequest-init/fake_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type FakeGitHub struct {
// We need to store references to both to emulate the API properly.
prComments map[key][]*github.IssueComment
comments map[key]*github.IssueComment
status map[statusKey]map[string]*github.RepoStatus
}

// NewFakeGitHub returns a new FakeGitHub.
Expand All @@ -44,13 +45,16 @@ func NewFakeGitHub() *FakeGitHub {
pr: make(map[key]*github.PullRequest),
prComments: make(map[key][]*github.IssueComment),
comments: make(map[key]*github.IssueComment),
status: make(map[statusKey]map[string]*github.RepoStatus),
}
s.HandleFunc("/repos/{owner}/{repo}/pulls/{number}", s.getPullRequest).Methods(http.MethodGet)
s.HandleFunc("/repos/{owner}/{repo}/issues/{number}/comments", s.getComments).Methods(http.MethodGet)
s.HandleFunc("/repos/{owner}/{repo}/issues/{number}/comments", s.createComment).Methods(http.MethodPost)
s.HandleFunc("/repos/{owner}/{repo}/issues/comments/{number}", s.updateComment).Methods(http.MethodPatch)
s.HandleFunc("/repos/{owner}/{repo}/issues/comments/{number}", s.deleteComment).Methods(http.MethodDelete)
s.HandleFunc("/repos/{owner}/{repo}/issues/{number}/labels", s.updateLabels).Methods(http.MethodPut)
s.HandleFunc("/repos/{owner}/{repo}/statuses/{revision}", s.createStatus).Methods(http.MethodPost)
s.HandleFunc("/repos/{owner}/{repo}/commits/{revision}/status", s.getStatuses).Methods(http.MethodGet)

return s
}
Expand Down Expand Up @@ -230,3 +234,50 @@ func (g *FakeGitHub) updateLabels(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusOK)
}

type statusKey struct {
owner string
repo string
revision string
}

func getStatusKey(r *http.Request) statusKey {
return statusKey{
owner: mux.Vars(r)["owner"],
repo: mux.Vars(r)["repo"],
revision: mux.Vars(r)["revision"],
}
}

func (g *FakeGitHub) createStatus(w http.ResponseWriter, r *http.Request) {
k := getStatusKey(r)

rs := new(github.RepoStatus)
if err := json.NewDecoder(r.Body).Decode(rs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if _, ok := g.status[k]; !ok {
g.status[k] = make(map[string]*github.RepoStatus)
}
g.status[k][rs.GetContext()] = rs
}

func (g *FakeGitHub) getStatuses(w http.ResponseWriter, r *http.Request) {
k := getStatusKey(r)

s := make([]github.RepoStatus, 0, len(g.status[k]))
for _, v := range g.status[k] {
s = append(s, *v)
}

cs := &github.CombinedStatus{
TotalCount: github.Int(len(s)),
Statuses: s,
}
if err := json.NewEncoder(w).Encode(cs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
35 changes: 35 additions & 0 deletions cmd/pullrequest-init/fake_github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,38 @@ func TestFakeGitHubBadKey(t *testing.T) {
t.Errorf("want BadRequest, got %+v, %v", resp, err)
}
}

func TestFakeGitHubStatus(t *testing.T) {
ctx := context.Background()
gh := NewFakeGitHub()
client, close := githubClient(t, gh)
defer close()

sha := "tacocat"

if got, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, sha, nil); err != nil || resp.StatusCode != http.StatusOK || len(got.Statuses) != 0 {
t.Fatalf("GetCombinedStatus: wanted [], got %+v, %+v, %v", got, resp, err)
}

rs := &github.RepoStatus{
Context: github.String("Tekton"),
Description: github.String("Test all the things!"),
State: github.String("success"),
TargetURL: github.String("https://tekton.dev"),
}
if _, _, err := client.Repositories.CreateStatus(ctx, owner, repo, sha, rs); err != nil {
t.Fatalf("CreateStatus: %v", err)
}

got, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, sha, nil)
if err != nil || resp.StatusCode != http.StatusOK {
t.Fatalf("GetCombinedStatus: wanted OK, got %+v, %v", resp, err)
}
want := &github.CombinedStatus{
TotalCount: github.Int(1),
Statuses: []github.RepoStatus{*rs},
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("GetCombinedStatus: -want +got: %s", diff)
}
}
86 changes: 86 additions & 0 deletions cmd/pullrequest-init/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ import (
"go.uber.org/zap"
)

var (
toGitHub = map[StatusCode]string{
Unknown: "error",
Success: "success",
Failure: "failure",
Error: "error",
// There's no analog for neutral in GitHub statuses, so default to success
// to make this non-blocking.
Neutral: "success",
Queued: "pending",
InProgress: "pending",
Timeout: "error",
Canceled: "error",
ActionRequired: "error",
}
toTekton = map[string]StatusCode{
"success": Success,
"failure": Failure,
"error": Error,
"pending": Queued,
}
)

// GitHubHandler handles interactions with the GitHub API.
type GitHubHandler struct {
*github.Client
Expand Down Expand Up @@ -97,6 +120,14 @@ func (h *GitHubHandler) Download(ctx context.Context, path string) error {
}
pr := baseGitHubPullRequest(gpr)

rawStatus := filepath.Join(rawPrefix, "status.json")
statuses, err := h.getStatuses(ctx, pr.Head.SHA, rawStatus)
if err != nil {
return err
}
pr.RawStatus = rawStatus
pr.Statuses = statuses

rawPR := filepath.Join(rawPrefix, "pr.json")
if err := writeJSON(rawPR, gpr); err != nil {
return err
Expand Down Expand Up @@ -197,6 +228,11 @@ func (h *GitHubHandler) Upload(ctx context.Context, path string) error {
}

var merr error

if err := h.uploadStatuses(ctx, pr.Head.SHA, pr.Statuses); err != nil {
merr = multierror.Append(merr, err)
}

if err := h.uploadLabels(ctx, pr.Labels); err != nil {
merr = multierror.Append(merr, err)
}
Expand Down Expand Up @@ -297,3 +333,53 @@ func (h *GitHubHandler) createNewComments(ctx context.Context, comments []*Comme
}
return merr
}

func (h *GitHubHandler) getStatuses(ctx context.Context, sha string, path string) ([]*Status, error) {
resp, _, err := h.Repositories.GetCombinedStatus(ctx, h.owner, h.repo, sha, nil)
if err != nil {
return nil, err
}
if err := writeJSON(path, resp); err != nil {
return nil, err
}

statuses := make([]*Status, 0, len(resp.Statuses))
for _, s := range resp.Statuses {
code, ok := toTekton[s.GetState()]
if !ok {
return nil, fmt.Errorf("unknown GitHub status state: %s", s.GetState())
}
statuses = append(statuses, &Status{
ID: s.GetContext(),
Code: code,
Description: s.GetDescription(),
URL: s.GetTargetURL(),
})
}
return statuses, nil
}

func (h *GitHubHandler) uploadStatuses(ctx context.Context, sha string, statuses []*Status) error {
var merr error

for _, s := range statuses {
state, ok := toGitHub[s.Code]
if !ok {
merr = multierror.Append(merr, fmt.Errorf("unknown status code %s", s.Code))
continue
}

rs := &github.RepoStatus{
Context: github.String(s.ID),
State: github.String(state),
Description: github.String(s.Description),
TargetURL: github.String(s.URL),
}
if _, _, err := h.Client.Repositories.CreateStatus(ctx, h.owner, h.repo, sha, rs); err != nil {
merr = multierror.Append(merr, err)
continue
}
}

return merr
}
Loading

0 comments on commit 2a8ec17

Please sign in to comment.