Skip to content

Instantly share code, notes, and snippets.

@jayco
Last active July 14, 2020 21:01
Show Gist options
  • Save jayco/c24cfc3d882f41e1cdaf225b40afa559 to your computer and use it in GitHub Desktop.
Save jayco/c24cfc3d882f41e1cdaf225b40afa559 to your computer and use it in GitHub Desktop.
Listen for job finished events and fail fast on builds
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)
}
@jayco
Copy link
Author

jayco commented Jun 24, 2020

Running this in the form of:

go run main.go --org your-org --token your-graphQL-token

@jayco
Copy link
Author

jayco commented Jun 24, 2020

Using the API, the build is cancelled when the job publishes a 'job.finished' event and has hard failed (no retries). The build is updated with an annotation to visually show the reason for cancellation and link to the failed job.

Screen Shot 2020-06-24 at 3 15 19 pm

Link to Buildkite build demo video https://www.loom.com/share/e6624f8d6d534e1195d551dffbe64540

@jayco
Copy link
Author

jayco commented Jun 24, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment