about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2021-11-27T17·36+0300
committerVincent Ambo <mail@tazj.in>2021-12-01T09·49+0300
commitc1aab56a02dc3b75bce1f5a493b718bfc9164de1 (patch)
tree1ca694fa18c3eb76dc6926f39c65bcd5c74c667b
parent8318178b5f34bae0d625ada1170006cb2d6a6a51 (diff)
feat(besadii): Support invocation as different Gerrit hooks r/3125
Removes besadii support for the previously used 'ref-updated' hook and
instead introduces support for the 'change-merged' and
'patchset-created' hooks.

These hooks more accurately capture the semantics of when besadii
should trigger CI builds and using them will avoid problems such as
skipping 'canon' builds if chains of CLs are submitted together.

Change-Id: Ib90356c069780bf0c0250e56b927e46a5b31ce7f
-rw-r--r--ops/besadii/main.go184
-rw-r--r--ops/modules/monorepo-gerrit.nix3
2 files changed, 119 insertions, 68 deletions
diff --git a/ops/besadii/main.go b/ops/besadii/main.go
index 86356a073fac..5ffe1beeda98 100644
--- a/ops/besadii/main.go
+++ b/ops/besadii/main.go
@@ -25,26 +25,25 @@ import (
 	"os"
 	"path"
 	"regexp"
+	"strconv"
 	"strings"
 )
 
