fix(featuredetection): add ReleaseFeatures method

Signed-off-by: Babak K. Shandiz <babakks@github.com>
This commit is contained in:
Babak K. Shandiz 2025-10-31 14:12:10 +00:00
parent 20c7bdc2a4
commit 26552f3489
No known key found for this signature in database
GPG key ID: 9472CAEFF56C742E
3 changed files with 156 additions and 0 deletions

View file

@ -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

View file

@ -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"`

View file

@ -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)
})
}
}