Skip to content

Commit e163af7

Browse files
rolandshoemakergopherbot
authored andcommitted
internal/task: add workflow to pre-announce x fixes
Similar to the minor release pre-announcement workflow, add a workflow which allows us to pre-announce fixes to golang.org/x/ (or other) modules. This lets us standardize the process, and send messages in the same way we do for other security content. Additionally, factor out the announcement email sending logic, which was duplicated across announcement and pre-announcement workflows. Change-Id: Id0bda3cb47b5107ab6b66da57a0d8641c4770db4 Reviewed-on: https://go-review.googlesource.com/c/build/+/642296 Auto-Submit: Roland Shoemaker <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 8a37d63 commit e163af7

File tree

4 files changed

+249
-67
lines changed

4 files changed

+249
-67
lines changed

internal/relui/workflows.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,22 @@ func RegisterReleaseWorkflows(ctx context.Context, h *DefinitionHolder, build *B
292292
h.RegisterDefinition("pre-announce next minor release for Go "+strings.Join(names, " and "), wd)
293293
}
294294

295+
// Register pre-announcement workflow for golang.org/x/ fixes.
296+
{
297+
wd := wf.New(wf.ACL{Groups: []string{groups.SecurityTeam}})
298+
299+
module := wf.Param(wd, wf.ParamDef[string]{Name: "Module path", ParamType: wf.BasicString})
300+
pkgs := wf.Param(wd, wf.ParamDef[[]string]{Name: "Packages affected", ParamType: wf.SliceShort})
301+
targetDate := wf.Param(wd, targetDateParam)
302+
cves := wf.Param(wd, securityPreAnnCVEsParam)
303+
coordinators := wf.Param(wd, releaseCoordinators)
304+
305+
sentMail := wf.Task5(wd, "mail-pre-announcement", comm.PreAnnounceXFix, module, pkgs, targetDate, cves, coordinators)
306+
wf.Output(wd, "Pre-announcement URL", wf.Task1(wd, "await-pre-announcement", comm.AwaitAnnounceMail, sentMail))
307+
308+
h.RegisterDefinition("pre-announce golang.org/x security fix", wd)
309+
}
310+
295311
// Register workflows for miscellaneous tasks that happen as part of the Go release cycle (go.dev/s/release).
296312
{
297313
// Register an "apply wait-release to CLs" workflow.

internal/task/announce.go

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ type releasePreAnnouncement struct {
8989
Names []string
9090
}
9191

92+
type golangOrgXPreAnnouncement struct {
93+
// Target is the planned date for the release.
94+
Target Date
95+
96+
// Module is the module the security fixes are in.
97+
Module string
98+
99+
// Package is the package the security fix is in.
100+
Packages []string
101+
102+
// CVEs is the list of CVEs for PRIVATE track fixes to
103+
// be included in the release pre-announcement.
104+
CVEs []string
105+
106+
// Names is an optional list of release coordinator names to
107+
// include in the sign-off message.
108+
Names []string
109+
}
110+
92111
// A Date represents a single calendar day (year, month, day).
93112
//
94113
// This type does not include location information, and
@@ -156,42 +175,7 @@ func (t AnnounceMailTasks) AnnounceRelease(ctx *workflow.TaskContext, kind Relea
156175
r.SecondaryVersion = published[1].Version
157176
}
158177

159-
// Generate the announcement email.
160-
m, err := announcementMail(r)
161-
if err != nil {
162-
return SentMail{}, err
163-
}
164-
ctx.Printf("announcement subject: %s\n\n", m.Subject)
165-
ctx.Printf("announcement body HTML:\n%s\n", m.BodyHTML)
166-
ctx.Printf("announcement body text:\n%s", m.BodyText)
167-
168-
// Before sending, check to see if this announcement already exists.
169-
if threadURL, err := findGoogleGroupsThread(ctx, m.Subject); err != nil {
170-
// Proceeding would risk sending a duplicate email, so error out instead.
171-
return SentMail{}, fmt.Errorf("stopping early due to error checking for an existing Google Groups thread: %w", err)
172-
} else if threadURL != "" {
173-
// This should never happen since this task runs once per release.
174-
// It can happen under unusual circumstances, for example if the task crashes after
175-
// mailing but before completion, or if parts of the release workflow are restarted,
176-
// or if a human mails the announcement email manually out of band.
177-
//
178-
// So if we see that the email exists, consider it as "task completed successfully"
179-
// and pretend we were the ones that sent it, so the high level workflow can keep going.
180-
ctx.Printf("a Google Groups thread with matching subject %q already exists at %q, so we'll consider that as it being sent successfully", m.Subject, threadURL)
181-
return SentMail{m.Subject}, nil
182-
}
183-
184-
// Send the announcement email to the destination mailing lists.
185-
if t.SendMail == nil {
186-
return SentMail{Subject: "[dry-run] " + m.Subject}, nil
187-
}
188-
ctx.DisableRetries()
189-
err = t.SendMail(t.AnnounceMailHeader, m)
190-
if err != nil {
191-
return SentMail{}, err
192-
}
193-
194-
return SentMail{m.Subject}, nil
178+
return t.generateAndSendAnnouncementMail(ctx, r)
195179
}
196180

197181
// PreAnnounceRelease sends an email pre-announcing a Go release
@@ -232,34 +216,7 @@ func (t AnnounceMailTasks) PreAnnounceRelease(ctx *workflow.TaskContext, version
232216
r.SecondaryVersion = versions[1]
233217
}
234218

235-
// Generate the pre-announcement email.
236-
m, err := announcementMail(r)
237-
if err != nil {
238-
return SentMail{}, err
239-
}
240-
ctx.Printf("pre-announcement subject: %s\n\n", m.Subject)
241-
ctx.Printf("pre-announcement body HTML:\n%s\n", m.BodyHTML)
242-
ctx.Printf("pre-announcement body text:\n%s", m.BodyText)
243-
244-
// Before sending, check to see if this pre-announcement already exists.
245-
if threadURL, err := findGoogleGroupsThread(ctx, m.Subject); err != nil {
246-
return SentMail{}, fmt.Errorf("stopping early due to error checking for an existing Google Groups thread: %w", err)
247-
} else if threadURL != "" {
248-
ctx.Printf("a Google Groups thread with matching subject %q already exists at %q, so we'll consider that as it being sent successfully", m.Subject, threadURL)
249-
return SentMail{m.Subject}, nil
250-
}
251-
252-
// Send the pre-announcement email to the destination mailing lists.
253-
if t.SendMail == nil {
254-
return SentMail{Subject: "[dry-run] " + m.Subject}, nil
255-
}
256-
ctx.DisableRetries()
257-
err = t.SendMail(t.AnnounceMailHeader, m)
258-
if err != nil {
259-
return SentMail{}, err
260-
}
261-
262-
return SentMail{m.Subject}, nil
219+
return t.generateAndSendAnnouncementMail(ctx, r)
263220
}
264221

265222
func coordinatorFirstNames(users []string) ([]string, error) {
@@ -269,6 +226,40 @@ func coordinatorFirstNames(users []string) ([]string, error) {
269226
})
270227
}
271228

