Repository: cli/cli
Pull request #7795 (2023-08-04 00:50:22)

Codespaces: Use the host name from the logged in server for commands
<!--
  Thank you for contributing to GitHub CLI!
  To reference an open issue, please write this in your description: `Fixes #NUMBER`
-->
Fixes https://github.com/github/codespaces/issues/14711

* Refactor the Codespaces root command configuration to follow the same pattern as other commands
    * Expose a `NewCmdCodespace` function to build the command and invoke it from `pkg/cmd/root/root.go`
    * Remove the lazy HTTP client initialization wrapper that existed in `pkg/cmd/root/root.go`. Push the HTTP client downstream into command runtime in `internal/codespaces/api/api.go` (which keeps it lazy, but is simpler and follows the pattern of the other commands)
* Use the logged in auth context to provide the API base url. Previously it would respect the `GITHUB_API_URL` environment variable if set, otherwise fallback to a hardcoded `https://api.github.com`. Now it uses the following precedence:
    * `GITHUB_API_URL` environment variable
    * Logged in auth context
    * Fallback to hardcoded `https://api.github.com`
 * Similarly for the server url, use the following precedence:
    * `GITHUB_SERVER_URL` environment variable
    * Logged in auth context
    * Fallback to hardcoded `https://github.com`
 * Remove dead code in the codespaces api package relating to the `vscsAPI` property

cliAutomation: Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message.

Patch:
diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go
index d66aff065..c55b92400 100644
--- a/internal/codespaces/api/api.go
+++ b/internal/codespaces/api/api.go
@@ -1,13 +1,12 @@
 package api
 
 // For descriptions of service interfaces, see:
-// - https://online.visualstudio.com/api/swagger (for visualstudio.com)
 // - https://docs.github.com/en/rest/reference/repos (for api.github.com)
 // - https://github.com/github/github/blob/master/app/api/codespaces.rb (for vscs_internal)
 // TODO(adonovan): replace the last link with a public doc URL when available.
 
 // TODO(adonovan): a possible reorganization would be to split this
-// file into three internal packages, one per backend service, and to
+// file into two internal packages, one per backend service, and to
 // rename api.API to github.Client:
 //
 // - github.GetUser(github.Client)
@@ -20,7 +19,6 @@ package api
 // - codespaces.GetToken(Client, login, name)
 // - codespaces.List(Client, user)
 // - codespaces.Start(Client, token, codespace)
-// - visualstudio.GetRegionLocation(http.Client) // no dependency on github
 //
 // This would make the meaning of each operation clearer.
 
