Skip to content

Commit

Permalink
Support new GitHub v3 API calendar-based versioning (#2581)
Browse files Browse the repository at this point in the history
Fixes: #2580.
  • Loading branch information
gmlewis committed Dec 9, 2022
1 parent 3e3f03c commit 9d020d7
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 10 deletions.
34 changes: 34 additions & 0 deletions README.md
Expand Up @@ -332,6 +332,40 @@ Preview functionality may take the form of entire methods or simply additional
data returned from an otherwise non-preview method. Refer to the GitHub API
documentation for details on preview functionality.

### Calendar Versioning ###

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/)
that they are starting to version their v3 API based on "calendar-versioning".

In practice, our goal is to make per-method version overrides (at
least in the core library) rare and temporary.

Our understanding of the GitHub docs is that they will be revving the
entire API to each new date-based version, even if only a few methods
have breaking changes. Other methods will accept the new version with
their existing functionality. So when a new date-based version of the
GitHub API is released, we (the repo maintainers) plan to:

* update each method that had breaking changes, overriding their
per-method API version header. This may happen in one or multiple
commits and PRs, and is all done in the main branch.

* once all of the methods with breaking changes have been updated,
have a final commit that bumps the default API version, and remove
all of the per-method overrides. That would now get a major version
bump when the next go-github release is made.

### Version Compatibility Table ###

The following table identifies which version of the GitHub API is
supported by this (and past) versions of this repo (go-github).
Versions prior to 48.2.0 are not listed.

| go-github Version | GitHub v3 API Version |
| ----------------- | --------------------- |
| 48.2.0 | 2022-11-28 |


## License ##

This library is distributed under the BSD-style license found in the [LICENSE](./LICENSE)
Expand Down
52 changes: 42 additions & 10 deletions github/github.go
Expand Up @@ -27,12 +27,14 @@ import (
)

const (
Version = "v48.0.0"
Version = "v48.2.0"

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

headerAPIVersion = "X-GitHub-Api-Version"
headerRateLimit = "X-RateLimit-Limit"
headerRateRemaining = "X-RateLimit-Remaining"
headerRateReset = "X-RateLimit-Reset"
Expand Down Expand Up @@ -392,12 +394,24 @@ func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*C
return c, nil
}

// RequestOption represents an option that can modify an http.Request.
type RequestOption func(req *http.Request)

// WithVersion overrides the GitHub v3 API version for this individual request.
// For more information, see:
// https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/
func WithVersion(version string) RequestOption {
return func(req *http.Request) {
req.Header.Set(headerAPIVersion, version)
}
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash. If
// specified, the value pointed to by body is JSON encoded and included as the
// request body.
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) {
if !strings.HasSuffix(c.BaseURL.Path, "/") {
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
}
Expand Down Expand Up @@ -430,14 +444,20 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
req.Header.Set(headerAPIVersion, defaultAPIVersion)

for _, opt := range opts {
opt(req)
}

return req, nil
}

// NewFormRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// Body is sent with Content-Type: application/x-www-form-urlencoded.
func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, error) {
func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
if !strings.HasSuffix(c.BaseURL.Path, "/") {
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
}
Expand All @@ -457,13 +477,19 @@ func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, e
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
req.Header.Set(headerAPIVersion, defaultAPIVersion)

for _, opt := range opts {
opt(req)
}

return req, nil
}

// NewUploadRequest creates an upload request. A relative URL can be provided in
// urlStr, in which case it is resolved relative to the UploadURL of the Client.
// Relative URLs should always be specified without a preceding slash.
func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) {
func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) {
if !strings.HasSuffix(c.UploadURL.Path, "/") {
return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
}
Expand All @@ -485,6 +511,12 @@ func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, m
req.Header.Set("Content-Type", mediaType)
req.Header.Set("Accept", mediaTypeV3)
req.Header.Set("User-Agent", c.UserAgent)
req.Header.Set(headerAPIVersion, defaultAPIVersion)

for _, opt := range opts {
opt(req)
}

return req, nil
}

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

// When using roundTripWithOptionalFollowRedirect, note that it
// is the responsibility of the caller to close the response body.
func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
req, err := c.NewRequest("GET", u, nil)
func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool, opts ...RequestOption) (*http.Response, error) {
req, err := c.NewRequest("GET", u, nil, opts...)
if err != nil {
return nil, err
}
Expand All @@ -1380,7 +1412,7 @@ func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u stri
if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
resp.Body.Close()
u = resp.Header.Get("Location")
resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false)
resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false, opts...)
}
return resp, err
}
Expand Down
38 changes: 38 additions & 0 deletions github/github_test.go
Expand Up @@ -517,6 +517,17 @@ func TestNewRequest(t *testing.T) {
if !strings.Contains(userAgent, Version) {
t.Errorf("NewRequest() User-Agent should contain %v, found %v", Version, userAgent)
}

apiVersion := req.Header.Get(headerAPIVersion)
if got, want := apiVersion, defaultAPIVersion; got != want {
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
}

req, _ = c.NewRequest("GET", inURL, inBody, WithVersion("2022-11-29"))
apiVersion = req.Header.Get(headerAPIVersion)
if got, want := apiVersion, "2022-11-29"; got != want {
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
}
}

func TestNewRequest_invalidJSON(t *testing.T) {
Expand Down Expand Up @@ -626,6 +637,17 @@ func TestNewFormRequest(t *testing.T) {
if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want {
t.Errorf("NewFormRequest() User-Agent is %v, want %v", got, want)
}

apiVersion := req.Header.Get(headerAPIVersion)
if got, want := apiVersion, defaultAPIVersion; got != want {
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
}

req, _ = c.NewFormRequest(inURL, inBody, WithVersion("2022-11-29"))
apiVersion = req.Header.Get(headerAPIVersion)
if got, want := apiVersion, "2022-11-29"; got != want {
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
}
}

func TestNewFormRequest_badURL(t *testing.T) {
Expand Down Expand Up @@ -680,6 +702,22 @@ func TestNewFormRequest_errorForNoTrailingSlash(t *testing.T) {
}
}

func TestNewUploadRequest_WithVersion(t *testing.T) {
c := NewClient(nil)
req, _ := c.NewUploadRequest("https://example.com/", nil, 0, "")

apiVersion := req.Header.Get(headerAPIVersion)
if got, want := apiVersion, defaultAPIVersion; got != want {
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
}

req, _ = c.NewUploadRequest("https://example.com/", nil, 0, "", WithVersion("2022-11-29"))
apiVersion = req.Header.Get(headerAPIVersion)
if got, want := apiVersion, "2022-11-29"; got != want {
t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want)
}
}

func TestNewUploadRequest_badURL(t *testing.T) {
c := NewClient(nil)
_, err := c.NewUploadRequest(":", nil, 0, "")
Expand Down

0 comments on commit 9d020d7

Please sign in to comment.