|
| 1 | +// Copyright 2024 The Go Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style |
| 3 | +// license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +package task |
| 6 | + |
| 7 | +import ( |
| 8 | + "bytes" |
| 9 | + "errors" |
| 10 | + "fmt" |
| 11 | + "net/mail" |
| 12 | + "regexp" |
| 13 | + "slices" |
| 14 | + "text/template" |
| 15 | + "time" |
| 16 | + |
| 17 | + "golang.org/x/build/gerrit" |
| 18 | + wf "golang.org/x/build/internal/workflow" |
| 19 | +) |
| 20 | + |
| 21 | +type PrivXPatch struct { |
| 22 | + PublicGerrit GerritClient |
| 23 | + PrivateGerrit GerritClient |
| 24 | + // PublicRepoURL returns a git clone URL for repo |
| 25 | + PublicRepoURL func(repo string) string |
| 26 | + |
| 27 | + ApproveAction func(*wf.TaskContext) error |
| 28 | + SendMail func(MailHeader, MailContent) error |
| 29 | + AnnounceMailHeader MailHeader |
| 30 | +} |
| 31 | + |
| 32 | +func (x *PrivXPatch) NewDefinition(tagx *TagXReposTasks) *wf.Definition { |
| 33 | + wd := wf.New() |
| 34 | + // TODO: this should be simpler, CL number + patchset? |
| 35 | + clNumber := wf.Param(wd, wf.ParamDef[string]{Name: "go-internal CL number", Example: "536316"}) |
| 36 | + reviewers := wf.Param(wd, reviewersParam) |
| 37 | + repoName := wf.Param(wd, wf.ParamDef[string]{Name: "Repository name", Example: "net"}) |
| 38 | + // TODO: probably always want to skip, might make sense to not include this |
| 39 | + skipPostSubmit := wf.Param(wd, wf.ParamDef[bool]{Name: "Skip post submit result (optional)", ParamType: wf.Bool}) |
| 40 | + cve := wf.Param(wd, wf.ParamDef[string]{Name: "CVE"}) |
| 41 | + githubIssue := wf.Param(wd, wf.ParamDef[string]{Name: "GitHub issue", Doc: "The GitHub issue number of the report.", Example: "#12345"}) |
| 42 | + relNote := wf.Param(wd, wf.ParamDef[string]{Name: "Release note", ParamType: wf.LongString}) |
| 43 | + acknowledgement := wf.Param(wd, wf.ParamDef[string]{Name: "Acknowledgement"}) |
| 44 | + |
| 45 | + repos := wf.Task0(wd, "Load all repositories", tagx.SelectRepos) |
| 46 | + |
| 47 | + repos = wf.Task4(wd, "Publish change", func(ctx *wf.TaskContext, clNumber string, reviewers []string, repos []TagRepo, repoName string) ([]TagRepo, error) { |
| 48 | + if !slices.ContainsFunc(repos, func(r TagRepo) bool { return r.Name == repoName }) { |
| 49 | + return nil, fmt.Errorf("no repository %q", repoName) |
| 50 | + } |
| 51 | + |
| 52 | + changeInfo, err := x.PrivateGerrit.GetChange(ctx, clNumber, gerrit.QueryChangesOpt{Fields: []string{"CURRENT_REVISION"}}) |
| 53 | + if err != nil { |
| 54 | + return nil, err |
| 55 | + } |
| 56 | + if changeInfo.Project != repoName { |
| 57 | + return nil, fmt.Errorf("CL is for unexpected project, got: %s, want %s", changeInfo.Project, repoName) |
| 58 | + } |
| 59 | + if changeInfo.Status != gerrit.ChangeStatusMerged { |
| 60 | + return nil, fmt.Errorf("CL %s not merged, status is %s", clNumber, changeInfo.Status) |
| 61 | + } |
| 62 | + rev, ok := changeInfo.Revisions[changeInfo.CurrentRevision] |
| 63 | + if !ok { |
| 64 | + return nil, errors.New("current revision not found") |
| 65 | + } |
| 66 | + fetch, ok := rev.Fetch["http"] |
| 67 | + if !ok { |
| 68 | + return nil, errors.New("fetch info not found") |
| 69 | + } |
| 70 | + origin, ref := fetch.URL, fetch.Ref |
| 71 | + |
| 72 | + // We directly use Git here, rather than the Gerrit API, as there are |
| 73 | + // limitations to the types of patches which you can create using said |
| 74 | + // API. In particular patches which contain any binary content are hard |
| 75 | + // to replicate from one instance to another using the API alone. Rather |
| 76 | + // than adding workarounds for those edge cases, we just use Git |
| 77 | + // directly, which makes the process extremely simple. |
| 78 | + git := &Git{} |
| 79 | + |
| 80 | + repo, err := git.Clone(ctx, x.PublicRepoURL(repoName)) |
| 81 | + if err != nil { |
| 82 | + return nil, err |
| 83 | + } |
| 84 | + ctx.Printf("cloned repo into %s", repo.dir) |
| 85 | + |
| 86 | + ctx.Printf("fetching %s from %s", ref, origin) |
| 87 | + if _, err := repo.RunCommand(ctx.Context, "fetch", origin, ref); err != nil { |
| 88 | + return nil, err |
| 89 | + } |
| 90 | + ctx.Printf("fetched") |
| 91 | + if _, err := repo.RunCommand(ctx.Context, "cherry-pick", "FETCH_HEAD"); err != nil { |
| 92 | + return nil, err |
| 93 | + } |
| 94 | + ctx.Printf("cherry-picked") |
| 95 | + refspec := "HEAD:refs/for/master%l=Auto-Submit,l=Commit-Queue+1" |
| 96 | + for _, reviewer := range reviewers { |
| 97 | + refspec += ",r=" + reviewer |
| 98 | + } |
| 99 | + |
| 100 | + // Beyond this point we don't want to retry any of the following steps. |
| 101 | + ctx.DisableRetries() |
| 102 | + |
| 103 | + ctx.Printf("pusing to %s", x.PublicRepoURL(repoName)) |
| 104 | + // We are unable to use repo.RunCommand here, because of strange i/o |
| 105 | + // changes that git made. The messages sent by the remote are printed by |
| 106 | + // git to stderr, and no matter what combination of options you pass it |
| 107 | + // (--verbose, --porcelain, etc), you cannot reasonably convince it to |
| 108 | + // print those messages to stdout. Because of this we need to use the |
| 109 | + // underlying repo.git.runGitStreamed method, so that we can inspect |
| 110 | + // stderr in order to extract the new CL number that gerrit sends us. |
| 111 | + var stdout, stderr bytes.Buffer |
| 112 | + err = repo.git.runGitStreamed(ctx.Context, &stdout, &stderr, repo.dir, "push", x.PublicRepoURL(repoName), refspec) |
| 113 | + if err != nil { |
| 114 | + return nil, err |
| 115 | + } |
| 116 | + |
| 117 | + // Extract the CL number from the output using a quick and dirty regex. |
| 118 | + re, err := regexp.Compile(fmt.Sprintf(`https:\/\/go-review.googlesource.com\/c\/%s\/\+\/(\d+)`, regexp.QuoteMeta(repoName))) |
| 119 | + if err != nil { |
| 120 | + return nil, err |
| 121 | + } |
| 122 | + matches := re.FindSubmatch(stderr.Bytes()) |
| 123 | + if len(matches) != 2 { |
| 124 | + return nil, errors.New("unable to find CL number") |
| 125 | + } |
| 126 | + changeID := string(matches[1]) |
| 127 | + |
| 128 | + ctx.Printf("Awaiting review/submit of %v", changeID) |
| 129 | + _, err = AwaitCondition(ctx, 10*time.Second, func() (string, bool, error) { |
| 130 | + return x.PublicGerrit.Submitted(ctx, changeID, "") |
| 131 | + }) |
| 132 | + if err != nil { |
| 133 | + return nil, err |
| 134 | + } |
| 135 | + return repos, nil |
| 136 | + }, clNumber, reviewers, repos, repoName) |
| 137 | + |
| 138 | + tagged := wf.Expand4(wd, "Create single-repo plan", tagx.BuildSingleRepoPlan, repos, repoName, skipPostSubmit, reviewers) |
| 139 | + |
| 140 | + okayToAnnoucne := wf.Action0(wd, "Wait to Announce", x.ApproveAction, wf.After(tagged)) |
| 141 | + |
| 142 | + wf.Task5(wd, "Mail announcement", func(ctx *wf.TaskContext, tagged TagRepo, cve string, githubIssue string, relNote string, acknowledgement string) (string, error) { |
| 143 | + var buf bytes.Buffer |
| 144 | + if err := privXPatchAnnouncementTmpl.Execute(&buf, map[string]string{ |
| 145 | + "Module": tagged.ModPath, |
| 146 | + "Version": tagged.NewerVersion, |
| 147 | + "RelNote": relNote, |
| 148 | + "Acknowledgement": acknowledgement, |
| 149 | + "CVE": cve, |
| 150 | + "GithubIssue": githubIssue, |
| 151 | + }); err != nil { |
| 152 | + return "", err |
| 153 | + } |
| 154 | + m, err := mail.ReadMessage(&buf) |
| 155 | + if err != nil { |
| 156 | + return "", err |
| 157 | + } |
| 158 | + html, text, err := renderMarkdown(m.Body) |
| 159 | + if err != nil { |
| 160 | + return "", err |
| 161 | + } |
| 162 | + |
| 163 | + mc := MailContent{m.Header.Get("Subject"), html, text} |
| 164 | + |
| 165 | + ctx.Printf("announcement subject: %s\n\n", mc.Subject) |
| 166 | + ctx.Printf("announcement body HTML:\n%s\n", mc.BodyHTML) |
| 167 | + ctx.Printf("announcement body text:\n%s", mc.BodyText) |
| 168 | + |
| 169 | + ctx.DisableRetries() |
| 170 | + err = x.SendMail(x.AnnounceMailHeader, mc) |
| 171 | + if err != nil { |
| 172 | + return "", err |
| 173 | + } |
| 174 | + |
| 175 | + return "", nil |
| 176 | + }, tagged, cve, githubIssue, relNote, acknowledgement, wf.After(okayToAnnoucne)) |
| 177 | + |
| 178 | + wf.Output(wd, "done", tagged) |
| 179 | + return wd |
| 180 | +} |
| 181 | + |
| 182 | +var privXPatchAnnouncementTmpl = template.Must(template.New("").Parse(`Subject: [security] Vulnerability in {{.Module}} |
| 183 | +
|
| 184 | +Hello gophers, |
| 185 | +
|
| 186 | +We have tagged version {{.Version}} of {{.Module}} in order to address a security issue. |
| 187 | +
|
| 188 | +{{.RelNote}} |
| 189 | +
|
| 190 | +Thanks to {{.Acknowledgement}} for reporting this issue. |
| 191 | +
|
| 192 | +This is {{.CVE}} and Go issue {{.GithubIssue}}. |
| 193 | +
|
| 194 | +Cheers, |
| 195 | +Go Security team`)) |
0 commit comments