github.com/goreleaser/goreleaser@v1.25.1/internal/client/github.go (about)

     1  package client
     2  
     3  import (
     4  	"crypto/tls"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"reflect"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/caarlos0/log"
    16  	"github.com/charmbracelet/x/exp/ordered"
    17  	"github.com/google/go-github/v61/github"
    18  	"github.com/goreleaser/goreleaser/internal/artifact"
    19  	"github.com/goreleaser/goreleaser/internal/tmpl"
    20  	"github.com/goreleaser/goreleaser/pkg/config"
    21  	"github.com/goreleaser/goreleaser/pkg/context"
    22  	"golang.org/x/oauth2"
    23  )
    24  
    25  const DefaultGitHubDownloadURL = "https://github.com"
    26  
    27  var (
    28  	_ Client                = &githubClient{}
    29  	_ ReleaseNotesGenerator = &githubClient{}
    30  	_ PullRequestOpener     = &githubClient{}
    31  	_ ForkSyncer            = &githubClient{}
    32  )
    33  
    34  type githubClient struct {
    35  	client *github.Client
    36  }
    37  
    38  // NewGitHubReleaseNotesGenerator returns a GitHub client that can generate
    39  // changelogs.
    40  func NewGitHubReleaseNotesGenerator(ctx *context.Context, token string) (ReleaseNotesGenerator, error) {
    41  	return newGitHub(ctx, token)
    42  }
    43  
    44  // newGitHub returns a github client implementation.
    45  func newGitHub(ctx *context.Context, token string) (*githubClient, error) {
    46  	ts := oauth2.StaticTokenSource(
    47  		&oauth2.Token{AccessToken: token},
    48  	)
    49  
    50  	httpClient := oauth2.NewClient(ctx, ts)
    51  	base := httpClient.Transport.(*oauth2.Transport).Base
    52  	if base == nil || reflect.ValueOf(base).IsNil() {
    53  		base = http.DefaultTransport
    54  	}
    55  	// nolint: gosec
    56  	base.(*http.Transport).TLSClientConfig = &tls.Config{
    57  		InsecureSkipVerify: ctx.Config.GitHubURLs.SkipTLSVerify,
    58  	}
    59  	base.(*http.Transport).Proxy = http.ProxyFromEnvironment
    60  	httpClient.Transport.(*oauth2.Transport).Base = base
    61  
    62  	client := github.NewClient(httpClient)
    63  	err := overrideGitHubClientAPI(ctx, client)
    64  	if err != nil {
    65  		return &githubClient{}, err
    66  	}
    67  
    68  	return &githubClient{client: client}, nil
    69  }
    70  
    71  func (c *githubClient) checkRateLimit(ctx *context.Context) {
    72  	limits, _, err := c.client.RateLimit.Get(ctx)
    73  	if err != nil {
    74  		log.Warn("could not check rate limits, hoping for the best...")
    75  		return
    76  	}
    77  	if limits.Core.Remaining > 100 { // 100 should be safe enough
    78  		return
    79  	}
    80  	sleep := limits.Core.Reset.UTC().Sub(time.Now().UTC())
    81  	if sleep <= 0 {
    82  		// it seems that sometimes, after the rate limit just reset, it might
    83  		// still get <100 remaining and a reset time in the past... in such
    84  		// cases we can probably sleep a bit more before trying again...
    85  		sleep = 15 * time.Second
    86  	}
    87  	log.Warnf("token too close to rate limiting, will sleep for %s before continuing...", sleep)
    88  	time.Sleep(sleep)
    89  	c.checkRateLimit(ctx)
    90  }
    91  
    92  func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, prev, current string) (string, error) {
    93  	c.checkRateLimit(ctx)
    94  	notes, _, err := c.client.Repositories.GenerateReleaseNotes(ctx, repo.Owner, repo.Name, &github.GenerateNotesOptions{
    95  		TagName:         current,
    96  		PreviousTagName: github.String(prev),
    97  	})
    98  	if err != nil {
    99  		return "", err
   100  	}
   101  	return notes.Body, err
   102  }
   103  
   104  func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
   105  	c.checkRateLimit(ctx)
   106  	var log []string
   107  	opts := &github.ListOptions{PerPage: 100}
   108  
   109  	for {
   110  		result, resp, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current, opts)
   111  		if err != nil {
   112  			return "", err
   113  		}
   114  		for _, commit := range result.Commits {
   115  			log = append(log, fmt.Sprintf(
   116  				"%s: %s (@%s)",
   117  				commit.GetSHA(),
   118  				strings.Split(commit.Commit.GetMessage(), "\n")[0],
   119  				commit.GetAuthor().GetLogin(),
   120  			))
   121  		}
   122  		if resp.NextPage == 0 {
   123  			break
   124  		}
   125  		opts.Page = resp.NextPage
   126  	}
   127  
   128  	return strings.Join(log, "\n"), nil
   129  }
   130  
   131  // getDefaultBranch returns the default branch of a github repo
   132  func (c *githubClient) getDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
   133  	c.checkRateLimit(ctx)
   134  	p, res, err := c.client.Repositories.Get(ctx, repo.Owner, repo.Name)
   135  	if err != nil {
   136  		log := log.WithField("projectID", repo.String())
   137  		if res != nil {
   138  			log = log.WithField("statusCode", res.StatusCode)
   139  		}
   140  		log.
   141  			WithError(err).
   142  			Warn("error checking for default branch")
   143  		return "", err
   144  	}
   145  	return p.GetDefaultBranch(), nil
   146  }
   147  
   148  // CloseMilestone closes a given milestone.
   149  func (c *githubClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
   150  	c.checkRateLimit(ctx)
   151  	milestone, err := c.getMilestoneByTitle(ctx, repo, title)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	if milestone == nil {
   157  		return ErrNoMilestoneFound{Title: title}
   158  	}
   159  
   160  	closedState := "closed"
   161  	milestone.State = &closedState
   162  
   163  	_, _, err = c.client.Issues.EditMilestone(
   164  		ctx,
   165  		repo.Owner,
   166  		repo.Name,
   167  		*milestone.Number,
   168  		milestone,
   169  	)
   170  
   171  	return err
   172  }
   173  
   174  func headString(base, head Repo) string {
   175  	return strings.Join([]string{
   176  		ordered.First(head.Owner, base.Owner),
   177  		ordered.First(head.Name, base.Name),
   178  		ordered.First(head.Branch, base.Branch),
   179  	}, ":")
   180  }
   181  
   182  func (c *githubClient) getPRTemplate(ctx *context.Context, repo Repo) (string, error) {
   183  	content, _, _, err := c.client.Repositories.GetContents(
   184  		ctx, repo.Owner, repo.Name,
   185  		".github/PULL_REQUEST_TEMPLATE.md",
   186  		&github.RepositoryContentGetOptions{
   187  			Ref: repo.Branch,
   188  		},
   189  	)
   190  	if err != nil {
   191  		return "", err
   192  	}
   193  	return content.GetContent()
   194  }
   195  
   196  const prFooter = "###### Automated with [GoReleaser](https://goreleaser.com)"
   197  
   198  func (c *githubClient) OpenPullRequest(
   199  	ctx *context.Context,
   200  	base, head Repo,
   201  	title string,
   202  	draft bool,
   203  ) error {
   204  	c.checkRateLimit(ctx)
   205  	base.Owner = ordered.First(base.Owner, head.Owner)
   206  	base.Name = ordered.First(base.Name, head.Name)
   207  	if base.Branch == "" {
   208  		def, err := c.getDefaultBranch(ctx, base)
   209  		if err != nil {
   210  			return err
   211  		}
   212  		base.Branch = def
   213  	}
   214  	tpl, err := c.getPRTemplate(ctx, base)
   215  	if err != nil {
   216  		log.WithError(err).Debug("no pull request template found...")
   217  	}
   218  	if len(tpl) > 0 {
   219  		log.Info("got a pr template")
   220  	}
   221  
   222  	log := log.
   223  		WithField("base", headString(base, Repo{})).
   224  		WithField("head", headString(base, head)).
   225  		WithField("draft", draft)
   226  	log.Info("opening pull request")
   227  	pr, res, err := c.client.PullRequests.Create(
   228  		ctx,
   229  		base.Owner,
   230  		base.Name,
   231  		&github.NewPullRequest{
   232  			Title: github.String(title),
   233  			Base:  github.String(base.Branch),
   234  			Head:  github.String(headString(base, head)),
   235  			Body:  github.String(strings.Join([]string{tpl, prFooter}, "\n")),
   236  			Draft: github.Bool(draft),
   237  		},
   238  	)
   239  	if err != nil {
   240  		if res.StatusCode == http.StatusUnprocessableEntity {
   241  			log.WithError(err).Warn("pull request validation failed")
   242  			return nil
   243  		}
   244  		return fmt.Errorf("could not create pull request: %w", err)
   245  	}
   246  	log.WithField("url", pr.GetHTMLURL()).Info("pull request created")
   247  	return nil
   248  }
   249  
   250  func (c *githubClient) SyncFork(ctx *context.Context, head, base Repo) error {
   251  	branch := base.Branch
   252  	if branch == "" {
   253  		def, err := c.getDefaultBranch(ctx, base)
   254  		if err != nil {
   255  			return err
   256  		}
   257  		branch = def
   258  	}
   259  	res, _, err := c.client.Repositories.MergeUpstream(
   260  		ctx,
   261  		head.Owner,
   262  		head.Name,
   263  		&github.RepoMergeUpstreamRequest{
   264  			Branch: github.String(branch),
   265  		},
   266  	)
   267  	if res != nil {
   268  		log.WithField("merge_type", res.GetMergeType()).
   269  			WithField("base_branch", res.GetBaseBranch()).
   270  			Info(res.GetMessage())
   271  	}
   272  	return err
   273  }
   274  
   275  func (c *githubClient) CreateFile(
   276  	ctx *context.Context,
   277  	commitAuthor config.CommitAuthor,
   278  	repo Repo,
   279  	content []byte,
   280  	path,
   281  	message string,
   282  ) error {
   283  	c.checkRateLimit(ctx)
   284  	defBranch, err := c.getDefaultBranch(ctx, repo)
   285  	if err != nil {
   286  		return fmt.Errorf("could not get default branch: %w", err)
   287  	}
   288  
   289  	branch := repo.Branch
   290  	if branch == "" {
   291  		branch = defBranch
   292  	}
   293  
   294  	options := &github.RepositoryContentFileOptions{
   295  		Committer: &github.CommitAuthor{
   296  			Name:  github.String(commitAuthor.Name),
   297  			Email: github.String(commitAuthor.Email),
   298  		},
   299  		Content: content,
   300  		Message: github.String(message),
   301  	}
   302  
   303  	// Set the branch if we got it above...otherwise, just default to
   304  	// whatever the SDK does auto-magically
   305  	if branch != "" {
   306  		options.Branch = &branch
   307  	}
   308  
   309  	log.
   310  		WithField("repository", repo.String()).
   311  		WithField("branch", repo.Branch).
   312  		WithField("file", path).
   313  		Info("pushing")
   314  
   315  	if defBranch != branch && branch != "" {
   316  		_, res, err := c.client.Repositories.GetBranch(ctx, repo.Owner, repo.Name, branch, 100)
   317  		if err != nil && (res == nil || res.StatusCode != http.StatusNotFound) {
   318  			return fmt.Errorf("could not get branch %q: %w", branch, err)
   319  		}
   320  
   321  		if res.StatusCode == http.StatusNotFound {
   322  			defRef, _, err := c.client.Git.GetRef(ctx, repo.Owner, repo.Name, "refs/heads/"+defBranch)
   323  			if err != nil {
   324  				return fmt.Errorf("could not get ref %q: %w", "refs/heads/"+defBranch, err)
   325  			}
   326  
   327  			if _, _, err := c.client.Git.CreateRef(ctx, repo.Owner, repo.Name, &github.Reference{
   328  				Ref: github.String("refs/heads/" + branch),
   329  				Object: &github.GitObject{
   330  					SHA: defRef.Object.SHA,
   331  				},
   332  			}); err != nil {
   333  				rerr := new(github.ErrorResponse)
   334  				if !errors.As(err, &rerr) || rerr.Message != "Reference already exists" {
   335  					return fmt.Errorf("could not create ref %q from %q: %w", "refs/heads/"+branch, defRef.Object.GetSHA(), err)
   336  				}
   337  			}
   338  		}
   339  	}
   340  
   341  	file, _, res, err := c.client.Repositories.GetContents(
   342  		ctx,
   343  		repo.Owner,
   344  		repo.Name,
   345  		path,
   346  		&github.RepositoryContentGetOptions{
   347  			Ref: branch,
   348  		},
   349  	)
   350  	if err != nil && (res == nil || res.StatusCode != http.StatusNotFound) {
   351  		return fmt.Errorf("could not get %q: %w", path, err)
   352  	}
   353  
   354  	options.SHA = github.String(file.GetSHA())
   355  	if _, _, err := c.client.Repositories.UpdateFile(
   356  		ctx,
   357  		repo.Owner,
   358  		repo.Name,
   359  		path,
   360  		options,
   361  	); err != nil {
   362  		return fmt.Errorf("could not update %q: %w", path, err)
   363  	}
   364  	return nil
   365  }
   366  
   367  func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string, error) {
   368  	c.checkRateLimit(ctx)
   369  	title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate)
   370  	if err != nil {
   371  		return "", err
   372  	}
   373  
   374  	if ctx.Config.Release.Draft && ctx.Config.Release.ReplaceExistingDraft {
   375  		if err := c.deleteExistingDraftRelease(ctx, title); err != nil {
   376  			return "", err
   377  		}
   378  	}
   379  
   380  	// Truncate the release notes if it's too long (github doesn't allow more than 125000 characters)
   381  	body = truncateReleaseBody(body)
   382  
   383  	data := &github.RepositoryRelease{
   384  		Name:    github.String(title),
   385  		TagName: github.String(ctx.Git.CurrentTag),
   386  		Body:    github.String(body),
   387  		// Always start with a draft release while uploading artifacts.
   388  		// PublishRelease will undraft it.
   389  		Draft:      github.Bool(true),
   390  		Prerelease: github.Bool(ctx.PreRelease),
   391  		MakeLatest: github.String("true"),
   392  	}
   393  
   394  	if ctx.Config.Release.DiscussionCategoryName != "" {
   395  		data.DiscussionCategoryName = github.String(ctx.Config.Release.DiscussionCategoryName)
   396  	}
   397  
   398  	if target := ctx.Config.Release.TargetCommitish; target != "" {
   399  		target, err := tmpl.New(ctx).Apply(target)
   400  		if err != nil {
   401  			return "", err
   402  		}
   403  		if target != "" {
   404  			data.TargetCommitish = github.String(target)
   405  		}
   406  	}
   407  
   408  	if latest := strings.TrimSpace(ctx.Config.Release.MakeLatest); latest == "false" {
   409  		data.MakeLatest = github.String(latest)
   410  	}
   411  
   412  	release, err := c.createOrUpdateRelease(ctx, data, body)
   413  	if err != nil {
   414  		return "", fmt.Errorf("could not release: %w", err)
   415  	}
   416  
   417  	return strconv.FormatInt(release.GetID(), 10), nil
   418  }
   419  
   420  func (c *githubClient) PublishRelease(ctx *context.Context, releaseID string) error {
   421  	draft := ctx.Config.Release.Draft
   422  	if draft {
   423  		return nil
   424  	}
   425  	releaseIDInt, err := strconv.ParseInt(releaseID, 10, 64)
   426  	if err != nil {
   427  		return fmt.Errorf("non-numeric release ID %q: %w", releaseID, err)
   428  	}
   429  	if _, err := c.updateRelease(ctx, releaseIDInt, &github.RepositoryRelease{
   430  		Draft: github.Bool(draft),
   431  	}); err != nil {
   432  		return fmt.Errorf("could not update existing release: %w", err)
   433  	}
   434  	return nil
   435  }
   436  
   437  func (c *githubClient) createOrUpdateRelease(ctx *context.Context, data *github.RepositoryRelease, body string) (*github.RepositoryRelease, error) {
   438  	c.checkRateLimit(ctx)
   439  	release, _, err := c.client.Repositories.GetReleaseByTag(
   440  		ctx,
   441  		ctx.Config.Release.GitHub.Owner,
   442  		ctx.Config.Release.GitHub.Name,
   443  		data.GetTagName(),
   444  	)
   445  	if err != nil {
   446  		release, resp, err := c.client.Repositories.CreateRelease(
   447  			ctx,
   448  			ctx.Config.Release.GitHub.Owner,
   449  			ctx.Config.Release.GitHub.Name,
   450  			data,
   451  		)
   452  		if err == nil {
   453  			log.WithField("name", data.GetName()).
   454  				WithField("release-id", release.GetID()).
   455  				WithField("request-id", resp.Header.Get("X-GitHub-Request-Id")).
   456  				Info("release created")
   457  		}
   458  		return release, err
   459  	}
   460  
   461  	data.Draft = release.Draft
   462  	data.Body = github.String(getReleaseNotes(release.GetBody(), body, ctx.Config.Release.ReleaseNotesMode))
   463  	return c.updateRelease(ctx, release.GetID(), data)
   464  }
   465  
   466  func (c *githubClient) updateRelease(ctx *context.Context, id int64, data *github.RepositoryRelease) (*github.RepositoryRelease, error) {
   467  	c.checkRateLimit(ctx)
   468  	release, resp, err := c.client.Repositories.EditRelease(
   469  		ctx,
   470  		ctx.Config.Release.GitHub.Owner,
   471  		ctx.Config.Release.GitHub.Name,
   472  		id,
   473  		data,
   474  	)
   475  	if err == nil {
   476  		log.WithField("name", data.GetName()).
   477  			WithField("release-id", release.GetID()).
   478  			WithField("request-id", resp.Header.Get("X-GitHub-Request-Id")).
   479  			Info("release updated")
   480  	}
   481  	return release, err
   482  }
   483  
   484  func (c *githubClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
   485  	downloadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Download)
   486  	if err != nil {
   487  		return "", fmt.Errorf("templating GitHub download URL: %w", err)
   488  	}
   489  
   490  	return fmt.Sprintf(
   491  		"%s/%s/%s/releases/download/{{ .Tag }}/{{ .ArtifactName }}",
   492  		downloadURL,
   493  		ctx.Config.Release.GitHub.Owner,
   494  		ctx.Config.Release.GitHub.Name,
   495  	), nil
   496  }
   497  
   498  func (c *githubClient) deleteReleaseArtifact(ctx *context.Context, releaseID int64, name string, page int) error {
   499  	c.checkRateLimit(ctx)
   500  	log.WithField("name", name).Info("delete pre-existing asset from the release")
   501  	assets, resp, err := c.client.Repositories.ListReleaseAssets(
   502  		ctx,
   503  		ctx.Config.Release.GitHub.Owner,
   504  		ctx.Config.Release.GitHub.Name,
   505  		releaseID,
   506  		&github.ListOptions{
   507  			PerPage: 100,
   508  			Page:    page,
   509  		},
   510  	)
   511  	if err != nil {
   512  		githubErrLogger(resp, err).
   513  			WithField("release-id", releaseID).
   514  			Warn("could not list release assets")
   515  		return err
   516  	}
   517  	for _, asset := range assets {
   518  		if asset.GetName() != name {
   519  			continue
   520  		}
   521  		resp, err := c.client.Repositories.DeleteReleaseAsset(
   522  			ctx,
   523  			ctx.Config.Release.GitHub.Owner,
   524  			ctx.Config.Release.GitHub.Name,
   525  			asset.GetID(),
   526  		)
   527  		if err != nil {
   528  			githubErrLogger(resp, err).
   529  				WithField("release-id", releaseID).
   530  				WithField("id", asset.GetID()).
   531  				WithField("name", name).
   532  				Warn("could not delete asset")
   533  		}
   534  		return err
   535  	}
   536  	if next := resp.NextPage; next > 0 {
   537  		return c.deleteReleaseArtifact(ctx, releaseID, name, next)
   538  	}
   539  	return nil
   540  }
   541  
   542  func (c *githubClient) Upload(
   543  	ctx *context.Context,
   544  	releaseID string,
   545  	artifact *artifact.Artifact,
   546  	file *os.File,
   547  ) error {
   548  	c.checkRateLimit(ctx)
   549  	githubReleaseID, err := strconv.ParseInt(releaseID, 10, 64)
   550  	if err != nil {
   551  		return err
   552  	}
   553  	_, resp, err := c.client.Repositories.UploadReleaseAsset(
   554  		ctx,
   555  		ctx.Config.Release.GitHub.Owner,
   556  		ctx.Config.Release.GitHub.Name,
   557  		githubReleaseID,
   558  		&github.UploadOptions{
   559  			Name: artifact.Name,
   560  		},
   561  		file,
   562  	)
   563  	if err != nil {
   564  		githubErrLogger(resp, err).
   565  			WithField("name", artifact.Name).
   566  			WithField("release-id", releaseID).
   567  			Warn("upload failed")
   568  	}
   569  	if err == nil {
   570  		return nil
   571  	}
   572  	// this status means the asset already exists
   573  	if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
   574  		if !ctx.Config.Release.ReplaceExistingArtifacts {
   575  			return err
   576  		}
   577  		// if the user allowed to delete assets, we delete it, and return a
   578  		// retriable error.
   579  		if err := c.deleteReleaseArtifact(ctx, githubReleaseID, artifact.Name, 1); err != nil {
   580  			return err
   581  		}
   582  		return RetriableError{err}
   583  	}
   584  	return RetriableError{err}
   585  }
   586  
   587  // getMilestoneByTitle returns a milestone by title.
   588  func (c *githubClient) getMilestoneByTitle(ctx *context.Context, repo Repo, title string) (*github.Milestone, error) {
   589  	c.checkRateLimit(ctx)
   590  	// The GitHub API/SDK does not provide lookup by title functionality currently.
   591  	opts := &github.MilestoneListOptions{
   592  		ListOptions: github.ListOptions{PerPage: 100},
   593  	}
   594  
   595  	for {
   596  		milestones, resp, err := c.client.Issues.ListMilestones(
   597  			ctx,
   598  			repo.Owner,
   599  			repo.Name,
   600  			opts,
   601  		)
   602  		if err != nil {
   603  			return nil, err
   604  		}
   605  
   606  		for _, m := range milestones {
   607  			if m != nil && m.Title != nil && *m.Title == title {
   608  				return m, nil
   609  			}
   610  		}
   611  
   612  		if resp.NextPage == 0 {
   613  			break
   614  		}
   615  
   616  		opts.Page = resp.NextPage
   617  	}
   618  
   619  	return nil, nil
   620  }
   621  
   622  func overrideGitHubClientAPI(ctx *context.Context, client *github.Client) error {
   623  	if ctx.Config.GitHubURLs.API == "" {
   624  		return nil
   625  	}
   626  
   627  	apiURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.API)
   628  	if err != nil {
   629  		return fmt.Errorf("templating GitHub API URL: %w", err)
   630  	}
   631  	api, err := url.Parse(apiURL)
   632  	if err != nil {
   633  		return err
   634  	}
   635  
   636  	uploadURL, err := tmpl.New(ctx).Apply(ctx.Config.GitHubURLs.Upload)
   637  	if err != nil {
   638  		return fmt.Errorf("templating GitHub upload URL: %w", err)
   639  	}
   640  	upload, err := url.Parse(uploadURL)
   641  	if err != nil {
   642  		return err
   643  	}
   644  
   645  	client.BaseURL = api
   646  	client.UploadURL = upload
   647  
   648  	return nil
   649  }
   650  
   651  func (c *githubClient) deleteExistingDraftRelease(ctx *context.Context, name string) error {
   652  	c.checkRateLimit(ctx)
   653  	opt := github.ListOptions{PerPage: 50}
   654  	for {
   655  		releases, resp, err := c.client.Repositories.ListReleases(
   656  			ctx,
   657  			ctx.Config.Release.GitHub.Owner,
   658  			ctx.Config.Release.GitHub.Name,
   659  			&opt,
   660  		)
   661  		if err != nil {
   662  			return fmt.Errorf("could not delete existing drafts: %w", err)
   663  		}
   664  		for _, r := range releases {
   665  			if r.GetDraft() && r.GetName() == name {
   666  				if _, err := c.client.Repositories.DeleteRelease(
   667  					ctx,
   668  					ctx.Config.Release.GitHub.Owner,
   669  					ctx.Config.Release.GitHub.Name,
   670  					r.GetID(),
   671  				); err != nil {
   672  					return fmt.Errorf("could not delete previous draft release: %w", err)
   673  				}
   674  
   675  				log.WithField("commit", r.GetTargetCommitish()).
   676  					WithField("tag", r.GetTagName()).
   677  					WithField("name", r.GetName()).
   678  					Info("deleted previous draft release")
   679  
   680  				// in theory, there should be only 1 release matching, so we can just return
   681  				return nil
   682  			}
   683  		}
   684  		if resp.NextPage == 0 {
   685  			return nil
   686  		}
   687  		opt.Page = resp.NextPage
   688  	}
   689  }
   690  
   691  func githubErrLogger(resp *github.Response, err error) *log.Entry {
   692  	requestID := ""
   693  	if resp != nil {
   694  		requestID = resp.Header.Get("X-GitHub-Request-Id")
   695  	}
   696  	return log.WithField("request-id", requestID).WithError(err)
   697  }