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 <dmitshur@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
This commit is contained in:
Heschi Kreinick 2022-05-25 19:04:41 +00:00
Родитель 1d197f9e3c
Коммит 757f53580f
5 изменённых файлов: 217 добавлений и 15 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -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 {

100
internal/task/gerrit.go Normal file
Просмотреть файл

@ -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]
}

36
internal/task/version.go Normal file
Просмотреть файл

@ -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)
}

Просмотреть файл

@ -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)
}
}