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