diff --git a/internal/task/fakes.go b/internal/task/fakes.go index c72d6372..8f64368c 100644 --- a/internal/task/fakes.go +++ b/internal/task/fakes.go @@ -220,9 +220,14 @@ func (g *FakeGerrit) ReadBranchHead(ctx context.Context, project, branch string) if err != nil { return "", err } - // TODO: If the branch doesn't exist, return an error matching gerrit.ErrResourceNotExist. out, err := repo.dir.RunCommand(ctx, "rev-parse", "refs/heads/"+branch) if err != nil { + // TODO(hxjiang): switch to git show-ref --exists refs/heads/branch after + // upgrade git to 2.43.0. + // https://git-scm.com/docs/git-show-ref/2.43.0#Documentation/git-show-ref.txt---exists + if strings.Contains(err.Error(), "unknown revision or path not in the working tree") { + return "", gerrit.ErrResourceNotExist + } // Returns empty string if the error is nil to align the same behavior with // the real Gerrit client. return "", err diff --git a/internal/task/releasegopls.go b/internal/task/releasegopls.go index b7a1251c..c6fbe0d7 100644 --- a/internal/task/releasegopls.go +++ b/internal/task/releasegopls.go @@ -551,6 +551,8 @@ func parseSemver(v string) (_ semversion, ok bool) { return parsed, ok } +// prereleaseVersion extracts the integer component from a pre-release version +// string in the format "${STRING}.${INT}". func (s *semversion) prereleaseVersion() (int, error) { parts := strings.Split(s.Pre, ".") if len(parts) == 1 { diff --git a/internal/task/releasevscodego.go b/internal/task/releasevscodego.go index bd19ce46..b96d9583 100644 --- a/internal/task/releasevscodego.go +++ b/internal/task/releasevscodego.go @@ -6,11 +6,13 @@ package task import ( _ "embed" + "errors" "fmt" "strconv" "strings" "github.com/google/go-github/v48/github" + "golang.org/x/build/gerrit" "golang.org/x/build/internal/relui/groups" "golang.org/x/build/internal/workflow" wf "golang.org/x/build/internal/workflow" @@ -130,6 +132,7 @@ func (r *ReleaseVSCodeGoTasks) NewPrereleaseDefinition() *wf.Definition { approved := wf.Action1(wd, "await release coordinator's approval", r.approveVersion, semv) _ = wf.Task1(wd, "create release milestone and issue", r.createReleaseMilestoneAndIssue, semv, wf.After(approved)) + _ = wf.Action1(wd, "create release branch", r.createReleaseBranch, semv, wf.After(approved)) return wd } @@ -173,6 +176,52 @@ func (r *ReleaseVSCodeGoTasks) createReleaseMilestoneAndIssue(ctx *wf.TaskContex return *issue.Number, nil } +// createReleaseBranch creates corresponding release branch only for the initial +// release candidate of a minor version. +func (r *ReleaseVSCodeGoTasks) createReleaseBranch(ctx *wf.TaskContext, semv semversion) error { + branch := fmt.Sprintf("release-v%v.%v", semv.Major, semv.Minor) + releaseHead, err := r.Gerrit.ReadBranchHead(ctx, "vscode-go", branch) + + if err == nil { + ctx.Printf("Found the release branch %q with head pointing to %s\n", branch, releaseHead) + return nil + } + + if !errors.Is(err, gerrit.ErrResourceNotExist) { + return fmt.Errorf("failed to read the release branch: %w", err) + } + + // Require vscode release branch existence if this is a non-minor release. + if semv.Patch != 0 { + return fmt.Errorf("release branch is required for patch releases: %w", err) + } + + rc, err := semv.prereleaseVersion() + if err != nil { + return err + } + + // Require vscode release branch existence if this is not the first rc in + // a minor release. + if rc != 1 { + return fmt.Errorf("release branch is required for non-initial release candidates: %w", err) + } + + // Create the release branch using the revision from the head of master branch. + head, err := r.Gerrit.ReadBranchHead(ctx, "vscode-go", "master") + if err != nil { + return err + } + + ctx.DisableRetries() // Beyond this point we want retries to be done manually, not automatically. + _, err = r.Gerrit.CreateBranch(ctx, "vscode-go", branch, gerrit.BranchInput{Revision: head}) + if err != nil { + return err + } + ctx.Printf("Created branch %q at revision %s.\n", branch, head) + return nil +} + // nextPrereleaseVersion determines the next pre-release version for the // upcoming stable release of vscode-go by examining all existing tags in the // repository. diff --git a/internal/task/releasevscodego_test.go b/internal/task/releasevscodego_test.go index 839116da..72aab418 100644 --- a/internal/task/releasevscodego_test.go +++ b/internal/task/releasevscodego_test.go @@ -6,6 +6,7 @@ package task import ( "context" + "fmt" "testing" "github.com/google/go-github/v48/github" @@ -79,6 +80,83 @@ func TestCreateReleaseMilestoneAndIssue(t *testing.T) { } } +func TestCreateReleaseBranch(t *testing.T) { + ctx := context.Background() + testcases := []struct { + name string + version string + existingBranch bool + wantErr bool + }{ + { + name: "nil if the release branch does not exist for first rc in a minor release", + version: "v0.44.0-rc.1", + existingBranch: false, + wantErr: false, + }, + { + name: "nil if the release branch already exist for non-initial rc in a minor release", + version: "v0.44.0-rc.4", + existingBranch: true, + wantErr: false, + }, + { + name: "fail if the release branch does not exist for non-initial rc in a minor release", + version: "v0.44.0-rc.4", + existingBranch: false, + wantErr: true, + }, + { + name: "nil if the release branch already exist for a patch version", + version: "v0.44.3-rc.3", + existingBranch: true, + wantErr: false, + }, + { + name: "fail if the release branch does not exist for a patch version", + version: "v0.44.3-rc.3", + existingBranch: false, + wantErr: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + semv, ok := parseSemver(tc.version) + if !ok { + t.Fatalf("failed to parse the want version: %q", tc.version) + } + + vscodego := NewFakeRepo(t, "vscode-go") + commit := vscodego.Commit(map[string]string{ + "go.mod": "module github.com/golang/vscode-go\n", + "go.sum": "\n", + }) + if tc.existingBranch { + vscodego.Branch(fmt.Sprintf("release-v%v.%v", semv.Major, semv.Minor), commit) + } + + gerrit := NewFakeGerrit(t, vscodego) + tasks := &ReleaseVSCodeGoTasks{ + Gerrit: gerrit, + } + + err := tasks.createReleaseBranch(&workflow.TaskContext{Context: ctx, Logger: &testLogger{t, ""}}, semv) + if tc.wantErr && err == nil { + t.Errorf("createReleaseBranch(%q) should return error but return nil", tc.version) + } else if !tc.wantErr && err != nil { + t.Errorf("createReleaseBranch(%q) should return nil but return err: %v", tc.version, err) + } + + if !tc.wantErr { + if _, err := gerrit.ReadBranchHead(ctx, "vscode-go", fmt.Sprintf("release-v%v.%v", semv.Major, semv.Minor)); err != nil { + t.Errorf("createReleaseBranch(%q) should ensure the release branch creation: %v", tc.version, err) + } + } + }) + } +} + func TestNextPrereleaseVersion(t *testing.T) { tests := []struct { name string