Repository: google/go-github
Pull request #2321 (2022-03-25 01:00:28)

Remove code duplication
This PR removes code duplication as detected by  `golangci-lint`.

gmlewis: @Parker77 - do you have time for a code review?
codecov[bot]: # [Codecov](https://codecov.io/gh/google/go-github/pull/2321?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> Merging [#2321](https://codecov.io/gh/google/go-github/pull/2321?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (106831b) into [master](https://codecov.io/gh/google/go-github/commit/631fa70d7dd77a7278b49fe724f5b0df1514c940?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (631fa70) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #2321      +/-   ##
==========================================
- Coverage   97.84%   97.83%   -0.02%     
==========================================
  Files         115      115              
  Lines       10416    10352      -64     
==========================================
- Hits        10192    10128      -64     
  Misses        156      156              
  Partials       68       68              
```


| [Impacted Files](https://codecov.io/gh/google/go-github/pull/2321?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | Coverage Δ | |
|---|---|---|
| [github/actions\_artifacts.go](https://codecov.io/gh/google/go-github/pull/2321/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2FjdGlvbnNfYXJ0aWZhY3RzLmdv) | `100.00% <100.00%> (ø)` | |
| [github/actions\_workflow\_jobs.go](https://codecov.io/gh/google/go-github/pull/2321/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2FjdGlvbnNfd29ya2Zsb3dfam9icy5nbw==) | `100.00% <100.00%> (ø)` | |
| [github/actions\_workflow\_runs.go](https://codecov.io/gh/google/go-github/pull/2321/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2FjdGlvbnNfd29ya2Zsb3dfcnVucy5nbw==) | `100.00% <100.00%> (ø)` | |
| [github/github.go](https://codecov.io/gh/google/go-github/pull/2321/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2dpdGh1Yi5nbw==) | `97.73% <100.00%> (+0.09%)` | :arrow_up: |
| [github/repos.go](https://codecov.io/gh/google/go-github/pull/2321/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL3JlcG9zLmdv) | `98.66% <100.00%> (-0.05%)` | :arrow_down: |
| [github/repos\_contents.go](https://codecov.io/gh/google/go-github/pull/2321/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL3JlcG9zX2NvbnRlbnRzLmdv) | `87.33% <100.00%> (-1.63%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/go-github/pull/2321?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/go-github/pull/2321?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Last update [631fa70...106831b](https://codecov.io/gh/google/go-github/pull/2321?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).


Patch:
diff --git a/example/actionpermissions/main.go b/example/actionpermissions/main.go
new file mode 100644
index 00000000..a84a8e54
--- /dev/null
+++ b/example/actionpermissions/main.go
@@ -0,0 +1,80 @@
+// Copyright 2022 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 actionpermissions command utilizes go-github as a cli tool for
+// changing GitHub Actions related permission settings for a repository.
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/google/go-github/v43/github"
+	"golang.org/x/oauth2"
+)
+
+var (
+	name  = flag.String("name", "", "repo to change Actions permissions.")
+	owner = flag.String("owner", "", "owner of targeted repo.")
+)
+
+func main() {
+	flag.Parse()
+	token := os.Getenv("GITHUB_AUTH_TOKEN")
+	if token == "" {
+		log.Fatal("Unauthorized: No token present")
+	}
+	if *name == "" {
+		log.Fatal("No name: repo name must be given")
+	}
+	if *owner == "" {
+		log.Fatal("No owner: owner of repo must be given")
+	}
+	ctx := context.Background()
+	ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
+	tc := oauth2.NewClient(ctx, ts)
+	client := github.NewClient(tc)
+
+	actionsPermissionsRepository, _, err := client.Repositories.GetActionsPermissions(ctx, *owner, *name)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("Current ActionsPermissions %s\n", actionsPermissionsRepository.String())
+
+	actionsPermissionsRepository = &github.ActionsPermissionsRepository{Enabled: github.Bool(true), AllowedActions: github.String("selected")}
+	_, _, err = client.Repositories.EditActionsPermissions(ctx, *owner, *name, *actionsPermissionsRepository)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("Current ActionsPermissions %s\n", actionsPermissionsRepository.String())
+
+	actionsAllowed, _, err := client.Repositories.GetActionsAllowed(ctx, *owner, *name)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("Current ActionsAllowed %s\n", actionsAllowed.String())
+
+	actionsAllowed = &github.ActionsAllowed{GithubOwnedAllowed: github.Bool(true), VerifiedAllowed: github.Bool(false), PatternsAllowed: []string{"a/b"}}
+	_, _, err = client.Repositories.EditActionsAllowed(ctx, *owner, *name, *actionsAllowed)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("Current ActionsAllowed %s\n", actionsAllowed.String())
+
+	actionsPermissionsRepository = &github.ActionsPermissionsRepository{Enabled: github.Bool(true), AllowedActions: github.String("all")}
+	_, _, err = client.Repositories.EditActionsPermissions(ctx, *owner, *name, *actionsPermissionsRepository)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	fmt.Printf("Current ActionsPermissions %s\n", actionsPermissionsRepository.String())
+}
diff --git a/github/actions_artifacts.go b/github/actions_artifacts.go
index 4aa7dc44..29259212 100644
--- a/github/actions_artifacts.go
+++ b/github/actions_artifacts.go
@@ -110,45 +110,20 @@ func (s *ActionsService) GetArtifact(ctx context.Context, owner, repo string, ar
 func (s *ActionsService) DownloadArtifact(ctx context.Context, owner, repo string, artifactID int64, followRedirects bool) (*url.URL, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/actions/artifacts/%v/zip", owner, repo, artifactID)
 
-	resp, err := s.getDownloadArtifactFromURL(ctx, u, followRedirects)
+	resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects)
 	if err != nil {
 		return nil, nil, err
 	}
+	defer resp.Body.Close()
 
 	if resp.StatusCode != http.StatusFound {
 		return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status)
 	}
+
 	parsedURL, err := url.Parse(resp.Header.Get("Location"))
 	return parsedURL, newResponse(resp), nil
 }
 
