about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2020-06-29T02·35+0100
committertazjin <mail@tazj.in>2020-06-29T15·15+0000
commit6d3a9e7b5feb13ae1595854254339686a2fc3c2d (patch)
tree5b5ea5164d60b8582dd69c9324e019a233b5292f
parentf28b0d01ef74c0c4e7a305e909bc88bfec0616f4 (diff)
feat(besadii): Implement support for Buildkite's post-command hook r/1127
This hook is invoked by Buildkite (on the runner) after every build
stage. This change adds support in Besadii to run as this hook and
update the build status on a Gerrit CL.

Change-Id: Ie07a94d9b41645a77681cf42f6969d218abf93c1
Reviewed-on: https://cl.tvl.fyi/c/depot/+/761
Tested-by: BuildkiteCI
Reviewed-by: Kane York <rikingcoding@gmail.com>
-rw-r--r--ops/besadii/main.go2137
-rw-r--r--users/tazjin/nixos/frog/default.nix8
2 files changed, 126 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")
 }
diff --git a/users/tazjin/nixos/frog/default.nix b/users/tazjin/nixos/frog/default.nix
index f3e75a2420..5d438e7b57 100644
--- a/users/tazjin/nixos/frog/default.nix
+++ b/users/tazjin/nixos/frog/default.nix
@@ -12,6 +12,13 @@ config: let
   frogEmacs = (depot.users.tazjin.emacs.overrideEmacs(epkgs: epkgs ++ [
     depot.third_party.emacsPackages.google-c-style
   ]));
+
+  # All Buildkite hooks are actually besadii, but it's being invoked
+  # with different names.
+  buildkiteHooks = depot.third_party.runCommandNoCC "buildkite-hooks" {} ''
+    mkdir -p $out/bin
+    ln -s ${depot.ops.besadii}/bin/besadii $out/bin/post-command
+  '';
 in depot.lib.fix(self: {
   imports = [
     "${depot.depotPath}/ops/nixos/v4l2loopback.nix"
@@ -198,6 +205,7 @@ in depot.lib.fix(self: {
   services.buildkite-agents.frog = {
     enable = true;
     tokenPath = "/etc/secrets/buildkite-token";
+    hooks.post-command = "${buildkiteHooks}/bin/post-command";
   };
 
   environment.systemPackages =