2017-05-13 18:09:42 -03:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
2019-01-17 11:25:57 -02:00
|
|
|
"crypto/tls"
|
2023-12-12 16:12:07 -03:00
|
|
|
"errors"
|
2020-07-06 14:48:17 +01:00
|
|
|
"fmt"
|
2019-01-17 11:25:57 -02:00
|
|
|
"net/http"
|
2017-09-23 19:42:07 +02:00
|
|
|
"net/url"
|
2017-05-13 18:09:42 -03:00
|
|
|
"os"
|
2019-08-10 21:35:20 +08:00
|
|
|
"reflect"
|
2019-06-29 16:02:40 +02:00
|
|
|
"strconv"
|
2021-10-04 09:32:30 -03:00
|
|
|
"strings"
|
2023-05-27 00:15:43 -03:00
|
|
|
"time"
|
2017-05-13 18:09:42 -03:00
|
|
|
|
2022-06-21 21:11:15 -03:00
|
|
|
"github.com/caarlos0/log"
|
2023-10-10 23:16:27 -03:00
|
|
|
"github.com/charmbracelet/x/exp/ordered"
|
2024-10-07 10:01:13 -03:00
|
|
|
"github.com/google/go-github/v66/github"
|
2024-05-26 15:02:57 -03:00
|
|
|
"github.com/goreleaser/goreleaser/v2/internal/artifact"
|
|
|
|
"github.com/goreleaser/goreleaser/v2/internal/tmpl"
|
|
|
|
"github.com/goreleaser/goreleaser/v2/pkg/config"
|
|
|
|
"github.com/goreleaser/goreleaser/v2/pkg/context"
|
2017-05-13 18:09:42 -03:00
|
|
|
"golang.org/x/oauth2"
|
|
|
|
)
|
|
|
|
|
2020-07-06 14:48:17 +01:00
|
|
|
const DefaultGitHubDownloadURL = "https://github.com"
|
|
|
|
|
2023-04-06 22:58:06 -03:00
|
|
|
var (
|
|
|
|
_ Client = &githubClient{}
|
|
|
|
_ ReleaseNotesGenerator = &githubClient{}
|
|
|
|
_ PullRequestOpener = &githubClient{}
|
2024-03-22 16:42:15 -03:00
|
|
|
_ ForkSyncer = &githubClient{}
|
2023-04-06 22:58:06 -03:00
|
|
|
)
|
|
|
|
|
2017-05-13 18:09:42 -03:00
|
|
|
type githubClient struct {
|
|
|
|
client *github.Client
|
|
|
|
}
|
|
|
|
|
2023-04-06 22:58:06 -03:00
|
|
|
// NewGitHubReleaseNotesGenerator returns a GitHub client that can generate
|
|
|
|
// changelogs.
|
|
|
|
func NewGitHubReleaseNotesGenerator(ctx *context.Context, token string) (ReleaseNotesGenerator, error) {
|
|
|
|
return newGitHub(ctx, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
// newGitHub returns a github client implementation.
|
|
|
|
func newGitHub(ctx *context.Context, token string) (*githubClient, error) {
|
2017-05-13 18:09:42 -03:00
|
|
|
ts := oauth2.StaticTokenSource(
|
2020-07-06 21:12:41 +01:00
|
|
|
&oauth2.Token{AccessToken: token},
|
2017-05-13 18:09:42 -03:00
|
|
|
)
|
2021-09-18 10:21:29 -03:00
|
|
|
|
2019-01-17 11:25:57 -02:00
|
|
|
httpClient := oauth2.NewClient(ctx, ts)
|
2019-01-17 18:03:36 -02:00
|
|
|
base := httpClient.Transport.(*oauth2.Transport).Base
|
2019-08-10 21:35:20 +08:00
|
|
|
if base == nil || reflect.ValueOf(base).IsNil() {
|
2019-01-17 18:03:36 -02:00
|
|
|
base = http.DefaultTransport
|
|
|
|
}
|
2024-05-12 20:11:11 +03:00
|
|
|
//nolint:gosec
|
2019-01-17 18:03:36 -02:00
|
|
|
base.(*http.Transport).TLSClientConfig = &tls.Config{
|
2019-01-17 11:28:10 -02:00
|
|
|
InsecureSkipVerify: ctx.Config.GitHubURLs.SkipTLSVerify,
|
2019-01-17 11:25:57 -02:00
|
|
|
}
|
2020-11-05 05:16:25 -03:00
|
|
|
base.(*http.Transport).Proxy = http.ProxyFromEnvironment
|
2019-01-17 18:03:36 -02:00
|
|
|
httpClient.Transport.(*oauth2.Transport).Base = base
|
2021-09-09 03:42:13 +02:00
|
|
|
|
2019-01-17 11:25:57 -02:00
|
|
|
client := github.NewClient(httpClient)
|
2021-09-09 03:42:13 +02:00
|
|
|
err := overrideGitHubClientAPI(ctx, client)
|
|
|
|
if err != nil {
|
|
|
|
return &githubClient{}, err
|
2017-09-23 19:42:07 +02:00
|
|
|
}
|
|
|
|
|
2018-06-19 15:53:14 -03:00
|
|
|
return &githubClient{client: client}, nil
|
2017-05-13 18:09:42 -03:00
|
|
|
}
|
|
|
|
|
2023-05-27 00:15:43 -03:00
|
|
|
func (c *githubClient) checkRateLimit(ctx *context.Context) {
|
2024-03-19 23:25:42 -03:00
|
|
|
limits, _, err := c.client.RateLimit.Get(ctx)
|
2023-05-27 00:15:43 -03:00
|
|
|
if err != nil {
|
|
|
|
log.Warn("could not check rate limits, hoping for the best...")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if limits.Core.Remaining > 100 { // 100 should be safe enough
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sleep := limits.Core.Reset.UTC().Sub(time.Now().UTC())
|
2023-06-29 14:00:23 -03:00
|
|
|
if sleep <= 0 {
|
|
|
|
// it seems that sometimes, after the rate limit just reset, it might
|
|
|
|
// still get <100 remaining and a reset time in the past... in such
|
|
|
|
// cases we can probably sleep a bit more before trying again...
|
|
|
|
sleep = 15 * time.Second
|
|
|
|
}
|
2023-05-27 00:15:43 -03:00
|
|
|
log.Warnf("token too close to rate limiting, will sleep for %s before continuing...", sleep)
|
|
|
|
time.Sleep(sleep)
|
2023-06-29 14:00:23 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2023-05-27 00:15:43 -03:00
|
|
|
}
|
|
|
|
|
2021-11-07 12:53:28 -03:00
|
|
|
func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, prev, current string) (string, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2021-11-07 12:53:28 -03:00
|
|
|
notes, _, err := c.client.Repositories.GenerateReleaseNotes(ctx, repo.Owner, repo.Name, &github.GenerateNotesOptions{
|
|
|
|
TagName: current,
|
|
|
|
PreviousTagName: github.String(prev),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return notes.Body, err
|
|
|
|
}
|
|
|
|
|
2024-04-24 09:08:20 -03:00
|
|
|
func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) ([]ChangelogItem, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2024-04-24 09:08:20 -03:00
|
|
|
var log []ChangelogItem
|
2021-10-14 09:32:52 -03:00
|
|
|
opts := &github.ListOptions{PerPage: 100}
|
2022-08-22 22:34:48 -03:00
|
|
|
|
2021-10-14 09:32:52 -03:00
|
|
|
for {
|
|
|
|
result, resp, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current, opts)
|
|
|
|
if err != nil {
|
2024-04-24 09:08:20 -03:00
|
|
|
return nil, err
|
2021-10-14 09:32:52 -03:00
|
|
|
}
|
|
|
|
for _, commit := range result.Commits {
|
2024-04-24 09:08:20 -03:00
|
|
|
log = append(log, ChangelogItem{
|
|
|
|
SHA: commit.GetSHA(),
|
|
|
|
Message: strings.Split(commit.Commit.GetMessage(), "\n")[0],
|
|
|
|
AuthorName: commit.GetAuthor().GetName(),
|
|
|
|
AuthorEmail: commit.GetAuthor().GetEmail(),
|
|
|
|
AuthorUsername: commit.GetAuthor().GetLogin(),
|
|
|
|
})
|
2021-10-14 09:32:52 -03:00
|
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
opts.Page = resp.NextPage
|
2021-10-04 09:32:30 -03:00
|
|
|
}
|
2021-10-14 09:32:52 -03:00
|
|
|
|
2024-04-24 09:08:20 -03:00
|
|
|
return log, nil
|
2021-10-04 09:32:30 -03:00
|
|
|
}
|
|
|
|
|
2023-04-06 22:58:06 -03:00
|
|
|
// getDefaultBranch returns the default branch of a github repo
|
|
|
|
func (c *githubClient) getDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2021-10-03 10:22:26 -04:00
|
|
|
p, res, err := c.client.Repositories.Get(ctx, repo.Owner, repo.Name)
|
|
|
|
if err != nil {
|
2024-01-21 21:46:59 -03:00
|
|
|
log := log.WithField("projectID", repo.String())
|
|
|
|
if res != nil {
|
|
|
|
log = log.WithField("statusCode", res.StatusCode)
|
|
|
|
}
|
|
|
|
log.
|
2023-05-02 09:06:35 -03:00
|
|
|
WithError(err).
|
|
|
|
Warn("error checking for default branch")
|
2021-10-03 10:22:26 -04:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return p.GetDefaultBranch(), nil
|
|
|
|
}
|
|
|
|
|
2020-07-09 16:40:37 -04:00
|
|
|
// CloseMilestone closes a given milestone.
|
|
|
|
func (c *githubClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2020-07-09 16:40:37 -04:00
|
|
|
milestone, err := c.getMilestoneByTitle(ctx, repo, title)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if milestone == nil {
|
|
|
|
return ErrNoMilestoneFound{Title: title}
|
|
|
|
}
|
|
|
|
|
|
|
|
closedState := "closed"
|
|
|
|
milestone.State = &closedState
|
|
|
|
|
|
|
|
_, _, err = c.client.Issues.EditMilestone(
|
|
|
|
ctx,
|
|
|
|
repo.Owner,
|
|
|
|
repo.Name,
|
|
|
|
*milestone.Number,
|
|
|
|
milestone,
|
|
|
|
)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-29 14:37:10 -03:00
|
|
|
func headString(base, head Repo) string {
|
|
|
|
return strings.Join([]string{
|
2023-10-10 23:16:27 -03:00
|
|
|
ordered.First(head.Owner, base.Owner),
|
|
|
|
ordered.First(head.Name, base.Name),
|
|
|
|
ordered.First(head.Branch, base.Branch),
|
2023-05-29 14:37:10 -03:00
|
|
|
}, ":")
|
|
|
|
}
|
|
|
|
|
2023-06-14 23:28:38 -03:00
|
|
|
func (c *githubClient) getPRTemplate(ctx *context.Context, repo Repo) (string, error) {
|
|
|
|
content, _, _, err := c.client.Repositories.GetContents(
|
|
|
|
ctx, repo.Owner, repo.Name,
|
|
|
|
".github/PULL_REQUEST_TEMPLATE.md",
|
|
|
|
&github.RepositoryContentGetOptions{
|
|
|
|
Ref: repo.Branch,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return content.GetContent()
|
|
|
|
}
|
|
|
|
|
2023-04-06 22:58:06 -03:00
|
|
|
func (c *githubClient) OpenPullRequest(
|
|
|
|
ctx *context.Context,
|
2023-05-29 14:37:10 -03:00
|
|
|
base, head Repo,
|
|
|
|
title string,
|
2023-05-29 15:07:00 -03:00
|
|
|
draft bool,
|
2023-04-06 22:58:06 -03:00
|
|
|
) error {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2023-10-10 23:16:27 -03:00
|
|
|
base.Owner = ordered.First(base.Owner, head.Owner)
|
|
|
|
base.Name = ordered.First(base.Name, head.Name)
|
2023-05-29 14:37:10 -03:00
|
|
|
if base.Branch == "" {
|
|
|
|
def, err := c.getDefaultBranch(ctx, base)
|
2023-04-06 22:58:06 -03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-05-29 14:37:10 -03:00
|
|
|
base.Branch = def
|
2023-04-06 22:58:06 -03:00
|
|
|
}
|
2023-06-14 23:28:38 -03:00
|
|
|
tpl, err := c.getPRTemplate(ctx, base)
|
|
|
|
if err != nil {
|
|
|
|
log.WithError(err).Debug("no pull request template found...")
|
|
|
|
}
|
|
|
|
if len(tpl) > 0 {
|
|
|
|
log.Info("got a pr template")
|
|
|
|
}
|
2024-03-22 16:42:15 -03:00
|
|
|
|
2023-05-29 14:37:10 -03:00
|
|
|
log := log.
|
|
|
|
WithField("base", headString(base, Repo{})).
|
2023-05-29 15:07:00 -03:00
|
|
|
WithField("head", headString(base, head)).
|
|
|
|
WithField("draft", draft)
|
2023-05-29 14:37:10 -03:00
|
|
|
log.Info("opening pull request")
|
2023-06-14 23:28:38 -03:00
|
|
|
pr, res, err := c.client.PullRequests.Create(
|
|
|
|
ctx,
|
2023-08-27 16:40:40 -03:00
|
|
|
base.Owner,
|
|
|
|
base.Name,
|
2023-06-14 23:28:38 -03:00
|
|
|
&github.NewPullRequest{
|
|
|
|
Title: github.String(title),
|
|
|
|
Base: github.String(base.Branch),
|
|
|
|
Head: github.String(headString(base, head)),
|
|
|
|
Body: github.String(strings.Join([]string{tpl, prFooter}, "\n")),
|
|
|
|
Draft: github.Bool(draft),
|
|
|
|
},
|
|
|
|
)
|
2023-04-06 22:58:06 -03:00
|
|
|
if err != nil {
|
2023-12-12 16:12:07 -03:00
|
|
|
if res.StatusCode == http.StatusUnprocessableEntity {
|
2023-06-14 23:28:38 -03:00
|
|
|
log.WithError(err).Warn("pull request validation failed")
|
2023-04-06 22:58:06 -03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return fmt.Errorf("could not create pull request: %w", err)
|
|
|
|
}
|
2023-06-14 23:28:38 -03:00
|
|
|
log.WithField("url", pr.GetHTMLURL()).Info("pull request created")
|
2023-04-06 22:58:06 -03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-22 16:42:15 -03:00
|
|
|
func (c *githubClient) SyncFork(ctx *context.Context, head, base Repo) error {
|
|
|
|
branch := base.Branch
|
|
|
|
if branch == "" {
|
|
|
|
def, err := c.getDefaultBranch(ctx, base)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
branch = def
|
|
|
|
}
|
|
|
|
res, _, err := c.client.Repositories.MergeUpstream(
|
|
|
|
ctx,
|
|
|
|
head.Owner,
|
|
|
|
head.Name,
|
|
|
|
&github.RepoMergeUpstreamRequest{
|
|
|
|
Branch: github.String(branch),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if res != nil {
|
|
|
|
log.WithField("merge_type", res.GetMergeType()).
|
|
|
|
WithField("base_branch", res.GetBaseBranch()).
|
|
|
|
Info(res.GetMessage())
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2017-05-13 18:09:42 -03:00
|
|
|
func (c *githubClient) CreateFile(
|
|
|
|
ctx *context.Context,
|
2018-02-09 13:35:51 +00:00
|
|
|
commitAuthor config.CommitAuthor,
|
2020-07-06 21:12:41 +01:00
|
|
|
repo Repo,
|
2019-06-26 14:12:33 -03:00
|
|
|
content []byte,
|
2019-03-31 20:11:35 +03:00
|
|
|
path,
|
2018-03-10 14:13:00 -03:00
|
|
|
message string,
|
2018-02-16 10:35:44 -02:00
|
|
|
) error {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2023-04-06 22:58:06 -03:00
|
|
|
defBranch, err := c.getDefaultBranch(ctx, repo)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not get default branch: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
branch := repo.Branch
|
|
|
|
if branch == "" {
|
|
|
|
branch = defBranch
|
2021-10-03 10:22:26 -04:00
|
|
|
}
|
2023-04-06 22:58:06 -03:00
|
|
|
|
2017-05-13 18:09:42 -03:00
|
|
|
options := &github.RepositoryContentFileOptions{
|
|
|
|
Committer: &github.CommitAuthor{
|
2018-02-09 13:43:00 +00:00
|
|
|
Name: github.String(commitAuthor.Name),
|
|
|
|
Email: github.String(commitAuthor.Email),
|
2017-05-13 18:09:42 -03:00
|
|
|
},
|
2019-06-26 14:12:33 -03:00
|
|
|
Content: content,
|
2018-03-10 14:13:00 -03:00
|
|
|
Message: github.String(message),
|
2017-05-13 18:09:42 -03:00
|
|
|
}
|
|
|
|
|
2021-10-03 10:22:26 -04:00
|
|
|
// Set the branch if we got it above...otherwise, just default to
|
|
|
|
// whatever the SDK does auto-magically
|
|
|
|
if branch != "" {
|
|
|
|
options.Branch = &branch
|
|
|
|
}
|
|
|
|
|
2023-05-19 14:10:06 +00:00
|
|
|
log.
|
|
|
|
WithField("repository", repo.String()).
|
2023-05-29 14:37:10 -03:00
|
|
|
WithField("branch", repo.Branch).
|
2023-06-14 23:28:38 -03:00
|
|
|
WithField("file", path).
|
2023-05-19 14:10:06 +00:00
|
|
|
Info("pushing")
|
|
|
|
|
2023-04-06 22:58:06 -03:00
|
|
|
if defBranch != branch && branch != "" {
|
2023-10-17 15:52:41 +00:00
|
|
|
_, res, err := c.client.Repositories.GetBranch(ctx, repo.Owner, repo.Name, branch, 100)
|
2023-12-12 16:12:07 -03:00
|
|
|
if err != nil && (res == nil || res.StatusCode != http.StatusNotFound) {
|
2023-04-06 22:58:06 -03:00
|
|
|
return fmt.Errorf("could not get branch %q: %w", branch, err)
|
|
|
|
}
|
|
|
|
|
2023-12-12 16:12:07 -03:00
|
|
|
if res.StatusCode == http.StatusNotFound {
|
2023-04-06 22:58:06 -03:00
|
|
|
defRef, _, err := c.client.Git.GetRef(ctx, repo.Owner, repo.Name, "refs/heads/"+defBranch)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not get ref %q: %w", "refs/heads/"+defBranch, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, _, err := c.client.Git.CreateRef(ctx, repo.Owner, repo.Name, &github.Reference{
|
|
|
|
Ref: github.String("refs/heads/" + branch),
|
|
|
|
Object: &github.GitObject{
|
|
|
|
SHA: defRef.Object.SHA,
|
|
|
|
},
|
|
|
|
}); err != nil {
|
2023-12-12 16:12:07 -03:00
|
|
|
rerr := new(github.ErrorResponse)
|
|
|
|
if !errors.As(err, &rerr) || rerr.Message != "Reference already exists" {
|
|
|
|
return fmt.Errorf("could not create ref %q from %q: %w", "refs/heads/"+branch, defRef.Object.GetSHA(), err)
|
|
|
|
}
|
2023-04-06 22:58:06 -03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-13 18:09:42 -03:00
|
|
|
file, _, res, err := c.client.Repositories.GetContents(
|
|
|
|
ctx,
|
2018-02-09 13:43:00 +00:00
|
|
|
repo.Owner,
|
|
|
|
repo.Name,
|
2017-05-13 18:09:42 -03:00
|
|
|
path,
|
2023-04-06 22:58:06 -03:00
|
|
|
&github.RepositoryContentGetOptions{
|
|
|
|
Ref: branch,
|
|
|
|
},
|
2017-05-13 18:09:42 -03:00
|
|
|
)
|
2023-12-12 16:12:07 -03:00
|
|
|
if err != nil && (res == nil || res.StatusCode != http.StatusNotFound) {
|
2023-04-06 22:58:06 -03:00
|
|
|
return fmt.Errorf("could not get %q: %w", path, err)
|
2018-02-10 13:14:52 +00:00
|
|
|
}
|
|
|
|
|
2023-04-06 22:58:06 -03:00
|
|
|
options.SHA = github.String(file.GetSHA())
|
|
|
|
if _, _, err := c.client.Repositories.UpdateFile(
|
2018-02-16 10:35:44 -02:00
|
|
|
ctx,
|
|
|
|
repo.Owner,
|
|
|
|
repo.Name,
|
|
|
|
path,
|
|
|
|
options,
|
2023-04-06 22:58:06 -03:00
|
|
|
); err != nil {
|
|
|
|
return fmt.Errorf("could not update %q: %w", path, err)
|
|
|
|
}
|
|
|
|
return nil
|
2017-05-13 18:09:42 -03:00
|
|
|
}
|
|
|
|
|
2019-06-29 16:02:40 +02:00
|
|
|
func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2018-07-08 20:47:30 -07:00
|
|
|
title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate)
|
2017-10-07 04:31:14 -05:00
|
|
|
if err != nil {
|
2019-06-29 16:02:40 +02:00
|
|
|
return "", err
|
2017-10-07 04:31:14 -05:00
|
|
|
}
|
2018-11-29 19:42:14 +01:00
|
|
|
|
2022-08-17 22:33:16 -03:00
|
|
|
if ctx.Config.Release.Draft && ctx.Config.Release.ReplaceExistingDraft {
|
2022-08-21 16:05:00 -03:00
|
|
|
if err := c.deleteExistingDraftRelease(ctx, title); err != nil {
|
2022-08-17 22:33:16 -03:00
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-11 13:31:01 +01:00
|
|
|
// Truncate the release notes if it's too long (github doesn't allow more than 125000 characters)
|
|
|
|
body = truncateReleaseBody(body)
|
|
|
|
|
2021-04-21 13:43:59 -03:00
|
|
|
data := &github.RepositoryRelease{
|
2024-02-19 13:50:47 +01:00
|
|
|
Name: github.String(title),
|
|
|
|
TagName: github.String(ctx.Git.CurrentTag),
|
|
|
|
Body: github.String(body),
|
|
|
|
// Always start with a draft release while uploading artifacts.
|
|
|
|
// PublishRelease will undraft it.
|
|
|
|
Draft: github.Bool(true),
|
2022-08-22 21:31:28 -03:00
|
|
|
Prerelease: github.Bool(ctx.PreRelease),
|
2017-05-13 18:09:42 -03:00
|
|
|
}
|
2022-08-22 21:31:28 -03:00
|
|
|
|
|
|
|
if target := ctx.Config.Release.TargetCommitish; target != "" {
|
|
|
|
target, err := tmpl.New(ctx).Apply(target)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if target != "" {
|
|
|
|
data.TargetCommitish = github.String(target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-17 22:57:42 -03:00
|
|
|
release, err := c.createOrUpdateRelease(ctx, data, body)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("could not release: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-06-29 19:42:20 -03:00
|
|
|
log.WithField("url", release.GetHTMLURL()).Info("created")
|
2022-11-17 22:57:42 -03:00
|
|
|
return strconv.FormatInt(release.GetID(), 10), nil
|
|
|
|
}
|
|
|
|
|
2024-04-03 13:56:22 -03:00
|
|
|
func (c *githubClient) PublishRelease(ctx *context.Context, releaseID string) error {
|
|
|
|
draft := ctx.Config.Release.Draft
|
|
|
|
if draft {
|
|
|
|
return nil
|
|
|
|
}
|
2024-02-19 13:50:47 +01:00
|
|
|
releaseIDInt, err := strconv.ParseInt(releaseID, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("non-numeric release ID %q: %w", releaseID, err)
|
|
|
|
}
|
2024-06-15 15:46:50 -03:00
|
|
|
data := &github.RepositoryRelease{
|
2024-04-03 13:56:22 -03:00
|
|
|
Draft: github.Bool(draft),
|
2024-06-15 15:46:50 -03:00
|
|
|
}
|
2024-06-29 19:42:20 -03:00
|
|
|
if latest := strings.TrimSpace(ctx.Config.Release.MakeLatest); latest != "" {
|
|
|
|
data.MakeLatest = github.String(latest)
|
|
|
|
}
|
2024-06-15 15:46:50 -03:00
|
|
|
if ctx.Config.Release.DiscussionCategoryName != "" {
|
|
|
|
data.DiscussionCategoryName = github.String(ctx.Config.Release.DiscussionCategoryName)
|
|
|
|
}
|
2024-06-29 19:42:20 -03:00
|
|
|
release, err := c.updateRelease(ctx, releaseIDInt, data)
|
|
|
|
if err != nil {
|
2024-02-19 13:50:47 +01:00
|
|
|
return fmt.Errorf("could not update existing release: %w", err)
|
|
|
|
}
|
2024-06-29 19:42:20 -03:00
|
|
|
log.WithField("url", release.GetHTMLURL()).Info("published")
|
2024-02-19 13:50:47 +01:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-11-17 22:57:42 -03:00
|
|
|
func (c *githubClient) createOrUpdateRelease(ctx *context.Context, data *github.RepositoryRelease, body string) (*github.RepositoryRelease, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2022-11-17 22:57:42 -03:00
|
|
|
release, _, err := c.client.Repositories.GetReleaseByTag(
|
2022-05-20 10:03:22 -03:00
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
2022-05-26 22:19:22 -03:00
|
|
|
data.GetTagName(),
|
2022-05-20 10:03:22 -03:00
|
|
|
)
|
2017-05-13 18:09:42 -03:00
|
|
|
if err != nil {
|
2022-11-29 21:38:39 -03:00
|
|
|
release, resp, err := c.client.Repositories.CreateRelease(
|
2017-05-13 18:09:42 -03:00
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
data,
|
|
|
|
)
|
fix: Handle error on failed release (github) (#5106)
When trying to release an artifact to github and it fails, I observed
the following stacktrace:
```
• publishing
• scm releases
• releasing tag=v1.11.0 repo=my-github-repo
• could not check rate limits, hoping for the best...
• could not check rate limits, hoping for the best...
• took: 1m40s
• took: 1m40s
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xe28b72]
goroutine 55 [running]:
github.com/goreleaser/goreleaser/v2/internal/client.(*githubClient).createOrUpdateRelease(0xc0001b24c8, 0xc0002c5108, 0xc000b478, {0xc00039ed00, 0x80e})
github.com/goreleaser/goreleaser/v2@v2.2.0/internal/client/github.go:454 +0x3b2
github.com/goreleaser/goreleaser/v2/internal/client.(*githubClient).CreateRelease(0xc0001b24c8, 0xc0002c5108, {0xc00039ed00, 0x80e})
github.com/goreleaser/goreleaser/v2@v2.2.0/internal/client/github.go:402 +0x339
github.com/goreleaser/goreleaser/v2/internal/pipe/release.doPublish(0xc0002c5108, {0x2ce2d40, 0xc0001b24c8})
...
```
I believe this happens because if the
[CreateRelease](https://github.com/google/go-github/blob/c96ef954c31ce1a9f74675b8ec2c66e24292ea51/github/repos_releases.go#L221)
fails, resp might be empty and the resp.Header does not exist, which
causes a segfault.
```
WithField("request-id", resp.Header.Get("X-GitHub-Request-Id")).
```
Signed-off-by: Manuel Rüger <manuel@rueg.eu>
2024-08-29 02:32:27 +02:00
|
|
|
if resp == nil {
|
|
|
|
log.WithField("name", data.GetName()).
|
|
|
|
WithError(err).
|
|
|
|
Debug("release creation failed")
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
log.WithField("name", data.GetName()).
|
|
|
|
WithField("request-id", resp.Header.Get("X-Github-Request-Id")).
|
|
|
|
WithError(err).
|
|
|
|
Debug("release creation failed")
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-06-22 22:43:57 -03:00
|
|
|
log.WithField("name", data.GetName()).
|
|
|
|
WithField("release-id", release.GetID()).
|
|
|
|
WithField("request-id", resp.Header.Get("X-GitHub-Request-Id")).
|
|
|
|
Debug("release created")
|
2022-11-17 22:57:42 -03:00
|
|
|
return release, err
|
2022-04-11 09:47:14 -03:00
|
|
|
}
|
|
|
|
|
2024-04-03 13:56:22 -03:00
|
|
|
data.Draft = release.Draft
|
2022-11-17 22:57:42 -03:00
|
|
|
data.Body = github.String(getReleaseNotes(release.GetBody(), body, ctx.Config.Release.ReleaseNotesMode))
|
|
|
|
return c.updateRelease(ctx, release.GetID(), data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *githubClient) updateRelease(ctx *context.Context, id int64, data *github.RepositoryRelease) (*github.RepositoryRelease, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2022-11-29 21:38:39 -03:00
|
|
|
release, resp, err := c.client.Repositories.EditRelease(
|
2022-11-17 22:57:42 -03:00
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
id,
|
|
|
|
data,
|
|
|
|
)
|
2024-06-22 22:43:57 -03:00
|
|
|
log.WithField("name", data.GetName()).
|
|
|
|
WithField("release-id", release.GetID()).
|
|
|
|
WithField("request-id", resp.Header.Get("X-GitHub-Request-Id")).
|
|
|
|
Debug("release updated")
|
2022-11-29 21:38:39 -03:00
|
|
|
return release, err
|
2017-05-13 18:09:42 -03:00
|
|
|
}
|
|
|
|
|
2020-07-06 14:48:17 +01:00
|
|
|
func (c *githubClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
|
2021-09-09 03:42:13 +02:00
|
|
|
downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Download)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("templating GitHub download URL: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-07-06 14:48:17 +01:00
|
|
|
return fmt.Sprintf(
|
|
|
|
"%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
|
2021-09-09 03:42:13 +02:00
|
|
|
downloadURL,
|
2020-07-06 14:48:17 +01:00
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
), nil
|
|
|
|
}
|
|
|
|
|
2024-03-19 23:25:42 -03:00
|
|
|
func (c *githubClient) deleteReleaseArtifact(ctx *context.Context, releaseID int64, name string, page int) error {
|
|
|
|
c.checkRateLimit(ctx)
|
|
|
|
log.WithField("name", name).Info("delete pre-existing asset from the release")
|
|
|
|
assets, resp, err := c.client.Repositories.ListReleaseAssets(
|
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
releaseID,
|
|
|
|
&github.ListOptions{
|
|
|
|
PerPage: 100,
|
|
|
|
Page: page,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
githubErrLogger(resp, err).
|
|
|
|
WithField("release-id", releaseID).
|
|
|
|
Warn("could not list release assets")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, asset := range assets {
|
|
|
|
if asset.GetName() != name {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
resp, err := c.client.Repositories.DeleteReleaseAsset(
|
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
asset.GetID(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
githubErrLogger(resp, err).
|
|
|
|
WithField("release-id", releaseID).
|
|
|
|
WithField("id", asset.GetID()).
|
|
|
|
WithField("name", name).
|
|
|
|
Warn("could not delete asset")
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if next := resp.NextPage; next > 0 {
|
|
|
|
return c.deleteReleaseArtifact(ctx, releaseID, name, next)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-13 18:09:42 -03:00
|
|
|
func (c *githubClient) Upload(
|
|
|
|
ctx *context.Context,
|
2019-06-29 16:02:40 +02:00
|
|
|
releaseID string,
|
2019-08-13 20:28:03 +02:00
|
|
|
artifact *artifact.Artifact,
|
2017-05-13 18:09:42 -03:00
|
|
|
file *os.File,
|
2018-02-16 10:35:44 -02:00
|
|
|
) error {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2019-06-29 16:02:40 +02:00
|
|
|
githubReleaseID, err := strconv.ParseInt(releaseID, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-03-22 17:03:31 -03:00
|
|
|
_, resp, err := c.client.Repositories.UploadReleaseAsset(
|
2017-05-13 18:09:42 -03:00
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
2019-06-29 16:02:40 +02:00
|
|
|
githubReleaseID,
|
2017-05-13 18:09:42 -03:00
|
|
|
&github.UploadOptions{
|
2019-08-13 20:28:03 +02:00
|
|
|
Name: artifact.Name,
|
2017-05-13 18:09:42 -03:00
|
|
|
},
|
|
|
|
file,
|
|
|
|
)
|
2022-11-29 21:38:39 -03:00
|
|
|
if err != nil {
|
2024-03-19 23:25:42 -03:00
|
|
|
githubErrLogger(resp, err).
|
|
|
|
WithField("name", artifact.Name).
|
2023-05-02 09:06:35 -03:00
|
|
|
WithField("release-id", releaseID).
|
|
|
|
Warn("upload failed")
|
2022-11-29 21:38:39 -03:00
|
|
|
}
|
2020-03-31 10:34:06 -03:00
|
|
|
if err == nil {
|
|
|
|
return nil
|
2020-03-22 17:03:31 -03:00
|
|
|
}
|
2024-03-19 23:25:42 -03:00
|
|
|
// this status means the asset already exists
|
2023-12-12 16:12:07 -03:00
|
|
|
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
|
2024-03-19 23:25:42 -03:00
|
|
|
if !ctx.Config.Release.ReplaceExistingArtifacts {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// if the user allowed to delete assets, we delete it, and return a
|
|
|
|
// retriable error.
|
|
|
|
if err := c.deleteReleaseArtifact(ctx, githubReleaseID, artifact.Name, 1); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return RetriableError{err}
|
2020-03-31 10:30:16 -03:00
|
|
|
}
|
2020-03-31 10:34:06 -03:00
|
|
|
return RetriableError{err}
|
2017-05-13 18:09:42 -03:00
|
|
|
}
|
2020-07-09 16:40:37 -04:00
|
|
|
|
|
|
|
// getMilestoneByTitle returns a milestone by title.
|
|
|
|
func (c *githubClient) getMilestoneByTitle(ctx *context.Context, repo Repo, title string) (*github.Milestone, error) {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2020-07-09 16:40:37 -04:00
|
|
|
// The GitHub API/SDK does not provide lookup by title functionality currently.
|
|
|
|
opts := &github.MilestoneListOptions{
|
|
|
|
ListOptions: github.ListOptions{PerPage: 100},
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
milestones, resp, err := c.client.Issues.ListMilestones(
|
|
|
|
ctx,
|
|
|
|
repo.Owner,
|
|
|
|
repo.Name,
|
|
|
|
opts,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, m := range milestones {
|
|
|
|
if m != nil && m.Title != nil && *m.Title == title {
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.NextPage == 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
opts.Page = resp.NextPage
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
2021-09-09 03:42:13 +02:00
|
|
|
|
|
|
|
func overrideGitHubClientAPI(ctx *context.Context, client *github.Client) error {
|
|
|
|
if ctx.Config.GitHubURLs.API == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.API)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("templating GitHub API URL: %w", err)
|
|
|
|
}
|
|
|
|
api, err := url.Parse(apiURL)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
uploadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Upload)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("templating GitHub upload URL: %w", err)
|
|
|
|
}
|
|
|
|
upload, err := url.Parse(uploadURL)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
client.BaseURL = api
|
|
|
|
client.UploadURL = upload
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-08-17 22:33:16 -03:00
|
|
|
|
2022-08-21 16:05:00 -03:00
|
|
|
func (c *githubClient) deleteExistingDraftRelease(ctx *context.Context, name string) error {
|
2023-05-27 00:15:43 -03:00
|
|
|
c.checkRateLimit(ctx)
|
2022-08-17 22:33:16 -03:00
|
|
|
opt := github.ListOptions{PerPage: 50}
|
|
|
|
for {
|
|
|
|
releases, resp, err := c.client.Repositories.ListReleases(
|
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
&opt,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not delete existing drafts: %w", err)
|
|
|
|
}
|
|
|
|
for _, r := range releases {
|
|
|
|
if r.GetDraft() && r.GetName() == name {
|
|
|
|
if _, err := c.client.Repositories.DeleteRelease(
|
|
|
|
ctx,
|
|
|
|
ctx.Config.Release.GitHub.Owner,
|
|
|
|
ctx.Config.Release.GitHub.Name,
|
|
|
|
r.GetID(),
|
|
|
|
); err != nil {
|
|
|
|
return fmt.Errorf("could not delete previous draft release: %w", err)
|
|
|
|
}
|
2022-08-21 16:02:36 -03:00
|
|
|
|
2023-05-02 09:06:35 -03:00
|
|
|
log.WithField("commit", r.GetTargetCommitish()).
|
|
|
|
WithField("tag", r.GetTagName()).
|
|
|
|
WithField("name", r.GetName()).
|
|
|
|
Info("deleted previous draft release")
|
2022-08-21 16:02:36 -03:00
|
|
|
|
|
|
|
// in theory, there should be only 1 release matching, so we can just return
|
|
|
|
return nil
|
2022-08-17 22:33:16 -03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
opt.Page = resp.NextPage
|
|
|
|
}
|
|
|
|
}
|
2024-03-19 23:25:42 -03:00
|
|
|
|
|
|
|
func githubErrLogger(resp *github.Response, err error) *log.Entry {
|
|
|
|
requestID := ""
|
|
|
|
if resp != nil {
|
|
|
|
requestID = resp.Header.Get("X-GitHub-Request-Id")
|
|
|
|
}
|
|
|
|
return log.WithField("request-id", requestID).WithError(err)
|
|
|
|
}
|