[cl-util] Handle transient errors when editing files

Editing files may encounter transient errors, for instance, when the
operation encounters a 404 due to the change not being visible.

Add internal retries for file edits to the `create-cl` command to handle
transient errors.

Bug: 55073
Change-Id: I184770706299156f3d59530dbb56f80fb94e2228
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/402617
Commit-Queue: Anthony Fandrianto <atyfto@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/cmd/cl-util/create_cl.go b/cmd/cl-util/create_cl.go
index d289dbe..78fdd1b 100644
--- a/cmd/cl-util/create_cl.go
+++ b/cmd/cl-util/create_cl.go
@@ -11,9 +11,12 @@
 	"fmt"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/maruel/subcommands"
 	"go.chromium.org/luci/auth"
+	"go.chromium.org/luci/common/retry"
+	"go.chromium.org/luci/common/retry/transient"
 )
 
 func cmdCreateCL(authOpts auth.Options) *subcommands.Command {
@@ -125,8 +128,20 @@
 	if err != nil {
 		return err
 	}
+	// Retry transient failures on edits.
+	editRetryPolicy := transient.Only(func() retry.Iterator {
+		return &retry.ExponentialBackoff{
+			Limited: retry.Limited{
+				Delay:   20 * time.Second,
+				Retries: 3,
+			},
+			Multiplier: 1,
+		}
+	})
 	for _, edit := range c.edits {
-		if err := gerritClient.editFile(ctx, changeInfo.Number, edit.filepath, edit.contents); err != nil {
+		if err := retry.Retry(ctx, editRetryPolicy, func() error {
+			return gerritClient.editFile(ctx, changeInfo.Number, edit.filepath, edit.contents)
+		}, nil); err != nil {
 			return err
 		}
 	}
diff --git a/cmd/cl-util/gerrit.go b/cmd/cl-util/gerrit.go
index b27b409..e17b915 100644
--- a/cmd/cl-util/gerrit.go
+++ b/cmd/cl-util/gerrit.go
@@ -7,6 +7,7 @@
 import (
 	"context"
 	"fmt"
+	"strings"
 
 	"go.chromium.org/luci/auth"
 	"go.chromium.org/luci/common/api/gerrit"
@@ -58,7 +59,12 @@
 		FilePath: filepath,
 		Content:  []byte(content),
 	}); err != nil {
-		return fmt.Errorf("failed to edit file: %v", err)
+		errorMsg := fmt.Sprintf("failed to edit %s: %v", filepath, err)
+		if strings.Contains(err.Error(), "404") {
+			// 404 error may be transient, as change may not yet be visible.
+			return errors.New(errorMsg, transient.Tag)
+		}
+		return errors.New(errorMsg)
 	}
 	return nil
 }