about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--tools/gerrit-hook/buildkite.go81
-rw-r--r--tools/gerrit-hook/default.nix16
-rw-r--r--tools/gerrit-hook/gerrit.go144
-rw-r--r--tools/gerrit-hook/go.mod3
-rw-r--r--tools/gerrit-hook/main.go64
6 files changed, 309 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 9eb84ac..cfbff8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
 /users/fcuny/blog/result
 /result
 /users/fcuny/notes/result
+/tools/gerrit-hook/gerrit-hook
diff --git a/tools/gerrit-hook/buildkite.go b/tools/gerrit-hook/buildkite.go
new file mode 100644
index 0000000..d8723b6
--- /dev/null
+++ b/tools/gerrit-hook/buildkite.go
@@ -0,0 +1,81 @@
+package main
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log/syslog"
+	"net/http"
+	"time"
+)
+
+// https://buildkite.com/docs/apis/rest-api/builds#create-a-Build
+type Build struct {
+	Commit string `json:"commit"`
+	Branch string `json:"branch"`
+}
+
+type buildResponse struct {
+	WebUrl string `json:"web_url"`
+}
+
+func triggerBuild(cfg *config, log *syslog.Writer, trigger *buildTrigger) error {
+	b := Build{
+		Commit: trigger.commit,
+		Branch: trigger.ref,
+	}
+
+	body, _ := json.Marshal(b)
+	reader := ioutil.NopCloser(bytes.NewReader(body))
+
+	bkUrl := fmt.Sprintf("https://api.buildkite.com/v2/organizations/%s/pipelines/%s/builds", cfg.BuildKiteOrganization, trigger.project)
+	req, err := http.NewRequest("POST", bkUrl, reader)
+	if err != nil {
+		return fmt.Errorf("failed to create an HTTP request: %v", err)
+	}
+
+	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", cfg.BuildKiteToken))
+	req.Header.Add("Content-Type", "application/json")
+
+	// Let's budget this to 10 seconds maximum, this should be more
+	// than enough, as we're only triggering the build, we're not
+	// waiting on the status of the build
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	resp, err := http.DefaultClient.Do(req.WithContext(ctx))
+	if err != nil {
+		return fmt.Errorf("failed to send buildKite request: %v", err)
+	}
+	defer resp.Body.Close()
+
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("failed to parse buildKite response: %v", err)
+	}
+
+	if resp.StatusCode != http.StatusCreated {
+		return fmt.Errorf("received a non-success response from buildKite: %s (%v)", respBody, resp.Status)
+	}
+
+	var buildResp buildResponse
+	err = json.Unmarshal(respBody, &buildResp)
+	if err != nil {
+		return fmt.Errorf("failed to unmarshal build response: %v", err)
+	}
+
+	// Report the status back to the Gerrit CL so that users can click
+	// through to the running build.
+	msg := fmt.Sprintf("started build for patchset #%s on: %s", trigger.patchset, buildResp.WebUrl)
+	review := reviewInput{
+		Message:                        msg,
+		OmitDuplicateComments:          true,
+		Tag:                            "autogenerated:buildkite~trigger",
+		IgnoreDefaultAttentionSetRules: true,
+		Notify:                         "NONE",
+	}
+	updateGerrit(cfg, review, trigger.changeId, trigger.patchset)
+	return nil
+}
diff --git a/tools/gerrit-hook/default.nix b/tools/gerrit-hook/default.nix
new file mode 100644
index 0000000..c0667ae
--- /dev/null
+++ b/tools/gerrit-hook/default.nix
@@ -0,0 +1,16 @@
+{ pkgs, ... }:
+
+pkgs.buildGoModule rec {
+  name = "gerrit-hook";
+  src = ./.;
+  subPackages = [ "." ];
+  vendorSha256 = null;
+
+  meta = with pkgs.lib; {
+    description = "hooks for gerrit";
+    homepage = "https://golang.fcuny.net";
+    license = licenses.mit;
+    platforms = platforms.linux;
+    maintainers = [ ];
+  };
+}
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)
+	}
+}
diff --git a/tools/gerrit-hook/go.mod b/tools/gerrit-hook/go.mod
new file mode 100644
index 0000000..98968da
--- /dev/null
+++ b/tools/gerrit-hook/go.mod
@@ -0,0 +1,3 @@
+module golang.fcuny.net/gerrit-hook
+
+go 1.16
diff --git a/tools/gerrit-hook/main.go b/tools/gerrit-hook/main.go
new file mode 100644
index 0000000..f8ed687
--- /dev/null
+++ b/tools/gerrit-hook/main.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log/syslog"
+	"os"
+	"path"
+)
+
+// config represents the configuration for the gerrit hook
+type config struct {
+	GerritUrl             string `json:"gerritUrl"`
+	GerritUser            string `json:"gerritUser"`
+	GerritPassword        string `json:"gerritPassword"`
+	BuildKiteToken        string `json:"buildKiteToken"`
+	BuildKiteOrganization string `json:"buildKiteOrganization"`
+}
+
+func loadConfig() (*config, error) {
+	configPath := "/var/run/agenix/gerrit/hooks"
+
+	configJson, err := ioutil.ReadFile(configPath)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read configuration file %s: %v", configPath, err)
+	}
+
+	var cfg config
+	err = json.Unmarshal(configJson, &cfg)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshall configuration: %v", err)
+	}
+
+	return &cfg, nil
+}
+
+func main() {
+	log, err := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "gerrit-hook")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to open syslog: %s\n", err)
+	}
+
+	log.Info(fmt.Sprintf("`gerrit-hook' called with arguments: %v\n", os.Args))
+
+	cmd := path.Base(os.Args[0])
+
+	cfg, err := loadConfig()
+	if err != nil {
+		os.Exit(1)
+	}
+
+	if cmd == "patchset-created" {
+		trigger, err := triggerForPatchsetCreated()
+		if err != nil {
+			log.Crit(fmt.Sprintf("failed to create a trigger: %s", err))
+			os.Exit(1)
+		}
+		gerritHookMain(cfg, log, trigger)
+	} else {
+		log.Info(fmt.Sprintf("`%s' is not a supported command", cmd))
+		os.Exit(1)
+	}
+}