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  }