github.com/google/go-github/v69@v69.2.0/github/github.go (about) 1 // Copyright 2013 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 //go:generate go run gen-accessors.go 7 //go:generate go run gen-stringify-test.go 8 //go:generate ../script/metadata.sh update-go 9 10 package github 11 12 import ( 13 "bytes" 14 "context" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "io" 19 "net/http" 20 "net/url" 21 "reflect" 22 "regexp" 23 "strconv" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/google/go-querystring/query" 29 ) 30 31 const ( 32 Version = "v69.2.0" 33 34 defaultAPIVersion = "2022-11-28" 35 defaultBaseURL = "https://api.github.com/" 36 defaultUserAgent = "go-github" + "/" + Version 37 uploadBaseURL = "https://uploads.github.com/" 38 39 headerAPIVersion = "X-Github-Api-Version" 40 headerRateLimit = "X-Ratelimit-Limit" 41 headerRateRemaining = "X-Ratelimit-Remaining" 42 headerRateUsed = "X-Ratelimit-Used" 43 headerRateReset = "X-Ratelimit-Reset" 44 headerRateResource = "X-Ratelimit-Resource" 45 headerOTP = "X-Github-Otp" 46 headerRetryAfter = "Retry-After" 47 48 headerTokenExpiration = "Github-Authentication-Token-Expiration" 49 50 mediaTypeV3 = "application/vnd.github.v3+json" 51 defaultMediaType = "application/octet-stream" 52 mediaTypeV3SHA = "application/vnd.github.v3.sha" 53 mediaTypeV3Diff = "application/vnd.github.v3.diff" 54 mediaTypeV3Patch = "application/vnd.github.v3.patch" 55 mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json" 56 mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json" 57 58 // Media Type values to access preview APIs 59 // These media types will be added to the API request as headers 60 // and used to enable particular features on GitHub API that are still in preview. 61 // After some time, specific media types will be promoted (to a "stable" state). 62 // From then on, the preview headers are not required anymore to activate the additional 63 // feature on GitHub.com's API. However, this API header might still be needed for users 64 // to run a GitHub Enterprise Server on-premise. 65 // It's not uncommon for GitHub Enterprise Server customers to run older versions which 66 // would probably rely on the preview headers for some time. 67 // While the header promotion is going out for GitHub.com, it may be some time before it 68 // even arrives in GitHub Enterprise Server. 69 // We keep those preview headers around to avoid breaking older GitHub Enterprise Server 70 // versions. Additionally, non-functional (preview) headers don't create any side effects 71 // on GitHub Cloud version. 72 // 73 // See https://github.com/google/go-github/pull/2125 for full context. 74 75 // https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/ 76 mediaTypeStarringPreview = "application/vnd.github.v3.star+json" 77 78 // https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/ 79 mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json" 80 81 // https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/ 82 mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json" 83 84 // https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/ 85 mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json" 86 87 // https://developer.github.com/changes/2016-05-12-reactions-api-preview/ 88 mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview" 89 90 // https://developer.github.com/changes/2016-05-23-timeline-preview-api/ 91 mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json" 92 93 // https://developer.github.com/changes/2016-09-14-projects-api/ 94 mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json" 95 96 // https://developer.github.com/changes/2017-01-05-commit-search-api/ 97 mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json" 98 99 // https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/ 100 mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json" 101 102 // https://developer.github.com/changes/2017-05-23-coc-api/ 103 mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json" 104 105 // https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/ 106 mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json" 107 108 // https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/ 109 mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json" 110 111 // https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/ 112 mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json" 113 114 // https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/ 115 mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview" 116 117 // https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/ 118 mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json" 119 120 // https://developer.github.com/changes/2018-09-05-project-card-events/ 121 mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json" 122 123 // https://developer.github.com/changes/2018-12-18-interactions-preview/ 124 mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json" 125 126 // https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/ 127 mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json" 128 129 // https://developer.github.com/changes/2019-04-24-vulnerability-alerts/ 130 mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json" 131 132 // https://developer.github.com/changes/2019-05-29-update-branch-api/ 133 mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json" 134 135 // https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/ 136 mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json" 137 138 // https://docs.github.com/rest/previews/#repository-creation-permissions 139 mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json" 140 141 // https://docs.github.com/rest/previews/#create-and-use-repository-templates 142 mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json" 143 144 // https://developer.github.com/changes/2019-10-03-multi-line-comments/ 145 mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json" 146 147 // https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/ 148 mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json" 149 150 // https://developer.github.com/changes/2019-12-03-internal-visibility-changes/ 151 mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json" 152 153 // https://developer.github.com/changes/2018-12-10-content-attachments-api/ 154 mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json" 155 ) 156 157 var errNonNilContext = errors.New("context must be non-nil") 158 159 // A Client manages communication with the GitHub API. 160 type Client struct { 161 clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. 162 client *http.Client // HTTP client used to communicate with the API. 163 clientIgnoreRedirects *http.Client // HTTP client used to communicate with the API on endpoints where we don't want to follow redirects. 164 165 // Base URL for API requests. Defaults to the public GitHub API, but can be 166 // set to a domain endpoint to use with GitHub Enterprise. BaseURL should 167 // always be specified with a trailing slash. 168 BaseURL *url.URL 169 170 // Base URL for uploading files. 171 UploadURL *url.URL 172 173 // User agent used when communicating with the GitHub API. 174 UserAgent string 175 176 rateMu sync.Mutex 177 rateLimits [Categories]Rate // Rate limits for the client as determined by the most recent API calls. 178 secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls. 179 180 // If specified, Client will block requests for at most this duration in case of reaching a secondary 181 // rate limit 182 MaxSecondaryRateLimitRetryAfterDuration time.Duration 183 184 // Whether to respect rate limit headers on endpoints that return 302 redirections to artifacts 185 RateLimitRedirectionalEndpoints bool 186 187 common service // Reuse a single struct instead of allocating one for each service on the heap. 188 189 // Services used for talking to different parts of the GitHub API. 190 Actions *ActionsService 191 Activity *ActivityService 192 Admin *AdminService 193 Apps *AppsService 194 Authorizations *AuthorizationsService 195 Billing *BillingService 196 Checks *ChecksService 197 CodeScanning *CodeScanningService 198 CodesOfConduct *CodesOfConductService 199 Codespaces *CodespacesService 200 Copilot *CopilotService 201 Dependabot *DependabotService 202 DependencyGraph *DependencyGraphService 203 Emojis *EmojisService 204 Enterprise *EnterpriseService 205 Gists *GistsService 206 Git *GitService 207 Gitignores *GitignoresService 208 Interactions *InteractionsService 209 IssueImport *IssueImportService 210 Issues *IssuesService 211 Licenses *LicensesService 212 Markdown *MarkdownService 213 Marketplace *MarketplaceService 214 Meta *MetaService 215 Migrations *MigrationService 216 Organizations *OrganizationsService 217 PullRequests *PullRequestsService 218 RateLimit *RateLimitService 219 Reactions *ReactionsService 220 Repositories *RepositoriesService 221 SCIM *SCIMService 222 Search *SearchService 223 SecretScanning *SecretScanningService 224 SecurityAdvisories *SecurityAdvisoriesService 225 Teams *TeamsService 226 Users *UsersService 227 } 228 229 type service struct { 230 client *Client 231 } 232 233 // Client returns the http.Client used by this GitHub client. 234 // This should only be used for requests to the GitHub API because 235 // request headers will contain an authorization token. 236 func (c *Client) Client() *http.Client { 237 c.clientMu.Lock() 238 defer c.clientMu.Unlock() 239 clientCopy := *c.client 240 return &clientCopy 241 } 242 243 // ListOptions specifies the optional parameters to various List methods that 244 // support offset pagination. 245 type ListOptions struct { 246 // For paginated result sets, page of results to retrieve. 247 Page int `url:"page,omitempty"` 248 249 // For paginated result sets, the number of results to include per page. 250 PerPage int `url:"per_page,omitempty"` 251 } 252 253 // ListCursorOptions specifies the optional parameters to various List methods that 254 // support cursor pagination. 255 type ListCursorOptions struct { 256 // For paginated result sets, page of results to retrieve. 257 Page string `url:"page,omitempty"` 258 259 // For paginated result sets, the number of results to include per page. 260 PerPage int `url:"per_page,omitempty"` 261 262 // For paginated result sets, the number of results per page (max 100), starting from the first matching result. 263 // This parameter must not be used in combination with last. 264 First int `url:"first,omitempty"` 265 266 // For paginated result sets, the number of results per page (max 100), starting from the last matching result. 267 // This parameter must not be used in combination with first. 268 Last int `url:"last,omitempty"` 269 270 // A cursor, as given in the Link header. If specified, the query only searches for events after this cursor. 271 After string `url:"after,omitempty"` 272 273 // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor. 274 Before string `url:"before,omitempty"` 275 276 // A cursor, as given in the Link header. If specified, the query continues the search using this cursor. 277 Cursor string `url:"cursor,omitempty"` 278 } 279 280 // UploadOptions specifies the parameters to methods that support uploads. 281 type UploadOptions struct { 282 Name string `url:"name,omitempty"` 283 Label string `url:"label,omitempty"` 284 MediaType string `url:"-"` 285 } 286 287 // RawType represents type of raw format of a request instead of JSON. 288 type RawType uint8 289 290 const ( 291 // Diff format. 292 Diff RawType = 1 + iota 293 // Patch format. 294 Patch 295 ) 296 297 // RawOptions specifies parameters when user wants to get raw format of 298 // a response instead of JSON. 299 type RawOptions struct { 300 Type RawType 301 } 302 303 // addOptions adds the parameters in opts as URL query parameters to s. opts 304 // must be a struct whose fields may contain "url" tags. 305 func addOptions(s string, opts interface{}) (string, error) { 306 v := reflect.ValueOf(opts) 307 if v.Kind() == reflect.Ptr && v.IsNil() { 308 return s, nil 309 } 310 311 u, err := url.Parse(s) 312 if err != nil { 313 return s, err 314 } 315 316 qs, err := query.Values(opts) 317 if err != nil { 318 return s, err 319 } 320 321 u.RawQuery = qs.Encode() 322 return u.String(), nil 323 } 324 325 // NewClient returns a new GitHub API client. If a nil httpClient is 326 // provided, a new http.Client will be used. To use API methods which require 327 // authentication, either use Client.WithAuthToken or provide NewClient with 328 // an http.Client that will perform the authentication for you (such as that 329 // provided by the golang.org/x/oauth2 library). 330 func NewClient(httpClient *http.Client) *Client { 331 if httpClient == nil { 332 httpClient = &http.Client{} 333 } 334 httpClient2 := *httpClient 335 c := &Client{client: &httpClient2} 336 c.initialize() 337 return c 338 } 339 340 // WithAuthToken returns a copy of the client configured to use the provided token for the Authorization header. 341 func (c *Client) WithAuthToken(token string) *Client { 342 c2 := c.copy() 343 defer c2.initialize() 344 transport := c2.client.Transport 345 if transport == nil { 346 transport = http.DefaultTransport 347 } 348 c2.client.Transport = roundTripperFunc( 349 func(req *http.Request) (*http.Response, error) { 350 req = req.Clone(req.Context()) 351 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 352 return transport.RoundTrip(req) 353 }, 354 ) 355 return c2 356 } 357 358 // WithEnterpriseURLs returns a copy of the client configured to use the provided base and 359 // upload URLs. If the base URL does not have the suffix "/api/v3/", it will be added 360 // automatically. If the upload URL does not have the suffix "/api/uploads", it will be 361 // added automatically. 362 // 363 // Note that WithEnterpriseURLs is a convenience helper only; 364 // its behavior is equivalent to setting the BaseURL and UploadURL fields. 365 // 366 // Another important thing is that by default, the GitHub Enterprise URL format 367 // should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code. 368 // The upload URL format should be http(s)://[hostname]/api/uploads/. 369 func (c *Client) WithEnterpriseURLs(baseURL, uploadURL string) (*Client, error) { 370 c2 := c.copy() 371 defer c2.initialize() 372 var err error 373 c2.BaseURL, err = url.Parse(baseURL) 374 if err != nil { 375 return nil, err 376 } 377 378 if !strings.HasSuffix(c2.BaseURL.Path, "/") { 379 c2.BaseURL.Path += "/" 380 } 381 if !strings.HasSuffix(c2.BaseURL.Path, "/api/v3/") && 382 !strings.HasPrefix(c2.BaseURL.Host, "api.") && 383 !strings.Contains(c2.BaseURL.Host, ".api.") { 384 c2.BaseURL.Path += "api/v3/" 385 } 386 387 c2.UploadURL, err = url.Parse(uploadURL) 388 if err != nil { 389 return nil, err 390 } 391 392 if !strings.HasSuffix(c2.UploadURL.Path, "/") { 393 c2.UploadURL.Path += "/" 394 } 395 if !strings.HasSuffix(c2.UploadURL.Path, "/api/uploads/") && 396 !strings.HasPrefix(c2.UploadURL.Host, "api.") && 397 !strings.Contains(c2.UploadURL.Host, ".api.") { 398 c2.UploadURL.Path += "api/uploads/" 399 } 400 return c2, nil 401 } 402 403 // initialize sets default values and initializes services. 404 func (c *Client) initialize() { 405 if c.client == nil { 406 c.client = &http.Client{} 407 } 408 // Copy the main http client into the IgnoreRedirects one, overriding the `CheckRedirect` func 409 c.clientIgnoreRedirects = &http.Client{} 410 c.clientIgnoreRedirects.Transport = c.client.Transport 411 c.clientIgnoreRedirects.Timeout = c.client.Timeout 412 c.clientIgnoreRedirects.Jar = c.client.Jar 413 c.clientIgnoreRedirects.CheckRedirect = func(req *http.Request, via []*http.Request) error { 414 return http.ErrUseLastResponse 415 } 416 if c.BaseURL == nil { 417 c.BaseURL, _ = url.Parse(defaultBaseURL) 418 } 419 if c.UploadURL == nil { 420 c.UploadURL, _ = url.Parse(uploadBaseURL) 421 } 422 if c.UserAgent == "" { 423 c.UserAgent = defaultUserAgent 424 } 425 c.common.client = c 426 c.Actions = (*ActionsService)(&c.common) 427 c.Activity = (*ActivityService)(&c.common) 428 c.Admin = (*AdminService)(&c.common) 429 c.Apps = (*AppsService)(&c.common) 430 c.Authorizations = (*AuthorizationsService)(&c.common) 431 c.Billing = (*BillingService)(&c.common) 432 c.Checks = (*ChecksService)(&c.common) 433 c.CodeScanning = (*CodeScanningService)(&c.common) 434 c.Codespaces = (*CodespacesService)(&c.common) 435 c.CodesOfConduct = (*CodesOfConductService)(&c.common) 436 c.Copilot = (*CopilotService)(&c.common) 437 c.Dependabot = (*DependabotService)(&c.common) 438 c.DependencyGraph = (*DependencyGraphService)(&c.common) 439 c.Emojis = (*EmojisService)(&c.common) 440 c.Enterprise = (*EnterpriseService)(&c.common) 441 c.Gists = (*GistsService)(&c.common) 442 c.Git = (*GitService)(&c.common) 443 c.Gitignores = (*GitignoresService)(&c.common) 444 c.Interactions = (*InteractionsService)(&c.common) 445 c.IssueImport = (*IssueImportService)(&c.common) 446 c.Issues = (*IssuesService)(&c.common) 447 c.Licenses = (*LicensesService)(&c.common) 448 c.Markdown = (*MarkdownService)(&c.common) 449 c.Marketplace = &MarketplaceService{client: c} 450 c.Meta = (*MetaService)(&c.common) 451 c.Migrations = (*MigrationService)(&c.common) 452 c.Organizations = (*OrganizationsService)(&c.common) 453 c.PullRequests = (*PullRequestsService)(&c.common) 454 c.RateLimit = (*RateLimitService)(&c.common) 455 c.Reactions = (*ReactionsService)(&c.common) 456 c.Repositories = (*RepositoriesService)(&c.common) 457 c.SCIM = (*SCIMService)(&c.common) 458 c.Search = (*SearchService)(&c.common) 459 c.SecretScanning = (*SecretScanningService)(&c.common) 460 c.SecurityAdvisories = (*SecurityAdvisoriesService)(&c.common) 461 c.Teams = (*TeamsService)(&c.common) 462 c.Users = (*UsersService)(&c.common) 463 } 464 465 // copy returns a copy of the current client. It must be initialized before use. 466 func (c *Client) copy() *Client { 467 c.clientMu.Lock() 468 // can't use *c here because that would copy mutexes by value. 469 clone := Client{ 470 client: &http.Client{}, 471 UserAgent: c.UserAgent, 472 BaseURL: c.BaseURL, 473 UploadURL: c.UploadURL, 474 RateLimitRedirectionalEndpoints: c.RateLimitRedirectionalEndpoints, 475 secondaryRateLimitReset: c.secondaryRateLimitReset, 476 } 477 c.clientMu.Unlock() 478 if c.client != nil { 479 clone.client.Transport = c.client.Transport 480 clone.client.CheckRedirect = c.client.CheckRedirect 481 clone.client.Jar = c.client.Jar 482 clone.client.Timeout = c.client.Timeout 483 } 484 c.rateMu.Lock() 485 copy(clone.rateLimits[:], c.rateLimits[:]) 486 c.rateMu.Unlock() 487 return &clone 488 } 489 490 // NewClientWithEnvProxy enhances NewClient with the HttpProxy env. 491 func NewClientWithEnvProxy() *Client { 492 return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}}) 493 } 494 495 // NewTokenClient returns a new GitHub API client authenticated with the provided token. 496 // Deprecated: Use NewClient(nil).WithAuthToken(token) instead. 497 func NewTokenClient(_ context.Context, token string) *Client { 498 // This always returns a nil error. 499 return NewClient(nil).WithAuthToken(token) 500 } 501 502 // NewEnterpriseClient returns a new GitHub API client with provided 503 // base URL and upload URL (often is your GitHub Enterprise hostname). 504 // 505 // Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead. 506 func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) { 507 return NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) 508 } 509 510 // RequestOption represents an option that can modify an http.Request. 511 type RequestOption func(req *http.Request) 512 513 // WithVersion overrides the GitHub v3 API version for this individual request. 514 // For more information, see: 515 // https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/ 516 func WithVersion(version string) RequestOption { 517 return func(req *http.Request) { 518 req.Header.Set(headerAPIVersion, version) 519 } 520 } 521 522 // NewRequest creates an API request. A relative URL can be provided in urlStr, 523 // in which case it is resolved relative to the BaseURL of the Client. 524 // Relative URLs should always be specified without a preceding slash. If 525 // specified, the value pointed to by body is JSON encoded and included as the 526 // request body. 527 func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) { 528 if !strings.HasSuffix(c.BaseURL.Path, "/") { 529 return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.BaseURL) 530 } 531 532 u, err := c.BaseURL.Parse(urlStr) 533 if err != nil { 534 return nil, err 535 } 536 537 var buf io.ReadWriter 538 if body != nil { 539 buf = &bytes.Buffer{} 540 enc := json.NewEncoder(buf) 541 enc.SetEscapeHTML(false) 542 err := enc.Encode(body) 543 if err != nil { 544 return nil, err 545 } 546 } 547 548 req, err := http.NewRequest(method, u.String(), buf) 549 if err != nil { 550 return nil, err 551 } 552 553 if body != nil { 554 req.Header.Set("Content-Type", "application/json") 555 } 556 req.Header.Set("Accept", mediaTypeV3) 557 if c.UserAgent != "" { 558 req.Header.Set("User-Agent", c.UserAgent) 559 } 560 req.Header.Set(headerAPIVersion, defaultAPIVersion) 561 562 for _, opt := range opts { 563 opt(req) 564 } 565 566 return req, nil 567 } 568 569 // NewFormRequest creates an API request. A relative URL can be provided in urlStr, 570 // in which case it is resolved relative to the BaseURL of the Client. 571 // Relative URLs should always be specified without a preceding slash. 572 // Body is sent with Content-Type: application/x-www-form-urlencoded. 573 func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) { 574 if !strings.HasSuffix(c.BaseURL.Path, "/") { 575 return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.BaseURL) 576 } 577 578 u, err := c.BaseURL.Parse(urlStr) 579 if err != nil { 580 return nil, err 581 } 582 583 req, err := http.NewRequest(http.MethodPost, u.String(), body) 584 if err != nil { 585 return nil, err 586 } 587 588 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 589 req.Header.Set("Accept", mediaTypeV3) 590 if c.UserAgent != "" { 591 req.Header.Set("User-Agent", c.UserAgent) 592 } 593 req.Header.Set(headerAPIVersion, defaultAPIVersion) 594 595 for _, opt := range opts { 596 opt(req) 597 } 598 599 return req, nil 600 } 601 602 // NewUploadRequest creates an upload request. A relative URL can be provided in 603 // urlStr, in which case it is resolved relative to the UploadURL of the Client. 604 // Relative URLs should always be specified without a preceding slash. 605 func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) { 606 if !strings.HasSuffix(c.UploadURL.Path, "/") { 607 return nil, fmt.Errorf("uploadURL must have a trailing slash, but %q does not", c.UploadURL) 608 } 609 u, err := c.UploadURL.Parse(urlStr) 610 if err != nil { 611 return nil, err 612 } 613 614 req, err := http.NewRequest("POST", u.String(), reader) 615 if err != nil { 616 return nil, err 617 } 618 619 req.ContentLength = size 620 621 if mediaType == "" { 622 mediaType = defaultMediaType 623 } 624 req.Header.Set("Content-Type", mediaType) 625 req.Header.Set("Accept", mediaTypeV3) 626 req.Header.Set("User-Agent", c.UserAgent) 627 req.Header.Set(headerAPIVersion, defaultAPIVersion) 628 629 for _, opt := range opts { 630 opt(req) 631 } 632 633 return req, nil 634 } 635 636 // Response is a GitHub API response. This wraps the standard http.Response 637 // returned from GitHub and provides convenient access to things like 638 // pagination links. 639 type Response struct { 640 *http.Response 641 642 // These fields provide the page values for paginating through a set of 643 // results. Any or all of these may be set to the zero value for 644 // responses that are not part of a paginated set, or for which there 645 // are no additional pages. 646 // 647 // These fields support what is called "offset pagination" and should 648 // be used with the ListOptions struct. 649 NextPage int 650 PrevPage int 651 FirstPage int 652 LastPage int 653 654 // Additionally, some APIs support "cursor pagination" instead of offset. 655 // This means that a token points directly to the next record which 656 // can lead to O(1) performance compared to O(n) performance provided 657 // by offset pagination. 658 // 659 // For APIs that support cursor pagination (such as 660 // TeamsService.ListIDPGroupsInOrganization), the following field 661 // will be populated to point to the next page. 662 // 663 // To use this token, set ListCursorOptions.Page to this value before 664 // calling the endpoint again. 665 NextPageToken string 666 667 // For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries, 668 // the following field will be populated to point to the next page. 669 // Set ListCursorOptions.Cursor to this value when calling the endpoint again. 670 Cursor string 671 672 // For APIs that support before/after pagination, such as OrganizationsService.AuditLog. 673 Before string 674 After string 675 676 // Explicitly specify the Rate type so Rate's String() receiver doesn't 677 // propagate to Response. 678 Rate Rate 679 680 // token's expiration date. Timestamp is 0001-01-01 when token doesn't expire. 681 // So it is valid for TokenExpiration.Equal(Timestamp{}) or TokenExpiration.Time.After(time.Now()) 682 TokenExpiration Timestamp 683 } 684 685 // newResponse creates a new Response for the provided http.Response. 686 // r must not be nil. 687 func newResponse(r *http.Response) *Response { 688 response := &Response{Response: r} 689 response.populatePageValues() 690 response.Rate = parseRate(r) 691 response.TokenExpiration = parseTokenExpiration(r) 692 return response 693 } 694 695 // populatePageValues parses the HTTP Link response headers and populates the 696 // various pagination link values in the Response. 697 func (r *Response) populatePageValues() { 698 if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { 699 for _, link := range strings.Split(links[0], ",") { 700 segments := strings.Split(strings.TrimSpace(link), ";") 701 702 // link must at least have href and rel 703 if len(segments) < 2 { 704 continue 705 } 706 707 // ensure href is properly formatted 708 if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { 709 continue 710 } 711 712 // try to pull out page parameter 713 url, err := url.Parse(segments[0][1 : len(segments[0])-1]) 714 if err != nil { 715 continue 716 } 717 718 q := url.Query() 719 720 if cursor := q.Get("cursor"); cursor != "" { 721 for _, segment := range segments[1:] { 722 switch strings.TrimSpace(segment) { 723 case `rel="next"`: 724 r.Cursor = cursor 725 } 726 } 727 728 continue 729 } 730 731 page := q.Get("page") 732 since := q.Get("since") 733 before := q.Get("before") 734 after := q.Get("after") 735 736 if page == "" && before == "" && after == "" && since == "" { 737 continue 738 } 739 740 if since != "" && page == "" { 741 page = since 742 } 743 744 for _, segment := range segments[1:] { 745 switch strings.TrimSpace(segment) { 746 case `rel="next"`: 747 if r.NextPage, err = strconv.Atoi(page); err != nil { 748 r.NextPageToken = page 749 } 750 r.After = after 751 case `rel="prev"`: 752 r.PrevPage, _ = strconv.Atoi(page) 753 r.Before = before 754 case `rel="first"`: 755 r.FirstPage, _ = strconv.Atoi(page) 756 case `rel="last"`: 757 r.LastPage, _ = strconv.Atoi(page) 758 } 759 } 760 } 761 } 762 } 763 764 // parseRate parses the rate related headers. 765 func parseRate(r *http.Response) Rate { 766 var rate Rate 767 if limit := r.Header.Get(headerRateLimit); limit != "" { 768 rate.Limit, _ = strconv.Atoi(limit) 769 } 770 if remaining := r.Header.Get(headerRateRemaining); remaining != "" { 771 rate.Remaining, _ = strconv.Atoi(remaining) 772 } 773 if used := r.Header.Get(headerRateUsed); used != "" { 774 rate.Used, _ = strconv.Atoi(used) 775 } 776 if reset := r.Header.Get(headerRateReset); reset != "" { 777 if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { 778 rate.Reset = Timestamp{time.Unix(v, 0)} 779 } 780 } 781 if resource := r.Header.Get(headerRateResource); resource != "" { 782 rate.Resource = resource 783 } 784 return rate 785 } 786 787 // parseSecondaryRate parses the secondary rate related headers, 788 // and returns the time to retry after. 789 func parseSecondaryRate(r *http.Response) *time.Duration { 790 // According to GitHub support, the "Retry-After" header value will be 791 // an integer which represents the number of seconds that one should 792 // wait before resuming making requests. 793 if v := r.Header.Get(headerRetryAfter); v != "" { 794 retryAfterSeconds, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. 795 retryAfter := time.Duration(retryAfterSeconds) * time.Second 796 return &retryAfter 797 } 798 799 // According to GitHub support, endpoints might return x-ratelimit-reset instead, 800 // as an integer which represents the number of seconds since epoch UTC, 801 // representing the time to resume making requests. 802 if v := r.Header.Get(headerRateReset); v != "" { 803 secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. 804 retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0)) 805 return &retryAfter 806 } 807 808 return nil 809 } 810 811 // parseTokenExpiration parses the TokenExpiration related headers. 812 // Returns 0001-01-01 if the header is not defined or could not be parsed. 813 func parseTokenExpiration(r *http.Response) Timestamp { 814 if v := r.Header.Get(headerTokenExpiration); v != "" { 815 if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil { 816 return Timestamp{t.Local()} 817 } 818 // Some tokens include the timezone offset instead of the timezone. 819 // https://github.com/google/go-github/issues/2649 820 if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil { 821 return Timestamp{t.Local()} 822 } 823 } 824 return Timestamp{} // 0001-01-01 00:00:00 825 } 826 827 type requestContext uint8 828 829 const ( 830 // BypassRateLimitCheck prevents a pre-emptive check for exceeded primary rate limits 831 // Specify this by providing a context with this key, e.g. 832 // context.WithValue(context.Background(), github.BypassRateLimitCheck, true) 833 BypassRateLimitCheck requestContext = iota 834 835 SleepUntilPrimaryRateLimitResetWhenRateLimited 836 ) 837 838 // bareDo sends an API request using `caller` http.Client passed in the parameters 839 // and lets you handle the api response. If an error or API Error occurs, the error 840 // will contain more information. Otherwise you are supposed to read and close the 841 // response's Body. If rate limit is exceeded and reset time is in the future, 842 // bareDo returns *RateLimitError immediately without making a network API call. 843 // 844 // The provided ctx must be non-nil, if it is nil an error is returned. If it is 845 // canceled or times out, ctx.Err() will be returned. 846 func (c *Client) bareDo(ctx context.Context, caller *http.Client, req *http.Request) (*Response, error) { 847 if ctx == nil { 848 return nil, errNonNilContext 849 } 850 851 req = withContext(ctx, req) 852 853 rateLimitCategory := GetRateLimitCategory(req.Method, req.URL.Path) 854 855 if bypass := ctx.Value(BypassRateLimitCheck); bypass == nil { 856 // If we've hit rate limit, don't make further requests before Reset time. 857 if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil { 858 return &Response{ 859 Response: err.Response, 860 Rate: err.Rate, 861 }, err 862 } 863 // If we've hit a secondary rate limit, don't make further requests before Retry After. 864 if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil { 865 return &Response{ 866 Response: err.Response, 867 }, err 868 } 869 } 870 871 resp, err := caller.Do(req) 872 var response *Response 873 if resp != nil { 874 response = newResponse(resp) 875 } 876 877 if err != nil { 878 // If we got an error, and the context has been canceled, 879 // the context's error is probably more useful. 880 select { 881 case <-ctx.Done(): 882 return response, ctx.Err() 883 default: 884 } 885 886 // If the error type is *url.Error, sanitize its URL before returning. 887 if e, ok := err.(*url.Error); ok { 888 if url, err := url.Parse(e.URL); err == nil { 889 e.URL = sanitizeURL(url).String() 890 return response, e 891 } 892 } 893 894 return response, err 895 } 896 897 // Don't update the rate limits if this was a cached response. 898 // X-From-Cache is set by https://github.com/gregjones/httpcache 899 if response.Header.Get("X-From-Cache") == "" { 900 c.rateMu.Lock() 901 c.rateLimits[rateLimitCategory] = response.Rate 902 c.rateMu.Unlock() 903 } 904 905 err = CheckResponse(resp) 906 if err != nil { 907 defer resp.Body.Close() 908 // Special case for AcceptedErrors. If an AcceptedError 909 // has been encountered, the response's payload will be 910 // added to the AcceptedError and returned. 911 // 912 // Issue #1022 913 aerr, ok := err.(*AcceptedError) 914 if ok { 915 b, readErr := io.ReadAll(resp.Body) 916 if readErr != nil { 917 return response, readErr 918 } 919 920 aerr.Raw = b 921 err = aerr 922 } 923 924 rateLimitError, ok := err.(*RateLimitError) 925 if ok && req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil { 926 if err := sleepUntilResetWithBuffer(req.Context(), rateLimitError.Rate.Reset.Time); err != nil { 927 return response, err 928 } 929 // retry the request once when the rate limit has reset 930 return c.bareDo(context.WithValue(req.Context(), SleepUntilPrimaryRateLimitResetWhenRateLimited, nil), caller, req) 931 } 932 933 // Update the secondary rate limit if we hit it. 934 rerr, ok := err.(*AbuseRateLimitError) 935 if ok && rerr.RetryAfter != nil { 936 // if a max duration is specified, make sure that we are waiting at most this duration 937 if c.MaxSecondaryRateLimitRetryAfterDuration > 0 && rerr.GetRetryAfter() > c.MaxSecondaryRateLimitRetryAfterDuration { 938 rerr.RetryAfter = &c.MaxSecondaryRateLimitRetryAfterDuration 939 } 940 c.rateMu.Lock() 941 c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter) 942 c.rateMu.Unlock() 943 } 944 } 945 return response, err 946 } 947 948 // BareDo sends an API request and lets you handle the api response. If an error 949 // or API Error occurs, the error will contain more information. Otherwise you 950 // are supposed to read and close the response's Body. If rate limit is exceeded 951 // and reset time is in the future, BareDo returns *RateLimitError immediately 952 // without making a network API call. 953 // 954 // The provided ctx must be non-nil, if it is nil an error is returned. If it is 955 // canceled or times out, ctx.Err() will be returned. 956 func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) { 957 return c.bareDo(ctx, c.client, req) 958 } 959 960 // bareDoIgnoreRedirects has the exact same behavior as BareDo but stops at the first 961 // redirection code returned by the API. If a redirection is returned by the api, bareDoIgnoreRedirects 962 // returns a *RedirectionError. 963 // 964 // The provided ctx must be non-nil, if it is nil an error is returned. If it is 965 // canceled or times out, ctx.Err() will be returned. 966 func (c *Client) bareDoIgnoreRedirects(ctx context.Context, req *http.Request) (*Response, error) { 967 return c.bareDo(ctx, c.clientIgnoreRedirects, req) 968 } 969 970 var errInvalidLocation = errors.New("invalid or empty Location header in redirection response") 971 972 // bareDoUntilFound has the exact same behavior as BareDo but only follows 301s, up to maxRedirects times. If it receives 973 // a 302, it will parse the Location header into a *url.URL and return that. 974 // This is useful for endpoints that return a 302 in successful cases but still might return 301s for 975 // permanent redirections. 976 // 977 // The provided ctx must be non-nil, if it is nil an error is returned. If it is 978 // canceled or times out, ctx.Err() will be returned. 979 func (c *Client) bareDoUntilFound(ctx context.Context, req *http.Request, maxRedirects int) (*url.URL, *Response, error) { 980 response, err := c.bareDoIgnoreRedirects(ctx, req) 981 if err != nil { 982 rerr, ok := err.(*RedirectionError) 983 if ok { 984 // If we receive a 302, transform potential relative locations into absolute and return it. 985 if rerr.StatusCode == http.StatusFound { 986 if rerr.Location == nil { 987 return nil, nil, errInvalidLocation 988 } 989 newURL := c.BaseURL.ResolveReference(rerr.Location) 990 return newURL, response, nil 991 } 992 // If permanent redirect response is returned, follow it 993 if maxRedirects > 0 && rerr.StatusCode == http.StatusMovedPermanently { 994 if rerr.Location == nil { 995 return nil, nil, errInvalidLocation 996 } 997 newURL := c.BaseURL.ResolveReference(rerr.Location) 998 newRequest := req.Clone(ctx) 999 newRequest.URL = newURL 1000 return c.bareDoUntilFound(ctx, newRequest, maxRedirects-1) 1001 } 1002 // If we reached the maximum amount of redirections, return an error 1003 if maxRedirects <= 0 && rerr.StatusCode == http.StatusMovedPermanently { 1004 return nil, response, fmt.Errorf("reached the maximum amount of redirections: %w", err) 1005 } 1006 return nil, response, fmt.Errorf("unexpected redirection response: %w", err) 1007 } 1008 } 1009 1010 // If we don't receive a redirection, forward the response and potential error 1011 return nil, response, err 1012 } 1013 1014 // Do sends an API request and returns the API response. The API response is 1015 // JSON decoded and stored in the value pointed to by v, or returned as an 1016 // error if an API error has occurred. If v implements the io.Writer interface, 1017 // the raw response body will be written to v, without attempting to first 1018 // decode it. If v is nil, and no error happens, the response is returned as is. 1019 // If rate limit is exceeded and reset time is in the future, Do returns 1020 // *RateLimitError immediately without making a network API call. 1021 // 1022 // The provided ctx must be non-nil, if it is nil an error is returned. If it 1023 // is canceled or times out, ctx.Err() will be returned. 1024 func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { 1025 resp, err := c.BareDo(ctx, req) 1026 if err != nil { 1027 return resp, err 1028 } 1029 defer resp.Body.Close() 1030 1031 switch v := v.(type) { 1032 case nil: 1033 case io.Writer: 1034 _, err = io.Copy(v, resp.Body) 1035 default: 1036 decErr := json.NewDecoder(resp.Body).Decode(v) 1037 if decErr == io.EOF { 1038 decErr = nil // ignore EOF errors caused by empty response body 1039 } 1040 if decErr != nil { 1041 err = decErr 1042 } 1043 } 1044 return resp, err 1045 } 1046 1047 // checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 1048 // current client state in order to quickly check if *RateLimitError can be immediately returned 1049 // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 1050 // Otherwise it returns nil, and Client.Do should proceed normally. 1051 func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory RateLimitCategory) *RateLimitError { 1052 c.rateMu.Lock() 1053 rate := c.rateLimits[rateLimitCategory] 1054 c.rateMu.Unlock() 1055 if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) { 1056 // Create a fake response. 1057 resp := &http.Response{ 1058 Status: http.StatusText(http.StatusForbidden), 1059 StatusCode: http.StatusForbidden, 1060 Request: req, 1061 Header: make(http.Header), 1062 Body: io.NopCloser(strings.NewReader("")), 1063 } 1064 1065 if req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil { 1066 if err := sleepUntilResetWithBuffer(req.Context(), rate.Reset.Time); err == nil { 1067 return nil 1068 } 1069 return &RateLimitError{ 1070 Rate: rate, 1071 Response: resp, 1072 Message: fmt.Sprintf("Context cancelled while waiting for rate limit to reset until %v, not making remote request.", rate.Reset.Time), 1073 } 1074 } 1075 1076 return &RateLimitError{ 1077 Rate: rate, 1078 Response: resp, 1079 Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time), 1080 } 1081 } 1082 1083 return nil 1084 } 1085 1086 // checkSecondaryRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 1087 // current client state in order to quickly check if *AbuseRateLimitError can be immediately returned 1088 // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 1089 // Otherwise it returns nil, and Client.Do should proceed normally. 1090 func (c *Client) checkSecondaryRateLimitBeforeDo(req *http.Request) *AbuseRateLimitError { 1091 c.rateMu.Lock() 1092 secondary := c.secondaryRateLimitReset 1093 c.rateMu.Unlock() 1094 if !secondary.IsZero() && time.Now().Before(secondary) { 1095 // Create a fake response. 1096 resp := &http.Response{ 1097 Status: http.StatusText(http.StatusForbidden), 1098 StatusCode: http.StatusForbidden, 1099 Request: req, 1100 Header: make(http.Header), 1101 Body: io.NopCloser(strings.NewReader("")), 1102 } 1103 1104 retryAfter := time.Until(secondary) 1105 return &AbuseRateLimitError{ 1106 Response: resp, 1107 Message: fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary), 1108 RetryAfter: &retryAfter, 1109 } 1110 } 1111 1112 return nil 1113 } 1114 1115 // compareHTTPResponse returns whether two http.Response objects are equal or not. 1116 // Currently, only StatusCode is checked. This function is used when implementing the 1117 // Is(error) bool interface for the custom error types in this package. 1118 func compareHTTPResponse(r1, r2 *http.Response) bool { 1119 if r1 == nil && r2 == nil { 1120 return true 1121 } 1122 1123 if r1 != nil && r2 != nil { 1124 return r1.StatusCode == r2.StatusCode 1125 } 1126 return false 1127 } 1128 1129 /* 1130 An ErrorResponse reports one or more errors caused by an API request. 1131 1132 GitHub API docs: https://docs.github.com/rest/#client-errors 1133 */ 1134 type ErrorResponse struct { 1135 Response *http.Response `json:"-"` // HTTP response that caused this error 1136 Message string `json:"message"` // error message 1137 //nolint:sliceofpointers 1138 Errors []Error `json:"errors"` // more detail on individual errors 1139 // Block is only populated on certain types of errors such as code 451. 1140 Block *ErrorBlock `json:"block,omitempty"` 1141 // Most errors will also include a documentation_url field pointing 1142 // to some content that might help you resolve the error, see 1143 // https://docs.github.com/rest/#client-errors 1144 DocumentationURL string `json:"documentation_url,omitempty"` 1145 } 1146 1147 // ErrorBlock contains a further explanation for the reason of an error. 1148 // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/ 1149 // for more information. 1150 type ErrorBlock struct { 1151 Reason string `json:"reason,omitempty"` 1152 CreatedAt *Timestamp `json:"created_at,omitempty"` 1153 } 1154 1155 func (r *ErrorResponse) Error() string { 1156 if r.Response != nil && r.Response.Request != nil { 1157 return fmt.Sprintf("%v %v: %d %v %+v", 1158 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1159 r.Response.StatusCode, r.Message, r.Errors) 1160 } 1161 1162 if r.Response != nil { 1163 return fmt.Sprintf("%d %v %+v", r.Response.StatusCode, r.Message, r.Errors) 1164 } 1165 1166 return fmt.Sprintf("%v %+v", r.Message, r.Errors) 1167 } 1168 1169 // Is returns whether the provided error equals this error. 1170 func (r *ErrorResponse) Is(target error) bool { 1171 v, ok := target.(*ErrorResponse) 1172 if !ok { 1173 return false 1174 } 1175 1176 if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) || 1177 !compareHTTPResponse(r.Response, v.Response) { 1178 return false 1179 } 1180 1181 // Compare Errors. 1182 if len(r.Errors) != len(v.Errors) { 1183 return false 1184 } 1185 for idx := range r.Errors { 1186 if r.Errors[idx] != v.Errors[idx] { 1187 return false 1188 } 1189 } 1190 1191 // Compare Block. 1192 if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) { 1193 return false 1194 } 1195 if r.Block != nil && v.Block != nil { 1196 if r.Block.Reason != v.Block.Reason { 1197 return false 1198 } 1199 if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt == 1200 nil && v.Block.CreatedAt != nil) { 1201 return false 1202 } 1203 if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil { 1204 if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) { 1205 return false 1206 } 1207 } 1208 } 1209 1210 return true 1211 } 1212 1213 // TwoFactorAuthError occurs when using HTTP Basic Authentication for a user 1214 // that has two-factor authentication enabled. The request can be reattempted 1215 // by providing a one-time password in the request. 1216 type TwoFactorAuthError ErrorResponse 1217 1218 func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() } 1219 1220 // RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit 1221 // remaining value of 0. 1222 type RateLimitError struct { 1223 Rate Rate // Rate specifies last known rate limit for the client 1224 Response *http.Response // HTTP response that caused this error 1225 Message string `json:"message"` // error message 1226 } 1227 1228 func (r *RateLimitError) Error() string { 1229 return fmt.Sprintf("%v %v: %d %v %v", 1230 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1231 r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time))) 1232 } 1233 1234 // Is returns whether the provided error equals this error. 1235 func (r *RateLimitError) Is(target error) bool { 1236 v, ok := target.(*RateLimitError) 1237 if !ok { 1238 return false 1239 } 1240 1241 return r.Rate == v.Rate && 1242 r.Message == v.Message && 1243 compareHTTPResponse(r.Response, v.Response) 1244 } 1245 1246 // AcceptedError occurs when GitHub returns 202 Accepted response with an 1247 // empty body, which means a job was scheduled on the GitHub side to process 1248 // the information needed and cache it. 1249 // Technically, 202 Accepted is not a real error, it's just used to 1250 // indicate that results are not ready yet, but should be available soon. 1251 // The request can be repeated after some time. 1252 type AcceptedError struct { 1253 // Raw contains the response body. 1254 Raw []byte 1255 } 1256 1257 func (*AcceptedError) Error() string { 1258 return "job scheduled on GitHub side; try again later" 1259 } 1260 1261 // Is returns whether the provided error equals this error. 1262 func (ae *AcceptedError) Is(target error) bool { 1263 v, ok := target.(*AcceptedError) 1264 if !ok { 1265 return false 1266 } 1267 return bytes.Equal(ae.Raw, v.Raw) 1268 } 1269 1270 // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the 1271 // "documentation_url" field value equal to "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits". 1272 type AbuseRateLimitError struct { 1273 Response *http.Response // HTTP response that caused this error 1274 Message string `json:"message"` // error message 1275 1276 // RetryAfter is provided with some abuse rate limit errors. If present, 1277 // it is the amount of time that the client should wait before retrying. 1278 // Otherwise, the client should try again later (after an unspecified amount of time). 1279 RetryAfter *time.Duration 1280 } 1281 1282 func (r *AbuseRateLimitError) Error() string { 1283 return fmt.Sprintf("%v %v: %d %v", 1284 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1285 r.Response.StatusCode, r.Message) 1286 } 1287 1288 // Is returns whether the provided error equals this error. 1289 func (r *AbuseRateLimitError) Is(target error) bool { 1290 v, ok := target.(*AbuseRateLimitError) 1291 if !ok { 1292 return false 1293 } 1294 1295 return r.Message == v.Message && 1296 r.RetryAfter == v.RetryAfter && 1297 compareHTTPResponse(r.Response, v.Response) 1298 } 1299 1300 // RedirectionError represents a response that returned a redirect status code: 1301 // 1302 // 301 (Moved Permanently) 1303 // 302 (Found) 1304 // 303 (See Other) 1305 // 307 (Temporary Redirect) 1306 // 308 (Permanent Redirect) 1307 // 1308 // If there was a valid Location header included, it will be parsed to a URL. You should use 1309 // `BaseURL.ResolveReference()` to enrich it with the correct hostname where needed. 1310 type RedirectionError struct { 1311 Response *http.Response // HTTP response that caused this error 1312 StatusCode int 1313 Location *url.URL // location header of the redirection if present 1314 } 1315 1316 func (r *RedirectionError) Error() string { 1317 return fmt.Sprintf("%v %v: %d location %v", 1318 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1319 r.StatusCode, sanitizeURL(r.Location)) 1320 } 1321 1322 // Is returns whether the provided error equals this error. 1323 func (r *RedirectionError) Is(target error) bool { 1324 v, ok := target.(*RedirectionError) 1325 if !ok { 1326 return false 1327 } 1328 1329 return r.StatusCode == v.StatusCode && 1330 (r.Location == v.Location || // either both locations are nil or exactly the same pointer 1331 r.Location != nil && v.Location != nil && r.Location.String() == v.Location.String()) // or they are both not nil and marshaled identically 1332 } 1333 1334 // sanitizeURL redacts the client_secret parameter from the URL which may be 1335 // exposed to the user. 1336 func sanitizeURL(uri *url.URL) *url.URL { 1337 if uri == nil { 1338 return nil 1339 } 1340 params := uri.Query() 1341 if len(params.Get("client_secret")) > 0 { 1342 params.Set("client_secret", "REDACTED") 1343 uri.RawQuery = params.Encode() 1344 } 1345 return uri 1346 } 1347 1348 /* 1349 An Error reports more details on an individual error in an ErrorResponse. 1350 These are the possible validation error codes: 1351 1352 missing: 1353 resource does not exist 1354 missing_field: 1355 a required field on a resource has not been set 1356 invalid: 1357 the formatting of a field is invalid 1358 already_exists: 1359 another resource has the same valid as this field 1360 custom: 1361 some resources return this (e.g. github.User.CreateKey()), additional 1362 information is set in the Message field of the Error 1363 1364 GitHub error responses structure are often undocumented and inconsistent. 1365 Sometimes error is just a simple string (Issue #540). 1366 In such cases, Message represents an error message as a workaround. 1367 1368 GitHub API docs: https://docs.github.com/rest/#client-errors 1369 */ 1370 type Error struct { 1371 Resource string `json:"resource"` // resource on which the error occurred 1372 Field string `json:"field"` // field on which the error occurred 1373 Code string `json:"code"` // validation error code 1374 Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set. 1375 } 1376 1377 func (e *Error) Error() string { 1378 return fmt.Sprintf("%v error caused by %v field on %v resource", 1379 e.Code, e.Field, e.Resource) 1380 } 1381 1382 func (e *Error) UnmarshalJSON(data []byte) error { 1383 type aliasError Error // avoid infinite recursion by using type alias. 1384 if err := json.Unmarshal(data, (*aliasError)(e)); err != nil { 1385 return json.Unmarshal(data, &e.Message) // data can be json string. 1386 } 1387 return nil 1388 } 1389 1390 // CheckResponse checks the API response for errors, and returns them if 1391 // present. A response is considered an error if it has a status code outside 1392 // the 200 range or equal to 202 Accepted. 1393 // API error responses are expected to have response 1394 // body, and a JSON response body that maps to ErrorResponse. 1395 // 1396 // The error type will be *RateLimitError for rate limit exceeded errors, 1397 // *AcceptedError for 202 Accepted status codes, 1398 // *TwoFactorAuthError for two-factor authentication errors, 1399 // and *RedirectionError for redirect status codes (only happens when ignoring redirections). 1400 func CheckResponse(r *http.Response) error { 1401 if r.StatusCode == http.StatusAccepted { 1402 return &AcceptedError{} 1403 } 1404 if c := r.StatusCode; 200 <= c && c <= 299 { 1405 return nil 1406 } 1407 1408 errorResponse := &ErrorResponse{Response: r} 1409 data, err := io.ReadAll(r.Body) 1410 if err == nil && data != nil { 1411 err = json.Unmarshal(data, errorResponse) 1412 if err != nil { 1413 // reset the response as if this never happened 1414 errorResponse = &ErrorResponse{Response: r} 1415 } 1416 } 1417 // Re-populate error response body because GitHub error responses are often 1418 // undocumented and inconsistent. 1419 // Issue #1136, #540. 1420 r.Body = io.NopCloser(bytes.NewBuffer(data)) 1421 switch { 1422 case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"): 1423 return (*TwoFactorAuthError)(errorResponse) 1424 case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0": 1425 return &RateLimitError{ 1426 Rate: parseRate(r), 1427 Response: errorResponse.Response, 1428 Message: errorResponse.Message, 1429 } 1430 case r.StatusCode == http.StatusForbidden && 1431 (strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") || 1432 strings.HasSuffix(errorResponse.DocumentationURL, "secondary-rate-limits")): 1433 abuseRateLimitError := &AbuseRateLimitError{ 1434 Response: errorResponse.Response, 1435 Message: errorResponse.Message, 1436 } 1437 if retryAfter := parseSecondaryRate(r); retryAfter != nil { 1438 abuseRateLimitError.RetryAfter = retryAfter 1439 } 1440 return abuseRateLimitError 1441 // Check that the status code is a redirection and return a sentinel error that can be used to handle special cases 1442 // where 302 is considered a successful result. 1443 // This should never happen with the default `CheckRedirect`, because it would return a `url.Error` that should be handled upstream. 1444 case r.StatusCode == http.StatusMovedPermanently || 1445 r.StatusCode == http.StatusFound || 1446 r.StatusCode == http.StatusSeeOther || 1447 r.StatusCode == http.StatusTemporaryRedirect || 1448 r.StatusCode == http.StatusPermanentRedirect: 1449 1450 locationStr := r.Header.Get("Location") 1451 var location *url.URL 1452 if locationStr != "" { 1453 location, _ = url.Parse(locationStr) 1454 } 1455 return &RedirectionError{ 1456 Response: errorResponse.Response, 1457 StatusCode: r.StatusCode, 1458 Location: location, 1459 } 1460 default: 1461 return errorResponse 1462 } 1463 } 1464 1465 // parseBoolResponse determines the boolean result from a GitHub API response. 1466 // Several GitHub API methods return boolean responses indicated by the HTTP 1467 // status code in the response (true indicated by a 204, false indicated by a 1468 // 404). This helper function will determine that result and hide the 404 1469 // error if present. Any other error will be returned through as-is. 1470 func parseBoolResponse(err error) (bool, error) { 1471 if err == nil { 1472 return true, nil 1473 } 1474 1475 if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound { 1476 // Simply false. In this one case, we do not pass the error through. 1477 return false, nil 1478 } 1479 1480 // some other real error occurred 1481 return false, err 1482 } 1483 1484 type RateLimitCategory uint8 1485 1486 const ( 1487 CoreCategory RateLimitCategory = iota 1488 SearchCategory 1489 GraphqlCategory 1490 IntegrationManifestCategory 1491 SourceImportCategory 1492 CodeScanningUploadCategory 1493 ActionsRunnerRegistrationCategory 1494 ScimCategory 1495 DependencySnapshotsCategory 1496 CodeSearchCategory 1497 AuditLogCategory 1498 1499 Categories // An array of this length will be able to contain all rate limit categories. 1500 ) 1501 1502 // GetRateLimitCategory returns the rate limit RateLimitCategory of the endpoint, determined by HTTP method and Request.URL.Path. 1503 func GetRateLimitCategory(method, path string) RateLimitCategory { 1504 switch { 1505 // https://docs.github.com/rest/rate-limit#about-rate-limits 1506 default: 1507 // NOTE: coreCategory is returned for actionsRunnerRegistrationCategory too, 1508 // because no API found for this category. 1509 return CoreCategory 1510 1511 // https://docs.github.com/en/rest/search/search#search-code 1512 case strings.HasPrefix(path, "/search/code") && 1513 method == http.MethodGet: 1514 return CodeSearchCategory 1515 1516 case strings.HasPrefix(path, "/search/"): 1517 return SearchCategory 1518 case path == "/graphql": 1519 return GraphqlCategory 1520 case strings.HasPrefix(path, "/app-manifests/") && 1521 strings.HasSuffix(path, "/conversions") && 1522 method == http.MethodPost: 1523 return IntegrationManifestCategory 1524 1525 // https://docs.github.com/rest/migrations/source-imports#start-an-import 1526 case strings.HasPrefix(path, "/repos/") && 1527 strings.HasSuffix(path, "/import") && 1528 method == http.MethodPut: 1529 return SourceImportCategory 1530 1531 // https://docs.github.com/rest/code-scanning#upload-an-analysis-as-sarif-data 1532 case strings.HasSuffix(path, "/code-scanning/sarifs"): 1533 return CodeScanningUploadCategory 1534 1535 // https://docs.github.com/enterprise-cloud@latest/rest/scim 1536 case strings.HasPrefix(path, "/scim/"): 1537 return ScimCategory 1538 1539 // https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository 1540 case strings.HasPrefix(path, "/repos/") && 1541 strings.HasSuffix(path, "/dependency-graph/snapshots") && 1542 method == http.MethodPost: 1543 return DependencySnapshotsCategory 1544 1545 // https://docs.github.com/en/enterprise-cloud@latest/rest/orgs/orgs?apiVersion=2022-11-28#get-the-audit-log-for-an-organization 1546 case strings.HasSuffix(path, "/audit-log"): 1547 return AuditLogCategory 1548 } 1549 } 1550 1551 // RateLimits returns the rate limits for the current client. 1552 // 1553 // Deprecated: Use RateLimitService.Get instead. 1554 func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) { 1555 return c.RateLimit.Get(ctx) 1556 } 1557 1558 func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request { 1559 // To set extra headers, we must make a copy of the Request so 1560 // that we don't modify the Request we were given. This is required by the 1561 // specification of http.RoundTripper. 1562 // 1563 // Since we are going to modify only req.Header here, we only need a deep copy 1564 // of req.Header. 1565 convertedRequest := new(http.Request) 1566 *convertedRequest = *req 1567 convertedRequest.Header = make(http.Header, len(req.Header)) 1568 1569 for k, s := range req.Header { 1570 convertedRequest.Header[k] = append([]string(nil), s...) 1571 } 1572 convertedRequest.SetBasicAuth(id, secret) 1573 return convertedRequest 1574 } 1575 1576 /* 1577 UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls 1578 that need to use a higher rate limit associated with your OAuth application. 1579 1580 t := &github.UnauthenticatedRateLimitedTransport{ 1581 ClientID: "your app's client ID", 1582 ClientSecret: "your app's client secret", 1583 } 1584 client := github.NewClient(t.Client()) 1585 1586 This will add the client id and secret as a base64-encoded string in the format 1587 ClientID:ClientSecret and apply it as an "Authorization": "Basic" header. 1588 1589 See https://docs.github.com/rest/#unauthenticated-rate-limited-requests for 1590 more information. 1591 */ 1592 type UnauthenticatedRateLimitedTransport struct { 1593 // ClientID is the GitHub OAuth client ID of the current application, which 1594 // can be found by selecting its entry in the list at 1595 // https://github.com/settings/applications. 1596 ClientID string 1597 1598 // ClientSecret is the GitHub OAuth client secret of the current 1599 // application. 1600 ClientSecret string 1601 1602 // Transport is the underlying HTTP transport to use when making requests. 1603 // It will default to http.DefaultTransport if nil. 1604 Transport http.RoundTripper 1605 } 1606 1607 // RoundTrip implements the RoundTripper interface. 1608 func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 1609 if t.ClientID == "" { 1610 return nil, errors.New("t.ClientID is empty") 1611 } 1612 if t.ClientSecret == "" { 1613 return nil, errors.New("t.ClientSecret is empty") 1614 } 1615 1616 req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret) 1617 // Make the HTTP request. 1618 return t.transport().RoundTrip(req2) 1619 } 1620 1621 // Client returns an *http.Client that makes requests which are subject to the 1622 // rate limit of your OAuth application. 1623 func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client { 1624 return &http.Client{Transport: t} 1625 } 1626 1627 func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper { 1628 if t.Transport != nil { 1629 return t.Transport 1630 } 1631 return http.DefaultTransport 1632 } 1633 1634 // BasicAuthTransport is an http.RoundTripper that authenticates all requests 1635 // using HTTP Basic Authentication with the provided username and password. It 1636 // additionally supports users who have two-factor authentication enabled on 1637 // their GitHub account. 1638 type BasicAuthTransport struct { 1639 Username string // GitHub username 1640 Password string // GitHub password 1641 OTP string // one-time password for users with two-factor auth enabled 1642 1643 // Transport is the underlying HTTP transport to use when making requests. 1644 // It will default to http.DefaultTransport if nil. 1645 Transport http.RoundTripper 1646 } 1647 1648 // RoundTrip implements the RoundTripper interface. 1649 func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { 1650 req2 := setCredentialsAsHeaders(req, t.Username, t.Password) 1651 if t.OTP != "" { 1652 req2.Header.Set(headerOTP, t.OTP) 1653 } 1654 return t.transport().RoundTrip(req2) 1655 } 1656 1657 // Client returns an *http.Client that makes requests that are authenticated 1658 // using HTTP Basic Authentication. 1659 func (t *BasicAuthTransport) Client() *http.Client { 1660 return &http.Client{Transport: t} 1661 } 1662 1663 func (t *BasicAuthTransport) transport() http.RoundTripper { 1664 if t.Transport != nil { 1665 return t.Transport 1666 } 1667 return http.DefaultTransport 1668 } 1669 1670 // formatRateReset formats d to look like "[rate reset in 2s]" or 1671 // "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]" 1672 // for the negative cases. 1673 func formatRateReset(d time.Duration) string { 1674 isNegative := d < 0 1675 if isNegative { 1676 d *= -1 1677 } 1678 secondsTotal := int(0.5 + d.Seconds()) 1679 minutes := secondsTotal / 60 1680 seconds := secondsTotal - minutes*60 1681 1682 var timeString string 1683 if minutes > 0 { 1684 timeString = fmt.Sprintf("%dm%02ds", minutes, seconds) 1685 } else { 1686 timeString = fmt.Sprintf("%ds", seconds) 1687 } 1688 1689 if isNegative { 1690 return fmt.Sprintf("[rate limit was reset %v ago]", timeString) 1691 } 1692 return fmt.Sprintf("[rate reset in %v]", timeString) 1693 } 1694 1695 func sleepUntilResetWithBuffer(ctx context.Context, reset time.Time) error { 1696 buffer := time.Second 1697 timer := time.NewTimer(time.Until(reset) + buffer) 1698 select { 1699 case <-ctx.Done(): 1700 if !timer.Stop() { 1701 <-timer.C 1702 } 1703 return ctx.Err() 1704 case <-timer.C: 1705 } 1706 return nil 1707 } 1708 1709 // When using roundTripWithOptionalFollowRedirect, note that it 1710 // is the responsibility of the caller to close the response body. 1711 func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, maxRedirects int, opts ...RequestOption) (*http.Response, error) { 1712 req, err := c.NewRequest("GET", u, nil, opts...) 1713 if err != nil { 1714 return nil, err 1715 } 1716 1717 var resp *http.Response 1718 // Use http.DefaultTransport if no custom Transport is configured 1719 req = withContext(ctx, req) 1720 if c.client.Transport == nil { 1721 resp, err = http.DefaultTransport.RoundTrip(req) 1722 } else { 1723 resp, err = c.client.Transport.RoundTrip(req) 1724 } 1725 if err != nil { 1726 return nil, err 1727 } 1728 1729 // If redirect response is returned, follow it 1730 if maxRedirects > 0 && resp.StatusCode == http.StatusMovedPermanently { 1731 _ = resp.Body.Close() 1732 u = resp.Header.Get("Location") 1733 resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, maxRedirects-1, opts...) 1734 } 1735 return resp, err 1736 } 1737 1738 // Ptr is a helper routine that allocates a new T value 1739 // to store v and returns a pointer to it. 1740 func Ptr[T any](v T) *T { 1741 return &v 1742 } 1743 1744 // Bool is a helper routine that allocates a new bool value 1745 // to store v and returns a pointer to it. 1746 // 1747 // Deprecated: use Ptr instead. 1748 func Bool(v bool) *bool { return &v } 1749 1750 // Int is a helper routine that allocates a new int value 1751 // to store v and returns a pointer to it. 1752 // 1753 // Deprecated: use Ptr instead. 1754 func Int(v int) *int { return &v } 1755 1756 // Int64 is a helper routine that allocates a new int64 value 1757 // to store v and returns a pointer to it. 1758 // 1759 // Deprecated: use Ptr instead. 1760 func Int64(v int64) *int64 { return &v } 1761 1762 // String is a helper routine that allocates a new string value 1763 // to store v and returns a pointer to it. 1764 // 1765 // Deprecated: use Ptr instead. 1766 func String(v string) *string { return &v } 1767 1768 // roundTripperFunc creates a RoundTripper (transport). 1769 type roundTripperFunc func(*http.Request) (*http.Response, error) 1770 1771 func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { 1772 return fn(r) 1773 } 1774 1775 var runIDFromURLRE = regexp.MustCompile(`^repos/.*/actions/runs/(\d+)/deployment_protection_rule$`) 1776 1777 // GetRunID is a Helper Function used to extract the workflow RunID from the *DeploymentProtectionRuleEvent.DeploymentCallBackURL. 1778 func (e *DeploymentProtectionRuleEvent) GetRunID() (int64, error) { 1779 match := runIDFromURLRE.FindStringSubmatch(*e.DeploymentCallbackURL) 1780 if len(match) != 2 { 1781 return -1, errors.New("no match") 1782 } 1783 runID, err := strconv.ParseInt(match[1], 10, 64) 1784 if err != nil { 1785 return -1, err 1786 } 1787 return runID, nil 1788 }