| // Copyright 2024 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package task | |
| import ( | |
| "errors" | |
| "fmt" | |
| goversion "go/version" | |
| "regexp" | |
| "strings" | |
| "time" | |
| "golang.org/x/build/gerrit" | |
| "golang.org/x/build/internal/relui/groups" | |
| wf "golang.org/x/build/internal/workflow" | |
| ) | |
| // SecurityReleaseCoalesceTask is the workflow used to preparing patches for | |
| // minor security releases. The workflow is described in detail in | |
| // go/go-security-release-workflow. | |
| // | |
| // In short, this workflow: | |
| // 1. Checks that all patches are ready, indicated by two Code-Review+2's labels | |
| // and a Security-Patch-Ready+1 label (this is checked via submit requirements | |
| // rather than directly inspecting the labels) and lack of merge conflicts | |
| // 2. Creates a new branch from master HEAD | |
| // 3. Moves all patches from master onto the new branch | |
| // 4. Submits the rebased patches | |
| // 5. Create internal release branches | |
| // 6. Creates cherry-picks of the submitted patches onto the two release branches, | |
| // setting Commit-Queue+1 | |
| type SecurityReleaseCoalesceTask struct { | |
| PrivateGerrit GerritClient | |
| Version *VersionTasks | |
| } | |
| func (x *SecurityReleaseCoalesceTask) NewDefinition() *wf.Definition { | |
| // TODO: this is currently not particularly tolerant of failures that happen | |
| // half way through the workflow. Will need to think a bit about how we can | |
| // recover in failure situations that doesn't require manually cleaning a | |
| // bunch of stuff up before re-running the workflow. | |
| var numOnlyRe = regexp.MustCompile(`^\d+$`) | |
| wd := wf.New(wf.ACL{Groups: []string{groups.SecurityTeam}}) | |
| clNums := wf.Param(wd, wf.ParamDef[[]string]{ | |
| Name: "Security Patch CL Numbers", | |
| ParamType: wf.SliceShort, | |
| Doc: `Gerrit CL numbers for each security patch in a release`, | |
| Example: "123456", | |
| Check: func(nums []string) error { | |
| for _, num := range nums { | |
| if !numOnlyRe.MatchString(num) { | |
| return errors.New("CL numbers must contain only numbers") | |
| } | |
| } | |
| return nil | |
| }, | |
| }) | |
| // check CLs are ready | |
| cls := wf.Task1(wd, "Check changes", x.CheckChanges, clNums) | |
| // look up branch names | |
| branchInfo := wf.Task0(wd, "Get branch names", x.GetBranchNames, wf.After(cls)) | |
| // create checkpoint branch | |
| checkpointBranch := wf.Task1(wd, "Create checkpoint branch", x.CreateCheckpoint, branchInfo) | |
| // rebase changes to checkpoint branch | |
| cls = wf.Task2(wd, "Move changes onto checkpoint branch", x.MoveAndRebaseChanges, checkpointBranch, cls) | |
| // wait for changes to be submittable, and then submit them | |
| cls = wf.Task1(wd, "Await submissions", x.WaitAndSubmit, cls) | |
| // create internal release branches | |
| internalReleaseBranches := wf.Task1(wd, "Create internal release branches", x.CreateInternalReleaseBranches, branchInfo, wf.After(cls)) | |
| // create cherry-picks to internal release branches | |
| cherryPicks := wf.Task2(wd, "Create cherry-picks", x.CreateCherryPicks, internalReleaseBranches, cls) | |
| wf.Output(wd, "Cherry-picks", cherryPicks) | |
| return wd | |
| } | |
| type branchInfo struct { | |
| CheckpointName string | |
| PublicReleaseBranches []string | |
| } | |
| func (x *SecurityReleaseCoalesceTask) GetBranchNames(ctx *wf.TaskContext) (branchInfo, error) { | |
| currentMajor, _, err := x.Version.GetCurrentMajor(ctx.Context) | |
| if err != nil { | |
| return branchInfo{}, err | |
| } | |
| nextMinors, err := x.Version.GetNextMinorVersions(ctx.Context, []int{currentMajor, currentMajor - 1}) | |
| if err != nil { | |
| return branchInfo{}, err | |
| } | |
| return branchInfo{ | |
| CheckpointName: fmt.Sprintf("%s-and-%s-checkpoint", nextMinors[0], nextMinors[1]), | |
| PublicReleaseBranches: []string{ | |
| fmt.Sprintf("release-branch.%s", nextMinors[0]), | |
| fmt.Sprintf("release-branch.%s", nextMinors[1]), | |
| }, | |
| }, nil | |
| } | |
| func (x *SecurityReleaseCoalesceTask) CheckChanges(ctx *wf.TaskContext, clNums []string) ([]*gerrit.ChangeInfo, error) { | |
| var cls []*gerrit.ChangeInfo | |
| for _, num := range clNums { | |
| ci, err := x.PrivateGerrit.GetChange(ctx.Context, num, gerrit.QueryChangesOpt{Fields: []string{"SUBMITTABLE"}}) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if !ci.Submittable { | |
| return nil, fmt.Errorf("Change %s is not submittable", internalGerritChangeURL(num)) | |
| } | |
| ra, err := x.PrivateGerrit.GetRevisionActions(ctx.Context, num, "current") | |
| if err != nil { | |
| return nil, err | |
| } | |
| if ra["submit"] == nil || !ra["submit"].Enabled { | |
| return nil, fmt.Errorf("Change %s is not submittable", internalGerritChangeURL(num)) | |
| } | |
| cls = append(cls, ci) | |
| } | |
| return cls, nil | |
| } | |
| func (x *SecurityReleaseCoalesceTask) CreateCheckpoint(ctx *wf.TaskContext, bi branchInfo) (string, error) { | |
| publicHead, err := x.PrivateGerrit.ReadBranchHead(ctx.Context, "go", "public") | |
| if err != nil { | |
| return "", err | |
| } | |
| if _, err := x.PrivateGerrit.CreateBranch(ctx.Context, "go", bi.CheckpointName, gerrit.BranchInput{Revision: publicHead}); err != nil { | |
| return "", err | |
| } | |
| return bi.CheckpointName, nil | |
| } | |
| func (x *SecurityReleaseCoalesceTask) MoveAndRebaseChanges(ctx *wf.TaskContext, checkpointBranch string, cls []*gerrit.ChangeInfo) ([]*gerrit.ChangeInfo, error) { | |
| for i, ci := range cls { | |
| newCI, err := x.PrivateGerrit.MoveChange(ctx.Context, ci.ChangeID, checkpointBranch, true) | |
| if err != nil { | |
| return nil, err | |
| } | |
| newCI, err = x.PrivateGerrit.RebaseChange(ctx.Context, ci.ChangeID, "") | |
| if err != nil { | |
| return nil, err | |
| } | |
| cls[i] = &newCI | |
| } | |
| return cls, nil | |
| } | |
| func (x *SecurityReleaseCoalesceTask) WaitAndSubmit(ctx *wf.TaskContext, cls []*gerrit.ChangeInfo) ([]*gerrit.ChangeInfo, error) { | |
| if _, err := AwaitCondition(ctx, time.Second*10, func() (string, bool, error) { | |
| unsubmitted := len(cls) | |
| for i, change := range cls { | |
| if change.Status == gerrit.ChangeStatusMerged { | |
| unsubmitted-- | |
| continue | |
| } | |
| ci, err := x.PrivateGerrit.GetChange(ctx, change.ChangeID, gerrit.QueryChangesOpt{Fields: []string{"SUBMITTABLE"}}) | |
| if err != nil { | |
| return "", false, err | |
| } | |
| if !ci.Submittable { | |
| continue | |
| } | |
| submitted, err := x.PrivateGerrit.SubmitChange(ctx, ci.ChangeID) | |
| if err != nil { | |
| return "", false, err | |
| } | |
| cls[i] = &submitted | |
| unsubmitted-- | |
| } | |
| if unsubmitted == 0 { | |
| return "", true, nil | |
| } | |
| return "", false, nil | |
| }); err != nil { | |
| return nil, err | |
| } | |
| return cls, nil | |
| } | |
| // majorFromMinor converts a release branch name from it's minor version form to | |
| // it's major version form (i.e. release-branch.go1.2.3 to | |
| // release-branch.go1.2). | |
| func majorFromMinor(branch string) string { | |
| stripped := strings.TrimPrefix(branch, "release-branch.") | |
| major := goversion.Lang(stripped) | |
| return "release-branch." + major | |
| } | |
| var internalReleaseBranchPrefix = "internal-" | |
| func (x *SecurityReleaseCoalesceTask) CreateInternalReleaseBranches(ctx *wf.TaskContext, bi branchInfo) ([]string, error) { | |
| var internalBranches []string | |
| for _, nextMinor := range bi.PublicReleaseBranches { | |
| publicHead, err := x.PrivateGerrit.ReadBranchHead(ctx.Context, "go", majorFromMinor(nextMinor)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| internalReleaseBranch := internalReleaseBranchPrefix + nextMinor | |
| if _, err := x.PrivateGerrit.CreateBranch(ctx.Context, "go", internalReleaseBranch, gerrit.BranchInput{Revision: publicHead}); err != nil { | |
| return nil, err | |
| } | |
| internalBranches = append(internalBranches, internalReleaseBranch) | |
| } | |
| return internalBranches, nil | |
| } | |
| func (x *SecurityReleaseCoalesceTask) CreateCherryPicks(ctx *wf.TaskContext, releaseBranches []string, cls []*gerrit.ChangeInfo) (map[string][]string, error) { | |
| // TODO: this currently assumes we want to cherry-pick everything to both | |
| // branches, which is _normally_ the case, but sometimes is not accurate. We | |
| // can manually just abandon cherry-picks we don't care about, but probably | |
| // we should have a way to indicate which branches we want each patch | |
| // cherry-picked onto. | |
| cherryPicks := map[string][]string{} | |
| for _, ci := range cls { | |
| for _, releaseBranch := range releaseBranches { | |
| commitMessage, err := x.PrivateGerrit.GetCommitMessage(ctx.Context, ci.ID) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // TODO: might be cleaner to just pass this information from CreateInternalReleaseBranches | |
| commitMessage = fmt.Sprintf("[%s] %s", majorFromMinor(strings.TrimPrefix(releaseBranch, internalReleaseBranchPrefix)), commitMessage) | |
| cpCI, conflicts, err := x.PrivateGerrit.CreateCherryPick(ctx.Context, ci.ID, releaseBranch, commitMessage) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if conflicts { | |
| ctx.Logger.Printf("Cherry-pick of %d has merge conflicts against %s: %s", internalGerritChangeURL(ci.ChangeNumber), releaseBranch, internalGerritChangeURL(cpCI.ChangeNumber)) | |
| } | |
| cherryPicks[releaseBranch] = append(cherryPicks[releaseBranch], internalGerritChangeURL(cpCI.ChangeNumber)) | |
| } | |
| } | |
| return cherryPicks, nil | |
| } | |
| // internalGerritChangeURL can take either a int or string and return the | |
| // relevant CL URL for a change number. | |
| func internalGerritChangeURL[T int | string](clNum T) string { | |
| return fmt.Sprintf("https://go-internal-review.git.corp.google.com/c/go/+/%v", clNum) | |
| } |