about summary refs log tree commit diff
path: root/ops/besadii/main.go2
diff options
context:
space:
mode:
Diffstat (limited to 'ops/besadii/main.go2')
-rw-r--r--ops/besadii/main.go2137
1 files changed, 118 insertions, 19 deletions
diff --git a/ops/besadii/main.go2 b/ops/besadii/main.go2
index 3acc8d8da8..3479a5a74d 100644
--- a/ops/besadii/main.go2
+++ b/ops/besadii/main.go2
@@ -1,11 +1,17 @@
 // Copyright 2019-2020 Google LLC.
 // SPDX-License-Identifier: Apache-2.0
 //
-// besadii is a small CLI tool that runs as a Gerrit hook (currently
-// 'ref-updated') to trigger various actions:
+// besadii is a small CLI tool that is invoked as a hook by various
+// programs to cause CI-related actions.
 //
-// - Buildkite CI builds
-// - SourceGraph (cs.tvl.fyi) repository index updates
+// It supports the following modes & operations:
+//
+// Gerrit (ref-updated) hook:
+// - Trigger Buildkite CI builds
+// - Trigger SourceGraph (cs.tvl.fyi) repository index updates
+//
+// Buildkite (post-command) hook:
+// - Submit CL verification status back to Gerrit
 package main
 
 import (
@@ -122,10 +128,6 @@ func triggerIndexUpdate(token string) error {
 }
 
 func refUpdatedFromFlags() (*refUpdated, error) {
-	if path.Base(os.Args[0]) != "ref-updated" {
-		return nil, fmt.Errorf("besadii must be invoked as the 'ref-updated' hook")
-	}
-
 	var update refUpdated
 
 	flag.StringVar(&update.project, "project", "", "Gerrit project")
@@ -170,13 +172,27 @@ func refUpdatedFromFlags() (*refUpdated, error) {
 	return nil, fmt.Errorf("besadii does not support updates for this type of ref (%q)", update.ref)
 }
 
-func main() {
+func refUpdatedMain() {
+	// Logging happens in syslog for Gerrit hooks because we don't want
+	// the hook output to be intermingled with Gerrit's own output
+	// stream
 	log, err := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "besadii")
 	if err != nil {
-		fmt.Printf("failed to open syslog: %s\n", err)
+		fmt.Fprintf(os.Stderr, "failed to open syslog: %s\n", err)
 		os.Exit(1)
 	}
 
+	update, err := refUpdatedFromFlags()
+	if err != nil {
+		log.Err(fmt.Sprintf("failed to parse ref update: %s", err))
+		os.Exit(1)
+	}
+
+	if update == nil { // the project was not 'depot'
+		log.Err("build triggers are only supported for the 'depot' project")
+		os.Exit(0)
+	}
+
 	buildkiteToken, err := ioutil.ReadFile("/etc/secrets/buildkite-besadii")
 	if err != nil {
 		log.Alert(fmt.Sprintf("buildkite token could not be read: %s", err))
@@ -189,24 +205,107 @@ func main() {
 		os.Exit(1)
 	}
 
-	update, err := refUpdatedFromFlags()
+	err = triggerBuild(log, string(buildkiteToken), update)
 	if err != nil {
-		log.Err(fmt.Sprintf("failed to parse ref update: %s", err))
+		log.Err(fmt.Sprintf("failed to trigger Buildkite build: %s", err))
+	}
+
+	err = triggerIndexUpdate(string(sourcegraphToken))
+	if err != nil {
+		log.Err(fmt.Sprintf("failed to trigger sourcegraph index update: %s", err))
+	}
+	log.Info("triggered sourcegraph index update")
+}
+
+// reviewInput is a struct representing the data submitted to Gerrit
+// to post a review on a CL.
+//
+// https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#review-input
+type reviewInput struct {
+	Message               string         `json:"message"`
+	Labels                map[string]int `json:"labels"`
+	OmitDuplicateComments bool           `json:"omit_duplicate_comments"`
+}
+
+func postCommandMain() {
+	changeId := os.Getenv("GERRIT_CHANGE_ID")
+	patchset := os.Getenv("GERRIT_PATCHSET")
+
+	if changeId == "" || patchset == "" {
+		// If these variables are unset, but the hook was invoked, the
+		// build was most likely for a branch and not for a CL - no status
+		// needs to be reported back to Gerrit!
+		fmt.Println("This isn't a CL build, nothing to do. Have a nice day!")
+		return
+	}
+
+	if os.Getenv("BUILDKITE_LABEL") != ":duck:" {
+		// this is not the build stage, don't do anything.
+		return
+	}
+
+	gerritPassword, err := ioutil.ReadFile("/etc/secrets/buildkite-gerrit")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Gerrit password could not be read: %s", err)
 		os.Exit(1)
 	}
 
-	if update == nil { // the project was not 'depot'
-		os.Exit(0)
+	var verified int
+	var verb string
+
+	if os.Getenv("BUILDKITE_COMMAND_EXIT_STATUS") == "0" {
+		verified = 1 // Verified: +1 in Gerrit
+		verb = "passed"
+	} else {
+		verified = -1
+		verb = "failed"
 	}
 
-	err = triggerBuild(log, string(buildkiteToken), update)
+	msg := fmt.Sprintf("Build of patchset %s %s: %s", patchset, verb, os.Getenv("BUILDKITE_BUILD_URL"))
+	review := reviewInput{
+		Message:               msg,
+		OmitDuplicateComments: true,
+		Labels: map[string]int{
+			"Verified": verified,
+		},
+	}
+
+	body, _ := json.Marshal(review)
+	reader := ioutil.NopCloser(bytes.NewReader(body))
+
+	url := fmt.Sprintf("https://cl.tvl.fyi/a/changes/%s/revisions/%s/review", changeId, patchset)
+	req, err := http.NewRequest("POST", url, reader)
 	if err != nil {
-		log.Err(fmt.Sprintf("failed to trigger Buildkite build: %s", err))
+		fmt.Fprintf(os.Stderr, "failed to create an HTTP request: %w", err)
+		os.Exit(1)
 	}
 
-	err = triggerIndexUpdate(string(sourcegraphToken))
+	req.SetBasicAuth("buildkite", string(gerritPassword))
+	req.Header.Add("Content-Type", "application/json")
+
+	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
-		log.Err(fmt.Sprintf("failed to trigger sourcegraph index update: %s", err))
+		fmt.Errorf("failed to update CL on Gerrit: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != 200 {
+		respBody, _ := ioutil.ReadAll(resp.Body)
+		fmt.Fprintf(os.Stderr, "received non-success response from Gerrit: %s (%v)", respBody, resp.Status)
+	} else {
+		fmt.Printf("Updated CI status on https://cl.tvl.fyi/c/depot/+/%s/%s", changeId, patchset)
+	}
+}
+
+func main() {
+	bin := path.Base(os.Args[0])
+
+	if bin == "ref-updated" {
+		refUpdatedMain()
+	} else if bin == "post-command" {
+		postCommandMain()
+	} else {
+		fmt.Fprintf(os.Stderr, "besadii does not know how to be invoked as %q, sorry!", bin)
+		os.Exit(1)
 	}
-	log.Info("triggered sourcegraph index update")
 }