1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-24 04:16:27 +02:00
Carlos Alexandro Becker 95bba02211
feat: delay github tag creation (#3330)
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>
2022-08-21 15:52:10 -03:00

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
}
}