-func (s *ActionsService) getDownloadArtifactFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
-	req, err := s.client.NewRequest("GET", u, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var resp *http.Response
-	// Use http.DefaultTransport if no custom Transport is configured
-	req = withContext(ctx, req)
-	if s.client.client.Transport == nil {
-		resp, err = http.DefaultTransport.RoundTrip(req)
-	} else {
-		resp, err = s.client.client.Transport.RoundTrip(req)
-	}
-	if err != nil {
-		return nil, err
-	}
-	resp.Body.Close()
-
-	// If redirect response is returned, follow it
-	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
-		u = resp.Header.Get("Location")
-		resp, err = s.getDownloadArtifactFromURL(ctx, u, false)
-	}
-	return resp, err
-}
-
 // DeleteArtifact deletes a workflow run artifact.
 //
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/actions/#delete-an-artifact
diff --git a/github/actions_workflow_jobs.go b/github/actions_workflow_jobs.go
index 66b8ff6e..ad0e7b6e 100644
--- a/github/actions_workflow_jobs.go
+++ b/github/actions_workflow_jobs.go
@@ -114,41 +114,16 @@ func (s *ActionsService) GetWorkflowJobByID(ctx context.Context, owner, repo str
 func (s *ActionsService) GetWorkflowJobLogs(ctx context.Context, owner, repo string, jobID int64, followRedirects bool) (*url.URL, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/actions/jobs/%v/logs", owner, repo, jobID)
 
-	resp, err := s.getWorkflowLogsFromURL(ctx, u, followRedirects)
+	resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects)
 	if err != nil {
 		return nil, nil, err
 	}
+	defer resp.Body.Close()
 
 	if resp.StatusCode != http.StatusFound {
 		return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status)
 	}
+
 	parsedURL, err := url.Parse(resp.Header.Get("Location"))
 	return parsedURL, newResponse(resp), err
 }
