Skip to content

Commit

Permalink
Revert "Revert "Support check enforcer with Github Actions (#7)" (#8)" (
Browse files Browse the repository at this point in the history
  • Loading branch information
benbp authored Dec 29, 2022
1 parent b86757c commit 40989eb
Show file tree
Hide file tree
Showing 9 changed files with 1,757 additions and 198 deletions.
2 changes: 1 addition & 1 deletion comments/no_pipelines.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Check Enforcer evaluate was requested, but there are no Azure Pipelines configured to trigger for the changed files.
Check Enforcer evaluate was requested, but no Azure Pipelines or Github Actions have been triggered for the changed files.

If you are initializing a new service, follow the [new service docs](https://aka.ms/azsdk/checkenforcer#onboarding-a-new-service). If no Azure Pipelines are desired, run `/check-enforcer override`.

Expand Down
61 changes: 37 additions & 24 deletions github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ import (
)

type GithubClient struct {
client *http.Client
token string
BaseUrl url.URL
AppTarget string
client *http.Client
token string
BaseUrl url.URL
AppTargets []string
}

func NewGithubClient(baseUrl string, token string, appTarget string) (*GithubClient, error) {
func NewGithubClient(baseUrl string, token string, appTargets ...string) (*GithubClient, error) {
u, err := url.Parse(baseUrl)
if err != nil {
return nil, err
}
return &GithubClient{
client: &http.Client{},
BaseUrl: *u,
token: token,
AppTarget: appTarget,
client: &http.Client{},
BaseUrl: *u,
token: token,
AppTargets: appTargets,
}, nil
}

Expand Down Expand Up @@ -102,39 +102,52 @@ func (gh *GithubClient) GetPullRequest(pullsUrl string) (PullRequest, error) {
return pr, nil
}

func (gh *GithubClient) GetCheckSuiteStatus(pr PullRequest) (CheckSuiteStatus, CheckSuiteConclusion, error) {
csUrl := pr.GetCheckSuiteUrl()
func (gh *GithubClient) FilterCheckSuiteStatuses(checkSuites []CheckSuite) []CheckSuite {
filteredCheckSuites := []CheckSuite{}

for _, cs := range checkSuites {
for _, target := range gh.AppTargets {
// Ignore auxiliary checks we don't control, e.g. Microsoft Policy Service.
// Github creates a check suite for each app with checks:write permissions,
// so also ignore any check suites with 0 check runs posted
//
// TODO: in the case where a check run isn't posted from azure pipelines due to invalid yaml, will this
// show up as 0 check runs? If so, how do we differentiate between the following so we don't submit a passing status:
// 1. Github Actions CI intended, Azure Pipelines CI NOT detected
// 2. Github Actions CI intended, Azure Pipelines CI intended, Azure Pipelines CI invalid yaml
if cs.App.Name == target && cs.LatestCheckRunCount > 0 {
filteredCheckSuites = append(filteredCheckSuites, cs)
}
}
}

return filteredCheckSuites
}

target, err := gh.getUrl(csUrl)
func (gh *GithubClient) GetCheckSuiteStatuses(checkSuiteUrl string) ([]CheckSuite, error) {
target, err := gh.getUrl(checkSuiteUrl)
if err != nil {
return "", "", err
return []CheckSuite{}, err
}

req, err := http.NewRequest("GET", target.String(), nil)
if err != nil {
return "", "", err
return []CheckSuite{}, err
}

gh.setHeaders(req)

fmt.Println("GET to", target.String())
data, err := gh.request(req)
if err != nil {
return "", "", err
return []CheckSuite{}, err
}
suites := CheckSuites{}
if err = json.Unmarshal(data, &suites); err != nil {
return "", "", err
}

for _, cs := range suites.CheckSuites {
if cs.App.Name != gh.AppTarget {
continue
}
return cs.Status, cs.Conclusion, nil
return []CheckSuite{}, err
}

return "", "", nil
return gh.FilterCheckSuiteStatuses(suites.CheckSuites), nil
}

func (gh *GithubClient) CreateIssueComment(commentsUrl string, body string) error {
Expand Down
96 changes: 64 additions & 32 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
const GithubTokenKey = "GITHUB_TOKEN"
const CommitStatusContext = "https://aka.ms/azsdk/checkenforcer"
const AzurePipelinesAppName = "Azure Pipelines"
const GithubActionsAppName = "GitHub Actions"

func newPendingBody() StatusBody {
return StatusBody{
Expand Down Expand Up @@ -58,7 +59,7 @@ func main() {
fmt.Println(fmt.Sprintf("WARNING: environment variable '%s' is not set", GithubTokenKey))
}

gh, err := NewGithubClient("https://api.github.com", github_token, AzurePipelinesAppName)
gh, err := NewGithubClient("https://api.github.com", github_token, AzurePipelinesAppName, GithubActionsAppName)
handleError(err)

err = handleEvent(gh, payload)
Expand All @@ -73,19 +74,23 @@ func handleEvent(gh *GithubClient, payload []byte) error {
fmt.Println()

if ic := NewIssueCommentWebhook(payload); ic != nil {
fmt.Println("Handling issue comment event.")
err := handleComment(gh, ic)
err := handleIssueComment(gh, ic)
handleError(err)
return nil
}

if cs := NewCheckSuiteWebhook(payload); cs != nil {
fmt.Println("Handling check suite event.")
err := handleCheckSuite(gh, cs)
handleError(err)
return nil
}

if wr := NewWorkflowRunWebhook(payload); wr != nil {
err := handleWorkflowRun(gh, wr)
handleError(err)
return nil
}

return errors.New("Error: Invalid or unsupported payload body.")
}

Expand Down Expand Up @@ -143,7 +148,29 @@ func getCheckEnforcerCommand(comment string) string {
}
}

func handleComment(gh *GithubClient, ic *IssueCommentWebhook) error {
func setStatusForCheckSuiteConclusions(gh *GithubClient, checkSuites []CheckSuite, statusesUrl string) error {
successCount := 0

for _, suite := range checkSuites {
fmt.Println(fmt.Sprintf("Check suite conclusion for '%s' is '%s'.", suite.App.Name, suite.Conclusion))
if IsCheckSuiteSucceeded(suite.Conclusion) {
successCount++
}
}

if successCount > 0 && successCount == len(checkSuites) {
return gh.SetStatus(statusesUrl, newSucceededBody())
}

// A pending status is redundant with the default status, but it allows us to
// add more details to the status check in the UI such as a link back to the
// check enforcer run that evaluated pending.
return gh.SetStatus(statusesUrl, newPendingBody())
}

func handleIssueComment(gh *GithubClient, ic *IssueCommentWebhook) error {
fmt.Println("Handling issue comment event.")

command := getCheckEnforcerCommand(ic.Comment.Body)

if command == "" {
Expand All @@ -159,25 +186,17 @@ func handleComment(gh *GithubClient, ic *IssueCommentWebhook) error {
// request branch is from, which may be a fork.
pr, err := gh.GetPullRequest(ic.GetPullsUrl())
handleError(err)
_, conclusion, err := gh.GetCheckSuiteStatus(pr)
checkSuites, err := gh.GetCheckSuiteStatuses(pr.GetCheckSuiteUrl())
handleError(err)

if IsCheckSuiteNoMatch(conclusion) {
if checkSuites == nil || len(checkSuites) == 0 {
noPipelineText, err := ioutil.ReadFile("./comments/no_pipelines.txt")
handleError(err)
err = gh.CreateIssueComment(ic.GetCommentsUrl(), string(noPipelineText))
handleError(err)
err = gh.SetStatus(pr.StatusesUrl, newPendingBody())
handleError(err)
} else if IsCheckSuiteSucceeded(conclusion) {
return gh.SetStatus(pr.StatusesUrl, newSucceededBody())
} else if IsCheckSuiteFailed(conclusion) {
// Mark as pending with link to action run even on failure, to maintain
// consistency with the old check enforcer behavior and avoid confusion for now.
return gh.SetStatus(pr.StatusesUrl, newPendingBody())
} else {
return gh.SetStatus(pr.StatusesUrl, newPendingBody())
}

return setStatusForCheckSuiteConclusions(gh, checkSuites, pr.StatusesUrl)
} else {
helpText, err := ioutil.ReadFile("./comments/help.txt")
handleError(err)
Expand All @@ -189,27 +208,40 @@ func handleComment(gh *GithubClient, ic *IssueCommentWebhook) error {
}

func handleCheckSuite(gh *GithubClient, cs *CheckSuiteWebhook) error {
if cs.CheckSuite.App.Name != gh.AppTarget {
fmt.Println(fmt.Sprintf(
"Check Enforcer only handles check suites from the '%s' app. Found: '%s'",
gh.AppTarget,
cs.CheckSuite.App.Name))
return nil
} else if cs.CheckSuite.HeadBranch == "main" {
fmt.Println("Handling check suite event.")

if cs.CheckSuite.HeadBranch == "main" {
fmt.Println("Skipping check suite for main branch.")
return nil
} else if cs.IsSucceeded() {
return gh.SetStatus(cs.GetStatusesUrl(), newSucceededBody())
} else if cs.IsFailed() {
// Mark as pending with link to action run even on failure, to maintain
// consistency with the old check enforcer behavior and avoid confusion for now.
return gh.SetStatus(cs.GetStatusesUrl(), newPendingBody())
}

if len(gh.AppTargets) > 1 {
checkSuites, err := gh.GetCheckSuiteStatuses(cs.GetCheckSuiteUrl())
handleError(err)
return setStatusForCheckSuiteConclusions(gh, checkSuites, cs.GetStatusesUrl())
} else {
fmt.Println("Skipping check suite with conclusion: ", cs.CheckSuite.Conclusion)
return nil
checkSuites := gh.FilterCheckSuiteStatuses([]CheckSuite{cs.CheckSuite})
return setStatusForCheckSuiteConclusions(gh, checkSuites, cs.GetStatusesUrl())
}
}

func handleWorkflowRun(gh *GithubClient, webhook *WorkflowRunWebhook) error {
workflowRun := webhook.WorkflowRun
fmt.Println("Handling workflow run event.")
fmt.Println(fmt.Sprintf("Workflow run url: %s", workflowRun.HtmlUrl))
fmt.Println(fmt.Sprintf("Workflow run commit: %s", workflowRun.HeadSha))

if workflowRun.Event != "pull_request" || workflowRun.PullRequests == nil || len(workflowRun.PullRequests) == 0 {
fmt.Println("Check enforcer only handles workflow_run events for pull requests. Skipping")
return gh.SetStatus(workflowRun.GetStatusesUrl(), newPendingBody())
}

checkSuites, err := gh.GetCheckSuiteStatuses(workflowRun.GetCheckSuiteUrl())
handleError(err)

return setStatusForCheckSuiteConclusions(gh, checkSuites, workflowRun.GetStatusesUrl())
}

func help() {
help := `Update pull request status checks based on github webhook events.
Expand Down
Loading

0 comments on commit 40989eb

Please sign in to comment.