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