| // Copyright 2017 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Releasebot manages the process of defining, |
| // packaging, and publishing Go releases. |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto/sha1" |
| "crypto/sha256" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime/debug" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "golang.org/x/build/buildenv" |
| "golang.org/x/build/internal/envutil" |
| "golang.org/x/build/internal/task" |
| "golang.org/x/build/maintner" |
| ) |
| |
| // A Target is a release target. |
| type Target struct { |
| // GoQuery is a Go version query specifying the Go versions the |
| // release target applies to. Empty string means all Go versions. |
| GoQuery string |
| |
| Name string // Target name as accepted by cmd/release. For example, "linux-amd64". |
| TestOnly bool // Run tests only; don't produce a release artifact. |
| } |
| |
| var releaseTargets = []Target{ |
| // Source-only target. |
| {Name: "src"}, |
| |
| // Binary targets. |
| {Name: "linux-386"}, |
| {Name: "linux-armv6l"}, |
| {Name: "linux-amd64"}, |
| {Name: "linux-arm64"}, |
| {Name: "freebsd-386"}, |
| {Name: "freebsd-amd64"}, |
| {Name: "windows-386"}, |
| {Name: "windows-amd64"}, |
| {Name: "windows-arm64", GoQuery: ">= go1.17beta1"}, |
| {Name: "darwin-amd64"}, |
| {Name: "darwin-arm64", GoQuery: ">= go1.16beta1"}, |
| {Name: "linux-s390x"}, |
| {Name: "linux-ppc64le"}, |
| |
| // Test-only targets. |
| {Name: "linux-386-longtest", TestOnly: true}, |
| {Name: "linux-amd64-longtest", TestOnly: true}, |
| {Name: "windows-amd64-longtest", TestOnly: true}, |
| } |
| |
| var releaseModes = map[string]bool{ |
| "prepare": true, |
| "release": true, |
| "mail-dl-cl": true, |
| } |
| |
| func usage() { |
| fmt.Fprintln(os.Stderr, "usage: releasebot -mode {prepare|release|mail-dl-cl} [-security] [-dry-run] {go1.8.5|go1.10beta2|go1.11rc1}") |
| flag.PrintDefaults() |
| os.Exit(2) |
| } |
| |
| var ( |
| skipTestFlag = flag.String("skip-test", "", "space-separated list of test-only targets to skip (only use if sufficient testing was done elsewhere)") |
| ) |
| |
| var ( |
| dryRun bool // only perform pre-flight checks, only log to terminal |
| skipTest = make(map[string]bool) // test-only targets that should be skipped |
| ) |
| |
| func main() { |
| modeFlag := flag.String("mode", "", "release mode (prepare, release)") |
| flag.BoolVar(&dryRun, "dry-run", false, "only perform pre-flight checks, only log to terminal") |
| security := flag.Bool("security", false, "cut a security release from the internal Gerrit") |
| flag.Usage = usage |
| flag.Parse() |
| if !releaseModes[*modeFlag] { |
| fmt.Fprintln(os.Stderr, "need to provide a valid mode") |
| usage() |
| } else if *modeFlag == "mail-dl-cl" { |
| mailDLCL() |
| return |
| } else if flag.NArg() != 1 { |
| fmt.Fprintln(os.Stderr, "need to provide a release name") |
| usage() |
| } |
| releaseVersion := flag.Arg(0) |
| for _, target := range strings.Fields(*skipTestFlag) { |
| if t, ok := releaseTarget(target, releaseVersion); !ok { |
| fmt.Fprintf(os.Stderr, "target %q in -skip-test=%q is not a known target\n", target, *skipTestFlag) |
| usage() |
| } else if !t.TestOnly { |
| fmt.Fprintf(os.Stderr, "%s is not a test-only target\n", target) |
| usage() |
| } |
| skipTest[target] = true |
| } |
| |
| http.DefaultTransport = newLogger(http.DefaultTransport) |
| |
| buildenv.CheckUserCredentials() |
| checkForGitCodereview() |
| loadMaintner() |
| loadGomoteUser() |
| loadGithubAuth() |
| loadGCSAuth() |
| |
| w := &Work{ |
| Prepare: *modeFlag == "prepare", |
| Version: releaseVersion, |
| BetaRelease: strings.Contains(releaseVersion, "beta"), |
| RCRelease: strings.Contains(releaseVersion, "rc"), |
| Security: *security, |
| } |
| |
| // Validate release version types. |
| if w.BetaRelease { |
| if w.Security { |
| log.Fatalf("%s is a beta version, it cannot be a security release", w.Version) |
| } |
| w.ReleaseBranch = "master" |
| } else if w.RCRelease { |
| if w.Security { |
| log.Fatalf("%s is a release candidate version, it cannot be a security release", w.Version) |
| } |
| shortRel := strings.Split(w.Version, "rc")[0] |
| w.ReleaseBranch = "release-branch." + shortRel |
| } else if strings.Count(w.Version, ".") == 1 { |
| // Major release like "go1.X". |
| if w.Security { |
| log.Fatalf("%s is a major version, it cannot be a security release", w.Version) |
| } |
| w.ReleaseBranch = "release-branch." + w.Version |
| } else if strings.Count(w.Version, ".") == 2 { |
| // Minor release or security release like "go1.X.Y". |
| shortRel := w.Version[:strings.LastIndex(w.Version, ".")] |
| w.ReleaseBranch = "release-branch." + shortRel |
| if w.Security { |
| w.ReleaseBranch += "-security" |
| } |
| } else { |
| log.Fatalf("cannot understand version %q", w.Version) |
| } |
| |
| // Select release targets for this Go version. |
| w.ReleaseTargets = matchTargets(w.Version) |
| |
| // Find milestones. |
| var err error |
| w.Milestone, err = getMilestone(w.Version) |
| if err != nil { |
| log.Fatalf("cannot find the GitHub milestone for release %s: %v", w.Version, err) |
| } |
| if !w.BetaRelease && !w.RCRelease { |
| nextV, err := nextVersion(w.Version) |
| if err != nil { |
| log.Fatalln("nextVersion:", err) |
| } |
| w.NextMilestone, err = getMilestone(nextV) |
| if err != nil { |
| log.Fatalf("cannot find %s, the next GitHub milestone after release %s: %v", nextV, w.Version, err) |
| } |
| } |
| // For major releases (go1.X), also check the "create first minor release milestone" |
| // step in the release process wasn't accidentally missed. See issue 44404. |
| if !w.BetaRelease && !w.RCRelease && strings.Count(w.Version, ".") == 1 { |
| firstMinor := w.Version + ".1" |
| if _, err := getMilestone(firstMinor); err != nil { |
| log.Fatalf("cannot find %s, the first minor release GitHub milestone after major release %s: %v", firstMinor, w.Version, err) |
| } |
| } |
| |
| w.doRelease() |
| } |
| |
| // mailDLCL parses command-line arguments for the mail-dl-cl mode, |
| // and runs it. |
| func mailDLCL() { |
| if flag.NArg() != 1 && flag.NArg() != 2 { |
| fmt.Fprintln(os.Stderr, "need to provide 1 or 2 versions") |
| usage() |
| } |
| versions := flag.Args() |
| |
| fmt.Printf("About to create a golang.org/dl CL for the following Go versions:\n\n\t• %s\n\nOk? (Y/n) ", strings.Join(versions, "\n\t• ")) |
| if dryRun { |
| fmt.Println("dry-run") |
| return |
| } |
| var response string |
| _, err := fmt.Scanln(&response) |
| if err != nil { |
| log.Fatalln(err) |
| } |
| if response != "Y" && response != "y" { |
| log.Fatalln("stopped as requested") |
| } |
| changeURL, err := task.MailDLCL(context.Background(), versions) |
| if err != nil { |
| log.Fatalf(`task.MailDLCL(ctx, %#v) failed: %v |
| |
| If it's neccessary to perform it manually as a workaround, |
| consider the following steps: |
| |
| git clone https://go.googlesource.com/dl && cd dl |
| go run ./internal/genv goX.Y.Z goX.A.B |
| git add . |
| git commit -m "dl: add goX.Y.Z and goX.A.B" |
| git codereview mail -trybot -trust |
| |
| Discuss with the secondary release coordinator as needed.`, versions, err) |
| } |
| fmt.Printf("\nPlease review and submit %s\nand then refer to the playbook for the next steps.\n\n", changeURL) |
| } |
| |
| // checkForGitCodereview exits the program if git-codereview is not installed |
| // in the user's path. |
| func checkForGitCodereview() { |
| cmd := exec.Command("which", "git-codereview") |
| if err := cmd.Run(); err != nil { |
| log.Fatal("could not find git-codereivew: ", cmd.Args, ": ", err, "\n\n"+ |
| "Please install it via go install golang.org/x/review/git-codereview@latest\n"+ |
| "to use this program.") |
| } |
| } |
| |
| var gomoteUser string |
| |
| func loadGomoteUser() { |
| tokenPath := filepath.Join(os.Getenv("HOME"), ".config/gomote") |
| files, _ := ioutil.ReadDir(tokenPath) |
| for _, file := range files { |
| if file.IsDir() { |
| continue |
| } |
| name := file.Name() |
| if strings.HasSuffix(name, ".token") && strings.HasPrefix(name, "user-") { |
| gomoteUser = strings.TrimPrefix(strings.TrimSuffix(name, ".token"), "user-") |
| return |
| } |
| } |
| log.Fatal("missing gomote token - cannot build releases.\n**FIX**: Download https://build-dot-golang-org.appspot.com/key?builder=user-YOURNAME\nand store in ~/.config/gomote/user-YOURNAME.token") |
| } |
| |
| // getMilestone returns the GitHub milestone corresponding to the specified version, |
| // or an error if it cannot be found. |
| func getMilestone(version string) (*maintner.GitHubMilestone, error) { |
| // Pre-release versions of Go share the same milestone as the |
| // release version, so trim the pre-release suffix, if any. |
| if i := strings.Index(version, "beta"); i != -1 { |
| version = version[:i] |
| } else if i := strings.Index(version, "rc"); i != -1 { |
| version = version[:i] |
| } |
| |
| var found *maintner.GitHubMilestone |
| goRepo.ForeachMilestone(func(m *maintner.GitHubMilestone) error { |
| if strings.ToLower(m.Title) != version { |
| return nil |
| } |
| found = m |
| return errors.New("stop iteration") |
| }) |
| if found == nil { |
| return nil, fmt.Errorf("no milestone found for version %q", version) |
| } |
| return found, nil |
| } |
| |
| func nextVersion(version string) (string, error) { |
| parts := strings.Split(version, ".") |
| n, err := strconv.Atoi(parts[len(parts)-1]) |
| if err != nil { |
| return "", err |
| } |
| parts[len(parts)-1] = strconv.Itoa(n + 1) |
| return strings.Join(parts, "."), nil |
| } |
| |
| // Work collects all the work state for managing a particular release. |
| // The intent is that the code could be used in a setting where one program |
| // is managing multiple releases, although the current releasebot command line |
| // only accepts a single release. |
| type Work struct { |
| logBuf *bytes.Buffer |
| log *log.Logger |
| |
| Prepare bool // create the release commit and submit it for review |
| BetaRelease bool |
| RCRelease bool |
| Security bool // cut a security release from the internal Gerrit |
| |
| ReleaseIssue int // Release status issue number |
| ReleaseBranch string // "master" for beta releases |
| Dir string // work directory ($HOME/go-releasebot-work/<release>) |
| StagingDir string // staging directory (a temporary directory inside <work>/release-staging) |
| Errors []string |
| ReleaseBinary string |
| ReleaseTargets []Target // Selected release targets for this release. |
| Version string |
| VersionCommit string |
| |
| releaseMu sync.Mutex |
| ReleaseInfo map[string]*ReleaseInfo // map and info protected by releaseMu |
| |
| Milestone *maintner.GitHubMilestone // Milestone for the current release. |
| // NextMilestone is the milestone of the next release of the same kind. |
| // For major releases, it's the milestone of the next major release (e.g., 1.14 → 1.15). |
| // For minor releases, it's the milestone of the next minor release (e.g., 1.14.1 → 1.14.2). |
| // For other release types, it's unset. |
| NextMilestone *maintner.GitHubMilestone |
| } |
| |
| // ReleaseInfo describes a release build for a specific target. |
| type ReleaseInfo struct { |
| Outputs []*ReleaseOutput |
| Msg string |
| } |
| |
| // ReleaseOutput describes a single release file. |
| type ReleaseOutput struct { |
| File string |
| Suffix string |
| Link string |
| Error string |
| } |
| |
| // logError records an error. |
| // The error is always shown in the "PROBLEMS WITH RELEASE" |
| // section at the top of the status page. |
| // If cl is not nil, the error is also shown in that CL's summary. |
| func (w *Work) logError(msg string, a ...interface{}) { |
| w.Errors = append(w.Errors, fmt.Sprintf(msg, a...)) |
| } |
| |
| // finally should be deferred at the top of each goroutine using a Work |
| // (as in "defer w.finally()"). It catches and logs panics and posts |
| // the log. |
| func (w *Work) finally() { |
| if err := recover(); err != nil { |
| w.log.Printf("\n\nPANIC: %v\n\n%s", err, debug.Stack()) |
| } |
| w.postSummary() |
| } |
| |
| type runner struct { |
| w *Work |
| dir string |
| extraEnv []string |
| } |
| |
| func (w *Work) runner(dir string, env ...string) *runner { |
| return &runner{ |
| w: w, |
| dir: dir, |
| extraEnv: env, |
| } |
| } |
| |
| // run runs the command and requires that it succeeds. |
| // If not, it logs the failure and aborts the work. |
| // It logs the command line. |
| func (r *runner) run(args ...string) { |
| out, err := r.runErr(args...) |
| if err != nil { |
| r.w.log.Printf("command failed: %s\n%s", err, out) |
| panic("command failed") |
| } |
| } |
| |
| // runOut runs the command, requires that it succeeds, |
| // and returns the command's output. |
| // It does not log the command line except in case of failure. |
| // Not logging these commands avoids filling the log with |
| // runs of side-effect-free commands like "git cat-file commit HEAD". |
| func (r *runner) runOut(args ...string) []byte { |
| cmd := exec.Command(args[0], args[1:]...) |
| envutil.SetDir(cmd, r.dir) |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| r.w.log.Printf("$ %s\n", strings.Join(args, " ")) |
| r.w.log.Printf("command failed: %s\n%s", err, out) |
| panic("command failed") |
| } |
| return out |
| } |
| |
| // runErr runs the given command and returns the output and status (error). |
| // It logs the command line. |
| func (r *runner) runErr(args ...string) ([]byte, error) { |
| r.w.log.Printf("$ %s\n", strings.Join(args, " ")) |
| cmd := exec.Command(args[0], args[1:]...) |
| envutil.SetDir(cmd, r.dir) |
| envutil.SetEnv(cmd, r.extraEnv...) |
| return cmd.CombinedOutput() |
| } |
| |
| func (w *Work) doRelease() { |
| w.logBuf = new(bytes.Buffer) |
| w.log = log.New(io.MultiWriter(os.Stdout, w.logBuf), "", log.LstdFlags) |
| defer w.finally() |
| |
| w.log.Printf("starting") |
| |
| w.checkSpelling() |
| w.gitCheckout() |
| |
| if !w.Security { |
| w.mustIncludeSecurityBranch() |
| } |
| |
| // In release mode we carry on even if the tag exists, in case we |
| // need to resume a failed build. |
| if w.Prepare && w.gitTagExists() { |
| w.logError("%s tag already exists in Go repository!", w.Version) |
| w.logError("**Found errors during release. Stopping!**") |
| return |
| } |
| if w.BetaRelease || w.RCRelease { |
| // TODO: go tool api -allow_new=false |
| if strings.HasSuffix(w.Version, "beta1") { |
| w.checkBeta1ReleaseBlockers() |
| } |
| } else { |
| if !w.Security { |
| w.checkReleaseBlockers() |
| } |
| } |
| w.findOrCreateReleaseIssue() |
| if len(w.Errors) > 0 && !dryRun { |
| w.logError("**Found errors during release. Stopping!**") |
| return |
| } |
| |
| if w.Prepare { |
| var changeID string |
| if !w.BetaRelease { |
| changeID = w.writeVersion() |
| } |
| |
| // Create release archives and run all.bash tests on the builders. |
| w.VersionCommit = w.gitHeadCommit() |
| w.buildReleases() |
| if len(w.Errors) > 0 { |
| w.logError("**Found errors during release. Stopping!**") |
| return |
| } |
| |
| if w.BetaRelease { |
| w.nextStepsBeta() |
| } else { |
| w.nextStepsPrepare(changeID) |
| } |
| } else { |
| if !w.BetaRelease { |
| w.checkVersion() |
| } |
| if len(w.Errors) > 0 { |
| w.logError("**Found errors during release. Stopping!**") |
| return |
| } |
| |
| // Create and push the Git tag for the release, then create or reuse release archives. |
| // (Tests are skipped here since they ran during the prepare mode.) |
| w.gitTagVersion() |
| w.buildReleases() |
| if len(w.Errors) > 0 { |
| w.logError("**Found errors during release. Stopping!**") |
| return |
| } |
| |
| switch { |
| case !w.BetaRelease && !w.RCRelease: |
| w.pushIssues() |
| w.closeMilestone() |
| case w.BetaRelease && strings.HasSuffix(w.Version, "beta1"): |
| w.removeOkayAfterBeta1() |
| } |
| w.nextStepsRelease() |
| } |
| } |
| |
| func (w *Work) checkSpelling() { |
| if w.Version != strings.ToLower(w.Version) { |
| w.logError("release name should be lowercase: %q", w.Version) |
| } |
| if strings.Contains(w.Version, " ") { |
| w.logError("release name should not contain any spaces: %q", w.Version) |
| } |
| if !strings.HasPrefix(w.Version, "go") { |
| w.logError("release name should have 'go' prefix: %q", w.Version) |
| } |
| } |
| |
| func (w *Work) checkReleaseBlockers() { |
| if err := goRepo.ForeachIssue(func(gi *maintner.GitHubIssue) error { |
| if gi.Milestone == nil || gi.Milestone.ID != w.Milestone.ID { |
| return nil |
| } |
| if !gi.Closed && gi.HasLabel("release-blocker") { |
| w.logError("open issue #%d is tagged release-blocker", gi.Number) |
| } |
| return nil |
| }); err != nil { |
| w.logError("error checking release-blockers: %v", err.Error()) |
| return |
| } |
| } |
| |
| func (w *Work) checkBeta1ReleaseBlockers() { |
| if err := goRepo.ForeachIssue(func(gi *maintner.GitHubIssue) error { |
| if gi.Milestone == nil || gi.Milestone.ID != w.Milestone.ID { |
| return nil |
| } |
| if !gi.Closed && gi.HasLabel("release-blocker") && !gi.HasLabel("okay-after-beta1") { |
| w.logError("open issue #%d is tagged release-blocker and not okay after beta1", gi.Number) |
| } |
| return nil |
| }); err != nil { |
| w.logError("error checking release-blockers: %v", err.Error()) |
| return |
| } |
| } |
| |
| func (w *Work) nextStepsPrepare(changeID string) { |
| if w.Security { |
| w.log.Printf(` |
| |
| The prepare stage has completed. |
| |
| Please review and submit https://team-review.git.corp.google.com/q/%s |
| and then run the release stage. |
| |
| `, changeID) |
| return |
| } |
| |
| w.log.Printf(` |
| |
| The prepare stage has completed. |
| |
| Please review and submit https://go-review.googlesource.com/q/%s |
| and then run the release stage. |
| |
| `, changeID) |
| } |
| |
| func (w *Work) nextStepsBeta() { |
| w.log.Printf(` |
| |
| The prepare stage has completed. |
| |
| Please run the release stage next. |
| |
| `) |
| } |
| |
| func (w *Work) nextStepsRelease() { |
| w.log.Printf(` |
| |
| The release stage has completed. Thanks for riding with releasebot today! |
| |
| Please refer to the playbook for the next steps. |
| |
| `) |
| } |
| |
| func (w *Work) postSummary() { |
| var md bytes.Buffer |
| |
| if len(w.Errors) > 0 { |
| fmt.Fprintf(&md, "## PROBLEMS WITH RELEASE\n\n") |
| for _, e := range w.Errors { |
| fmt.Fprintf(&md, " - ") |
| fmt.Fprintf(&md, "%s\n", strings.Replace(strings.TrimRight(e, "\n"), "\n", "\n ", -1)) |
| } |
| } |
| |
| if !w.Prepare { |
| fmt.Fprintf(&md, "\n## Latest build: %s\n\n", mdEscape(w.Version)) |
| w.printReleaseTable(&md) |
| } |
| |
| fmt.Fprintf(&md, "\n## Log\n\n ") |
| md.WriteString(strings.Replace(w.logBuf.String(), "\n", "\n ", -1)) |
| fmt.Fprintf(&md, "\n\n") |
| |
| if len(w.Errors) > 0 { |
| fmt.Fprintf(&md, "There were problems with the release, see above for details.\n") |
| } |
| |
| body := md.String() |
| fmt.Printf("%s", body) |
| // Avoid the risk of leaking sensitive test failures on security releases. |
| if dryRun || w.Security { |
| return |
| } |
| |
| // Ensure that the entire body can be posted to the issue by splitting it into multiple |
| // GitHub comments if necessary. See golang.org/issue/45998. |
| bodyParts := splitLogMessage(body, githubCommentCharacterLimit) |
| for _, b := range bodyParts { |
| err := postGithubComment(w.ReleaseIssue, b) |
| if err != nil { |
| fmt.Printf("error posting update comment: %v\n", err) |
| } |
| } |
| } |
| |
| func (w *Work) printReleaseTable(md *bytes.Buffer) { |
| // TODO: print sha256 |
| w.releaseMu.Lock() |
| defer w.releaseMu.Unlock() |
| for _, target := range w.ReleaseTargets { |
| fmt.Fprintf(md, "- %s", mdEscape(target.Name)) |
| if target.TestOnly { |
| fmt.Fprintf(md, " (test only)") |
| } |
| info := w.ReleaseInfo[target.Name] |
| if info == nil { |
| fmt.Fprintf(md, " - not started\n") |
| continue |
| } |
| if len(info.Outputs) == 0 { |
| fmt.Fprintf(md, " - not built") |
| } else if target.TestOnly && len(info.Outputs) == 1 && info.Outputs[0].Suffix == "test-only" { |
| fmt.Fprintf(md, " - ok") |
| } |
| for _, out := range info.Outputs { |
| if out.Suffix == "test-only" { |
| continue |
| } else if out.Link != "" { |
| fmt.Fprintf(md, " ([%s](%s))", mdEscape(out.Suffix), out.Link) |
| } else { |
| fmt.Fprintf(md, " (~~%s~~)", mdEscape(out.Suffix)) |
| } |
| } |
| fmt.Fprintf(md, "\n") |
| if info.Msg != "" { |
| fmt.Fprintf(md, " - %s\n", strings.Replace(strings.TrimRight(info.Msg, "\n"), "\n", "\n ", -1)) |
| } |
| } |
| } |
| |
| func (w *Work) writeVersion() (changeID string) { |
| changeID = fmt.Sprintf("I%x", sha1.Sum([]byte(fmt.Sprintf("cmd/release-version-%s", w.Version)))) |
| |
| err := ioutil.WriteFile(filepath.Join(w.Dir, "gitwork", "VERSION"), []byte(w.Version), 0666) |
| if err != nil { |
| w.log.Panic(err) |
| } |
| |
| desc := w.Version + "\n\n" |
| desc += "Change-Id: " + changeID + "\n" |
| |
| r := w.runner(filepath.Join(w.Dir, "gitwork")) |
| r.run("git", "add", "VERSION") |
| r.run("git", "commit", "-m", desc, "VERSION") |
| if dryRun { |
| fmt.Printf("\n### VERSION commit\n\n%s\n", r.runOut("git", "show", "HEAD")) |
| } else if w.Security { |
| r.run("git", "codereview", "mail") |
| } else { |
| r.run("git", "codereview", "mail", "-trybot", "-trust") |
| } |
| return |
| } |
| |
| // checkVersion makes sure that the version commit has been submitted. |
| func (w *Work) checkVersion() { |
| ver, err := ioutil.ReadFile(filepath.Join(w.Dir, "gitwork", "VERSION")) |
| if err != nil { |
| w.log.Panic(err) |
| } |
| if string(ver) != w.Version { |
| w.logError("VERSION is %q; want %q. Did you run prepare and submit the CL?", string(ver), w.Version) |
| } |
| } |
| |
| func (w *Work) buildReleaseBinary() { |
| gopath := filepath.Join(w.Dir, "gopath") |
| r := w.runner(w.Dir, "GOPATH="+gopath, "GOBIN="+filepath.Join(gopath, "bin")) |
| r.run("go", "clean", "-modcache") |
| if err := os.RemoveAll(gopath); err != nil { |
| w.log.Panic(err) |
| } |
| if err := os.MkdirAll(gopath, 0777); err != nil { |
| w.log.Panic(err) |
| } |
| r.run("go", "install", "golang.org/x/build/cmd/release@latest") |
| w.ReleaseBinary = filepath.Join(gopath, "bin/release") |
| } |
| |
| func (w *Work) buildReleases() { |
| w.buildReleaseBinary() |
| if err := os.MkdirAll(filepath.Join(w.Dir, "release", w.VersionCommit), 0777); err != nil { |
| w.log.Panic(err) |
| } |
| if err := os.MkdirAll(filepath.Join(w.Dir, "release-staging"), 0777); err != nil { |
| w.log.Panic(err) |
| } |
| stagingDir, err := ioutil.TempDir(filepath.Join(w.Dir, "release-staging"), w.VersionCommit+"_") |
| if err != nil { |
| w.log.Panic(err) |
| } |
| w.StagingDir = stagingDir |
| w.ReleaseInfo = make(map[string]*ReleaseInfo) |
| |
| if w.Security { |
| fmt.Printf(` |
| |
| Please download |
| |
| https://team.git.corp.google.com/golang/go-private/+archive/%s.tar.gz |
| |
| to %s and press enter. |
| `, w.VersionCommit, filepath.Join(w.Dir, w.VersionCommit+".tar.gz")) |
| |
| _, err := fmt.Scanln() |
| if err != nil { |
| w.log.Panic(err) |
| } |
| } |
| |
| var wg sync.WaitGroup |
| for _, target := range w.ReleaseTargets { |
| w.releaseMu.Lock() |
| w.ReleaseInfo[target.Name] = new(ReleaseInfo) |
| w.releaseMu.Unlock() |
| |
| if target.TestOnly && skipTest[target.Name] { |
| w.log.Printf("skipping test-only target %s because of -skip-test=%q flag", target.Name, *skipTestFlag) |
| w.releaseMu.Lock() |
| w.ReleaseInfo[target.Name].Msg = fmt.Sprintf("skipped because of -skip-test=%q flag", *skipTestFlag) |
| w.releaseMu.Unlock() |
| continue |
| } |
| |
| wg.Add(1) |
| target := target |
| go func() { |
| defer wg.Done() |
| defer func() { |
| if err := recover(); err != nil { |
| stk := strings.TrimSpace(string(debug.Stack())) |
| msg := fmt.Sprintf("PANIC: %v\n\n %s\n", mdEscape(fmt.Sprint(err)), strings.Replace(stk, "\n", "\n ", -1)) |
| w.logError(msg) |
| w.log.Printf("\n\nBuilding %s: PANIC: %v\n\n%s", target.Name, err, debug.Stack()) |
| w.releaseMu.Lock() |
| w.ReleaseInfo[target.Name].Msg = msg |
| w.releaseMu.Unlock() |
| } |
| }() |
| w.buildRelease(target) |
| }() |
| } |
| wg.Wait() |
| |
| // Check for release errors and stop if any. |
| w.releaseMu.Lock() |
| for _, target := range w.ReleaseTargets { |
| for _, out := range w.ReleaseInfo[target.Name].Outputs { |
| if out.Error != "" || len(w.Errors) > 0 { |
| w.logError("RELEASE BUILD FAILED\n") |
| w.releaseMu.Unlock() |
| return |
| } |
| } |
| } |
| w.releaseMu.Unlock() |
| } |
| |
| // buildRelease builds the release packaging for a given target. Because the |
| // "release" program can be flaky, it tries multiple times before stopping. |
| // The release files are first written to a staging directory specified in w.StagingDir |
| // (a temporary directory inside $HOME/go-releasebot-work/go1.2.3/release-staging), |
| // then after the all.bash tests complete successfully (or get skipped), |
| // they get moved to the final release directory |
| // ($HOME/go-releasebot-work/go1.2.3/release/COMMIT_HASH). |
| // |
| // If files for the current version commit are already present in the release directory, |
| // they are reused instead of being rebuilt. In release mode, buildRelease then uploads |
| // the release packaging to the gs://golang-release-staging bucket, along with files |
| // containing the SHA256 hash of the releases, for eventual use by the download page. |
| func (w *Work) buildRelease(target Target) { |
| log.Printf("BUILDRELEASE %s %s\n", w.Version, target.Name) |
| defer log.Printf("DONE BUILDRELEASE %s %s\n", w.Version, target.Name) |
| releaseDir := filepath.Join(w.Dir, "release", w.VersionCommit) |
| prefix := fmt.Sprintf("%s.%s.", w.Version, target.Name) |
| var files []string |
| switch { |
| case target.TestOnly: |
| files = []string{prefix + "test-only"} |
| case strings.HasPrefix(target.Name, "windows-"): |
| files = []string{prefix + "zip", prefix + "msi"} |
| default: |
| files = []string{prefix + "tar.gz"} |
| } |
| var outs []*ReleaseOutput |
| haveFiles := true |
| for _, file := range files { |
| out := &ReleaseOutput{ |
| File: file, |
| Suffix: strings.TrimPrefix(file, prefix), |
| } |
| outs = append(outs, out) |
| _, err := os.Stat(filepath.Join(releaseDir, file)) |
| if err != nil { |
| haveFiles = false |
| } |
| } |
| w.releaseMu.Lock() |
| w.ReleaseInfo[target.Name].Outputs = outs |
| w.releaseMu.Unlock() |
| |
| if haveFiles { |
| w.log.Printf("release -target=%q: already have %v; not rebuilding files", target.Name, files) |
| } else { |
| failures := 0 |
| for { |
| args := []string{w.ReleaseBinary, "-target", target.Name, "-user", gomoteUser, |
| "-version", w.Version, "-staging_dir", w.StagingDir} |
| if w.Security { |
| args = append(args, "-tarball", filepath.Join(w.Dir, w.VersionCommit+".tar.gz")) |
| } else { |
| args = append(args, "-rev", w.VersionCommit) |
| } |
| // The prepare step will run the tests on a commit that has the same |
| // tree (but maybe different message) as the one that the release |
| // step will process, so we can skip tests the second time. |
| if !w.Prepare { |
| args = append(args, "-skip_tests") |
| } |
| releaseOutput, releaseError := w.runner(releaseDir, "GOPATH="+filepath.Join(w.Dir, "gopath")).runErr(args...) |
| // Exit code from release binary is apparently unreliable. |
| // Look to see if the files we expected were created instead. |
| failed := false |
| w.releaseMu.Lock() |
| for _, out := range outs { |
| if _, err := os.Stat(filepath.Join(releaseDir, out.File)); err != nil { |
| failed = true |
| } |
| } |
| w.releaseMu.Unlock() |
| if !failed { |
| w.log.Printf("release -target=%q: build succeeded (after %d retries)\n", target.Name, failures) |
| break |
| } |
| w.log.Printf("release -target=%q did not produce expected output files %v:\nerror from cmd/release binary = %v\noutput from cmd/release binary:\n%s", target.Name, files, releaseError, releaseOutput) |
| if failures++; failures >= 3 { |
| w.log.Printf("release -target=%q: too many failed attempts, stopping\n", target.Name) |
| for _, out := range outs { |
| w.releaseMu.Lock() |
| out.Error = fmt.Sprintf("release -target=%q: build failed", target.Name) |
| w.releaseMu.Unlock() |
| } |
| return |
| } |
| w.log.Printf("release -target=%q: waiting a bit and trying again\n", target.Name) |
| time.Sleep(1 * time.Minute) |
| } |
| } |
| |
| if dryRun || w.Prepare { |
| return |
| } |
| |
| if target.TestOnly { |
| // This was a test-only target, nothing to upload. |
| return |
| } |
| |
| for _, out := range outs { |
| if err := w.uploadStagingRelease(target, out); err != nil { |
| w.log.Printf("error uploading release %s to staging bucket: %s", target.Name, err) |
| w.releaseMu.Lock() |
| out.Error = err.Error() |
| w.releaseMu.Unlock() |
| } |
| } |
| } |
| |
| // uploadStagingRelease uploads target to the release staging bucket. |
| // If successful, it records the corresponding URL in out.Link. |
| // In addition to uploading target, it creates and uploads a file |
| // named "<target>.sha256" containing the hex sha256 hash |
| // of the target file. This is needed for the release signing process |
| // and also displayed on the eventual download page. |
| func (w *Work) uploadStagingRelease(target Target, out *ReleaseOutput) error { |
| if dryRun { |
| return errors.New("attempted write operation in dry-run mode") |
| } else if target.TestOnly { |
| return errors.New("attempted to upload a test-only target") |
| } |
| |
| src := filepath.Join(w.Dir, "release", w.VersionCommit, out.File) |
| h := sha256.New() |
| f, err := os.Open(src) |
| if err != nil { |
| return err |
| } |
| _, err = io.Copy(h, f) |
| f.Close() |
| if err != nil { |
| return err |
| } |
| if err := ioutil.WriteFile(src+".sha256", []byte(fmt.Sprintf("%x", h.Sum(nil))), 0666); err != nil { |
| return err |
| } |
| |
| dst := w.Version + "/" + out.File |
| if err := gcsUpload(src, dst); err != nil { |
| return err |
| } |
| if err := gcsUpload(src+".sha256", dst+".sha256"); err != nil { |
| return err |
| } |
| |
| w.releaseMu.Lock() |
| out.Link = "https://" + releaseBucket + ".storage.googleapis.com/" + dst |
| w.releaseMu.Unlock() |
| return nil |
| } |
| |
| // mustIncludeSecurityBranch remotely checks if there is an associated release branch |
| // for the current release. If one exists, it ensures that the HEAD commit in the latest |
| // security release branch exists within the current release branch. If the latest security |
| // branch has changes which have not been merged into the proposed release, it will exit |
| // fatally. If an asssociated security release branch does not exist, the function will |
| // return without doing the check. It assumes that if the security branch doesn't exist, |
| // it's because it was already merged everywhere and deleted. |
| func (w *Work) mustIncludeSecurityBranch() { |
| securityReleaseBranch := fmt.Sprintf("%s-security", w.ReleaseBranch) |
| |
| sha, ok := w.gitRemoteBranchCommit(privateGoRepoURL, securityReleaseBranch) |
| if !ok { |
| w.log.Printf("an associated security release branch %q does not exist; assuming it has been merged and deleted, so proceeding as usual", securityReleaseBranch) |
| return |
| } |
| |
| if !w.gitCommitExistsInBranch(sha) { |
| log.Fatalf("release branch does not contain security release HEAD commit %q; aborting", sha) |
| } |
| } |
| |
| // releaseTarget returns a release target with the specified name |
| // for the specified Go version. |
| func releaseTarget(name, goVer string) (_ Target, ok bool) { |
| for _, t := range releaseTargets { |
| if !match(t.GoQuery, goVer) { |
| continue |
| } |
| if t.Name == name { |
| return t, true |
| } |
| } |
| return Target{}, false |
| } |
| |
| // matchTargets selects release targets that have a matching |
| // GoQuery value for the specified Go version. |
| func matchTargets(goVer string) (matched []Target) { |
| for _, t := range releaseTargets { |
| if !match(t.GoQuery, goVer) { |
| continue |
| } |
| matched = append(matched, t) |
| } |
| return matched |
| } |
| |
| // match reports whether the Go version goVer matches the provided version query. |
| // The empty query matches all Go versions. |
| // match panics if given a query that it doesn't support. |
| func match(query, goVer string) bool { |
| // TODO(golang.org/issue/40558): This should help inform the API for a Go version parser. |
| switch query { |
| case "": // A special case to make the zero Target.GoQuery value useful. |
| return true |
| case ">= go1.17beta1": |
| return !strings.HasPrefix(goVer, "go1.16") && !strings.HasPrefix(goVer, "go1.15") |
| case ">= go1.16beta1": |
| return !strings.HasPrefix(goVer, "go1.15") |
| default: |
| panic(fmt.Errorf("match: query %q is not supported", query)) |
| } |
| } |
| |
| // splitLogMessage splits a string into n number of strings of maximum size maxStrLen. |
| // It naively attempts to split the string along the bounderies of new line characters in order |
| // to make each individual string as readable as possible. |
| func splitLogMessage(s string, maxStrLen int) []string { |
| sl := []string{} |
| for len(s) > maxStrLen { |
| end := strings.LastIndex(s[:maxStrLen], "\n") |
| if end == -1 { |
| end = maxStrLen |
| } |
| sl = append(sl, s[:end]) |
| |
| if string(s[end]) == "\n" { |
| s = s[end+1:] |
| } else { |
| s = s[end:] |
| } |
| } |
| sl = append(sl, s) |
| return sl |
| } |