From 757f53580fa9a91699c4404a8d0ba40e61f94a1a Mon Sep 17 00:00:00 2001 From: Heschi Kreinick Date: Wed, 25 May 2022 19:04:41 +0000 Subject: [PATCH] gerrit, internal/task: add Gerrit-related release tasks I refactored out the Gerrit-related code from MailDLCL, and added support for waiting for submit and creating tags. No test for creating tags but that logic has virtually nothing to it so I think I'm okay with it? For golang/go#51797. Change-Id: Ia8c24536bbee27e0b7bef04769ac5a81dc3021ab Reviewed-on: https://go-review.googlesource.com/c/build/+/408674 Reviewed-by: Dmitri Shuralyov TryBot-Result: Gopher Robot Reviewed-by: Alex Rakoczy Run-TryBot: Heschi Kreinick Reviewed-by: Dmitri Shuralyov --- gerrit/gerrit.go | 25 ++++++++- internal/task/dlcl.go | 18 ++---- internal/task/gerrit.go | 100 ++++++++++++++++++++++++++++++++++ internal/task/version.go | 36 ++++++++++++ internal/task/version_test.go | 53 ++++++++++++++++++ 5 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 internal/task/gerrit.go create mode 100644 internal/task/version.go create mode 100644 internal/task/version_test.go diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go index bdc93ab6..3ced0aac 100644 --- a/gerrit/gerrit.go +++ b/gerrit/gerrit.go @@ -66,7 +66,7 @@ type HTTPError struct { } func (e *HTTPError) Error() string { - return fmt.Sprintf("HTTP status %s; %s", e.Res.Status, e.Body) + return fmt.Sprintf("HTTP status %s on request to %s; %s", e.Res.Status, e.Res.Request.URL, e.Body) } // doArg is an optional argument for the Client.do method. @@ -760,6 +760,13 @@ func (c *Client) ChangeFileContentInChangeEdit(ctx context.Context, changeID str return err } +// DeleteFileInChangeEdit deletes a file from a change edit. +// +// See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#delete-edit-file. +func (c *Client) DeleteFileInChangeEdit(ctx context.Context, changeID string, path string) error { + return c.do(ctx, nil, "DELETE", "/changes/"+changeID+"/edit/"+url.QueryEscape(path), wantResStatus(http.StatusNoContent)) +} + // PublishChangeEdit promotes the change edit to a regular patch set. // // See https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#publish-edit. @@ -876,6 +883,22 @@ func (c *Client) GetProjectTags(ctx context.Context, name string) (map[string]Ta return m, nil } +// TagInput contains information for creating a tag. +// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#tag-input +type TagInput struct { + // Ref is optional, and when present must be equal to the URL parameter. Removed. + Revision string `json:"revision,omitempty"` + Message string `json:"message,omitempty"` +} + +// CreateTag creates a tag on project. +// See https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#create-tag. +func (c *Client) CreateTag(ctx context.Context, project, tag string, input TagInput) (TagInfo, error) { + var res TagInfo + err := c.do(ctx, &res, "PUT", fmt.Sprintf("/projects/%s/tags/%s", project, url.PathEscape(tag)), reqBodyJSON{&input}, wantResStatus(http.StatusCreated)) + return res, err +} + // GetAccountInfo gets the specified account's information from Gerrit. // For the API call, see https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#get-account // The accountID is https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#account-id diff --git a/internal/task/dlcl.go b/internal/task/dlcl.go index 061aedc8..65fcebc7 100644 --- a/internal/task/dlcl.go +++ b/internal/task/dlcl.go @@ -74,26 +74,16 @@ func MailDLCL(ctx *workflow.TaskContext, versions []string, e ExternalConfig) (c return "(dry-run)", nil } cl := gerrit.NewClient(e.GerritAPI.URL, e.GerritAPI.Auth) - c, err := cl.CreateChange(ctx, gerrit.ChangeInput{ + changeInput := gerrit.ChangeInput{ Project: "dl", Subject: "dl: add " + strings.Join(versions, " and "), Branch: "master", - }) + } + changeID, err := (&realGerritClient{client: cl}).CreateAutoSubmitChange(ctx, changeInput, files) if err != nil { return "", err } - changeID := fmt.Sprintf("%s~%d", c.Project, c.ChangeNumber) - for path, content := range files { - err := cl.ChangeFileContentInChangeEdit(ctx, changeID, path, content) - if err != nil { - return "", err - } - } - err = cl.PublishChangeEdit(ctx, changeID) - if err != nil { - return "", err - } - return fmt.Sprintf("https://go.dev/cl/%d", c.ChangeNumber), nil + return changeLink(changeID), nil } func verifyGoVersions(versions ...string) error { diff --git a/internal/task/gerrit.go b/internal/task/gerrit.go new file mode 100644 index 00000000..ccf5fa4e --- /dev/null +++ b/internal/task/gerrit.go @@ -0,0 +1,100 @@ +package task + +import ( + "context" + "fmt" + "strings" + "time" + + "golang.org/x/build/gerrit" +) + +type GerritClient interface { + // CreateAutoSubmitChange creates a change with the given metadata and contents, sets + // Run-TryBots and Auto-Submit, and returns its change ID. + // If the content of a file is empty, that file will be deleted from the repository. + CreateAutoSubmitChange(ctx context.Context, input gerrit.ChangeInput, contents map[string]string) (string, error) + // AwaitSubmit waits for the specified change to be auto-submitted or fail + // trybots. If the CL is submitted, returns the submitted commit hash. + AwaitSubmit(ctx context.Context, changeID string) (string, error) + // Tag creates a tag on project at the specified commit. + Tag(ctx context.Context, project, tag, commit string) error +} + +type realGerritClient struct { + client *gerrit.Client +} + +func (c *realGerritClient) CreateAutoSubmitChange(ctx context.Context, input gerrit.ChangeInput, files map[string]string) (string, error) { + change, err := c.client.CreateChange(ctx, input) + if err != nil { + return "", err + } + changeID := fmt.Sprintf("%s~%d", change.Project, change.ChangeNumber) + for path, content := range files { + if content == "" { + if err := c.client.DeleteFileInChangeEdit(ctx, changeID, path); err != nil { + return "", err + } + } else { + if err := c.client.ChangeFileContentInChangeEdit(ctx, changeID, path, content); err != nil { + return "", err + } + } + } + + if err := c.client.PublishChangeEdit(ctx, changeID); err != nil { + return "", err + } + if err := c.client.SetReview(ctx, changeID, "current", gerrit.ReviewInput{ + Labels: map[string]int{ + "Run-TryBot": 1, + "Auto-Submit": 1, + }, + }); err != nil { + return "", err + } + return changeID, nil +} + +func (c *realGerritClient) AwaitSubmit(ctx context.Context, changeID string) (string, error) { + for { + detail, err := c.client.GetChangeDetail(ctx, changeID, gerrit.QueryChangesOpt{ + Fields: []string{"CURRENT_REVISION", "DETAILED_LABELS"}, + }) + if err != nil { + return "", err + } + if detail.Status == "MERGED" { + return detail.CurrentRevision, nil + } + for _, approver := range detail.Labels["TryBot-Result"].All { + if approver.Value < 0 { + return "", fmt.Errorf("trybots failed on %v", changeLink(changeID)) + } + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(10 * time.Second): + } + } +} + +func (c *realGerritClient) Tag(ctx context.Context, project, tag, commit string) error { + _, err := c.client.CreateTag(ctx, project, tag, gerrit.TagInput{ + Revision: commit, + }) + return err +} + +// changeLink returns a link to the review page for the CL with the specified +// change ID. The change ID must be in the project~cl# form. +func changeLink(changeID string) string { + parts := strings.SplitN(changeID, "~", 3) + if len(parts) != 2 { + return fmt.Sprintf("(unparseable change ID %q)", changeID) + } + return "https://go.dev/cl/" + parts[1] +} diff --git a/internal/task/version.go b/internal/task/version.go new file mode 100644 index 00000000..e83c687e --- /dev/null +++ b/internal/task/version.go @@ -0,0 +1,36 @@ +package task + +import ( + "fmt" + + "golang.org/x/build/gerrit" + "golang.org/x/build/internal/workflow" +) + +// VersionTasks contains tasks related to versioning the release. +type VersionTasks struct { + Gerrit GerritClient + Project string +} + +// CreateAutoSubmitVersionCL mails an auto-submit change to update VERSION on branch. +func (t *VersionTasks) CreateAutoSubmitVersionCL(ctx *workflow.TaskContext, branch, version string) (string, error) { + return t.Gerrit.CreateAutoSubmitChange(ctx, gerrit.ChangeInput{ + Project: t.Project, + Branch: branch, + Subject: fmt.Sprintf("[%v] %v", branch, version), + }, map[string]string{ + "VERSION": version, + }) +} + +// AwaitCL waits for the specified CL to be submitted. +func (t *VersionTasks) AwaitCL(ctx *workflow.TaskContext, changeID string) (string, error) { + ctx.Printf("Awaiting review/submit of %v", changeLink(changeID)) + return t.Gerrit.AwaitSubmit(ctx, changeID) +} + +// TagRelease tags commit as version. +func (t *VersionTasks) TagRelease(ctx *workflow.TaskContext, version, commit string) (string, error) { + return "", t.Gerrit.Tag(ctx, t.Project, version, commit) +} diff --git a/internal/task/version_test.go b/internal/task/version_test.go new file mode 100644 index 00000000..08809838 --- /dev/null +++ b/internal/task/version_test.go @@ -0,0 +1,53 @@ +package task + +import ( + "context" + "flag" + "strings" + "testing" + + "golang.org/x/build/gerrit" + "golang.org/x/build/internal/workflow" +) + +var flagRunVersionTest = flag.Bool("run-version-test", false, "run version test, which will submit CLs to go.googlesource.com/scratch. Must have a Gerrit cookie in gitcookies.") + +func TestVersion(t *testing.T) { + if !*flagRunVersionTest { + t.Skip("Not enabled by flags") + } + cl := gerrit.NewClient("https://go-review.googlesource.com", gerrit.GitCookiesAuth()) + tasks := &VersionTasks{ + Gerrit: &realGerritClient{client: cl}, + Project: "scratch", + } + ctx := &workflow.TaskContext{ + Context: context.Background(), + Logger: &testLogger{t}, + } + + changeID, err := tasks.CreateAutoSubmitVersionCL(ctx, "master", "version string") + if err != nil { + t.Fatal(err) + } + _, err = tasks.AwaitCL(ctx, changeID) + if strings.Contains(err.Error(), "trybots failed") { + t.Logf("Trybots failed, as they usually do: %v. Abandoning CL and ending test.", err) + if err := cl.AbandonChange(ctx, changeID, "test is done"); err != nil { + t.Fatal(err) + } + return + } + + changeID, err = tasks.Gerrit.CreateAutoSubmitChange(ctx, gerrit.ChangeInput{ + Project: "scratch", + Branch: "master", + Subject: "Clean up VERSION", + }, map[string]string{"VERSION": ""}) + if err != nil { + t.Fatalf("cleaning up VERSION: %v", err) + } + if _, err := tasks.AwaitCL(ctx, changeID); err != nil { + t.Fatalf("cleaning up VERSION: %v", err) + } +}