-
-func (s *ActionsService) getWorkflowLogsFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
-	req, err := s.client.NewRequest("GET", u, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var resp *http.Response
-	// Use http.DefaultTransport if no custom Transport is configured
-	req = withContext(ctx, req)
-	if s.client.client.Transport == nil {
-		resp, err = http.DefaultTransport.RoundTrip(req)
-	} else {
-		resp, err = s.client.client.Transport.RoundTrip(req)
-	}
-	if err != nil {
-		return nil, err
-	}
-	resp.Body.Close()
-
-	// If redirect response is returned, follow it
-	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
-		u = resp.Header.Get("Location")
-		resp, err = s.getWorkflowLogsFromURL(ctx, u, false)
-	}
-	return resp, err
-}
diff --git a/github/actions_workflow_runs.go b/github/actions_workflow_runs.go
index 273d2cc4..05d55c03 100644
--- a/github/actions_workflow_runs.go
+++ b/github/actions_workflow_runs.go
@@ -231,14 +231,16 @@ func (s *ActionsService) CancelWorkflowRunByID(ctx context.Context, owner, repo
 func (s *ActionsService) GetWorkflowRunLogs(ctx context.Context, owner, repo string, runID int64, followRedirects bool) (*url.URL, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v/logs", owner, repo, runID)
 
-	resp, err := s.getWorkflowLogsFromURL(ctx, u, followRedirects)
+	resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects)
 	if err != nil {
 		return nil, nil, err
 	}
+	defer resp.Body.Close()
 
 	if resp.StatusCode != http.StatusFound {
 		return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status)
 	}
+
 	parsedURL, err := url.Parse(resp.Header.Get("Location"))
 	return parsedURL, newResponse(resp), err
 }
diff --git a/github/github-accessors.go b/github/github-accessors.go
index ce5fa26a..cad631db 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -60,6 +60,30 @@ func (a *ActionsPermissions) GetSelectedActionsURL() string {
 	return *a.SelectedActionsURL
 }
 
+// GetAllowedActions returns the AllowedActions field if it's non-nil, zero value otherwise.
+func (a *ActionsPermissionsRepository) GetAllowedActions() string {
+	if a == nil || a.AllowedActions == nil {
+		return ""
+	}
+	return *a.AllowedActions
+}
+
+// GetEnabled returns the Enabled field if it's non-nil, zero value otherwise.
+func (a *ActionsPermissionsRepository) GetEnabled() bool {
+	if a == nil || a.Enabled == nil {
+		return false
+	}
+	return *a.Enabled
+}
+
+// GetSelectedActionsURL returns the SelectedActionsURL field if it's non-nil, zero value otherwise.
+func (a *ActionsPermissionsRepository) GetSelectedActionsURL() string {
+	if a == nil || a.SelectedActionsURL == nil {
+		return ""
+	}
+	return *a.SelectedActionsURL
+}
+
 // GetURL returns the URL field if it's non-nil, zero value otherwise.
 func (a *AdminEnforcement) GetURL() string {
 	if a == nil || a.URL == nil {
diff --git a/github/github.go b/github/github.go
index 164b2d06..be46c310 100644
--- a/github/github.go
+++ b/github/github.go
@@ -1279,6 +1279,35 @@ func formatRateReset(d time.Duration) string {
 	return fmt.Sprintf("[rate reset in %v]", timeString)
 }
 
+// When using roundTripWithOptionalFollowRedirect, note that it
+// is the responsibility of the caller to close the response body.
+func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
+	req, err := c.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var resp *http.Response
+	// Use http.DefaultTransport if no custom Transport is configured
+	req = withContext(ctx, req)
+	if c.client.Transport == nil {
+		resp, err = http.DefaultTransport.RoundTrip(req)
+	} else {
+		resp, err = c.client.Transport.RoundTrip(req)
+	}
+	if err != nil {
+		return nil, err
+	}
+
+	// If redirect response is returned, follow it
+	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
+		resp.Body.Close()
+		u = resp.Header.Get("Location")
+		resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false)
+	}
+	return resp, err
+}
+
 // Bool is a helper routine that allocates a new bool value
 // to store v and returns a pointer to it.
 func Bool(v bool) *bool { return &v }
diff --git a/github/orgs_actions_allowed.go b/github/orgs_actions_allowed.go
index 9032d033..687ab2f6 100644
--- a/github/orgs_actions_allowed.go
+++ b/github/orgs_actions_allowed.go
@@ -10,9 +10,9 @@ import (
 	"fmt"
 )
 
-// ActionsAllowed represents selected actions that are allowed in an organization.
+// ActionsAllowed represents selected actions that are allowed.
 //
-// GitHub API docs: https://docs.github.com/en/rest/reference/actions#get-allowed-actions-for-an-organization
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#set-allowed-actions-and-workflows-for-an-organization--parameters
 type ActionsAllowed struct {
 	GithubOwnedAllowed *bool    `json:"github_owned_allowed,omitempty"`
 	VerifiedAllowed    *bool    `json:"verified_allowed,omitempty"`
diff --git a/github/repos.go b/github/repos.go
index b1ce0796..3e8b4217 100644
--- a/github/repos.go
+++ b/github/repos.go
@@ -1061,7 +1061,7 @@ func (s *RepositoriesService) ListBranches(ctx context.Context, owner string, re
 func (s *RepositoriesService) GetBranch(ctx context.Context, owner, repo, branch string, followRedirects bool) (*Branch, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/branches/%v", owner, repo, branch)
 
-	resp, err := s.getBranchFromURL(ctx, u, followRedirects)
+	resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -1076,33 +1076,6 @@ func (s *RepositoriesService) GetBranch(ctx context.Context, owner, repo, branch
 	return b, newResponse(resp), err
 }
 
-func (s *RepositoriesService) getBranchFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
-	req, err := s.client.NewRequest("GET", u, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var resp *http.Response
-	// Use http.DefaultTransport if no custom Transport is configured
-	req = withContext(ctx, req)
-	if s.client.client.Transport == nil {
-		resp, err = http.DefaultTransport.RoundTrip(req)
-	} else {
-		resp, err = s.client.client.Transport.RoundTrip(req)
-	}
-	if err != nil {
-		return nil, err
-	}
-
-	// If redirect response is returned, follow it
-	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
-		resp.Body.Close()
-		u = resp.Header.Get("Location")
-		resp, err = s.getBranchFromURL(ctx, u, false)
-	}
-	return resp, err
-}
-
 // renameBranchRequest represents a request to rename a branch.
 type renameBranchRequest struct {
 	NewName string `json:"new_name"`
diff --git a/github/repos_actions_allowed.go b/github/repos_actions_allowed.go
new file mode 100644
index 00000000..2770fccc
--- /dev/null
+++ b/github/repos_actions_allowed.go
@@ -0,0 +1,49 @@
+// Copyright 2022 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"
+)
+
+// GetActionsAllowed gets the actions that are allowed in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#get-allowed-actions-and-workflows-for-a-repository
+func (s *RepositoriesService) GetActionsAllowed(ctx context.Context, org, repo string) (*ActionsAllowed, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/permissions/selected-actions", org, repo)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	actionsAllowed := new(ActionsAllowed)
+	resp, err := s.client.Do(ctx, req, actionsAllowed)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return actionsAllowed, resp, nil
+}
+
+// EditActionsAllowed sets the actions that are allowed in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#set-allowed-actions-and-workflows-for-a-repository
+func (s *RepositoriesService) EditActionsAllowed(ctx context.Context, org, repo string, actionsAllowed ActionsAllowed) (*ActionsAllowed, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/permissions/selected-actions", org, repo)
+	req, err := s.client.NewRequest("PUT", u, actionsAllowed)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	p := new(ActionsAllowed)
+	resp, err := s.client.Do(ctx, req, p)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return p, resp, nil
+}
diff --git a/github/repos_actions_permissions.go b/github/repos_actions_permissions.go
new file mode 100644
index 00000000..bff8c2f5
--- /dev/null
+++ b/github/repos_actions_permissions.go
@@ -0,0 +1,62 @@
+// Copyright 2022 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"
+)
+
+// ActionsPermissionsRepository represents a policy for repositories and allowed actions in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#set-github-actions-permissions-for-a-repository--parameters
+type ActionsPermissionsRepository struct {
+	Enabled            *bool   `json:"enabled,omitempty"`
+	AllowedActions     *string `json:"allowed_actions,omitempty"`
+	SelectedActionsURL *string `json:"selected_actions_url,omitempty"`
+}
+
+func (a ActionsPermissionsRepository) String() string {
+	return Stringify(a)
+}
+
+// GetActionsPermissions gets the GitHub Actions permissions policy for repositories and allowed actions in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#get-github-actions-permissions-for-a-repository
+func (s *RepositoriesService) GetActionsPermissions(ctx context.Context, owner, repo string) (*ActionsPermissionsRepository, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/permissions", owner, repo)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	permissions := new(ActionsPermissionsRepository)
+	resp, err := s.client.Do(ctx, req, permissions)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return permissions, resp, nil
+}
+
+// EditActionsPermissions sets the permissions policy for repositories and allowed actions in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/actions#set-github-actions-permissions-for-a-repository
+func (s *RepositoriesService) EditActionsPermissions(ctx context.Context, owner, repo string, actionsPermissionsRepository ActionsPermissionsRepository) (*ActionsPermissionsRepository, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/permissions", owner, repo)
+	req, err := s.client.NewRequest("PUT", u, actionsPermissionsRepository)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	permissions := new(ActionsPermissionsRepository)
+	resp, err := s.client.Do(ctx, req, permissions)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return permissions, resp, nil
+}
diff --git a/github/repos_contents.go b/github/repos_contents.go
index 86e11c0a..3fe27101 100644
--- a/github/repos_contents.go
+++ b/github/repos_contents.go
@@ -284,40 +284,16 @@ func (s *RepositoriesService) GetArchiveLink(ctx context.Context, owner, repo st
 	if opts != nil && opts.Ref != "" {
 		u += fmt.Sprintf("/%s", opts.Ref)
 	}
-	resp, err := s.getArchiveLinkFromURL(ctx, u, followRedirects)
+	resp, err := s.client.roundTripWithOptionalFollowRedirect(ctx, u, followRedirects)
 	if err != nil {
 		return nil, nil, err
 	}
+	defer resp.Body.Close()
+
 	if resp.StatusCode != http.StatusFound {
 		return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status)
 	}
+
 	parsedURL, err := url.Parse(resp.Header.Get("Location"))
 	return parsedURL, newResponse(resp), err
 }
