From 1df9581dca7620d73f81944e344bbb80cea09186 Mon Sep 17 00:00:00 2001 From: ntsk Date: Sun, 6 Jul 2025 00:24:29 +0900 Subject: [PATCH] feat: add GitHub API rate limit check before issue creation --- internal/github/client.go | 14 +++++++ internal/github/client_test.go | 73 ++++++++++++++++++++++++++++++++++ main.go | 34 ++++++++++++++++ pkg/models/issue.go | 12 ++++++ 4 files changed, 133 insertions(+) diff --git a/internal/github/client.go b/internal/github/client.go index 61c8faa..832d69f 100644 --- a/internal/github/client.go +++ b/internal/github/client.go @@ -16,6 +16,7 @@ import ( type ClientInterface interface { CreateIssue(issue *models.Issue, repo string) (*models.IssueResponse, error) GetCurrentRepository() (string, error) + GetRateLimit() (*models.RateLimitResponse, error) } // Client provides GitHub API functionality @@ -94,3 +95,16 @@ func (c *Client) GetCurrentRepository() (string, error) { return fmt.Sprintf("%s/%s", info.Owner.Login, info.Name), nil } + +// GetRateLimit gets the current GitHub API rate limit information +func (c *Client) GetRateLimit() (*models.RateLimitResponse, error) { + response := &models.RateLimitResponse{} + + // Send GET request to rate_limit endpoint + err := c.client.Get("rate_limit", response) + if err != nil { + return nil, fmt.Errorf("failed to get rate limit: %v", err) + } + + return response, nil +} diff --git a/internal/github/client_test.go b/internal/github/client_test.go index 626febc..ec22919 100644 --- a/internal/github/client_test.go +++ b/internal/github/client_test.go @@ -10,6 +10,7 @@ import ( type MockClient struct { CreateIssueFunc func(issue *models.Issue, repo string) (*models.IssueResponse, error) GetCurrentRepoFunc func() (string, error) + GetRateLimitFunc func() (*models.RateLimitResponse, error) CreatedIssues []*models.Issue GetCurrentRepoCounter int } @@ -32,6 +33,20 @@ func (m *MockClient) GetCurrentRepository() (string, error) { return "mock/repo", nil } +// GetRateLimit implements the ClientInterface for testing +func (m *MockClient) GetRateLimit() (*models.RateLimitResponse, error) { + if m.GetRateLimitFunc != nil { + return m.GetRateLimitFunc() + } + return &models.RateLimitResponse{ + Rate: models.RateLimit{ + Limit: 5000, + Remaining: 4999, + Reset: 1234567890, + }, + }, nil +} + func TestMockClient(t *testing.T) { // Create mock client mockClient := &MockClient{} @@ -125,4 +140,62 @@ func TestClientInterface(t *testing.T) { if repo != "mock/repo" { t.Errorf("Expected repo 'mock/repo', got '%s'", repo) } + + // Test GetRateLimit interface method + rateLimit, err := client.GetRateLimit() + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if rateLimit.Rate.Limit != 5000 { + t.Errorf("Expected rate limit 5000, got %d", rateLimit.Rate.Limit) + } +} + +// TestRateLimit tests the GetRateLimit functionality +func TestRateLimit(t *testing.T) { + // Create mock client + mockClient := &MockClient{} + + // Test GetRateLimit with default values + rateLimit, err := mockClient.GetRateLimit() + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + // Check default values + if rateLimit.Rate.Limit != 5000 { + t.Errorf("Expected rate limit 5000, got %d", rateLimit.Rate.Limit) + } + if rateLimit.Rate.Remaining != 4999 { + t.Errorf("Expected remaining 4999, got %d", rateLimit.Rate.Remaining) + } + if rateLimit.Rate.Reset != 1234567890 { + t.Errorf("Expected reset 1234567890, got %d", rateLimit.Rate.Reset) + } + + // Test with custom function + mockClient = &MockClient{ + GetRateLimitFunc: func() (*models.RateLimitResponse, error) { + return &models.RateLimitResponse{ + Rate: models.RateLimit{ + Limit: 60, + Remaining: 30, + Reset: 1234567900, + }, + }, nil + }, + } + + rateLimit, err = mockClient.GetRateLimit() + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if rateLimit.Rate.Limit != 60 { + t.Errorf("Expected custom limit 60, got %d", rateLimit.Rate.Limit) + } + if rateLimit.Rate.Remaining != 30 { + t.Errorf("Expected custom remaining 30, got %d", rateLimit.Rate.Remaining) + } } diff --git a/main.go b/main.go index 3962102..109899d 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/ntsk/gh-issue-bulk-create/internal/csv" "github.com/ntsk/gh-issue-bulk-create/internal/github" @@ -165,6 +166,39 @@ func main() { fmt.Printf("Target repository: %s\n", targetRepo) + // Check rate limit before creating issues + if !opts.dryRun { + rateLimit, err := githubClient.GetRateLimit() + if err != nil { + fmt.Printf("Warning: Failed to check rate limit: %v\n", err) + } else { + fmt.Printf("Current rate limit: %d remaining out of %d\n", + rateLimit.Rate.Remaining, rateLimit.Rate.Limit) + + resetTime := time.Unix(int64(rateLimit.Rate.Reset), 0) + fmt.Printf("Reset time: %s (in %s)\n", + resetTime.Format(time.RFC3339), + time.Until(resetTime).Round(time.Minute)) + + issueCount := len(dataMaps) + if rateLimit.Rate.Remaining < issueCount { + fmt.Printf("Warning: Not enough rate limit remaining (%d) for %d issues\n", + rateLimit.Rate.Remaining, issueCount) + fmt.Printf("You may hit the rate limit during execution.\n") + fmt.Printf("Do you want to continue? (y/N): ") + var response string + fmt.Scanln(&response) + response = strings.ToLower(strings.TrimSpace(response)) + if response != "y" && response != "yes" { + fmt.Println("Aborted.") + os.Exit(0) + } + } else { + fmt.Printf("Rate limit looks sufficient for %d issues\n", issueCount) + } + } + } + // Process template and create issues for _, data := range dataMaps { // Render template with data diff --git a/pkg/models/issue.go b/pkg/models/issue.go index 6b7d0e3..6f45263 100644 --- a/pkg/models/issue.go +++ b/pkg/models/issue.go @@ -42,3 +42,15 @@ type IssueResponse struct { Number int `json:"number"` URL string `json:"html_url"` } + +// RateLimit represents GitHub API rate limit information +type RateLimit struct { + Limit int `json:"limit"` + Remaining int `json:"remaining"` + Reset int `json:"reset"` +} + +// RateLimitResponse represents GitHub API rate limit response +type RateLimitResponse struct { + Rate RateLimit `json:"rate"` +}