1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-11-29 23:07:42 +02:00

feat(github): avoid rate limits (#4037)

before doing anything, check the `/rate_limits` API... if we have less
than 100 remaining, wait for the reset timer.

closes #4028
This commit is contained in:
Carlos Alexandro Becker
2023-05-27 00:15:43 -03:00
committed by GitHub
parent 051381837d
commit 1f8a7b2fc5
2 changed files with 120 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import (
"reflect"
"strconv"
"strings"
"time"
"github.com/caarlos0/log"
"github.com/google/go-github/v50/github"
@@ -64,7 +65,22 @@ func newGitHub(ctx *context.Context, token string) (*githubClient, error) {
return &githubClient{client: client}, nil
}
func (c *githubClient) checkRateLimit(ctx *context.Context) {
limits, _, err := c.client.RateLimits(ctx)
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())
log.Warnf("token too close to rate limiting, will sleep for %s before continuing...", sleep)
time.Sleep(sleep)
}
func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, prev, current string) (string, error) {
c.checkRateLimit(ctx)
notes, _, err := c.client.Repositories.GenerateReleaseNotes(ctx, repo.Owner, repo.Name, &github.GenerateNotesOptions{
TagName: current,
PreviousTagName: github.String(prev),
@@ -76,6 +92,7 @@ func (c *githubClient) GenerateReleaseNotes(ctx *context.Context, repo Repo, pre
}
func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
c.checkRateLimit(ctx)
var log []string
opts := &github.ListOptions{PerPage: 100}
@@ -103,6 +120,7 @@ func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current
// getDefaultBranch returns the default branch of a github repo
func (c *githubClient) getDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
c.checkRateLimit(ctx)
p, res, err := c.client.Repositories.Get(ctx, repo.Owner, repo.Name)
if err != nil {
log.WithField("projectID", repo.String()).
@@ -116,6 +134,7 @@ func (c *githubClient) getDefaultBranch(ctx *context.Context, repo Repo) (string
// CloseMilestone closes a given milestone.
func (c *githubClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
c.checkRateLimit(ctx)
milestone, err := c.getMilestoneByTitle(ctx, repo, title)
if err != nil {
return err
@@ -144,6 +163,7 @@ func (c *githubClient) OpenPullRequest(
repo Repo,
base, title string,
) error {
c.checkRateLimit(ctx)
if base == "" {
def, err := c.getDefaultBranch(ctx, repo)
if err != nil {
@@ -176,6 +196,7 @@ func (c *githubClient) CreateFile(
path,
message string,
) error {
c.checkRateLimit(ctx)
defBranch, err := c.getDefaultBranch(ctx, repo)
if err != nil {
return fmt.Errorf("could not get default branch: %w", err)
@@ -257,6 +278,7 @@ func (c *githubClient) CreateFile(
}
func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string, error) {
c.checkRateLimit(ctx)
title, err := tmpl.New(ctx).Apply(ctx.Config.Release.NameTemplate)
if err != nil {
return "", err
@@ -302,6 +324,7 @@ func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string,
}
func (c *githubClient) createOrUpdateRelease(ctx *context.Context, data *github.RepositoryRelease, body string) (*github.RepositoryRelease, error) {
c.checkRateLimit(ctx)
release, _, err := c.client.Repositories.GetReleaseByTag(
ctx,
ctx.Config.Release.GitHub.Owner,
@@ -329,6 +352,7 @@ func (c *githubClient) createOrUpdateRelease(ctx *context.Context, data *github.
}
func (c *githubClient) updateRelease(ctx *context.Context, id int64, data *github.RepositoryRelease) (*github.RepositoryRelease, error) {
c.checkRateLimit(ctx)
release, resp, err := c.client.Repositories.EditRelease(
ctx,
ctx.Config.Release.GitHub.Owner,
@@ -365,6 +389,7 @@ func (c *githubClient) Upload(
artifact *artifact.Artifact,
file *os.File,
) error {
c.checkRateLimit(ctx)
githubReleaseID, err := strconv.ParseInt(releaseID, 10, 64)
if err != nil {
return err
@@ -400,6 +425,7 @@ func (c *githubClient) Upload(
// getMilestoneByTitle returns a milestone by title.
func (c *githubClient) getMilestoneByTitle(ctx *context.Context, repo Repo, title string) (*github.Milestone, error) {
c.checkRateLimit(ctx)
// The GitHub API/SDK does not provide lookup by title functionality currently.
opts := &github.MilestoneListOptions{
ListOptions: github.ListOptions{PerPage: 100},
@@ -462,6 +488,7 @@ func overrideGitHubClientAPI(ctx *context.Context, client *github.Client) error
}
func (c *githubClient) deleteExistingDraftRelease(ctx *context.Context, name string) error {
c.checkRateLimit(ctx)
opt := github.ListOptions{PerPage: 50}
for {
releases, resp, err := c.client.Repositories.ListReleases(

View File

@@ -8,7 +8,9 @@ import (
"os"
"testing"
"text/template"
"time"
"github.com/google/go-github/v50/github"
"github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/testctx"
"github.com/goreleaser/goreleaser/pkg/config"
@@ -201,6 +203,12 @@ func TestGithubGetDefaultBranch(t *testing.T) {
totalRequests++
defer r.Body.Close()
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
// Assume the request to create a branch was good
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"default_branch": "main"}`)
@@ -224,7 +232,7 @@ func TestGithubGetDefaultBranch(t *testing.T) {
b, err := client.getDefaultBranch(ctx, repo)
require.NoError(t, err)
require.Equal(t, "main", b)
require.Equal(t, 1, totalRequests)
require.Equal(t, 2, totalRequests)
}
func TestGithubGetDefaultBranchErr(t *testing.T) {
@@ -265,6 +273,11 @@ func TestChangelog(t *testing.T) {
require.NoError(t, err)
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
}))
defer srv.Close()
@@ -297,6 +310,11 @@ func TestReleaseNotes(t *testing.T) {
require.NoError(t, err)
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
}))
defer srv.Close()
@@ -325,6 +343,11 @@ func TestReleaseNotesError(t *testing.T) {
if r.URL.Path == "/repos/someone/something/releases/generate-notes" {
w.WriteHeader(http.StatusBadRequest)
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
}))
defer srv.Close()
@@ -357,6 +380,12 @@ func TestCloseMilestone(t *testing.T) {
require.NoError(t, err)
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
}))
defer srv.Close()
@@ -387,6 +416,12 @@ func TestOpenPullRequestHappyPath(t *testing.T) {
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()
@@ -419,6 +454,12 @@ func TestOpenPullRequestPRExists(t *testing.T) {
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()
@@ -456,6 +497,12 @@ func TestOpenPullRequestBaseEmpty(t *testing.T) {
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()
@@ -495,6 +542,12 @@ func TestGitHubCreateFileHappyPathCreate(t *testing.T) {
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()
@@ -535,6 +588,12 @@ func TestGitHubCreateFileHappyPathUpdate(t *testing.T) {
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
t.Error("unhandled request: " + r.URL.Path)
}))
defer srv.Close()
@@ -589,6 +648,12 @@ func TestGitHubCreateFileFeatureBranchDoesNotExist(t *testing.T) {
return
}
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"resources":{"core":{"remaining":120}}}`)
return
}
t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
}))
defer srv.Close()
@@ -609,6 +674,33 @@ func TestGitHubCreateFileFeatureBranchDoesNotExist(t *testing.T) {
require.NoError(t, client.CreateFile(ctx, config.CommitAuthor{}, repo, []byte("content"), "file.txt", "message"))
}
func TestCheckRateLimit(t *testing.T) {
now := time.Now().UTC()
reset := now.Add(1392 * time.Millisecond)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if r.URL.Path == "/rate_limit" {
w.WriteHeader(http.StatusOK)
resetstr, _ := github.Timestamp{Time: reset}.MarshalJSON()
fmt.Fprintf(w, `{"resources":{"core":{"remaining":98,"reset":%s}}}`, string(resetstr))
return
}
t.Error("unhandled request: " + r.Method + " " + r.URL.Path)
}))
defer srv.Close()
ctx := testctx.NewWithCfg(config.Project{
GitHubURLs: config.GitHubURLs{
API: srv.URL + "/",
},
})
client, err := newGitHub(ctx, "test-token")
require.NoError(t, err)
client.checkRateLimit(ctx)
require.True(t, time.Now().UTC().After(reset))
}
// TODO: test create release
// TODO: test create upload file to release
// TODO: test delete draft release
// TODO: test create PR