-
-func (s *RepositoriesService) getArchiveLinkFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
-	req, err := s.client.NewRequest("GET", u, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var resp *http.Response
-	// Use http.DefaultTransport if no custom Transport is configured
-	req = withContext(ctx, req)
-	if s.client.client.Transport == nil {
-		resp, err = http.DefaultTransport.RoundTrip(req)
-	} else {
-		resp, err = s.client.client.Transport.RoundTrip(req)
-	}
-	if err != nil {
-		return nil, err
-	}
-	resp.Body.Close()
-
-	// If redirect response is returned, follow it
-	if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
-		u = resp.Header.Get("Location")
-		resp, err = s.getArchiveLinkFromURL(ctx, u, false)
-	}
-	return resp, err
-}


Test patch:
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index f0ce54ec..a796fe05 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -73,6 +73,36 @@ func TestActionsPermissions_GetSelectedActionsURL(tt *testing.T) {
 	a.GetSelectedActionsURL()
 }
 
+func TestActionsPermissionsRepository_GetAllowedActions(tt *testing.T) {
+	var zeroValue string
+	a := &ActionsPermissionsRepository{AllowedActions: &zeroValue}
+	a.GetAllowedActions()
+	a = &ActionsPermissionsRepository{}
+	a.GetAllowedActions()
+	a = nil
+	a.GetAllowedActions()
+}
+
+func TestActionsPermissionsRepository_GetEnabled(tt *testing.T) {
+	var zeroValue bool
+	a := &ActionsPermissionsRepository{Enabled: &zeroValue}
+	a.GetEnabled()
+	a = &ActionsPermissionsRepository{}
+	a.GetEnabled()
+	a = nil
+	a.GetEnabled()
+}
+
+func TestActionsPermissionsRepository_GetSelectedActionsURL(tt *testing.T) {
+	var zeroValue string
+	a := &ActionsPermissionsRepository{SelectedActionsURL: &zeroValue}
+	a.GetSelectedActionsURL()
+	a = &ActionsPermissionsRepository{}
+	a.GetSelectedActionsURL()
+	a = nil
+	a.GetSelectedActionsURL()
+}
+
 func TestAdminEnforcement_GetURL(tt *testing.T) {
 	var zeroValue string
 	a := &AdminEnforcement{URL: &zeroValue}
diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go
index c5bbca9d..142baa21 100644
--- a/github/github-stringify_test.go
+++ b/github/github-stringify_test.go
@@ -36,6 +36,18 @@ func TestActionsPermissions_String(t *testing.T) {
 	}
 }
 
