diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d4a197746b8..c35dbc1b438 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: test: strategy: matrix: - go-version: [1.x, 1.18.x] + go-version: [1.x, 1.19.x] platform: [ubuntu-latest] include: # include windows, but only with the latest Go version, since there diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25d455b8f77..fb3660461fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,9 +44,8 @@ are more sensitive, emailed to . 1. Go makes it very simple to ensure properly formatted code, so always run `go fmt` on your code before committing it. You should also run - [golint][] over your code. As noted in the [golint readme][], it's not - strictly necessary that your code be completely "lint-free", but this will - help you find common style issues. + [go vet][] over your code. this will help you find common style issues + within your code and will keep styling consistent within the project. 1. Any significant changes should almost always be accompanied by tests. The project already has good test coverage, so look at some of the existing @@ -59,18 +58,35 @@ are more sensitive, emailed to . * `go test github.com/google/go-github/...` * `go vet github.com/google/go-github/...` + The `go generate ./...` command will update or generate certain files, and the + resulting changes should be included in your pull request. + + The `go test ./...` command will run tests inside your code. This will help you + spot places where code might be faulty before committing. + + And finally, the `go vet ./...` command will check linting and styling over your + code, keeping the project consistent formatting-wise. + + In any case, it is always a good idea to read [official Go documentation][] when working + on this project, as the definition of tools and commands of the Go programming + language is described in further detail there. + 1. Do your best to have [well-formed commit messages][] for each change. This provides consistency throughout the project, and ensures that commit messages are able to be formatted properly by various git tools. 1. Finally, push the commits to your fork and submit a [pull request][]. + Before pushing commits, it is highly advised to check for generated files + that were either created or modified for the sake of your commit. Running + `go generate -x ./...` should return a log of modified generated files that should + be included alongside the manually written code in the commit. **NOTE:** Please do not use force-push on PRs in this repo, as it makes it more difficult for reviewers to see what has changed since the last code review. +[official Go documentation]: https://pkg.go.dev/std [forking]: https://help.github.com/articles/fork-a-repo -[golint]: https://github.com/golang/lint -[golint readme]: https://github.com/golang/lint/blob/master/README.md +[go vet]: https://pkg.go.dev/cmd/vet [gocov]: https://github.com/axw/gocov [gocov-html]: https://github.com/matm/gocov-html [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html @@ -128,4 +144,5 @@ this][modified-comment]. [rebase-comment]: https://github.com/google/go-github/pull/277#issuecomment-183035491 [modified-comment]: https://github.com/google/go-github/pull/280#issuecomment-184859046 -**When creating a release, don't forget to update the `Version` constant in `github.go`.** This is used to send the version in the `User-Agent` header to identify clients to the GitHub API. +**When creating a release, don't forget to update the `Version` constant in `github.go`.** This is used to +send the version in the `User-Agent` header to identify clients to the GitHub API. diff --git a/README.md b/README.md index 0e76752c82b..37b373ceecf 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,24 @@ if _, ok := err.(*github.RateLimitError); ok { Learn more about GitHub rate limiting at https://docs.github.com/en/rest/rate-limit . +In addition to these rate limits, GitHub imposes a secondary rate limit on all API clients. +This rate limit prevents clients from making too many concurrent requests. + +To detect an API secondary rate limit error, you can check if its type is `*github.AbuseRateLimitError`: + +```go +repos, _, err := client.Repositories.List(ctx, "", nil) +if _, ok := err.(*github.AbuseRateLimitError); ok { + log.Println("hit secondary rate limit") +} +``` + +You can use [go-github-ratelimit](https://github.com/gofri/go-github-ratelimit) to handle +secondary rate limit sleep-and-retry for you. + +Learn more about GitHub secondary rate limiting at +https://docs.github.com/en/rest/overview/resources-in-the-rest-api#secondary-rate-limits . + ### Accepted Status ### Some endpoints may return a 202 Accepted status code, meaning that the @@ -378,6 +396,7 @@ Versions prior to 48.2.0 are not listed. | go-github Version | GitHub v3 API Version | | ----------------- | --------------------- | +| 50.1.0 | 2022-11-28 | | 50.0.0 | 2022-11-28 | | 49.1.0 | 2022-11-28 | | 49.0.0 | 2022-11-28 | diff --git a/example/go.mod b/example/go.mod index 85db0c81f3b..e268c7ad626 100644 --- a/example/go.mod +++ b/example/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 + github.com/gofri/go-github-ratelimit v1.0.1 github.com/google/go-github/v50 v50.0.0 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be diff --git a/example/go.sum b/example/go.sum index 663dff29860..1043059ffa2 100644 --- a/example/go.sum +++ b/example/go.sum @@ -1,5 +1,7 @@ github.com/bradleyfalzon/ghinstallation/v2 v2.0.4 h1:tXKVfhE7FcSkhkv0UwkLvPDeZ4kz6OXd0PKPlFqf81M= github.com/bradleyfalzon/ghinstallation/v2 v2.0.4/go.mod h1:B40qPqJxWE0jDZgOR1JmaMy+4AY1eBP+IByOvqyAKp0= +github.com/gofri/go-github-ratelimit v1.0.1 h1:sgefSzxhnvwZ+wR9uZ4l9TnjgLuNiwipJVzJL4YLj9A= +github.com/gofri/go-github-ratelimit v1.0.1/go.mod h1:OnCi5gV+hAG/LMR7llGhU7yHt44se9sYgKPnafoL7RY= github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/example/ratelimit/main.go b/example/ratelimit/main.go new file mode 100644 index 00000000000..228099faa9f --- /dev/null +++ b/example/ratelimit/main.go @@ -0,0 +1,42 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The ratelimit command demonstrates using the github_ratelimit.SecondaryRateLimitWaiter. +// By using the waiter, the client automatically sleeps and retry requests +// when it hits secondary rate limits. +package main + +import ( + "context" + "fmt" + + "github.com/gofri/go-github-ratelimit/github_ratelimit" + "github.com/google/go-github/v50/github" +) + +func main() { + var username string + fmt.Print("Enter GitHub username: ") + fmt.Scanf("%s", &username) + + rateLimiter, err := github_ratelimit.NewRateLimitWaiterClient(nil) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + client := github.NewClient(rateLimiter) + + // arbitrary usage of the client + organizations, _, err := client.Organizations.List(context.Background(), username, nil) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + for i, organization := range organizations { + fmt.Printf("%v. %v\n", i+1, organization.GetLogin()) + } +} diff --git a/github/actions_artifacts.go b/github/actions_artifacts.go index 99329d989d9..441a53910e3 100644 --- a/github/actions_artifacts.go +++ b/github/actions_artifacts.go @@ -12,20 +12,34 @@ import ( "net/url" ) -// Artifact reprents a GitHub artifact. Artifacts allow sharing +// ArtifactWorkflowRun represents a GitHub artifact's workflow run. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/artifacts +type ArtifactWorkflowRun struct { + ID *int64 `json:"id,omitempty"` + RepositoryID *int64 `json:"repository_id,omitempty"` + HeadRepositoryID *int64 `json:"head_repository_id,omitempty"` + HeadBranch *string `json:"head_branch,omitempty"` + HeadSHA *string `json:"head_sha,omitempty"` +} + +// Artifact represents a GitHub artifact. Artifacts allow sharing // data between jobs in a workflow and provide storage for data // once a workflow is complete. // // GitHub API docs: https://docs.github.com/en/rest/actions/artifacts type Artifact struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - Name *string `json:"name,omitempty"` - SizeInBytes *int64 `json:"size_in_bytes,omitempty"` - ArchiveDownloadURL *string `json:"archive_download_url,omitempty"` - Expired *bool `json:"expired,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - ExpiresAt *Timestamp `json:"expires_at,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Name *string `json:"name,omitempty"` + SizeInBytes *int64 `json:"size_in_bytes,omitempty"` + URL *string `json:"url,omitempty"` + ArchiveDownloadURL *string `json:"archive_download_url,omitempty"` + Expired *bool `json:"expired,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ExpiresAt *Timestamp `json:"expires_at,omitempty"` + WorkflowRun *ArtifactWorkflowRun `json:"workflow_run,omitempty"` } // ArtifactList represents a list of GitHub artifacts. diff --git a/github/actions_artifacts_test.go b/github/actions_artifacts_test.go index 345a0875348..de07c14f847 100644 --- a/github/actions_artifacts_test.go +++ b/github/actions_artifacts_test.go @@ -368,23 +368,6 @@ func TestActionsService_DownloadArtifact_StatusMovedPermanently_followRedirects( } } -func TestActionsService_DownloadArtifact_invalidLocationHeader(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/repos/o/r/actions/artifacts/1/zip", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - ctlChar := 0x7f - badURL := "https://google.com" + string(byte(ctlChar)) - w.Header().Add("Location", badURL) - w.WriteHeader(http.StatusFound) - }) - - ctx := context.Background() - _, _, err := client.Actions.DownloadArtifact(ctx, "o", "r", 1, false) - testURLParseError(t, err) -} - func TestActionsService_DeleteArtifact(t *testing.T) { client, mux, _, teardown := setup() defer teardown() @@ -455,10 +438,19 @@ func TestArtifact_Marshal(t *testing.T) { NodeID: String("nid"), Name: String("n"), SizeInBytes: Int64(1), + URL: String("u"), ArchiveDownloadURL: String("a"), Expired: Bool(false), CreatedAt: &Timestamp{referenceTime}, + UpdatedAt: &Timestamp{referenceTime}, ExpiresAt: &Timestamp{referenceTime}, + WorkflowRun: &ArtifactWorkflowRun{ + ID: Int64(1), + RepositoryID: Int64(1), + HeadRepositoryID: Int64(1), + HeadBranch: String("b"), + HeadSHA: String("s"), + }, } want := `{ @@ -466,10 +458,19 @@ func TestArtifact_Marshal(t *testing.T) { "node_id": "nid", "name": "n", "size_in_bytes": 1, + "url": "u", "archive_download_url": "a", "expired": false, "created_at": ` + referenceTimeStr + `, - "expires_at": ` + referenceTimeStr + ` + "updated_at": ` + referenceTimeStr + `, + "expires_at": ` + referenceTimeStr + `, + "workflow_run": { + "id": 1, + "repository_id": 1, + "head_repository_id": 1, + "head_branch": "b", + "head_sha": "s" + } }` testJSONMarshal(t, u, want) @@ -486,10 +487,19 @@ func TestArtifactList_Marshal(t *testing.T) { NodeID: String("nid"), Name: String("n"), SizeInBytes: Int64(1), + URL: String("u"), ArchiveDownloadURL: String("a"), Expired: Bool(false), CreatedAt: &Timestamp{referenceTime}, + UpdatedAt: &Timestamp{referenceTime}, ExpiresAt: &Timestamp{referenceTime}, + WorkflowRun: &ArtifactWorkflowRun{ + ID: Int64(1), + RepositoryID: Int64(1), + HeadRepositoryID: Int64(1), + HeadBranch: String("b"), + HeadSHA: String("s"), + }, }, }, } @@ -501,10 +511,19 @@ func TestArtifactList_Marshal(t *testing.T) { "node_id": "nid", "name": "n", "size_in_bytes": 1, + "url": "u", "archive_download_url": "a", "expired": false, "created_at": ` + referenceTimeStr + `, - "expires_at": ` + referenceTimeStr + ` + "updated_at": ` + referenceTimeStr + `, + "expires_at": ` + referenceTimeStr + `, + "workflow_run": { + "id": 1, + "repository_id": 1, + "head_repository_id": 1, + "head_branch": "b", + "head_sha": "s" + } }] }` diff --git a/github/actions_variables.go b/github/actions_variables.go new file mode 100644 index 00000000000..29445edd042 --- /dev/null +++ b/github/actions_variables.go @@ -0,0 +1,293 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" +) + +// ActionsVariable represents a repository action variable. +type ActionsVariable struct { + Name string `json:"name"` + Value string `json:"value"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + Visibility *string `json:"visibility,omitempty"` + // Used by ListOrgVariables and GetOrgVariables + SelectedRepositoriesURL *string `json:"selected_repositories_url,omitempty"` + // Used by UpdateOrgVariable and CreateOrgVariable + SelectedRepositoryIDs *SelectedRepoIDs `json:"selected_repository_ids,omitempty"` +} + +// ActionsVariables represents one item from the ListVariables response. +type ActionsVariables struct { + TotalCount int `json:"total_count"` + Variables []*ActionsVariable `json:"variables"` +} + +func (s *ActionsService) listVariables(ctx context.Context, url string, opts *ListOptions) (*ActionsVariables, *Response, error) { + u, err := addOptions(url, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + variables := new(ActionsVariables) + resp, err := s.client.Do(ctx, req, &variables) + if err != nil { + return nil, resp, err + } + + return variables, resp, nil +} + +// ListRepoVariables lists all variables available in a repository. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#list-repository-variables +func (s *ActionsService) ListRepoVariables(ctx context.Context, owner, repo string, opts *ListOptions) (*ActionsVariables, *Response, error) { + url := fmt.Sprintf("repos/%v/%v/actions/variables", owner, repo) + return s.listVariables(ctx, url, opts) +} + +// ListOrgVariables lists all variables available in an organization. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#list-organization-variables +func (s *ActionsService) ListOrgVariables(ctx context.Context, org string, opts *ListOptions) (*ActionsVariables, *Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables", org) + return s.listVariables(ctx, url, opts) +} + +// ListEnvVariables lists all variables available in an environment. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#list-environment-variables +func (s *ActionsService) ListEnvVariables(ctx context.Context, repoID int, env string, opts *ListOptions) (*ActionsVariables, *Response, error) { + url := fmt.Sprintf("repositories/%v/environments/%v/variables", repoID, env) + return s.listVariables(ctx, url, opts) +} + +func (s *ActionsService) getVariable(ctx context.Context, url string) (*ActionsVariable, *Response, error) { + req, err := s.client.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + + variable := new(ActionsVariable) + resp, err := s.client.Do(ctx, req, variable) + if err != nil { + return nil, resp, err + } + + return variable, resp, nil +} + +// GetRepoVariable gets a single repository variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#get-a-repository-variable +func (s *ActionsService) GetRepoVariable(ctx context.Context, owner, repo, name string) (*ActionsVariable, *Response, error) { + url := fmt.Sprintf("repos/%v/%v/actions/variables/%v", owner, repo, name) + return s.getVariable(ctx, url) +} + +// GetOrgVariable gets a single organization variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#get-an-organization-variable +func (s *ActionsService) GetOrgVariable(ctx context.Context, org, name string) (*ActionsVariable, *Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v", org, name) + return s.getVariable(ctx, url) +} + +// GetEnvVariable gets a single environment variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#get-an-environment-variable +func (s *ActionsService) GetEnvVariable(ctx context.Context, repoID int, env, variableName string) (*ActionsVariable, *Response, error) { + url := fmt.Sprintf("repositories/%v/environments/%v/variables/%v", repoID, env, variableName) + return s.getVariable(ctx, url) +} + +func (s *ActionsService) postVariable(ctx context.Context, url string, variable *ActionsVariable) (*Response, error) { + req, err := s.client.NewRequest("POST", url, variable) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// CreateRepoVariable creates a repository variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#create-a-repository-variable +func (s *ActionsService) CreateRepoVariable(ctx context.Context, owner, repo string, variable *ActionsVariable) (*Response, error) { + url := fmt.Sprintf("repos/%v/%v/actions/variables", owner, repo) + return s.postVariable(ctx, url, variable) +} + +// CreateOrgVariable creates an organization variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#create-an-organization-variable +func (s *ActionsService) CreateOrgVariable(ctx context.Context, org string, variable *ActionsVariable) (*Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables", org) + return s.postVariable(ctx, url, variable) +} + +// CreateEnvVariable creates an environment variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#create-an-environment-variable +func (s *ActionsService) CreateEnvVariable(ctx context.Context, repoID int, env string, variable *ActionsVariable) (*Response, error) { + url := fmt.Sprintf("repositories/%v/environments/%v/variables", repoID, env) + return s.postVariable(ctx, url, variable) +} + +func (s *ActionsService) patchVariable(ctx context.Context, url string, variable *ActionsVariable) (*Response, error) { + req, err := s.client.NewRequest("PATCH", url, variable) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// UpdateRepoVariable updates a repository variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#update-a-repository-variable +func (s *ActionsService) UpdateRepoVariable(ctx context.Context, owner, repo string, variable *ActionsVariable) (*Response, error) { + url := fmt.Sprintf("repos/%v/%v/actions/variables/%v", owner, repo, variable.Name) + return s.patchVariable(ctx, url, variable) +} + +// UpdateOrgVariable updates an organization variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#update-an-organization-variable +func (s *ActionsService) UpdateOrgVariable(ctx context.Context, org string, variable *ActionsVariable) (*Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v", org, variable.Name) + return s.patchVariable(ctx, url, variable) +} + +// UpdateEnvVariable updates an environment variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#create-an-environment-variable +func (s *ActionsService) UpdateEnvVariable(ctx context.Context, repoID int, env string, variable *ActionsVariable) (*Response, error) { + url := fmt.Sprintf("repositories/%v/environments/%v/variables/%v", repoID, env, variable.Name) + return s.patchVariable(ctx, url, variable) +} + +func (s *ActionsService) deleteVariable(ctx context.Context, url string) (*Response, error) { + req, err := s.client.NewRequest("DELETE", url, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// DeleteRepoVariable deletes a variable in a repository. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#delete-a-repository-variable +func (s *ActionsService) DeleteRepoVariable(ctx context.Context, owner, repo, name string) (*Response, error) { + url := fmt.Sprintf("repos/%v/%v/actions/variables/%v", owner, repo, name) + return s.deleteVariable(ctx, url) +} + +// DeleteOrgVariable deletes a variable in an organization. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#delete-an-organization-variable +func (s *ActionsService) DeleteOrgVariable(ctx context.Context, org, name string) (*Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v", org, name) + return s.deleteVariable(ctx, url) +} + +// DeleteEnvVariable deletes a variable in an environment. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#delete-an-environment-variable +func (s *ActionsService) DeleteEnvVariable(ctx context.Context, repoID int, env, variableName string) (*Response, error) { + url := fmt.Sprintf("repositories/%v/environments/%v/variables/%v", repoID, env, variableName) + return s.deleteVariable(ctx, url) +} + +func (s *ActionsService) listSelectedReposForVariable(ctx context.Context, url string, opts *ListOptions) (*SelectedReposList, *Response, error) { + u, err := addOptions(url, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + result := new(SelectedReposList) + resp, err := s.client.Do(ctx, req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// ListSelectedReposForOrgVariable lists all repositories that have access to a variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#list-selected-repositories-for-an-organization-variable +func (s *ActionsService) ListSelectedReposForOrgVariable(ctx context.Context, org, name string, opts *ListOptions) (*SelectedReposList, *Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v/repositories", org, name) + return s.listSelectedReposForVariable(ctx, url, opts) +} + +func (s *ActionsService) setSelectedReposForVariable(ctx context.Context, url string, ids SelectedRepoIDs) (*Response, error) { + type repoIDs struct { + SelectedIDs SelectedRepoIDs `json:"selected_repository_ids"` + } + + req, err := s.client.NewRequest("PUT", url, repoIDs{SelectedIDs: ids}) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// SetSelectedReposForOrgVariable sets the repositories that have access to a variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#set-selected-repositories-for-an-organization-variable +func (s *ActionsService) SetSelectedReposForOrgVariable(ctx context.Context, org, name string, ids SelectedRepoIDs) (*Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v/repositories", org, name) + return s.setSelectedReposForVariable(ctx, url, ids) +} + +func (s *ActionsService) addSelectedRepoToVariable(ctx context.Context, url string) (*Response, error) { + req, err := s.client.NewRequest("PUT", url, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// AddSelectedRepoToOrgVariable adds a repository to an organization variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#add-selected-repository-to-an-organization-variable +func (s *ActionsService) AddSelectedRepoToOrgVariable(ctx context.Context, org, name string, repo *Repository) (*Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v/repositories/%v", org, name, *repo.ID) + return s.addSelectedRepoToVariable(ctx, url) +} + +func (s *ActionsService) removeSelectedRepoFromVariable(ctx context.Context, url string) (*Response, error) { + req, err := s.client.NewRequest("DELETE", url, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveSelectedRepoFromOrgVariable removes a repository from an organization variable. +// +// GitHub API docs: https://docs.github.com/en/rest/actions/variables#remove-selected-repository-from-an-organization-variable +func (s *ActionsService) RemoveSelectedRepoFromOrgVariable(ctx context.Context, org, name string, repo *Repository) (*Response, error) { + url := fmt.Sprintf("orgs/%v/actions/variables/%v/repositories/%v", org, name, *repo.ID) + return s.removeSelectedRepoFromVariable(ctx, url) +} diff --git a/github/actions_variables_test.go b/github/actions_variables_test.go new file mode 100644 index 00000000000..646da92b1e8 --- /dev/null +++ b/github/actions_variables_test.go @@ -0,0 +1,659 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +func TestActionsService_ListRepoVariables(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `{"total_count":4,"variables":[{"name":"A","value":"AA","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"},{"name":"B","value":"BB","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}]}`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := context.Background() + variables, _, err := client.Actions.ListRepoVariables(ctx, "o", "r", opts) + if err != nil { + t.Errorf("Actions.ListRepoVariables returned error: %v", err) + } + + want := &ActionsVariables{ + TotalCount: 4, + Variables: []*ActionsVariable{ + {Name: "A", Value: "AA", CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + {Name: "B", Value: "BB", CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + }, + } + if !cmp.Equal(variables, want) { + t.Errorf("Actions.ListRepoVariables returned %+v, want %+v", variables, want) + } + + const methodName = "ListRepoVariables" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.ListRepoVariables(ctx, "\n", "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.ListRepoVariables(ctx, "o", "r", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_GetRepoVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/variables/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"name":"NAME","value":"VALUE","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}`) + }) + + ctx := context.Background() + variable, _, err := client.Actions.GetRepoVariable(ctx, "o", "r", "NAME") + if err != nil { + t.Errorf("Actions.GetRepoVariable returned error: %v", err) + } + + want := &ActionsVariable{ + Name: "NAME", + Value: "VALUE", + CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, + UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}, + } + if !cmp.Equal(variable, want) { + t.Errorf("Actions.GetRepoVariable returned %+v, want %+v", variable, want) + } + + const methodName = "GetRepoVariable" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.GetRepoVariable(ctx, "\n", "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.GetRepoVariable(ctx, "o", "r", "NAME") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_CreateRepoVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"name":"NAME","value":"VALUE"}`+"\n") + w.WriteHeader(http.StatusCreated) + }) + + input := &ActionsVariable{ + Name: "NAME", + Value: "VALUE", + } + ctx := context.Background() + _, err := client.Actions.CreateRepoVariable(ctx, "o", "r", input) + if err != nil { + t.Errorf("Actions.CreateRepoVariable returned error: %v", err) + } + + const methodName = "CreateRepoVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.CreateRepoVariable(ctx, "\n", "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.CreateRepoVariable(ctx, "o", "r", input) + }) +} + +func TestActionsService_UpdateRepoVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/variables/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"name":"NAME","value":"VALUE"}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + input := &ActionsVariable{ + Name: "NAME", + Value: "VALUE", + } + ctx := context.Background() + _, err := client.Actions.UpdateRepoVariable(ctx, "o", "r", input) + if err != nil { + t.Errorf("Actions.UpdateRepoVariable returned error: %v", err) + } + + const methodName = "UpdateRepoVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.UpdateRepoVariable(ctx, "\n", "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.UpdateRepoVariable(ctx, "o", "r", input) + }) +} + +func TestActionsService_DeleteRepoVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/variables/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + ctx := context.Background() + _, err := client.Actions.DeleteRepoVariable(ctx, "o", "r", "NAME") + if err != nil { + t.Errorf("Actions.( returned error: %v", err) + } + + const methodName = "DeleteRepoVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.DeleteRepoVariable(ctx, "\n", "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.DeleteRepoVariable(ctx, "o", "r", "NAME") + }) +} + +func TestActionsService_ListOrgVariables(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `{"total_count":3,"variables":[{"name":"A","value":"AA","created_at":"2019-08-10T14:59:22Z","updated_at":"2020-01-10T14:59:22Z","visibility":"private"},{"name":"B","value":"BB","created_at":"2019-08-10T14:59:22Z","updated_at":"2020-01-10T14:59:22Z","visibility":"all"},{"name":"C","value":"CC","created_at":"2019-08-10T14:59:22Z","updated_at":"2020-01-10T14:59:22Z","visibility":"selected","selected_repositories_url":"https://api.github.com/orgs/octo-org/actions/variables/VAR/repositories"}]}`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := context.Background() + variables, _, err := client.Actions.ListOrgVariables(ctx, "o", opts) + if err != nil { + t.Errorf("Actions.ListOrgVariables returned error: %v", err) + } + + want := &ActionsVariables{ + TotalCount: 3, + Variables: []*ActionsVariable{ + {Name: "A", Value: "AA", CreatedAt: &Timestamp{time.Date(2019, time.August, 10, 14, 59, 22, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 10, 14, 59, 22, 0, time.UTC)}, Visibility: String("private")}, + {Name: "B", Value: "BB", CreatedAt: &Timestamp{time.Date(2019, time.August, 10, 14, 59, 22, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 10, 14, 59, 22, 0, time.UTC)}, Visibility: String("all")}, + {Name: "C", Value: "CC", CreatedAt: &Timestamp{time.Date(2019, time.August, 10, 14, 59, 22, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 10, 14, 59, 22, 0, time.UTC)}, Visibility: String("selected"), SelectedRepositoriesURL: String("https://api.github.com/orgs/octo-org/actions/variables/VAR/repositories")}, + }, + } + if !cmp.Equal(variables, want) { + t.Errorf("Actions.ListOrgVariables returned %+v, want %+v", variables, want) + } + + const methodName = "ListOrgVariables" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.ListOrgVariables(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.ListOrgVariables(ctx, "o", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_GetOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"name":"NAME","value":"VALUE","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z","visibility":"selected","selected_repositories_url":"https://api.github.com/orgs/octo-org/actions/variables/VAR/repositories"}`) + }) + + ctx := context.Background() + variable, _, err := client.Actions.GetOrgVariable(ctx, "o", "NAME") + if err != nil { + t.Errorf("Actions.GetOrgVariable returned error: %v", err) + } + + want := &ActionsVariable{ + Name: "NAME", + Value: "VALUE", + CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, + UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}, + Visibility: String("selected"), + SelectedRepositoriesURL: String("https://api.github.com/orgs/octo-org/actions/variables/VAR/repositories"), + } + if !cmp.Equal(variable, want) { + t.Errorf("Actions.GetOrgVariable returned %+v, want %+v", variable, want) + } + + const methodName = "GetOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.GetOrgVariable(ctx, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.GetOrgVariable(ctx, "o", "NAME") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_CreateOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"name":"NAME","value":"VALUE","visibility":"selected","selected_repository_ids":[1296269,1269280]}`+"\n") + w.WriteHeader(http.StatusCreated) + }) + + input := &ActionsVariable{ + Name: "NAME", + Value: "VALUE", + Visibility: String("selected"), + SelectedRepositoryIDs: &SelectedRepoIDs{1296269, 1269280}, + } + ctx := context.Background() + _, err := client.Actions.CreateOrgVariable(ctx, "o", input) + if err != nil { + t.Errorf("Actions.CreateOrgVariable returned error: %v", err) + } + + const methodName = "CreateOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.CreateOrgVariable(ctx, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.CreateOrgVariable(ctx, "o", input) + }) +} + +func TestActionsService_UpdateOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"name":"NAME","value":"VALUE","visibility":"selected","selected_repository_ids":[1296269,1269280]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + input := &ActionsVariable{ + Name: "NAME", + Value: "VALUE", + Visibility: String("selected"), + SelectedRepositoryIDs: &SelectedRepoIDs{1296269, 1269280}, + } + ctx := context.Background() + _, err := client.Actions.UpdateOrgVariable(ctx, "o", input) + if err != nil { + t.Errorf("Actions.UpdateOrgVariable returned error: %v", err) + } + + const methodName = "UpdateOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.UpdateOrgVariable(ctx, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.UpdateOrgVariable(ctx, "o", input) + }) +} + +func TestActionsService_ListSelectedReposForOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, `{"total_count":1,"repositories":[{"id":1}]}`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := context.Background() + repos, _, err := client.Actions.ListSelectedReposForOrgVariable(ctx, "o", "NAME", opts) + if err != nil { + t.Errorf("Actions.( returned error: %v", err) + } + + want := &SelectedReposList{ + TotalCount: Int(1), + Repositories: []*Repository{ + {ID: Int64(1)}, + }, + } + if !cmp.Equal(repos, want) { + t.Errorf("Actions.( returned %+v, want %+v", repos, want) + } + + const methodName = "ListSelectedReposForOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.ListSelectedReposForOrgVariable(ctx, "\n", "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.ListSelectedReposForOrgVariable(ctx, "o", "NAME", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_SetSelectedReposForOrgSVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME/repositories", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"selected_repository_ids":[64780797]}`+"\n") + }) + + ctx := context.Background() + _, err := client.Actions.SetSelectedReposForOrgVariable(ctx, "o", "NAME", SelectedRepoIDs{64780797}) + if err != nil { + t.Errorf("Actions.( returned error: %v", err) + } + + const methodName = "SetSelectedReposForOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.SetSelectedReposForOrgVariable(ctx, "\n", "\n", SelectedRepoIDs{64780797}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.SetSelectedReposForOrgVariable(ctx, "o", "NAME", SelectedRepoIDs{64780797}) + }) +} + +func TestActionsService_AddSelectedRepoToOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME/repositories/1234", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + }) + + repo := &Repository{ID: Int64(1234)} + ctx := context.Background() + _, err := client.Actions.AddSelectedRepoToOrgVariable(ctx, "o", "NAME", repo) + if err != nil { + t.Errorf("Actions.AddSelectedRepoToOrgVariable returned error: %v", err) + } + + const methodName = "AddSelectedRepoToOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.AddSelectedRepoToOrgVariable(ctx, "\n", "\n", repo) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.AddSelectedRepoToOrgVariable(ctx, "o", "NAME", repo) + }) +} + +func TestActionsService_RemoveSelectedRepoFromOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME/repositories/1234", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + repo := &Repository{ID: Int64(1234)} + ctx := context.Background() + _, err := client.Actions.RemoveSelectedRepoFromOrgVariable(ctx, "o", "NAME", repo) + if err != nil { + t.Errorf("Actions.RemoveSelectedRepoFromOrgVariable returned error: %v", err) + } + + const methodName = "RemoveSelectedRepoFromOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.RemoveSelectedRepoFromOrgVariable(ctx, "\n", "\n", repo) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.RemoveSelectedRepoFromOrgVariable(ctx, "o", "NAME", repo) + }) +} + +func TestActionsService_DeleteOrgVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/actions/variables/NAME", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + ctx := context.Background() + _, err := client.Actions.DeleteOrgVariable(ctx, "o", "NAME") + if err != nil { + t.Errorf("Actions.DeleteOrgVariable returned error: %v", err) + } + + const methodName = "DeleteOrgVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.DeleteOrgVariable(ctx, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.DeleteOrgVariable(ctx, "o", "NAME") + }) +} + +func TestActionsService_ListEnvVariables(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repositories/1/environments/e/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `{"total_count":4,"variables":[{"name":"A","value":"AA","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"},{"name":"B","value":"BB","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}]}`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := context.Background() + variables, _, err := client.Actions.ListEnvVariables(ctx, 1, "e", opts) + if err != nil { + t.Errorf("Actions.ListEnvVariables returned error: %v", err) + } + + want := &ActionsVariables{ + TotalCount: 4, + Variables: []*ActionsVariable{ + {Name: "A", Value: "AA", CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + {Name: "B", Value: "BB", CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + }, + } + if !cmp.Equal(variables, want) { + t.Errorf("Actions.ListEnvVariables returned %+v, want %+v", variables, want) + } + + const methodName = "ListEnvVariables" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.ListEnvVariables(ctx, 0.0, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.ListEnvVariables(ctx, 1, "e", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_GetEnvVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repositories/1/environments/e/variables/variable", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"name":"variable","value":"VAR","created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}`) + }) + + ctx := context.Background() + variable, _, err := client.Actions.GetEnvVariable(ctx, 1, "e", "variable") + if err != nil { + t.Errorf("Actions.GetEnvVariable returned error: %v", err) + } + + want := &ActionsVariable{ + Name: "variable", + Value: "VAR", + CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, + UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}, + } + if !cmp.Equal(variable, want) { + t.Errorf("Actions.GetEnvVariable returned %+v, want %+v", variable, want) + } + + const methodName = "GetEnvVariable" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Actions.GetEnvVariable(ctx, 0.0, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Actions.GetEnvVariable(ctx, 1, "e", "variable") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestActionsService_CreateEnvVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repositories/1/environments/e/variables", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"name":"variable","value":"VAR"}`+"\n") + w.WriteHeader(http.StatusCreated) + }) + + input := &ActionsVariable{ + Name: "variable", + Value: "VAR", + } + ctx := context.Background() + _, err := client.Actions.CreateEnvVariable(ctx, 1, "e", input) + if err != nil { + t.Errorf("Actions.CreateEnvVariable returned error: %v", err) + } + + const methodName = "CreateEnvVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.CreateEnvVariable(ctx, 0.0, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.CreateEnvVariable(ctx, 1, "e", input) + }) +} + +func TestActionsService_UpdateEnvVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repositories/1/environments/e/variables/variable", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testHeader(t, r, "Content-Type", "application/json") + testBody(t, r, `{"name":"variable","value":"VAR"}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + input := &ActionsVariable{ + Name: "variable", + Value: "VAR", + } + ctx := context.Background() + _, err := client.Actions.UpdateEnvVariable(ctx, 1, "e", input) + if err != nil { + t.Errorf("Actions.UpdateEnvVariable returned error: %v", err) + } + + const methodName = "UpdateEnvVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.UpdateEnvVariable(ctx, 0.0, "\n", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.UpdateEnvVariable(ctx, 1, "e", input) + }) +} + +func TestActionsService_DeleteEnvVariable(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repositories/1/environments/e/variables/variable", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + ctx := context.Background() + _, err := client.Actions.DeleteEnvVariable(ctx, 1, "e", "variable") + if err != nil { + t.Errorf("Actions.DeleteEnvVariable returned error: %v", err) + } + + const methodName = "DeleteEnvVariable" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Actions.DeleteEnvVariable(ctx, 0.0, "\n", "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Actions.DeleteEnvVariable(ctx, 1, "r", "variable") + }) +} diff --git a/github/actions_workflow_jobs.go b/github/actions_workflow_jobs.go index b55f9aa7f8d..b7130916fe5 100644 --- a/github/actions_workflow_jobs.go +++ b/github/actions_workflow_jobs.go @@ -33,6 +33,7 @@ type WorkflowJob struct { HTMLURL *string `json:"html_url,omitempty"` Status *string `json:"status,omitempty"` Conclusion *string `json:"conclusion,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` StartedAt *Timestamp `json:"started_at,omitempty"` CompletedAt *Timestamp `json:"completed_at,omitempty"` Name *string `json:"name,omitempty"` diff --git a/github/actions_workflow_jobs_test.go b/github/actions_workflow_jobs_test.go index da52619a195..521e401102b 100644 --- a/github/actions_workflow_jobs_test.go +++ b/github/actions_workflow_jobs_test.go @@ -251,6 +251,7 @@ func TestWorkflowJob_Marshal(t *testing.T) { HTMLURL: String("h"), Status: String("s"), Conclusion: String("c"), + CreatedAt: &Timestamp{referenceTime}, StartedAt: &Timestamp{referenceTime}, CompletedAt: &Timestamp{referenceTime}, Name: String("n"), @@ -278,6 +279,7 @@ func TestWorkflowJob_Marshal(t *testing.T) { "html_url": "h", "status": "s", "conclusion": "c", + "created_at": ` + referenceTimeStr + `, "started_at": ` + referenceTimeStr + `, "completed_at": ` + referenceTimeStr + `, "name": "n", @@ -312,6 +314,7 @@ func TestJobs_Marshal(t *testing.T) { HTMLURL: String("h"), Status: String("s"), Conclusion: String("c"), + CreatedAt: &Timestamp{referenceTime}, StartedAt: &Timestamp{referenceTime}, CompletedAt: &Timestamp{referenceTime}, Name: String("n"), @@ -344,6 +347,7 @@ func TestJobs_Marshal(t *testing.T) { "html_url": "h", "status": "s", "conclusion": "c", + "created_at": ` + referenceTimeStr + `, "started_at": ` + referenceTimeStr + `, "completed_at": ` + referenceTimeStr + `, "name": "n", diff --git a/github/apps_marketplace.go b/github/apps_marketplace.go index 82530136842..32889abd240 100644 --- a/github/apps_marketplace.go +++ b/github/apps_marketplace.go @@ -46,6 +46,7 @@ type MarketplacePlan struct { // MarketplacePurchase represents a GitHub Apps Marketplace Purchase. type MarketplacePurchase struct { + Account *MarketplacePurchaseAccount `json:"account,omitempty"` // BillingCycle can be one of the values "yearly", "monthly" or nil. BillingCycle *string `json:"billing_cycle,omitempty"` NextBillingDate *Timestamp `json:"next_billing_date,omitempty"` @@ -75,6 +76,17 @@ type MarketplacePlanAccount struct { MarketplacePendingChange *MarketplacePendingChange `json:"marketplace_pending_change,omitempty"` } +// MarketplacePurchaseAccount represents a GitHub Account (user or organization) for a Purchase. +type MarketplacePurchaseAccount struct { + URL *string `json:"url,omitempty"` + Type *string `json:"type,omitempty"` + ID *int64 `json:"id,omitempty"` + Login *string `json:"login,omitempty"` + OrganizationBillingEmail *string `json:"organization_billing_email,omitempty"` + Email *string `json:"email,omitempty"` + NodeID *string `json:"node_id,omitempty"` +} + // ListPlans lists all plans for your Marketplace listing. // // GitHub API docs: https://docs.github.com/en/rest/apps#list-plans diff --git a/github/code-scanning_test.go b/github/code-scanning_test.go index 1ae05096d3f..7911fca6360 100644 --- a/github/code-scanning_test.go +++ b/github/code-scanning_test.go @@ -83,7 +83,7 @@ func TestCodeScanningService_UploadSarif(t *testing.T) { return err }) - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + testNewRequestAndDoFailureCategory(t, methodName, client, codeScanningUploadCategory, func() (*Response, error) { _, resp, err := client.CodeScanning.UploadSarif(ctx, "o", "r", sarifAnalysis) return resp, err }) diff --git a/github/event_types.go b/github/event_types.go index 8330353a3c1..50c7b567bcf 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -485,6 +485,7 @@ type IssuesEvent struct { Repo *Repository `json:"repository,omitempty"` Sender *User `json:"sender,omitempty"` Installation *Installation `json:"installation,omitempty"` + Milestone *Milestone `json:"milestone,omitempty"` } // LabelEvent is triggered when a repository's label is created, edited, or deleted. diff --git a/github/github-accessors.go b/github/github-accessors.go index e2d0d7f8357..a7d800e20a9 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -174,6 +174,46 @@ func (a *ActionsPermissionsRepository) GetSelectedActionsURL() string { return *a.SelectedActionsURL } +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (a *ActionsVariable) GetCreatedAt() Timestamp { + if a == nil || a.CreatedAt == nil { + return Timestamp{} + } + return *a.CreatedAt +} + +// GetSelectedRepositoriesURL returns the SelectedRepositoriesURL field if it's non-nil, zero value otherwise. +func (a *ActionsVariable) GetSelectedRepositoriesURL() string { + if a == nil || a.SelectedRepositoriesURL == nil { + return "" + } + return *a.SelectedRepositoriesURL +} + +// GetSelectedRepositoryIDs returns the SelectedRepositoryIDs field. +func (a *ActionsVariable) GetSelectedRepositoryIDs() *SelectedRepoIDs { + if a == nil { + return nil + } + return a.SelectedRepositoryIDs +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (a *ActionsVariable) GetUpdatedAt() Timestamp { + if a == nil || a.UpdatedAt == nil { + return Timestamp{} + } + return *a.UpdatedAt +} + +// GetVisibility returns the Visibility field if it's non-nil, zero value otherwise. +func (a *ActionsVariable) GetVisibility() string { + if a == nil || a.Visibility == nil { + return "" + } + return *a.Visibility +} + // GetFrom returns the From field if it's non-nil, zero value otherwise. func (a *AdminEnforcedChanges) GetFrom() bool { if a == nil || a.From == nil { @@ -862,6 +902,30 @@ func (a *Artifact) GetSizeInBytes() int64 { return *a.SizeInBytes } +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (a *Artifact) GetUpdatedAt() Timestamp { + if a == nil || a.UpdatedAt == nil { + return Timestamp{} + } + return *a.UpdatedAt +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (a *Artifact) GetURL() string { + if a == nil || a.URL == nil { + return "" + } + return *a.URL +} + +// GetWorkflowRun returns the WorkflowRun field. +func (a *Artifact) GetWorkflowRun() *ArtifactWorkflowRun { + if a == nil { + return nil + } + return a.WorkflowRun +} + // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. func (a *ArtifactList) GetTotalCount() int64 { if a == nil || a.TotalCount == nil { @@ -870,6 +934,46 @@ func (a *ArtifactList) GetTotalCount() int64 { return *a.TotalCount } +// GetHeadBranch returns the HeadBranch field if it's non-nil, zero value otherwise. +func (a *ArtifactWorkflowRun) GetHeadBranch() string { + if a == nil || a.HeadBranch == nil { + return "" + } + return *a.HeadBranch +} + +// GetHeadRepositoryID returns the HeadRepositoryID field if it's non-nil, zero value otherwise. +func (a *ArtifactWorkflowRun) GetHeadRepositoryID() int64 { + if a == nil || a.HeadRepositoryID == nil { + return 0 + } + return *a.HeadRepositoryID +} + +// GetHeadSHA returns the HeadSHA field if it's non-nil, zero value otherwise. +func (a *ArtifactWorkflowRun) GetHeadSHA() string { + if a == nil || a.HeadSHA == nil { + return "" + } + return *a.HeadSHA +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (a *ArtifactWorkflowRun) GetID() int64 { + if a == nil || a.ID == nil { + return 0 + } + return *a.ID +} + +// GetRepositoryID returns the RepositoryID field if it's non-nil, zero value otherwise. +func (a *ArtifactWorkflowRun) GetRepositoryID() int64 { + if a == nil || a.RepositoryID == nil { + return 0 + } + return *a.RepositoryID +} + // GetBody returns the Body field if it's non-nil, zero value otherwise. func (a *Attachment) GetBody() string { if a == nil || a.Body == nil { @@ -1118,6 +1222,14 @@ func (a *AuditEntry) GetName() string { return *a.Name } +// GetOldPermission returns the OldPermission field if it's non-nil, zero value otherwise. +func (a *AuditEntry) GetOldPermission() string { + if a == nil || a.OldPermission == nil { + return "" + } + return *a.OldPermission +} + // GetOldUser returns the OldUser field if it's non-nil, zero value otherwise. func (a *AuditEntry) GetOldUser() string { if a == nil || a.OldUser == nil { @@ -1142,6 +1254,14 @@ func (a *AuditEntry) GetOrg() string { return *a.Org } +// GetPermission returns the Permission field if it's non-nil, zero value otherwise. +func (a *AuditEntry) GetPermission() string { + if a == nil || a.Permission == nil { + return "" + } + return *a.Permission +} + // GetPreviousVisibility returns the PreviousVisibility field if it's non-nil, zero value otherwise. func (a *AuditEntry) GetPreviousVisibility() string { if a == nil || a.PreviousVisibility == nil { @@ -8286,6 +8406,14 @@ func (i *IssuesEvent) GetLabel() *Label { return i.Label } +// GetMilestone returns the Milestone field. +func (i *IssuesEvent) GetMilestone() *Milestone { + if i == nil { + return nil + } + return i.Milestone +} + // GetRepo returns the Repo field. func (i *IssuesEvent) GetRepo() *Repository { if i == nil { @@ -9134,6 +9262,14 @@ func (m *MarketplacePlanAccount) GetURL() string { return *m.URL } +// GetAccount returns the Account field. +func (m *MarketplacePurchase) GetAccount() *MarketplacePurchaseAccount { + if m == nil { + return nil + } + return m.Account +} + // GetBillingCycle returns the BillingCycle field if it's non-nil, zero value otherwise. func (m *MarketplacePurchase) GetBillingCycle() string { if m == nil || m.BillingCycle == nil { @@ -9190,6 +9326,62 @@ func (m *MarketplacePurchase) GetUpdatedAt() Timestamp { return *m.UpdatedAt } +// GetEmail returns the Email field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetEmail() string { + if m == nil || m.Email == nil { + return "" + } + return *m.Email +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetID() int64 { + if m == nil || m.ID == nil { + return 0 + } + return *m.ID +} + +// GetLogin returns the Login field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetLogin() string { + if m == nil || m.Login == nil { + return "" + } + return *m.Login +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetNodeID() string { + if m == nil || m.NodeID == nil { + return "" + } + return *m.NodeID +} + +// GetOrganizationBillingEmail returns the OrganizationBillingEmail field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetOrganizationBillingEmail() string { + if m == nil || m.OrganizationBillingEmail == nil { + return "" + } + return *m.OrganizationBillingEmail +} + +// GetType returns the Type field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetType() string { + if m == nil || m.Type == nil { + return "" + } + return *m.Type +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (m *MarketplacePurchaseAccount) GetURL() string { + if m == nil || m.URL == nil { + return "" + } + return *m.URL +} + // GetAction returns the Action field if it's non-nil, zero value otherwise. func (m *MarketplacePurchaseEvent) GetAction() string { if m == nil || m.Action == nil { @@ -19326,6 +19518,14 @@ func (t *Timeline) GetRename() *Rename { return t.Rename } +// GetRequestedTeam returns the RequestedTeam field. +func (t *Timeline) GetRequestedTeam() *Team { + if t == nil { + return nil + } + return t.RequestedTeam +} + // GetRequester returns the Requester field. func (t *Timeline) GetRequester() *User { if t == nil { @@ -20806,6 +21006,14 @@ func (w *WorkflowJob) GetConclusion() string { return *w.Conclusion } +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetCreatedAt() Timestamp { + if w == nil || w.CreatedAt == nil { + return Timestamp{} + } + return *w.CreatedAt +} + // GetHeadSHA returns the HeadSHA field if it's non-nil, zero value otherwise. func (w *WorkflowJob) GetHeadSHA() string { if w == nil || w.HeadSHA == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 24e0a758264..2bd40a94be4 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -215,6 +215,53 @@ func TestActionsPermissionsRepository_GetSelectedActionsURL(tt *testing.T) { a.GetSelectedActionsURL() } +func TestActionsVariable_GetCreatedAt(tt *testing.T) { + var zeroValue Timestamp + a := &ActionsVariable{CreatedAt: &zeroValue} + a.GetCreatedAt() + a = &ActionsVariable{} + a.GetCreatedAt() + a = nil + a.GetCreatedAt() +} + +func TestActionsVariable_GetSelectedRepositoriesURL(tt *testing.T) { + var zeroValue string + a := &ActionsVariable{SelectedRepositoriesURL: &zeroValue} + a.GetSelectedRepositoriesURL() + a = &ActionsVariable{} + a.GetSelectedRepositoriesURL() + a = nil + a.GetSelectedRepositoriesURL() +} + +func TestActionsVariable_GetSelectedRepositoryIDs(tt *testing.T) { + a := &ActionsVariable{} + a.GetSelectedRepositoryIDs() + a = nil + a.GetSelectedRepositoryIDs() +} + +func TestActionsVariable_GetUpdatedAt(tt *testing.T) { + var zeroValue Timestamp + a := &ActionsVariable{UpdatedAt: &zeroValue} + a.GetUpdatedAt() + a = &ActionsVariable{} + a.GetUpdatedAt() + a = nil + a.GetUpdatedAt() +} + +func TestActionsVariable_GetVisibility(tt *testing.T) { + var zeroValue string + a := &ActionsVariable{Visibility: &zeroValue} + a.GetVisibility() + a = &ActionsVariable{} + a.GetVisibility() + a = nil + a.GetVisibility() +} + func TestAdminEnforcedChanges_GetFrom(tt *testing.T) { var zeroValue bool a := &AdminEnforcedChanges{From: &zeroValue} @@ -1009,6 +1056,33 @@ func TestArtifact_GetSizeInBytes(tt *testing.T) { a.GetSizeInBytes() } +func TestArtifact_GetUpdatedAt(tt *testing.T) { + var zeroValue Timestamp + a := &Artifact{UpdatedAt: &zeroValue} + a.GetUpdatedAt() + a = &Artifact{} + a.GetUpdatedAt() + a = nil + a.GetUpdatedAt() +} + +func TestArtifact_GetURL(tt *testing.T) { + var zeroValue string + a := &Artifact{URL: &zeroValue} + a.GetURL() + a = &Artifact{} + a.GetURL() + a = nil + a.GetURL() +} + +func TestArtifact_GetWorkflowRun(tt *testing.T) { + a := &Artifact{} + a.GetWorkflowRun() + a = nil + a.GetWorkflowRun() +} + func TestArtifactList_GetTotalCount(tt *testing.T) { var zeroValue int64 a := &ArtifactList{TotalCount: &zeroValue} @@ -1019,6 +1093,56 @@ func TestArtifactList_GetTotalCount(tt *testing.T) { a.GetTotalCount() } +func TestArtifactWorkflowRun_GetHeadBranch(tt *testing.T) { + var zeroValue string + a := &ArtifactWorkflowRun{HeadBranch: &zeroValue} + a.GetHeadBranch() + a = &ArtifactWorkflowRun{} + a.GetHeadBranch() + a = nil + a.GetHeadBranch() +} + +func TestArtifactWorkflowRun_GetHeadRepositoryID(tt *testing.T) { + var zeroValue int64 + a := &ArtifactWorkflowRun{HeadRepositoryID: &zeroValue} + a.GetHeadRepositoryID() + a = &ArtifactWorkflowRun{} + a.GetHeadRepositoryID() + a = nil + a.GetHeadRepositoryID() +} + +func TestArtifactWorkflowRun_GetHeadSHA(tt *testing.T) { + var zeroValue string + a := &ArtifactWorkflowRun{HeadSHA: &zeroValue} + a.GetHeadSHA() + a = &ArtifactWorkflowRun{} + a.GetHeadSHA() + a = nil + a.GetHeadSHA() +} + +func TestArtifactWorkflowRun_GetID(tt *testing.T) { + var zeroValue int64 + a := &ArtifactWorkflowRun{ID: &zeroValue} + a.GetID() + a = &ArtifactWorkflowRun{} + a.GetID() + a = nil + a.GetID() +} + +func TestArtifactWorkflowRun_GetRepositoryID(tt *testing.T) { + var zeroValue int64 + a := &ArtifactWorkflowRun{RepositoryID: &zeroValue} + a.GetRepositoryID() + a = &ArtifactWorkflowRun{} + a.GetRepositoryID() + a = nil + a.GetRepositoryID() +} + func TestAttachment_GetBody(tt *testing.T) { var zeroValue string a := &Attachment{Body: &zeroValue} @@ -1323,6 +1447,16 @@ func TestAuditEntry_GetName(tt *testing.T) { a.GetName() } +func TestAuditEntry_GetOldPermission(tt *testing.T) { + var zeroValue string + a := &AuditEntry{OldPermission: &zeroValue} + a.GetOldPermission() + a = &AuditEntry{} + a.GetOldPermission() + a = nil + a.GetOldPermission() +} + func TestAuditEntry_GetOldUser(tt *testing.T) { var zeroValue string a := &AuditEntry{OldUser: &zeroValue} @@ -1353,6 +1487,16 @@ func TestAuditEntry_GetOrg(tt *testing.T) { a.GetOrg() } +func TestAuditEntry_GetPermission(tt *testing.T) { + var zeroValue string + a := &AuditEntry{Permission: &zeroValue} + a.GetPermission() + a = &AuditEntry{} + a.GetPermission() + a = nil + a.GetPermission() +} + func TestAuditEntry_GetPreviousVisibility(tt *testing.T) { var zeroValue string a := &AuditEntry{PreviousVisibility: &zeroValue} @@ -9719,6 +9863,13 @@ func TestIssuesEvent_GetLabel(tt *testing.T) { i.GetLabel() } +func TestIssuesEvent_GetMilestone(tt *testing.T) { + i := &IssuesEvent{} + i.GetMilestone() + i = nil + i.GetMilestone() +} + func TestIssuesEvent_GetRepo(tt *testing.T) { i := &IssuesEvent{} i.GetRepo() @@ -10743,6 +10894,13 @@ func TestMarketplacePlanAccount_GetURL(tt *testing.T) { m.GetURL() } +func TestMarketplacePurchase_GetAccount(tt *testing.T) { + m := &MarketplacePurchase{} + m.GetAccount() + m = nil + m.GetAccount() +} + func TestMarketplacePurchase_GetBillingCycle(tt *testing.T) { var zeroValue string m := &MarketplacePurchase{BillingCycle: &zeroValue} @@ -10810,6 +10968,76 @@ func TestMarketplacePurchase_GetUpdatedAt(tt *testing.T) { m.GetUpdatedAt() } +func TestMarketplacePurchaseAccount_GetEmail(tt *testing.T) { + var zeroValue string + m := &MarketplacePurchaseAccount{Email: &zeroValue} + m.GetEmail() + m = &MarketplacePurchaseAccount{} + m.GetEmail() + m = nil + m.GetEmail() +} + +func TestMarketplacePurchaseAccount_GetID(tt *testing.T) { + var zeroValue int64 + m := &MarketplacePurchaseAccount{ID: &zeroValue} + m.GetID() + m = &MarketplacePurchaseAccount{} + m.GetID() + m = nil + m.GetID() +} + +func TestMarketplacePurchaseAccount_GetLogin(tt *testing.T) { + var zeroValue string + m := &MarketplacePurchaseAccount{Login: &zeroValue} + m.GetLogin() + m = &MarketplacePurchaseAccount{} + m.GetLogin() + m = nil + m.GetLogin() +} + +func TestMarketplacePurchaseAccount_GetNodeID(tt *testing.T) { + var zeroValue string + m := &MarketplacePurchaseAccount{NodeID: &zeroValue} + m.GetNodeID() + m = &MarketplacePurchaseAccount{} + m.GetNodeID() + m = nil + m.GetNodeID() +} + +func TestMarketplacePurchaseAccount_GetOrganizationBillingEmail(tt *testing.T) { + var zeroValue string + m := &MarketplacePurchaseAccount{OrganizationBillingEmail: &zeroValue} + m.GetOrganizationBillingEmail() + m = &MarketplacePurchaseAccount{} + m.GetOrganizationBillingEmail() + m = nil + m.GetOrganizationBillingEmail() +} + +func TestMarketplacePurchaseAccount_GetType(tt *testing.T) { + var zeroValue string + m := &MarketplacePurchaseAccount{Type: &zeroValue} + m.GetType() + m = &MarketplacePurchaseAccount{} + m.GetType() + m = nil + m.GetType() +} + +func TestMarketplacePurchaseAccount_GetURL(tt *testing.T) { + var zeroValue string + m := &MarketplacePurchaseAccount{URL: &zeroValue} + m.GetURL() + m = &MarketplacePurchaseAccount{} + m.GetURL() + m = nil + m.GetURL() +} + func TestMarketplacePurchaseEvent_GetAction(tt *testing.T) { var zeroValue string m := &MarketplacePurchaseEvent{Action: &zeroValue} @@ -22520,6 +22748,13 @@ func TestTimeline_GetRename(tt *testing.T) { t.GetRename() } +func TestTimeline_GetRequestedTeam(tt *testing.T) { + t := &Timeline{} + t.GetRequestedTeam() + t = nil + t.GetRequestedTeam() +} + func TestTimeline_GetRequester(tt *testing.T) { t := &Timeline{} t.GetRequester() @@ -24313,6 +24548,16 @@ func TestWorkflowJob_GetConclusion(tt *testing.T) { w.GetConclusion() } +func TestWorkflowJob_GetCreatedAt(tt *testing.T) { + var zeroValue Timestamp + w := &WorkflowJob{CreatedAt: &zeroValue} + w.GetCreatedAt() + w = &WorkflowJob{} + w.GetCreatedAt() + w = nil + w.GetCreatedAt() +} + func TestWorkflowJob_GetHeadSHA(tt *testing.T) { var zeroValue string w := &WorkflowJob{HeadSHA: &zeroValue} diff --git a/github/github.go b/github/github.go index 629218165c7..4e0d2f47e8f 100644 --- a/github/github.go +++ b/github/github.go @@ -28,7 +28,7 @@ import ( ) const ( - Version = "v50.0.0" + Version = "v50.1.0" defaultAPIVersion = "2022-11-28" defaultBaseURL = "https://api.github.com/" @@ -171,8 +171,9 @@ type Client struct { // User agent used when communicating with the GitHub API. UserAgent string - rateMu sync.Mutex - rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls. + rateMu sync.Mutex + rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls. + secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls. common service // Reuse a single struct instead of allocating one for each service on the heap. @@ -570,7 +571,8 @@ type Response struct { // propagate to Response. Rate Rate - // token's expiration date + // token's expiration date. Timestamp is 0001-01-01 when token doesn't expire. + // So it is valid for TokenExpiration.Equal(Timestamp{}) or TokenExpiration.Time.After(time.Now()) TokenExpiration Timestamp } @@ -671,14 +673,19 @@ func parseRate(r *http.Response) Rate { } // parseTokenExpiration parses the TokenExpiration related headers. +// Returns 0001-01-01 if the header is not defined or could not be parsed. func parseTokenExpiration(r *http.Response) Timestamp { - var exp Timestamp if v := r.Header.Get(headerTokenExpiration); v != "" { - if t, err := time.Parse("2006-01-02 03:04:05 MST", v); err == nil { - exp = Timestamp{t.Local()} + if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil { + return Timestamp{t.Local()} + } + // Some tokens include the timezone offset instead of the timezone. + // https://github.com/google/go-github/issues/2649 + if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil { + return Timestamp{t.Local()} } } - return exp + return Timestamp{} // 0001-01-01 00:00:00 } type requestContext uint8 @@ -702,7 +709,7 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro req = withContext(ctx, req) - rateLimitCategory := category(req.URL.Path) + rateLimitCategory := category(req.Method, req.URL.Path) if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil { // If we've hit rate limit, don't make further requests before Reset time. @@ -712,6 +719,12 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro Rate: err.Rate, }, err } + // If we've hit a secondary rate limit, don't make further requests before Retry After. + if err := c.checkSecondaryRateLimitBeforeDo(ctx, req); err != nil { + return &Response{ + Response: err.Response, + }, err + } } resp, err := c.client.Do(req) @@ -763,6 +776,14 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro aerr.Raw = b err = aerr } + + // Update the secondary rate limit if we hit it. + rerr, ok := err.(*AbuseRateLimitError) + if ok && rerr.RetryAfter != nil { + c.rateMu.Lock() + c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter) + c.rateMu.Unlock() + } } return response, err } @@ -827,6 +848,35 @@ func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rat return nil } +// checkSecondaryRateLimitBeforeDo does not make any network calls, but uses existing knowledge from +// current client state in order to quickly check if *AbuseRateLimitError can be immediately returned +// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. +// Otherwise it returns nil, and Client.Do should proceed normally. +func (c *Client) checkSecondaryRateLimitBeforeDo(ctx context.Context, req *http.Request) *AbuseRateLimitError { + c.rateMu.Lock() + secondary := c.secondaryRateLimitReset + c.rateMu.Unlock() + if !secondary.IsZero() && time.Now().Before(secondary) { + // Create a fake response. + resp := &http.Response{ + Status: http.StatusText(http.StatusForbidden), + StatusCode: http.StatusForbidden, + Request: req, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader("")), + } + + retryAfter := time.Until(secondary) + return &AbuseRateLimitError{ + Response: resp, + Message: fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary), + RetryAfter: &retryAfter, + } + } + + return nil +} + // compareHTTPResponse returns whether two http.Response objects are equal or not. // Currently, only StatusCode is checked. This function is used when implementing the // Is(error) bool interface for the custom error types in this package. @@ -1197,13 +1247,36 @@ const ( categories // An array of this length will be able to contain all rate limit categories. ) -// category returns the rate limit category of the endpoint, determined by Request.URL.Path. -func category(path string) rateLimitCategory { +// category returns the rate limit category of the endpoint, determined by HTTP method and Request.URL.Path. +func category(method, path string) rateLimitCategory { switch { + // https://docs.github.com/en/rest/rate-limit#about-rate-limits default: + // NOTE: coreCategory is returned for actionsRunnerRegistrationCategory too, + // because no API found for this category. return coreCategory case strings.HasPrefix(path, "/search/"): return searchCategory + case path == "/graphql": + return graphqlCategory + case strings.HasPrefix(path, "/app-manifests/") && + strings.HasSuffix(path, "/conversions") && + method == http.MethodPost: + return integrationManifestCategory + + // https://docs.github.com/en/rest/migrations/source-imports#start-an-import + case strings.HasPrefix(path, "/repos/") && + strings.HasSuffix(path, "/import") && + method == http.MethodPut: + return sourceImportCategory + + // https://docs.github.com/en/rest/code-scanning#upload-an-analysis-as-sarif-data + case strings.HasSuffix(path, "/code-scanning/sarifs"): + return codeScanningUploadCategory + + // https://docs.github.com/en/enterprise-cloud@latest/rest/scim + case strings.HasPrefix(path, "/scim/"): + return scimCategory } } diff --git a/github/github_test.go b/github/github_test.go index 50ecd5435bc..0c969404341 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -185,6 +185,11 @@ func testBadOptions(t *testing.T, methodName string, f func() error) { // Method f should be a regular call that would normally succeed, but // should return an error when NewRequest or s.client.Do fails. func testNewRequestAndDoFailure(t *testing.T, methodName string, client *Client, f func() (*Response, error)) { + testNewRequestAndDoFailureCategory(t, methodName, client, coreCategory, f) +} + +// testNewRequestAndDoFailureCategory works Like testNewRequestAndDoFailure, but allows setting the category +func testNewRequestAndDoFailureCategory(t *testing.T, methodName string, client *Client, category rateLimitCategory, f func() (*Response, error)) { t.Helper() if methodName == "" { t.Error("testNewRequestAndDoFailure: must supply method methodName") @@ -200,7 +205,7 @@ func testNewRequestAndDoFailure(t *testing.T, methodName string, client *Client, } client.BaseURL.Path = "/api-v3/" - client.rateLimits[0].Reset.Time = time.Now().Add(10 * time.Minute) + client.rateLimits[category].Reset.Time = time.Now().Add(10 * time.Minute) resp, err = f() if bypass := resp.Request.Context().Value(bypassRateLimitCheck); bypass != nil { return @@ -1136,6 +1141,67 @@ func TestDo_rateLimit(t *testing.T) { } } +func TestDo_rateLimitCategory(t *testing.T) { + tests := []struct { + method string + url string + category rateLimitCategory + }{ + { + method: http.MethodGet, + url: "/", + category: coreCategory, + }, + { + method: http.MethodGet, + url: "/search/issues?q=rate", + category: searchCategory, + }, + { + method: http.MethodGet, + url: "/graphql", + category: graphqlCategory, + }, + { + method: http.MethodPost, + url: "/app-manifests/code/conversions", + category: integrationManifestCategory, + }, + { + method: http.MethodGet, + url: "/app-manifests/code/conversions", + category: coreCategory, // only POST requests are in the integration manifest category + }, + { + method: http.MethodPut, + url: "/repos/google/go-github/import", + category: sourceImportCategory, + }, + { + method: http.MethodGet, + url: "/repos/google/go-github/import", + category: coreCategory, // only PUT requests are in the source import category + }, + { + method: http.MethodPost, + url: "/repos/google/go-github/code-scanning/sarifs", + category: codeScanningUploadCategory, + }, + { + method: http.MethodGet, + url: "/scim/v2/organizations/ORG/Users", + category: scimCategory, + }, + // missing a check for actionsRunnerRegistrationCategory: API not found + } + + for _, tt := range tests { + if got, want := category(tt.method, tt.url), tt.category; got != want { + t.Errorf("expecting category %v, found %v", got, want) + } + } +} + // ensure rate limit is still parsed, even for error responses func TestDo_rateLimit_errorResponse(t *testing.T) { client, mux, _, teardown := setup() @@ -1407,6 +1473,25 @@ func TestDo_rateLimit_abuseRateLimitError_retryAfter(t *testing.T) { if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; got != want { t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want) } + + // expect prevention of a following request + if _, err = client.Do(ctx, req, nil); err == nil { + t.Error("Expected error to be returned.") + } + abuseRateLimitErr, ok = err.(*AbuseRateLimitError) + if !ok { + t.Fatalf("Expected a *AbuseRateLimitError error; got %#v.", err) + } + if abuseRateLimitErr.RetryAfter == nil { + t.Fatalf("abuseRateLimitErr RetryAfter is nil, expected not-nil") + } + // the saved duration might be a bit smaller than Retry-After because the duration is calculated from the expected end-of-cooldown time + if got, want := *abuseRateLimitErr.RetryAfter, 123*time.Second; want-got > 1*time.Second { + t.Errorf("abuseRateLimitErr RetryAfter = %v, want %v", got, want) + } + if got, wantSuffix := abuseRateLimitErr.Message, "not making remote request."; !strings.HasSuffix(got, wantSuffix) { + t.Errorf("Expected request to be prevented because of secondary rate limit, got: %v.", got) + } } func TestDo_noContent(t *testing.T) { @@ -2065,30 +2150,48 @@ func TestRateLimits(t *testing.T) { if !cmp.Equal(rate, want) { t.Errorf("RateLimits returned %+v, want %+v", rate, want) } - - if got, want := client.rateLimits[coreCategory], *want.Core; got != want { - t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want) - } - if got, want := client.rateLimits[searchCategory], *want.Search; got != want { - t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want) - } - if got, want := client.rateLimits[graphqlCategory], *want.GraphQL; got != want { - t.Errorf("client.rateLimits[graphqlCategory] is %+v, want %+v", got, want) - } - if got, want := client.rateLimits[integrationManifestCategory], *want.IntegrationManifest; got != want { - t.Errorf("client.rateLimits[integrationManifestCategory] is %+v, want %+v", got, want) - } - if got, want := client.rateLimits[sourceImportCategory], *want.SourceImport; got != want { - t.Errorf("client.rateLimits[sourceImportCategory] is %+v, want %+v", got, want) - } - if got, want := client.rateLimits[codeScanningUploadCategory], *want.CodeScanningUpload; got != want { - t.Errorf("client.rateLimits[codeScanningUploadCategory] is %+v, want %+v", got, want) - } - if got, want := client.rateLimits[actionsRunnerRegistrationCategory], *want.ActionsRunnerRegistration; got != want { - t.Errorf("client.rateLimits[actionsRunnerRegistrationCategory] is %+v, want %+v", got, want) + tests := []struct { + category rateLimitCategory + rate *Rate + }{ + { + category: coreCategory, + rate: want.Core, + }, + { + category: searchCategory, + rate: want.Search, + }, + { + category: graphqlCategory, + rate: want.GraphQL, + }, + { + category: integrationManifestCategory, + rate: want.IntegrationManifest, + }, + { + category: sourceImportCategory, + rate: want.SourceImport, + }, + { + category: codeScanningUploadCategory, + rate: want.CodeScanningUpload, + }, + { + category: actionsRunnerRegistrationCategory, + rate: want.ActionsRunnerRegistration, + }, + { + category: scimCategory, + rate: want.SCIM, + }, } - if got, want := client.rateLimits[scimCategory], *want.SCIM; got != want { - t.Errorf("client.rateLimits[scimCategory] is %+v, want %+v", got, want) + + for _, tt := range tests { + if got, want := client.rateLimits[tt.category], *tt.rate; got != want { + t.Errorf("client.rateLimits[%v] is %+v, want %+v", tt.category, got, want) + } } } @@ -2117,7 +2220,13 @@ func TestRateLimits_overQuota(t *testing.T) { mux.HandleFunc("/rate_limit", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"resources":{ "core": {"limit":2,"remaining":1,"reset":1372700873}, - "search": {"limit":3,"remaining":2,"reset":1372700874} + "search": {"limit":3,"remaining":2,"reset":1372700874}, + "graphql": {"limit":4,"remaining":3,"reset":1372700875}, + "integration_manifest": {"limit":5,"remaining":4,"reset":1372700876}, + "source_import": {"limit":6,"remaining":5,"reset":1372700877}, + "code_scanning_upload": {"limit":7,"remaining":6,"reset":1372700878}, + "actions_runner_registration": {"limit":8,"remaining":7,"reset":1372700879}, + "scim": {"limit":9,"remaining":8,"reset":1372700880} }}`) }) @@ -2138,16 +2247,82 @@ func TestRateLimits_overQuota(t *testing.T) { Remaining: 2, Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 54, 0, time.UTC).Local()}, }, + GraphQL: &Rate{ + Limit: 4, + Remaining: 3, + Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 55, 0, time.UTC).Local()}, + }, + IntegrationManifest: &Rate{ + Limit: 5, + Remaining: 4, + Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 56, 0, time.UTC).Local()}, + }, + SourceImport: &Rate{ + Limit: 6, + Remaining: 5, + Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 57, 0, time.UTC).Local()}, + }, + CodeScanningUpload: &Rate{ + Limit: 7, + Remaining: 6, + Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 58, 0, time.UTC).Local()}, + }, + ActionsRunnerRegistration: &Rate{ + Limit: 8, + Remaining: 7, + Reset: Timestamp{time.Date(2013, time.July, 1, 17, 47, 59, 0, time.UTC).Local()}, + }, + SCIM: &Rate{ + Limit: 9, + Remaining: 8, + Reset: Timestamp{time.Date(2013, time.July, 1, 17, 48, 00, 0, time.UTC).Local()}, + }, } if !cmp.Equal(rate, want) { t.Errorf("RateLimits returned %+v, want %+v", rate, want) } - if got, want := client.rateLimits[coreCategory], *want.Core; got != want { - t.Errorf("client.rateLimits[coreCategory] is %+v, want %+v", got, want) + tests := []struct { + category rateLimitCategory + rate *Rate + }{ + { + category: coreCategory, + rate: want.Core, + }, + { + category: searchCategory, + rate: want.Search, + }, + { + category: graphqlCategory, + rate: want.GraphQL, + }, + { + category: integrationManifestCategory, + rate: want.IntegrationManifest, + }, + { + category: sourceImportCategory, + rate: want.SourceImport, + }, + { + category: codeScanningUploadCategory, + rate: want.CodeScanningUpload, + }, + { + category: actionsRunnerRegistrationCategory, + rate: want.ActionsRunnerRegistration, + }, + { + category: scimCategory, + rate: want.SCIM, + }, } - if got, want := client.rateLimits[searchCategory], *want.Search; got != want { - t.Errorf("client.rateLimits[searchCategory] is %+v, want %+v", got, want) + for _, tt := range tests { + if got, want := client.rateLimits[tt.category], *tt.rate; got != want { + t.Errorf("client.rateLimits[%v] is %+v, want %+v", tt.category, got, want) + } } } @@ -2685,6 +2860,16 @@ func TestParseTokenExpiration(t *testing.T) { header: "2021-09-03 02:34:04 UTC", want: Timestamp{time.Date(2021, time.September, 3, 2, 34, 4, 0, time.UTC)}, }, + { + header: "2021-09-03 14:34:04 UTC", + want: Timestamp{time.Date(2021, time.September, 3, 14, 34, 4, 0, time.UTC)}, + }, + // Some tokens include the timezone offset instead of the timezone. + // https://github.com/google/go-github/issues/2649 + { + header: "2023-04-26 20:23:26 +0200", + want: Timestamp{time.Date(2023, time.April, 26, 18, 23, 26, 0, time.UTC)}, + }, } for _, tt := range tests { @@ -2696,7 +2881,7 @@ func TestParseTokenExpiration(t *testing.T) { res.Header.Set(headerTokenExpiration, tt.header) exp := parseTokenExpiration(res) if !exp.Equal(tt.want) { - t.Errorf("parseTokenExpiration returned %#v, want %#v", exp, tt.want) + t.Errorf("parseTokenExpiration of %q\nreturned %#v\n want %#v", tt.header, exp, tt.want) } } } diff --git a/github/issues_timeline.go b/github/issues_timeline.go index 2f049aff56f..9c73e6176d1 100644 --- a/github/issues_timeline.go +++ b/github/issues_timeline.go @@ -143,6 +143,8 @@ type Timeline struct { // The person requested to review the pull request. Reviewer *User `json:"requested_reviewer,omitempty"` + // RequestedTeam contains the team requested to review the pull request. + RequestedTeam *Team `json:"requested_team,omitempty"` // The person who requested a review. Requester *User `json:"review_requester,omitempty"` diff --git a/github/orgs_audit_log.go b/github/orgs_audit_log.go index be5fd24d6ed..700c233c803 100644 --- a/github/orgs_audit_log.go +++ b/github/orgs_audit_log.go @@ -63,8 +63,10 @@ type AuditEntry struct { Message *string `json:"message,omitempty"` Name *string `json:"name,omitempty"` OldUser *string `json:"old_user,omitempty"` + OldPermission *string `json:"old_permission,omitempty"` // The permission level for membership changes, for example `admin` or `read`. OpenSSHPublicKey *string `json:"openssh_public_key,omitempty"` Org *string `json:"org,omitempty"` + Permission *string `json:"permission,omitempty"` // The permission level for membership changes, for example `admin` or `read`. PreviousVisibility *string `json:"previous_visibility,omitempty"` ReadOnly *string `json:"read_only,omitempty"` Repo *string `json:"repo,omitempty"` diff --git a/github/orgs_audit_log_test.go b/github/orgs_audit_log_test.go index 1f060072452..be6361560f9 100644 --- a/github/orgs_audit_log_test.go +++ b/github/orgs_audit_log_test.go @@ -34,6 +34,8 @@ func TestOrganizationService_GetAuditLog(t *testing.T) { "created_at": 1615077308538, "head_sha": "5acdeadbeef64d1a62388e901e5cdc9358644b37", "conclusion": "success", + "old_permission": "read", + "permission": "admin", "actor": "testactor", "completed_at": "2021-03-07T00:35:08.000Z", "@timestamp": 1615077308538, @@ -80,7 +82,9 @@ func TestOrganizationService_GetAuditLog(t *testing.T) { HeadBranch: String("master"), HeadSHA: String("5acdeadbeef64d1a62388e901e5cdc9358644b37"), Name: String("Code scanning - action"), + OldPermission: String("read"), Org: String("o"), + Permission: String("admin"), Repo: String("o/blue-crayon-1"), StartedAt: &Timestamp{startedAt}, WorkflowID: Int64(123456), @@ -199,9 +203,11 @@ func TestAuditEntry_Marshal(t *testing.T) { LimitedAvailability: Bool(false), Message: String("m"), Name: String("n"), + OldPermission: String("op"), OldUser: String("ou"), OpenSSHPublicKey: String("osshpk"), Org: String("o"), + Permission: String("p"), PreviousVisibility: String("pv"), ReadOnly: String("ro"), Repo: String("r"), @@ -268,9 +274,11 @@ func TestAuditEntry_Marshal(t *testing.T) { "limited_availability": false, "message": "m", "name": "n", + "old_permission": "op", "old_user": "ou", "openssh_public_key": "osshpk", "org": "o", + "permission": "p", "previous_visibility": "pv", "read_only": "ro", "repo": "r", diff --git a/github/repos_collaborators.go b/github/repos_collaborators.go index abc4161c3bb..c2396872f2e 100644 --- a/github/repos_collaborators.go +++ b/github/repos_collaborators.go @@ -23,6 +23,13 @@ type ListCollaboratorsOptions struct { // Default value is "all". Affiliation string `url:"affiliation,omitempty"` + // Permission specifies how collaborators should be filtered by the permissions they have on the repository. + // Possible values are: + // "pull", "triage", "push", "maintain", "admin" + // + // If not specified, all collaborators will be returned. + Permission string `url:"permission,omitempty"` + ListOptions } diff --git a/github/repos_collaborators_test.go b/github/repos_collaborators_test.go index 8bb6c15cf1a..8953d36bccf 100644 --- a/github/repos_collaborators_test.go +++ b/github/repos_collaborators_test.go @@ -94,6 +94,46 @@ func TestRepositoriesService_ListCollaborators_withAffiliation(t *testing.T) { }) } +func TestRepositoriesService_ListCollaborators_withPermission(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/collaborators", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"permission": "pull", "page": "2"}) + fmt.Fprintf(w, `[{"id":1}, {"id":2}]`) + }) + + opt := &ListCollaboratorsOptions{ + ListOptions: ListOptions{Page: 2}, + Permission: "pull", + } + ctx := context.Background() + users, _, err := client.Repositories.ListCollaborators(ctx, "o", "r", opt) + if err != nil { + t.Errorf("Repositories.ListCollaborators returned error: %v", err) + } + + want := []*User{{ID: Int64(1)}, {ID: Int64(2)}} + if !cmp.Equal(users, want) { + t.Errorf("Repositories.ListCollaborators returned %+v, want %+v", users, want) + } + + const methodName = "ListCollaborators" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Repositories.ListCollaborators(ctx, "\n", "\n", opt) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Repositories.ListCollaborators(ctx, "o", "r", opt) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestRepositoriesService_ListCollaborators_invalidOwner(t *testing.T) { client, _, _, teardown := setup() defer teardown() diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go index db3efb91c60..6616e5af0c0 100644 --- a/github/repos_contents_test.go +++ b/github/repos_contents_test.go @@ -744,23 +744,6 @@ func TestRepositoriesService_GetArchiveLink_StatusMovedPermanently_followRedirec } } -func TestRepositoriesService_GetArchiveLink_invalidLocationHeader(t *testing.T) { - client, mux, _, teardown := setup() - defer teardown() - - mux.HandleFunc("/repos/o/r/tarball", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - ctlChar := 0x7f - badURL := "https://google.com" + string(byte(ctlChar)) - w.Header().Add("Location", badURL) - w.WriteHeader(http.StatusFound) - }) - - ctx := context.Background() - _, _, err := client.Repositories.GetArchiveLink(ctx, "o", "r", Tarball, &RepositoryContentGetOptions{}, false) - testURLParseError(t, err) -} - func TestRepositoriesService_GetContents_NoTrailingSlashInDirectoryApiPath(t *testing.T) { client, mux, _, teardown := setup() defer teardown() diff --git a/scrape/apps.go b/scrape/apps.go index 693addeb33d..48480a3d807 100644 --- a/scrape/apps.go +++ b/scrape/apps.go @@ -17,7 +17,7 @@ import ( "strings" "github.com/PuerkitoBio/goquery" - "github.com/google/go-github/v49/github" + "github.com/google/go-github/v50/github" ) // AppRestrictionsEnabled returns whether the specified organization has diff --git a/scrape/apps_test.go b/scrape/apps_test.go index 6750f807392..779ffdbbd33 100644 --- a/scrape/apps_test.go +++ b/scrape/apps_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/google/go-github/v49/github" + "github.com/google/go-github/v50/github" ) func Test_AppRestrictionsEnabled(t *testing.T) { diff --git a/scrape/go.mod b/scrape/go.mod index 3facac41259..3aa019c51dc 100644 --- a/scrape/go.mod +++ b/scrape/go.mod @@ -3,9 +3,9 @@ module github.com/google/go-github/scrape go 1.13 require ( - github.com/PuerkitoBio/goquery v1.8.0 + github.com/PuerkitoBio/goquery v1.8.1 github.com/google/go-cmp v0.5.9 - github.com/google/go-github/v49 v49.1.0 + github.com/google/go-github/v50 v50.0.0 github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 - golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 + golang.org/x/net v0.7.0 ) diff --git a/scrape/go.sum b/scrape/go.sum index 024869e76a4..a67b43953bf 100644 --- a/scrape/go.sum +++ b/scrape/go.sum @@ -1,35 +1,56 @@ -github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= -github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v49 v49.1.0 h1:LFkMgawGQ8dfzWLH/rNE0b3u1D3n6/dw7ZmrN3b+YFY= -github.com/google/go-github/v49 v49.1.0/go.mod h1:MUUzHPrhGniB6vUKa27y37likpipzG+BXXJbG04J334= +github.com/google/go-github/v50 v50.0.0 h1:gdO1AeuSZZK4iYWwVbjni7zg8PIQhp7QfmPunr016Jk= +github.com/google/go-github/v50 v50.0.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119 h1:YyPWX3jLOtYKulBR6AScGIs74lLrJcgeKRwcbAuQOG4= github.com/xlzd/gotp v0.0.0-20181030022105-c8557ba2c119/go.mod h1:/nuTSlK+okRfR/vnIPqR89fFKonnWPiZymN5ydRJkX8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=