about summary refs log tree commit diff
path: root/tools/gerrit-hook/gerrit.go
diff options
context:
space:
mode:
authorFranck Cuny <franck@fcuny.net>2022-05-30 13:32:43 -0700
committerFranck Cuny <franck@fcuny.net>2022-06-04 15:25:46 -0700
commit83a38a6da9ef99bc6596f6cfb53395a89f0165c7 (patch)
treec808d9dd826757c440f1632fd758503f563c0403 /tools/gerrit-hook/gerrit.go
parentmeta: remove pre-commit checks (diff)
downloadworld-83a38a6da9ef99bc6596f6cfb53395a89f0165c7.tar.gz
feat(gerrit-hook): a small tool to act as a dispatcher for gerrit
When a patchset is created, gerrit will call this tool with a number of
arguments.

This hook triggers a build with buildKite for the given patchset, and
add a comment to gerrit with a link to the build.

We do not wait for the build to be successful to update gerrit. This
will be done by another hook which the buildKite agents will call once
they are done with the build.

Change-Id: Iaa221765f3c52875ec37c5d282ba0557291eb5a4
Reviewed-on: https://cl.fcuny.net/c/world/+/171
Reviewed-by: Franck Cuny <franck@fcuny.net>
Diffstat (limited to '')
-rw-r--r--tools/gerrit-hook/gerrit.go144
1 files changed, 144 insertions, 0 deletions
diff --git a/tools/gerrit-hook/gerrit.go b/tools/gerrit-hook/gerrit.go
new file mode 100644
index 0000000..6a23527
--- /dev/null
+++ b/tools/gerrit-hook/gerrit.go
@@ -0,0 +1,144 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"log/syslog"
+	"net/http"
+	"os"
+	"regexp"
+	"strconv"
+	"time"
+)
+
+// Regular expression to extract change ID out of a URL
+var changeIdRegexp = regexp.MustCompile(`^.*/(\d+)$`)
+
+func gerritHookMain(cfg *config, log *syslog.Writer, trigger *buildTrigger) {
+	if trigger == nil {
+		os.Exit(0)
+	}
+
+	err := triggerBuild(cfg, log, trigger)
+	if err != nil {
+		log.Err(fmt.Sprintf("failed to trigger Buildkite build: %s", err))
+		os.Exit(1)
+	}
+}
+
+type reviewInput struct {
+	Message                        string         `json:"message"`
+	Labels                         map[string]int `json:"labels,omitempty"`
+	OmitDuplicateComments          bool           `json:"omit_duplicate_comments"`
+	IgnoreDefaultAttentionSetRules bool           `json:"ignore_default_attention_set_rules"`
+	Tag                            string         `json:"tag"`
+	Notify                         string         `json:"notify,omitempty"`
+}
+
+type buildTrigger struct {
+	project             string
+	change              string
+	kind                string
+	changeUrl           string
+	changeOwner         string
+	changeOwnerUserName string
+	branch              string
+	topic               string
+	uploader            string
+	uploaderUserName    string
+	commit              string
+	patchset            string
+	changeId            string
+	ref                 string
+}
+
+// https://gerrit.googlesource.com/plugins/hooks/+/HEAD/src/main/resources/Documentation/hooks.md#patchset_created
+func triggerForPatchsetCreated() (*buildTrigger, error) {
+	var trigger buildTrigger
+
+	flag.StringVar(&trigger.project, "project", "", "Gerrit project")
+	flag.StringVar(&trigger.change, "change", "", "Gerrit change")
+	flag.StringVar(&trigger.kind, "kind", "", "Gerrit kind")
+	flag.StringVar(&trigger.changeUrl, "change-url", "", "Gerrit URL for the change")
+	flag.StringVar(&trigger.changeOwner, "change-owner", "", "Gerrit owner")
+	flag.StringVar(&trigger.changeOwnerUserName, "change-owner-username", "", "Gerrit username")
+	flag.StringVar(&trigger.branch, "branch", "", "name of the branch")
+	flag.StringVar(&trigger.topic, "topic", "", "name of the topic")
+	flag.StringVar(&trigger.uploader, "uploader", "", "name ofthe uploader")
+	flag.StringVar(&trigger.uploaderUserName, "uploader-username", "", "")
+	flag.StringVar(&trigger.commit, "commit", "", "")
+	flag.StringVar(&trigger.patchset, "patchset", "", "")
+
+	flag.Parse()
+
+	// for now we only care about the project named `world' and the
+	// branch named 'main'
+	if trigger.project != "world" || trigger.branch != "main" {
+		return nil, nil
+	}
+
+	// We only care about patchset that are actually modifying the
+	// code. See
+	// https://gerrit-review.googlesource.com/Documentation/config-labels.html
+	if trigger.kind == "NO_CODE_CHANGE" || trigger.kind == "NO_CHANGE" {
+		return nil, nil
+	}
+
+	// extract the changeId from the URL
+	matches := changeIdRegexp.FindStringSubmatch(trigger.changeUrl)
+	trigger.changeId = matches[1]
+
+	// build the ref
+	changeId, _ := strconv.Atoi(trigger.changeId)
+	trigger.ref = fmt.Sprintf(
+		"refs/changes/%02d/%s/%s",
+		changeId%100, trigger.changeId, trigger.patchset,
+	)
+
+	return &trigger, nil
+}
+
+// after triggering a build with buildKite, we update gerrit to add a
+// comment that links to the build.
+func updateGerrit(cfg *config, review reviewInput, changeId string, patchSet string) {
+	body, err := json.Marshal(review)
+	if err != nil {
+		log.Fatal(fmt.Sprintf("failed to marshal gerrit update: %v", err))
+		os.Exit(1)
+	}
+
+	reader := ioutil.NopCloser(bytes.NewReader(body))
+	url := fmt.Sprintf("%s/a/changes/%s/revisions/%s/review", cfg.GerritUrl, changeId, patchSet)
+	req, err := http.NewRequest("POST", url, reader)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to create an HTTP request: %v", err)
+		os.Exit(1)
+	}
+
+	req.SetBasicAuth(cfg.GerritUser, cfg.GerritPassword)
+	req.Header.Add("Content-Type", "application/json")
+
+	// Let's budget this to 10 seconds maximum, this should be more
+	// than enough to add a comment to gerrit.
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to send gerrit request: %v", err)
+		os.Exit(1)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		respBody, _ := ioutil.ReadAll(resp.Body)
+		fmt.Fprintf(os.Stderr, "failed to update gerrit: %s: %s ", respBody, resp.Status)
+	} else {
+		fmt.Printf("added link to CI build to %s", patchSet)
+	}
+}