Skip to content

Commit

Permalink
[WENGINES-4505] Github Client Checks Implementation (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aayyush authored May 26, 2022
1 parent cda21bc commit 4f72f23
Show file tree
Hide file tree
Showing 7 changed files with 411 additions and 17 deletions.
5 changes: 3 additions & 2 deletions server/controllers/events/handlers/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ type asyncHandler struct {

func (h *asyncHandler) Handle(ctx context.Context, request *http.BufferedRequest, event event_types.Comment, command *command.Comment) error {
go func() {
err := h.commandHandler.Handle(ctx, request, event, command)
// Passing background context to avoid context cancellation since the parent goroutine does not wait for this goroutine to finish execution.
err := h.commandHandler.Handle(context.Background(), request, event, command)

if err != nil {
h.logger.ErrorContext(ctx, err.Error())
h.logger.ErrorContext(context.Background(), err.Error())
}
}()
return nil
Expand Down
5 changes: 3 additions & 2 deletions server/controllers/events/handlers/pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ type asyncAutoplanner struct {

func (p *asyncAutoplanner) Handle(ctx context.Context, request *http.BufferedRequest, event event_types.PullRequest) error {
go func() {
err := p.autoplanner.Handle(ctx, request, event)
// Passing background context to avoid context cancellation since the parent goroutine does not wait for this goroutine to finish execution.
err := p.autoplanner.Handle(context.Background(), request, event)

if err != nil {
p.logger.ErrorContext(ctx, err.Error())
p.logger.ErrorContext(context.Background(), err.Error())
}
}()
return nil
Expand Down
37 changes: 36 additions & 1 deletion server/events/output_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package events

import (
"fmt"
"strings"

"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
Expand Down Expand Up @@ -49,19 +50,53 @@ type ChecksOutputUpdater struct {

func (c *ChecksOutputUpdater) UpdateOutput(ctx *command.Context, cmd PullCommand, res command.Result) {

if res.Error != nil || res.Failure != "" {
output := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo)
updateStatusReq := types.UpdateStatusRequest{
Repo: ctx.HeadRepo,
Ref: ctx.Pull.HeadCommit,
StatusName: c.TitleBuilder.Build(cmd.CommandName().String()),
PullNum: ctx.Pull.Num,
Description: fmt.Sprintf("%s failed", strings.Title(cmd.CommandName().String())),
Output: output,
State: models.FailedCommitStatus,
}

if err := c.VCSClient.UpdateStatus(ctx.RequestCtx, updateStatusReq); err != nil {
ctx.Log.Error("updable to update check run", map[string]interface{}{
"error": err.Error(),
})
}
return
}

// iterate through all project results and the update the github check
for _, projectResult := range res.ProjectResults {
statusName := c.TitleBuilder.Build(cmd.CommandName().String(), vcs.StatusTitleOptions{
ProjectName: projectResult.ProjectName,
})

// Description is a required field
var description string
var state models.CommitStatus
if projectResult.Error != nil || projectResult.Failure != "" {
description = fmt.Sprintf("%s failed for %s", strings.Title(projectResult.Command.String()), projectResult.ProjectName)
state = models.FailedCommitStatus
} else {
description = fmt.Sprintf("%s succeeded for %s", strings.Title(projectResult.Command.String()), projectResult.ProjectName)
state = models.SuccessCommitStatus
}

// TODO: Make the mark down rendered project specific
output := c.MarkdownRenderer.Render(res, cmd.CommandName(), ctx.Pull.BaseRepo)
updateStatusReq := types.UpdateStatusRequest{
Repo: ctx.HeadRepo,
Ref: ctx.Pull.HeadCommit,
StatusName: statusName,
PullNum: ctx.Pull.Num,
Description: output,
Description: description,
Output: output,
State: state,
}

if err := c.VCSClient.UpdateStatus(ctx.RequestCtx, updateStatusReq); err != nil {
Expand Down
148 changes: 137 additions & 11 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,57 @@ import (
"github.com/shurcooL/githubv4"
)

// github checks conclusion
type ChecksConclusion int

const (
Neutral ChecksConclusion = iota
TimedOut
ActionRequired
Cancelled
Failure
Success
)

func (e ChecksConclusion) String() string {
switch e {
case Neutral:
return "neutral"
case TimedOut:
return "timed_out"
case ActionRequired:
return "action_required"
case Cancelled:
return "cancelled"
case Failure:
return "failure"
case Success:
return "success"
}
return ""
}

// github checks status
type CheckStatus int

const (
Queued CheckStatus = iota
InProgress
Completed
)

func (e CheckStatus) String() string {
switch e {
case Queued:
return "queued"
case InProgress:
return "in_progress"
case Completed:
return "completed"
}
return ""
}

// maxCommentLength is the maximum number of chars allowed in a single comment
// by GitHub.
const (
Expand Down Expand Up @@ -299,7 +350,7 @@ func (g *GithubClient) PullIsMergeable(repo models.Repo, pull models.PullRequest
return false, errors.Wrap(err, "getting commit statuses")
}

checks, err := g.GetRepoChecks(repo, pull)
checks, err := g.GetRepoChecks(repo, pull.HeadCommit)

if err != nil {
return false, errors.Wrapf(err, "getting check runs")
Expand Down Expand Up @@ -341,7 +392,7 @@ func (g *GithubClient) GetPullRequest(repo models.Repo, num int) (*github.PullRe
return g.GetPullRequestFromName(repo.Name, repo.Owner, num)
}

func (g *GithubClient) GetRepoChecks(repo models.Repo, pull models.PullRequest) ([]*github.CheckRun, error) {
func (g *GithubClient) GetRepoChecks(repo models.Repo, commitSHA string) ([]*github.CheckRun, error) {
nextPage := 0

var results []*github.CheckRun
Expand All @@ -357,7 +408,7 @@ func (g *GithubClient) GetRepoChecks(repo models.Repo, pull models.PullRequest)
opts.Page = nextPage
}

result, response, err := g.client.Checks.ListCheckRunsForRef(g.ctx, repo.Owner, repo.Name, pull.HeadCommit, opts)
result, response, err := g.client.Checks.ListCheckRunsForRef(g.ctx, repo.Owner, repo.Name, commitSHA, opts)

if err != nil {
return results, errors.Wrapf(err, "getting check runs for page %d", nextPage)
Expand Down Expand Up @@ -429,17 +480,92 @@ func (g *GithubClient) UpdateStatus(ctx context.Context, request types.UpdateSta
return err
}

func (g *GithubClient) findCheckRun(statusName string, checkRuns []*github.CheckRun) *github.CheckRun {
for _, checkRun := range checkRuns {
if *checkRun.Name == statusName {
return checkRun
}
}
return nil
}

// [WENGINES-4643] TODO: Move the checks implementation to UpdateStatus once github checks is stable
// UpdateChecksStatus updates the status check
func (g *GithubClient) UpdateChecksStatus(ctx context.Context, request types.UpdateStatusRequest) error {
// TODO: Implement updating github checks
// - Get all checkruns for this SHA
// - Match the UpdateReqIdentifier with the check run. If it exists, update the checkrun. If it does not, create a new check run.
checkRuns, err := g.GetRepoChecks(request.Repo, request.Ref)
if err != nil {
return err
}

// Checks uses Status and Conlusion. Need to map models.CommitStatus to Status and Conclusion
// Status -> queued, in_progress, completed
// Conclusion -> failure, neutral, cancelled, timed_out, or action_required. (Optional. Required if you provide a status of "completed".)
return nil
status, conclusion := g.resolveChecksStatus(request.State)
checkRunOutput := github.CheckRunOutput{
Title: &request.StatusName,
Summary: &request.Description,
}
if request.Output != "" {
checkRunOutput.Text = &request.Output
}

if checkRun := g.findCheckRun(request.StatusName, checkRuns); checkRun != nil {
updateCheckRunOpts := github.UpdateCheckRunOptions{
Name: request.StatusName,
HeadSHA: &request.Ref,
Status: &status,
Output: &checkRunOutput,
}

if request.DetailsURL != "" {
updateCheckRunOpts.DetailsURL = &request.DetailsURL
}

// Conclusion is required if status is Completed
if status == Completed.String() {
updateCheckRunOpts.Conclusion = &conclusion
}
_, _, err := g.client.Checks.UpdateCheckRun(ctx, request.Repo.Owner, request.Repo.Name, *checkRun.ID, updateCheckRunOpts)
return err
}

createCheckRunOpts := github.CreateCheckRunOptions{
Name: request.StatusName,
HeadSHA: request.Ref,
Status: &status,
Output: &checkRunOutput,
}

if request.DetailsURL != "" {
createCheckRunOpts.DetailsURL = &request.DetailsURL
}

// Conclusion is required if status is Completed
if status == Completed.String() {
createCheckRunOpts.Conclusion = &conclusion
}

_, _, err = g.client.Checks.CreateCheckRun(ctx, request.Repo.Owner, request.Repo.Name, createCheckRunOpts)
return err
}

// Github Checks uses Status and Conclusion to report status of the check run. Need to map models.CommitStatus to Status and Conclusion
// Status -> queued, in_progress, completed
// Conclusion -> failure, neutral, cancelled, timed_out, or action_required. (Optional. Required if you provide a status of "completed".)
func (g *GithubClient) resolveChecksStatus(state models.CommitStatus) (string, string) {
status := Queued
conclusion := Neutral

switch state {
case models.SuccessCommitStatus:
status = Completed
conclusion = Success

case models.PendingCommitStatus:
status = InProgress

case models.FailedCommitStatus:
status = Completed
conclusion = Failure
}

return status.String(), conclusion.String()
}

// MarkdownPullLink specifies the string used in a pull request comment to reference another pull request.
Expand Down
Loading

0 comments on commit 4f72f23

Please sign in to comment.