2017-10-20 19:06:12 +03:00
|
|
|
// 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.
|
|
|
|
|
2018-08-14 00:29:36 +03:00
|
|
|
// Releasebot manages the process of defining, packaging, and publishing Go
|
|
|
|
// releases. It is a work in progress; right now it only handles beta, rc and
|
|
|
|
// minor (point) releases, but eventually we want it to handle major releases too.
|
2017-10-20 19:06:12 +03:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/sha1"
|
|
|
|
"crypto/sha256"
|
2018-06-06 18:39:58 +03:00
|
|
|
"errors"
|
2017-10-20 19:06:12 +03:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"runtime/debug"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2018-08-21 01:49:40 +03:00
|
|
|
"golang.org/x/build/buildenv"
|
2018-06-06 18:39:58 +03:00
|
|
|
"golang.org/x/build/maintner"
|
2017-10-20 19:06:12 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var releaseTargets = []string{
|
|
|
|
"src",
|
|
|
|
"linux-386",
|
|
|
|
"linux-armv6l",
|
|
|
|
"linux-amd64",
|
|
|
|
"linux-arm64",
|
|
|
|
"freebsd-386",
|
|
|
|
"freebsd-amd64",
|
|
|
|
"windows-386",
|
|
|
|
"windows-amd64",
|
|
|
|
"darwin-amd64",
|
|
|
|
"linux-s390x",
|
|
|
|
"linux-ppc64le",
|
|
|
|
}
|
|
|
|
|
2017-12-11 23:49:42 +03:00
|
|
|
var releaseModes = map[string]bool{
|
2018-06-06 18:39:58 +03:00
|
|
|
"prepare": true,
|
|
|
|
"release": true,
|
2017-12-11 23:49:42 +03:00
|
|
|
}
|
|
|
|
|
2017-10-20 19:06:12 +03:00
|
|
|
func usage() {
|
2018-12-12 22:37:02 +03:00
|
|
|
fmt.Fprintln(os.Stderr, "usage: releasebot -mode {prepare|release} [-security] [-dry-run] {go1.8.5|go1.10beta2|go1.11rc1}")
|
2017-10-20 19:06:12 +03:00
|
|
|
os.Exit(2)
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
var dryRun bool // only perform pre-flight checks, only log to terminal
|
|
|
|
|
2017-10-20 19:06:12 +03:00
|
|
|
func main() {
|
2018-06-06 18:39:58 +03:00
|
|
|
modeFlag := flag.String("mode", "", "release mode (prepare, release)")
|
|
|
|
flag.BoolVar(&dryRun, "dry-run", false, "only perform pre-flight checks, only log to terminal")
|
2018-12-12 22:37:02 +03:00
|
|
|
security := flag.Bool("security", false, "cut a security release from the internal Gerrit")
|
2017-10-20 19:06:12 +03:00
|
|
|
flag.Usage = usage
|
|
|
|
flag.Parse()
|
2018-12-15 00:53:07 +03:00
|
|
|
if *modeFlag == "" || !releaseModes[*modeFlag] || flag.NArg() != 1 {
|
2017-10-20 19:06:12 +03:00
|
|
|
usage()
|
|
|
|
}
|
|
|
|
|
|
|
|
http.DefaultTransport = newLogger(http.DefaultTransport)
|
|
|
|
|
2018-08-21 01:49:40 +03:00
|
|
|
buildenv.CheckUserCredentials()
|
2017-12-11 23:49:42 +03:00
|
|
|
checkForGitCodereview()
|
2018-06-06 18:39:58 +03:00
|
|
|
loadMaintner()
|
|
|
|
loadGomoteUser()
|
2017-10-20 19:06:12 +03:00
|
|
|
loadGithubAuth()
|
|
|
|
loadGCSAuth()
|
|
|
|
|
2018-12-15 00:53:07 +03:00
|
|
|
release := flag.Arg(0)
|
|
|
|
|
|
|
|
if strings.Contains(release, "beta") || strings.Contains(release, "rc") {
|
|
|
|
if *security {
|
|
|
|
log.Printf("error: only minor releases are supported in security mode")
|
|
|
|
usage()
|
|
|
|
}
|
|
|
|
w := &Work{
|
|
|
|
Prepare: *modeFlag == "prepare",
|
|
|
|
Version: release,
|
|
|
|
BetaRelease: strings.Contains(release, "beta"),
|
|
|
|
RCRelease: strings.Contains(release, "rc"),
|
2018-06-18 23:41:24 +03:00
|
|
|
}
|
2018-12-15 00:53:07 +03:00
|
|
|
w.doRelease()
|
|
|
|
return
|
|
|
|
}
|
2018-06-18 23:41:24 +03:00
|
|
|
|
2018-12-15 00:53:07 +03:00
|
|
|
errFoundMilestone := errors.New("found milestone")
|
|
|
|
err := goRepo.ForeachMilestone(func(m *maintner.GitHubMilestone) error {
|
|
|
|
if strings.ToLower(m.Title) == release {
|
|
|
|
nextM, err := nextMilestone(m)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
2018-12-15 00:53:07 +03:00
|
|
|
w := &Work{
|
|
|
|
Milestone: m,
|
|
|
|
NextMilestone: nextM,
|
|
|
|
Prepare: *modeFlag == "prepare",
|
|
|
|
Version: release,
|
|
|
|
Security: *security,
|
|
|
|
}
|
|
|
|
w.doRelease()
|
|
|
|
return errFoundMilestone
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
2018-12-15 00:53:07 +03:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil && err != errFoundMilestone {
|
|
|
|
log.Fatalf("error looking for release %s: %v", release, err)
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
log.Fatalf("cannot find release %s", release)
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
func nextMilestone(m *maintner.GitHubMilestone) (*maintner.GitHubMilestone, error) {
|
|
|
|
titleParts := strings.Split(m.Title, ".")
|
|
|
|
n, err := strconv.Atoi(titleParts[len(titleParts)-1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
titleParts[len(titleParts)-1] = strconv.Itoa(n + 1)
|
|
|
|
newTitle := strings.Join(titleParts, ".")
|
|
|
|
var res *maintner.GitHubMilestone
|
|
|
|
err = goRepo.ForeachMilestone(func(m *maintner.GitHubMilestone) error {
|
|
|
|
if m.Title == newTitle {
|
|
|
|
res = m
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if res == nil {
|
|
|
|
return res, fmt.Errorf("no next milestone found with title %q", newTitle)
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2017-12-11 23:49:42 +03:00
|
|
|
// checkForGitCodereview exits the program if git-codereview is not installed
|
|
|
|
// in the user's path.
|
|
|
|
func checkForGitCodereview() {
|
2018-01-11 21:07:24 +03:00
|
|
|
cmd := exec.Command("which", "git-codereview")
|
2017-12-11 23:49:42 +03:00
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
log.Fatal("could not find git-codereivew: ", cmd.Args, ": ", err, "\n\n"+
|
|
|
|
"Please install it via go get golang.org/x/review/git-codereview\n"+
|
|
|
|
"to use this program.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2017-10-20 19:06:12 +03:00
|
|
|
// 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 {
|
2018-06-18 23:21:54 +03:00
|
|
|
logBuf *bytes.Buffer
|
|
|
|
log *log.Logger
|
2017-10-20 19:06:12 +03:00
|
|
|
|
2018-06-18 23:41:24 +03:00
|
|
|
Prepare bool // create the release commit and submit it for review
|
|
|
|
BetaRelease bool
|
2018-08-14 00:29:36 +03:00
|
|
|
RCRelease bool
|
2018-12-12 22:37:02 +03:00
|
|
|
Security bool // cut a security release from the internal Gerrit
|
2017-10-20 19:06:12 +03:00
|
|
|
|
2018-06-18 23:41:24 +03:00
|
|
|
ReleaseIssue int // Release status issue number
|
|
|
|
ReleaseBranch string // "master" for beta releases
|
2018-06-18 23:21:54 +03:00
|
|
|
Dir string // work directory ($HOME/go-releasebot-work/<release>)
|
2019-08-22 17:30:51 +03:00
|
|
|
StagingDir string // staging directory (a temporary directory inside <work>/release-staging)
|
2018-06-06 18:39:58 +03:00
|
|
|
Errors []string
|
2017-10-20 19:06:12 +03:00
|
|
|
ReleaseBinary string
|
|
|
|
Version string
|
|
|
|
VersionCommit string
|
|
|
|
|
|
|
|
releaseMu sync.Mutex
|
|
|
|
ReleaseInfo map[string]*ReleaseInfo // map and info protected by releaseMu
|
2018-06-18 23:41:24 +03:00
|
|
|
|
|
|
|
// Properties set for minor releases only.
|
|
|
|
Milestone *maintner.GitHubMilestone
|
|
|
|
NextMilestone *maintner.GitHubMilestone // Next minor milestone
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2018-06-06 18:39:58 +03:00
|
|
|
func (w *Work) logError(msg string, a ...interface{}) {
|
|
|
|
w.Errors = append(w.Errors, fmt.Sprintf(msg, a...))
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
// 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() {
|
2017-10-20 19:06:12 +03:00
|
|
|
if err := recover(); err != nil {
|
|
|
|
w.log.Printf("\n\nPANIC: %v\n\n%s", err, debug.Stack())
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
w.postSummary()
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
2018-06-18 23:21:54 +03:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-20 19:06:12 +03:00
|
|
|
// run runs the command and requires that it succeeds.
|
|
|
|
// If not, it logs the failure and aborts the work.
|
|
|
|
// It logs the command line.
|
2018-06-18 23:21:54 +03:00
|
|
|
func (r *runner) run(args ...string) {
|
|
|
|
out, err := r.runErr(args...)
|
2017-10-20 19:06:12 +03:00
|
|
|
if err != nil {
|
2018-06-18 23:21:54 +03:00
|
|
|
r.w.log.Printf("command failed: %s\n%s", err, out)
|
|
|
|
panic("command failed")
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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".
|
2018-06-18 23:21:54 +03:00
|
|
|
func (r *runner) runOut(args ...string) []byte {
|
2017-10-20 19:06:12 +03:00
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
2018-06-18 23:21:54 +03:00
|
|
|
cmd.Dir = r.dir
|
2017-10-20 19:06:12 +03:00
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
2018-06-18 23:21:54 +03:00
|
|
|
r.w.log.Printf("$ %s\n", strings.Join(args, " "))
|
|
|
|
r.w.log.Printf("command failed: %s\n%s", err, out)
|
|
|
|
panic("command failed")
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
// runErr runs the given command and returns the output and status (error).
|
|
|
|
// It logs the command line.
|
2018-06-18 23:21:54 +03:00
|
|
|
func (r *runner) runErr(args ...string) ([]byte, error) {
|
|
|
|
r.w.log.Printf("$ %s\n", strings.Join(args, " "))
|
2017-10-20 19:06:12 +03:00
|
|
|
cmd := exec.Command(args[0], args[1:]...)
|
2018-06-18 23:21:54 +03:00
|
|
|
cmd.Dir = r.dir
|
|
|
|
if len(r.extraEnv) > 0 {
|
|
|
|
cmd.Env = append(os.Environ(), r.extraEnv...)
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
2018-06-18 23:21:54 +03:00
|
|
|
return cmd.CombinedOutput()
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Work) doRelease() {
|
|
|
|
w.logBuf = new(bytes.Buffer)
|
|
|
|
w.log = log.New(io.MultiWriter(os.Stdout, w.logBuf), "", log.LstdFlags)
|
2018-06-06 18:39:58 +03:00
|
|
|
defer w.finally()
|
2017-10-20 19:06:12 +03:00
|
|
|
|
|
|
|
w.log.Printf("starting")
|
|
|
|
|
2018-06-18 23:41:24 +03:00
|
|
|
if w.BetaRelease {
|
|
|
|
w.ReleaseBranch = "master"
|
2018-08-14 00:29:36 +03:00
|
|
|
} else if w.RCRelease {
|
|
|
|
shortRel := strings.Split(w.Version, "rc")[0]
|
|
|
|
w.ReleaseBranch = "release-branch." + shortRel
|
2019-09-03 18:20:20 +03:00
|
|
|
} else if strings.Count(w.Version, ".") == 1 {
|
|
|
|
// Major release like "go1.X".
|
|
|
|
if w.Security {
|
|
|
|
// TODO(dmitshur): move this error check to happen earlier
|
|
|
|
w.logError("%s is a major version, it cannot be a security release.", w.Version)
|
|
|
|
w.logError("**Found errors during release. Stopping!**")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.ReleaseBranch = "release-branch." + w.Version
|
|
|
|
} else if strings.Count(w.Version, ".") == 2 {
|
|
|
|
// Minor release or security release like "go1.X.Y".
|
2018-08-14 00:29:36 +03:00
|
|
|
shortRel := w.Version[:strings.LastIndex(w.Version, ".")]
|
2018-06-18 23:41:24 +03:00
|
|
|
w.ReleaseBranch = "release-branch." + shortRel
|
2018-12-12 22:37:02 +03:00
|
|
|
if w.Security {
|
|
|
|
w.ReleaseBranch += "-security"
|
|
|
|
}
|
2019-09-03 18:20:20 +03:00
|
|
|
} else {
|
|
|
|
// TODO(dmitshur): move this error check to happen earlier
|
|
|
|
w.logError("Cannot understand version %q.", w.Version)
|
|
|
|
w.logError("**Found errors during release. Stopping!**")
|
|
|
|
return
|
2018-06-18 23:41:24 +03:00
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
w.checkSpelling()
|
2017-12-11 23:49:42 +03:00
|
|
|
w.gitCheckout()
|
2018-06-25 22:54:24 +03:00
|
|
|
// 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
|
|
|
|
}
|
2018-08-14 00:29:36 +03:00
|
|
|
if w.BetaRelease || w.RCRelease {
|
|
|
|
// TODO: go tool api -allow_new=false
|
2018-06-25 22:54:24 +03:00
|
|
|
} else {
|
2018-12-12 22:37:02 +03:00
|
|
|
if !w.Security {
|
|
|
|
w.checkReleaseBlockers()
|
|
|
|
}
|
2018-06-18 23:41:24 +03:00
|
|
|
w.checkDocs()
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
w.findOrCreateReleaseIssue()
|
|
|
|
if len(w.Errors) > 0 && !dryRun {
|
|
|
|
w.logError("**Found errors during release. Stopping!**")
|
2017-12-11 23:49:42 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-06-25 22:54:24 +03:00
|
|
|
if w.Prepare {
|
2018-12-15 00:55:42 +03:00
|
|
|
var changeID string
|
|
|
|
if !w.BetaRelease {
|
|
|
|
changeID = w.writeVersion()
|
|
|
|
}
|
|
|
|
|
2019-08-21 23:51:48 +03:00
|
|
|
// Create release archives and run all.bash tests on the builders.
|
2018-12-15 00:55:42 +03:00
|
|
|
w.VersionCommit = w.gitHeadCommit()
|
|
|
|
w.buildReleases()
|
2019-08-21 23:51:48 +03:00
|
|
|
if len(w.Errors) > 0 {
|
|
|
|
w.logError("**Found errors during release. Stopping!**")
|
|
|
|
return
|
|
|
|
}
|
2018-12-15 00:55:42 +03:00
|
|
|
|
2018-06-25 22:54:24 +03:00
|
|
|
if w.BetaRelease {
|
|
|
|
w.nextStepsBeta()
|
|
|
|
} else {
|
|
|
|
w.nextStepsPrepare(changeID)
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
} else {
|
2018-06-18 23:41:24 +03:00
|
|
|
if !w.BetaRelease {
|
|
|
|
w.checkVersion()
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
if len(w.Errors) > 0 {
|
|
|
|
w.logError("**Found errors during release. Stopping!**")
|
2017-10-20 19:06:12 +03:00
|
|
|
return
|
|
|
|
}
|
2019-08-21 23:51:48 +03:00
|
|
|
|
|
|
|
// 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.)
|
2018-06-06 18:39:58 +03:00
|
|
|
w.gitTagVersion()
|
|
|
|
w.buildReleases()
|
2019-08-21 23:51:48 +03:00
|
|
|
if len(w.Errors) > 0 {
|
|
|
|
w.logError("**Found errors during release. Stopping!**")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-08-14 00:29:36 +03:00
|
|
|
if !w.BetaRelease && !w.RCRelease {
|
2018-06-18 23:41:24 +03:00
|
|
|
w.pushIssues()
|
|
|
|
w.closeMilestone()
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
w.nextStepsRelease()
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
}
|
2017-10-20 19:06:12 +03:00
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
func (w *Work) checkSpelling() {
|
|
|
|
if w.Version != strings.ToLower(w.Version) {
|
|
|
|
w.logError("release name should be lowercase: %q", w.Version)
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
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)
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
func (w *Work) checkReleaseBlockers() {
|
|
|
|
if err := goRepo.ForeachIssue(func(gi *maintner.GitHubIssue) error {
|
|
|
|
if gi.Milestone == nil || gi.Milestone.Title != w.Milestone.Title {
|
|
|
|
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())
|
2017-12-11 23:49:42 +03:00
|
|
|
return
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Work) nextStepsPrepare(changeID string) {
|
2018-12-12 22:37:02 +03:00
|
|
|
if w.Security {
|
|
|
|
w.log.Printf(`
|
|
|
|
|
|
|
|
The release is ready.
|
|
|
|
|
|
|
|
Please review and submit https://team-review.git.corp.google.com/q/%s
|
2018-12-15 00:55:42 +03:00
|
|
|
and then run the release stage.
|
2018-12-12 22:37:02 +03:00
|
|
|
|
|
|
|
`, changeID)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
w.log.Printf(`
|
|
|
|
|
|
|
|
The release is ready.
|
|
|
|
|
|
|
|
Please review and submit https://go-review.googlesource.com/q/%s
|
2018-12-15 00:55:42 +03:00
|
|
|
and then run the release stage.
|
2018-06-06 18:39:58 +03:00
|
|
|
|
|
|
|
`, changeID)
|
|
|
|
}
|
|
|
|
|
2018-06-18 23:41:24 +03:00
|
|
|
func (w *Work) nextStepsBeta() {
|
|
|
|
w.log.Printf(`
|
|
|
|
|
|
|
|
The release is ready. Run with mode=release to execute it.
|
|
|
|
|
|
|
|
`)
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
func (w *Work) nextStepsRelease() {
|
|
|
|
w.log.Printf(`
|
2017-12-11 23:49:42 +03:00
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
The release run is complete! Refer to the playbook for the next steps.
|
2017-10-20 19:06:12 +03:00
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
Thanks for riding with releasebot today.
|
|
|
|
|
|
|
|
`)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Work) postSummary() {
|
2017-10-20 19:06:12 +03:00
|
|
|
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, " - ")
|
2018-06-06 18:39:58 +03:00
|
|
|
fmt.Fprintf(&md, "%s\n", strings.Replace(strings.TrimRight(e, "\n"), "\n", "\n ", -1))
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
if !w.Prepare {
|
|
|
|
fmt.Fprintf(&md, "\n## Latest build: %s\n\n", mdEscape(w.Version))
|
2017-10-20 19:06:12 +03:00
|
|
|
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")
|
|
|
|
|
2019-08-21 23:51:48 +03:00
|
|
|
if len(w.Errors) > 0 {
|
|
|
|
fmt.Fprintf(&md, "There were problems with the release, see above for details.\n")
|
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
body := md.String()
|
|
|
|
fmt.Printf("%s", body)
|
2018-12-12 22:37:02 +03:00
|
|
|
// Avoid the risk of leaking sensitive test failures on security releases.
|
|
|
|
if dryRun || w.Security {
|
2018-06-06 18:39:58 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
err := postGithubComment(w.ReleaseIssue, body)
|
2017-10-20 19:06:12 +03:00
|
|
|
if err != nil {
|
2018-06-06 18:39:58 +03:00
|
|
|
fmt.Printf("error posting update comment: %v\n", err)
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Work) printReleaseTable(md *bytes.Buffer) {
|
2018-06-06 18:39:58 +03:00
|
|
|
// TODO: print sha256
|
2017-10-20 19:06:12 +03:00
|
|
|
w.releaseMu.Lock()
|
|
|
|
defer w.releaseMu.Unlock()
|
|
|
|
for _, target := range releaseTargets {
|
|
|
|
fmt.Fprintf(md, "%s", mdEscape(target))
|
|
|
|
info := w.ReleaseInfo[target]
|
|
|
|
if info == nil {
|
|
|
|
fmt.Fprintf(md, " not started\n")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, out := range info.Outputs {
|
|
|
|
if out.Link == "" {
|
|
|
|
fmt.Fprintf(md, " (~~%s~~)", mdEscape(out.Suffix))
|
|
|
|
} else {
|
|
|
|
fmt.Fprintf(md, " ([%s](%s))", mdEscape(out.Suffix), out.Link)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(info.Outputs) == 0 {
|
|
|
|
fmt.Fprintf(md, " not built")
|
|
|
|
}
|
|
|
|
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) checkDocs() {
|
2019-09-03 18:20:20 +03:00
|
|
|
// Check that the major version is listed on the project page.
|
|
|
|
data, err := ioutil.ReadFile(filepath.Join(w.Dir, "gitwork", "doc/contrib.html"))
|
|
|
|
if err != nil {
|
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
|
|
|
major := major(w.Version)
|
|
|
|
if !strings.Contains(string(data), major) {
|
|
|
|
w.logError("doc/contrib.html does not list major version %s", major)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that the release is listed on the release history page.
|
|
|
|
data, err = ioutil.ReadFile(filepath.Join(w.Dir, "gitwork", "doc/devel/release.html"))
|
2017-10-20 19:06:12 +03:00
|
|
|
if err != nil {
|
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
2019-09-03 19:40:34 +03:00
|
|
|
if !strings.Contains(string(data), w.Version+" (released ") {
|
2019-09-03 18:20:20 +03:00
|
|
|
w.logError("doc/devel/release.html does not document %s", w.Version)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// major takes a go version like "go1.5", "go1.5.1", "go1.5.2", etc.,
|
|
|
|
// and returns the corresponding major version like "go1.5".
|
|
|
|
func major(v string) string {
|
|
|
|
if strings.Count(v, ".") != 2 {
|
|
|
|
// No minor component to drop, return as is.
|
|
|
|
return v
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
2019-09-03 18:20:20 +03:00
|
|
|
return v[:strings.LastIndex(v, ".")]
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
2018-06-06 18:39:58 +03:00
|
|
|
func (w *Work) writeVersion() (changeID string) {
|
2018-08-14 00:29:36 +03:00
|
|
|
changeID = fmt.Sprintf("I%x", sha1.Sum([]byte(fmt.Sprintf("cmd/release-version-%s", w.Version))))
|
2017-10-20 19:06:12 +03:00
|
|
|
|
2018-08-14 00:29:36 +03:00
|
|
|
err := ioutil.WriteFile(filepath.Join(w.Dir, "gitwork", "VERSION"), []byte(w.Version), 0666)
|
2017-10-20 19:06:12 +03:00
|
|
|
if err != nil {
|
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
|
|
|
|
2018-08-14 00:29:36 +03:00
|
|
|
desc := w.Version + "\n\n"
|
2017-10-20 19:06:12 +03:00
|
|
|
desc += "Change-Id: " + changeID + "\n"
|
|
|
|
|
2018-06-18 23:21:54 +03:00
|
|
|
r := w.runner(filepath.Join(w.Dir, "gitwork"))
|
2018-08-14 00:29:36 +03:00
|
|
|
r.run("git", "add", "VERSION")
|
2018-06-18 23:21:54 +03:00
|
|
|
r.run("git", "commit", "-m", desc, "VERSION")
|
2018-06-06 18:39:58 +03:00
|
|
|
if dryRun {
|
2018-06-18 23:21:54 +03:00
|
|
|
fmt.Printf("\n### VERSION commit\n\n%s\n", r.runOut("git", "show", "HEAD"))
|
2018-12-12 22:37:02 +03:00
|
|
|
} else if w.Security {
|
|
|
|
r.run("git", "codereview", "mail")
|
2018-06-06 18:39:58 +03:00
|
|
|
} else {
|
2018-12-12 22:37:02 +03:00
|
|
|
r.run("git", "codereview", "mail", "-trybot")
|
2018-06-06 18:39:58 +03:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// checkVersion makes sure that the version commit has been submitted.
|
|
|
|
func (w *Work) checkVersion() {
|
2018-06-18 23:21:54 +03:00
|
|
|
ver, err := ioutil.ReadFile(filepath.Join(w.Dir, "gitwork", "VERSION"))
|
2018-06-06 18:39:58 +03:00
|
|
|
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)
|
|
|
|
}
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Work) buildReleaseBinary() {
|
|
|
|
gopath := filepath.Join(w.Dir, "gopath")
|
|
|
|
if err := os.RemoveAll(gopath); err != nil {
|
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
|
|
|
if err := os.MkdirAll(gopath, 0777); err != nil {
|
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
2019-09-06 01:13:28 +03:00
|
|
|
r := w.runner(w.Dir, "GO111MODULE=off", "GOPATH="+gopath, "GOBIN="+filepath.Join(gopath, "bin"))
|
2018-06-18 23:21:54 +03:00
|
|
|
r.run("go", "get", "golang.org/x/build/cmd/release")
|
2017-10-20 19:06:12 +03:00
|
|
|
w.ReleaseBinary = filepath.Join(gopath, "bin/release")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Work) buildReleases() {
|
|
|
|
w.buildReleaseBinary()
|
2018-12-15 00:55:42 +03:00
|
|
|
if err := os.MkdirAll(filepath.Join(w.Dir, "release", w.VersionCommit), 0777); err != nil {
|
2017-10-20 19:06:12 +03:00
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
2019-08-22 17:30:51 +03:00
|
|
|
if err := os.MkdirAll(filepath.Join(w.Dir, "release-staging"), 0777); err != nil {
|
2019-08-21 21:11:59 +03:00
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
2019-08-22 17:30:51 +03:00
|
|
|
stagingDir, err := ioutil.TempDir(filepath.Join(w.Dir, "release-staging"), w.VersionCommit+"_")
|
|
|
|
if err != nil {
|
|
|
|
w.log.Panic(err)
|
|
|
|
}
|
|
|
|
w.StagingDir = stagingDir
|
2017-10-20 19:06:12 +03:00
|
|
|
w.ReleaseInfo = make(map[string]*ReleaseInfo)
|
|
|
|
|
2018-12-12 22:37:02 +03:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-20 19:06:12 +03:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
for _, target := range releaseTargets {
|
|
|
|
func() {
|
|
|
|
w.releaseMu.Lock()
|
|
|
|
defer w.releaseMu.Unlock()
|
|
|
|
w.ReleaseInfo[target] = new(ReleaseInfo)
|
|
|
|
}()
|
|
|
|
|
|
|
|
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))
|
2018-06-06 18:39:58 +03:00
|
|
|
w.logError(msg)
|
2017-10-20 19:06:12 +03:00
|
|
|
w.log.Printf("\n\nBuilding %s: PANIC: %v\n\n%s", target, err, debug.Stack())
|
|
|
|
w.releaseMu.Lock()
|
|
|
|
w.ReleaseInfo[target].Msg = msg
|
|
|
|
w.releaseMu.Unlock()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
w.buildRelease(target)
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
// Check for release errors and stop if any.
|
2018-06-06 18:39:58 +03:00
|
|
|
w.releaseMu.Lock()
|
|
|
|
for _, target := range releaseTargets {
|
|
|
|
for _, out := range w.ReleaseInfo[target].Outputs {
|
|
|
|
if out.Error != "" || len(w.Errors) > 0 {
|
|
|
|
w.logError("RELEASE BUILD FAILED\n")
|
|
|
|
w.releaseMu.Unlock()
|
|
|
|
return
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-06-06 18:39:58 +03:00
|
|
|
w.releaseMu.Unlock()
|
2017-10-20 19:06:12 +03:00
|
|
|
}
|
|
|
|
|
2018-12-15 00:55:42 +03:00
|
|
|
// buildRelease builds the release packaging for a given target. Because the
|
|
|
|
// "release" program can be flaky, it tries up to five times. The release files
|
2019-08-22 17:30:51 +03:00
|
|
|
// are first written to a staging directory specified in w.StagingDir
|
|
|
|
// (a temporary directory inside $HOME/go-releasebot-work/go1.2.3/release-staging),
|
2019-08-21 21:11:59 +03:00
|
|
|
// 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
|
2018-12-15 00:55:42 +03:00
|
|
|
// containing the SHA256 hash of the releases, for eventual use by the download page.
|
2017-10-20 19:06:12 +03:00
|
|
|
func (w *Work) buildRelease(target string) {
|
|
|
|
log.Printf("BUILDRELEASE %s %s\n", w.Version, target)
|
|
|
|
defer log.Printf("DONE BUILDRELEASE %s\n", target)
|
2018-12-15 00:55:42 +03:00
|
|
|
releaseDir := filepath.Join(w.Dir, "release", w.VersionCommit)
|
2017-10-20 19:06:12 +03:00
|
|
|
prefix := fmt.Sprintf("%s.%s.", w.Version, target)
|
|
|
|
var files []string
|
|
|
|
switch {
|
|
|
|
case strings.HasPrefix(target, "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)
|
2018-12-15 00:55:42 +03:00
|
|
|
_, err := os.Stat(filepath.Join(releaseDir, file))
|
2017-10-20 19:06:12 +03:00
|
|
|
if err != nil {
|
|
|
|
haveFiles = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w.releaseMu.Lock()
|
|
|
|
w.ReleaseInfo[target].Outputs = outs
|
|
|
|
w.releaseMu.Unlock()
|
|
|
|
|
|
|
|
if haveFiles {
|
|
|
|
w.log.Printf("release %s: already have %v; not rebuilding files", target, files)
|
|
|
|
} else {
|
2018-06-18 23:21:54 +03:00
|
|
|
failures := 0
|
|
|
|
for {
|
2018-12-12 22:37:02 +03:00
|
|
|
releaseBranch := strings.TrimSuffix(w.ReleaseBranch, "-security")
|
|
|
|
args := []string{w.ReleaseBinary, "-target", target, "-user", gomoteUser,
|
2019-08-21 21:11:59 +03:00
|
|
|
"-version", w.Version, "-tools", releaseBranch, "-net", releaseBranch,
|
2019-08-22 17:30:51 +03:00
|
|
|
"-staging_dir", w.StagingDir}
|
2018-12-12 22:37:02 +03:00
|
|
|
if w.Security {
|
|
|
|
args = append(args, "-tarball", filepath.Join(w.Dir, w.VersionCommit+".tar.gz"))
|
|
|
|
} else {
|
|
|
|
args = append(args, "-rev", w.VersionCommit)
|
|
|
|
}
|
2018-12-15 03:15:45 +03:00
|
|
|
// 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")
|
|
|
|
}
|
2018-12-15 00:55:42 +03:00
|
|
|
out, err := w.runner(releaseDir, "GOPATH="+filepath.Join(w.Dir, "gopath")).runErr(args...)
|
2017-10-20 19:06:12 +03:00
|
|
|
// 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 {
|
2018-12-15 00:55:42 +03:00
|
|
|
if _, err := os.Stat(filepath.Join(releaseDir, out.File)); err != nil {
|
2017-10-20 19:06:12 +03:00
|
|
|
failed = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w.releaseMu.Unlock()
|
|
|
|
if !failed {
|
|
|
|
break
|
|
|
|
}
|
2019-08-10 02:11:39 +03:00
|
|
|
w.log.Printf("release %s:\nerror from cmd/release binary = %v\noutput from cmd/release binary:\n%s", target, err, out)
|
2017-10-20 19:06:12 +03:00
|
|
|
if failures++; failures >= 3 {
|
|
|
|
w.log.Printf("release %s: too many failures\n", target)
|
|
|
|
for _, out := range outs {
|
|
|
|
w.releaseMu.Lock()
|
|
|
|
out.Error = fmt.Sprintf("release %s: build failed", target)
|
|
|
|
w.releaseMu.Unlock()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
time.Sleep(1 * time.Minute)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-15 00:55:42 +03:00
|
|
|
if dryRun || w.Prepare {
|
2018-06-06 18:39:58 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-20 19:06:12 +03:00
|
|
|
for _, out := range outs {
|
|
|
|
if err := w.uploadStagingRelease(target, out); err != nil {
|
|
|
|
w.log.Printf("release %s: %s", target, 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 string, out *ReleaseOutput) error {
|
2018-06-06 18:39:58 +03:00
|
|
|
if dryRun {
|
|
|
|
return errors.New("attempted write operation in dry-run mode")
|
|
|
|
}
|
|
|
|
|
2018-12-15 00:55:42 +03:00
|
|
|
src := filepath.Join(w.Dir, "release", w.VersionCommit, out.File)
|
2017-10-20 19:06:12 +03:00
|
|
|
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
|
|
|
|
}
|