@@ -34,6 +32,7 @@ import (
 	"io"
 	"net/http"
 	"net/url"
+	"os"
 	"reflect"
 	"regexp"
 	"strconv"
@@ -42,13 +41,14 @@ import (
 
 	"github.com/cenkalti/backoff/v4"
 	"github.com/cli/cli/v2/api"
+	"github.com/cli/cli/v2/internal/ghinstance"
+	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/opentracing/opentracing-go"
 )
 
 const (
-	githubServer = "https://github.com"
-	githubAPI    = "https://api.github.com"
-	vscsAPI      = "https://online.visualstudio.com"
+	defaultAPIURL    = "https://api.github.com"
+	defaultServerURL = "https://github.com"
 )
 
 const (
@@ -60,31 +60,40 @@ const (
 
 // API is the interface to the codespace service.
 type API struct {
-	client       httpClient
-	vscsAPI      string
+	client       func() (*http.Client, error)
 	githubAPI    string
 	githubServer string
 	retryBackoff time.Duration
 }
 
-type httpClient interface {
-	Do(req *http.Request) (*http.Response, error)
-}
-
 // New creates a new API client connecting to the configured endpoints with the HTTP client.
-func New(serverURL, apiURL, vscsURL string, httpClient httpClient) *API {
-	if serverURL == "" {
-		serverURL = githubServer
-	}
+func New(f *cmdutil.Factory) *API {
+	apiURL := os.Getenv("GITHUB_API_URL")
 	if apiURL == "" {
-		apiURL = githubAPI
+		cfg, err := f.Config()
+		if err != nil {
+			// fallback to the default api endpoint
+			apiURL = defaultAPIURL
+		} else {
+			host, _ := cfg.Authentication().DefaultHost()
+			apiURL = ghinstance.RESTPrefix(host)
+		}
 	}
-	if vscsURL == "" {
-		vscsURL = vscsAPI
+
+	serverURL := os.Getenv("GITHUB_SERVER_URL")
+	if serverURL == "" {
+		cfg, err := f.Config()
+		if err != nil {
+			// fallback to the default server endpoint
+			serverURL = defaultServerURL
+		} else {
+			host, _ := cfg.Authentication().DefaultHost()
+			serverURL = ghinstance.HostPrefix(host)
+		}
 	}
+
 	return &API{
-		client:       httpClient,
-		vscsAPI:      strings.TrimSuffix(vscsURL, "/"),
+		client:       f.HttpClient,
 		githubAPI:    strings.TrimSuffix(apiURL, "/"),
 		githubServer: strings.TrimSuffix(serverURL, "/"),
 		retryBackoff: 100 * time.Millisecond,
@@ -97,6 +106,11 @@ type User struct {
 	Type  string `json:"type"`
 }
 
+// ServerURL returns the server url (not the API url), such as https://github.com
+func (a *API) ServerURL() string {
+	return a.githubServer
+}
+
 // GetUser returns the user associated with the given token.
 func (a *API) GetUser(ctx context.Context) (*User, error) {
 	req, err := http.NewRequest(http.MethodGet, a.githubAPI+"/user", nil)
@@ -1119,7 +1133,13 @@ func (a *API) do(ctx context.Context, req *http.Request, spanName string) (*http
 	span, ctx := opentracing.StartSpanFromContext(ctx, spanName)
 	defer span.Finish()
 	req = req.WithContext(ctx)
-	return a.client.Do(req)
+
+	httpClient, err := a.client()
+	if err != nil {
+		return nil, err
+	}
+
+	return httpClient.Do(req)
 }
 
 // setHeaders sets the required headers for the API.
diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go
index f6f6fb937..006669151 100644
--- a/pkg/cmd/codespace/common.go
+++ b/pkg/cmd/codespace/common.go
@@ -88,6 +88,7 @@ func startLiveShareSession(ctx context.Context, codespace *api.Codespace, a *App
 
 //go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
 type apiClient interface {
+	ServerURL() string
 	GetUser(ctx context.Context) (*api.User, error)
 	GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
 	GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error)
diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go
index 6f973d3cb..889f0b33b 100644
--- a/pkg/cmd/codespace/create.go
+++ b/pkg/cmd/codespace/create.go
@@ -129,7 +129,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 	}
 
 	if opts.useWeb && userInputs.Repository == "" {
-		return a.browser.Browse("https://github.com/codespaces/new")
+		return a.browser.Browse(fmt.Sprintf("%s/codespaces/new", a.apiClient.ServerURL()))
 	}
 
 	promptForRepoAndBranch := userInputs.Repository == "" && !opts.useWeb
@@ -293,7 +293,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 	}
 
 	if opts.useWeb {
-		return a.browser.Browse(fmt.Sprintf("https://github.com/codespaces/new?repo=%d&ref=%s&machine=%s&location=%s", createParams.RepositoryID, createParams.Branch, createParams.Machine, createParams.Location))
+		return a.browser.Browse(fmt.Sprintf("%s/codespaces/new?repo=%d&ref=%s&machine=%s&location=%s", a.apiClient.ServerURL(), createParams.RepositoryID, createParams.Branch, createParams.Machine, createParams.Location))
 	}
 
 	var codespace *api.Codespace
diff --git a/pkg/cmd/codespace/list.go b/pkg/cmd/codespace/list.go
index bba34ff45..4775e3db5 100644
--- a/pkg/cmd/codespace/list.go
+++ b/pkg/cmd/codespace/list.go
@@ -79,7 +79,7 @@ func newListCmd(app *App) *cobra.Command {
 
 func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Exporter) error {
 	if opts.useWeb && opts.repo == "" {
-		return a.browser.Browse("https://github.com/codespaces")
+		return a.browser.Browse(fmt.Sprintf("%s/codespaces", a.apiClient.ServerURL()))
 	}
 
 	var codespaces []*api.Codespace
@@ -113,7 +113,7 @@ func (a *App) List(ctx context.Context, opts *listOptions, exporter cmdutil.Expo
 	}
 
 	if opts.useWeb && codespaces[0].Repository.ID > 0 {
-		return a.browser.Browse(fmt.Sprintf("https://github.com/codespaces?repository_id=%d", codespaces[0].Repository.ID))
+		return a.browser.Browse(fmt.Sprintf("%s/codespaces?repository_id=%d", a.apiClient.ServerURL(), codespaces[0].Repository.ID))
 	}
 
 	//nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter
diff --git a/pkg/cmd/codespace/mock_api.go b/pkg/cmd/codespace/mock_api.go
index 0796679fc..7e211fd7c 100644
--- a/pkg/cmd/codespace/mock_api.go
+++ b/pkg/cmd/codespace/mock_api.go
@@ -7,7 +7,7 @@ import (
 	"context"
 	"sync"
 
-	"github.com/cli/cli/v2/internal/codespaces/api"
+	codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
 )
 
 // apiClientMock is a mock implementation of apiClient.
@@ -16,43 +16,46 @@ import (
 //
 //		// make and configure a mocked apiClient
 //		mockedapiClient := &apiClientMock{
-//			CreateCodespaceFunc: func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
+//			CreateCodespaceFunc: func(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error) {
 //				panic("mock out the CreateCodespace method")
 //			},
 //			DeleteCodespaceFunc: func(ctx context.Context, name string, orgName string, userName string) error {
 //				panic("mock out the DeleteCodespace method")
 //			},
-//			EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
+//			EditCodespaceFunc: func(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error) {
 //				panic("mock out the EditCodespace method")
 //			},
-//			GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
+//			GetCodespaceFunc: func(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error) {
 //				panic("mock out the GetCodespace method")
 //			},
-//			GetCodespaceBillableOwnerFunc: func(ctx context.Context, nwo string) (*api.User, error) {
+//			GetCodespaceBillableOwnerFunc: func(ctx context.Context, nwo string) (*codespacesAPI.User, error) {
 //				panic("mock out the GetCodespaceBillableOwner method")
 //			},
-//			GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
+//			GetCodespaceRepoSuggestionsFunc: func(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error) {
 //				panic("mock out the GetCodespaceRepoSuggestions method")
 //			},
-//			GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
+//			GetCodespaceRepositoryContentsFunc: func(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error) {
 //				panic("mock out the GetCodespaceRepositoryContents method")
 //			},
-//			GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error) {
+//			GetCodespacesMachinesFunc: func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error) {
 //				panic("mock out the GetCodespacesMachines method")
 //			},
-//			GetOrgMemberCodespaceFunc: func(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error) {
+//			GetOrgMemberCodespaceFunc: func(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error) {
 //				panic("mock out the GetOrgMemberCodespace method")
 //			},
-//			GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
+//			GetRepositoryFunc: func(ctx context.Context, nwo string) (*codespacesAPI.Repository, error) {
 //				panic("mock out the GetRepository method")
 //			},
-//			GetUserFunc: func(ctx context.Context) (*api.User, error) {
+//			ServerURLFunc: func() string {
+//				panic("mock out the ServerURL method")
+//			},
+//			GetUserFunc: func(ctx context.Context) (*codespacesAPI.User, error) {
 //				panic("mock out the GetUser method")
 //			},
-//			ListCodespacesFunc: func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
+//			ListCodespacesFunc: func(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error) {
 //				panic("mock out the ListCodespaces method")
 //			},
-//			ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
+//			ListDevContainersFunc: func(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error) {
 //				panic("mock out the ListDevContainers method")
 //			},
 //			StartCodespaceFunc: func(ctx context.Context, name string) error {
@@ -69,43 +72,46 @@ import (
 //	}
 type apiClientMock struct {
 	// CreateCodespaceFunc mocks the CreateCodespace method.
-	CreateCodespaceFunc func(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error)
+	CreateCodespaceFunc func(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error)
 
 	// DeleteCodespaceFunc mocks the DeleteCodespace method.
 	DeleteCodespaceFunc func(ctx context.Context, name string, orgName string, userName string) error
 
 	// EditCodespaceFunc mocks the EditCodespace method.
-	EditCodespaceFunc func(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
+	EditCodespaceFunc func(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error)
 
 	// GetCodespaceFunc mocks the GetCodespace method.
-	GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error)
+	GetCodespaceFunc func(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error)
 
 	// GetCodespaceBillableOwnerFunc mocks the GetCodespaceBillableOwner method.
-	GetCodespaceBillableOwnerFunc func(ctx context.Context, nwo string) (*api.User, error)
+	GetCodespaceBillableOwnerFunc func(ctx context.Context, nwo string) (*codespacesAPI.User, error)
 
 	// GetCodespaceRepoSuggestionsFunc mocks the GetCodespaceRepoSuggestions method.
-	GetCodespaceRepoSuggestionsFunc func(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error)
+	GetCodespaceRepoSuggestionsFunc func(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error)
 
 	// GetCodespaceRepositoryContentsFunc mocks the GetCodespaceRepositoryContents method.
-	GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
+	GetCodespaceRepositoryContentsFunc func(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error)
 
 	// GetCodespacesMachinesFunc mocks the GetCodespacesMachines method.
-	GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error)
+	GetCodespacesMachinesFunc func(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error)
 
 	// GetOrgMemberCodespaceFunc mocks the GetOrgMemberCodespace method.
-	GetOrgMemberCodespaceFunc func(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error)
+	GetOrgMemberCodespaceFunc func(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error)
 
 	// GetRepositoryFunc mocks the GetRepository method.
-	GetRepositoryFunc func(ctx context.Context, nwo string) (*api.Repository, error)
+	GetRepositoryFunc func(ctx context.Context, nwo string) (*codespacesAPI.Repository, error)
+
+	// ServerURLFunc mocks the ServerURL method.
+	ServerURLFunc func() string
 
 	// GetUserFunc mocks the GetUser method.
-	GetUserFunc func(ctx context.Context) (*api.User, error)
+	GetUserFunc func(ctx context.Context) (*codespacesAPI.User, error)
 
 	// ListCodespacesFunc mocks the ListCodespaces method.
-	ListCodespacesFunc func(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error)
+	ListCodespacesFunc func(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error)
 
 	// ListDevContainersFunc mocks the ListDevContainers method.
-	ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error)
+	ListDevContainersFunc func(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error)
 
 	// StartCodespaceFunc mocks the StartCodespace method.
 	StartCodespaceFunc func(ctx context.Context, name string) error
@@ -120,7 +126,7 @@ type apiClientMock struct {
 			// Ctx is the ctx argument value.
 			Ctx context.Context
 			// Params is the params argument value.
-			Params *api.CreateCodespaceParams
+			Params *codespacesAPI.CreateCodespaceParams
 		}
 		// DeleteCodespace holds details about calls to the DeleteCodespace method.
 		DeleteCodespace []struct {
@@ -140,7 +146,7 @@ type apiClientMock struct {
 			// CodespaceName is the codespaceName argument value.
 			CodespaceName string
 			// Params is the params argument value.
-			Params *api.EditCodespaceParams
+			Params *codespacesAPI.EditCodespaceParams
 		}
 		// GetCodespace holds details about calls to the GetCodespace method.
 		GetCodespace []struct {
@@ -165,14 +171,14 @@ type apiClientMock struct {
 			// PartialSearch is the partialSearch argument value.
 			PartialSearch string
 			// Params is the params argument value.
-			Params api.RepoSearchParameters
+			Params codespacesAPI.RepoSearchParameters
 		}
 		// GetCodespaceRepositoryContents holds details about calls to the GetCodespaceRepositoryContents method.
 		GetCodespaceRepositoryContents []struct {
 			// Ctx is the ctx argument value.
 			Ctx context.Context
 			// Codespace is the codespace argument value.
-			Codespace *api.Codespace
+			Codespace *codespacesAPI.Codespace
 			// Path is the path argument value.
 			Path string
 		}
@@ -207,6 +213,9 @@ type apiClientMock struct {
 			// Nwo is the nwo argument value.
 			Nwo string
 		}
+		// ServerURL holds details about calls to the ServerURL method.
+		ServerURL []struct {
+		}
 		// GetUser holds details about calls to the GetUser method.
 		GetUser []struct {
 			// Ctx is the ctx argument value.
@@ -217,7 +226,7 @@ type apiClientMock struct {
 			// Ctx is the ctx argument value.
 			Ctx context.Context
 			// Opts is the opts argument value.
-			Opts api.ListCodespacesOptions
+			Opts codespacesAPI.ListCodespacesOptions
 		}
 		// ListDevContainers holds details about calls to the ListDevContainers method.
 		ListDevContainers []struct {
@@ -259,6 +268,7 @@ type apiClientMock struct {
 	lockGetCodespacesMachines          sync.RWMutex
 	lockGetOrgMemberCodespace          sync.RWMutex
 	lockGetRepository                  sync.RWMutex
+	lockServerURL                      sync.RWMutex
 	lockGetUser                        sync.RWMutex
 	lockListCodespaces                 sync.RWMutex
 	lockListDevContainers              sync.RWMutex
@@ -267,13 +277,13 @@ type apiClientMock struct {
 }
 
 // CreateCodespace calls CreateCodespaceFunc.
-func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.CreateCodespaceParams) (*api.Codespace, error) {
+func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *codespacesAPI.CreateCodespaceParams) (*codespacesAPI.Codespace, error) {
 	if mock.CreateCodespaceFunc == nil {
 		panic("apiClientMock.CreateCodespaceFunc: method is nil but apiClient.CreateCodespace was just called")
 	}
 	callInfo := struct {
 		Ctx    context.Context
-		Params *api.CreateCodespaceParams
+		Params *codespacesAPI.CreateCodespaceParams
 	}{
 		Ctx:    ctx,
 		Params: params,
@@ -290,11 +300,11 @@ func (mock *apiClientMock) CreateCodespace(ctx context.Context, params *api.Crea
 //	len(mockedapiClient.CreateCodespaceCalls())
 func (mock *apiClientMock) CreateCodespaceCalls() []struct {
 	Ctx    context.Context
-	Params *api.CreateCodespaceParams
+	Params *codespacesAPI.CreateCodespaceParams
 } {
 	var calls []struct {
 		Ctx    context.Context
-		Params *api.CreateCodespaceParams
+		Params *codespacesAPI.CreateCodespaceParams
 	}
 	mock.lockCreateCodespace.RLock()
 	calls = mock.calls.CreateCodespace
@@ -347,14 +357,14 @@ func (mock *apiClientMock) DeleteCodespaceCalls() []struct {
 }
 
 // EditCodespace calls EditCodespaceFunc.
-func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error) {
+func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName string, params *codespacesAPI.EditCodespaceParams) (*codespacesAPI.Codespace, error) {
 	if mock.EditCodespaceFunc == nil {
 		panic("apiClientMock.EditCodespaceFunc: method is nil but apiClient.EditCodespace was just called")
 	}
 	callInfo := struct {
 		Ctx           context.Context
 		CodespaceName string
-		Params        *api.EditCodespaceParams
+		Params        *codespacesAPI.EditCodespaceParams
 	}{
 		Ctx:           ctx,
 		CodespaceName: codespaceName,
@@ -373,12 +383,12 @@ func (mock *apiClientMock) EditCodespace(ctx context.Context, codespaceName stri
 func (mock *apiClientMock) EditCodespaceCalls() []struct {
 	Ctx           context.Context
 	CodespaceName string
-	Params        *api.EditCodespaceParams
+	Params        *codespacesAPI.EditCodespaceParams
 } {
 	var calls []struct {
 		Ctx           context.Context
 		CodespaceName string
-		Params        *api.EditCodespaceParams
+		Params        *codespacesAPI.EditCodespaceParams
 	}
 	mock.lockEditCodespace.RLock()
 	calls = mock.calls.EditCodespace
@@ -387,7 +397,7 @@ func (mock *apiClientMock) EditCodespaceCalls() []struct {
 }
 
 // GetCodespace calls GetCodespaceFunc.
-func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*api.Codespace, error) {
+func (mock *apiClientMock) GetCodespace(ctx context.Context, name string, includeConnection bool) (*codespacesAPI.Codespace, error) {
 	if mock.GetCodespaceFunc == nil {
 		panic("apiClientMock.GetCodespaceFunc: method is nil but apiClient.GetCodespace was just called")
 	}
@@ -427,7 +437,7 @@ func (mock *apiClientMock) GetCodespaceCalls() []struct {
 }
 
 // GetCodespaceBillableOwner calls GetCodespaceBillableOwnerFunc.
-func (mock *apiClientMock) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*api.User, error) {
+func (mock *apiClientMock) GetCodespaceBillableOwner(ctx context.Context, nwo string) (*codespacesAPI.User, error) {
 	if mock.GetCodespaceBillableOwnerFunc == nil {
 		panic("apiClientMock.GetCodespaceBillableOwnerFunc: method is nil but apiClient.GetCodespaceBillableOwner was just called")
 	}
@@ -463,14 +473,14 @@ func (mock *apiClientMock) GetCodespaceBillableOwnerCalls() []struct {
 }
 
 // GetCodespaceRepoSuggestions calls GetCodespaceRepoSuggestionsFunc.
-func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error) {
+func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params codespacesAPI.RepoSearchParameters) ([]string, error) {
 	if mock.GetCodespaceRepoSuggestionsFunc == nil {
 		panic("apiClientMock.GetCodespaceRepoSuggestionsFunc: method is nil but apiClient.GetCodespaceRepoSuggestions was just called")
 	}
 	callInfo := struct {
 		Ctx           context.Context
 		PartialSearch string
-		Params        api.RepoSearchParameters
+		Params        codespacesAPI.RepoSearchParameters
 	}{
 		Ctx:           ctx,
 		PartialSearch: partialSearch,
@@ -489,12 +499,12 @@ func (mock *apiClientMock) GetCodespaceRepoSuggestions(ctx context.Context, part
 func (mock *apiClientMock) GetCodespaceRepoSuggestionsCalls() []struct {
 	Ctx           context.Context
 	PartialSearch string
-	Params        api.RepoSearchParameters
+	Params        codespacesAPI.RepoSearchParameters
 } {
 	var calls []struct {
 		Ctx           context.Context
 		PartialSearch string
-		Params        api.RepoSearchParameters
+		Params        codespacesAPI.RepoSearchParameters
 	}
 	mock.lockGetCodespaceRepoSuggestions.RLock()
 	calls = mock.calls.GetCodespaceRepoSuggestions
@@ -503,13 +513,13 @@ func (mock *apiClientMock) GetCodespaceRepoSuggestionsCalls() []struct {
 }
 
 // GetCodespaceRepositoryContents calls GetCodespaceRepositoryContentsFunc.
-func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error) {
+func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, codespace *codespacesAPI.Codespace, path string) ([]byte, error) {
 	if mock.GetCodespaceRepositoryContentsFunc == nil {
 		panic("apiClientMock.GetCodespaceRepositoryContentsFunc: method is nil but apiClient.GetCodespaceRepositoryContents was just called")
 	}
 	callInfo := struct {
 		Ctx       context.Context
-		Codespace *api.Codespace
+		Codespace *codespacesAPI.Codespace
 		Path      string
 	}{
 		Ctx:       ctx,
@@ -528,12 +538,12 @@ func (mock *apiClientMock) GetCodespaceRepositoryContents(ctx context.Context, c
 //	len(mockedapiClient.GetCodespaceRepositoryContentsCalls())
 func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
 	Ctx       context.Context
-	Codespace *api.Codespace
+	Codespace *codespacesAPI.Codespace
 	Path      string
 } {
 	var calls []struct {
 		Ctx       context.Context
-		Codespace *api.Codespace
+		Codespace *codespacesAPI.Codespace
 		Path      string
 	}
 	mock.lockGetCodespaceRepositoryContents.RLock()
@@ -543,7 +553,7 @@ func (mock *apiClientMock) GetCodespaceRepositoryContentsCalls() []struct {
 }
 
 // GetCodespacesMachines calls GetCodespacesMachinesFunc.
-func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*api.Machine, error) {
+func (mock *apiClientMock) GetCodespacesMachines(ctx context.Context, repoID int, branch string, location string, devcontainerPath string) ([]*codespacesAPI.Machine, error) {
 	if mock.GetCodespacesMachinesFunc == nil {
 		panic("apiClientMock.GetCodespacesMachinesFunc: method is nil but apiClient.GetCodespacesMachines was just called")
 	}
@@ -591,7 +601,7 @@ func (mock *apiClientMock) GetCodespacesMachinesCalls() []struct {
 }
 
 // GetOrgMemberCodespace calls GetOrgMemberCodespaceFunc.
-func (mock *apiClientMock) GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*api.Codespace, error) {
+func (mock *apiClientMock) GetOrgMemberCodespace(ctx context.Context, orgName string, userName string, codespaceName string) (*codespacesAPI.Codespace, error) {
 	if mock.GetOrgMemberCodespaceFunc == nil {
 		panic("apiClientMock.GetOrgMemberCodespaceFunc: method is nil but apiClient.GetOrgMemberCodespace was just called")
 	}
@@ -635,7 +645,7 @@ func (mock *apiClientMock) GetOrgMemberCodespaceCalls() []struct {
 }
 
 // GetRepository calls GetRepositoryFunc.
-func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*api.Repository, error) {
+func (mock *apiClientMock) GetRepository(ctx context.Context, nwo string) (*codespacesAPI.Repository, error) {
 	if mock.GetRepositoryFunc == nil {
 		panic("apiClientMock.GetRepositoryFunc: method is nil but apiClient.GetRepository was just called")
 	}
@@ -670,8 +680,35 @@ func (mock *apiClientMock) GetRepositoryCalls() []struct {
 	return calls
 }
 
+// ServerURL calls ServerURLFunc.
+func (mock *apiClientMock) ServerURL() string {
+	if mock.ServerURLFunc == nil {
+		panic("apiClientMock.ServerURLFunc: method is nil but apiClient.ServerURL was just called")
+	}
+	callInfo := struct {
+	}{}
+	mock.lockServerURL.Lock()
+	mock.calls.ServerURL = append(mock.calls.ServerURL, callInfo)
+	mock.lockServerURL.Unlock()
+	return mock.ServerURLFunc()
+}
+
+// ServerURLCalls gets all the calls that were made to ServerURL.
+// Check the length with:
+//
+//	len(mockedapiClient.ServerURLCalls())
+func (mock *apiClientMock) ServerURLCalls() []struct {
+} {
+	var calls []struct {
+	}
+	mock.lockServerURL.RLock()
+	calls = mock.calls.ServerURL
+	mock.lockServerURL.RUnlock()
+	return calls
+}
+
 // GetUser calls GetUserFunc.
-func (mock *apiClientMock) GetUser(ctx context.Context) (*api.User, error) {
+func (mock *apiClientMock) GetUser(ctx context.Context) (*codespacesAPI.User, error) {
 	if mock.GetUserFunc == nil {
 		panic("apiClientMock.GetUserFunc: method is nil but apiClient.GetUser was just called")
 	}
@@ -703,13 +740,13 @@ func (mock *apiClientMock) GetUserCalls() []struct {
 }
 
 // ListCodespaces calls ListCodespacesFunc.
-func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts api.ListCodespacesOptions) ([]*api.Codespace, error) {
+func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts codespacesAPI.ListCodespacesOptions) ([]*codespacesAPI.Codespace, error) {
 	if mock.ListCodespacesFunc == nil {
 		panic("apiClientMock.ListCodespacesFunc: method is nil but apiClient.ListCodespaces was just called")
 	}
 	callInfo := struct {
 		Ctx  context.Context
-		Opts api.ListCodespacesOptions
+		Opts codespacesAPI.ListCodespacesOptions
 	}{
 		Ctx:  ctx,
 		Opts: opts,
@@ -726,11 +763,11 @@ func (mock *apiClientMock) ListCodespaces(ctx context.Context, opts api.ListCode
 //	len(mockedapiClient.ListCodespacesCalls())
 func (mock *apiClientMock) ListCodespacesCalls() []struct {
 	Ctx  context.Context
-	Opts api.ListCodespacesOptions
+	Opts codespacesAPI.ListCodespacesOptions
 } {
 	var calls []struct {
 		Ctx  context.Context
-		Opts api.ListCodespacesOptions
+		Opts codespacesAPI.ListCodespacesOptions
 	}
 	mock.lockListCodespaces.RLock()
 	calls = mock.calls.ListCodespaces
@@ -739,7 +776,7 @@ func (mock *apiClientMock) ListCodespacesCalls() []struct {
 }
 
 // ListDevContainers calls ListDevContainersFunc.
-func (mock *apiClientMock) ListDevContainers(ctx context.Context, repoID int, branch string, limit int) ([]api.DevContainerEntry, error) {
+func (mock *apiClientMock) ListDevContainers(ctx context.Context, repoID int, branch string, limit int) ([]codespacesAPI.DevContainerEntry, error) {
 	if mock.ListDevContainersFunc == nil {
 		panic("apiClientMock.ListDevContainersFunc: method is nil but apiClient.ListDevContainers was just called")
 	}
diff --git a/pkg/cmd/codespace/root.go b/pkg/cmd/codespace/root.go
index 28a80edae..d1675a8f7 100644
--- a/pkg/cmd/codespace/root.go
+++ b/pkg/cmd/codespace/root.go
@@ -1,15 +1,28 @@
 package codespace
 
 import (
+	codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
+
+	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/spf13/cobra"
 )
 
-func NewRootCmd(app *App) *cobra.Command {
+func NewCmdCodespace(f *cmdutil.Factory) *cobra.Command {
 	root := &cobra.Command{
-		Use:   "codespace",
-		Short: "Connect to and manage codespaces",
+		Use:     "codespace",
+		Short:   "Connect to and manage codespaces",
+		Aliases: []string{"cs"},
+		GroupID: "core",
 	}
 
+	app := NewApp(
+		f.IOStreams,
+		f,
+		codespacesAPI.New(f),
+		f.Browser,
+		f.Remotes,
+	)
+
 	root.AddCommand(newCodeCmd(app))
 	root.AddCommand(newCreateCmd(app))
 	root.AddCommand(newEditCmd(app))
diff --git a/pkg/cmd/release/download/download.go b/pkg/cmd/release/download/download.go
index 16e95db29..bab6bd19b 100644
--- a/pkg/cmd/release/download/download.go
+++ b/pkg/cmd/release/download/download.go
@@ -290,7 +290,7 @@ func downloadAsset(dest *destinationWriter, httpClient *http.Client, assetURL, f
 			return fmt.Errorf("unable to parse file name of archive: %w", err)
 		}
 		if serverFileName, ok := params["filename"]; ok {
-			fileName = filepath.Clean(serverFileName)
+			fileName = filepath.Base(serverFileName)
 		} else {
 			return errors.New("unable to determine file name of archive")
 		}
diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go
index fd6378216..8c8420bee 100644
--- a/pkg/cmd/root/root.go
+++ b/pkg/cmd/root/root.go
@@ -5,11 +5,9 @@ import (
 	"net/http"
 	"os"
 	"strings"
-	"sync"
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
-	codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
 	actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
 	aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
 	"github.com/cli/cli/v2/pkg/cmd/alias/shared"
@@ -134,7 +132,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command,
 	cmd.AddCommand(variableCmd.NewCmdVariable(f))
 	cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
 	cmd.AddCommand(statusCmd.NewCmdStatus(f, nil))
-	cmd.AddCommand(newCodespaceCmd(f))
+	cmd.AddCommand(codespaceCmd.NewCmdCodespace(f))
 	cmd.AddCommand(projectCmd.NewCmdProject(f))
 
 	// below here at the commands that require the "intelligent" BaseRepo resolver
@@ -241,51 +239,3 @@ func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, er
 		return api.NewHTTPClient(opts)
 	}
 }
-
-func newCodespaceCmd(f *cmdutil.Factory) *cobra.Command {
-	serverURL := os.Getenv("GITHUB_SERVER_URL")
-	apiURL := os.Getenv("GITHUB_API_URL")
-	vscsURL := os.Getenv("INTERNAL_VSCS_TARGET_URL")
-	app := codespaceCmd.NewApp(
-		f.IOStreams,
-		f,
-		codespacesAPI.New(
-			serverURL,
-			apiURL,
-			vscsURL,
-			&lazyLoadedHTTPClient{factory: f},
-		),
-		f.Browser,
-		f.Remotes,
-	)
-	cmd := codespaceCmd.NewRootCmd(app)
-	cmd.Use = "codespace"
-	cmd.Aliases = []string{"cs"}
-	cmd.GroupID = "core"
-	return cmd
-}
-
-type lazyLoadedHTTPClient struct {
-	factory *cmdutil.Factory
-
-	httpClientMu sync.RWMutex // guards httpClient
-	httpClient   *http.Client
-}
-
-func (l *lazyLoadedHTTPClient) Do(req *http.Request) (*http.Response, error) {
-	l.httpClientMu.RLock()
-	httpClient := l.httpClient
-	l.httpClientMu.RUnlock()
-
-	if httpClient == nil {
-		var err error
-		l.httpClientMu.Lock()
-		l.httpClient, err = l.factory.HttpClient()
-		l.httpClientMu.Unlock()
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return l.httpClient.Do(req)
-}


Test patch:
diff --git a/internal/codespaces/api/api_test.go b/internal/codespaces/api/api_test.go
index 5e9316fc1..75f82c39e 100644
--- a/internal/codespaces/api/api_test.go
+++ b/internal/codespaces/api/api_test.go
@@ -3,12 +3,16 @@ package api
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
 	"strconv"
 	"testing"
+
+	"github.com/cli/cli/v2/internal/config"
+	"github.com/cli/cli/v2/pkg/cmdutil"
 )
 
 func generateCodespaceList(start int, end int) []*Codespace {
@@ -126,13 +130,181 @@ func createFakeCreateEndpointServer(t *testing.T, wantStatus int) *httptest.Serv
 	}))
 }
 
+func createHttpClient() (*http.Client, error) {
+	return &http.Client{}, nil
+}
+
+func TestNew_APIURL_dotcomConfig(t *testing.T) {
+	t.Setenv("GITHUB_API_URL", "")
+	cfg := &config.ConfigMock{
+		AuthenticationFunc: func() *config.AuthConfig {
+			return &config.AuthConfig{}
+		},
+	}
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return cfg, nil
+		},
+	}
+	api := New(f)
+
+	if api.githubAPI != "https://api.github.com" {
+		t.Fatalf("expected https://api.github.com, got %s", api.githubAPI)
+	}
+	if len(cfg.AuthenticationCalls()) != 1 {
+		t.Fatalf("API url was not pulled from the config")
+	}
+}
+
+func TestNew_APIURL_customConfig(t *testing.T) {
+	t.Setenv("GITHUB_API_URL", "")
+	cfg := &config.ConfigMock{
+		AuthenticationFunc: func() *config.AuthConfig {
+			authCfg := &config.AuthConfig{}
+			authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
+			return authCfg
+		},
+	}
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return cfg, nil
+		},
+	}
+	api := New(f)
+
+	if api.githubAPI != "https://github.mycompany.com/api/v3" {
+		t.Fatalf("expected https://github.mycompany.com/api/v3, got %s", api.githubAPI)
+	}
+	if len(cfg.AuthenticationCalls()) != 1 {
+		t.Fatalf("API url was not pulled from the config")
+	}
+}
+
+func TestNew_APIURL_env(t *testing.T) {
+	t.Setenv("GITHUB_API_URL", "https://api.mycompany.com")
+	cfg := &config.ConfigMock{
+		AuthenticationFunc: func() *config.AuthConfig {
+			return &config.AuthConfig{}
+		},
+	}
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return cfg, nil
+		},
+	}
+	api := New(f)
+
+	if api.githubAPI != "https://api.mycompany.com" {
+		t.Fatalf("expected https://api.mycompany.com, got %s", api.githubAPI)
+	}
+	if len(cfg.AuthenticationCalls()) != 0 {
+		t.Fatalf("Configuration was checked instead of using the GITHUB_API_URL environment variable")
+	}
+}
+
+func TestNew_APIURL_dotcomFallback(t *testing.T) {
+	t.Setenv("GITHUB_API_URL", "")
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return nil, errors.New("Failed to load")
+		},
+	}
+	api := New(f)
+
+	if api.githubAPI != "https://api.github.com" {
+		t.Fatalf("expected https://api.github.com, got %s", api.githubAPI)
+	}
+}
+
+func TestNew_ServerURL_dotcomConfig(t *testing.T) {
+	t.Setenv("GITHUB_SERVER_URL", "")
+	cfg := &config.ConfigMock{
+		AuthenticationFunc: func() *config.AuthConfig {
+			return &config.AuthConfig{}
+		},
+	}
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return cfg, nil
+		},
+	}
+	api := New(f)
+
+	if api.githubServer != "https://github.com" {
+		t.Fatalf("expected https://github.com, got %s", api.githubServer)
+	}
+	if len(cfg.AuthenticationCalls()) != 1 {
+		t.Fatalf("Server url was not pulled from the config")
+	}
+}
+
+func TestNew_ServerURL_customConfig(t *testing.T) {
+	t.Setenv("GITHUB_SERVER_URL", "")
+	cfg := &config.ConfigMock{
+		AuthenticationFunc: func() *config.AuthConfig {
+			authCfg := &config.AuthConfig{}
+			authCfg.SetDefaultHost("github.mycompany.com", "GH_HOST")
+			return authCfg
+		},
+	}
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return cfg, nil
+		},
+	}
+	api := New(f)
+
+	if api.githubServer != "https://github.mycompany.com" {
+		t.Fatalf("expected https://github.mycompany.com, got %s", api.githubServer)
+	}
+	if len(cfg.AuthenticationCalls()) != 1 {
+		t.Fatalf("Server url was not pulled from the config")
+	}
+}
+
+func TestNew_ServerURL_env(t *testing.T) {
+	t.Setenv("GITHUB_SERVER_URL", "https://mycompany.com")
+	cfg := &config.ConfigMock{
+		AuthenticationFunc: func() *config.AuthConfig {
+			return &config.AuthConfig{}
+		},
+	}
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return cfg, nil
+		},
+	}
+	api := New(f)
+
+	if api.githubServer != "https://mycompany.com" {
+		t.Fatalf("expected https://mycompany.com, got %s", api.githubServer)
+	}
+	if len(cfg.AuthenticationCalls()) != 0 {
+		t.Fatalf("Configuration was checked instead of using the GITHUB_SERVER_URL environment variable")
+	}
+}
+
+func TestNew_ServerURL_dotcomFallback(t *testing.T) {
+	t.Setenv("GITHUB_SERVER_URL", "")
+	f := &cmdutil.Factory{
+		Config: func() (config.Config, error) {
+			return nil, errors.New("Failed to load")
+		},
+	}
+	api := New(f)
+
+	if api.githubServer != "https://github.com" {
+		t.Fatalf("expected https://github.com, got %s", api.githubServer)
+	}
+}
+
 func TestCreateCodespaces(t *testing.T) {
 	svr := createFakeCreateEndpointServer(t, http.StatusCreated)
 	defer svr.Close()
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    &http.Client{},
+		client:    createHttpClient,
 	}
 
 	ctx := context.TODO()
@@ -161,7 +333,7 @@ func TestCreateCodespaces_displayName(t *testing.T) {
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    &http.Client{},
+		client:    createHttpClient,
 	}
 
 	retentionPeriod := 0
@@ -186,7 +358,7 @@ func TestCreateCodespaces_Pending(t *testing.T) {
 
 	api := API{
 		githubAPI:    svr.URL,
-		client:       &http.Client{},
+		client:       createHttpClient,
 		retryBackoff: 0,
 	}
 
@@ -213,7 +385,7 @@ func TestListCodespaces_limited(t *testing.T) {
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    &http.Client{},
+		client:    createHttpClient,
 	}
 	ctx := context.TODO()
 	codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{Limit: 200})
@@ -238,7 +410,7 @@ func TestListCodespaces_unlimited(t *testing.T) {
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    &http.Client{},
+		client:    createHttpClient,
 	}
 	ctx := context.TODO()
 	codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{})
@@ -329,7 +501,7 @@ func runRepoSearchTest(t *testing.T, searchText, wantQueryText, wantSort, wantMa
 
 	api := API{
 		githubAPI: svr.URL,
-		client:    &http.Client{},
+		client:    createHttpClient,
 	}
 
 	ctx := context.Background()
@@ -374,7 +546,7 @@ func TestRetries(t *testing.T) {
 	t.Cleanup(srv.Close)
 	a := &API{
 		githubAPI: srv.URL,
-		client:    &http.Client{},
+		client:    createHttpClient,
 	}
 	cs, err := a.GetCodespace(context.Background(), "test", false)
 	if err != nil {
@@ -562,7 +734,7 @@ func TestAPI_EditCodespace(t *testing.T) {
 			defer svr.Close()
 
 			a := &API{
-				client:    &http.Client{},
+				client:    createHttpClient,
 				githubAPI: svr.URL,
 			}
 			got, err := a.EditCodespace(tt.args.ctx, tt.args.codespaceName, tt.args.params)
@@ -602,7 +774,7 @@ func TestAPI_EditCodespacePendingOperation(t *testing.T) {
 	defer svr.Close()
 
 	a := &API{
-		client:    &http.Client{},
+		client:    createHttpClient,
 		githubAPI: svr.URL,
 	}
 
diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go
index 7b74c206d..caf6df271 100644
--- a/pkg/cmd/codespace/create_test.go
+++ b/pkg/cmd/codespace/create_test.go
@@ -453,11 +453,32 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 		},
 		{
 			name: "return default url when using web flag without other flags",
+			fields: fields{
+				apiClient: apiCreateDefaults(&apiClientMock{
+					ServerURLFunc: func() string {
+						return "https://github.com"
+					},
+				}),
+			},
 			opts: createOptions{
 				useWeb: true,
 			},
 			wantURL: "https://github.com/codespaces/new",
 		},
+		{
+			name: "return custom server url when using web flag",
+			fields: fields{
+				apiClient: apiCreateDefaults(&apiClientMock{
+					ServerURLFunc: func() string {
+						return "https://github.mycompany.com"
+					},
+				}),
+			},
+			opts: createOptions{
+				useWeb: true,
+			},
+			wantURL: "https://github.mycompany.com/codespaces/new",
+		},
 		{
 			name: "skip machine check when using web flag and no machine provided",
 			fields: fields{
@@ -473,6 +494,9 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 							Name: "monalisa-dotfiles-abcd1234",
 						}, nil
 					},
+					ServerURLFunc: func() string {
+						return "https://github.com"
+					},
 				}),
 			},
 			opts: createOptions{
@@ -499,6 +523,9 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 							Name: "monalisa-dotfiles-abcd1234",
 						}, nil
 					},
+					ServerURLFunc: func() string {
+						return "https://github.com"
+					},
 				}),
 			},
 			opts: createOptions{
@@ -524,6 +551,9 @@ Alternatively, you can run "create" with the "--default-permissions" option to c
 							Machine: api.CodespaceMachine{Name: "GIGA"},
 						}, nil
 					},
+					ServerURLFunc: func() string {
+						return "https://github.com"
+					},
 				}),
 			},
 			opts: createOptions{
diff --git a/pkg/cmd/codespace/list_test.go b/pkg/cmd/codespace/list_test.go
index d99b8b97e..35f30581b 100644
--- a/pkg/cmd/codespace/list_test.go
+++ b/pkg/cmd/codespace/list_test.go
@@ -170,11 +170,32 @@ func TestApp_List(t *testing.T) {
 		},
 		{
 			name: "list codespaces,--web",
+			fields: fields{
+				apiClient: &apiClientMock{
+					ServerURLFunc: func() string {
+						return "https://github.com"
+					},
+				},
+			},
 			opts: &listOptions{
 				useWeb: true,
 			},
 			wantURL: "https://github.com/codespaces",
 		},
+		{
+			name: "list codespaces,--web with custom server url",
+			fields: fields{
+				apiClient: &apiClientMock{
+					ServerURLFunc: func() string {
+						return "https://github.mycompany.com"
+					},
+				},
+			},
+			opts: &listOptions{
+				useWeb: true,
+			},
+			wantURL: "https://github.mycompany.com/codespaces",
+		},
 		{
 			name: "list codespaces,--web, --repo flag",
 			fields: fields{
@@ -199,6 +220,9 @@ func TestApp_List(t *testing.T) {
 							},
 						}, nil
 					},
+					ServerURLFunc: func() string {
+						return "https://github.com"
+					},
 				},
 			},
 			opts: &listOptions{
diff --git a/pkg/cmd/project/close/close_test.go b/pkg/cmd/project/close/close_test.go
index a19dcdabd..96ec6ac50 100644
--- a/pkg/cmd/project/close/close_test.go
+++ b/pkg/cmd/project/close/close_test.go
@@ -1,7 +1,6 @@
 package close
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -56,8 +55,7 @@ func TestNewCmdClose(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/copy/copy_test.go b/pkg/cmd/project/copy/copy_test.go
index d8abf5ec9..3caec592d 100644
--- a/pkg/cmd/project/copy/copy_test.go
+++ b/pkg/cmd/project/copy/copy_test.go
@@ -1,7 +1,6 @@
 package copy
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -75,8 +74,7 @@ func TestNewCmdCopy(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/create/create_test.go b/pkg/cmd/project/create/create_test.go
index 3ef773cb9..74d1747b7 100644
--- a/pkg/cmd/project/create/create_test.go
+++ b/pkg/cmd/project/create/create_test.go
@@ -1,7 +1,6 @@
 package create
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -45,8 +44,7 @@ func TestNewCmdCreate(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/delete/delete_test.go b/pkg/cmd/project/delete/delete_test.go
index a27180266..589ef72bc 100644
--- a/pkg/cmd/project/delete/delete_test.go
+++ b/pkg/cmd/project/delete/delete_test.go
@@ -1,7 +1,6 @@
 package delete
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -49,8 +48,7 @@ func TestNewCmdDelete(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/edit/edit_test.go b/pkg/cmd/project/edit/edit_test.go
index 605c5f5cc..da30b0935 100644
--- a/pkg/cmd/project/edit/edit_test.go
+++ b/pkg/cmd/project/edit/edit_test.go
@@ -1,7 +1,6 @@
 package edit
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -92,8 +91,7 @@ func TestNewCmdEdit(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/field-create/field_create_test.go b/pkg/cmd/project/field-create/field_create_test.go
index 31328a04f..01904d9e9 100644
--- a/pkg/cmd/project/field-create/field_create_test.go
+++ b/pkg/cmd/project/field-create/field_create_test.go
@@ -1,7 +1,6 @@
 package fieldcreate
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -79,8 +78,7 @@ func TestNewCmdCreateField(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/field-delete/field_delete_test.go b/pkg/cmd/project/field-delete/field_delete_test.go
index 61783d44d..6b1e6c763 100644
--- a/pkg/cmd/project/field-delete/field_delete_test.go
+++ b/pkg/cmd/project/field-delete/field_delete_test.go
@@ -1,7 +1,6 @@
 package fielddelete
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -43,8 +42,7 @@ func TestNewCmdDeleteField(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/field-list/field_list_test.go b/pkg/cmd/project/field-list/field_list_test.go
index 60e61188d..c6c2f97c6 100644
--- a/pkg/cmd/project/field-list/field_list_test.go
+++ b/pkg/cmd/project/field-list/field_list_test.go
@@ -1,7 +1,6 @@
 package fieldlist
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/internal/tableprinter"
@@ -53,8 +52,7 @@ func TestNewCmdList(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-add/item_add_test.go b/pkg/cmd/project/item-add/item_add_test.go
index 4f1ebd4f6..0beb1fed6 100644
--- a/pkg/cmd/project/item-add/item_add_test.go
+++ b/pkg/cmd/project/item-add/item_add_test.go
@@ -1,7 +1,6 @@
 package itemadd
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -65,8 +64,7 @@ func TestNewCmdaddItem(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-archive/item_archive_test.go b/pkg/cmd/project/item-archive/item_archive_test.go
index 85a34af22..6eb8336f2 100644
--- a/pkg/cmd/project/item-archive/item_archive_test.go
+++ b/pkg/cmd/project/item-archive/item_archive_test.go
@@ -1,7 +1,6 @@
 package itemarchive
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -73,8 +72,7 @@ func TestNewCmdarchiveItem(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-create/item_create_test.go b/pkg/cmd/project/item-create/item_create_test.go
index d49144f3c..1482fe268 100644
--- a/pkg/cmd/project/item-create/item_create_test.go
+++ b/pkg/cmd/project/item-create/item_create_test.go
@@ -1,7 +1,6 @@
 package itemcreate
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -73,8 +72,7 @@ func TestNewCmdCreateItem(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-delete/item_delete_test.go b/pkg/cmd/project/item-delete/item_delete_test.go
index c5a3f01fd..4d3e97059 100644
--- a/pkg/cmd/project/item-delete/item_delete_test.go
+++ b/pkg/cmd/project/item-delete/item_delete_test.go
@@ -1,7 +1,6 @@
 package itemdelete
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -65,8 +64,7 @@ func TestNewCmdDeleteItem(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-edit/item_edit_test.go b/pkg/cmd/project/item-edit/item_edit_test.go
index 156871f32..5cdc69b87 100644
--- a/pkg/cmd/project/item-edit/item_edit_test.go
+++ b/pkg/cmd/project/item-edit/item_edit_test.go
@@ -1,7 +1,6 @@
 package itemedit
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -105,8 +104,7 @@ func TestNewCmdeditItem(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/item-list/item_list_test.go b/pkg/cmd/project/item-list/item_list_test.go
index d618451b1..490473691 100644
--- a/pkg/cmd/project/item-list/item_list_test.go
+++ b/pkg/cmd/project/item-list/item_list_test.go
@@ -1,7 +1,6 @@
 package itemlist
 
 import (
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -54,8 +53,7 @@ func TestNewCmdList(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/list/list_test.go b/pkg/cmd/project/list/list_test.go
index 48fb689ed..492d224b0 100644
--- a/pkg/cmd/project/list/list_test.go
+++ b/pkg/cmd/project/list/list_test.go
@@ -2,7 +2,6 @@ package list
 
 import (
 	"bytes"
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/internal/tableprinter"
@@ -56,8 +55,7 @@ func TestNewCmdlist(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/cmd/project/view/view_test.go b/pkg/cmd/project/view/view_test.go
index 5c76c1413..35f6dfc9b 100644
--- a/pkg/cmd/project/view/view_test.go
+++ b/pkg/cmd/project/view/view_test.go
@@ -2,7 +2,6 @@ package view
 
 import (
 	"bytes"
-	"os"
 	"testing"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
@@ -57,8 +56,7 @@ func TestNewCmdview(t *testing.T) {
 		},
 	}
 
-	os.Setenv("GH_TOKEN", "auth-token")
-	defer os.Unsetenv("GH_TOKEN")
+	t.Setenv("GH_TOKEN", "auth-token")
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {