golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/relui/workflows.go (about) 1 // Copyright 2021 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 relui 6 7 import ( 8 "archive/tar" 9 "archive/zip" 10 "bytes" 11 "compress/flate" 12 "compress/gzip" 13 "context" 14 "crypto/sha256" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "io" 19 "io/fs" 20 "net/http" 21 "net/url" 22 "path" 23 "regexp" 24 "slices" 25 "sort" 26 "strings" 27 "sync" 28 "time" 29 30 "cloud.google.com/go/storage" 31 "github.com/google/go-cmp/cmp" 32 pb "go.chromium.org/luci/buildbucket/proto" 33 "golang.org/x/build/dashboard" 34 "golang.org/x/build/internal/gcsfs" 35 "golang.org/x/build/internal/installer/darwinpkg" 36 "golang.org/x/build/internal/installer/windowsmsi" 37 "golang.org/x/build/internal/releasetargets" 38 "golang.org/x/build/internal/relui/db" 39 "golang.org/x/build/internal/relui/sign" 40 "golang.org/x/build/internal/task" 41 "golang.org/x/build/internal/workflow" 42 wf "golang.org/x/build/internal/workflow" 43 "golang.org/x/exp/maps" 44 "golang.org/x/net/context/ctxhttp" 45 "google.golang.org/protobuf/types/known/structpb" 46 ) 47 48 // DefinitionHolder holds workflow definitions. 49 type DefinitionHolder struct { 50 mu sync.Mutex 51 definitions map[string]*wf.Definition 52 } 53 54 // NewDefinitionHolder creates a new DefinitionHolder, 55 // initialized with a sample "echo" wf. 56 func NewDefinitionHolder() *DefinitionHolder { 57 return &DefinitionHolder{definitions: map[string]*wf.Definition{ 58 "echo": newEchoWorkflow(), 59 }} 60 } 61 62 // Definition returns the initialized wf.Definition registered 63 // for a given name. 64 func (h *DefinitionHolder) Definition(name string) *wf.Definition { 65 h.mu.Lock() 66 defer h.mu.Unlock() 67 return h.definitions[name] 68 } 69 70 // RegisterDefinition registers a definition with a name. 71 // If a definition with the same name already exists, RegisterDefinition panics. 72 func (h *DefinitionHolder) RegisterDefinition(name string, d *wf.Definition) { 73 h.mu.Lock() 74 defer h.mu.Unlock() 75 if _, exist := h.definitions[name]; exist { 76 panic("relui: multiple registrations for " + name) 77 } 78 h.definitions[name] = d 79 } 80 81 // Definitions returns the names of all registered definitions. 82 func (h *DefinitionHolder) Definitions() map[string]*wf.Definition { 83 h.mu.Lock() 84 defer h.mu.Unlock() 85 defs := make(map[string]*wf.Definition) 86 for k, v := range h.definitions { 87 defs[k] = v 88 } 89 return defs 90 } 91 92 // Release parameter definitions. 93 var ( 94 targetDateParam = wf.ParamDef[task.Date]{ 95 Name: "Target Release Date", 96 ParamType: wf.ParamType[task.Date]{ 97 HTMLElement: "input", 98 HTMLInputType: "date", 99 }, 100 Doc: `Target Release Date is the date on which the release is scheduled. 101 102 It must be three to seven days after the pre-announcement as documented in the security policy.`, 103 } 104 securityPreAnnParam = wf.ParamDef[string]{ 105 Name: "Security Content", 106 ParamType: workflow.ParamType[string]{ 107 HTMLElement: "select", 108 HTMLSelectOptions: []string{ 109 "the standard library", 110 "the toolchain", 111 "the standard library and the toolchain", 112 }, 113 }, 114 Doc: `Security Content is the security content to be included in the release pre-announcement. 115 116 It must not reveal details beyond what's allowed by the security policy.`, 117 } 118 securityPreAnnCVEsParam = wf.ParamDef[[]string]{ 119 Name: "PRIVATE-track CVEs", 120 ParamType: wf.SliceShort, 121 Example: "CVE-2023-XXXX", 122 Doc: "List of CVEs for PRIVATE track fixes contained in the release to be included in the pre-announcement.", 123 Check: func(cves []string) error { 124 var m = make(map[string]bool) 125 for _, c := range cves { 126 switch { 127 case !cveRE.MatchString(c): 128 return fmt.Errorf("CVE ID %q doesn't match %s", c, cveRE) 129 case m[c]: 130 return fmt.Errorf("duplicate CVE ID %q", c) 131 } 132 m[c] = true 133 } 134 return nil 135 }, 136 } 137 cveRE = regexp.MustCompile(`^CVE-\d{4}-\d{4,7}$`) 138 139 securitySummaryParameter = wf.ParamDef[string]{ 140 Name: "Security Summary (optional)", 141 Doc: `Security Summary is an optional sentence describing security fixes included in this release. 142 143 It shows up in the release tweet. 144 145 The empty string means there are no security fixes to highlight. 146 147 Past examples: 148 • "Includes a security fix for crypto/tls (CVE-2021-34558)." 149 • "Includes a security fix for the Wasm port (CVE-2021-38297)." 150 • "Includes security fixes for encoding/pem (CVE-2022-24675), crypto/elliptic (CVE-2022-28327), crypto/x509 (CVE-2022-27536)."`, 151 } 152 153 securityFixesParameter = wf.ParamDef[[]string]{ 154 Name: "Security Fixes (optional)", 155 ParamType: wf.SliceLong, 156 Doc: `Security Fixes is a list of descriptions, one for each distinct security fix included in this release, in Markdown format. 157 158 It shows up in the announcement mail. 159 160 The empty list means there are no security fixes included. 161 162 Past examples: 163 • "encoding/pem: fix stack overflow in Decode 164 165 A large (more than 5 MB) PEM input can cause a stack overflow in Decode, 166 leading the program to crash. 167 168 Thanks to Juho Nurminen of Mattermost who reported the error. 169 170 This is CVE-2022-24675 and Go issue https://go.dev/issue/51853." 171 • "crypto/elliptic: tolerate all oversized scalars in generic P-256 172 173 A crafted scalar input longer than 32 bytes can cause P256().ScalarMult 174 or P256().ScalarBaseMult to panic. Indirect uses through crypto/ecdsa and 175 crypto/tls are unaffected. amd64, arm64, ppc64le, and s390x are unaffected. 176 177 This was discovered thanks to a Project Wycheproof test vector. 178 179 This is CVE-2022-28327 and Go issue https://go.dev/issue/52075."`, 180 Example: `encoding/pem: fix stack overflow in Decode 181 182 A large (more than 5 MB) PEM input can cause a stack overflow in Decode, 183 leading the program to crash. 184 185 Thanks to Juho Nurminen of Mattermost who reported the error. 186 187 This is CVE-2022-24675 and Go issue https://go.dev/issue/51853.`, 188 } 189 190 releaseCoordinators = wf.ParamDef[[]string]{ 191 Name: "Release Coordinator Usernames (optional)", 192 ParamType: wf.SliceShort, 193 Doc: `Release Coordinator Usernames is an optional list of the coordinators of the release. 194 195 Their first names will be included at the end of the release announcement, and CLs will be mailed to them.`, 196 Example: "heschi", 197 Check: task.CheckCoordinators, 198 } 199 ) 200 201 // newEchoWorkflow returns a runnable wf.Definition for 202 // development. 203 func newEchoWorkflow() *wf.Definition { 204 wd := wf.New() 205 wf.Output(wd, "greeting", wf.Task1(wd, "greeting", echo, wf.Param(wd, wf.ParamDef[string]{Name: "greeting"}))) 206 wf.Output(wd, "farewell", wf.Task1(wd, "farewell", echo, wf.Param(wd, wf.ParamDef[string]{Name: "farewell"}))) 207 return wd 208 } 209 210 func echo(ctx *wf.TaskContext, arg string) (string, error) { 211 ctx.Printf("echo(%v, %q)", ctx, arg) 212 return arg, nil 213 } 214 215 func checkTaskApproved(ctx *wf.TaskContext, p db.PGDBTX) (bool, error) { 216 q := db.New(p) 217 t, err := q.Task(ctx, db.TaskParams{ 218 Name: ctx.TaskName, 219 WorkflowID: ctx.WorkflowID, 220 }) 221 if !t.ReadyForApproval { 222 _, err := q.UpdateTaskReadyForApproval(ctx, db.UpdateTaskReadyForApprovalParams{ 223 ReadyForApproval: true, 224 Name: ctx.TaskName, 225 WorkflowID: ctx.WorkflowID, 226 }) 227 if err != nil { 228 return false, err 229 } 230 } 231 return t.ApprovedAt.Valid, err 232 } 233 234 // ApproveActionDep returns a function for defining approval Actions. 235 // 236 // ApproveActionDep takes a single *pgxpool.Pool argument, which is 237 // used to query the database to determine if a task has been marked 238 // approved. 239 // 240 // ApproveActionDep marks the task as requiring approval in the 241 // database once the task is started. This can be used to show an 242 // "approve" control in the UI. 243 // 244 // waitAction := wf.ActionN(wd, "Wait for Approval", ApproveActionDep(db), wf.After(someDependency)) 245 func ApproveActionDep(p db.PGDBTX) func(*wf.TaskContext) error { 246 return func(ctx *wf.TaskContext) error { 247 _, err := task.AwaitCondition(ctx, 5*time.Second, func() (int, bool, error) { 248 done, err := checkTaskApproved(ctx, p) 249 return 0, done, err 250 }) 251 return err 252 } 253 } 254 255 // RegisterReleaseWorkflows registers workflows for issuing Go releases. 256 func RegisterReleaseWorkflows(ctx context.Context, h *DefinitionHolder, build *BuildReleaseTasks, milestone *task.MilestoneTasks, version *task.VersionTasks, comm task.CommunicationTasks) error { 257 // Register prod release workflows. 258 if err := registerProdReleaseWorkflows(ctx, h, build, milestone, version, comm); err != nil { 259 return err 260 } 261 262 // Register pre-announcement workflows. 263 currentMajor, _, err := version.GetCurrentMajor(ctx) 264 if err != nil { 265 return err 266 } 267 releases := []struct { 268 majors []int 269 }{ 270 {[]int{currentMajor, currentMajor - 1}}, // Both minors. 271 {[]int{currentMajor}}, // Current minor only. 272 {[]int{currentMajor - 1}}, // Previous minor only. 273 } 274 for _, r := range releases { 275 wd := wf.New() 276 277 versions := wf.Task1(wd, "Get next versions", version.GetNextMinorVersions, wf.Const(r.majors)) 278 targetDate := wf.Param(wd, targetDateParam) 279 securityContent := wf.Param(wd, securityPreAnnParam) 280 cves := wf.Param(wd, securityPreAnnCVEsParam) 281 coordinators := wf.Param(wd, releaseCoordinators) 282 283 sentMail := wf.Task5(wd, "mail-pre-announcement", comm.PreAnnounceRelease, versions, targetDate, securityContent, cves, coordinators) 284 wf.Output(wd, "Pre-announcement URL", wf.Task1(wd, "await-pre-announcement", comm.AwaitAnnounceMail, sentMail)) 285 286 var names []string 287 for _, m := range r.majors { 288 names = append(names, fmt.Sprintf("1.%d", m)) 289 } 290 h.RegisterDefinition("pre-announce next minor release for Go "+strings.Join(names, " and "), wd) 291 } 292 293 // Register workflows for miscellaneous tasks that happen as part of the Go release cycle. 294 { 295 // Register a "ping early-in-cycle issues" workflow. 296 wd := wf.New() 297 openTreeURL := wf.Param(wd, wf.ParamDef[string]{ 298 Name: "Open Tree URL", 299 Doc: `Open Tree URL is the URL of an announcement that the tree is open for general Go 1.x development.`, 300 Example: "https://groups.google.com/g/golang-dev/c/09IwUs7cxXA/m/c2jyIhECBQAJ", 301 Check: func(openTreeURL string) error { 302 if !strings.HasPrefix(openTreeURL, "https://groups.google.com/g/golang-dev/c/") { 303 return fmt.Errorf("openTreeURL value %q doesn't begin with the usual prefix, so please double-check that the URL is correct", openTreeURL) 304 } 305 return nil 306 }, 307 }) 308 devVer := wf.Task0(wd, "Get development version", version.GetDevelVersion) 309 pinged := wf.Task2(wd, "Ping early-in-cycle issues", milestone.PingEarlyIssues, devVer, openTreeURL) 310 wf.Output(wd, "pinged", pinged) 311 h.RegisterDefinition("ping early-in-cycle issues in development milestone", wd) 312 } 313 { 314 // Register an "unwait wait-release CLs" workflow. 315 wd := wf.New() 316 unwaited := wf.Task0(wd, "Unwait wait-release CLs", version.UnwaitWaitReleaseCLs) 317 wf.Output(wd, "unwaited", unwaited) 318 h.RegisterDefinition("unwait wait-release CLs", wd) 319 } 320 321 // Register dry-run release workflows. 322 registerBuildTestSignOnlyWorkflow(h, version, build, currentMajor+1, task.KindBeta) 323 324 return nil 325 } 326 327 func registerProdReleaseWorkflows(ctx context.Context, h *DefinitionHolder, build *BuildReleaseTasks, milestone *task.MilestoneTasks, version *task.VersionTasks, comm task.CommunicationTasks) error { 328 currentMajor, majorReleaseTime, err := version.GetCurrentMajor(ctx) 329 if err != nil { 330 return err 331 } 332 type release struct { 333 major int 334 kind task.ReleaseKind 335 suffix string 336 } 337 releases := []release{ 338 {currentMajor + 1, task.KindMajor, "final"}, 339 {currentMajor + 1, task.KindRC, "next RC"}, 340 {currentMajor + 1, task.KindBeta, "next beta"}, 341 {currentMajor, task.KindMinor, "next minor"}, // Current minor only. 342 {currentMajor - 1, task.KindMinor, "next minor"}, // Previous minor only. 343 } 344 if time.Since(majorReleaseTime) < 7*24*time.Hour { 345 releases = append(releases, release{currentMajor, task.KindMajor, "final"}) 346 } 347 for _, r := range releases { 348 wd := wf.New() 349 350 coordinators := wf.Param(wd, releaseCoordinators) 351 352 published := addSingleReleaseWorkflow(build, milestone, version, wd, r.major, r.kind, coordinators) 353 354 securitySummary := wf.Const("") 355 securityFixes := wf.Slice[string]() 356 if r.kind == task.KindMinor { 357 securitySummary = wf.Param(wd, securitySummaryParameter) 358 securityFixes = wf.Param(wd, securityFixesParameter) 359 } 360 addCommTasks(wd, build, comm, r.kind, wf.Slice(published), securitySummary, securityFixes, coordinators) 361 if r.major >= currentMajor { 362 // Add a task for updating the module proxy test repo that makes sure modules containing go directives 363 // of the latest published version are fetchable. 364 wf.Task1(wd, "update-proxy-test", version.UpdateProxyTestRepo, published) 365 } 366 367 h.RegisterDefinition(fmt.Sprintf("Go 1.%d %s", r.major, r.suffix), wd) 368 } 369 370 wd, err := createMinorReleaseWorkflow(build, milestone, version, comm, currentMajor-1, currentMajor) 371 if err != nil { 372 return err 373 } 374 h.RegisterDefinition(fmt.Sprintf("Minor releases for Go 1.%d and 1.%d", currentMajor-1, currentMajor), wd) 375 376 return nil 377 } 378 379 func registerBuildTestSignOnlyWorkflow(h *DefinitionHolder, version *task.VersionTasks, build *BuildReleaseTasks, major int, kind task.ReleaseKind) { 380 wd := wf.New() 381 382 nextVersion := wf.Task2(wd, "Get next version", version.GetNextVersion, wf.Const(major), wf.Const(kind)) 383 branch := fmt.Sprintf("release-branch.go1.%d", major) 384 if kind == task.KindBeta { 385 branch = "master" 386 } 387 branchVal := wf.Const(branch) 388 timestamp := wf.Task0(wd, "Timestamp release", now) 389 versionFile := wf.Task2(wd, "Generate VERSION file", version.GenerateVersionFile, nextVersion, timestamp) 390 wf.Output(wd, "VERSION file", versionFile) 391 head := wf.Task1(wd, "Read branch head", version.ReadBranchHead, branchVal) 392 srcSpec := wf.Task5(wd, "Select source spec", build.getGitSource, branchVal, head, wf.Const(""), wf.Const(""), versionFile) 393 source, artifacts, mods := build.addBuildTasks(wd, major, kind, nextVersion, timestamp, srcSpec) 394 wf.Output(wd, "Source", source) 395 wf.Output(wd, "Artifacts", artifacts) 396 wf.Output(wd, "Modules", mods) 397 398 h.RegisterDefinition(fmt.Sprintf("dry-run (build, test, and sign only): Go 1.%d next beta", major), wd) 399 } 400 401 func createMinorReleaseWorkflow(build *BuildReleaseTasks, milestone *task.MilestoneTasks, version *task.VersionTasks, comm task.CommunicationTasks, prevMajor, currentMajor int) (*wf.Definition, error) { 402 wd := wf.New() 403 404 coordinators := wf.Param(wd, releaseCoordinators) 405 currPublished := addSingleReleaseWorkflow(build, milestone, version, wd.Sub(fmt.Sprintf("Go 1.%d", currentMajor)), currentMajor, task.KindMinor, coordinators) 406 prevPublished := addSingleReleaseWorkflow(build, milestone, version, wd.Sub(fmt.Sprintf("Go 1.%d", prevMajor)), prevMajor, task.KindMinor, coordinators) 407 408 securitySummary := wf.Param(wd, securitySummaryParameter) 409 securityFixes := wf.Param(wd, securityFixesParameter) 410 addCommTasks(wd, build, comm, task.KindMinor, wf.Slice(currPublished, prevPublished), securitySummary, securityFixes, coordinators) 411 wf.Task1(wd, "update-proxy-test", version.UpdateProxyTestRepo, currPublished) 412 413 return wd, nil 414 } 415 416 func addCommTasks( 417 wd *wf.Definition, build *BuildReleaseTasks, comm task.CommunicationTasks, 418 kind task.ReleaseKind, published wf.Value[[]task.Published], securitySummary wf.Value[string], securityFixes, coordinators wf.Value[[]string], 419 ) { 420 okayToAnnounce := wf.Action0(wd, "Wait to Announce", build.ApproveAction, wf.After(published)) 421 422 // Announce that a new Go release has been published. 423 sentMail := wf.Task4(wd, "mail-announcement", comm.AnnounceRelease, wf.Const(kind), published, securityFixes, coordinators, wf.After(okayToAnnounce)) 424 announcementURL := wf.Task1(wd, "await-announcement", comm.AwaitAnnounceMail, sentMail) 425 tweetURL := wf.Task4(wd, "post-tweet", comm.TweetRelease, wf.Const(kind), published, securitySummary, announcementURL, wf.After(okayToAnnounce)) 426 mastodonURL := wf.Task4(wd, "post-mastodon", comm.TrumpetRelease, wf.Const(kind), published, securitySummary, announcementURL, wf.After(okayToAnnounce)) 427 428 wf.Output(wd, "Announcement URL", announcementURL) 429 wf.Output(wd, "Tweet URL", tweetURL) 430 wf.Output(wd, "Mastodon URL", mastodonURL) 431 } 432 433 func now(_ context.Context) (time.Time, error) { 434 return time.Now().UTC().Round(time.Second), nil 435 } 436 437 var securityProjectNameToProject = map[string]string{ 438 "go-internal/go (new)": "go", 439 "go-internal/golang/go-private (old)": "golang/go-private", 440 } 441 442 func addSingleReleaseWorkflow( 443 build *BuildReleaseTasks, milestone *task.MilestoneTasks, version *task.VersionTasks, 444 wd *wf.Definition, major int, kind task.ReleaseKind, coordinators wf.Value[[]string], 445 ) wf.Value[task.Published] { 446 kindVal := wf.Const(kind) 447 branch := fmt.Sprintf("release-branch.go1.%d", major) 448 if kind == task.KindBeta { 449 branch = "master" 450 } 451 branchVal := wf.Const(branch) 452 startingHead := wf.Task1(wd, "Read starting branch head", version.ReadBranchHead, branchVal) 453 454 // Select version, check milestones. 455 nextVersion := wf.Task2(wd, "Get next version", version.GetNextVersion, wf.Const(major), kindVal) 456 timestamp := wf.Task0(wd, "Timestamp release", now) 457 versionFile := wf.Task2(wd, "Generate VERSION file", version.GenerateVersionFile, nextVersion, timestamp) 458 wf.Output(wd, "VERSION file", versionFile) 459 milestones := wf.Task2(wd, "Pick milestones", milestone.FetchMilestones, nextVersion, kindVal) 460 checked := wf.Action3(wd, "Check blocking issues", milestone.CheckBlockers, milestones, nextVersion, kindVal) 461 462 securityProjectName := wf.Param(wd, wf.ParamDef[string]{ 463 Name: "Security repository to retrieve ref from (optional)", 464 ParamType: workflow.ParamType[string]{ 465 HTMLElement: "select", 466 HTMLSelectOptions: []string{ 467 "go-internal/go (new)", 468 "go-internal/golang/go-private (old)", 469 }, 470 }, 471 Doc: `"go-internal/golang/go-private" is the old internal gerrit repository, "go-internal/go" is the new repository.`, 472 }) 473 securityProject := wf.Task1(wd, "Convert security project name", func(ctx *wf.TaskContext, projectName string) (string, error) { 474 return securityProjectNameToProject[projectName], nil 475 }, securityProjectName) 476 securityRef := wf.Param(wd, wf.ParamDef[string]{Name: "Ref from the private repository to build from (optional)"}) 477 securityCommit := wf.Task2(wd, "Read security ref", build.readSecurityRef, securityProject, securityRef) 478 srcSpec := wf.Task5(wd, "Select source spec", build.getGitSource, branchVal, startingHead, securityProject, securityCommit, versionFile, wf.After(checked)) 479 480 // Build, test, and sign release. 481 source, signedAndTestedArtifacts, modules := build.addBuildTasks(wd, major, kind, nextVersion, timestamp, srcSpec) 482 okayToTagAndPublish := wf.Action0(wd, "Wait for Release Coordinator Approval", build.ApproveAction, wf.After(signedAndTestedArtifacts)) 483 484 dlcl := wf.Task5(wd, "Mail DL CL", version.MailDLCL, wf.Const(major), kindVal, nextVersion, coordinators, wf.Const(false), wf.After(okayToTagAndPublish)) 485 dlclCommit := wf.Task2(wd, "Wait for DL CL submission", version.AwaitCL, dlcl, wf.Const("")) 486 wf.Output(wd, "Download CL submitted", dlclCommit) 487 488 // Tag version and upload to CDN/website. 489 // If we're releasing a beta from master, tagging is easy; we just tag the 490 // commit we started from. Otherwise, we're going to submit a VERSION CL, 491 // and we need to make sure that that CL is submitted on top of the same 492 // state we built from. For security releases that state may not have 493 // been public when we started, but it should be now. 494 tagCommit := startingHead 495 if branch != "master" { 496 publishingHead := wf.Task3(wd, "Check branch state matches source archive", build.checkSourceMatch, branchVal, versionFile, source, wf.After(okayToTagAndPublish)) 497 versionCL := wf.Task4(wd, "Mail version CL", version.CreateAutoSubmitVersionCL, branchVal, nextVersion, coordinators, versionFile, wf.After(publishingHead)) 498 tagCommit = wf.Task2(wd, "Wait for version CL submission", version.AwaitCL, versionCL, publishingHead) 499 } 500 tagged := wf.Action2(wd, "Tag version", version.TagRelease, nextVersion, tagCommit, wf.After(okayToTagAndPublish)) 501 uploaded := wf.Action1(wd, "Upload artifacts to CDN", build.uploadArtifacts, signedAndTestedArtifacts, wf.After(tagged)) 502 uploadedMods := wf.Action2(wd, "Upload modules to CDN", build.uploadModules, nextVersion, modules, wf.After(tagged)) 503 availableOnProxy := wf.Action2(wd, "Wait for modules on proxy.golang.org", build.awaitProxy, nextVersion, modules, wf.After(uploadedMods)) 504 pushed := wf.Action3(wd, "Push issues", milestone.PushIssues, milestones, nextVersion, kindVal, wf.After(tagged)) 505 published := wf.Task2(wd, "Publish to website", build.publishArtifacts, nextVersion, signedAndTestedArtifacts, wf.After(uploaded, availableOnProxy, pushed)) 506 if kind == task.KindMajor { 507 xToolsStdlibCL := wf.Task2(wd, fmt.Sprintf("Mail x/tools stdlib CL for 1.%d", major), version.CreateUpdateStdlibIndexCL, coordinators, nextVersion, wf.After(published)) 508 xToolsStdlibCommit := wf.Task2(wd, "Wait for x/tools stdlib CL submission", version.AwaitCL, xToolsStdlibCL, wf.Const("")) 509 wf.Output(wd, "x/tools stdlib CL submitted", xToolsStdlibCommit) 510 } 511 512 dockerBuild := wf.Task1(wd, "Start Google Docker build", build.runGoogleDockerBuild, nextVersion, wf.After(uploaded)) 513 dockerResult := wf.Task1(wd, "Await Google Docker build", build.awaitCloudBuild, dockerBuild) 514 wf.Output(wd, "Google Docker image status", dockerResult) 515 516 wf.Output(wd, "Published to website", published) 517 return published 518 } 519 520 // sourceSpec encapsulates all the information that describes a source archive. 521 type sourceSpec struct { 522 GitilesURL, Project, Branch, Revision string 523 VersionFile string 524 } 525 526 func (s *sourceSpec) ArchiveURL() string { 527 return fmt.Sprintf("%s/%s/+archive/%s.tar.gz", s.GitilesURL, s.Project, s.Revision) 528 } 529 530 type moduleArtifact struct { 531 // The target for this module. 532 Target *releasetargets.Target 533 // The contents of the mod and info files. 534 Mod, Info string 535 // The scratch path of the zip within the scratch directory. 536 ZipScratch string // scratch path 537 } 538 539 // addBuildTasks registers tasks to build, test, and sign the release onto wd. 540 // It returns the resulting artifacts of various kinds. 541 func (tasks *BuildReleaseTasks) addBuildTasks(wd *wf.Definition, major int, kind task.ReleaseKind, version wf.Value[string], timestamp wf.Value[time.Time], sourceSpec wf.Value[sourceSpec]) (wf.Value[artifact], wf.Value[[]artifact], wf.Value[[]moduleArtifact]) { 542 targets := releasetargets.TargetsForGo1Point(major) 543 skipTests := wf.Param(wd, wf.ParamDef[[]string]{Name: "Targets to skip testing (or 'all') (optional)", ParamType: wf.SliceShort}) 544 545 source := wf.Task1(wd, "Build source archive", tasks.buildSource, sourceSpec) 546 artifacts := []wf.Value[artifact]{source} 547 var mods []wf.Value[moduleArtifact] 548 var blockers []wf.Dependency 549 550 // Build and sign binary artifacts for all targets. 551 for _, target := range targets { 552 wd := wd.Sub(target.Name) 553 554 // Build release artifacts for the platform using make.bash -distpack. 555 // For windows, produce both a tgz and zip -- we need tgzs to run 556 // tests, even though we'll eventually publish the zips. 557 var tar, zip wf.Value[artifact] 558 var mod wf.Value[moduleArtifact] 559 { // Block to improve diff readability. Can be unnested later. 560 distpack := wf.Task2(wd, "Build distpack", tasks.buildDistpack, wf.Const(target), source) 561 reproducer := wf.Task2(wd, "Reproduce distpack on Windows", tasks.reproduceDistpack, wf.Const(target), source) 562 match := wf.Action2(wd, "Check distpacks match", tasks.checkDistpacksMatch, distpack, reproducer) 563 blockers = append(blockers, match) 564 if target.GOOS == "windows" { 565 zip = wf.Task1(wd, "Get binary from distpack", tasks.binaryArchiveFromDistpack, distpack) 566 tar = wf.Task1(wd, "Convert zip to .tgz", tasks.convertZipToTGZ, zip) 567 } else { 568 tar = wf.Task1(wd, "Get binary from distpack", tasks.binaryArchiveFromDistpack, distpack) 569 } 570 mod = wf.Task1(wd, "Get module files from distpack", tasks.modFilesFromDistpack, distpack) 571 } 572 573 // Create installers and perform platform-specific signing where 574 // applicable. For macOS, produce updated tgz and module zips that 575 // include the signed binaries. 576 switch target.GOOS { 577 case "darwin": 578 pkg := wf.Task1(wd, "Build PKG installer", tasks.buildDarwinPKG, tar) 579 signedPKG := wf.Task2(wd, "Sign PKG installer", tasks.signArtifact, pkg, wf.Const(sign.BuildMacOS)) 580 signedTGZ := wf.Task2(wd, "Merge signed files into .tgz", tasks.mergeSignedToTGZ, tar, signedPKG) 581 mod = wf.Task4(wd, "Merge signed files into module zip", tasks.mergeSignedToModule, version, timestamp, mod, signedPKG) 582 artifacts = append(artifacts, signedPKG, signedTGZ) 583 case "windows": 584 msi := wf.Task1(wd, "Build MSI installer", tasks.buildWindowsMSI, tar) 585 signedMSI := wf.Task2(wd, "Sign MSI installer", tasks.signArtifact, msi, wf.Const(sign.BuildWindows)) 586 artifacts = append(artifacts, signedMSI, zip) 587 default: 588 artifacts = append(artifacts, tar) 589 } 590 mods = append(mods, mod) 591 } 592 signedArtifacts := wf.Task1(wd, "Compute GPG signature for artifacts", tasks.computeGPG, wf.Slice(artifacts...)) 593 594 // Test all targets. 595 builders := wf.Task2(wd, "Read builders", tasks.readRelevantBuilders, wf.Const(major), wf.Const(kind)) 596 builderResults := wf.Expand1(wd, "Plan builders", func(wd *wf.Definition, builders []string) (wf.Value[[]testResult], error) { 597 var results []wf.Value[testResult] 598 for _, b := range builders { 599 // Note: We can consider adding an "is_first_class" property into builder config 600 // and using it to display whether the builder is for a first class port or not. 601 // Until then, it's up to the release coordinator to make this distintinction when 602 // approving any failures. 603 res := wf.Task3(wd, "Run advisory builder "+b, tasks.runAdvisoryBuildBucket, wf.Const(b), skipTests, sourceSpec) 604 results = append(results, res) 605 } 606 return wf.Slice(results...), nil 607 }, builders) 608 buildersApproved := wf.Action1(wd, "Wait for advisory builders", tasks.checkTestResults, builderResults) 609 blockers = append(blockers, buildersApproved) 610 611 signedAndTested := wf.Task2(wd, "Wait for signing and tests", func(ctx *wf.TaskContext, artifacts []artifact, version string) ([]artifact, error) { 612 // Note: Note this needs to happen somewhere, doesn't matter where. Maybe move it to a nicer place later. 613 for i, a := range artifacts { 614 if a.Target != nil { 615 artifacts[i].Filename = version + "." + a.Target.Name + "." + a.Suffix 616 } else { 617 artifacts[i].Filename = version + "." + a.Suffix 618 } 619 } 620 621 return artifacts, nil 622 }, signedArtifacts, version, wf.After(blockers...), wf.After(wf.Slice(mods...))) 623 return source, signedAndTested, wf.Slice(mods...) 624 } 625 626 // BuildReleaseTasks serves as an adapter to the various build tasks in the task package. 627 type BuildReleaseTasks struct { 628 GerritClient task.GerritClient 629 GerritProject string 630 GerritHTTPClient *http.Client // GerritHTTPClient is an HTTP client that authenticates to Gerrit instances. (Both public and private.) 631 PrivateGerritClient task.GerritClient 632 GCSClient *storage.Client 633 ScratchFS *task.ScratchFS 634 SignedURL string // SignedURL is a gs:// or file:// URL, no trailing slash. 635 ServingURL string // ServingURL is a gs:// or file:// URL, no trailing slash. 636 DownloadURL string 637 ProxyPrefix string // ProxyPrefix is the prefix at which module files are published, e.g. https://proxy.golang.org/golang.org/toolchain/@v 638 PublishFile func(task.WebsiteFile) error 639 SignService sign.Service 640 GoogleDockerBuildProject string 641 GoogleDockerBuildTrigger string 642 CloudBuildClient task.CloudBuildClient 643 BuildBucketClient task.BuildBucketClient 644 SwarmingClient task.SwarmingClient 645 ApproveAction func(*wf.TaskContext) error 646 } 647 648 var commitRE = regexp.MustCompile(`[a-f0-9]{40}`) 649 650 func (b *BuildReleaseTasks) readSecurityRef(ctx *wf.TaskContext, project, ref string) (string, error) { 651 if ref == "" { 652 return "", nil 653 } 654 if commitRE.MatchString(ref) { 655 return ref, nil 656 } 657 commit, err := b.PrivateGerritClient.ReadBranchHead(ctx, project, ref) 658 if err != nil { 659 return "", fmt.Errorf("%q doesn't appear to be a commit hash, but resolving it as a branch failed: %v", ref, err) 660 } 661 return commit, nil 662 } 663 664 func (b *BuildReleaseTasks) getGitSource(ctx *wf.TaskContext, branch, commit, securityProject, securityCommit, versionFile string) (sourceSpec, error) { 665 client, project, rev := b.GerritClient, b.GerritProject, commit 666 if securityCommit != "" { 667 client, project, rev = b.PrivateGerritClient, securityProject, securityCommit 668 } 669 return sourceSpec{ 670 GitilesURL: client.GitilesURL(), 671 Project: project, 672 Branch: branch, 673 Revision: rev, 674 VersionFile: versionFile, 675 }, nil 676 } 677 678 func (b *BuildReleaseTasks) buildSource(ctx *wf.TaskContext, source sourceSpec) (artifact, error) { 679 resp, err := b.GerritHTTPClient.Get(source.ArchiveURL()) 680 if err != nil { 681 return artifact{}, err 682 } 683 if resp.StatusCode != http.StatusOK { 684 return artifact{}, fmt.Errorf("failed to fetch %q: %v", source.ArchiveURL(), resp.Status) 685 } 686 defer resp.Body.Close() 687 return b.runBuildStep(ctx, nil, artifact{}, "src.tar.gz", func(_ io.Reader, w io.Writer) error { 688 return b.buildSourceGCB(ctx, resp.Body, source.VersionFile, w) 689 }) 690 } 691 692 func (b *BuildReleaseTasks) buildSourceGCB(ctx *wf.TaskContext, r io.Reader, versionFile string, w io.Writer) error { 693 filename, f, err := b.ScratchFS.OpenWrite(ctx, "source.tgz") 694 if err != nil { 695 return err 696 } 697 if _, err := io.Copy(f, r); err != nil { 698 return err 699 } 700 if err := f.Close(); err != nil { 701 return err 702 } 703 704 script := fmt.Sprintf(` 705 gsutil cp %q source.tgz 706 mkdir go 707 tar -xf source.tgz -C go 708 echo -ne %q > go/VERSION 709 (cd go/src && GOOS=linux GOARCH=amd64 ./make.bash -distpack) 710 mv go/pkg/distpack/*.src.tar.gz src.tar.gz 711 `, b.ScratchFS.URL(ctx, filename), versionFile) 712 713 build, err := b.CloudBuildClient.RunScript(ctx, script, "", []string{"src.tar.gz"}) 714 if err != nil { 715 return err 716 } 717 if _, err := task.AwaitCondition(ctx, 30*time.Second, func() (string, bool, error) { 718 return b.CloudBuildClient.Completed(ctx, build) 719 }); err != nil { 720 return err 721 } 722 resultFS, err := b.CloudBuildClient.ResultFS(ctx, build) 723 if err != nil { 724 return err 725 } 726 distpack, err := resultFS.Open("src.tar.gz") 727 if err != nil { 728 return err 729 } 730 _, err = io.Copy(w, distpack) 731 return err 732 } 733 734 func (b *BuildReleaseTasks) checkSourceMatch(ctx *wf.TaskContext, branch, versionFile string, source artifact) (head string, _ error) { 735 head, err := b.GerritClient.ReadBranchHead(ctx, b.GerritProject, branch) 736 if err != nil { 737 return "", err 738 } 739 spec, err := b.getGitSource(ctx, branch, head, "", "", versionFile) 740 if err != nil { 741 return "", err 742 } 743 branchArchive, err := b.buildSource(ctx, spec) 744 if err != nil { 745 return "", err 746 } 747 diff, err := b.diffArtifacts(ctx, branchArchive, source) 748 if err != nil { 749 return "", err 750 } 751 if diff != "" { 752 return "", fmt.Errorf("branch state doesn't match source archive (-branch, +archive):\n%v", diff) 753 } 754 return head, nil 755 } 756 757 func (b *BuildReleaseTasks) diffArtifacts(ctx *wf.TaskContext, a1, a2 artifact) (string, error) { 758 h1, err := b.hashArtifact(ctx, a1) 759 if err != nil { 760 return "", fmt.Errorf("hashing first tarball: %v", err) 761 } 762 h2, err := b.hashArtifact(ctx, a2) 763 if err != nil { 764 return "", fmt.Errorf("hashing second tarball: %v", err) 765 } 766 return cmp.Diff(h1, h2), nil 767 } 768 769 func (b *BuildReleaseTasks) hashArtifact(ctx *wf.TaskContext, a artifact) (map[string]string, error) { 770 hashes := map[string]string{} 771 _, err := b.runBuildStep(ctx, nil, a, "", func(r io.Reader, _ io.Writer) error { 772 return tarballHashes(r, "", hashes, false) 773 }) 774 return hashes, err 775 } 776 777 func tarballHashes(r io.Reader, prefix string, hashes map[string]string, includeHeaders bool) error { 778 gzr, err := gzip.NewReader(r) 779 if err != nil { 780 return err 781 } 782 defer gzr.Close() 783 tr := tar.NewReader(gzr) 784 for { 785 header, err := tr.Next() 786 if err == io.EOF { 787 break 788 } else if err != nil { 789 return fmt.Errorf("reading tar header: %v", err) 790 } 791 if strings.HasSuffix(header.Name, ".tar.gz") { 792 if err := tarballHashes(tr, header.Name+":", hashes, true); err != nil { 793 return fmt.Errorf("reading inner tarball %v: %v", header.Name, err) 794 } 795 } else { 796 h := sha256.New() 797 if _, err := io.CopyN(h, tr, header.Size); err != nil { 798 return fmt.Errorf("reading file %q: %v", header.Name, err) 799 } 800 // At the top level, we don't care about headers, only contents. 801 // But in inner archives, headers are contents and we care a lot. 802 if includeHeaders { 803 hashes[prefix+header.Name] = fmt.Sprintf("%v %X", header, h.Sum(nil)) 804 } else { 805 hashes[prefix+header.Name] = fmt.Sprintf("%X", h.Sum(nil)) 806 } 807 } 808 } 809 return nil 810 } 811 812 func (b *BuildReleaseTasks) buildDistpack(ctx *wf.TaskContext, target *releasetargets.Target, source artifact) (artifact, error) { 813 return b.runBuildStep(ctx, target, artifact{}, "tar.gz", func(_ io.Reader, w io.Writer) error { 814 // We need GOROOT_FINAL both during the binary build and test runs. See go.dev/issue/52236. 815 // TODO(go.dev/issue/62047): GOROOT_FINAL is being removed. Remove it from here too. 816 makeEnv := []string{"GOROOT_FINAL=" + dashboard.GorootFinal(target.GOOS)} 817 // Add extra vars from the target's configuration. 818 makeEnv = append(makeEnv, target.ExtraEnv...) 819 makeEnv = append(makeEnv, "GOOS="+target.GOOS, "GOARCH="+target.GOARCH) 820 821 script := fmt.Sprintf(` 822 gsutil cp %q src.tar.gz 823 tar -xf src.tar.gz 824 (cd go/src && %v ./make.bash -distpack) 825 (cd go/pkg/distpack && tar -czf ../../../distpacks.tar.gz *) 826 `, b.ScratchFS.URL(ctx, source.Scratch), strings.Join(makeEnv, " ")) 827 build, err := b.CloudBuildClient.RunScript(ctx, script, "", []string{"distpacks.tar.gz"}) 828 if err != nil { 829 return err 830 } 831 if _, err := task.AwaitCondition(ctx, 30*time.Second, func() (string, bool, error) { 832 return b.CloudBuildClient.Completed(ctx, build) 833 }); err != nil { 834 return err 835 } 836 resultFS, err := b.CloudBuildClient.ResultFS(ctx, build) 837 if err != nil { 838 return err 839 } 840 distpack, err := resultFS.Open("distpacks.tar.gz") 841 if err != nil { 842 return err 843 } 844 _, err = io.Copy(w, distpack) 845 return err 846 }) 847 } 848 849 func (b *BuildReleaseTasks) reproduceDistpack(ctx *wf.TaskContext, target *releasetargets.Target, source artifact) (artifact, error) { 850 return b.runBuildStep(ctx, target, artifact{}, "tar.gz", func(_ io.Reader, w io.Writer) error { 851 scratchFile := b.ScratchFS.WriteFilename(ctx, fmt.Sprintf("reproduce-distpack-%v.tar.gz", target.Name)) 852 // This script is carefully crafted to work on both Windows and Unix 853 // for testing. In particular, Windows doesn't seem to like ./foo.exe, 854 // so we have to run it unadorned with . on PATH. 855 script := fmt.Sprintf( 856 `gsutil cat %s | tar -xzf - && cd go/src && make.bat -distpack && cd ../pkg/distpack && tar -czf - * | gsutil cp - %s`, 857 b.ScratchFS.URL(ctx, source.Scratch), b.ScratchFS.URL(ctx, scratchFile)) 858 859 env := map[string]string{ 860 "GOOS": target.GOOS, 861 "GOARCH": target.GOARCH, 862 } 863 for _, e := range target.ExtraEnv { 864 k, v, ok := strings.Cut(e, "=") 865 if !ok { 866 return fmt.Errorf("malformed env var %q", e) 867 } 868 env[k] = v 869 } 870 871 id, err := b.SwarmingClient.RunTask(ctx, map[string]string{ 872 "cipd_platform": "windows-amd64", 873 "os": "Windows-10", 874 }, script, env) 875 if err != nil { 876 return err 877 } 878 if _, err := task.AwaitCondition(ctx, 30*time.Second, func() (string, bool, error) { 879 return b.SwarmingClient.Completed(ctx, id) 880 }); err != nil { 881 return err 882 } 883 884 distpack, err := b.ScratchFS.OpenRead(ctx, scratchFile) 885 if err != nil { 886 return err 887 } 888 _, err = io.Copy(w, distpack) 889 return err 890 }) 891 892 } 893 894 func (b *BuildReleaseTasks) checkDistpacksMatch(ctx *wf.TaskContext, linux, windows artifact) error { 895 diff, err := b.diffArtifacts(ctx, linux, windows) 896 if err != nil { 897 return err 898 } 899 if diff != "" { 900 return fmt.Errorf("distpacks don't match (-linux, +windows): %v", diff) 901 } 902 return nil 903 } 904 905 func (b *BuildReleaseTasks) binaryArchiveFromDistpack(ctx *wf.TaskContext, distpack artifact) (artifact, error) { 906 // This must not match the module files, which currently start with v0.0.1. 907 glob := fmt.Sprintf("go*%v-%v.*", distpack.Target.GOOS, distpack.Target.GOARCH) 908 suffix := "tar.gz" 909 if distpack.Target.GOOS == "windows" { 910 suffix = "zip" 911 } 912 return b.runBuildStep(ctx, distpack.Target, distpack, suffix, func(r io.Reader, w io.Writer) error { 913 return task.ExtractFile(r, w, glob) 914 }) 915 } 916 917 func (b *BuildReleaseTasks) modFilesFromDistpack(ctx *wf.TaskContext, distpack artifact) (moduleArtifact, error) { 918 result := moduleArtifact{Target: distpack.Target} 919 artifact, err := b.runBuildStep(ctx, nil, distpack, "mod.zip", func(r io.Reader, w io.Writer) error { 920 zr, err := gzip.NewReader(r) 921 if err != nil { 922 return err 923 } 924 tr := tar.NewReader(zr) 925 foundZip := false 926 for { 927 h, err := tr.Next() 928 if err == io.EOF { 929 return io.ErrUnexpectedEOF 930 } else if err != nil { 931 return err 932 } 933 if h.FileInfo().IsDir() || !strings.HasPrefix(h.Name, "v0.0.1") { 934 continue 935 } 936 937 switch { 938 case strings.HasSuffix(h.Name, ".zip"): 939 if _, err := io.Copy(w, tr); err != nil { 940 return err 941 } 942 foundZip = true 943 case strings.HasSuffix(h.Name, ".info"): 944 buf := &bytes.Buffer{} 945 if _, err := io.Copy(buf, tr); err != nil { 946 return err 947 } 948 result.Info = buf.String() 949 case strings.HasSuffix(h.Name, ".mod"): 950 buf := &bytes.Buffer{} 951 if _, err := io.Copy(buf, tr); err != nil { 952 return err 953 } 954 result.Mod = buf.String() 955 } 956 957 if foundZip && result.Mod != "" && result.Info != "" { 958 return nil 959 } 960 } 961 }) 962 if err != nil { 963 return moduleArtifact{}, err 964 } 965 result.ZipScratch = artifact.Scratch 966 return result, nil 967 } 968 969 func (b *BuildReleaseTasks) modFilesFromBinary(ctx *wf.TaskContext, version string, t time.Time, tar artifact) (moduleArtifact, error) { 970 result := moduleArtifact{Target: tar.Target} 971 a, err := b.runBuildStep(ctx, nil, tar, "mod.zip", func(r io.Reader, w io.Writer) error { 972 ctx.DisableWatchdog() // The zipping process can be time consuming and is unlikely to hang. 973 var err error 974 result.Mod, result.Info, err = task.TarToModFiles(tar.Target, version, t, r, w) 975 return err 976 }) 977 if err != nil { 978 return moduleArtifact{}, err 979 } 980 result.ZipScratch = a.Scratch 981 return result, nil 982 } 983 984 func (b *BuildReleaseTasks) mergeSignedToTGZ(ctx *wf.TaskContext, unsigned, signed artifact) (artifact, error) { 985 return b.runBuildStep(ctx, unsigned.Target, signed, "tar.gz", func(signed io.Reader, w io.Writer) error { 986 signedBinaries, err := task.ReadBinariesFromPKG(signed) 987 if err != nil { 988 return err 989 } else if _, ok := signedBinaries["go/bin/go"]; !ok { 990 return fmt.Errorf("didn't find go/bin/go among %d signed binaries %+q", len(signedBinaries), maps.Keys(signedBinaries)) 991 } 992 993 // Copy files from the tgz, overwriting with binaries from the signed tar. 994 ur, err := b.ScratchFS.OpenRead(ctx, unsigned.Scratch) 995 if err != nil { 996 return err 997 } 998 defer ur.Close() 999 uzr, err := gzip.NewReader(ur) 1000 if err != nil { 1001 return err 1002 } 1003 defer uzr.Close() 1004 1005 utr := tar.NewReader(uzr) 1006 1007 zw, err := gzip.NewWriterLevel(w, gzip.BestCompression) 1008 if err != nil { 1009 return err 1010 } 1011 tw := tar.NewWriter(zw) 1012 1013 for { 1014 th, err := utr.Next() 1015 if err == io.EOF { 1016 break 1017 } else if err != nil { 1018 return err 1019 } 1020 1021 hdr := *th 1022 src := io.NopCloser(utr) 1023 if signed, ok := signedBinaries[th.Name]; ok { 1024 src = io.NopCloser(bytes.NewReader(signed)) 1025 hdr.Size = int64(len(signed)) 1026 } 1027 1028 if err := tw.WriteHeader(&hdr); err != nil { 1029 return err 1030 } 1031 if _, err := io.Copy(tw, src); err != nil { 1032 return err 1033 } 1034 } 1035 1036 if err := tw.Close(); err != nil { 1037 return err 1038 } 1039 return zw.Close() 1040 }) 1041 } 1042 1043 func (b *BuildReleaseTasks) mergeSignedToModule(ctx *wf.TaskContext, version string, timestamp time.Time, mod moduleArtifact, signed artifact) (moduleArtifact, error) { 1044 a, err := b.runBuildStep(ctx, nil, signed, "signedmod.zip", func(signed io.Reader, w io.Writer) error { 1045 signedBinaries, err := task.ReadBinariesFromPKG(signed) 1046 if err != nil { 1047 return err 1048 } else if _, ok := signedBinaries["go/bin/go"]; !ok { 1049 return fmt.Errorf("didn't find go/bin/go among %d signed binaries %+q", len(signedBinaries), maps.Keys(signedBinaries)) 1050 } 1051 1052 // Copy files from the module zip, overwriting with binaries from the signed tar. 1053 mr, err := b.ScratchFS.OpenRead(ctx, mod.ZipScratch) 1054 if err != nil { 1055 return err 1056 } 1057 defer mr.Close() 1058 mbytes, err := io.ReadAll(mr) 1059 if err != nil { 1060 return err 1061 } 1062 mzr, err := zip.NewReader(bytes.NewReader(mbytes), int64(len(mbytes))) 1063 if err != nil { 1064 return err 1065 } 1066 1067 prefix := task.ToolchainZipPrefix(mod.Target, version) + "/" 1068 mzw := zip.NewWriter(w) 1069 mzw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) { 1070 return flate.NewWriter(out, flate.BestCompression) 1071 }) 1072 for _, f := range mzr.File { 1073 var in io.ReadCloser 1074 suffix, ok := strings.CutPrefix(f.Name, prefix) 1075 if !ok { 1076 continue 1077 } 1078 if contents, ok := signedBinaries["go/"+suffix]; ok { 1079 in = io.NopCloser(bytes.NewReader(contents)) 1080 } else { 1081 in, err = f.Open() 1082 if err != nil { 1083 return err 1084 } 1085 } 1086 1087 hdr := f.FileHeader 1088 out, err := mzw.CreateHeader(&hdr) 1089 if err != nil { 1090 return err 1091 } 1092 if _, err := io.Copy(out, in); err != nil { 1093 return err 1094 } 1095 } 1096 return mzw.Close() 1097 }) 1098 if err != nil { 1099 return moduleArtifact{}, err 1100 } 1101 mod.ZipScratch = a.Scratch 1102 return mod, nil 1103 } 1104 1105 // buildDarwinPKG constructs an installer for the given binary artifact, to be signed. 1106 func (b *BuildReleaseTasks) buildDarwinPKG(ctx *wf.TaskContext, binary artifact) (artifact, error) { 1107 return b.runBuildStep(ctx, binary.Target, artifact{}, "pkg", func(_ io.Reader, w io.Writer) error { 1108 metadataFile, err := jsonEncodeScratchFile(ctx, b.ScratchFS, darwinpkg.InstallerOptions{ 1109 GOARCH: binary.Target.GOARCH, 1110 MinMacOSVersion: binary.Target.MinMacOSVersion, 1111 }) 1112 if err != nil { 1113 return err 1114 } 1115 installerPaths, err := b.signArtifacts(ctx, sign.BuildMacOSConstructInstallerOnly, []string{ 1116 b.ScratchFS.URL(ctx, binary.Scratch), 1117 b.ScratchFS.URL(ctx, metadataFile), 1118 }) 1119 if err != nil { 1120 return err 1121 } else if len(installerPaths) != 1 { 1122 return fmt.Errorf("got %d outputs, want 1 macOS .pkg installer", len(installerPaths)) 1123 } else if ext := path.Ext(installerPaths[0]); ext != ".pkg" { 1124 return fmt.Errorf("got output extension %q, want .pkg", ext) 1125 } 1126 resultFS, err := gcsfs.FromURL(ctx, b.GCSClient, b.SignedURL) 1127 if err != nil { 1128 return err 1129 } 1130 r, err := resultFS.Open(installerPaths[0]) 1131 if err != nil { 1132 return err 1133 } 1134 defer r.Close() 1135 _, err = io.Copy(w, r) 1136 return err 1137 }) 1138 } 1139 1140 // buildWindowsMSI constructs an installer for the given binary artifact, to be signed. 1141 func (b *BuildReleaseTasks) buildWindowsMSI(ctx *wf.TaskContext, binary artifact) (artifact, error) { 1142 return b.runBuildStep(ctx, binary.Target, artifact{}, "msi", func(_ io.Reader, w io.Writer) error { 1143 metadataFile, err := jsonEncodeScratchFile(ctx, b.ScratchFS, windowsmsi.InstallerOptions{ 1144 GOARCH: binary.Target.GOARCH, 1145 }) 1146 if err != nil { 1147 return err 1148 } 1149 installerPaths, err := b.signArtifacts(ctx, sign.BuildWindowsConstructInstallerOnly, []string{ 1150 b.ScratchFS.URL(ctx, binary.Scratch), 1151 b.ScratchFS.URL(ctx, metadataFile), 1152 }) 1153 if err != nil { 1154 return err 1155 } else if len(installerPaths) != 1 { 1156 return fmt.Errorf("got %d outputs, want 1 Windows .msi installer", len(installerPaths)) 1157 } else if ext := path.Ext(installerPaths[0]); ext != ".msi" { 1158 return fmt.Errorf("got output extension %q, want .msi", ext) 1159 } 1160 resultFS, err := gcsfs.FromURL(ctx, b.GCSClient, b.SignedURL) 1161 if err != nil { 1162 return err 1163 } 1164 r, err := resultFS.Open(installerPaths[0]) 1165 if err != nil { 1166 return err 1167 } 1168 defer r.Close() 1169 _, err = io.Copy(w, r) 1170 return err 1171 }) 1172 } 1173 1174 func (b *BuildReleaseTasks) convertZipToTGZ(ctx *wf.TaskContext, binary artifact) (artifact, error) { 1175 return b.runBuildStep(ctx, binary.Target, binary, "tar.gz", func(r io.Reader, w io.Writer) error { 1176 // Reading the whole file isn't ideal, but we need a ReaderAt, and 1177 // don't have access to the lower-level file (which would support 1178 // seeking) here. 1179 content, err := io.ReadAll(r) 1180 if err != nil { 1181 return err 1182 } 1183 return task.ConvertZIPToTGZ(bytes.NewReader(content), int64(len(content)), w) 1184 }) 1185 } 1186 1187 // computeGPG performs GPG signing on artifacts, and sets their GPGSignature field. 1188 func (b *BuildReleaseTasks) computeGPG(ctx *wf.TaskContext, artifacts []artifact) ([]artifact, error) { 1189 // doGPG reports whether to do GPG signature computation for artifact a. 1190 doGPG := func(a artifact) bool { 1191 return a.Suffix == "src.tar.gz" || a.Suffix == "tar.gz" 1192 } 1193 1194 // Start a signing job on all artifacts that want to do GPG signing and await its results. 1195 var in []string 1196 for _, a := range artifacts { 1197 if !doGPG(a) { 1198 continue 1199 } 1200 1201 in = append(in, b.ScratchFS.URL(ctx, a.Scratch)) 1202 } 1203 out, err := b.signArtifacts(ctx, sign.BuildGPG, in) 1204 if err != nil { 1205 return nil, err 1206 } else if len(out) != len(in) { 1207 return nil, fmt.Errorf("got %d outputs, want %d .asc signatures", len(out), len(in)) 1208 } 1209 // All done, we have our GPG signatures. 1210 // Put them in a base name → scratch path map. 1211 var signatures = make(map[string]string) 1212 for _, o := range out { 1213 signatures[path.Base(o)] = o 1214 } 1215 1216 // Set the artifacts' GPGSignature field. 1217 signedFS, err := gcsfs.FromURL(ctx, b.GCSClient, b.SignedURL) 1218 if err != nil { 1219 return nil, err 1220 } 1221 for i, a := range artifacts { 1222 if !doGPG(a) { 1223 continue 1224 } 1225 1226 sigPath, ok := signatures[path.Base(a.Scratch)+".asc"] 1227 if !ok { 1228 return nil, fmt.Errorf("no GPG signature for %q", path.Base(a.Scratch)) 1229 } 1230 sig, err := fs.ReadFile(signedFS, sigPath) 1231 if err != nil { 1232 return nil, err 1233 } 1234 artifacts[i].GPGSignature = string(sig) 1235 } 1236 1237 return artifacts, nil 1238 } 1239 1240 // signArtifact signs a single artifact of specified type. 1241 func (b *BuildReleaseTasks) signArtifact(ctx *wf.TaskContext, a artifact, bt sign.BuildType) (signed artifact, _ error) { 1242 return b.runBuildStep(ctx, a.Target, artifact{}, a.Suffix, func(_ io.Reader, w io.Writer) error { 1243 signedPaths, err := b.signArtifacts(ctx, bt, []string{b.ScratchFS.URL(ctx, a.Scratch)}) 1244 if err != nil { 1245 return err 1246 } else if len(signedPaths) != 1 { 1247 return fmt.Errorf("got %d outputs, want 1 signed artifact", len(signedPaths)) 1248 } 1249 1250 signedFS, err := gcsfs.FromURL(ctx, b.GCSClient, b.SignedURL) 1251 if err != nil { 1252 return err 1253 } 1254 r, err := signedFS.Open(signedPaths[0]) 1255 if err != nil { 1256 return err 1257 } 1258 _, err = io.Copy(w, r) 1259 return err 1260 }) 1261 } 1262 1263 // signArtifacts starts signing on the artifacts provided via the gs:// URL inputs, 1264 // waits for signing to complete, and returns the output paths relative to SignedURL. 1265 func (b *BuildReleaseTasks) signArtifacts(ctx *wf.TaskContext, bt sign.BuildType, inURLs []string) (outFiles []string, _ error) { 1266 jobID, err := b.SignService.SignArtifact(ctx, bt, inURLs) 1267 if err != nil { 1268 return nil, err 1269 } 1270 outURLs, jobError := task.AwaitCondition(ctx, time.Minute, func() (out []string, done bool, _ error) { 1271 statusContext, cancel := context.WithTimeout(ctx, time.Minute) 1272 defer cancel() 1273 t := time.Now() 1274 status, desc, out, err := b.SignService.ArtifactSigningStatus(statusContext, jobID) 1275 if err != nil { 1276 ctx.Printf("ArtifactSigningStatus ran into a retryable communication error after %v: %v\n", time.Since(t), err) 1277 return nil, false, nil 1278 } 1279 switch status { 1280 case sign.StatusCompleted: 1281 return out, true, nil // All done. 1282 case sign.StatusFailed: 1283 if desc != "" { 1284 return nil, true, fmt.Errorf("signing attempt failed: %s", desc) 1285 } 1286 return nil, true, fmt.Errorf("signing attempt failed") 1287 default: 1288 if desc != "" { 1289 ctx.Printf("still waiting: %s\n", desc) 1290 } 1291 return nil, false, nil // Still waiting. 1292 } 1293 }) 1294 if jobError != nil { 1295 // If ctx is canceled, also cancel the signing request. 1296 if ctx.Err() != nil { 1297 cancelContext, cancel := context.WithTimeout(context.Background(), time.Minute) 1298 defer cancel() 1299 t := time.Now() 1300 err := b.SignService.CancelSigning(cancelContext, jobID) 1301 if err != nil { 1302 ctx.Printf("CancelSigning error after %v: %v\n", time.Since(t), err) 1303 } 1304 } 1305 1306 return nil, jobError 1307 } 1308 1309 for _, url := range outURLs { 1310 f, ok := strings.CutPrefix(url, b.SignedURL+"/") 1311 if !ok { 1312 return nil, fmt.Errorf("got signed URL %q outside of signing result dir %q, which is unsupported", url, b.SignedURL+"/") 1313 } 1314 outFiles = append(outFiles, f) 1315 } 1316 return outFiles, nil 1317 } 1318 1319 func (b *BuildReleaseTasks) readRelevantBuilders(ctx *wf.TaskContext, major int, kind task.ReleaseKind) ([]string, error) { 1320 prefix := fmt.Sprintf("go1.%v-", major) 1321 if kind == task.KindBeta { 1322 prefix = "gotip-" 1323 } 1324 builders, err := b.BuildBucketClient.ListBuilders(ctx, "security-try") 1325 if err != nil { 1326 return nil, err 1327 } 1328 var relevant []string 1329 for name, b := range builders { 1330 if !strings.HasPrefix(name, prefix) { 1331 continue 1332 } 1333 var props struct { 1334 BuilderMode int `json:"mode"` 1335 KnownIssue int `json:"known_issue"` 1336 } 1337 if err := json.Unmarshal([]byte(b.Properties), &props); err != nil { 1338 return nil, fmt.Errorf("error unmarshaling properties for %v: %v", name, err) 1339 } 1340 var skip []string // Log-worthy causes of skip, if any. 1341 // golangbuildModePerf is golangbuild's MODE_PERF mode that 1342 // runs benchmarks. It's the first custom mode not relevant 1343 // to building and testing, and the expectation is that any 1344 // modes after it will be fine to skip for release purposes. 1345 // 1346 // See https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/experimental/golangbuild/golangbuildpb/params.proto;l=174-177;drc=fdea4abccf8447808d4e702c8d09fdd20fd81acb. 1347 const golangbuildModePerf = 4 1348 if props.BuilderMode >= golangbuildModePerf { 1349 skip = append(skip, fmt.Sprintf("custom mode %d", props.BuilderMode)) 1350 } 1351 if props.KnownIssue != 0 { 1352 skip = append(skip, fmt.Sprintf("known issue %d", props.KnownIssue)) 1353 } 1354 if len(skip) != 0 { 1355 ctx.Printf("skipping %s because of %s", name, strings.Join(skip, ", ")) 1356 continue 1357 } 1358 relevant = append(relevant, name) 1359 } 1360 slices.Sort(relevant) 1361 return relevant, nil 1362 } 1363 1364 type testResult struct { 1365 Name string 1366 Passed bool 1367 } 1368 1369 func (b *BuildReleaseTasks) runAdvisoryBuildBucket(ctx *wf.TaskContext, name string, skipTests []string, source sourceSpec) (testResult, error) { 1370 return b.runAdvisoryTest(ctx, name, skipTests, func() error { 1371 u, err := url.Parse(source.GitilesURL) 1372 if err != nil { 1373 return err 1374 } 1375 commit := &pb.GitilesCommit{ 1376 Host: u.Host, 1377 Project: source.Project, 1378 Id: source.Revision, 1379 Ref: "refs/heads/" + source.Branch, 1380 } 1381 id, err := b.BuildBucketClient.RunBuild(ctx, "security-try", name, commit, map[string]*structpb.Value{ 1382 "version_file": structpb.NewStringValue(source.VersionFile), 1383 }) 1384 if err != nil { 1385 return err 1386 } 1387 _, err = task.AwaitCondition(ctx, 30*time.Second, func() (string, bool, error) { 1388 return b.BuildBucketClient.Completed(ctx, id) 1389 }) 1390 return err 1391 }) 1392 } 1393 1394 func (b *BuildReleaseTasks) runAdvisoryTest(ctx *wf.TaskContext, name string, skipTests []string, run func() error) (testResult, error) { 1395 for _, skip := range skipTests { 1396 if skip == "all" || name == skip { 1397 ctx.Printf("Skipping test") 1398 return testResult{name, true}, nil 1399 } 1400 } 1401 err := errors.New("untested") // prime the loop 1402 for attempt := 1; attempt <= workflow.MaxRetries && err != nil; attempt++ { 1403 ctx.Printf("======== Attempt %d of %d ========\n", attempt, workflow.MaxRetries) 1404 err = run() 1405 if err != nil { 1406 ctx.Printf("Attempt failed: %v\n", err) 1407 } 1408 } 1409 if err != nil { 1410 ctx.Printf("Advisory test failed. Check the logs and approve this task if it's okay:\n") 1411 return testResult{name, false}, b.ApproveAction(ctx) 1412 } 1413 return testResult{name, true}, nil 1414 1415 } 1416 1417 func (b *BuildReleaseTasks) checkTestResults(ctx *wf.TaskContext, results []testResult) error { 1418 var fails []string 1419 for _, r := range results { 1420 if !r.Passed { 1421 fails = append(fails, r.Name) 1422 } 1423 } 1424 if len(fails) != 0 { 1425 sort.Strings(fails) 1426 ctx.Printf("Some advisory tests failed and their failures have been approved:\n%v", strings.Join(fails, "\n")) 1427 return nil 1428 } 1429 return nil 1430 } 1431 1432 // runBuildStep is a convenience function that manages resources a build step might need. 1433 // If input with a scratch file is specified, its content will be opened and passed as a Reader to f. 1434 // If outputSuffix is specified, a unique filename will be generated based off 1435 // it (and the target name, if any), the file will be opened and passed as a 1436 // Writer to f, and an artifact representing it will be returned as the result. 1437 func (b *BuildReleaseTasks) runBuildStep( 1438 ctx *wf.TaskContext, 1439 target *releasetargets.Target, 1440 input artifact, 1441 outputSuffix string, 1442 f func(io.Reader, io.Writer) error, 1443 ) (artifact, error) { 1444 var err error 1445 var in io.ReadCloser 1446 if input.Scratch != "" { 1447 in, err = b.ScratchFS.OpenRead(ctx, input.Scratch) 1448 if err != nil { 1449 return artifact{}, err 1450 } 1451 defer in.Close() 1452 } 1453 var out io.WriteCloser 1454 var scratch string 1455 hash := sha256.New() 1456 size := &sizeWriter{} 1457 var multiOut io.Writer 1458 if outputSuffix != "" { 1459 name := outputSuffix 1460 if target != nil { 1461 name = target.Name + "." + outputSuffix 1462 } 1463 scratch, out, err = b.ScratchFS.OpenWrite(ctx, name) 1464 if err != nil { 1465 return artifact{}, err 1466 } 1467 defer out.Close() 1468 multiOut = io.MultiWriter(out, hash, size) 1469 } 1470 // Hide in's Close method from the task, which may assert it to Closer. 1471 nopIn := io.NopCloser(in) 1472 if err := f(nopIn, multiOut); err != nil { 1473 return artifact{}, err 1474 } 1475 if in != nil { 1476 if err := in.Close(); err != nil { 1477 return artifact{}, err 1478 } 1479 } 1480 if out != nil { 1481 if err := out.Close(); err != nil { 1482 return artifact{}, err 1483 } 1484 } 1485 return artifact{ 1486 Target: target, 1487 Scratch: scratch, 1488 Suffix: outputSuffix, 1489 SHA256: fmt.Sprintf("%x", string(hash.Sum([]byte(nil)))), 1490 Size: size.size, 1491 }, nil 1492 } 1493 1494 // An artifact represents a file as it moves through the release process. Most 1495 // files will appear on go.dev/dl eventually. 1496 type artifact struct { 1497 // The target platform of this artifact, or nil for source. 1498 Target *releasetargets.Target 1499 // The filename of this artifact, as used with the tasks' ScratchFS. 1500 Scratch string 1501 // The contents of the GPG signature for this artifact (.asc file). 1502 GPGSignature string 1503 // The filename suffix of the artifact, e.g. "tar.gz" or "src.tar.gz", 1504 // combined with the version and Target name to produce Filename. 1505 Suffix string 1506 // The final Filename of this artifact as it will be downloaded. 1507 Filename string 1508 SHA256 string 1509 Size int 1510 } 1511 1512 type sizeWriter struct { 1513 size int 1514 } 1515 1516 func (w *sizeWriter) Write(p []byte) (n int, err error) { 1517 w.size += len(p) 1518 return len(p), nil 1519 } 1520 1521 func (tasks *BuildReleaseTasks) uploadArtifacts(ctx *wf.TaskContext, artifacts []artifact) error { 1522 servingFS, err := gcsfs.FromURL(ctx, tasks.GCSClient, tasks.ServingURL) 1523 if err != nil { 1524 return err 1525 } 1526 1527 want := map[string]bool{} // URLs we're waiting on becoming available. 1528 for _, a := range artifacts { 1529 if err := tasks.uploadFile(ctx, servingFS, a.Scratch, a.Filename); err != nil { 1530 return err 1531 } 1532 want[tasks.DownloadURL+"/"+a.Filename] = true 1533 1534 if err := gcsfs.WriteFile(servingFS, a.Filename+".sha256", []byte(a.SHA256)); err != nil { 1535 return err 1536 } 1537 want[tasks.DownloadURL+"/"+a.Filename+".sha256"] = true 1538 1539 if a.GPGSignature != "" { 1540 if err := gcsfs.WriteFile(servingFS, a.Filename+".asc", []byte(a.GPGSignature)); err != nil { 1541 return err 1542 } 1543 want[tasks.DownloadURL+"/"+a.Filename+".asc"] = true 1544 } 1545 } 1546 _, err = task.AwaitCondition(ctx, 30*time.Second, checkFiles(ctx, want)) 1547 return err 1548 } 1549 1550 func (tasks *BuildReleaseTasks) uploadModules(ctx *wf.TaskContext, version string, modules []moduleArtifact) error { 1551 servingFS, err := gcsfs.FromURL(ctx, tasks.GCSClient, tasks.ServingURL) 1552 if err != nil { 1553 return err 1554 } 1555 want := map[string]bool{} // URLs we're waiting on becoming available. 1556 for _, mod := range modules { 1557 base := task.ToolchainModuleVersion(mod.Target, version) 1558 if err := tasks.uploadFile(ctx, servingFS, mod.ZipScratch, fmt.Sprintf(base+".zip")); err != nil { 1559 return err 1560 } 1561 if err := gcsfs.WriteFile(servingFS, base+".info", []byte(mod.Info)); err != nil { 1562 return err 1563 } 1564 if err := gcsfs.WriteFile(servingFS, base+".mod", []byte(mod.Mod)); err != nil { 1565 return err 1566 } 1567 for _, ext := range []string{".zip", ".info", ".mod"} { 1568 want[tasks.DownloadURL+"/"+base+ext] = true 1569 } 1570 } 1571 _, err = task.AwaitCondition(ctx, 30*time.Second, checkFiles(ctx, want)) 1572 return err 1573 } 1574 1575 func (tasks *BuildReleaseTasks) awaitProxy(ctx *wf.TaskContext, version string, modules []moduleArtifact) error { 1576 want := map[string]bool{} 1577 for _, mod := range modules { 1578 url := fmt.Sprintf("%v/%v.info", tasks.ProxyPrefix, task.ToolchainModuleVersion(mod.Target, version)) 1579 want[url] = true 1580 } 1581 _, err := task.AwaitCondition(ctx, 30*time.Second, checkFiles(ctx, want)) 1582 return err 1583 } 1584 1585 func checkFiles(ctx context.Context, want map[string]bool) func() (int, bool, error) { 1586 found := map[string]bool{} 1587 return func() (int, bool, error) { 1588 for url := range want { 1589 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 1590 defer cancel() 1591 resp, err := ctxhttp.Head(ctx, http.DefaultClient, url) 1592 if err == context.DeadlineExceeded { 1593 cancel() 1594 continue 1595 } 1596 if err != nil { 1597 return 0, false, err 1598 } 1599 resp.Body.Close() 1600 cancel() 1601 if resp.StatusCode == http.StatusOK { 1602 found[url] = true 1603 } 1604 } 1605 return 0, len(want) == len(found), nil 1606 } 1607 } 1608 1609 // uploadFile copies a file from tasks.ScratchFS to servingFS. 1610 func (tasks *BuildReleaseTasks) uploadFile(ctx *wf.TaskContext, servingFS fs.FS, scratch, filename string) error { 1611 in, err := tasks.ScratchFS.OpenRead(ctx, scratch) 1612 if err != nil { 1613 return err 1614 } 1615 defer in.Close() 1616 1617 out, err := gcsfs.Create(servingFS, filename) 1618 if err != nil { 1619 return err 1620 } 1621 defer out.Close() 1622 if _, err := io.Copy(out, in); err != nil { 1623 return err 1624 } 1625 if err := out.Close(); err != nil { 1626 return err 1627 } 1628 return nil 1629 } 1630 1631 // publishArtifacts publishes artifacts for version (typically so they appear at https://go.dev/dl/). 1632 // It returns the Go version and files that have been successfully published. 1633 func (tasks *BuildReleaseTasks) publishArtifacts(ctx *wf.TaskContext, version string, artifacts []artifact) (task.Published, error) { 1634 // Each release artifact corresponds to a single website file. 1635 var files = make([]task.WebsiteFile, len(artifacts)) 1636 for i, a := range artifacts { 1637 // Define website file metadata. 1638 f := task.WebsiteFile{ 1639 Filename: a.Filename, 1640 Version: version, 1641 ChecksumSHA256: a.SHA256, 1642 Size: int64(a.Size), 1643 } 1644 if a.Target != nil { 1645 f.OS = a.Target.GOOS 1646 f.Arch = a.Target.GOARCH 1647 if a.Target.GOARCH == "arm" { 1648 f.Arch = "armv6l" 1649 } 1650 } 1651 switch a.Suffix { 1652 case "src.tar.gz": 1653 f.Kind = "source" 1654 case "tar.gz", "zip": 1655 f.Kind = "archive" 1656 case "msi", "pkg": 1657 f.Kind = "installer" 1658 } 1659 1660 // Publish it. 1661 if err := tasks.PublishFile(f); err != nil { 1662 return task.Published{}, err 1663 } 1664 ctx.Printf("Published %q.", f.Filename) 1665 files[i] = f 1666 } 1667 ctx.Printf("Published all %d files for %s.", len(files), version) 1668 return task.Published{Version: version, Files: files}, nil 1669 } 1670 1671 func (b *BuildReleaseTasks) runGoogleDockerBuild(ctx context.Context, version string) (task.CloudBuild, error) { 1672 // Because we want to publish versions without the leading "go", it's easiest to strip it here. 1673 v := strings.TrimPrefix(version, "go") 1674 return b.CloudBuildClient.RunBuildTrigger(ctx, b.GoogleDockerBuildProject, b.GoogleDockerBuildTrigger, map[string]string{"_GO_VERSION": v}) 1675 } 1676 1677 func (b *BuildReleaseTasks) awaitCloudBuild(ctx *wf.TaskContext, build task.CloudBuild) (string, error) { 1678 detail, err := task.AwaitCondition(ctx, 30*time.Second, func() (string, bool, error) { 1679 return b.CloudBuildClient.Completed(ctx, build) 1680 }) 1681 return detail, err 1682 } 1683 1684 // jsonEncodeScratchFile JSON encodes v into a new scratch file and returns its name. 1685 func jsonEncodeScratchFile(ctx *wf.TaskContext, fs *task.ScratchFS, v any) (name string, _ error) { 1686 name, f, err := fs.OpenWrite(ctx, "f.json") 1687 if err != nil { 1688 return "", err 1689 } 1690 e := json.NewEncoder(f) 1691 e.SetIndent("", "\t") 1692 e.SetEscapeHTML(false) 1693 if err := e.Encode(v); err != nil { 1694 f.Close() 1695 return "", err 1696 } 1697 if err := f.Close(); err != nil { 1698 return "", err 1699 } 1700 return name, nil 1701 }