-var branchRegexp = regexp.MustCompile(`^refs/heads/(.*)$`)
-var metaRegexp = regexp.MustCompile(`^refs/changes/\d{0,2}/(\d+)/meta$`)
-var patchsetRegexp = regexp.MustCompile(`^refs/changes/\d{0,2}/(\d+)/(\d+)$`)
+// Regular expression to extract change ID out of a URL
+var changeIdRegexp = regexp.MustCompile(`^.*/(\d+)$`)
 
 // buildTrigger represents the information passed to besadii when it
 // is invoked as a Gerrit hook.
 //
 // https://gerrit.googlesource.com/plugins/hooks/+/HEAD/src/main/resources/Documentation/hooks.md
 type buildTrigger struct {
-	project   string
-	ref       string
-	commit    string
-	submitter string
-	email     string
-
-	changeId *string
-	patchset *string
+	project string
+	ref     string
+	commit  string
+	owner   string
+
+	changeId string
+	patchset string
 }
 
 type Author struct {
@@ -118,9 +117,11 @@ func triggerBuild(log *syslog.Writer, token string, trigger *buildTrigger) error
 	//
 	// This information is later used by besadii when invoked by Gerrit
 	// to communicate the build status back to Gerrit.
-	if trigger.changeId != nil && trigger.patchset != nil {
-		env["GERRIT_CHANGE_ID"] = *trigger.changeId
-		env["GERRIT_PATCHSET"] = *trigger.patchset
+	canonBuild := true
+	if trigger.changeId != "" && trigger.patchset != "" {
+		env["GERRIT_CHANGE_ID"] = trigger.changeId
+		env["GERRIT_PATCHSET"] = trigger.patchset
+		canonBuild = false
 	}
 
 	build := Build{
@@ -128,8 +129,7 @@ func triggerBuild(log *syslog.Writer, token string, trigger *buildTrigger) error
 		Branch: trigger.ref,
 		Env:    env,
 		Author: Author{
-			Name:  trigger.submitter,
-			Email: trigger.email,
+			Name: trigger.owner,
 		},
 	}
 
@@ -168,9 +168,14 @@ func triggerBuild(log *syslog.Writer, token string, trigger *buildTrigger) error
 
 	fmt.Fprintf(log, "triggered build for ref %q at commit %q: %s", trigger.ref, trigger.commit, buildResp.WebUrl)
 
+	// For builds of canon there is nothing else to do
+	if canonBuild {
+		return nil
+	}
+
 	// 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 of cl/%s: %s", *trigger.patchset, *trigger.changeId, buildResp.WebUrl)
+	msg := fmt.Sprintf("Started build for patchset #%s of cl/%s: %s", trigger.patchset, trigger.changeId, buildResp.WebUrl)
 	review := reviewInput{
 		Message:               msg,
 		OmitDuplicateComments: true,
@@ -179,7 +184,7 @@ func triggerBuild(log *syslog.Writer, token string, trigger *buildTrigger) error
 		// Do not update the attention set for this comment.
 		IgnoreDefaultAttentionSetRules: true,
 	}
-	updateGerrit(review, *trigger.changeId, *trigger.patchset)
+	updateGerrit(review, trigger.changeId, trigger.patchset)
 
 	return nil
 }
@@ -199,69 +204,96 @@ func triggerIndexUpdate(token string) error {
 	return err
 }
 
-func buildTriggerFromFlags() (*buildTrigger, error) {
+// Gerrit passes more flags than we want, but Rob Pike decided[0] in
+// 2013 that the Go art project will not allow users to ignore flags
+// because he "doesn't like it". This function allows users to ignore
+// flags.
+//
+// [0]: https://github.com/golang/go/issues/6112#issuecomment-66083768
+func ignoreFlags(ignore []string) {
+	for _, f := range ignore {
+		flag.String(f, "", "flag to ignore")
+	}
+}
+
+// Extract the buildtrigger struct out of the flags passed to besadii
+// when invoked as Gerrit's 'patchset-created' hook. This hook is used
+// for triggering CI on in-progress CLs.
+func buildTriggerFromPatchsetCreated() (*buildTrigger, error) {
+	// Information that needs to be returned
 	var trigger buildTrigger
 
+	// Information that is only needed for parsing
+	var targetBranch, changeUrl string
+
 	flag.StringVar(&trigger.project, "project", "", "Gerrit project")
-	flag.StringVar(&trigger.commit, "newrev", "", "new revision")
-	flag.StringVar(&trigger.email, "submitter", "", "Submitter email")
-	flag.StringVar(&trigger.submitter, "submitter-username", "", "Submitter username")
-	flag.StringVar(&trigger.ref, "refname", "", "updated reference name")
-
-	// Gerrit passes more flags than we want, but Rob Pike decided[0] in
-	// 2013 that the Go art project will not allow users to ignore flags
-	// because he "doesn't like it". The following code ignores the
-	// flags.
-	//
-	// [0]: https://github.com/golang/go/issues/6112#issuecomment-66083768
-	var _old string
-	flag.StringVar(&_old, "oldrev", "", "")
+	flag.StringVar(&trigger.commit, "commit", "", "commit hash")
+	flag.StringVar(&trigger.owner, "change-owner", "", "change owner")
+	flag.StringVar(&trigger.patchset, "patchset", "", "patchset ID")
 
-	flag.Parse()
+	flag.StringVar(&targetBranch, "branch", "", "CL target branch")
+	flag.StringVar(&changeUrl, "change-url", "", "HTTPS URL of change")
 
-	if trigger.project == "" || trigger.ref == "" || trigger.commit == "" || trigger.submitter == "" {
-		// If we get here, the user is probably being a dummy and invoking
-		// this manually - but incorrectly.
-		return nil, fmt.Errorf("'ref-updated' hook invoked without required arguments")
-	}
+	// patchset-created also passes various flags which we don't need.
+	ignoreFlags([]string{"kind", "topic", "change", "uploader", "uploader-username", "change-owner-username"})
+
+	flag.Parse()
 
-	if trigger.project != "depot" || metaRegexp.MatchString(trigger.ref) {
-		// this is not an error, but also not something we handle.
+	// If the patchset is not for depot@canon then we can ignore it. It
+	// might be some other kind of change (refs/meta/config or
+	// Gerrit-internal), but it is not an error.
+	if trigger.project != "depot" || targetBranch != "canon" {
 		return nil, nil
 	}
 
-	if branchRegexp.MatchString(trigger.ref) {
-		// these refs don't need special handling, just move on
-		return &trigger, nil
-	}
+	// Change ID is not directly passed in the numeric format, so we
+	// need to extract it out of the URL
+	matches := changeIdRegexp.FindStringSubmatch(changeUrl)
+	trigger.changeId = matches[1]
 
-	if matches := patchsetRegexp.FindStringSubmatch(trigger.ref); matches != nil {
-		trigger.changeId = &matches[1]
-		trigger.patchset = &matches[2]
-		return &trigger, nil
-	}
+	// Construct the CL ref from which the build should happen.
+	changeId, _ := strconv.Atoi(trigger.changeId)
+	trigger.ref = fmt.Sprintf(
+		"refs/changes/%02d/%s/%s",
+		changeId%100, trigger.changeId, trigger.patchset,
+	)
 
-	return nil, fmt.Errorf("besadii does not support updates for this type of ref (%q)", trigger.ref)
+	return &trigger, nil
 }
 
-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.Fprintf(os.Stderr, "failed to open syslog: %s\n", err)
-		os.Exit(1)
-	}
+// Extract the buildtrigger struct out of the flags passed to besadii
+// when invoked as Gerrit's 'change-merged' hook. This hook is used
+// for triggering canon builds after change submission.
+func buildTriggerFromChangeMerged() *buildTrigger {
+	// Information that needs to be returned
+	var trigger buildTrigger
 
-	trigger, err := buildTriggerFromFlags()
-	if err != nil {
-		log.Err(fmt.Sprintf("failed to parse ref update: %s", err))
-		os.Exit(1)
+	// Information that is only needed for parsing
+	var targetBranch string
+
+	flag.StringVar(&trigger.project, "project", "", "Gerrit project")
+	flag.StringVar(&trigger.commit, "commit", "", "Commit hash")
+	flag.StringVar(&trigger.owner, "change-owner", "", "Change owner")
+	flag.StringVar(&targetBranch, "branch", "", "CL target branch")
+
+	// Ignore extra flags passed by change-merged
+	ignoreFlags([]string{"change", "topic", "change-url", "submitter", "submitter-username", "newrev", "change-owner-username"})
+
+	flag.Parse()
+
+	// Skip builds for anything other than depot@canon
+	if trigger.project != "depot" || targetBranch != "canon" {
+		return nil
 	}
 
-	if trigger == nil { // the project was not 'depot'
-		log.Err("build triggers are only supported for the 'depot' project")
+	trigger.ref = "refs/heads/canon"
+
+	return &trigger
+}
+
+func gerritHookMain(log *syslog.Writer, trigger *buildTrigger) {
+	if trigger == nil {
+		// The hook was not for something we care about.
 		os.Exit(0)
 	}
 
@@ -347,10 +379,28 @@ func postCommandMain() {
 }
 
 func main() {
+	// Logging happens in syslog because it's almost impossible to get
+	// output out of Gerrit hooks otherwise.
+	log, err := syslog.New(syslog.LOG_INFO|syslog.LOG_USER, "besadii")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to open syslog: %s\n", err)
+		os.Exit(1)
+	}
+
+	log.Info(fmt.Sprintf("besadii called with arguments: %v", os.Args))
+
 	bin := path.Base(os.Args[0])
 
-	if bin == "ref-updated" {
-		refUpdatedMain()
+	if bin == "patchset-created" {
+		trigger, err := buildTriggerFromPatchsetCreated()
+		if err != nil {
+			log.Crit("failed to parse 'patchset-created' invocation from args")
+			os.Exit(1)
+		}
+		gerritHookMain(log, trigger)
+	} else if bin == "change-merged" {
+		trigger := buildTriggerFromChangeMerged()
+		gerritHookMain(log, trigger)
 	} else if bin == "post-command" {
 		postCommandMain()
 	} else {
diff --git a/ops/modules/monorepo-gerrit.nix b/ops/modules/monorepo-gerrit.nix
index 69850fa15aaa..714aeeaa6ef1 100644
--- a/ops/modules/monorepo-gerrit.nix
+++ b/ops/modules/monorepo-gerrit.nix
@@ -5,7 +5,8 @@ let
   cfg = config.services.gerrit;
   gerritHooks = pkgs.runCommandNoCC "gerrit-hooks" {} ''
     mkdir -p $out
-    ln -s ${depot.ops.besadii}/bin/besadii $out/ref-updated
+    ln -s ${depot.ops.besadii}/bin/besadii $out/change-merged
+    ln -s ${depot.ops.besadii}/bin/besadii $out/patchset-created
   '';
 in {
   services.gerrit = {