Repository: cli/cli

Issue:
Consume dependabot minor upgrades
## Description

Currently, [Dependabot will open pull requests for new patch versions](https://github.com/cli/cli/blob/dc7b22b65971f0937211bab26a7b00b2a26d2d83/.github/dependabot.yml#L3-L11):

```yaml
version: 2
updates:
- package-ecosystem: gomod
  directory: "/"
  schedule:
    interval: "daily"
  ignore:
    - dependency-name: "*"
      update-types:
        - version-update:semver-minor
        - version-update:semver-major
- package-ecosystem: "github-actions"
  directory: "/"
  schedule:
      interval: "daily"
```

We want to start consuming minor versions.

### Expected Output

Dependabot is configured to consume minor versions.

Discussion:

Solution patch:
diff --git a/api/queries_pr.go b/api/queries_pr.go
index 1d394e864..525418a11 100644
--- a/api/queries_pr.go
+++ b/api/queries_pr.go
@@ -85,25 +85,15 @@ type PullRequest struct {
 
 	Assignees      Assignees
 	AssignedActors AssignedActors
-	// AssignedActorsUsed is a GIGANTIC hack to carry around whether we expected AssignedActors to be requested
-	// on this PR. This is required because the Feature Detection of support for AssignedActors occurs inside the
-	// PR Finder, but knowledge of support is required at the command level. However, we can't easily construct
-	// the feature detector at the command level because it needs knowledge of the BaseRepo, which is only available
-	// inside the PR Finder. This is bad and we should feel bad.
-	//
-	// The right solution is to extract argument parsing from the PR Finder into each command, so that we have access
-	// to the BaseRepo and can construct the feature detector there. This is what happens in the issue commands with
-	// `shared.ParseIssueFromArg`.
-	AssignedActorsUsed bool
-	Labels             Labels
-	ProjectCards       ProjectCards
-	ProjectItems       ProjectItems
-	Milestone          *Milestone
-	Comments           Comments
-	ReactionGroups     ReactionGroups
-	Reviews            PullRequestReviews
-	LatestReviews      PullRequestReviews
-	ReviewRequests     ReviewRequests
+	Labels         Labels
+	ProjectCards   ProjectCards
+	ProjectItems   ProjectItems
+	Milestone      *Milestone
+	Comments       Comments
+	ReactionGroups ReactionGroups
+	Reviews        PullRequestReviews
+	LatestReviews  PullRequestReviews
+	ReviewRequests ReviewRequests
 
 	ClosingIssuesReferences ClosingIssuesReferences
 }
diff --git a/go.mod b/go.mod
index e8be99e08..0291023a7 100644
--- a/go.mod
+++ b/go.mod
@@ -39,7 +39,7 @@ require (
 	github.com/mattn/go-colorable v0.1.14
 	github.com/mattn/go-isatty v0.0.20
 	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
-	github.com/microsoft/dev-tunnels v0.0.25
+	github.com/microsoft/dev-tunnels v0.1.13
 	github.com/muhammadmuzzammil1998/jsonc v1.0.0
 	github.com/opentracing/opentracing-go v1.2.0
 	github.com/rivo/tview v0.0.0-20250625164341-a4a78f1e05cb
diff --git a/go.sum b/go.sum
index fd2a9c32b..c64e0b244 100644
--- a/go.sum
+++ b/go.sum
@@ -370,8 +370,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
-github.com/microsoft/dev-tunnels v0.0.25 h1:UlMKUI+2O8cSu4RlB52ioSyn1LthYSVkJA+CSTsdKoA=
-github.com/microsoft/dev-tunnels v0.0.25/go.mod h1:frU++12T/oqxckXkDpTuYa427ncguEOodSPZcGCCrzQ=
+github.com/microsoft/dev-tunnels v0.1.13 h1:bp1qqCvP/5iLol1Vz0c/lM2sexG7Gd8fRGcGv58vZdE=
+github.com/microsoft/dev-tunnels v0.1.13/go.mod h1:Jvr6RlyjUXomM6KsDmIQbq+hhKd5mWrBcv3MEsa78dc=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
diff --git a/internal/codespaces/connection/connection.go b/internal/codespaces/connection/connection.go
index 36fef3e17..155e349bb 100644
--- a/internal/codespaces/connection/connection.go
+++ b/internal/codespaces/connection/connection.go
@@ -132,7 +132,9 @@ func getTunnelManager(tunnelProperties api.TunnelProperties, httpClient *http.Cl
 	}
 
 	// Create the tunnel manager
-	tunnelManager, err = tunnels.NewManager(userAgent, nil, url, httpClient)
+	// This api version seems to be the only acceptable api version: https://github.com/microsoft/dev-tunnels/blob/bf96ae5a128041d1a23f81d53a47e9e6c26fdc8d/go/tunnels/manager.go#L66
+	apiVersion := "2023-09-27-preview"
+	tunnelManager, err = tunnels.NewManager(userAgent, nil, url, httpClient, apiVersion)
 	if err != nil {
 		return nil, fmt.Errorf("error creating tunnel manager: %w", err)
 	}
diff --git a/internal/codespaces/connection/tunnels_api_server_mock.go b/internal/codespaces/connection/tunnels_api_server_mock.go
index cf8f05cfa..8f040886c 100644
--- a/internal/codespaces/connection/tunnels_api_server_mock.go
+++ b/internal/codespaces/connection/tunnels_api_server_mock.go
@@ -14,6 +14,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -25,7 +26,28 @@ import (
 	"golang.org/x/crypto/ssh"
 )
 
-func NewMockHttpClient() (*http.Client, error) {
+type mockClientOpts struct {
+	ports map[int]tunnels.TunnelPort // Port number to protocol
+}
+
+type mockClientOpt func(*mockClientOpts)
+
+// WithSpecificPorts allows you to specify a map of ports to TunnelPorts that will be returned by the mock HTTP client.
+// Note that this does not take a copy of the map, so you should not modify the map after passing it to this function.
+func WithSpecificPorts(ports map[int]tunnels.TunnelPort) mockClientOpt {
+	return func(opts *mockClientOpts) {
+		opts.ports = ports
+	}
+}
+
+func NewMockHttpClient(opts ...mockClientOpt) (*http.Client, error) {
+	mockClientOpts := &mockClientOpts{}
+	for _, opt := range opts {
+		opt(mockClientOpts)
+	}
+
+	specifiedPorts := mockClientOpts.ports
+
 	accessToken := "tunnel access-token"
 	relayServer, err := newMockrelayServer(withAccessToken(accessToken))
 	if err != nil {
@@ -35,7 +57,7 @@ func NewMockHttpClient() (*http.Client, error) {
 	hostURL := strings.Replace(relayServer.URL(), "http://", "ws://", 1)
 	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		var response []byte
-		if r.URL.Path == "/api/v1/tunnels/tunnel-id" {
+		if r.URL.Path == "/tunnels/tunnel-id" {
 			tunnel := &tunnels.Tunnel{
 				AccessTokens: map[tunnels.TunnelAccessScope]string{
 					tunnels.TunnelAccessScopeConnect: accessToken,
@@ -54,54 +76,141 @@ func NewMockHttpClient() (*http.Client, error) {
 			if err != nil {
 				log.Fatalf("json.Marshal returned an error: %v", err)
 			}
-		} else if strings.HasPrefix(r.URL.Path, "/api/v1/tunnels/tunnel-id/ports") {
-			// Use regex to check if the path ends with a number
-			match, err := regexp.MatchString(`\/\d+$`, r.URL.Path)
-			if err != nil {
-				log.Fatalf("regexp.MatchString returned an error: %v", err)
-			}
 
-			// If the path ends with a number, it's a request for a specific port
-			if match || r.Method == http.MethodPost {
+			_, _ = w.Write(response)
+			return
+		} else if strings.HasPrefix(r.URL.Path, "/tunnels/tunnel-id/ports") {
+			// Use regex to capture the port number from the end of the path
+			re := regexp.MustCompile(`\/(\d+)$`)
+			matches := re.FindStringSubmatch(r.URL.Path)
+			targetingSpecificPort := len(matches) > 0
+
+			if targetingSpecificPort {
 				if r.Method == http.MethodDelete {
 					w.WriteHeader(http.StatusOK)
 					return
 				}
 
-				tunnelPort := &tunnels.TunnelPort{
+				if r.Method == http.MethodGet {
+					// If no ports were configured, then we assume that every request for a port is valid.
+					if specifiedPorts == nil {
+						response, err := json.Marshal(tunnels.TunnelPort{
+							AccessControl: &tunnels.TunnelAccessControl{
+								Entries: []tunnels.TunnelAccessControlEntry{},
+							},
+						})
+
+						if err != nil {
+							log.Fatalf("json.Marshal returned an error: %v", err)
+						}
+
+						_, _ = w.Write(response)
+						return
+					} else {
+						// Otherwise we'll fetch the port from our configured ports and include the protocol in the response.
+						port, err := strconv.Atoi(matches[1])
+						if err != nil {
+							log.Fatalf("strconv.Atoi returned an error: %v", err)
+						}
+
+						tunnelPort, ok := specifiedPorts[port]
+						if !ok {
+							w.WriteHeader(http.StatusNotFound)
+							return
+						}
+
+						response, err := json.Marshal(tunnelPort)
+
+						if err != nil {
+							log.Fatalf("json.Marshal returned an error: %v", err)
+						}
+
+						_, _ = w.Write(response)
+						return
+					}
+				}
+
+				// Else this is an unexpected request, fall through to 404 at the bottom
+			}
+
+			// If it's a PUT request, we assume it's for creating a new port so we'll do some validation
+			// and then return a stub.
+			if r.Method == http.MethodPut {
+				// If a port was already configured with this number, and the protocol has changed, return a 400 Bad Request.
+				if specifiedPorts != nil {
+					port, err := strconv.Atoi(matches[1])
+					if err != nil {
+						log.Fatalf("strconv.Atoi returned an error: %v", err)
+					}
+
+					var portRequest tunnels.TunnelPort
+					if err := json.NewDecoder(r.Body).Decode(&portRequest); err != nil {
+						log.Fatalf("json.NewDecoder returned an error: %v", err)
+					}
+
+					tunnelPort, ok := specifiedPorts[port]
+					if ok {
+						if tunnelPort.Protocol != portRequest.Protocol {
+							w.WriteHeader(http.StatusBadRequest)
+							return
+						}
+					}
+
+					// Create or update the new port entry.
+					specifiedPorts[port] = portRequest
+				}
+
+				response, err := json.Marshal(tunnels.TunnelPort{
 					AccessControl: &tunnels.TunnelAccessControl{
 						Entries: []tunnels.TunnelAccessControlEntry{},
 					},
-				}
+				})
 
-				// Convert the tunnel to JSON and write it to the response
-				response, err = json.Marshal(*tunnelPort)
 				if err != nil {
 					log.Fatalf("json.Marshal returned an error: %v", err)
 				}
-			} else {
-				// If the path doesn't end with a number and we aren't making a POST request, return an array of ports
-				tunnelPorts := []tunnels.TunnelPort{
-					{
-						AccessControl: &tunnels.TunnelAccessControl{
-							Entries: []tunnels.TunnelAccessControlEntry{},
+
+				_, _ = w.Write(response)
+				return
+			}
+
+			// Finally, if it's not targeting a specific port or a POST request, we return a list of ports, either
+			// totally stubbed, or whatever was configured in the mock client options.
+			if specifiedPorts == nil {
+				response, err := json.Marshal(tunnels.TunnelPortListResponse{
+					Value: []tunnels.TunnelPort{
+						{
+							AccessControl: &tunnels.TunnelAccessControl{
+								Entries: []tunnels.TunnelAccessControlEntry{},
+							},
 						},
 					},
+				})
+				if err != nil {
+					log.Fatalf("json.Marshal returned an error: %v", err)
 				}
 
-				response, err = json.Marshal(tunnelPorts)
+				_, _ = w.Write(response)
+				return
+			} else {
+				var ports []tunnels.TunnelPort
+				for _, tunnelPort := range specifiedPorts {
+					ports = append(ports, tunnelPort)
+				}
+				response, err := json.Marshal(tunnels.TunnelPortListResponse{
+					Value: ports,
+				})
 				if err != nil {
 					log.Fatalf("json.Marshal returned an error: %v", err)
 				}
-			}
 
+				_, _ = w.Write(response)
+				return
+			}
 		} else {
 			w.WriteHeader(http.StatusNotFound)
 			return
 		}
-
-		// Write the response
-		_, _ = w.Write(response)
 	}))
 
 	url, err := url.Parse(mockServer.URL)
diff --git a/internal/codespaces/portforwarder/port_forwarder.go b/internal/codespaces/portforwarder/port_forwarder.go
index b62d13715..7f696c1a4 100644
--- a/internal/codespaces/portforwarder/port_forwarder.go
+++ b/internal/codespaces/portforwarder/port_forwarder.go
@@ -12,9 +12,9 @@ import (
 )
 
 const (
-	githubSubjectId      = "1"
-	InternalPortTag      = "InternalPort"
-	UserForwardedPortTag = "UserForwardedPort"
+	githubSubjectId        = "1"
+	InternalPortLabel      = "InternalPort"
+	UserForwardedPortLabel = "UserForwardedPort"
 )
 
 const (
@@ -108,7 +108,26 @@ func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts Forwar
 		return fmt.Errorf("error converting port: %w", err)
 	}
 
-	tunnelPort := tunnels.NewTunnelPort(port, "", "", tunnels.TunnelProtocolHttp)
+	// In v0.0.25 of dev-tunnels, the dev-tunnel manager `CreateTunnelPort` would "accept" requests that
+	// change the port protocol but they would not result in any actual change. This has changed, resulting in
+	// an error `Invalid arguments. The tunnel port protocol cannot be changed.`. It's not clear why the previous
+	// behaviour existed, whether it was truly the API version, or whether the `If-Not-Match` header being set inside
+	// `CreateTunnelPort` avoided the server accepting the request to change the protocol and that has since regressed.
+	//
+	// In any case, now we check whether a port exists with the given port number, if it does, we use the existing protocol.
+	// If it doesn't exist, we default to HTTP, which was the previous behaviour for all ports.
+	protocol := tunnels.TunnelProtocolHttp
+
+	existingPort, err := fwd.connection.TunnelManager.GetTunnelPort(ctx, fwd.connection.Tunnel, opts.Port, fwd.connection.Options)
+	if err != nil && !strings.Contains(err.Error(), "404") {
+		return fmt.Errorf("error checking whether tunnel port already exists: %v", err)
+	}
+
+	if existingPort != nil {
+		protocol = tunnels.TunnelProtocol(existingPort.Protocol)
+	}
+
+	tunnelPort := tunnels.NewTunnelPort(port, "", "", protocol)
 
 	// If no visibility is provided, Dev Tunnels will use the default (private)
 	if opts.Visibility != "" {
@@ -136,9 +155,9 @@ func (fwd *CodespacesPortForwarder) ForwardPort(ctx context.Context, opts Forwar
 
 	// Tag the port as internal or user forwarded so we know if it needs to be shown in the UI
 	if opts.Internal {
-		tunnelPort.Tags = []string{InternalPortTag}
+		tunnelPort.Labels = []string{InternalPortLabel}
 	} else {
-		tunnelPort.Tags = []string{UserForwardedPortTag}
+		tunnelPort.Labels = []string{UserForwardedPortLabel}
 	}
 
 	// Create the tunnel port
@@ -362,8 +381,8 @@ func visibilityToAccessControlEntries(visibility string) []tunnels.TunnelAccessC
 
 // IsInternalPort returns true if the port is internal.
 func IsInternalPort(port *tunnels.TunnelPort) bool {
-	for _, tag := range port.Tags {
-		if strings.EqualFold(tag, InternalPortTag) {
+	for _, label := range port.Labels {
+		if strings.EqualFold(label, InternalPortLabel) {
 			return true
 		}
 	}
diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go
index becbfce47..c01364d24 100644
--- a/pkg/cmd/pr/edit/edit.go
+++ b/pkg/cmd/pr/edit/edit.go
@@ -3,6 +3,7 @@ package edit
 import (
 	"fmt"
 	"net/http"
+	"time"
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
@@ -81,12 +82,28 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
 		Args: cobra.MaximumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			opts.Finder = shared.NewFinder(f)
+
+			// support `-R, --repo` override
 			opts.BaseRepo = f.BaseRepo
 
 			if len(args) > 0 {
 				opts.SelectorArg = args[0]
 			}
 
+			if opts.SelectorArg != "" {
+				// If a URL is provided, we need to parse it to override the
+				// base repository, especially the hostname part. That's because
+				// we need a feature detector down in this command, and that
+				// needs to know the API host. If the command is run outside of
+				// a git repo, we cannot instantiate the detector unless we have
+				// already parsed the URL.
+				if baseRepo, _, err := shared.ParseURL(opts.SelectorArg); err == nil {
+					opts.BaseRepo = func() (ghrepo.Interface, error) {
+						return baseRepo, nil
+					}
+				}
+			}
+
 			flags := cmd.Flags()
 
 			bodyProvided := flags.Changed("body")
@@ -203,17 +220,38 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
 }
 
 func editRun(opts *EditOptions) error {
+	httpClient, err := opts.HttpClient()
+	if err != nil {
+		return err
+	}
+
+	if opts.Detector == nil {
+		baseRepo, err := opts.BaseRepo()
+		if err != nil {
+			return err
+		}
+
+		cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+		opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
+	}
+
 	findOptions := shared.FindOptions{
 		Selector: opts.SelectorArg,
-		Fields:   []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone", "assignees"},
+		Fields:   []string{"id", "url", "title", "body", "baseRefName", "reviewRequests", "labels", "projectCards", "projectItems", "milestone"},
 		Detector: opts.Detector,
 	}
 
-	httpClient, err := opts.HttpClient()
+	issueFeatures, err := opts.Detector.IssueFeatures()
 	if err != nil {
 		return err
 	}
 
+	if issueFeatures.ActorIsAssignable {
+		findOptions.Fields = append(findOptions.Fields, "assignedActors")
+	} else {
+		findOptions.Fields = append(findOptions.Fields, "assignees")
+	}
+
 	pr, repo, err := opts.Finder.Find(findOptions)
 	if err != nil {
 		return err
@@ -225,7 +263,7 @@ func editRun(opts *EditOptions) error {
 	editable.Body.Default = pr.Body
 	editable.Base.Default = pr.BaseRefName
 	editable.Reviewers.Default = pr.ReviewRequests.Logins()
-	if pr.AssignedActorsUsed {
+	if issueFeatures.ActorIsAssignable {
 		editable.Assignees.ActorAssignees = true
 		editable.Assignees.Default = pr.AssignedActors.DisplayNames()
 		editable.Assignees.DefaultLogins = pr.AssignedActors.Logins()
diff --git a/pkg/cmd/pr/shared/finder.go b/pkg/cmd/pr/shared/finder.go
index a19c69669..df8cc0fd4 100644
--- a/pkg/cmd/pr/shared/finder.go
+++ b/pkg/cmd/pr/shared/finder.go
@@ -112,7 +112,7 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		return nil, nil, errors.New("Find error: no fields specified")
 	}
 
-	if repo, prNumber, err := f.parseURL(opts.Selector); err == nil {
+	if repo, prNumber, err := ParseURL(opts.Selector); err == nil {
 		f.prNumber = prNumber
 		f.baseRefRepo = repo
 	}
@@ -242,33 +242,6 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		}
 	}
 
-	// Ok this is super, super horrible so bear with me.
-	// The `assignees` field on a Pull Request exposes users that are assigned. It is also possible for bots to be
-	// assigned, but they only appear under the `assignedActors` field. Ideally, the caller of `Find` would determine
-	// the correct field to use based on the `fd.Detector` that is passed in, but they can't construct a detector
-	// because the BaseRepo is only determined within this function. The more correct solution is to do what I did with
-	// the issue commands and decouple argument parsing from API lookup. See PR #10811 for example.
-	var actorAssigneesUsed bool
-	if fields.Contains("assignees") {
-		if opts.Detector == nil {
-			cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
-			opts.Detector = fd.NewDetector(cachedClient, f.baseRefRepo.RepoHost())
-		}
-
-		issueFeatures, err := opts.Detector.IssueFeatures()
-		if err != nil {
-			return nil, nil, fmt.Errorf("error detecting issue features: %v", err)
-		}
-
-		// If actors are assignable on this host then we additionally request the `assignedActors` field.
-		// Note that we don't remove the `assignees` field because some commands (`pr view`) do not display actor
-		// assignees yet, so we have to have both sets of data.
-		if issueFeatures.ActorIsAssignable {
-			fields.Add("assignedActors")
-			actorAssigneesUsed = true
-		}
-	}
-
 	var pr *api.PullRequest
 	if f.prNumber > 0 {
 		// If we have a PR number, let's look it up
@@ -324,16 +297,14 @@ func (f *finder) Find(opts FindOptions) (*api.PullRequest, ghrepo.Interface, err
 		})
 	}
 
-	if actorAssigneesUsed {
-		pr.AssignedActorsUsed = true
-	}
-
 	return pr, f.baseRefRepo, g.Wait()
 }
 
 var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
 
-func (f *finder) parseURL(prURL string) (ghrepo.Interface, int, error) {
+// ParseURL parses a pull request URL and returns the repository and pull
+// request number.
+func ParseURL(prURL string) (ghrepo.Interface, int, error) {
 	if prURL == "" {
 		return nil, 0, fmt.Errorf("invalid URL: %q", prURL)
 	}
diff --git a/third-party-licenses.darwin.md b/third-party-licenses.darwin.md
index 60498f974..53514f14d 100644
--- a/third-party-licenses.darwin.md
+++ b/third-party-licenses.darwin.md
@@ -106,7 +106,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/mattn/go-runewidth](https://pkg.go.dev/github.com/mattn/go-runewidth) ([MIT](https://github.com/mattn/go-runewidth/blob/v0.0.16/LICENSE))
 - [github.com/mgutz/ansi](https://pkg.go.dev/github.com/mgutz/ansi) ([MIT](https://github.com/mgutz/ansi/blob/d51e80ef957d/LICENSE))
 - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
-- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.0.25/LICENSE))
+- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.1.13/LICENSE))
 - [github.com/mitchellh/copystructure](https://pkg.go.dev/github.com/mitchellh/copystructure) ([MIT](https://github.com/mitchellh/copystructure/blob/v1.2.0/LICENSE))
 - [github.com/mitchellh/go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) ([MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE))
 - [github.com/mitchellh/hashstructure/v2](https://pkg.go.dev/github.com/mitchellh/hashstructure/v2) ([MIT](https://github.com/mitchellh/hashstructure/blob/v2.0.2/LICENSE))
diff --git a/third-party-licenses.linux.md b/third-party-licenses.linux.md
index cb5d2db05..6ce47d8bc 100644
--- a/third-party-licenses.linux.md
+++ b/third-party-licenses.linux.md
@@ -106,7 +106,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/mattn/go-runewidth](https://pkg.go.dev/github.com/mattn/go-runewidth) ([MIT](https://github.com/mattn/go-runewidth/blob/v0.0.16/LICENSE))
 - [github.com/mgutz/ansi](https://pkg.go.dev/github.com/mgutz/ansi) ([MIT](https://github.com/mgutz/ansi/blob/d51e80ef957d/LICENSE))
 - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
-- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.0.25/LICENSE))
+- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.1.13/LICENSE))
 - [github.com/mitchellh/copystructure](https://pkg.go.dev/github.com/mitchellh/copystructure) ([MIT](https://github.com/mitchellh/copystructure/blob/v1.2.0/LICENSE))
 - [github.com/mitchellh/go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) ([MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE))
 - [github.com/mitchellh/hashstructure/v2](https://pkg.go.dev/github.com/mitchellh/hashstructure/v2) ([MIT](https://github.com/mitchellh/hashstructure/blob/v2.0.2/LICENSE))
diff --git a/third-party-licenses.windows.md b/third-party-licenses.windows.md
index d276a5e44..c4ebb297e 100644
--- a/third-party-licenses.windows.md
+++ b/third-party-licenses.windows.md
@@ -109,7 +109,7 @@ Some packages may only be included on certain architectures or operating systems
 - [github.com/mattn/go-runewidth](https://pkg.go.dev/github.com/mattn/go-runewidth) ([MIT](https://github.com/mattn/go-runewidth/blob/v0.0.16/LICENSE))
 - [github.com/mgutz/ansi](https://pkg.go.dev/github.com/mgutz/ansi) ([MIT](https://github.com/mgutz/ansi/blob/d51e80ef957d/LICENSE))
 - [github.com/microcosm-cc/bluemonday](https://pkg.go.dev/github.com/microcosm-cc/bluemonday) ([BSD-3-Clause](https://github.com/microcosm-cc/bluemonday/blob/v1.0.27/LICENSE.md))
-- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.0.25/LICENSE))
+- [github.com/microsoft/dev-tunnels/go/tunnels](https://pkg.go.dev/github.com/microsoft/dev-tunnels/go/tunnels) ([MIT](https://github.com/microsoft/dev-tunnels/blob/v0.1.13/LICENSE))
 - [github.com/mitchellh/copystructure](https://pkg.go.dev/github.com/mitchellh/copystructure) ([MIT](https://github.com/mitchellh/copystructure/blob/v1.2.0/LICENSE))
 - [github.com/mitchellh/go-homedir](https://pkg.go.dev/github.com/mitchellh/go-homedir) ([MIT](https://github.com/mitchellh/go-homedir/blob/v1.1.0/LICENSE))
 - [github.com/mitchellh/hashstructure/v2](https://pkg.go.dev/github.com/mitchellh/hashstructure/v2) ([MIT](https://github.com/mitchellh/hashstructure/blob/v2.0.2/LICENSE))



Test patch:
diff --git a/internal/codespaces/portforwarder/port_forwarder_test.go b/internal/codespaces/portforwarder/port_forwarder_test.go
index d107afec4..a951ed2b1 100644
--- a/internal/codespaces/portforwarder/port_forwarder_test.go
+++ b/internal/codespaces/portforwarder/port_forwarder_test.go
@@ -105,10 +105,10 @@ func TestAccessControlEntriesToVisibility(t *testing.T) {
 
 func TestIsInternalPort(t *testing.T) {
 	internalPort := &tunnels.TunnelPort{
-		Tags: []string{"InternalPort"},
+		Labels: []string{"InternalPort"},
 	}
 	userForwardedPort := &tunnels.TunnelPort{
-		Tags: []string{"UserForwardedPort"},
+		Labels: []string{"UserForwardedPort"},
 	}
 
 	tests := []struct {
@@ -137,3 +137,131 @@ func TestIsInternalPort(t *testing.T) {
 		})
 	}
 }
+
+func TestForwardPortDefaultsToHTTPProtocol(t *testing.T) {
+	codespace := &api.Codespace{
+		Name:  "codespace-name",
+		State: api.CodespaceStateAvailable,
+		Connection: api.CodespaceConnection{
+			TunnelProperties: api.TunnelProperties{
+				ConnectAccessToken:     "tunnel access-token",
+				ManagePortsAccessToken: "manage-ports-token",
+				ServiceUri:             "http://global.rel.tunnels.api.visualstudio.com/",
+				TunnelId:               "tunnel-id",
+				ClusterId:              "usw2",
+				Domain:                 "domain.com",
+			},
+		},
+		RuntimeConstraints: api.RuntimeConstraints{
+			AllowedPortPrivacySettings: []string{"public", "private"},
+		},
+	}
+
+	// Given there are no forwarded ports.
+	tunnelPorts := map[int]tunnels.TunnelPort{}
+
+	httpClient, err := connection.NewMockHttpClient(
+		connection.WithSpecificPorts(tunnelPorts),
+	)
+	if err != nil {
+		t.Fatalf("NewMockHttpClient returned an error: %v", err)
+	}
+
+	connection, err := connection.NewCodespaceConnection(t.Context(), codespace, httpClient)
+	if err != nil {
+		t.Fatalf("NewCodespaceConnection returned an error: %v", err)
+	}
+
+	fwd, err := NewPortForwarder(t.Context(), connection)
+	if err != nil {
+		t.Fatalf("NewPortForwarder returned an error: %v", err)
+	}
+
+	// When we forward a port without an existing one to use for a protocol, it should default to HTTP.
+	if err := fwd.ForwardPort(t.Context(), ForwardPortOpts{
+		Port: 1337,
+	}); err != nil {
+		t.Fatalf("ForwardPort returned an error: %v", err)
+	}
+
+	ports, err := fwd.ListPorts(t.Context())
+	if err != nil {
+		t.Fatalf("ListPorts returned an error: %v", err)
+	}
+
+	if len(ports) != 1 {
+		t.Fatalf("expected 1 port, got %d", len(ports))
+	}
+
+	if ports[0].Protocol != string(tunnels.TunnelProtocolHttp) {
+		t.Fatalf("expected port protocol to be http, got %s", ports[0].Protocol)
+	}
+}
+
+func TestForwardPortRespectsProtocolOfExistingTunneledPorts(t *testing.T) {
+	codespace := &api.Codespace{
+		Name:  "codespace-name",
+		State: api.CodespaceStateAvailable,
+		Connection: api.CodespaceConnection{
+			TunnelProperties: api.TunnelProperties{
+				ConnectAccessToken:     "tunnel access-token",
+				ManagePortsAccessToken: "manage-ports-token",
+				ServiceUri:             "http://global.rel.tunnels.api.visualstudio.com/",
+				TunnelId:               "tunnel-id",
+				ClusterId:              "usw2",
+				Domain:                 "domain.com",
+			},
+		},
+		RuntimeConstraints: api.RuntimeConstraints{
+			AllowedPortPrivacySettings: []string{"public", "private"},
+		},
+	}
+
+	// Given we already have a port forwarded with an HTTPS protocol.
+	tunnelPorts := map[int]tunnels.TunnelPort{
+		1337: {
+			Protocol: string(tunnels.TunnelProtocolHttps),
+			AccessControl: &tunnels.TunnelAccessControl{
+				Entries: []tunnels.TunnelAccessControlEntry{},
+			},
+		},
+	}
+
+	httpClient, err := connection.NewMockHttpClient(
+		connection.WithSpecificPorts(tunnelPorts),
+	)
+	if err != nil {
+		t.Fatalf("NewMockHttpClient returned an error: %v", err)
+	}
+
+	connection, err := connection.NewCodespaceConnection(t.Context(), codespace, httpClient)
+	if err != nil {
+		t.Fatalf("NewCodespaceConnection returned an error: %v", err)
+	}
+
+	fwd, err := NewPortForwarder(t.Context(), connection)
+	if err != nil {
+		t.Fatalf("NewPortForwarder returned an error: %v", err)
+	}
+
+	// When we forward a port, it would typically default to HTTP, to which the mock server would respond with a 400,
+	// but it should respect the existing port's protocol and forward it as HTTPS.
+	if err := fwd.ForwardPort(t.Context(), ForwardPortOpts{
+		Port: 1337,
+	}); err != nil {
+		t.Fatalf("ForwardPort returned an error: %v", err)
+	}
+
+	ports, err := fwd.ListPorts(t.Context())
+	if err != nil {
+		t.Fatalf("ListPorts returned an error: %v", err)
+	}
+
+	if len(ports) != 1 {
+		t.Fatalf("expected 1 port, got %d", len(ports))
+	}
+
+	if ports[0].Protocol != string(tunnels.TunnelProtocolHttps) {
+		t.Fatalf("expected port protocol to be https, got %s", ports[0].Protocol)
+	}
+}
diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go
index d14b2f462..caf99df58 100644
--- a/pkg/cmd/issue/edit/edit_test.go
+++ b/pkg/cmd/issue/edit/edit_test.go
@@ -263,12 +263,12 @@ func TestNewCmdEdit(t *testing.T) {
 		},
 		{
 			name:  "argument is a URL",
-			input: "https://github.com/cli/cli/issues/23",
+			input: "https://example.com/cli/cli/issues/23",
 			output: EditOptions{
 				IssueNumbers: []int{23},
 				Interactive:  true,
 			},
-			expectedBaseRepo: ghrepo.New("cli", "cli"),
+			expectedBaseRepo: ghrepo.NewWithHost("cli", "cli", "example.com"),
 			wantsErr:         false,
 		},
 		{
diff --git a/pkg/cmd/pr/edit/edit_test.go b/pkg/cmd/pr/edit/edit_test.go
index 374625912..bb468a307 100644
--- a/pkg/cmd/pr/edit/edit_test.go
+++ b/pkg/cmd/pr/edit/edit_test.go
@@ -26,11 +26,12 @@ func TestNewCmdEdit(t *testing.T) {
 	require.NoError(t, err)
 
 	tests := []struct {
-		name     string
-		input    string
-		stdin    string
-		output   EditOptions
-		wantsErr bool
+		name             string
+		input            string
+		stdin            string
+		output           EditOptions
+		expectedBaseRepo ghrepo.Interface
+		wantsErr         bool
 	}{
 		{
 			name:  "no argument",
@@ -47,6 +48,16 @@ func TestNewCmdEdit(t *testing.T) {
 			output:   EditOptions{},
 			wantsErr: true,
 		},
+		{
+			name:  "URL argument",
+			input: "https://example.com/cli/cli/pull/23",
+			output: EditOptions{
+				SelectorArg: "https://example.com/cli/cli/pull/23",
+				Interactive: true,
+			},
+			expectedBaseRepo: ghrepo.NewWithHost("cli", "cli", "example.com"),
+			wantsErr:         false,
+		},
 		{
 			name:  "pull request number argument",
 			input: "23",
@@ -326,6 +337,15 @@ func TestNewCmdEdit(t *testing.T) {
 			assert.Equal(t, tt.output.SelectorArg, gotOpts.SelectorArg)
 			assert.Equal(t, tt.output.Interactive, gotOpts.Interactive)
 			assert.Equal(t, tt.output.Editable, gotOpts.Editable)
+			if tt.expectedBaseRepo != nil {
+				baseRepo, err := gotOpts.BaseRepo()
+				require.NoError(t, err)
+				require.True(
+					t,
+					ghrepo.IsSame(tt.expectedBaseRepo, baseRepo),
+					"expected base repo %+v, got %+v", tt.expectedBaseRepo, baseRepo,
+				)
+			}
 		})
 	}
 }
@@ -344,8 +364,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 				}, ghrepo.New("OWNER", "REPO")),
 				Interactive: false,
 				Editable: shared.Editable{
@@ -408,8 +427,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 				}, ghrepo.New("OWNER", "REPO")),
 				Interactive: false,
 				Editable: shared.Editable{
@@ -466,8 +484,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 				}, ghrepo.New("OWNER", "REPO")),
 				Interactive: false,
 				Editable: shared.Editable{
@@ -529,8 +546,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 				}, ghrepo.New("OWNER", "REPO")),
 				Interactive: true,
 				Surveyor: testSurveyor{
@@ -576,8 +592,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 				}, ghrepo.New("OWNER", "REPO")),
 				Interactive: true,
 				Surveyor: testSurveyor{
@@ -620,8 +635,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 				}, ghrepo.New("OWNER", "REPO")),
 				Interactive: true,
 				Surveyor: testSurveyor{
@@ -667,8 +681,7 @@ func Test_editRun(t *testing.T) {
 				Detector:    &fd.EnabledDetectorMock{},
 				SelectorArg: "123",
 				Finder: shared.NewMockFinder("123", &api.PullRequest{
-					URL:                "https://github.com/OWNER/REPO/pull/123",
-					AssignedActorsUsed: true,
+					URL: "https://github.com/OWNER/REPO/pull/123",
 					AssignedActors: api.AssignedActors{
 						Nodes: []api.Actor{
 							{
diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go
index 0fd96e09b..0f6da5a6e 100644
--- a/pkg/cmd/pr/shared/finder_test.go
+++ b/pkg/cmd/pr/shared/finder_test.go
@@ -9,12 +9,75 @@ import (
 
 	ghContext "github.com/cli/cli/v2/context"
 	"github.com/cli/cli/v2/git"
-	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/stretchr/testify/require"
 )
 
+func TestParseURL(t *testing.T) {
+	tests := []struct {
+		name     string
+		arg      string
+		wantRepo ghrepo.Interface
+		wantNum  int
+		wantErr  string
+	}{
+		{
+			name:     "valid HTTPS URL",
+			arg:      "https://example.com/owner/repo/pull/123",
+			wantRepo: ghrepo.NewWithHost("owner", "repo", "example.com"),
+			wantNum:  123,
+		},
+		{
+			name:     "valid HTTP URL",
+			arg:      "http://example.com/owner/repo/pull/123",
+			wantRepo: ghrepo.NewWithHost("owner", "repo", "example.com"),
+			wantNum:  123,
+		},
+		{
+			name:    "empty URL",
+			wantErr: "invalid URL: \"\"",
+		},
+		{
+			name:    "invalid scheme",
+			arg:     "ftp://github.com/owner/repo/pull/123",
+			wantErr: "invalid scheme: ftp",
+		},
+		{
+			name:    "incorrect path",
+			arg:     "https://github.com/owner/repo/issues/123",
+			wantErr: "not a pull request URL: https://github.com/owner/repo/issues/123",
+		},
+		{
+			name:    "no PR number",
+			arg:     "https://github.com/owner/repo/pull/",
+			wantErr: "not a pull request URL: https://github.com/owner/repo/pull/",
+		},
+		{
+			name:    "invalid PR number",
+			arg:     "https://github.com/owner/repo/pull/foo",
+			wantErr: "not a pull request URL: https://github.com/owner/repo/pull/foo",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			repo, num, err := ParseURL(tt.arg)
+
+			if tt.wantErr != "" {
+				require.Error(t, err)
+				require.Equal(t, tt.wantErr, err.Error())
+				return
+			}
+
+			require.NoError(t, err)
+			require.Equal(t, tt.wantNum, num)
+			require.NotNil(t, repo)
+			require.True(t, ghrepo.IsSame(tt.wantRepo, repo))
+		})
+	}
+}
+
 type args struct {
 	baseRepoFn      func() (ghrepo.Interface, error)
 	branchFn        func() (string, error)
@@ -706,81 +769,6 @@ func TestFind(t *testing.T) {
 	}
 }
 
-func TestFindAssignableActors(t *testing.T) {
-	t.Run("given actors are not assignable, do nothing special", func(t *testing.T) {
-		reg := &httpmock.Registry{}
-		defer reg.Verify(t)
-
-		// Ensure we never request assignedActors
-		reg.Exclude(t, httpmock.GraphQL(`assignedActors`))
-		reg.Register(
-			httpmock.GraphQL(`query PullRequestByNumber\b`),
-			httpmock.StringResponse(`{"data":{"repository":{
-						"pullRequest":{"number":13}
-					}}}`))
-
-		f := finder{
-			httpClient: func() (*http.Client, error) {
-				return &http.Client{Transport: reg}, nil
-			},
-		}
-
-		pr, _, err := f.Find(FindOptions{
-			Detector: &fd.DisabledDetectorMock{},
-			Fields:   []string{"assignees"},
-			Selector: "https://github.com/cli/cli/pull/13",
-		})
-		require.NoError(t, err)
-
-		require.False(t, pr.AssignedActorsUsed, "expected PR not to have assigned actors used")
-	})
-
-	t.Run("given actors are assignable, request assignedActors and indicate that on the returned PR", func(t *testing.T) {
-		reg := &httpmock.Registry{}
-		defer reg.Verify(t)
-
-		// Ensure that we only respond if assignedActors is requested
-		reg.Register(
-			httpmock.GraphQL(`assignedActors`),
-			httpmock.StringResponse(`{"data":{"repository":{
-						"pullRequest":{
-						    "number":13, 
-							"assignedActors": {
-								"nodes": [
-									{
-										"id": "HUBOTID",
-										"login": "hubot",
-										"__typename": "Bot"
-									},
-									{
-										"id": "MONAID",
-										"login": "MonaLisa",
-										"name": "Mona Display Name",
-										"__typename": "User"
-									}
-								],
-								"totalCount": 2
-							}}
-					}}}`))
-
-		f := finder{
-			httpClient: func() (*http.Client, error) {
-				return &http.Client{Transport: reg}, nil
-			},
-		}
-
-		pr, _, err := f.Find(FindOptions{
-			Detector: &fd.EnabledDetectorMock{},
-			Fields:   []string{"assignees"},
-			Selector: "https://github.com/cli/cli/pull/13",
-		})
-		require.NoError(t, err)
-
-		require.Equal(t, []string{"hubot", "MonaLisa"}, pr.AssignedActors.Logins())
-		require.True(t, pr.AssignedActorsUsed, "expected PR to have assigned actors used")
-	})
-}
-
 func stubBranchConfig(branchConfig git.BranchConfig, err error) func(context.Context, string) (git.BranchConfig, error) {
 	return func(_ context.Context, branch string) (git.BranchConfig, error) {
 		return branchConfig, err