From 65c7ebc79eec996dfa29f02bc5ebe6cf20600b71 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 28 Jul 2025 17:16:56 -0400 Subject: [PATCH 1/3] v1 project feature detection spike using version These changes are demonstrating how `gh` commands that support v1 classic projects can determine if support exists by checking the GHES server version. --- .../featuredetection/feature_detection.go | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index a2f34a60b..702b9619f 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -5,6 +5,7 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/gh" + "github.com/hashicorp/go-version" "golang.org/x/sync/errgroup" ghauth "github.com/cli/go-gh/v2/pkg/auth" @@ -205,12 +206,37 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) { return features, nil } +const ( + enterpriseProjectsV1Removed = "3.17.0" +) + func (d *detector) ProjectsV1() gh.ProjectsV1Support { // Currently, projects v1 support is entirely dependent on the host. As this is deprecated in GHES, // we will do feature detection on whether the GHES version has support. - if ghauth.IsEnterprise(d.host) { + if !ghauth.IsEnterprise(d.host) { + return gh.ProjectsV1Unsupported + } + + hostVersion, hostVersionErr := resolveEnterpriseVersion(d.httpClient, d.host) + v1ProjectCutoffVersion, v1ProjectCutoffVersionErr := version.NewVersion(enterpriseProjectsV1Removed) + + if hostVersionErr == nil && v1ProjectCutoffVersionErr == nil && hostVersion.LessThan(v1ProjectCutoffVersion) { return gh.ProjectsV1Supported } return gh.ProjectsV1Unsupported } + +func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) { + var metaResponse struct { + InstalledVersion string `json:"installed_version"` + } + + apiClient := api.NewClientFromHTTP(httpClient) + err := apiClient.REST(host, "GET", "meta", nil, &metaResponse) + if err != nil { + return nil, err + } + + return version.NewVersion(metaResponse.InstalledVersion) +} From 3bafd883b5c6c35eb70136d11e6d65523a59bda3 Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Mon, 11 Aug 2025 14:50:07 -0400 Subject: [PATCH 2/3] Update v1 project detection logic This change updates the v1 project detection logic to retrieve the API version of GHES host to determine support. --- .../feature_detection_test.go | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index 2c7d19071..e850546a7 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -373,17 +373,69 @@ func TestRepositoryFeatures(t *testing.T) { } func TestProjectV1Support(t *testing.T) { - t.Parallel() + tests := []struct { + name string + hostname string + httpStubs func(*httpmock.Registry) + wantFeatures gh.ProjectsV1Support + }{ + { + name: "github.com", + hostname: "github.com", + wantFeatures: gh.ProjectsV1Unsupported, + }, + { + name: "ghec data residency (ghe.com)", + hostname: "stampname.ghe.com", + wantFeatures: gh.ProjectsV1Unsupported, + }, + { + name: "GHE 3.16.0", + hostname: "git.my.org", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "api/v3/meta"), + httpmock.StringResponse(`{"installed_version":"3.16.0"}`), + ) + }, + wantFeatures: gh.ProjectsV1Supported, + }, + { + name: "GHE 3.16.1", + hostname: "git.my.org", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "api/v3/meta"), + httpmock.StringResponse(`{"installed_version":"3.16.1"}`), + ) + }, + wantFeatures: gh.ProjectsV1Supported, + }, + { + name: "GHE 3.17", + hostname: "git.my.org", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", "api/v3/meta"), + httpmock.StringResponse(`{"installed_version":"3.17.0"}`), + ) + }, + wantFeatures: gh.ProjectsV1Unsupported, + }, + } - t.Run("when the host is enterprise, project v1 is supported", func(t *testing.T) { - detector := detector{host: "my.ghes.com"} - isProjectV1Supported := detector.ProjectsV1() - require.Equal(t, gh.ProjectsV1Supported, isProjectV1Supported) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reg := &httpmock.Registry{} + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + httpClient := &http.Client{} + httpmock.ReplaceTripper(httpClient, reg) - t.Run("when the host is not enterprise, project v1 is not supported", func(t *testing.T) { - detector := detector{host: "github.com"} - isProjectV1Supported := detector.ProjectsV1() - require.Equal(t, gh.ProjectsV1Unsupported, isProjectV1Supported) - }) + detector := NewDetector(httpClient, tt.hostname) + require.Equal(t, tt.wantFeatures, detector.ProjectsV1()) + }) + } } From d10251211c66a592c671b2471682b58b1d76d3dc Mon Sep 17 00:00:00 2001 From: Andy Feller Date: Tue, 12 Aug 2025 21:18:47 -0400 Subject: [PATCH 3/3] Update feature_detection.go --- internal/featuredetection/feature_detection.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 702b9619f..c61f47aeb 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -211,8 +211,6 @@ const ( ) func (d *detector) ProjectsV1() gh.ProjectsV1Support { - // Currently, projects v1 support is entirely dependent on the host. As this is deprecated in GHES, - // we will do feature detection on whether the GHES version has support. if !ghauth.IsEnterprise(d.host) { return gh.ProjectsV1Unsupported }