-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmain.go
282 lines (238 loc) · 8.91 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"regexp"
"strings"
"unicode"
)
const GithubTokenKey = "GITHUB_TOKEN"
const CommitStatusContext = "https://aka.ms/azsdk/checkenforcer"
const AzurePipelinesAppName = "Azure Pipelines"
const GithubActionsAppName = "GitHub Actions"
func newPendingBody() StatusBody {
return StatusBody{
State: CommitStatePending,
Description: "Waiting for all checks to succeed",
Context: CommitStatusContext,
TargetUrl: getActionLink(),
}
}
func newSucceededBody() StatusBody {
return StatusBody{
State: CommitStateSuccess,
Description: "All checks passed",
Context: CommitStatusContext,
TargetUrl: getActionLink(),
}
}
// NOTE: This is currently unused as we post a pending state on check_suite failure,
// but keep the function around for now in case we want to revert this behavior.
func newFailedBody() StatusBody {
return StatusBody{
State: CommitStateFailure,
Description: "Some checks failed",
Context: CommitStatusContext,
TargetUrl: getActionLink(),
}
}
func main() {
if len(os.Args) <= 1 {
help()
os.Exit(1)
}
payloadPath := os.Args[1]
payload, err := ioutil.ReadFile(payloadPath)
handleError(err)
github_token := os.Getenv(GithubTokenKey)
if github_token == "" {
fmt.Println(fmt.Sprintf("WARNING: environment variable '%s' is not set", GithubTokenKey))
}
gh, err := NewGithubClient("https://api.github.com", github_token, AzurePipelinesAppName, GithubActionsAppName)
handleError(err)
err = handleEvent(gh, payload)
handleError(err)
}
func handleEvent(gh *GithubClient, payload []byte) error {
fmt.Println("################################################")
fmt.Println("# AZURE SDK CHECK ENFORCER #")
fmt.Println("# Docs: https://aka.ms/azsdk/checkenforcer #")
fmt.Println("################################################")
fmt.Println()
if ic := NewIssueCommentWebhook(payload); ic != nil {
err := handleIssueComment(gh, ic)
handleError(err)
return nil
}
if cs := NewCheckSuiteWebhook(payload); cs != nil {
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.")
}
func handleError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func getActionLink() string {
// In a github actions environment these env variables will be set.
// https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
if runId, ok := os.LookupEnv("GITHUB_RUN_ID"); ok {
repo := os.Getenv("GITHUB_REPOSITORY")
server := os.Getenv("GITHUB_SERVER_URL")
return fmt.Sprintf("%s/%s/actions/runs/%s", server, repo, runId)
}
return CommitStatusContext
}
func sanitizeComment(comment string) string {
result := []rune{}
comment = strings.TrimSpace(comment)
for _, r := range comment {
if unicode.IsLetter(r) || unicode.IsSpace(r) || r == '/' || r == '-' {
result = append(result, unicode.ToLower(r))
}
}
return string(result)
}
func getCheckEnforcerCommand(comment string) string {
comment = sanitizeComment(comment)
baseCommand := "/check-enforcer"
if !strings.HasPrefix(comment, baseCommand) {
fmt.Println(fmt.Sprintf("Skipping comment that does not start with '%s'", baseCommand))
return ""
}
re := regexp.MustCompile(`\s*` + baseCommand + `\s+([A-z]*)`)
matches := re.FindStringSubmatch(comment)
if len(matches) >= 1 {
command := matches[1]
if command == "override" || command == "evaluate" || command == "reset" || command == "help" {
fmt.Println("Parsed check enforcer command", command)
return command
}
fmt.Println("Supported commands are 'override', 'evaluate', 'reset', or 'help' but found:", command)
return command
} else {
fmt.Println("Command does not match format '/check-enforcer [override|reset|evaluate|help]'")
return "UNKNOWN"
}
}
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 == "" {
return nil
} else if command == "override" {
pr, err := gh.GetPullRequest(ic.GetPullsUrl())
handleError(err)
return gh.SetStatus(pr.StatusesUrl, newSucceededBody())
} else if command == "evaluate" || command == "reset" {
// We cannot use the commits url from the issue object because it
// is targeted to the main repo. To get all check suites for a commit,
// a request must be made to the repos API for the repository the pull
// request branch is from, which may be a fork.
pr, err := gh.GetPullRequest(ic.GetPullsUrl())
handleError(err)
checkSuites, err := gh.GetCheckSuiteStatuses(pr.GetCheckSuiteUrl())
handleError(err)
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)
}
return setStatusForCheckSuiteConclusions(gh, checkSuites, pr.StatusesUrl)
} else {
helpText, err := ioutil.ReadFile("./comments/help.txt")
handleError(err)
err = gh.CreateIssueComment(ic.GetCommentsUrl(), string(helpText))
handleError(err)
}
return nil
}
func handleCheckSuite(gh *GithubClient, cs *CheckSuiteWebhook) error {
fmt.Println("Handling check suite event.")
if cs.CheckSuite.HeadBranch == "main" {
fmt.Println("Skipping check suite for main branch.")
return nil
}
eventIsFromSupportedApp := false
for _, app := range gh.AppTargets {
if app == cs.CheckSuite.App.Name {
eventIsFromSupportedApp = true
break
}
}
// Ignore check suite events from apps that are not in the list of apps to target. This is to avoid
// race conditions with Github Actions events that show up in the check suites but are not workflows
// we have set to trigger check enforcer via the workflow_dispatch event in the workflow yaml.
//
// The original issue involved the Github Policy Service check suites causing us to evaluate
// Github Event Processor check suites before Azure Pipelines had registered any check suites.
// This caused us to return a success status incorrectly because we cannot differentiate
// Github Actions check suites intended as CI gates vs. generic runs without calling the check-runs
// API, which could many extra API calls per event.
if !eventIsFromSupportedApp {
fmt.Println("Skipping check suite evaluation for event from ignored github app", cs.CheckSuite.App.Name)
// 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(cs.GetStatusesUrl(), newPendingBody())
}
if len(gh.AppTargets) > 1 {
checkSuites, err := gh.GetCheckSuiteStatuses(cs.GetCheckSuiteUrl())
handleError(err)
return setStatusForCheckSuiteConclusions(gh, checkSuites, cs.GetStatusesUrl())
} else {
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" {
fmt.Println(fmt.Sprintf("Check enforcer only handles workflow_run events for pull requests. Skipping event for '%s'", workflowRun.Event))
return nil
}
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.
USAGE
go run main.go <payload json file>
BEHAVIORS
complete:
Sets the check enforcer status for a commit to the value of the check_suite status
Handles payload type: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#check_suite`
fmt.Println(help)
}