From 26552f34890946ca927efc1943170cc9a1c2b948 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Fri, 31 Oct 2025 14:12:10 +0000 Subject: [PATCH] fix(featuredetection): add `ReleaseFeatures` method Signed-off-by: Babak K. Shandiz --- internal/featuredetection/detector_mock.go | 10 ++ .../featuredetection/feature_detection.go | 35 ++++++ .../feature_detection_test.go | 111 ++++++++++++++++++ 3 files changed, 156 insertions(+) diff --git a/internal/featuredetection/detector_mock.go b/internal/featuredetection/detector_mock.go index 2d41c707f..b0ca81f40 100644 --- a/internal/featuredetection/detector_mock.go +++ b/internal/featuredetection/detector_mock.go @@ -24,6 +24,10 @@ func (md *DisabledDetectorMock) SearchFeatures() (SearchFeatures, error) { return advancedIssueSearchNotSupported, nil } +func (md *DisabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) { + return ReleaseFeatures{}, nil +} + type EnabledDetectorMock struct{} func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) { @@ -46,6 +50,12 @@ func (md *EnabledDetectorMock) SearchFeatures() (SearchFeatures, error) { return advancedIssueSearchNotSupported, nil } +func (md *EnabledDetectorMock) ReleaseFeatures() (ReleaseFeatures, error) { + return ReleaseFeatures{ + ImmutableReleases: true, + }, nil +} + type AdvancedIssueSearchDetectorMock struct { EnabledDetectorMock searchFeatures SearchFeatures diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go index 786093d93..2aff20d98 100644 --- a/internal/featuredetection/feature_detection.go +++ b/internal/featuredetection/feature_detection.go @@ -17,6 +17,7 @@ type Detector interface { RepositoryFeatures() (RepositoryFeatures, error) ProjectsV1() gh.ProjectsV1Support SearchFeatures() (SearchFeatures, error) + ReleaseFeatures() (ReleaseFeatures, error) } type IssueFeatures struct { @@ -93,6 +94,10 @@ var advancedIssueSearchSupportedAsOnlyBackend = SearchFeatures{ AdvancedIssueSearchAPIOptIn: false, } +type ReleaseFeatures struct { + ImmutableReleases bool +} + type detector struct { host string httpClient *http.Client @@ -358,6 +363,36 @@ func (d *detector) SearchFeatures() (SearchFeatures, error) { return feature, nil } +func (d *detector) ReleaseFeatures() (ReleaseFeatures, error) { + // TODO: immutableReleaseFullSupport + // Once all supported GHES versions fully support immutable releases, we can + // remove this function, of course, unless there will be other release-related + // features that are not available on all GH hosts. + + var releaseFeatureDetection struct { + Release struct { + Fields []struct { + Name string + } `graphql:"fields"` + } `graphql:"Release: __type(name: \"Release\")"` + } + + gql := api.NewClientFromHTTP(d.httpClient) + if err := gql.Query(d.host, "Release_fields", &releaseFeatureDetection, nil); err != nil { + return ReleaseFeatures{}, err + } + + for _, field := range releaseFeatureDetection.Release.Fields { + if field.Name == "immutable" { + return ReleaseFeatures{ + ImmutableReleases: true, + }, nil + } + } + + return ReleaseFeatures{}, nil +} + func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) { var metaResponse struct { InstalledVersion string `json:"installed_version"` diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go index 18a846268..d3ee1a7e9 100644 --- a/internal/featuredetection/feature_detection_test.go +++ b/internal/featuredetection/feature_detection_test.go @@ -585,3 +585,114 @@ func TestAdvancedIssueSearchSupport(t *testing.T) { }) } } + +func TestReleaseFeatures(t *testing.T) { + withImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"},{"name":"immutable"}]}}}` + withoutImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"}]}}}` + + tests := []struct { + name string + hostname string + httpStubs func(*httpmock.Registry) + wantFeatures ReleaseFeatures + }{ + { + // This is not a real case as `github.com` supports immutable releases. + name: "github.com, immutable releases unsupported", + hostname: "github.com", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query Release_fields\b`), + httpmock.StringResponse(withoutImmutableReleaseSupport), + ) + }, + wantFeatures: ReleaseFeatures{ + ImmutableReleases: false, + }, + }, + { + name: "github.com, immutable releases supported", + hostname: "github.com", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query Release_fields\b`), + httpmock.StringResponse(withImmutableReleaseSupport), + ) + }, + wantFeatures: ReleaseFeatures{ + ImmutableReleases: true, + }, + }, + { + // This is not a real case as `github.com` supports immutable releases. + name: "ghec data residency (ghe.com), immutable releases unsupported", + hostname: "stampname.ghe.com", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query Release_fields\b`), + httpmock.StringResponse(withoutImmutableReleaseSupport), + ) + }, + wantFeatures: ReleaseFeatures{ + ImmutableReleases: false, + }, + }, + { + name: "ghec data residency (ghe.com), immutable releases supported", + hostname: "stampname.ghe.com", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query Release_fields\b`), + httpmock.StringResponse(withImmutableReleaseSupport), + ) + }, + wantFeatures: ReleaseFeatures{ + ImmutableReleases: true, + }, + }, + { + name: "GHE, immutable releases unsupported", + hostname: "git.my.org", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query Release_fields\b`), + httpmock.StringResponse(withoutImmutableReleaseSupport), + ) + }, + wantFeatures: ReleaseFeatures{ + ImmutableReleases: false, + }, + }, + { + name: "GHE, immutable releases supported", + hostname: "git.my.org", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query Release_fields\b`), + httpmock.StringResponse(withImmutableReleaseSupport), + ) + }, + wantFeatures: ReleaseFeatures{ + ImmutableReleases: true, + }, + }, + } + + 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) + + detector := NewDetector(httpClient, tt.hostname) + + features, err := detector.ReleaseFeatures() + require.NoError(t, err) + require.Equal(t, tt.wantFeatures, features) + }) + } +}