зеркало из https://github.com/golang/build.git
cmd/releasebot, cmd/release: include long tests in release process
The goal of this change is to reduce the chance of issuing a release with an unintended regression that would be caught by running long tests. This change adds long tests that are run during the -prepare step, in addition to all the existing short tests that are run. Executing the long tests is implemented by adding two new test-only release targets. For a release to be considered complete, all release targets must complete. These test-only targets are built only for the purpose of verifying their tests succeed, or to block the release otherwise. They do not produce release artifacts. The new test-only targets are named after the builder which is used to perform their tests, and they are: • linux-amd64-longtest • windows-amd64-longtest More builders may be added in the future, but care must be taken to ensure the test execution environment is as close as possible to that of build.golang.org post-submit builders, in order to avoid false positives and false negatives. As part of a gradual rollout, this change also adds a flag to skip longtest builders. It's meant to be used in case a long test proves to be flaky, and enough confidence can be gained through testing elsewhere that the failure is not a regression caused by a change merged to the release branch. For now, its default value includes both longtest builders, so they are currently opt-in and this CL is a no-op. After testing proves that it is viable to rely on this (and any issues preventing that from being possible are resolved), the default value of the flag will be changed to the empty string. For golang/go#29252. For golang/go#39054. For golang/go#37827. Fixes golang/go#24614. Change-Id: I33ea6601aade2873f857c63f00a0c11821f35a95 Reviewed-on: https://go-review.googlesource.com/c/build/+/234531 Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Carlos Amedee <carlos@golang.org> Reviewed-by: Alexander Rakoczy <alex@golang.org>
This commit is contained in:
Родитель
e0af7f00b9
Коммит
e566a70d10
|
@ -44,7 +44,7 @@ var (
|
|||
tarball = flag.String("tarball", "", "Go tree tarball to build, alternative to -rev")
|
||||
version = flag.String("version", "", "Version string (go1.5.2)")
|
||||
user = flag.String("user", username(), "coordinator username, appended to 'user-'")
|
||||
skipTests = flag.Bool("skip_tests", false, "skip tests; run make.bash instead of all.bash (only use if you ran trybots first)")
|
||||
skipTests = flag.Bool("skip_tests", false, "skip tests; run make.bash but not all.bash (only use if sufficient testing was done elsewhere)")
|
||||
|
||||
uploadMode = flag.Bool("upload", false, "Upload files (exclusive to all other flags)")
|
||||
uploadKick = flag.String("edge_kick_command", "", "Command to run to kick off an edge cache update")
|
||||
|
@ -108,20 +108,26 @@ type Build struct {
|
|||
|
||||
Race bool // Build race detector.
|
||||
|
||||
Builder string // Key for dashboard.Builders.
|
||||
Builder string // Key for dashboard.Builders.
|
||||
TestOnly bool // Run tests only; don't produce a release artifact.
|
||||
|
||||
Goarm int // GOARM value if set.
|
||||
MakeOnly bool // don't run tests; needed by cross-compile builders (s390x)
|
||||
Goarm int // GOARM value if set.
|
||||
SkipTests bool // skip tests (run make.bash but not all.bash); needed by cross-compile builders (s390x)
|
||||
}
|
||||
|
||||
func (b *Build) String() string {
|
||||
if b.Source {
|
||||
switch {
|
||||
case b.Source:
|
||||
return "src"
|
||||
}
|
||||
if b.Goarm != 0 {
|
||||
case b.TestOnly:
|
||||
// Test-only builds are named after the builder used to
|
||||
// perform them. For example, "linux-amd64-longtest".
|
||||
return b.Builder
|
||||
case b.Goarm != 0:
|
||||
return fmt.Sprintf("%v-%vv%vl", b.OS, b.Arch, b.Goarm)
|
||||
default:
|
||||
return fmt.Sprintf("%v-%v", b.OS, b.Arch)
|
||||
}
|
||||
return fmt.Sprintf("%v-%v", b.OS, b.Arch)
|
||||
}
|
||||
|
||||
func (b *Build) toolDir() string { return "go/pkg/tool/" + b.OS + "_" + b.Arch }
|
||||
|
@ -149,13 +155,13 @@ var builds = []*Build{
|
|||
Goarm: 6, // for compatibility with all Raspberry Pi models.
|
||||
// The tests take too long for the release packaging.
|
||||
// Much of the time the whole buildlet times out.
|
||||
MakeOnly: true,
|
||||
SkipTests: true,
|
||||
},
|
||||
{
|
||||
OS: "linux",
|
||||
Arch: "amd64",
|
||||
Race: true,
|
||||
Builder: "linux-amd64-jessie", // using Jessie for at least [Go 1.11, Go 1.13] due to golang.org/issue/31336
|
||||
Builder: "linux-amd64-jessie", // using Jessie for at least [Go 1.11, Go 1.13] due to golang.org/issue/31293
|
||||
},
|
||||
{
|
||||
OS: "linux",
|
||||
|
@ -191,20 +197,32 @@ var builds = []*Build{
|
|||
Builder: "darwin-amd64-10_15",
|
||||
},
|
||||
{
|
||||
OS: "linux",
|
||||
Arch: "s390x",
|
||||
MakeOnly: true,
|
||||
Builder: "linux-s390x-crosscompile",
|
||||
OS: "linux",
|
||||
Arch: "s390x",
|
||||
SkipTests: true,
|
||||
Builder: "linux-s390x-crosscompile",
|
||||
},
|
||||
// TODO(bradfitz): switch this ppc64 builder to a Kubernetes
|
||||
// container cross-compiling ppc64 like the s390x one? For
|
||||
// now, the ppc64le builders (5) are back, so let's see if we
|
||||
// can just depend on them not going away.
|
||||
{
|
||||
OS: "linux",
|
||||
Arch: "ppc64le",
|
||||
MakeOnly: true,
|
||||
Builder: "linux-ppc64le-buildlet",
|
||||
OS: "linux",
|
||||
Arch: "ppc64le",
|
||||
SkipTests: true,
|
||||
Builder: "linux-ppc64le-buildlet",
|
||||
},
|
||||
|
||||
// Test-only builds.
|
||||
{
|
||||
Builder: "linux-amd64-longtest",
|
||||
OS: "linux", Arch: "amd64",
|
||||
TestOnly: true,
|
||||
},
|
||||
{
|
||||
Builder: "windows-amd64-longtest",
|
||||
OS: "windows", Arch: "amd64",
|
||||
TestOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -236,7 +254,7 @@ func (b *Build) make() error {
|
|||
}
|
||||
|
||||
var hostArch string // non-empty if we're cross-compiling (s390x)
|
||||
if b.MakeOnly && bc.IsContainer() && (bc.GOARCH() != "amd64" && bc.GOARCH() != "386") {
|
||||
if b.SkipTests && bc.IsContainer() && (bc.GOARCH() != "amd64" && bc.GOARCH() != "386") {
|
||||
hostArch = "amd64"
|
||||
}
|
||||
|
||||
|
@ -464,7 +482,8 @@ func (b *Build) make() error {
|
|||
// So far, we've run make.bash. We want to create the release archive next.
|
||||
// Since the release archive hasn't been tested yet, place it in a temporary
|
||||
// location. After all.bash runs successfully (or gets explicitly skipped),
|
||||
// we'll move the release archive to its final location.
|
||||
// we'll move the release archive to its final location. For TestOnly builds,
|
||||
// we only care whether tests passed and do not produce release artifacts.
|
||||
type releaseFile struct {
|
||||
Untested string // Temporary location of the file before the release has been tested.
|
||||
Final string // Final location where to move the file after the release has been tested.
|
||||
|
@ -482,7 +501,7 @@ func (b *Build) make() error {
|
|||
return filepath.Join(stagingDir, *version+"."+b.String()+ext+".untested")
|
||||
}
|
||||
|
||||
if b.OS == "windows" {
|
||||
if !b.TestOnly && b.OS == "windows" {
|
||||
untested := stagingFile(".msi")
|
||||
if err := b.fetchFile(client, untested, "msi"); err != nil {
|
||||
return err
|
||||
|
@ -491,6 +510,9 @@ func (b *Build) make() error {
|
|||
Untested: untested,
|
||||
Final: *version + "." + b.String() + ".msi",
|
||||
})
|
||||
}
|
||||
|
||||
if b.OS == "windows" {
|
||||
cleanFiles = append(cleanFiles, "msi")
|
||||
}
|
||||
|
||||
|
@ -510,8 +532,8 @@ func (b *Build) make() error {
|
|||
return fmt.Errorf("verifying file permissions: %v", err)
|
||||
}
|
||||
|
||||
switch b.OS {
|
||||
default:
|
||||
switch {
|
||||
case !b.TestOnly && b.OS != "windows":
|
||||
untested := stagingFile(".tar.gz")
|
||||
if err := b.fetchTarball(ctx, client, untested); err != nil {
|
||||
return fmt.Errorf("fetching and writing tarball: %v", err)
|
||||
|
@ -520,7 +542,7 @@ func (b *Build) make() error {
|
|||
Untested: untested,
|
||||
Final: *version + "." + b.String() + ".tar.gz",
|
||||
})
|
||||
case "windows":
|
||||
case !b.TestOnly && b.OS == "windows":
|
||||
untested := stagingFile(".zip")
|
||||
if err := b.fetchZip(client, untested); err != nil {
|
||||
return fmt.Errorf("fetching and writing zip: %v", err)
|
||||
|
@ -529,10 +551,23 @@ func (b *Build) make() error {
|
|||
Untested: untested,
|
||||
Final: *version + "." + b.String() + ".zip",
|
||||
})
|
||||
case b.TestOnly:
|
||||
// Use an empty .test-only file to indicate the test outcome.
|
||||
// This file gets moved from its initial location in the
|
||||
// release-staging directory to the final release directory
|
||||
// when the test-only build passes tests successfully.
|
||||
untested := stagingFile(".test-only")
|
||||
if err := ioutil.WriteFile(untested, nil, 0600); err != nil {
|
||||
return fmt.Errorf("writing empty test-only file: %v", err)
|
||||
}
|
||||
releases = append(releases, releaseFile{
|
||||
Untested: untested,
|
||||
Final: *version + "." + b.String() + ".test-only",
|
||||
})
|
||||
}
|
||||
|
||||
// Execute build (all.bash) if running tests.
|
||||
if *skipTests || b.MakeOnly {
|
||||
if *skipTests || b.SkipTests {
|
||||
b.logf("Skipping all.bash tests.")
|
||||
} else {
|
||||
if u := bc.GoBootstrapURL(buildEnv); u != "" {
|
||||
|
@ -760,9 +795,12 @@ func (b *Build) writeFile(name string, r io.Reader) error {
|
|||
}
|
||||
|
||||
// checkRelocations runs readelf on pkg/linux_amd64/runtime/cgo.a and makes sure
|
||||
// we don't see R_X86_64_REX_GOTPCRELX. See issue 31293.
|
||||
// we don't see R_X86_64_REX_GOTPCRELX. See golang.org/issue/31293.
|
||||
func (b *Build) checkRelocations(client *buildlet.Client) error {
|
||||
if b.OS != "linux" || b.Arch != "amd64" {
|
||||
if b.OS != "linux" || b.Arch != "amd64" || b.TestOnly {
|
||||
// This check is only applicable to linux/amd64 builds.
|
||||
// However, skip it on test-only builds because they
|
||||
// don't produce binaries that are shipped to users.
|
||||
return nil
|
||||
}
|
||||
var out bytes.Buffer
|
||||
|
|
|
@ -19,6 +19,14 @@ func TestBuildersExist(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTestOnlyBuildsDontSkipTests(t *testing.T) {
|
||||
for _, b := range builds {
|
||||
if b.TestOnly && b.SkipTests {
|
||||
t.Errorf("build %s is configured to run tests only, but also to skip tests; is that intentional?", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinSupportedMacOSVersion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
|
|
@ -31,19 +31,32 @@ import (
|
|||
"golang.org/x/build/maintner"
|
||||
)
|
||||
|
||||
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",
|
||||
// A Target is a release target.
|
||||
type Target struct {
|
||||
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: "darwin-amd64"},
|
||||
{Name: "linux-s390x"},
|
||||
{Name: "linux-ppc64le"},
|
||||
|
||||
// Test-only targets.
|
||||
{Name: "linux-amd64-longtest", TestOnly: true},
|
||||
{Name: "windows-amd64-longtest", TestOnly: true},
|
||||
}
|
||||
|
||||
var releaseModes = map[string]bool{
|
||||
|
@ -53,10 +66,18 @@ var releaseModes = map[string]bool{
|
|||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: releasebot -mode {prepare|release} [-security] [-dry-run] {go1.8.5|go1.10beta2|go1.11rc1}")
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
var dryRun bool // only perform pre-flight checks, only log to terminal
|
||||
var (
|
||||
skipTestFlag = flag.String("skip-test", "linux-amd64-longtest windows-amd64-longtest", "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)")
|
||||
|
@ -65,8 +86,19 @@ func main() {
|
|||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if *modeFlag == "" || !releaseModes[*modeFlag] || flag.NArg() != 1 {
|
||||
fmt.Fprintln(os.Stderr, "need to provide a valid mode and a release name")
|
||||
usage()
|
||||
}
|
||||
for _, target := range strings.Fields(*skipTestFlag) {
|
||||
if t, ok := releaseTarget(target); !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)
|
||||
|
||||
|
@ -521,8 +553,8 @@ func (w *Work) printReleaseTable(md *bytes.Buffer) {
|
|||
w.releaseMu.Lock()
|
||||
defer w.releaseMu.Unlock()
|
||||
for _, target := range releaseTargets {
|
||||
fmt.Fprintf(md, "%s", mdEscape(target))
|
||||
info := w.ReleaseInfo[target]
|
||||
fmt.Fprintf(md, "%s", mdEscape(target.Name))
|
||||
info := w.ReleaseInfo[target.Name]
|
||||
if info == nil {
|
||||
fmt.Fprintf(md, " not started\n")
|
||||
continue
|
||||
|
@ -647,11 +679,17 @@ to %s and press enter.
|
|||
|
||||
var wg sync.WaitGroup
|
||||
for _, target := range releaseTargets {
|
||||
func() {
|
||||
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()
|
||||
defer w.releaseMu.Unlock()
|
||||
w.ReleaseInfo[target] = new(ReleaseInfo)
|
||||
}()
|
||||
w.ReleaseInfo[target.Name].Msg = fmt.Sprintf("skipped because of -skip-test=%q flag", *skipTestFlag)
|
||||
w.releaseMu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
target := target
|
||||
|
@ -662,9 +700,9 @@ to %s and press enter.
|
|||
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, err, debug.Stack())
|
||||
w.log.Printf("\n\nBuilding %s: PANIC: %v\n\n%s", target.Name, err, debug.Stack())
|
||||
w.releaseMu.Lock()
|
||||
w.ReleaseInfo[target].Msg = msg
|
||||
w.ReleaseInfo[target.Name].Msg = msg
|
||||
w.releaseMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
@ -676,7 +714,7 @@ to %s and press enter.
|
|||
// Check for release errors and stop if any.
|
||||
w.releaseMu.Lock()
|
||||
for _, target := range releaseTargets {
|
||||
for _, out := range w.ReleaseInfo[target].Outputs {
|
||||
for _, out := range w.ReleaseInfo[target.Name].Outputs {
|
||||
if out.Error != "" || len(w.Errors) > 0 {
|
||||
w.logError("RELEASE BUILD FAILED\n")
|
||||
w.releaseMu.Unlock()
|
||||
|
@ -699,14 +737,16 @@ to %s and press enter.
|
|||
// 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 string) {
|
||||
log.Printf("BUILDRELEASE %s %s\n", w.Version, target)
|
||||
defer log.Printf("DONE BUILDRELEASE %s\n", target)
|
||||
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)
|
||||
prefix := fmt.Sprintf("%s.%s.", w.Version, target.Name)
|
||||
var files []string
|
||||
switch {
|
||||
case strings.HasPrefix(target, "windows-"):
|
||||
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"}
|
||||
|
@ -725,15 +765,15 @@ func (w *Work) buildRelease(target string) {
|
|||
}
|
||||
}
|
||||
w.releaseMu.Lock()
|
||||
w.ReleaseInfo[target].Outputs = outs
|
||||
w.ReleaseInfo[target.Name].Outputs = outs
|
||||
w.releaseMu.Unlock()
|
||||
|
||||
if haveFiles {
|
||||
w.log.Printf("release %s: already have %v; not rebuilding files", target, files)
|
||||
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, "-user", gomoteUser,
|
||||
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"))
|
||||
|
@ -746,7 +786,7 @@ func (w *Work) buildRelease(target string) {
|
|||
if !w.Prepare {
|
||||
args = append(args, "-skip_tests")
|
||||
}
|
||||
out, err := w.runner(releaseDir, "GOPATH="+filepath.Join(w.Dir, "gopath")).runErr(args...)
|
||||
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
|
||||
|
@ -760,12 +800,12 @@ func (w *Work) buildRelease(target string) {
|
|||
if !failed {
|
||||
break
|
||||
}
|
||||
w.log.Printf("release %s:\nerror from cmd/release binary = %v\noutput from cmd/release binary:\n%s", target, err, out)
|
||||
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 %s: too many failures\n", target)
|
||||
w.log.Printf("release -target=%q: too many failures\n", target.Name)
|
||||
for _, out := range outs {
|
||||
w.releaseMu.Lock()
|
||||
out.Error = fmt.Sprintf("release %s: build failed", target)
|
||||
out.Error = fmt.Sprintf("release -target=%q: build failed", target.Name)
|
||||
w.releaseMu.Unlock()
|
||||
}
|
||||
return
|
||||
|
@ -778,9 +818,14 @@ func (w *Work) buildRelease(target string) {
|
|||
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("release %s: %s", target, err)
|
||||
w.log.Printf("error uploading release %s to staging bucket: %s", target.Name, err)
|
||||
w.releaseMu.Lock()
|
||||
out.Error = err.Error()
|
||||
w.releaseMu.Unlock()
|
||||
|
@ -794,9 +839,11 @@ func (w *Work) buildRelease(target string) {
|
|||
// 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 {
|
||||
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)
|
||||
|
@ -848,3 +895,13 @@ func (w *Work) mustIncludeSecurityBranch() {
|
|||
log.Fatalf("release branch does not contain security release HEAD commit %q; aborting", sha)
|
||||
}
|
||||
}
|
||||
|
||||
// releaseTarget returns a release target with the specified name.
|
||||
func releaseTarget(name string) (_ Target, ok bool) {
|
||||
for _, t := range releaseTargets {
|
||||
if t.Name == name {
|
||||
return t, true
|
||||
}
|
||||
}
|
||||
return Target{}, false
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче