From 6d3a9e7b5feb13ae1595854254339686a2fc3c2d Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 29 Jun 2020 03:35:39 +0100 Subject: feat(besadii): Implement support for Buildkite's post-command hook 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 --- ops/besadii/main.go2 | 137 +++++++++++++++++++++++++++++++----- users/tazjin/nixos/frog/default.nix | 8 +++ 2 files changed, 126 insertions(+), 19 deletions(-) diff --git a/ops/besadii/main.go2 b/ops/besadii/main.go2 index 3acc8d8da8ff..3479a5a74d86 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 f3e75a2420ec..5d438e7b57f1 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 = -- cgit 1.4.1