internal/task: use milestone metadata to derive security patch changelists
Updates golang/go#76157
Change-Id: I76f6b9c6f75dc3dd331657967641194c84fe2998
Reviewed-on: https://go-review.googlesource.com/c/build/+/696936
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/go.mod b/go.mod
index 7173b51..c417eb6 100644
--- a/go.mod
+++ b/go.mod
@@ -71,6 +71,7 @@
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.34.2
gopkg.in/inf.v0 v0.9.1
+ gopkg.in/yaml.v3 v3.0.1
rsc.io/github v0.3.1-0.20240418182958-01bebb0c456a
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef
)
diff --git a/internal/task/security_release_coalesce.go b/internal/task/security_release_coalesce.go
index 8f0f4f6..58ad3dc 100644
--- a/internal/task/security_release_coalesce.go
+++ b/internal/task/security_release_coalesce.go
@@ -5,10 +5,12 @@
package task
import (
+ "bytes"
"errors"
"fmt"
goversion "go/version"
"net/http"
+ "path"
"regexp"
"strings"
"time"
@@ -16,6 +18,7 @@
"golang.org/x/build/gerrit"
"golang.org/x/build/internal/relui/groups"
wf "golang.org/x/build/internal/workflow"
+ yaml "gopkg.in/yaml.v3"
)
// SecurityReleaseCoalesceTask is the workflow used to preparing patches for
@@ -46,23 +49,21 @@
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`,
+ milestoneNum := wf.Param(wd, wf.ParamDef[string]{
+ Name: "Release Milestone",
+ ParamType: wf.BasicString,
+ Doc: `Release milestone for the security patch(es) being released.`,
Example: "123456",
- Check: func(nums []string) error {
- for _, num := range nums {
- if !numOnlyRe.MatchString(num) {
- return errors.New("CL numbers must contain only numbers")
- }
+ Check: func(num string) error {
+ if !numOnlyRe.MatchString(num) {
+ return errors.New("Milestone number must contain only numbers")
}
return nil
},
})
// check CLs are ready
- cls := wf.Task1(wd, "Check changes", x.CheckChanges, clNums)
+ cls := wf.Task1(wd, "Check changes", x.CheckChanges, milestoneNum)
// look up branch names
branchInfo := wf.Task0(wd, "Get branch names", x.GetBranchNames, wf.After(cls))
// create checkpoint branch
@@ -86,6 +87,8 @@
}
func (x *SecurityReleaseCoalesceTask) GetBranchNames(ctx *wf.TaskContext) (branchInfo, error) {
+ // TODO: consider using the release milestone to derive
+ // the active version patch and backports?
currentMajor, _, err := x.Version.GetCurrentMajor(ctx)
if err != nil {
return branchInfo{}, err
@@ -123,24 +126,60 @@
}
}
-func (x *SecurityReleaseCoalesceTask) CheckChanges(ctx *wf.TaskContext, clNums []string) ([]*gerrit.ChangeInfo, error) {
+// ReleaseMilestone contains all
+// patches and their respective
+// metadata for a given release.
+type ReleaseMilestone struct {
+ BuganizerID int `yaml:"buganizer_id"`
+ Patches []*SecurityPatch `yaml:"security_patches"`
+}
+
+// SecurityPatch is a subset of the
+// required metadata to release all
+// patches contained by a milestone.
+type SecurityPatch struct {
+ Changelists []string `yaml:"changelists"`
+ TargetedReleases []string `yaml:"target_releases"`
+}
+
+func (x *SecurityReleaseCoalesceTask) CheckChanges(ctx *wf.TaskContext, milestoneNum string) ([]*gerrit.ChangeInfo, error) {
+ const project = "security-metadata"
var cls []*gerrit.ChangeInfo
- for _, num := range clNums {
- ci, err := x.PrivateGerrit.GetChange(ctx, num, gerrit.QueryChangesOpt{Fields: []string{"SUBMITTABLE"}})
- if err != nil {
- return nil, err
+
+ head, err := x.PrivateGerrit.ReadBranchHead(ctx, project, "main")
+ if err != nil {
+ return nil, err
+ }
+
+ buf, err := x.PrivateGerrit.ReadFile(ctx, project, head, path.Join("data", "milestones", milestoneNum+".yaml"))
+ if err != nil {
+ return nil, err
+ }
+
+ var rm ReleaseMilestone
+ if err := yaml.NewDecoder(bytes.NewReader(buf)).Decode(&rm); err != nil {
+ return nil, fmt.Errorf("cannot read milestone: %v", err)
+ }
+
+ for _, patch := range rm.Patches {
+ for _, url := range patch.Changelists {
+ _, num, _ := strings.Cut(url, "/+/")
+ ci, err := x.PrivateGerrit.GetChange(ctx, 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, 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)
}
- if !ci.Submittable {
- return nil, fmt.Errorf("Change %s is not submittable", internalGerritChangeURL(num))
- }
- ra, err := x.PrivateGerrit.GetRevisionActions(ctx, 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
}
@@ -232,6 +271,9 @@
var internalReleaseBranchPrefix = "internal-"
func (x *SecurityReleaseCoalesceTask) CreateInternalReleaseBranches(ctx *wf.TaskContext, bi branchInfo) ([]string, error) {
+ // TODO: update step to commit the metadata
+ // about the submitted changes and their
+ // branch hashes to security-metadata.
var internalBranches []string
for _, next := range bi.PublicReleaseBranches {
publicHead, err := x.PrivateGerrit.ReadBranchHead(ctx, "go", majorFromMinor(next))
diff --git a/internal/task/security_release_coalesce_test.go b/internal/task/security_release_coalesce_test.go
index f456628..260999f 100644
--- a/internal/task/security_release_coalesce_test.go
+++ b/internal/task/security_release_coalesce_test.go
@@ -9,6 +9,7 @@
"crypto/rand"
"errors"
"fmt"
+ "path"
"slices"
"strings"
"testing"
@@ -105,10 +106,8 @@
if project != "go" {
return gerrit.TagInfo{}, gerrit.ErrResourceNotExist
}
- for _, t := range c.tags {
- if tag == t {
- return gerrit.TagInfo{Created: gerrit.TimeStamp(time.Now())}, nil
- }
+ if slices.Contains(c.tags, tag) {
+ return gerrit.TagInfo{Created: gerrit.TimeStamp(time.Now())}, nil
}
return gerrit.TagInfo{}, gerrit.ErrResourceNotExist
}
@@ -132,14 +131,25 @@
})
}
+const milestoneYAML = `buganizer_id: 100001
+security_patches:
+ - is_toolchain: false
+ package: runtime
+ changelists:
+ - https://go-internal-review.git.corp.google.com/c/security-metadata/+/1234
+ - https://go-internal-review.git.corp.google.com/c/security-metadata/+/5678
+ target_releases:
+ - go1.3.1
+ - go1.4.1`
+
func testSecurityReleaseCoalesceTask(t *testing.T, withNextReleaseBranch bool) {
publicTags := []string{"go1.3", "go1.3.1", "go1.4", "go1.4.1"}
publicBranches := []string{"release-branch.go1.3", "release-branch.go1.4"}
if withNextReleaseBranch {
publicBranches = append(publicBranches, "release-branch.go1.5")
}
- privRepo := NewFakeRepo(t, "go")
- privGerrit := &fakeCoalesceGerrit{FakeGerrit: NewFakeGerrit(t, privRepo), cherryPicks: map[string][]cherryPickedCommit{}, commitMessages: map[string]string{}}
+ privGoRepo, smRepo := NewFakeRepo(t, "go"), NewFakeRepo(t, "security-metadata")
+ privGerrit := &fakeCoalesceGerrit{FakeGerrit: NewFakeGerrit(t, privGoRepo, smRepo), cherryPicks: map[string][]cherryPickedCommit{}, commitMessages: map[string]string{}}
task := &SecurityReleaseCoalesceTask{
PrivateGerrit: privGerrit,
Version: &VersionTasks{
@@ -176,17 +186,21 @@
other body`,
}
- head := privRepo.History()[0]
- privRepo.Branch("public", head)
- privRepo.Branch("release-branch.go1.3", head)
- privRepo.Branch("release-branch.go1.4", head)
+ head := privGoRepo.History()[0]
+ privGoRepo.Branch("public", head)
+ privGoRepo.Branch("release-branch.go1.3", head)
+ privGoRepo.Branch("release-branch.go1.4", head)
if withNextReleaseBranch {
- privRepo.Branch("release-branch.go1.5", head)
+ privGoRepo.Branch("release-branch.go1.5", head)
}
+ head = smRepo.History()[0]
+ smRepo.Branch("main", head)
+ smRepo.CommitOnBranch("main", map[string]string{path.Join("data", "milestones", "100001.yaml"): milestoneYAML})
+
wd := task.NewDefinition()
w, err := wf.Start(wd, map[string]any{
- "Security Patch CL Numbers": []string{"1234", "5678"},
+ "Release Milestone": "100001",
})
if err != nil {
t.Fatal(err)
@@ -203,7 +217,7 @@
if withNextReleaseBranch {
checkpointBranch = "go1.5rc1-go1.4.2-go1.3.2-checkpoint"
}
- commits := len(strings.Split(string(privRepo.runGit("log", checkpointBranch, "--format=%H")), "\n")) - 1
+ commits := len(strings.Split(string(privGoRepo.runGit("log", checkpointBranch, "--format=%H")), "\n")) - 1
if commits != 3 {
t.Errorf("unexpected number of commits on checkpoint branch: got %d, want 3", commits)
}