Skip to content

Commit 9d020d7

Browse files
authored
Support new GitHub v3 API calendar-based versioning (#2581)
Fixes: #2580.
1 parent 3e3f03c commit 9d020d7

File tree

3 files changed

+114
-10
lines changed

3 files changed

+114
-10
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,40 @@ Preview functionality may take the form of entire methods or simply additional
332332
data returned from an otherwise non-preview method. Refer to the GitHub API
333333
documentation for details on preview functionality.
334334

335+
### Calendar Versioning ###
336+
337+
As of 2022-11-28, GitHub [has announced](https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/)
338+
that they are starting to version their v3 API based on "calendar-versioning".
339+
340+
In practice, our goal is to make per-method version overrides (at
341+
least in the core library) rare and temporary.
342+
343+
Our understanding of the GitHub docs is that they will be revving the
344+
entire API to each new date-based version, even if only a few methods
345+
have breaking changes. Other methods will accept the new version with
346+
their existing functionality. So when a new date-based version of the
347+
GitHub API is released, we (the repo maintainers) plan to:
348+
349+
* update each method that had breaking changes, overriding their
350+
per-method API version header. This may happen in one or multiple
351+
commits and PRs, and is all done in the main branch.
352+
353+
* once all of the methods with breaking changes have been updated,
354+
have a final commit that bumps the default API version, and remove
355+
all of the per-method overrides. That would now get a major version
356+
bump when the next go-github release is made.
357+
358+
### Version Compatibility Table ###
359+
360+
The following table identifies which version of the GitHub API is
361+
supported by this (and past) versions of this repo (go-github).
362+
Versions prior to 48.2.0 are not listed.
363+
364+
| go-github Version | GitHub v3 API Version |
365+
| ----------------- | --------------------- |
366+
| 48.2.0 | 2022-11-28 |
367+
368+
335369
## License ##
336370

337371
This library is distributed under the BSD-style license found in the [LICENSE](./LICENSE)

github/github.go

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import (
2727
)
2828

2929
const (
30-
Version = "v48.0.0"
30+
Version = "v48.2.0"
3131

32-
defaultBaseURL = "https://api.github.com/"
33-
defaultUserAgent = "go-github" + "/" + Version
34-
uploadBaseURL = "https://uploads.github.com/"
32+
defaultAPIVersion = "2022-11-28"
33+
defaultBaseURL = "https://api.github.com/"
34+
defaultUserAgent = "go-github" + "/" + Version
35+
uploadBaseURL = "https://uploads.github.com/"
3536

37+
headerAPIVersion = "X-GitHub-Api-Version"
3638
headerRateLimit = "X-RateLimit-Limit"
3739
headerRateRemaining = "X-RateLimit-Remaining"
3840
headerRateReset = "X-RateLimit-Reset"
@@ -392,12 +394,24 @@ func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*C
392394
return c, nil
393395
}
394396

397+
// RequestOption represents an option that can modify an http.Request.
398+
type RequestOption func(req *http.Request)
399+
400+
// WithVersion overrides the GitHub v3 API version for this individual request.
401+
// For more information, see:
402+
// https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/
403+
func WithVersion(version string) RequestOption {
404+
return func(req *http.Request) {
405+
req.Header.Set(headerAPIVersion, version)
406+
}
407+
}
408+
395409
// NewRequest creates an API request. A relative URL can be provided in urlStr,
396410
// in which case it is resolved relative to the BaseURL of the Client.
397411
// Relative URLs should always be specified without a preceding slash. If
398412
// specified, the value pointed to by body is JSON encoded and included as the
399413
// request body.
400-
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
414+
func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) {
401415
if !strings.HasSuffix(c.BaseURL.Path, "/") {
402416
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
403417
}
@@ -430,14 +444,20 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
430444
if c.UserAgent != "" {
431445
req.Header.Set("User-Agent", c.UserAgent)
432446
}
447+
req.Header.Set(headerAPIVersion, defaultAPIVersion)
448+
449+
for _, opt := range opts {
450+
opt(req)
451+
}
452+
433453
return req, nil
434454
}
435455

436456
// NewFormRequest creates an API request. A relative URL can be provided in urlStr,
437457
// in which case it is resolved relative to the BaseURL of the Client.
438458
// Relative URLs should always be specified without a preceding slash.
439459
// Body is sent with Content-Type: application/x-www-form-urlencoded.
440-
func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, error) {
460+
func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
441461
if !strings.HasSuffix(c.BaseURL.Path, "/") {
442462
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
443463
}
@@ -457,13 +477,19 @@ func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, e
457477
if c.UserAgent != "" {
458478
req.Header.Set("User-Agent", c.UserAgent)
459479
}
480+
req.Header.Set(headerAPIVersion, defaultAPIVersion)
481+
482+
for _, opt := range opts {
483+
opt(req)
484+
}
485+
460486
return req, nil
461487
}
462488

