mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-01-24 04:16:27 +02:00
95bba02211
closes #3044 What happens is that, if the tag is created only locally, it'll create the tag in the remote upon creating the release. The tag still needs to be created locally though! @bep let me know if that works for you. Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
398 lines
9.5 KiB
Go
398 lines
9.5 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/v46/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) {
|
|
var release *github.RepositoryRelease
|
|
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.deleteExistedDraftRelease(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),
|
|
TargetCommitish: github.String(ctx.Git.Commit),
|
|
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)
|
|
}
|
|
|
|
release, _, err = c.client.Repositories.GetReleaseByTag(
|
|
ctx,
|
|
ctx.Config.Release.GitHub.Owner,
|
|
ctx.Config.Release.GitHub.Name,
|
|
data.GetTagName(),
|
|
)
|
|
if err != nil {
|
|
release, _, err = c.client.Repositories.CreateRelease(
|
|
ctx,
|
|
ctx.Config.Release.GitHub.Owner,
|
|
ctx.Config.Release.GitHub.Name,
|
|
data,
|
|
)
|
|
} else {
|
|
data.Body = github.String(getReleaseNotes(release.GetBody(), body, ctx.Config.Release.ReleaseNotesMode))
|
|
release, _, err = c.client.Repositories.EditRelease(
|
|
ctx,
|
|
ctx.Config.Release.GitHub.Owner,
|
|
ctx.Config.Release.GitHub.Name,
|
|
release.GetID(),
|
|
data,
|
|
)
|
|
}
|
|
if err != nil {
|
|
log.WithField("url", release.GetHTMLURL()).Info("release updated")
|
|
}
|
|
|
|
githubReleaseID := strconv.FormatInt(release.GetID(), 10)
|
|
return githubReleaseID, 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 {
|
|
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) deleteExistedDraftRelease(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)
|
|
}
|
|
}
|
|
}
|
|
if resp.NextPage == 0 {
|
|
return nil
|
|
}
|
|
opt.Page = resp.NextPage
|
|
}
|
|
}
|