xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/releaser/prepare.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	"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