1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-08 03:31:59 +02:00
goreleaser/internal/client/github.go
Carlos Alexandro Becker 50bd78439c
fix: log request-id on upload error and release create/update (#3608)
this should help when reporting issues to github, especially the magical
"failed successfully" error

Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
2022-11-29 21:38:39 -03:00

445 lines
11 KiB
Go

package client
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"github.com/caarlos0/log"
"github.com/google/go-github/v48/github"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"golang.org/x/oauth2"
)
const DefaultGitHubDownloadURL = "https://github.com"
type githubClient struct {
client *github.Client
}
// NewGitHub returns a github client implementation.
func NewGitHub(ctx *context.Context, token string) (GitHubClient, error) {
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
httpClient := oauth2.NewClient(ctx, ts)
base := httpClient.Transport.(*oauth2.Transport).Base
if base == nil || reflect.ValueOf(base).IsNil() {
base = http.DefaultTransport
}
// nolint: gosec
base.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: ctx.Config.GitHubURLs.SkipTLSVerify,
}
base.(*http.Transport).Proxy = http.ProxyFromEnvironment
httpClient.Transport.(*oauth2.Transport).Base = base
client := github.NewClient(httpClient)
err := overrideGitHubClientAPI(ctx, client)
if err != nil {
return &githubClient{}, err
}
return &githubClient{client: client}, nil
}
func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, prev, current string) (string, error) {
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
}
func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
var log []string
opts := &github.ListOptions{PerPage: 100}
for {
result, resp, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current, opts)
if err != nil {
return "", err
}
for _, commit := range result.Commits {
log = append(log, fmt.Sprintf(
"%s: %s (@%s)",
commit.GetSHA(),
strings.Split(commit.Commit.GetMessage(), "\n")[0],
commit.GetAuthor().GetLogin(),
))
}
if resp.NextPage == 0 {
break
}
opts.Page = resp.NextPage
}
return strings.Join(log, "\n"), nil
}
// GetDefaultBranch returns the default branch of a github repo
func (c *githubClient) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
p, res, err := c.client.Repositories.Get(ctx, repo.Owner, repo.Name)
if err != nil {
log.WithFields(log.Fields{
"projectID": repo.String(),
"statusCode": res.StatusCode,
"err": err.Error(),
}).Warn("error checking for default branch")
return "", err
}
return p.GetDefaultBranch(), nil
}
// CloseMilestone closes a given milestone.
func (c *githubClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
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
}
func (c *githubClient) CreateFile(
ctx *context.Context,
commitAuthor config.CommitAuthor,
repo Repo,
content []byte,
path,
message string,
) error {
var branch string
var err error
if repo.Branch != "" {
branch = repo.Branch
} else {
branch, err = c.GetDefaultBranch(ctx, repo)
if err != nil {
// Fall back to sdk default
log.WithFields(log.Fields{
"fileName": path,
"projectID": repo.String(),
"requestedBranch": branch,
"err": err.Error(),
}).Warn("error checking for default branch, using master")
}
}
options := &github.RepositoryContentFileOptions{
Committer: &github.CommitAuthor{
Name: github.String(commitAuthor.Name),
Email: github.String(commitAuthor.Email),
},
Content: content,
Message: github.String(message),
}
// Set the branch if we got it above...otherwise, just default to
// whatever the SDK does auto-magically
if branch != "" {
options.Branch = &branch
}
file, _, res, err := c.client.Repositories.GetContents(
ctx,
repo.Owner,
repo.Name,
path,
&github.RepositoryContentGetOptions{},
)
if err != nil && (res == nil || res.StatusCode != 404) {
return err
}
if res.StatusCode == 404 {
_, _, err = c.client.Repositories.CreateFile(
ctx,
repo.Owner,
repo.Name,
path,
options,
)
return err
}
options.SHA = file.SHA
_, _, err = c.client.Repositories.UpdateFile(
ctx,
repo.Owner,
repo.Name,
path,
options,
)
return err
}
func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string, error) {
title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate)
if err != nil {
return "", err
}
if ctx.Config.Release.Draft && ctx.Config.Release.ReplaceExistingDraft {
if err := c.deleteExistingDraftRelease(ctx, title); err != nil {
return "", err
}
}
// Truncate the release notes if it's too long (github doesn't allow more than 125000 characters)
body = truncateReleaseBody(body)
data := &github.RepositoryRelease{
Name: github.String(title),
TagName: github.String(ctx.Git.CurrentTag),
Body: github.String(body),
Draft: github.Bool(ctx.Config.Release.Draft),
Prerelease: github.Bool(ctx.PreRelease),
}
if ctx.Config.Release.DiscussionCategoryName != "" {
data.DiscussionCategoryName = github.String(ctx.Config.Release.DiscussionCategoryName)
}
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)
}
}
release, err := c.createOrUpdateRelease(ctx, data, body)
if err != nil {
return "", fmt.Errorf("could not release: %w", err)
}
return strconv.FormatInt(release.GetID(), 10), nil
}
func (c *githubClient) createOrUpdateRelease(ctx *context.Context, data *github.RepositoryRelease, body string) (*github.RepositoryRelease, error) {
release, _, err := c.client.Repositories.GetReleaseByTag(
ctx,
ctx.Config.Release.GitHub.Owner,
ctx.Config.Release.GitHub.Name,
data.GetTagName(),
)
if err != nil {
release, resp, err := c.client.Repositories.CreateRelease(
ctx,
ctx.Config.Release.GitHub.Owner,
ctx.Config.Release.GitHub.Name,
data,
)
if err == nil {
log.WithFields(log.Fields{
"name": data.GetName(),
"release-id": release.GetID(),
"request-id": resp.Header.Get("X-GitHub-Request-Id"),
}).Info("release created")
}
return release, err
}
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) {
release, resp, err := c.client.Repositories.EditRelease(
ctx,
ctx.Config.Release.GitHub.Owner,
ctx.Config.Release.GitHub.Name,
id,
data,
)
if err == nil {
log.WithFields(log.Fields{
"name": data.GetName(),
"release-id": release.GetID(),
"request-id": resp.Header.Get("X-GitHub-Request-Id"),
}).Info("release updated")
}
return release, err
}
func (c *githubClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Download)
if err != nil {
return "", fmt.Errorf("templating GitHub download URL: %w", err)
}
return fmt.Sprintf(
"%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
downloadURL,
ctx.Config.Release.GitHub.Owner,
ctx.Config.Release.GitHub.Name,
), nil
}
func (c *githubClient) Upload(
ctx *context.Context,
releaseID string,
artifact *artifact.Artifact,
file *os.File,
) error {
githubReleaseID, err := strconv.ParseInt(releaseID, 10, 64)
if err != nil {
return err
}
_, resp, err := c.client.Repositories.UploadReleaseAsset(
ctx,
ctx.Config.Release.GitHub.Owner,
ctx.Config.Release.GitHub.Name,
githubReleaseID,
&github.UploadOptions{
Name: artifact.Name,
},
file,
)
if err != nil {
log.WithFields(log.Fields{
"name": artifact.Name,
"release-id": releaseID,
"request-id": resp.Header.Get("X-GitHub-Request-Id"),
}).Warn("upload failed")
}
if err == nil {
return nil
}
if resp != nil && resp.StatusCode == 422 {
return err
}
return RetriableError{err}
}
// getMilestoneByTitle returns a milestone by title.
func (c *githubClient) getMilestoneByTitle(ctx *context.Context, repo Repo, title string) (*github.Milestone, error) {
// 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
}
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
}
func (c *githubClient) deleteExistingDraftRelease(ctx *context.Context, name string) error {
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)
}
log.WithFields(log.Fields{
"commit": r.GetTargetCommitish(),
"tag": r.GetTagName(),
"name": r.GetName(),
}).Info("deleted previous draft release")
// in theory, there should be only 1 release matching, so we can just return
return nil
}
}
if resp.NextPage == 0 {
return nil
}
opt.Page = resp.NextPage
}
}