Last active
July 14, 2020 21:01
-
-
Save jayco/c24cfc3d882f41e1cdaf225b40afa559 to your computer and use it in GitHub Desktop.
Listen for job finished events and fail fast on builds
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
package main | |
import ( | |
"bytes" | |
"context" | |
"encoding/base64" | |
"encoding/json" | |
"flag" | |
"fmt" | |
"io/ioutil" | |
"log" | |
"net/http" | |
"strconv" | |
"golang.org/x/oauth2" | |
) | |
// general config values - org pipeline and generated api token with full access | |
var ( | |
apiToken = flag.String("token", "", "GRAPHQL API token") | |
orgSlug = flag.String("org", "", "Orginization slug") | |
port = 4587 | |
defaultAnnotationStyle = "ERROR" | |
graphAPI = "https://graphql.buildkite.com/v1" | |
) | |
// Buildkite job (unused values omitted) | |
type job struct { | |
ID string `json:"id"` | |
State string `json:"state"` | |
WebURL string `json:"web_url"` | |
ExitStatus int `json:"exit_status"` | |
SoftFailed bool `json:"soft_failed"` | |
RetriedInNewJob *string `json:"retried_in_job_id"` | |
} | |
// Buildkite build (unused values omitted) | |
type build struct { | |
ID string `json:"id"` | |
Number *int `json:"number"` | |
URL string `json:"url"` | |
} | |
// Buildkite pipeline (unused values omitted) | |
type pipeline struct { | |
Slug string `json:"slug"` | |
} | |
// Buildkite webhook event payload | |
type payload struct { | |
Event string `json:"event"` | |
Job job `json:"job"` | |
Build build `json:"build"` | |
Pipeline pipeline `json:"pipeline"` | |
} | |
// Buildkite GraphQL API requires a base64 encoded id in the fromat Build---UUID | |
func buildGQLID(uuid string) string { | |
return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("Build---%s", uuid))) | |
} | |
// Nothing fancy, lets just post using http client | |
func jsonPost(c *http.Client, jsonData *map[string]string) { | |
jsonValue, _ := json.Marshal(jsonData) | |
payload := bytes.NewBuffer(jsonValue) | |
if _, err := c.Post(graphAPI, "application/json", payload); err != nil { | |
log.Printf("%v", err) | |
} | |
} | |
// cancel build grapthql query | |
func cancel(buildUUID string) *map[string]string { | |
cancelTemplate := `mutation { | |
buildCancel(input: {id: "%s"}) { | |
build { | |
canceledAt | |
} | |
} | |
}` | |
return &map[string]string{"query": fmt.Sprintf(cancelTemplate, buildGQLID(buildUUID))} | |
} | |
// annotate build grapthql query | |
func annotate(buildUUID string, body string, style string) *map[string]string { | |
annotateTemplate := `mutation { | |
buildAnnotate(input: {buildID: "%s", body: "%s", style: %s}) { | |
annotation { | |
uuid | |
body { text } | |
style | |
context | |
} | |
} | |
}` | |
return &map[string]string{"query": fmt.Sprintf(annotateTemplate, buildGQLID(buildUUID), body, style)} | |
} | |
// handler with a http client - best effort fast failing | |
func handler(c *http.Client) func(w http.ResponseWriter, r *http.Request) { | |
return func(w http.ResponseWriter, r *http.Request) { | |
var p payload | |
b, _ := ioutil.ReadAll(r.Body) | |
json.Unmarshal(b, &p) | |
// call the API and fail fast if the build is hard failing | |
if (p.Job.State == "failed") && (p.Job.ExitStatus > 0) && (p.Job.SoftFailed == false) && (p.Job.RetriedInNewJob == nil) { | |
log.Println("hardfailed job, attempting to cancel build") | |
jsonPost(c, cancel(p.Build.ID)) | |
log.Printf("Build #%s canceled, %s", strconv.Itoa(*p.Build.Number), *&p.Build.URL) | |
jsonPost(c, annotate(p.Build.ID, fmt.Sprintf("Canceled because of hard failure at %s", p.Job.WebURL), defaultAnnotationStyle)) | |
} | |
w.WriteHeader(http.StatusNoContent) | |
} | |
} | |
// small server to listen for webhooks | |
func main() { | |
flag.Parse() | |
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: *apiToken}) | |
httpClient := oauth2.NewClient(context.Background(), src) | |
webhookHandler := handler(httpClient) | |
http.HandleFunc("/", webhookHandler) | |
log.Printf("starting server, listening on port %v \n", port) | |
http.ListenAndServe(fmt.Sprintf(":%v", port), nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Webhook setup https://buildkite.com/organizations/your-org/services: