xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/releaser/upgradedep.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1*9bb1b549SSpandan Das// Copyright 2021 The Bazel Authors. All rights reserved.
2*9bb1b549SSpandan Das//
3*9bb1b549SSpandan Das// Licensed under the Apache License, Version 2.0 (the "License");
4*9bb1b549SSpandan Das// you may not use this file except in compliance with the License.
5*9bb1b549SSpandan Das// You may obtain a copy of the License at
6*9bb1b549SSpandan Das//
7*9bb1b549SSpandan Das//    http://www.apache.org/licenses/LICENSE-2.0
8*9bb1b549SSpandan Das//
9*9bb1b549SSpandan Das// Unless required by applicable law or agreed to in writing, software
10*9bb1b549SSpandan Das// distributed under the License is distributed on an "AS IS" BASIS,
11*9bb1b549SSpandan Das// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*9bb1b549SSpandan Das// See the License for the specific language governing permissions and
13*9bb1b549SSpandan Das// limitations under the License.
14*9bb1b549SSpandan Das
15*9bb1b549SSpandan Daspackage main
16*9bb1b549SSpandan Das
17*9bb1b549SSpandan Dasimport (
18*9bb1b549SSpandan Das	"bytes"
19*9bb1b549SSpandan Das	"context"
20*9bb1b549SSpandan Das	"crypto/sha256"
21*9bb1b549SSpandan Das	"encoding/hex"
22*9bb1b549SSpandan Das	"errors"
23*9bb1b549SSpandan Das	"flag"
24*9bb1b549SSpandan Das	"fmt"
25*9bb1b549SSpandan Das	"io"
26*9bb1b549SSpandan Das	"net/http"
27*9bb1b549SSpandan Das	"os"
28*9bb1b549SSpandan Das	"os/exec"
29*9bb1b549SSpandan Das	"path"
30*9bb1b549SSpandan Das	"path/filepath"
31*9bb1b549SSpandan Das	"strings"
32*9bb1b549SSpandan Das	"time"
33*9bb1b549SSpandan Das
34*9bb1b549SSpandan Das	bzl "github.com/bazelbuild/buildtools/build"
35*9bb1b549SSpandan Das	"github.com/google/go-github/v36/github"
36*9bb1b549SSpandan Das	"golang.org/x/mod/semver"
37*9bb1b549SSpandan Das	"golang.org/x/oauth2"
38*9bb1b549SSpandan Das	"golang.org/x/sync/errgroup"
39*9bb1b549SSpandan Das)
40*9bb1b549SSpandan Das
41*9bb1b549SSpandan Dasvar upgradeDepCmd = command{
42*9bb1b549SSpandan Das	name:        "upgrade-dep",
43*9bb1b549SSpandan Das	description: "upgrades a dependency in WORKSPACE or go_repositories.bzl",
44*9bb1b549SSpandan Das	help: `releaser upgrade-dep [-githubtoken=token] [-mirror] [-work] deps...
45*9bb1b549SSpandan Das
46*9bb1b549SSpandan Dasupgrade-dep upgrades one or more rules_go dependencies in WORKSPACE or
47*9bb1b549SSpandan Dasgo/private/repositories.bzl. Dependency names (matching the name attributes)
48*9bb1b549SSpandan Dascan be specified with positional arguments. "all" may be specified to upgrade
49*9bb1b549SSpandan Dasall upgradeable dependencies.
50*9bb1b549SSpandan Das
51*9bb1b549SSpandan DasFor each dependency, upgrade-dep finds the highest version available in the
52*9bb1b549SSpandan Dasupstream repository. If no version is available, upgrade-dep uses the commit
53*9bb1b549SSpandan Dasat the tip of the default branch. If a version is part of a release,
54*9bb1b549SSpandan Dasupgrade-dep will try to use an archive attached to the release; if none is
55*9bb1b549SSpandan Dasavailable, upgrade-dep uses an archive generated by GitHub.
56*9bb1b549SSpandan Das
57*9bb1b549SSpandan DasOnce upgrade-dep has found the URL for the latest version, it will:
58*9bb1b549SSpandan Das
59*9bb1b549SSpandan Das* Download the archive.
60*9bb1b549SSpandan Das* Upload the archive to mirror.bazel.build.
61*9bb1b549SSpandan Das* Re-generate patches, either by running a command or by re-applying the
62*9bb1b549SSpandan Das  old patches.
63*9bb1b549SSpandan Das* Update dependency attributes in WORKSPACE and repositories.bzl, then format
64*9bb1b549SSpandan Das  and rewrite those files.
65*9bb1b549SSpandan Das
66*9bb1b549SSpandan DasUpgradeable dependencies need a comment like '# releaser:upgrade-dep org repo'
67*9bb1b549SSpandan Daswhere org and repo are the GitHub organization and repository. We could
68*9bb1b549SSpandan Daspotentially fetch archives from proxy.golang.org instead, but it's not available
69*9bb1b549SSpandan Dasin as many countries.
70*9bb1b549SSpandan Das
71*9bb1b549SSpandan DasPatches may have a comment like '# releaser:patch-cmd name args...'. If this
72*9bb1b549SSpandan Dascomment is present, upgrade-dep will generate the patch by running the specified
73*9bb1b549SSpandan Dascommand in a temporary directory containing the extracted archive with the
74*9bb1b549SSpandan Dasprevious patches applied.
75*9bb1b549SSpandan Das`,
76*9bb1b549SSpandan Das}
77*9bb1b549SSpandan Das
78*9bb1b549SSpandan Dasfunc init() {
79*9bb1b549SSpandan Das	// break init cycle
80*9bb1b549SSpandan Das	upgradeDepCmd.run = runUpgradeDep
81*9bb1b549SSpandan Das}
82*9bb1b549SSpandan Das
83*9bb1b549SSpandan Dasfunc runUpgradeDep(ctx context.Context, stderr io.Writer, args []string) error {
84*9bb1b549SSpandan Das	// Parse arguments.
85*9bb1b549SSpandan Das	flags := flag.NewFlagSet("releaser upgrade-dep", flag.ContinueOnError)
86*9bb1b549SSpandan Das	var githubToken githubTokenFlag
87*9bb1b549SSpandan Das	var uploadToMirror, leaveWorkDir bool
88*9bb1b549SSpandan Das	flags.Var(&githubToken, "githubtoken", "GitHub personal access token or path to a file containing it")
89*9bb1b549SSpandan Das	flags.BoolVar(&uploadToMirror, "mirror", true, "whether to upload dependency archives to mirror.bazel.build")
90*9bb1b549SSpandan Das	flags.BoolVar(&leaveWorkDir, "work", false, "don't delete temporary work directory (for debugging)")
91*9bb1b549SSpandan Das	if err := flags.Parse(args); err != nil {
92*9bb1b549SSpandan Das		return err
93*9bb1b549SSpandan Das	}
94*9bb1b549SSpandan Das	if flags.NArg() == 0 {
95*9bb1b549SSpandan Das		return usageErrorf(&upgradeDepCmd, "No dependencies specified")
96*9bb1b549SSpandan Das	}
97*9bb1b549SSpandan Das	upgradeAll := false
98*9bb1b549SSpandan Das	for _, arg := range flags.Args() {
99*9bb1b549SSpandan Das		if arg == "all" {
100*9bb1b549SSpandan Das			upgradeAll = true
101*9bb1b549SSpandan Das			break
102*9bb1b549SSpandan Das		}
103*9bb1b549SSpandan Das	}
104*9bb1b549SSpandan Das	if upgradeAll && flags.NArg() != 1 {
105*9bb1b549SSpandan Das		return usageErrorf(&upgradeDepCmd, "When 'all' is specified, it must be the only argument")
106*9bb1b549SSpandan Das	}
107*9bb1b549SSpandan Das
108*9bb1b549SSpandan Das	httpClient := http.DefaultClient
109*9bb1b549SSpandan Das	if githubToken != "" {
110*9bb1b549SSpandan Das		ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: string(githubToken)})
111*9bb1b549SSpandan Das		httpClient = oauth2.NewClient(ctx, ts)
112*9bb1b549SSpandan Das	}
113*9bb1b549SSpandan Das	gh := &githubClient{Client: github.NewClient(httpClient)}
114*9bb1b549SSpandan Das
115*9bb1b549SSpandan Das	workDir, err := os.MkdirTemp("", "releaser-upgrade-dep-*")
116*9bb1b549SSpandan Das	if leaveWorkDir {
117*9bb1b549SSpandan Das		fmt.Fprintf(stderr, "work dir: %s\n", workDir)
118*9bb1b549SSpandan Das	} else {
119*9bb1b549SSpandan Das		defer func() {
120*9bb1b549SSpandan Das			if rerr := os.RemoveAll(workDir); err == nil && rerr != nil {
121*9bb1b549SSpandan Das				err = rerr
122*9bb1b549SSpandan Das			}
123*9bb1b549SSpandan Das		}()
124*9bb1b549SSpandan Das	}
125*9bb1b549SSpandan Das
126*9bb1b549SSpandan Das	// Make sure we have everything we need.
127*9bb1b549SSpandan Das	// upgrade-dep must be run inside rules_go (though we just check for
128*9bb1b549SSpandan Das	// WORKSPACE), and a few tools must be available.
129*9bb1b549SSpandan Das	rootDir, err := repoRoot()
130*9bb1b549SSpandan Das	if err != nil {
131*9bb1b549SSpandan Das		return err
132*9bb1b549SSpandan Das	}
133*9bb1b549SSpandan Das	for _, tool := range []string{"diff", "gazelle", "gsutil", "patch"} {
134*9bb1b549SSpandan Das		if _, err := exec.LookPath(tool); err != nil {
135*9bb1b549SSpandan Das			return fmt.Errorf("%s must be installed in PATH", tool)
136*9bb1b549SSpandan Das		}
137*9bb1b549SSpandan Das	}
138*9bb1b549SSpandan Das
139*9bb1b549SSpandan Das	// Parse and index files we might want to update.
140*9bb1b549SSpandan Das	type file struct {
141*9bb1b549SSpandan Das		path     string
142*9bb1b549SSpandan Das		funcName string
143*9bb1b549SSpandan Das		parsed   *bzl.File
144*9bb1b549SSpandan Das		body     []bzl.Expr
145*9bb1b549SSpandan Das	}
146*9bb1b549SSpandan Das	files := []file{
147*9bb1b549SSpandan Das		{path: filepath.Join(rootDir, "WORKSPACE")},
148*9bb1b549SSpandan Das		{path: filepath.Join(rootDir, "go/private/repositories.bzl"), funcName: "go_rules_dependencies"},
149*9bb1b549SSpandan Das	}
150*9bb1b549SSpandan Das	depIndex := make(map[string]*bzl.CallExpr)
151*9bb1b549SSpandan Das
152*9bb1b549SSpandan Das	for i := range files {
153*9bb1b549SSpandan Das		f := &files[i]
154*9bb1b549SSpandan Das		data, err := os.ReadFile(f.path)
155*9bb1b549SSpandan Das		if err != nil {
156*9bb1b549SSpandan Das			return err
157*9bb1b549SSpandan Das		}
158*9bb1b549SSpandan Das		f.parsed, err = bzl.Parse(f.path, data)
159*9bb1b549SSpandan Das		if err != nil {
160*9bb1b549SSpandan Das			return err
161*9bb1b549SSpandan Das		}
162*9bb1b549SSpandan Das
163*9bb1b549SSpandan Das		if f.funcName == "" {
164*9bb1b549SSpandan Das			f.body = f.parsed.Stmt
165*9bb1b549SSpandan Das		} else {
166*9bb1b549SSpandan Das			for _, expr := range f.parsed.Stmt {
167*9bb1b549SSpandan Das				def, ok := expr.(*bzl.DefStmt)
168*9bb1b549SSpandan Das				if !ok {
169*9bb1b549SSpandan Das					continue
170*9bb1b549SSpandan Das				}
171*9bb1b549SSpandan Das				if def.Name == f.funcName {
172*9bb1b549SSpandan Das					f.body = def.Body
173*9bb1b549SSpandan Das					break
174*9bb1b549SSpandan Das				}
175*9bb1b549SSpandan Das			}
176*9bb1b549SSpandan Das			if f.body == nil {
177*9bb1b549SSpandan Das				return fmt.Errorf("in file %s, could not find function %s", f.path, f.funcName)
178*9bb1b549SSpandan Das			}
179*9bb1b549SSpandan Das		}
180*9bb1b549SSpandan Das
181*9bb1b549SSpandan Das		for _, expr := range f.body {
182*9bb1b549SSpandan Das			call, ok := expr.(*bzl.CallExpr)
183*9bb1b549SSpandan Das			if !ok {
184*9bb1b549SSpandan Das				continue
185*9bb1b549SSpandan Das			}
186*9bb1b549SSpandan Das			for _, arg := range call.List {
187*9bb1b549SSpandan Das				kwarg, ok := arg.(*bzl.AssignExpr)
188*9bb1b549SSpandan Das				if !ok {
189*9bb1b549SSpandan Das					continue
190*9bb1b549SSpandan Das				}
191*9bb1b549SSpandan Das				key := kwarg.LHS.(*bzl.Ident) // required by parser
192*9bb1b549SSpandan Das				if key.Name != "name" {
193*9bb1b549SSpandan Das					continue
194*9bb1b549SSpandan Das				}
195*9bb1b549SSpandan Das				value, ok := kwarg.RHS.(*bzl.StringExpr)
196*9bb1b549SSpandan Das				if !ok {
197*9bb1b549SSpandan Das					continue
198*9bb1b549SSpandan Das				}
199*9bb1b549SSpandan Das				depIndex[value.Value] = call
200*9bb1b549SSpandan Das			}
201*9bb1b549SSpandan Das		}
202*9bb1b549SSpandan Das	}
203*9bb1b549SSpandan Das
204*9bb1b549SSpandan Das	// Update dependencies in those files.
205*9bb1b549SSpandan Das	eg, egctx := errgroup.WithContext(ctx)
206*9bb1b549SSpandan Das	if upgradeAll {
207*9bb1b549SSpandan Das		for name := range depIndex {
208*9bb1b549SSpandan Das			name := name
209*9bb1b549SSpandan Das			if _, _, err := parseUpgradeDepDirective(depIndex[name]); err != nil {
210*9bb1b549SSpandan Das				continue
211*9bb1b549SSpandan Das			}
212*9bb1b549SSpandan Das			eg.Go(func() error {
213*9bb1b549SSpandan Das				return upgradeDepDecl(egctx, gh, workDir, name, depIndex[name], uploadToMirror)
214*9bb1b549SSpandan Das			})
215*9bb1b549SSpandan Das		}
216*9bb1b549SSpandan Das	} else {
217*9bb1b549SSpandan Das		for _, arg := range flags.Args() {
218*9bb1b549SSpandan Das			if depIndex[arg] == nil {
219*9bb1b549SSpandan Das				return fmt.Errorf("could not find dependency %s", arg)
220*9bb1b549SSpandan Das			}
221*9bb1b549SSpandan Das		}
222*9bb1b549SSpandan Das		for _, arg := range flags.Args() {
223*9bb1b549SSpandan Das			arg := arg
224*9bb1b549SSpandan Das			eg.Go(func() error {
225*9bb1b549SSpandan Das				return upgradeDepDecl(egctx, gh, workDir, arg, depIndex[arg], uploadToMirror)
226*9bb1b549SSpandan Das			})
227*9bb1b549SSpandan Das		}
228*9bb1b549SSpandan Das	}
229*9bb1b549SSpandan Das	if err := eg.Wait(); err != nil {
230*9bb1b549SSpandan Das		return err
231*9bb1b549SSpandan Das	}
232*9bb1b549SSpandan Das
233*9bb1b549SSpandan Das	// Format and write files back to disk.
234*9bb1b549SSpandan Das	for _, f := range files {
235*9bb1b549SSpandan Das		if err := os.WriteFile(f.path, bzl.Format(f.parsed), 0666); err != nil {
236*9bb1b549SSpandan Das			return err
237*9bb1b549SSpandan Das		}
238*9bb1b549SSpandan Das	}
239*9bb1b549SSpandan Das	return nil
240*9bb1b549SSpandan Das}
241*9bb1b549SSpandan Das
242*9bb1b549SSpandan Das// upgradeDepDecl upgrades a specific dependency.
243*9bb1b549SSpandan Dasfunc upgradeDepDecl(ctx context.Context, gh *githubClient, workDir, name string, call *bzl.CallExpr, uploadToMirror bool) (err error) {
244*9bb1b549SSpandan Das	defer func() {
245*9bb1b549SSpandan Das		if err != nil {
246*9bb1b549SSpandan Das			err = fmt.Errorf("upgrading %s: %w", name, err)
247*9bb1b549SSpandan Das		}
248*9bb1b549SSpandan Das	}()
249*9bb1b549SSpandan Das
250*9bb1b549SSpandan Das	// Find a '# releaser:upgrade-dep org repo' comment. We could probably
251*9bb1b549SSpandan Das	// figure this out from URLs but this also serves to mark a dependency as
252*9bb1b549SSpandan Das	// being automatically upgradeable.
253*9bb1b549SSpandan Das	orgName, repoName, err := parseUpgradeDepDirective(call)
254*9bb1b549SSpandan Das	if err != nil {
255*9bb1b549SSpandan Das		return err
256*9bb1b549SSpandan Das	}
257*9bb1b549SSpandan Das
258*9bb1b549SSpandan Das	// Find attributes we'll need to read or write. We'll modify these directly
259*9bb1b549SSpandan Das	// in the AST. Nothing else should read or write them while we're working.
260*9bb1b549SSpandan Das	attrs := map[string]*bzl.Expr{
261*9bb1b549SSpandan Das		"patches":      nil,
262*9bb1b549SSpandan Das		"sha256":       nil,
263*9bb1b549SSpandan Das		"strip_prefix": nil,
264*9bb1b549SSpandan Das		"urls":         nil,
265*9bb1b549SSpandan Das	}
266*9bb1b549SSpandan Das	var urlsKwarg *bzl.AssignExpr
267*9bb1b549SSpandan Das	for _, arg := range call.List {
268*9bb1b549SSpandan Das		kwarg, ok := arg.(*bzl.AssignExpr)
269*9bb1b549SSpandan Das		if !ok {
270*9bb1b549SSpandan Das			continue
271*9bb1b549SSpandan Das		}
272*9bb1b549SSpandan Das		key := kwarg.LHS.(*bzl.Ident) // required by parser
273*9bb1b549SSpandan Das		if _, ok := attrs[key.Name]; ok {
274*9bb1b549SSpandan Das			attrs[key.Name] = &kwarg.RHS
275*9bb1b549SSpandan Das		}
276*9bb1b549SSpandan Das		if key.Name == "urls" {
277*9bb1b549SSpandan Das			urlsKwarg = kwarg
278*9bb1b549SSpandan Das		}
279*9bb1b549SSpandan Das	}
280*9bb1b549SSpandan Das	for key := range attrs {
281*9bb1b549SSpandan Das		if key == "patches" {
282*9bb1b549SSpandan Das			// Don't add optional attributes.
283*9bb1b549SSpandan Das			continue
284*9bb1b549SSpandan Das		}
285*9bb1b549SSpandan Das		if attrs[key] == nil {
286*9bb1b549SSpandan Das			kwarg := &bzl.AssignExpr{LHS: &bzl.Ident{Name: key}, Op: "="}
287*9bb1b549SSpandan Das			call.List = append(call.List, kwarg)
288*9bb1b549SSpandan Das			attrs[key] = &kwarg.RHS
289*9bb1b549SSpandan Das		}
290*9bb1b549SSpandan Das	}
291*9bb1b549SSpandan Das
292*9bb1b549SSpandan Das	// Find the highest tag in semver order, ignoring whether the version has a
293*9bb1b549SSpandan Das	// leading "v" or not. If there are no tags, find the commit at the tip of the
294*9bb1b549SSpandan Das	// default branch.
295*9bb1b549SSpandan Das	tags, err := gh.listTags(ctx, orgName, repoName)
296*9bb1b549SSpandan Das	if err != nil {
297*9bb1b549SSpandan Das		return err
298*9bb1b549SSpandan Das	}
299*9bb1b549SSpandan Das
300*9bb1b549SSpandan Das	vname := func(name string) string {
301*9bb1b549SSpandan Das		if !strings.HasPrefix(name, "v") {
302*9bb1b549SSpandan Das			return "v" + name
303*9bb1b549SSpandan Das		}
304*9bb1b549SSpandan Das		return name
305*9bb1b549SSpandan Das	}
306*9bb1b549SSpandan Das
307*9bb1b549SSpandan Das	w := 0
308*9bb1b549SSpandan Das	for r := range tags {
309*9bb1b549SSpandan Das		name := vname(*tags[r].Name)
310*9bb1b549SSpandan Das		if name != semver.Canonical(name) {
311*9bb1b549SSpandan Das			continue
312*9bb1b549SSpandan Das		}
313*9bb1b549SSpandan Das		tags[w] = tags[r]
314*9bb1b549SSpandan Das		w++
315*9bb1b549SSpandan Das	}
316*9bb1b549SSpandan Das	tags = tags[:w]
317*9bb1b549SSpandan Das
318*9bb1b549SSpandan Das	var highestTag *github.RepositoryTag
319*9bb1b549SSpandan Das	var highestVname string
320*9bb1b549SSpandan Das	for _, tag := range tags {
321*9bb1b549SSpandan Das		name := vname(*tag.Name)
322*9bb1b549SSpandan Das		if highestTag == nil || semver.Compare(name, highestVname) > 0 {
323*9bb1b549SSpandan Das			highestTag = tag
324*9bb1b549SSpandan Das			highestVname = name
325*9bb1b549SSpandan Das		}
326*9bb1b549SSpandan Das	}
327*9bb1b549SSpandan Das
328*9bb1b549SSpandan Das	var ghURL, stripPrefix, urlComment string
329*9bb1b549SSpandan Das	date := time.Now().Format("2006-01-02")
330*9bb1b549SSpandan Das	if highestTag != nil {
331*9bb1b549SSpandan Das		// If the tag is part of a release, check whether there is a release
332*9bb1b549SSpandan Das		// artifact we should use.
333*9bb1b549SSpandan Das		release, _, err := gh.Repositories.GetReleaseByTag(ctx, orgName, repoName, *highestTag.Name)
334*9bb1b549SSpandan Das		if err == nil {
335*9bb1b549SSpandan Das			wantNames := []string{
336*9bb1b549SSpandan Das				fmt.Sprintf("%s-%s.tar.gz", repoName, *highestTag.Name),
337*9bb1b549SSpandan Das				fmt.Sprintf("%s-%s.zip", repoName, *highestTag.Name),
338*9bb1b549SSpandan Das			}
339*9bb1b549SSpandan Das		AssetName:
340*9bb1b549SSpandan Das			for _, asset := range release.Assets {
341*9bb1b549SSpandan Das				for _, wantName := range wantNames {
342*9bb1b549SSpandan Das					if *asset.Name == wantName {
343*9bb1b549SSpandan Das						ghURL = asset.GetBrowserDownloadURL()
344*9bb1b549SSpandan Das						stripPrefix = "" // may not always be correct
345*9bb1b549SSpandan Das						break AssetName
346*9bb1b549SSpandan Das					}
347*9bb1b549SSpandan Das				}
348*9bb1b549SSpandan Das			}
349*9bb1b549SSpandan Das		}
350*9bb1b549SSpandan Das		if ghURL == "" {
351*9bb1b549SSpandan Das			ghURL = fmt.Sprintf("https://github.com/%s/%s/archive/refs/tags/%s.zip", orgName, repoName, *highestTag.Name)
352*9bb1b549SSpandan Das			stripPrefix = repoName + "-" + strings.TrimPrefix(*highestTag.Name, "v")
353*9bb1b549SSpandan Das		}
354*9bb1b549SSpandan Das		urlComment = fmt.Sprintf("%s, latest as of %s", *highestTag.Name, date)
355*9bb1b549SSpandan Das	} else {
356*9bb1b549SSpandan Das		repo, _, err := gh.Repositories.Get(ctx, orgName, repoName)
357*9bb1b549SSpandan Das		if err != nil {
358*9bb1b549SSpandan Das			return err
359*9bb1b549SSpandan Das		}
360*9bb1b549SSpandan Das		defaultBranchName := "main"
361*9bb1b549SSpandan Das		if repo.DefaultBranch != nil {
362*9bb1b549SSpandan Das			defaultBranchName = *repo.DefaultBranch
363*9bb1b549SSpandan Das		}
364*9bb1b549SSpandan Das		branch, _, err := gh.Repositories.GetBranch(ctx, orgName, repoName, defaultBranchName)
365*9bb1b549SSpandan Das		if err != nil {
366*9bb1b549SSpandan Das			return err
367*9bb1b549SSpandan Das		}
368*9bb1b549SSpandan Das		ghURL = fmt.Sprintf("https://github.com/%s/%s/archive/%s.zip", orgName, repoName, *branch.Commit.SHA)
369*9bb1b549SSpandan Das		stripPrefix = repoName + "-" + *branch.Commit.SHA
370*9bb1b549SSpandan Das		urlComment = fmt.Sprintf("%s, as of %s", defaultBranchName, date)
371*9bb1b549SSpandan Das	}
372*9bb1b549SSpandan Das	ghURLWithoutScheme := ghURL[len("https://"):]
373*9bb1b549SSpandan Das	mirrorURL := "https://mirror.bazel.build/" + ghURLWithoutScheme
374*9bb1b549SSpandan Das
375*9bb1b549SSpandan Das	// Download the archive and find the SHA.
376*9bb1b549SSpandan Das	archiveFile, err := os.CreateTemp("", "")
377*9bb1b549SSpandan Das	if err != nil {
378*9bb1b549SSpandan Das		return err
379*9bb1b549SSpandan Das	}
380*9bb1b549SSpandan Das	defer func() {
381*9bb1b549SSpandan Das		archiveFile.Close()
382*9bb1b549SSpandan Das		if rerr := os.Remove(archiveFile.Name()); err == nil && rerr != nil {
383*9bb1b549SSpandan Das			err = rerr
384*9bb1b549SSpandan Das		}
385*9bb1b549SSpandan Das	}()
386*9bb1b549SSpandan Das	resp, err := http.Get(ghURL)
387*9bb1b549SSpandan Das	if err != nil {
388*9bb1b549SSpandan Das		return err
389*9bb1b549SSpandan Das	}
390*9bb1b549SSpandan Das	hw := sha256.New()
391*9bb1b549SSpandan Das	mw := io.MultiWriter(hw, archiveFile)
392*9bb1b549SSpandan Das	if _, err := io.Copy(mw, resp.Body); err != nil {
393*9bb1b549SSpandan Das		resp.Body.Close()
394*9bb1b549SSpandan Das		return err
395*9bb1b549SSpandan Das	}
396*9bb1b549SSpandan Das	if err := resp.Body.Close(); err != nil {
397*9bb1b549SSpandan Das		return err
398*9bb1b549SSpandan Das	}
399*9bb1b549SSpandan Das	sha256Sum := hex.EncodeToString(hw.Sum(nil))
400*9bb1b549SSpandan Das	if _, err := archiveFile.Seek(0, io.SeekStart); err != nil {
401*9bb1b549SSpandan Das		return err
402*9bb1b549SSpandan Das	}
403*9bb1b549SSpandan Das
404*9bb1b549SSpandan Das	// Upload the archive to mirror.bazel.build.
405*9bb1b549SSpandan Das	if uploadToMirror {
406*9bb1b549SSpandan Das		if err := copyFileToMirror(ctx, ghURLWithoutScheme, archiveFile.Name()); err != nil {
407*9bb1b549SSpandan Das			return err
408*9bb1b549SSpandan Das		}
409*9bb1b549SSpandan Das	}
410*9bb1b549SSpandan Das
411*9bb1b549SSpandan Das	// If there are patches, re-apply or re-generate them.
412*9bb1b549SSpandan Das	// Patch labels may have "# releaser:patch-cmd name args..." directives
413*9bb1b549SSpandan Das	// that instruct this program to generate the patch by running a commnad
414*9bb1b549SSpandan Das	// in the directory. If there is no such directive, we apply the old patch
415*9bb1b549SSpandan Das	// using "patch". In either case, we'll generate a new patch with "diff".
416*9bb1b549SSpandan Das	// We'll scrub the timestamps to avoid excessive diffs in the PR that
417*9bb1b549SSpandan Das	// updates dependencies.
418*9bb1b549SSpandan Das	rootDir, err := repoRoot()
419*9bb1b549SSpandan Das	if err != nil {
420*9bb1b549SSpandan Das		return err
421*9bb1b549SSpandan Das	}
422*9bb1b549SSpandan Das	if attrs["patches"] != nil {
423*9bb1b549SSpandan Das		if err != nil {
424*9bb1b549SSpandan Das			return err
425*9bb1b549SSpandan Das		}
426*9bb1b549SSpandan Das		patchDir := filepath.Join(workDir, name, "a")
427*9bb1b549SSpandan Das		if err := extractArchive(archiveFile, path.Base(ghURL), patchDir, stripPrefix); err != nil {
428*9bb1b549SSpandan Das			return err
429*9bb1b549SSpandan Das		}
430*9bb1b549SSpandan Das
431*9bb1b549SSpandan Das		patchesList, ok := (*attrs["patches"]).(*bzl.ListExpr)
432*9bb1b549SSpandan Das		if !ok {
433*9bb1b549SSpandan Das			return fmt.Errorf("\"patches\" attribute is not a list")
434*9bb1b549SSpandan Das		}
435*9bb1b549SSpandan Das		for patchIndex, patchLabelExpr := range patchesList.List {
436*9bb1b549SSpandan Das			patchLabelValue, comments, err := parsePatchesItem(patchLabelExpr)
437*9bb1b549SSpandan Das			if err != nil {
438*9bb1b549SSpandan Das				return fmt.Errorf("parsing expr %#v : %w", patchLabelExpr, err)
439*9bb1b549SSpandan Das			}
440*9bb1b549SSpandan Das
441*9bb1b549SSpandan Das			if !strings.HasPrefix(patchLabelValue, "//third_party:") {
442*9bb1b549SSpandan Das				return fmt.Errorf("patch does not start with '//third_party:': %q", patchLabelValue)
443*9bb1b549SSpandan Das			}
444*9bb1b549SSpandan Das			patchName := patchLabelValue[len("//third_party:"):]
445*9bb1b549SSpandan Das			patchPath := filepath.Join(rootDir, "third_party", patchName)
446*9bb1b549SSpandan Das			prevDir := filepath.Join(workDir, name, string('a'+patchIndex))
447*9bb1b549SSpandan Das			patchDir := filepath.Join(workDir, name, string('a'+patchIndex+1))
448*9bb1b549SSpandan Das			var patchCmd []string
449*9bb1b549SSpandan Das			for _, c := range comments.Before {
450*9bb1b549SSpandan Das				words := strings.Fields(strings.TrimPrefix(c.Token, "#"))
451*9bb1b549SSpandan Das				if len(words) > 0 && words[0] == "releaser:patch-cmd" {
452*9bb1b549SSpandan Das					patchCmd = words[1:]
453*9bb1b549SSpandan Das					break
454*9bb1b549SSpandan Das				}
455*9bb1b549SSpandan Das			}
456*9bb1b549SSpandan Das
457*9bb1b549SSpandan Das			if err := copyDir(patchDir, prevDir); err != nil {
458*9bb1b549SSpandan Das				return err
459*9bb1b549SSpandan Das			}
460*9bb1b549SSpandan Das			if patchCmd == nil {
461*9bb1b549SSpandan Das				if err := runForError(ctx, patchDir, "patch", "-Np1", "-i", patchPath); err != nil {
462*9bb1b549SSpandan Das					return err
463*9bb1b549SSpandan Das				}
464*9bb1b549SSpandan Das			} else {
465*9bb1b549SSpandan Das				if err := runForError(ctx, patchDir, patchCmd[0], patchCmd[1:]...); err != nil {
466*9bb1b549SSpandan Das					return err
467*9bb1b549SSpandan Das				}
468*9bb1b549SSpandan Das			}
469*9bb1b549SSpandan Das			patch, _ := runForOutput(ctx, filepath.Join(workDir, name), "diff", "-urN", string('a'+patchIndex), string('a'+patchIndex+1))
470*9bb1b549SSpandan Das			patch = sanitizePatch(patch)
471*9bb1b549SSpandan Das			if err := os.WriteFile(patchPath, patch, 0666); err != nil {
472*9bb1b549SSpandan Das				return err
473*9bb1b549SSpandan Das			}
474*9bb1b549SSpandan Das		}
475*9bb1b549SSpandan Das	}
476*9bb1b549SSpandan Das
477*9bb1b549SSpandan Das	// Update the attributes.
478*9bb1b549SSpandan Das	*attrs["sha256"] = &bzl.StringExpr{Value: sha256Sum}
479*9bb1b549SSpandan Das	*attrs["strip_prefix"] = &bzl.StringExpr{Value: stripPrefix}
480*9bb1b549SSpandan Das	*attrs["urls"] = &bzl.ListExpr{
481*9bb1b549SSpandan Das		List: []bzl.Expr{
482*9bb1b549SSpandan Das			&bzl.StringExpr{Value: mirrorURL},
483*9bb1b549SSpandan Das			&bzl.StringExpr{Value: ghURL},
484*9bb1b549SSpandan Das		},
485*9bb1b549SSpandan Das		ForceMultiLine: true,
486*9bb1b549SSpandan Das	}
487*9bb1b549SSpandan Das	urlsKwarg.Before = []bzl.Comment{{Token: "# " + urlComment}}
488*9bb1b549SSpandan Das
489*9bb1b549SSpandan Das	return nil
490*9bb1b549SSpandan Das}
491*9bb1b549SSpandan Das
492*9bb1b549SSpandan Dasfunc parsePatchesItem(patchLabelExpr bzl.Expr) (value string, comments *bzl.Comments, err error) {
493*9bb1b549SSpandan Das	switch patchLabel := patchLabelExpr.(type) {
494*9bb1b549SSpandan Das	case *bzl.CallExpr:
495*9bb1b549SSpandan Das		// Verify the identifier, should be Label
496*9bb1b549SSpandan Das		if ident, ok := patchLabel.X.(*bzl.Ident); !ok {
497*9bb1b549SSpandan Das			return "", nil, fmt.Errorf("invalid identifier while parsing patch label")
498*9bb1b549SSpandan Das		} else if ident.Name != "Label" {
499*9bb1b549SSpandan Das			return "", nil, fmt.Errorf("invalid patch function: %q", ident.Name)
500*9bb1b549SSpandan Das		}
501*9bb1b549SSpandan Das
502*9bb1b549SSpandan Das		// Expect 1 String argument with the patch
503*9bb1b549SSpandan Das		if len(patchLabel.List) != 1 {
504*9bb1b549SSpandan Das			return "", nil, fmt.Errorf("Label expr should have 1 argument, found %d", len(patchLabel.List))
505*9bb1b549SSpandan Das		}
506*9bb1b549SSpandan Das
507*9bb1b549SSpandan Das		// Parse patch as a string
508*9bb1b549SSpandan Das		patchLabelStr, ok := patchLabel.List[0].(*bzl.StringExpr)
509*9bb1b549SSpandan Das		if !ok {
510*9bb1b549SSpandan Das			return "", nil, fmt.Errorf("Label expr does not contain a string literal")
511*9bb1b549SSpandan Das		}
512*9bb1b549SSpandan Das		return patchLabelStr.Value, patchLabel.Comment(), nil
513*9bb1b549SSpandan Das	case *bzl.StringExpr:
514*9bb1b549SSpandan Das		return strings.TrimPrefix(patchLabel.Value, "@io_bazel_rules_go"), patchLabel.Comment(), nil
515*9bb1b549SSpandan Das	default:
516*9bb1b549SSpandan Das		return "", nil, fmt.Errorf("not all patches are string literals or Label()")
517*9bb1b549SSpandan Das	}
518*9bb1b549SSpandan Das}
519*9bb1b549SSpandan Das
520*9bb1b549SSpandan Das// parseUpgradeDepDirective parses a '# releaser:upgrade-dep org repo' directive
521*9bb1b549SSpandan Das// and returns the organization and repository name or an error if the directive
522*9bb1b549SSpandan Das// was not found or malformed.
523*9bb1b549SSpandan Dasfunc parseUpgradeDepDirective(call *bzl.CallExpr) (orgName, repoName string, err error) {
524*9bb1b549SSpandan Das	// TODO: support other upgrade strategies. For example, support git_repository
525*9bb1b549SSpandan Das	// and go_repository (possibly wrapped in _maybe).
526*9bb1b549SSpandan Das	for _, c := range call.Comment().Before {
527*9bb1b549SSpandan Das		words := strings.Fields(strings.TrimPrefix(c.Token, "#"))
528*9bb1b549SSpandan Das		if len(words) == 0 || words[0] != "releaser:upgrade-dep" {
529*9bb1b549SSpandan Das			continue
530*9bb1b549SSpandan Das		}
531*9bb1b549SSpandan Das		if len(words) != 3 {
532*9bb1b549SSpandan Das			return "", "", errors.New("invalid upgrade-dep directive; expected org, and name fields")
533*9bb1b549SSpandan Das		}
534*9bb1b549SSpandan Das		return words[1], words[2], nil
535*9bb1b549SSpandan Das	}
536*9bb1b549SSpandan Das	return "", "", errors.New("releaser:upgrade-dep directive not found")
537*9bb1b549SSpandan Das}
538*9bb1b549SSpandan Das
539*9bb1b549SSpandan Das// sanitizePatch sets all of the non-zero patch dates to the same value. This
540*9bb1b549SSpandan Das// reduces churn in the PR that updates the patches.
541*9bb1b549SSpandan Das//
542*9bb1b549SSpandan Das// We avoid changing zero-valued patch dates, which are used in added or
543*9bb1b549SSpandan Das// deleted files. Since zero-valued dates can vary a bit by time zone, we assume
544*9bb1b549SSpandan Das// that any year starting with "19" is a zero-valeud date.
545*9bb1b549SSpandan Dasfunc sanitizePatch(patch []byte) []byte {
546*9bb1b549SSpandan Das	lines := bytes.Split(patch, []byte{'\n'})
547*9bb1b549SSpandan Das
548*9bb1b549SSpandan Das	for i, line := range lines {
549*9bb1b549SSpandan Das		if !bytes.HasPrefix(line, []byte("+++ ")) && !bytes.HasPrefix(line, []byte("--- ")) {
550*9bb1b549SSpandan Das			continue
551*9bb1b549SSpandan Das		}
552*9bb1b549SSpandan Das
553*9bb1b549SSpandan Das		tab := bytes.LastIndexByte(line, '\t')
554*9bb1b549SSpandan Das		if tab < 0 || bytes.HasPrefix(line[tab+1:], []byte("19")) {
555*9bb1b549SSpandan Das			continue
556*9bb1b549SSpandan Das		}
557*9bb1b549SSpandan Das
558*9bb1b549SSpandan Das		lines[i] = append(line[:tab+1], []byte("2000-01-01 00:00:00.000000000 -0000")...)
559*9bb1b549SSpandan Das	}
560*9bb1b549SSpandan Das	return bytes.Join(lines, []byte{'\n'})
561*9bb1b549SSpandan Das}
562