+func TestActionsPermissionsRepository_String(t *testing.T) {
+	v := ActionsPermissionsRepository{
+		Enabled:            Bool(false),
+		AllowedActions:     String(""),
+		SelectedActionsURL: String(""),
+	}
+	want := `github.ActionsPermissionsRepository{Enabled:false, AllowedActions:"", SelectedActionsURL:""}`
+	if got := v.String(); got != want {
+		t.Errorf("ActionsPermissionsRepository.String = %v, want %v", got, want)
+	}
+}
+
 func TestAdminStats_String(t *testing.T) {
 	v := AdminStats{
 		Issues:     &IssueStats{},
diff --git a/github/repos_actions_allowed_test.go b/github/repos_actions_allowed_test.go
new file mode 100644
index 00000000..6736f909
--- /dev/null
+++ b/github/repos_actions_allowed_test.go
@@ -0,0 +1,93 @@
+// Copyright 2022 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"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestRepositoryService_GetActionsAllowed(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/permissions/selected-actions", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"github_owned_allowed":true, "verified_allowed":false, "patterns_allowed":["a/b"]}`)
+	})
+
+	ctx := context.Background()
+	org, _, err := client.Repositories.GetActionsAllowed(ctx, "o", "r")
+	if err != nil {
+		t.Errorf("Repositories.GetActionsAllowed returned error: %v", err)
+	}
+	want := &ActionsAllowed{GithubOwnedAllowed: Bool(true), VerifiedAllowed: Bool(false), PatternsAllowed: []string{"a/b"}}
+	if !cmp.Equal(org, want) {
+		t.Errorf("Repositories.GetActionsAllowed returned %+v, want %+v", org, want)
+	}
+
+	const methodName = "GetActionsAllowed"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.GetActionsAllowed(ctx, "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.GetActionsAllowed(ctx, "o", "r")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_EditActionsAllowed(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+	input := &ActionsAllowed{GithubOwnedAllowed: Bool(true), VerifiedAllowed: Bool(false), PatternsAllowed: []string{"a/b"}}
+
+	mux.HandleFunc("/repos/o/r/actions/permissions/selected-actions", func(w http.ResponseWriter, r *http.Request) {
+		v := new(ActionsAllowed)
+		json.NewDecoder(r.Body).Decode(v)
+
+		testMethod(t, r, "PUT")
+		if !cmp.Equal(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+
+		fmt.Fprint(w, `{"github_owned_allowed":true, "verified_allowed":false, "patterns_allowed":["a/b"]}`)
+	})
+
+	ctx := context.Background()
+	org, _, err := client.Repositories.EditActionsAllowed(ctx, "o", "r", *input)
+	if err != nil {
+		t.Errorf("Repositories.EditActionsAllowed returned error: %v", err)
+	}
+
+	want := &ActionsAllowed{GithubOwnedAllowed: Bool(true), VerifiedAllowed: Bool(false), PatternsAllowed: []string{"a/b"}}
+	if !cmp.Equal(org, want) {
+		t.Errorf("Repositories.EditActionsAllowed returned %+v, want %+v", org, want)
+	}
+
+	const methodName = "EditActionsAllowed"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.EditActionsAllowed(ctx, "\n", "\n", *input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.EditActionsAllowed(ctx, "o", "r", *input)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
diff --git a/github/repos_actions_permissions_test.go b/github/repos_actions_permissions_test.go
new file mode 100644
index 00000000..1d6012f6
--- /dev/null
+++ b/github/repos_actions_permissions_test.go
@@ -0,0 +1,112 @@
+// Copyright 2022 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"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestRepositoriesService_GetActionsPermissions(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/permissions", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"enabled": true, "allowed_actions": "all"}`)
+	})
+
+	ctx := context.Background()
+	org, _, err := client.Repositories.GetActionsPermissions(ctx, "o", "r")
+	if err != nil {
+		t.Errorf("Repositories.GetActionsPermissions returned error: %v", err)
+	}
+	want := &ActionsPermissionsRepository{Enabled: Bool(true), AllowedActions: String("all")}
+	if !cmp.Equal(org, want) {
+		t.Errorf("Repositories.GetActionsPermissions returned %+v, want %+v", org, want)
+	}
+
+	const methodName = "GetActionsPermissions"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.GetActionsPermissions(ctx, "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.GetActionsPermissions(ctx, "o", "r")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_EditActionsPermissions(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	input := &ActionsPermissionsRepository{Enabled: Bool(true), AllowedActions: String("selected")}
+
+	mux.HandleFunc("/repos/o/r/actions/permissions", func(w http.ResponseWriter, r *http.Request) {
+		v := new(ActionsPermissionsRepository)
+		json.NewDecoder(r.Body).Decode(v)
+
+		testMethod(t, r, "PUT")
+		if !cmp.Equal(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+
+		fmt.Fprint(w, `{"enabled": true, "allowed_actions": "selected"}`)
+	})
+
+	ctx := context.Background()
+	org, _, err := client.Repositories.EditActionsPermissions(ctx, "o", "r", *input)
+	if err != nil {
+		t.Errorf("Repositories.EditActionsPermissions returned error: %v", err)
+	}
+
+	want := &ActionsPermissionsRepository{Enabled: Bool(true), AllowedActions: String("selected")}
+	if !cmp.Equal(org, want) {
+		t.Errorf("Repositories.EditActionsPermissions returned %+v, want %+v", org, want)
+	}
+
+	const methodName = "EditActionsPermissions"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.EditActionsPermissions(ctx, "\n", "\n", *input)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.EditActionsPermissions(ctx, "o", "r", *input)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestActionsPermissionsRepository_Marshal(t *testing.T) {
+	testJSONMarshal(t, &ActionsPermissions{}, "{}")
+
+	u := &ActionsPermissionsRepository{
+		Enabled:            Bool(true),
+		AllowedActions:     String("all"),
+		SelectedActionsURL: String("someURL"),
+	}
+
+	want := `{
+		"enabled": true,
+		"allowed_actions": "all",
+		"selected_actions_url": "someURL"
+	}`
+
+	testJSONMarshal(t, u, want)
+}
diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go
index befb2fad..e0465e93 100644
--- a/github/repos_contents_test.go
+++ b/github/repos_contents_test.go
@@ -670,12 +670,12 @@ func TestRepositoriesService_DeleteFile(t *testing.T) {
 func TestRepositoriesService_GetArchiveLink(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()
-	mux.HandleFunc("/repos/o/r/tarball", func(w http.ResponseWriter, r *http.Request) {
+	mux.HandleFunc("/repos/o/r/tarball/yo", func(w http.ResponseWriter, r *http.Request) {
 		testMethod(t, r, "GET")
 		http.Redirect(w, r, "http://github.com/a", http.StatusFound)
 	})
 	ctx := context.Background()
-	url, resp, err := client.Repositories.GetArchiveLink(ctx, "o", "r", Tarball, &RepositoryContentGetOptions{}, true)
+	url, resp, err := client.Repositories.GetArchiveLink(ctx, "o", "r", Tarball, &RepositoryContentGetOptions{Ref: "yo"}, true)
 	if err != nil {
 		t.Errorf("Repositories.GetArchiveLink returned error: %v", err)
 	}