463489
// NewUploadRequest creates an upload request. A relative URL can be provided in
464490
// urlStr, in which case it is resolved relative to the UploadURL of the Client.
465491
// Relative URLs should always be specified without a preceding slash.
466-
func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) {
492+
func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) {
467493
if !strings.HasSuffix(c.UploadURL.Path, "/") {
468494
return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
469495
}
@@ -485,6 +511,12 @@ func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, m
485511
req.Header.Set("Content-Type", mediaType)
486512
req.Header.Set("Accept", mediaTypeV3)
487513
req.Header.Set("User-Agent", c.UserAgent)
514+
req.Header.Set(headerAPIVersion, defaultAPIVersion)
515+
516+
for _, opt := range opts {
517+
opt(req)
518+
}
519+
488520
return req, nil
489521
}
490522

@@ -1358,8 +1390,8 @@ func formatRateReset(d time.Duration) string {
13581390

13591391
// When using roundTripWithOptionalFollowRedirect, note that it
13601392
// is the responsibility of the caller to close the response body.
1361-
func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
1362-
req, err := c.NewRequest("GET", u, nil)
1393+
func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool, opts ...RequestOption) (*http.Response, error) {
1394+
req, err := c.NewRequest("GET", u, nil, opts...)
13631395
if err != nil {
13641396
return nil, err
13651397
}
@@ -1380,7 +1412,7 @@ func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u stri
13801412
if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
13811413
resp.Body.Close()
13821414
u = resp.Header.Get("Location")
1383-
resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false)
1415+
resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false, opts...)
13841416
}
13851417
return resp, err
13861418
}

github/github_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,17 @@ func TestNewRequest(t *testing.T) {
517517
if !strings.Contains(userAgent, Version) {
518518
t.Errorf("NewRequest() User-Agent should contain %v, found %v", Version, userAgent)
519519
}
520+
521+
apiVersion := req.Header.Get(headerAPIVersion)
522+
if got, want := apiVersion, defaultAPIVersion; got != want {
523+
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
524+
}
525+
526+
req, _ = c.NewRequest("GET", inURL, inBody, WithVersion("2022-11-29"))
527+
apiVersion = req.Header.Get(headerAPIVersion)
528+
if got, want := apiVersion, "2022-11-29"; got != want {
529+
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
530+
}
520531
}
521532

522533
func TestNewRequest_invalidJSON(t *testing.T) {
@@ -626,6 +637,17 @@ func TestNewFormRequest(t *testing.T) {
626637
if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want {
627638
t.Errorf("NewFormRequest() User-Agent is %v, want %v", got, want)
628639
}
640+
641+
apiVersion := req.Header.Get(headerAPIVersion)
642+
if got, want := apiVersion, defaultAPIVersion; got != want {
643+
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
644+
}
645+
646+
req, _ = c.NewFormRequest(inURL, inBody, WithVersion("2022-11-29"))
647+
apiVersion = req.Header.Get(headerAPIVersion)
648+
if got, want := apiVersion, "2022-11-29"; got != want {
649+
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
650+
}
629651
}
630652

631653
func TestNewFormRequest_badURL(t *testing.T) {
@@ -680,6 +702,22 @@ func TestNewFormRequest_errorForNoTrailingSlash(t *testing.T) {
680702
}
681703
}
682704

705+
func TestNewUploadRequest_WithVersion(t *testing.T) {
706+
c := NewClient(nil)
707+
req, _ := c.NewUploadRequest("https://example.com/", nil, 0, "")
708+
709+
apiVersion := req.Header.Get(headerAPIVersion)
710+
if got, want := apiVersion, defaultAPIVersion; got != want {
711+
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
712+
}
713+
714+
req, _ = c.NewUploadRequest("https://example.com/", nil, 0, "", WithVersion("2022-11-29"))
715+
apiVersion = req.Header.Get(headerAPIVersion)
716+
if got, want := apiVersion, "2022-11-29"; got != want {
717+
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
718+
}
719+
}
720+
683721
func TestNewUploadRequest_badURL(t *testing.T) {
684722
c := NewClient(nil)
685723
_, err := c.NewUploadRequest(":", nil, 0, "")

0 commit comments

Comments
 (0)