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 "errors" 21*9bb1b549SSpandan Das "flag" 22*9bb1b549SSpandan Das "fmt" 23*9bb1b549SSpandan Das "io" 24*9bb1b549SSpandan Das "os" 25*9bb1b549SSpandan Das 26*9bb1b549SSpandan Das "github.com/google/go-github/v36/github" 27*9bb1b549SSpandan Das "golang.org/x/mod/semver" 28*9bb1b549SSpandan Das "golang.org/x/oauth2" 29*9bb1b549SSpandan Das) 30*9bb1b549SSpandan Das 31*9bb1b549SSpandan Dasvar prepareCmd = command{ 32*9bb1b549SSpandan Das name: "prepare", 33*9bb1b549SSpandan Das description: "prepares a GitHub release with notes and attached archive", 34*9bb1b549SSpandan Das help: `prepare -rnotes=file -version=version -githubtoken=token [-mirror] 35*9bb1b549SSpandan Das 36*9bb1b549SSpandan Das'prepare' performs most tasks related to a rules_go release. It does everything 37*9bb1b549SSpandan Dasexcept publishing and tagging the release, which must be done manually, 38*9bb1b549SSpandan Daswith review. Specifically, prepare does the following: 39*9bb1b549SSpandan Das 40*9bb1b549SSpandan Das* Creates the release branch if it doesn't exist locally. Release branches 41*9bb1b549SSpandan Das have names like "release-X.Y" where X and Y are the major and minor version 42*9bb1b549SSpandan Das numbers. 43*9bb1b549SSpandan Das* Checks that RULES_GO_VERSION is set in go/def.bzl on the local release branch 44*9bb1b549SSpandan Das for the minor version being released. RULES_GO_VERSION must be a sematic 45*9bb1b549SSpandan Das version without the "v" prefix that Go uses, like "1.2.4". It must match 46*9bb1b549SSpandan Das the -version flag, which does require the "v" prefix. 47*9bb1b549SSpandan Das* Creates an archive zip file from the tip of the local release branch. 48*9bb1b549SSpandan Das* Creates or updates a draft GitHub release with the given release notes. 49*9bb1b549SSpandan Das http_archive boilerplate is generated and appended to the release notes. 50*9bb1b549SSpandan Das* Uploads and attaches the release archive to the GitHub release. 51*9bb1b549SSpandan Das* Uploads the release archive to mirror.bazel.build. If the file already exists, 52*9bb1b549SSpandan Das it may be manually removed with 'gsutil rm gs://bazel-mirror/<github-url>' 53*9bb1b549SSpandan Das or manually updated with 'gsutil cp <file> gs://bazel-mirror/<github-url>'. 54*9bb1b549SSpandan Das This step may be skipped by setting -mirror=false. 55*9bb1b549SSpandan Das 56*9bb1b549SSpandan DasAfter these steps are completed successfully, 'prepare' prompts the user to 57*9bb1b549SSpandan Dascheck that CI passes, then review and publish the release. 58*9bb1b549SSpandan Das 59*9bb1b549SSpandan DasNote that 'prepare' does not update boilerplate in WORKSPACE or README.rst for 60*9bb1b549SSpandan Daseither rules_go or Gazelle. 61*9bb1b549SSpandan Das`, 62*9bb1b549SSpandan Das} 63*9bb1b549SSpandan Das 64*9bb1b549SSpandan Dasfunc init() { 65*9bb1b549SSpandan Das // break init cycle 66*9bb1b549SSpandan Das prepareCmd.run = runPrepare 67*9bb1b549SSpandan Das} 68*9bb1b549SSpandan Das 69*9bb1b549SSpandan Dasfunc runPrepare(ctx context.Context, stderr io.Writer, args []string) error { 70*9bb1b549SSpandan Das // Parse arguments. 71*9bb1b549SSpandan Das flags := flag.NewFlagSet("releaser prepare", flag.ContinueOnError) 72*9bb1b549SSpandan Das var rnotesPath, version string 73*9bb1b549SSpandan Das var githubToken githubTokenFlag 74*9bb1b549SSpandan Das var uploadToMirror bool 75*9bb1b549SSpandan Das flags.Var(&githubToken, "githubtoken", "GitHub personal access token or path to a file containing it") 76*9bb1b549SSpandan Das flags.BoolVar(&uploadToMirror, "mirror", true, "whether to upload dependency archives to mirror.bazel.build") 77*9bb1b549SSpandan Das flags.StringVar(&rnotesPath, "rnotes", "", "Name of file containing release notes in Markdown") 78*9bb1b549SSpandan Das flags.StringVar(&version, "version", "", "Version to release") 79*9bb1b549SSpandan Das if err := flags.Parse(args); err != nil { 80*9bb1b549SSpandan Das return err 81*9bb1b549SSpandan Das } 82*9bb1b549SSpandan Das if flags.NArg() > 0 { 83*9bb1b549SSpandan Das return usageErrorf(&prepareCmd, "No arguments expected") 84*9bb1b549SSpandan Das } 85*9bb1b549SSpandan Das if githubToken == "" { 86*9bb1b549SSpandan Das return usageErrorf(&prepareCmd, "-githubtoken must be set") 87*9bb1b549SSpandan Das } 88*9bb1b549SSpandan Das if rnotesPath == "" { 89*9bb1b549SSpandan Das return usageErrorf(&prepareCmd, "-rnotes must be set") 90*9bb1b549SSpandan Das } 91*9bb1b549SSpandan Das if version == "" { 92*9bb1b549SSpandan Das return usageErrorf(&prepareCmd, "-version must be set") 93*9bb1b549SSpandan Das } 94*9bb1b549SSpandan Das if semver.Canonical(version) != version || semver.Prerelease(version) != "" || semver.Build(version) != "" { 95*9bb1b549SSpandan Das return usageErrorf(&prepareCmd, "-version must be a canonical version, like v1.2.3") 96*9bb1b549SSpandan Das } 97*9bb1b549SSpandan Das 98*9bb1b549SSpandan Das ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: string(githubToken)}) 99*9bb1b549SSpandan Das tc := oauth2.NewClient(ctx, ts) 100*9bb1b549SSpandan Das gh := &githubClient{Client: github.NewClient(tc)} 101*9bb1b549SSpandan Das 102*9bb1b549SSpandan Das // Get the GitHub release. 103*9bb1b549SSpandan Das fmt.Fprintf(stderr, "checking if release %s exists...\n", version) 104*9bb1b549SSpandan Das release, err := gh.getReleaseByTagIncludingDraft(ctx, "bazelbuild", "rules_go", version) 105*9bb1b549SSpandan Das if err != nil && !errors.Is(err, errReleaseNotFound) { 106*9bb1b549SSpandan Das return err 107*9bb1b549SSpandan Das } 108*9bb1b549SSpandan Das if release != nil && !release.GetDraft() { 109*9bb1b549SSpandan Das return fmt.Errorf("release %s was already published", version) 110*9bb1b549SSpandan Das } 111*9bb1b549SSpandan Das 112*9bb1b549SSpandan Das // Check that RULES_GO_VERSION is set correctly on the release branch. 113*9bb1b549SSpandan Das // If this is a minor release (x.y.0), create the release branch if it 114*9bb1b549SSpandan Das // does not exist. 115*9bb1b549SSpandan Das fmt.Fprintf(stderr, "checking RULES_GO_VERSION...\n") 116*9bb1b549SSpandan Das rootDir, err := repoRoot() 117*9bb1b549SSpandan Das if err != nil { 118*9bb1b549SSpandan Das return err 119*9bb1b549SSpandan Das } 120*9bb1b549SSpandan Das if err := checkNoGitChanges(ctx, rootDir); err != nil { 121*9bb1b549SSpandan Das return err 122*9bb1b549SSpandan Das } 123*9bb1b549SSpandan Das majorMinor := semver.MajorMinor(version) 124*9bb1b549SSpandan Das isMinorRelease := semver.Canonical(majorMinor) == version 125*9bb1b549SSpandan Das branchName := "release-" + majorMinor[len("v"):] 126*9bb1b549SSpandan Das if !gitBranchExists(ctx, rootDir, branchName) { 127*9bb1b549SSpandan Das if !isMinorRelease { 128*9bb1b549SSpandan Das return fmt.Errorf("release branch %q does not exist locally. Fetch it, set RULES_GO_VERSION, add commits, and run this command again.") 129*9bb1b549SSpandan Das } 130*9bb1b549SSpandan Das if err := checkRulesGoVersion(ctx, rootDir, "HEAD", version); err != nil { 131*9bb1b549SSpandan Das return err 132*9bb1b549SSpandan Das } 133*9bb1b549SSpandan Das fmt.Fprintf(stderr, "creating branch %s...\n", branchName) 134*9bb1b549SSpandan Das if err := gitCreateBranch(ctx, rootDir, branchName, "HEAD"); err != nil { 135*9bb1b549SSpandan Das return err 136*9bb1b549SSpandan Das } 137*9bb1b549SSpandan Das } else { 138*9bb1b549SSpandan Das if err := checkRulesGoVersion(ctx, rootDir, branchName, version); err != nil { 139*9bb1b549SSpandan Das return err 140*9bb1b549SSpandan Das } 141*9bb1b549SSpandan Das } 142*9bb1b549SSpandan Das 143*9bb1b549SSpandan Das // Create an archive. 144*9bb1b549SSpandan Das fmt.Fprintf(stderr, "creating archive...\n") 145*9bb1b549SSpandan Das arcFile, err := os.CreateTemp("", "rules_go-%s-*.zip") 146*9bb1b549SSpandan Das if err != nil { 147*9bb1b549SSpandan Das return err 148*9bb1b549SSpandan Das } 149*9bb1b549SSpandan Das arcName := arcFile.Name() 150*9bb1b549SSpandan Das arcFile.Close() 151*9bb1b549SSpandan Das defer func() { 152*9bb1b549SSpandan Das if rerr := os.Remove(arcName); err == nil && rerr != nil { 153*9bb1b549SSpandan Das err = rerr 154*9bb1b549SSpandan Das } 155*9bb1b549SSpandan Das }() 156*9bb1b549SSpandan Das if err := gitCreateArchive(ctx, rootDir, branchName, arcName); err != nil { 157*9bb1b549SSpandan Das return err 158*9bb1b549SSpandan Das } 159*9bb1b549SSpandan Das arcSum, err := sha256SumFile(arcName) 160*9bb1b549SSpandan Das if err != nil { 161*9bb1b549SSpandan Das return err 162*9bb1b549SSpandan Das } 163*9bb1b549SSpandan Das 164*9bb1b549SSpandan Das // Read release notes, append boilerplate. 165*9bb1b549SSpandan Das rnotesData, err := os.ReadFile(rnotesPath) 166*9bb1b549SSpandan Das if err != nil { 167*9bb1b549SSpandan Das return err 168*9bb1b549SSpandan Das } 169*9bb1b549SSpandan Das rnotesData = bytes.TrimSpace(rnotesData) 170*9bb1b549SSpandan Das goVersion, err := findLatestGoVersion() 171*9bb1b549SSpandan Das if err != nil { 172*9bb1b549SSpandan Das return err 173*9bb1b549SSpandan Das } 174*9bb1b549SSpandan Das boilerplate := genBoilerplate(version, arcSum, goVersion) 175*9bb1b549SSpandan Das rnotesStr := string(rnotesData) + "\n\n## `WORKSPACE` code\n\n```\n" + boilerplate + "\n```\n" 176*9bb1b549SSpandan Das 177*9bb1b549SSpandan Das // Push the release branch. 178*9bb1b549SSpandan Das fmt.Fprintf(stderr, "pushing branch %s to origin...\n", branchName) 179*9bb1b549SSpandan Das if err := gitPushBranch(ctx, rootDir, branchName); err != nil { 180*9bb1b549SSpandan Das return err 181*9bb1b549SSpandan Das } 182*9bb1b549SSpandan Das 183*9bb1b549SSpandan Das // Upload to mirror.bazel.build. 184*9bb1b549SSpandan Das arcGHURLWithoutScheme := fmt.Sprintf("github.com/bazelbuild/rules_go/releases/download/%[1]s/rules_go-%[1]s.zip", version) 185*9bb1b549SSpandan Das if uploadToMirror { 186*9bb1b549SSpandan Das fmt.Fprintf(stderr, "uploading archive to mirror.bazel.build...\n") 187*9bb1b549SSpandan Das if err := copyFileToMirror(ctx, arcGHURLWithoutScheme, arcName); err != nil { 188*9bb1b549SSpandan Das return err 189*9bb1b549SSpandan Das } 190*9bb1b549SSpandan Das } 191*9bb1b549SSpandan Das 192*9bb1b549SSpandan Das // Create or update the GitHub release. 193*9bb1b549SSpandan Das if release == nil { 194*9bb1b549SSpandan Das fmt.Fprintf(stderr, "creating draft release...\n") 195*9bb1b549SSpandan Das draft := true 196*9bb1b549SSpandan Das release = &github.RepositoryRelease{ 197*9bb1b549SSpandan Das TagName: &version, 198*9bb1b549SSpandan Das TargetCommitish: &branchName, 199*9bb1b549SSpandan Das Name: &version, 200*9bb1b549SSpandan Das Body: &rnotesStr, 201*9bb1b549SSpandan Das Draft: &draft, 202*9bb1b549SSpandan Das } 203*9bb1b549SSpandan Das if release, _, err = gh.Repositories.CreateRelease(ctx, "bazelbuild", "rules_go", release); err != nil { 204*9bb1b549SSpandan Das return err 205*9bb1b549SSpandan Das } 206*9bb1b549SSpandan Das } else { 207*9bb1b549SSpandan Das fmt.Fprintf(stderr, "updating release...\n") 208*9bb1b549SSpandan Das release.Body = &rnotesStr 209*9bb1b549SSpandan Das if release, _, err = gh.Repositories.EditRelease(ctx, "bazelbuild", "rules_go", release.GetID(), release); err != nil { 210*9bb1b549SSpandan Das return err 211*9bb1b549SSpandan Das } 212*9bb1b549SSpandan Das for _, asset := range release.Assets { 213*9bb1b549SSpandan Das if _, err := gh.Repositories.DeleteReleaseAsset(ctx, "bazelbuild", "rules_go", asset.GetID()); err != nil { 214*9bb1b549SSpandan Das return err 215*9bb1b549SSpandan Das } 216*9bb1b549SSpandan Das } 217*9bb1b549SSpandan Das } 218*9bb1b549SSpandan Das arcFile, err = os.Open(arcName) 219*9bb1b549SSpandan Das if err != nil { 220*9bb1b549SSpandan Das return err 221*9bb1b549SSpandan Das } 222*9bb1b549SSpandan Das defer arcFile.Close() 223*9bb1b549SSpandan Das uploadOpts := &github.UploadOptions{ 224*9bb1b549SSpandan Das Name: "rules_go-" + version + ".zip", 225*9bb1b549SSpandan Das MediaType: "application/zip", 226*9bb1b549SSpandan Das } 227*9bb1b549SSpandan Das if _, _, err := gh.Repositories.UploadReleaseAsset(ctx, "bazelbuild", "rules_go", release.GetID(), uploadOpts, arcFile); err != nil { 228*9bb1b549SSpandan Das return err 229*9bb1b549SSpandan Das } 230*9bb1b549SSpandan Das 231*9bb1b549SSpandan Das testURL := fmt.Sprintf("https://buildkite.com/bazel/rules-go-golang/builds?branch=%s", branchName) 232*9bb1b549SSpandan Das fmt.Fprintf(stderr, ` 233*9bb1b549SSpandan DasRelease %s has been prepared and uploaded. 234*9bb1b549SSpandan Das 235*9bb1b549SSpandan Das* Ensure that all tests pass in CI at %s. 236*9bb1b549SSpandan Das* Review and publish the release at %s. 237*9bb1b549SSpandan Das* Update README.rst and WORKSPACE if necessary. 238*9bb1b549SSpandan Das`, version, testURL, release.GetHTMLURL()) 239*9bb1b549SSpandan Das 240*9bb1b549SSpandan Das return nil 241*9bb1b549SSpandan Das} 242*9bb1b549SSpandan Das 243*9bb1b549SSpandan Dasfunc checkRulesGoVersion(ctx context.Context, dir, refName, version string) error { 244*9bb1b549SSpandan Das data, err := gitCatFile(ctx, dir, refName, "go/def.bzl") 245*9bb1b549SSpandan Das if err != nil { 246*9bb1b549SSpandan Das return err 247*9bb1b549SSpandan Das } 248*9bb1b549SSpandan Das rulesGoVersionStr := []byte(fmt.Sprintf(`RULES_GO_VERSION = "%s"`, version[len("v"):])) 249*9bb1b549SSpandan Das if !bytes.Contains(data, rulesGoVersionStr) { 250*9bb1b549SSpandan Das return fmt.Errorf("RULES_GO_VERSION was not set to %q in go/def.bzl. Set it, add commits, and run this command again.") 251*9bb1b549SSpandan Das } 252*9bb1b549SSpandan Das return nil 253*9bb1b549SSpandan Das} 254