229+
func (t AnnounceMailTasks) PreAnnounceXFix(ctx *workflow.TaskContext, module string, pkgs []string, target Date, cves []string, users []string) (SentMail, error) {
230+
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < time.Minute {
231+
return SentMail{}, fmt.Errorf("insufficient time for pre-announce release task; a minimum of a minute left on context is required")
232+
}
233+
now := time.Now().UTC()
234+
if t.testHookNow != nil {
235+
now = t.testHookNow()
236+
}
237+
for _, p := range pkgs {
238+
if !strings.HasPrefix(p, module) {
239+
return SentMail{}, fmt.Errorf("package %q is not inside module %q", p, module)
240+
}
241+
}
242+
if !target.After(now.Year(), now.Month(), now.Day()) { // A very simple check. Improve as needed.
243+
return SentMail{}, fmt.Errorf("target release date is not in the future")
244+
}
245+
if len(cves) == 0 {
246+
return SentMail{}, errors.New("CVEs are not specified")
247+
}
248+
names, err := coordinatorFirstNames(users)
249+
if err != nil {
250+
return SentMail{}, err
251+
}
252+
r := golangOrgXPreAnnouncement{
253+
Target: target,
254+
Module: module,
255+
Packages: pkgs,
256+
CVEs: cves,
257+
Names: names,
258+
}
259+
260+
return t.generateAndSendAnnouncementMail(ctx, r)
261+
}
262+
272263
func coordinatorEmails(users []string) ([]string, error) {
273264
return mapCoordinators(users, func(p *gophers.Person) string {
274265
return p.Gerrit
@@ -337,6 +328,8 @@ type MailContent struct {
337328
// which must be one of these types:
338329
// - releaseAnnouncement for a release announcement
339330
// - releasePreAnnouncement for a release pre-announcement
331+
// - golangOrgXPreAnnouncement for a golang.org/x (or other Go module) security
332+
// release pre-announcement
340333
// - goplsPrereleaseAnnouncement for a gopls pre-announcement
341334
func announcementMail(data any) (MailContent, error) {
342335
// Select the appropriate template name.
@@ -368,6 +361,8 @@ func announcementMail(data any) (MailContent, error) {
368361
}
369362
case releasePreAnnouncement:
370363
name = "pre-announce-minor.md"
364+
case golangOrgXPreAnnouncement:
365+
name = "pre-announce-x.md"
371366
case goplsReleaseAnnouncement:
372367
name = "gopls-announce.md"
373368
case goplsPrereleaseAnnouncement:
@@ -482,6 +477,7 @@ var announceTmpl = template.Must(template.New("").Funcs(template.FuncMap{
482477
}).ParseFS(tmplDir,
483478
"template/announce-*.md",
484479
"template/pre-announce-minor.md",
480+
"template/pre-announce-x.md",
485481
// Gopls release announcements.
486482
"template/gopls-announce.md",
487483
"template/gopls-pre-announce.md",
@@ -786,3 +782,41 @@ func (markdownToTextRenderer) Render(w io.Writer, source []byte, n ast.Node) err
786782
return ast.Walk(n, walk)
787783
}
788784
func (markdownToTextRenderer) AddOptions(...renderer.Option) {}
785+
786+
func (t AnnounceMailTasks) generateAndSendAnnouncementMail(ctx *workflow.TaskContext, data any) (SentMail, error) {
787+
// Generate the announcement email.
788+
m, err := announcementMail(data)
789+
if err != nil {
790+
return SentMail{}, err
791+
}
792+
ctx.Printf("announcement subject: %s\n\n", m.Subject)
793+
ctx.Printf("announcement body HTML:\n%s\n", m.BodyHTML)
794+
ctx.Printf("announcement body text:\n%s", m.BodyText)
795+
796+
// Before sending, check to see if this announcement already exists.
797+
if threadURL, err := findGoogleGroupsThread(ctx, m.Subject); err != nil {
798+
return SentMail{}, fmt.Errorf("stopping early due to error checking for an existing Google Groups thread: %w", err)
799+
} else if threadURL != "" {
800+
// This should never happen since this task runs once per release.
801+
// It can happen under unusual circumstances, for example if the task crashes after
802+
// mailing but before completion, or if parts of the release workflow are restarted,
803+
// or if a human mails the announcement email manually out of band.
804+
//
805+
// So if we see that the email exists, consider it as "task completed successfully"
806+
// and pretend we were the ones that sent it, so the high level workflow can keep going.
807+
ctx.Printf("a Google Groups thread with matching subject %q already exists at %q, so we'll consider that as it being sent successfully", m.Subject, threadURL)
808+
return SentMail{m.Subject}, nil
809+
}
810+
811+
// Send the announcement email to the destination mailing lists.
812+
if t.SendMail == nil {
813+
return SentMail{Subject: "[dry-run] " + m.Subject}, nil
814+
}
815+
ctx.DisableRetries()
816+
err = t.SendMail(t.AnnounceMailHeader, m)
817+
if err != nil {
818+
return SentMail{}, err
819+
}
820+
821+
return SentMail{m.Subject}, nil
822+
}

internal/task/announce_test.go

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,9 +349,9 @@ func TestPreAnnounceRelease(t *testing.T) {
349349
cves: []string{"cve-2022-1234", "cve-2023-1234"},
350350
coordinators: []string{"tatiana"},
351351
want: SentMail{Subject: "[security] Go 1.18.4 and Go 1.17.11 pre-announcement"},
352-
wantLog: `pre-announcement subject: [security] Go 1.18.4 and Go 1.17.11 pre-announcement
352+
wantLog: `announcement subject: [security] Go 1.18.4 and Go 1.17.11 pre-announcement
353353
354-
pre-announcement body HTML:
354+
announcement body HTML:
355355
<p>Hello gophers,</p>
356356
<p>We plan to issue Go 1.18.4 and Go 1.17.11 during US business hours on Tuesday, July 12.</p>
357357
<p>These minor releases include PRIVATE security fixes to the standard library, covering the following CVEs:</p>
@@ -363,7 +363,7 @@ pre-announcement body HTML:
363363
<p>Thanks,<br>
364364
Tatiana for the Go team</p>
365365
366-
pre-announcement body text:
366+
announcement body text:
367367
Hello gophers,
368368
369369
We plan to issue Go 1.18.4 and Go 1.17.11 during US business hours on Tuesday, July 12.
@@ -406,6 +406,123 @@ Tatiana for the Go team` + "\n",
406406
}
407407
}
408408

409+
func TestPreAnnounceXFix(t *testing.T) {
410+
if testing.Short() {
411+
t.Skip("not running test that uses internet in short mode")
412+
}
413+
414+
tests := [...]struct {
415+
name string
416+
target Date
417+
module string
418+
packages []string
419+
cves []string
420+
coordinators []string
421+
want SentMail
422+
wantLog string
423+
}{
424+
{
425+
name: "x fix, single package",
426+
target: Date{2022, time.July, 12},
427+
module: "golang.org/x/crypto",
428+
packages: []string{"golang.org/x/crypto/ssh"},
429+
cves: []string{"CVE-2025-1234", "CVE-2025-1235"},
430+
coordinators: []string{"roland"},
431+
want: SentMail{Subject: "[security] golang.org/x/crypto fix pre-announcement"},
432+
wantLog: `announcement subject: [security] golang.org/x/crypto fix pre-announcement
433+
434+
announcement body HTML:
435+
<p>Hello gophers,</p>
436+
<p>We plan to issue a security fix for the package golang.org/x/crypto/ssh in the golang.org/x/crypto module during US business hours on Tuesday, July 12.</p>
437+
<p>This will cover the following CVEs:</p>
438+
<ul>
439+
<li>CVE-2025-1234</li>
440+
<li>CVE-2025-1235</li>
441+
</ul>
442+
<p>Following our security policy, this is the pre-announcement of the fix.</p>
443+
<p>Thanks,<br>
444+
Roland for the Go team</p>
445+
446+
announcement body text:
447+
Hello gophers,
448+
449+
We plan to issue a security fix for the package golang.org/x/crypto/ssh in the golang.org/x/crypto module during US business hours on Tuesday, July 12.
450+
451+
This will cover the following CVEs:
452+
453+
- CVE-2025-1234
454+
455+
- CVE-2025-1235
456+
457+
Following our security policy, this is the pre-announcement of the fix.
458+
459+
Thanks,
460+
Roland for the Go team` + "\n",
461+
},
462+
{
463+
name: "x fix, multiple packages",
464+
target: Date{2022, time.July, 12},
465+
module: "golang.org/x/crypto",
466+
packages: []string{"golang.org/x/crypto/ssh", "golang.org/x/crypto/ocsp", "golang.org/x/crypto/xts"},
467+
cves: []string{"CVE-2025-1234", "CVE-2025-1235"},
468+
coordinators: []string{"roland"},
469+
want: SentMail{Subject: "[security] golang.org/x/crypto fix pre-announcement"},
470+
wantLog: `announcement subject: [security] golang.org/x/crypto fix pre-announcement
471+
472+
announcement body HTML:
473+
<p>Hello gophers,</p>
474+
<p>We plan to issue a security fix for the packages golang.org/x/crypto/ssh, golang.org/x/crypto/ocsp, and golang.org/x/crypto/xts in the golang.org/x/crypto module during US business hours on Tuesday, July 12.</p>
475+
<p>This will cover the following CVEs:</p>
476+
<ul>
477+
<li>CVE-2025-1234</li>
478+
<li>CVE-2025-1235</li>
479+
</ul>
480+
<p>Following our security policy, this is the pre-announcement of the fix.</p>
481+
<p>Thanks,<br>
482+
Roland for the Go team</p>
483+
484+
announcement body text:
485+
Hello gophers,
486+
487+
We plan to issue a security fix for the packages golang.org/x/crypto/ssh, golang.org/x/crypto/ocsp, and golang.org/x/crypto/xts in the golang.org/x/crypto module during US business hours on Tuesday, July 12.
488+
489+
This will cover the following CVEs:
490+
491+
- CVE-2025-1234
492+
493+
- CVE-2025-1235
494+
495+
Following our security policy, this is the pre-announcement of the fix.
496+
497+
Thanks,
498+
Roland for the Go team` + "\n",
499+
},
500+
}
501+
for _, tc := range tests {
502+
t.Run(tc.name, func(t *testing.T) {
503+
tasks := AnnounceMailTasks{
504+
SendMail: func(h MailHeader, c MailContent) error { return nil },
505+
testHookNow: func() time.Time { return time.Date(2022, time.July, 7, 0, 0, 0, 0, time.UTC) },
506+
}
507+
var buf bytes.Buffer
508+
ctx := &workflow.TaskContext{Context: context.Background(), Logger: fmtWriter{&buf}}
509+
sentMail, err := tasks.PreAnnounceXFix(ctx, tc.module, tc.packages, tc.target, tc.cves, tc.coordinators)
510+
if err != nil {
511+
if fe := (fetchError{}); errors.As(err, &fe) && fe.PossiblyRetryable {
512+
t.Skip("test run produced no actionable signal due to a transient network error:", err) // See go.dev/issue/60541.
513+
}
514+
t.Fatal("task function returned non-nil error:", err)
515+
}
516+
if diff := cmp.Diff(tc.want, sentMail); diff != "" {
517+
t.Errorf("sent mail mismatch (-want +got):\n%s", diff)
518+
}
519+
if diff := cmp.Diff(tc.wantLog, buf.String()); diff != "" {
520+
t.Errorf("log mismatch (-want +got):\n%s", diff)
521+
}
522+
})
523+
}
524+
}
525+
409526
func TestFindGoogleGroupsThread(t *testing.T) {
410527
if testing.Short() {
411528
t.Skip("not running test that uses internet in short mode")
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Subject: [security] {{.Module}} fix pre-announcement
2+
3+
Hello gophers,
4+
5+
We plan to issue a security fix for the package{{$numPkgs := len .Packages}}{{if gt $numPkgs 1}}s{{end}} {{join .Packages}} in the {{.Module}} module during US business hours on {{.Target.Format "Monday, January 2"}}.
6+
7+
This will cover the following CVEs:
8+
9+
{{range .CVEs}}- {{.}}
10+
{{end}}
11+
12+
Following our security policy, this is the pre-announcement of the fix.
13+
14+
Thanks,
15+
{{with .Names}}{{join .}} for the{{else}}The{{end}} Go team

0 commit comments

Comments
 (0)