Github support rate limit (#85)

Reviewed-on: https://gitea.com/gitea/changelog/pulls/85
This commit is contained in:
Lunny Xiao 2024-05-24 05:59:15 +00:00
parent 9d720a45a2
commit ee1b5532aa
12 changed files with 81 additions and 35 deletions

@ -8,13 +8,13 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: '>=1.20.1' go-version-file: 'go.mod'
- name: goreleaser - name: goreleaser
uses: https://github.com/goreleaser/goreleaser-action@v4 uses: https://github.com/goreleaser/goreleaser-action@v4
with: with:

@ -9,13 +9,13 @@ jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: '>=1.20.1' go-version-file: 'go.mod'
- name: Import GPG key - name: Import GPG key
id: import_gpg id: import_gpg
uses: https://github.com/crazy-max/ghaction-import-gpg@v5 uses: https://github.com/crazy-max/ghaction-import-gpg@v5

@ -6,10 +6,10 @@ jobs:
check-and-test: check-and-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-go@v3 - uses: actions/setup-go@v5
with: with:
go-version: '>=1.20.1' go-version-file: 'go.mod'
- name: check-and-test - name: check-and-test
run: | run: |
go test -race ./... go test -race ./...

@ -21,7 +21,7 @@ var Generate = &cli.Command{
Action: runGenerate, Action: runGenerate,
} }
func runGenerate(_ *cli.Context) error { func runGenerate(ctx *cli.Context) error {
cfg, err := config.New(configPathFlag) cfg, err := config.New(configPathFlag)
if err != nil { if err != nil {
return err return err
@ -32,7 +32,7 @@ func runGenerate(_ *cli.Context) error {
return err return err
} }
title, prs, err := s.Generate() title, prs, err := s.Generate(ctx.Context)
if err != nil { if err != nil {
return err return err
} }

@ -6,7 +6,6 @@ package cmd
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"code.gitea.io/changelog/config" "code.gitea.io/changelog/config"
@ -37,7 +36,7 @@ func runInit(_ *cli.Context) error {
return fmt.Errorf("file '%s' already exists", nameFlag) return fmt.Errorf("file '%s' already exists", nameFlag)
} }
if err := ioutil.WriteFile(nameFlag, config.DefaultConfig, os.ModePerm); err != nil { if err := os.WriteFile(nameFlag, config.DefaultConfig, os.ModePerm); err != nil {
return err return err
} }

@ -6,7 +6,7 @@ package config
import ( import (
_ "embed" _ "embed"
"io/ioutil" "os"
"regexp" "regexp"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
@ -64,7 +64,7 @@ func New(configPath string) (*Config, error) {
var err error var err error
configContent := DefaultConfig configContent := DefaultConfig
if len(configPath) != 0 { if len(configPath) != 0 {
configContent, err = ioutil.ReadFile(configPath) configContent, err = os.ReadFile(configPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }

5
go.mod

@ -1,10 +1,10 @@
module code.gitea.io/changelog module code.gitea.io/changelog
go 1.18 go 1.22
require ( require (
code.gitea.io/sdk/gitea v0.14.0 code.gitea.io/sdk/gitea v0.14.0
github.com/google/go-github/v50 v50.0.0 github.com/google/go-github/v61 v61.0.0
github.com/urfave/cli/v3 v3.0.0-alpha2 github.com/urfave/cli/v3 v3.0.0-alpha2
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0
@ -17,7 +17,6 @@ require (
github.com/hashicorp/go-version v1.2.1 // indirect github.com/hashicorp/go-version v1.2.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
) )

9
go.sum

@ -8,9 +8,10 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-github/v50 v50.0.0 h1:gdO1AeuSZZK4iYWwVbjni7zg8PIQhp7QfmPunr016Jk= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v50 v50.0.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA= github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
@ -27,8 +28,6 @@ github.com/urfave/cli/v3 v3.0.0-alpha2/go.mod h1:gHI/xEYplFhOa3Y90xJleh3kqqsSanB
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=

@ -5,6 +5,7 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"time" "time"
@ -23,7 +24,7 @@ type Gitea struct {
} }
// Generate returns a Gitea changelog // Generate returns a Gitea changelog
func (ge *Gitea) Generate() (string, []Entry, error) { func (ge *Gitea) Generate(_ context.Context) (string, []Entry, error) {
client, err := gitea.NewClient(ge.BaseURL, gitea.SetToken(ge.Token)) client, err := gitea.NewClient(ge.BaseURL, gitea.SetToken(ge.Token))
if err != nil { if err != nil {
return "", nil, err return "", nil, err

@ -8,13 +8,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
log "log/slog"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/google/go-github/v50/github" "github.com/google/go-github/v61/github"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -25,7 +26,45 @@ type GitHub struct {
Token string Token string
Repo string Repo string
Issues bool Issues bool
ctx context.Context
client *github.Client client *github.Client
rate *github.Rate
}
func (gh *GitHub) setRate(rate *github.Rate) {
gh.rate = rate
}
func (gh *GitHub) RefreshRate() error {
rates, _, err := gh.client.RateLimit.Get(gh.ctx)
if err != nil {
// if rate limit is not enabled, ignore it
if strings.Contains(err.Error(), "404") {
gh.setRate(nil)
return nil
}
return err
}
gh.setRate(rates.GetCore())
return nil
}
func (gh *GitHub) waitAndPickClient() {
for gh.rate != nil && gh.rate.Remaining <= 0 {
timer := time.NewTimer(time.Until(gh.rate.Reset.Time))
select {
case <-gh.ctx.Done():
timer.Stop()
return
case <-timer.C:
}
err := gh.RefreshRate()
if err != nil {
log.Error("g.getClient().RateLimit.Get: %s", err)
}
}
} }
// OwnerRepo splits owner/repo // OwnerRepo splits owner/repo
@ -38,12 +77,11 @@ func (gh *GitHub) OwnerRepo() (string, string) {
} }
// Generate returns a GitHub changelog // Generate returns a GitHub changelog
func (gh *GitHub) Generate() (string, []Entry, error) { func (gh *GitHub) Generate(ctx context.Context) (string, []Entry, error) {
owner, repo := gh.OwnerRepo() owner, repo := gh.OwnerRepo()
ctx := context.Background()
gh.initClient(ctx) gh.initClient(ctx)
tagURL := fmt.Sprintf("## [%s](https://github.com/%s/releases/tag/%s) - %s", gh.Milestone, gh.Repo, gh.GitTag, time.Now().Format("2006-01-02")) tagURL := fmt.Sprintf("## [%s](https://github.com/%s/releases/tag/v%s) - %s", gh.Milestone, gh.Repo, gh.GitTag, time.Now().Format("2006-01-02"))
prs := make([]Entry, 0) prs := make([]Entry, 0)
@ -55,7 +93,8 @@ func (gh *GitHub) Generate() (string, []Entry, error) {
p := 1 p := 1
perPage := 100 perPage := 100
for { for {
result, _, err := gh.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{ gh.waitAndPickClient()
result, resp, err := gh.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{
Milestone: strconv.Itoa(milestoneNum), Milestone: strconv.Itoa(milestoneNum),
State: "closed", State: "closed",
ListOptions: github.ListOptions{ ListOptions: github.ListOptions{
@ -66,6 +105,7 @@ func (gh *GitHub) Generate() (string, []Entry, error) {
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
gh.setRate(&resp.Rate)
p++ p++
isPull := !(gh.Issues) isPull := !(gh.Issues)
@ -111,7 +151,8 @@ func (gh *GitHub) Contributors() (ContributorList, error) {
p := 1 p := 1
perPage := 100 perPage := 100
for { for {
result, _, err := gh.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{ gh.waitAndPickClient()
result, resp, err := gh.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{
Milestone: strconv.Itoa(milestoneNum), Milestone: strconv.Itoa(milestoneNum),
State: "closed", State: "closed",
ListOptions: github.ListOptions{ ListOptions: github.ListOptions{
@ -122,6 +163,7 @@ func (gh *GitHub) Contributors() (ContributorList, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
gh.setRate(&resp.Rate)
p++ p++
for _, pr := range result { for _, pr := range result {
@ -158,6 +200,7 @@ func (gh *GitHub) initClient(ctx context.Context) {
} }
gh.client = github.NewClient(cl) gh.client = github.NewClient(cl)
gh.ctx = ctx
} }
func (gh *GitHub) milestoneNum(ctx context.Context) (int, error) { func (gh *GitHub) milestoneNum(ctx context.Context) (int, error) {
@ -165,7 +208,8 @@ func (gh *GitHub) milestoneNum(ctx context.Context) (int, error) {
p := 1 p := 1
perPage := 100 perPage := 100
for { for {
milestones, _, err := gh.client.Issues.ListMilestones(ctx, owner, repo, &github.MilestoneListOptions{ gh.waitAndPickClient()
milestones, resp, err := gh.client.Issues.ListMilestones(ctx, owner, repo, &github.MilestoneListOptions{
State: "all", State: "all",
ListOptions: github.ListOptions{ ListOptions: github.ListOptions{
Page: p, Page: p,
@ -177,6 +221,7 @@ func (gh *GitHub) milestoneNum(ctx context.Context) (int, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
gh.setRate(&resp.Rate)
p++ p++
for _, milestone := range milestones { for _, milestone := range milestones {

@ -4,7 +4,10 @@
package service package service
import "testing" import (
"context"
"testing"
)
var gh = &GitHub{ var gh = &GitHub{
Milestone: "1.1.0", // https://github.com/go-gitea/test_repo/milestone/2?closed=1 Milestone: "1.1.0", // https://github.com/go-gitea/test_repo/milestone/2?closed=1
@ -12,7 +15,7 @@ var gh = &GitHub{
} }
func TestGitHubGenerate(t *testing.T) { func TestGitHubGenerate(t *testing.T) {
_, entries, err := gh.Generate() _, entries, err := gh.Generate(context.Background())
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()

@ -5,6 +5,7 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"strings" "strings"
"unicode" "unicode"
@ -48,7 +49,7 @@ func New(serviceType, repo, baseURL, milestone, tag, token string, issues bool)
// Service defines how a struct can be a Changelog Service // Service defines how a struct can be a Changelog Service
type Service interface { type Service interface {
Generate() (string, []Entry, error) Generate(ctx context.Context) (string, []Entry, error)
Contributors() (ContributorList, error) Contributors() (ContributorList, error)
} }
@ -90,7 +91,6 @@ func (cl ContributorList) Swap(i, j int) {
// CleanTitle returns the string with spaces trimmed and the first rune title-cased // CleanTitle returns the string with spaces trimmed and the first rune title-cased
func CleanTitle(s string) string { func CleanTitle(s string) string {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
r := []rune(s) r := []rune(s)