xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/releaser/prepare.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
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