-
Notifications
You must be signed in to change notification settings - Fork 8.7k
Expand file tree
/
Copy pathfeature_detection.go
More file actions
517 lines (433 loc) · 15.4 KB
/
Copy pathfeature_detection.go
File metadata and controls
517 lines (433 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
package featuredetection
import (
"net/http"
"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"
)
type Detector interface {
IssueFeatures() (IssueFeatures, error)
PullRequestFeatures() (PullRequestFeatures, error)
RepositoryFeatures() (RepositoryFeatures, error)
ProjectsV1() gh.ProjectsV1Support
ProjectFeatures() (ProjectFeatures, error)
SearchFeatures() (SearchFeatures, error)
ReleaseFeatures() (ReleaseFeatures, error)
ActionsFeatures() (ActionsFeatures, error)
}
type IssueFeatures struct {
StateReason bool
ActorIsAssignable bool
}
var allIssueFeatures = IssueFeatures{
StateReason: true,
ActorIsAssignable: true,
}
type PullRequestFeatures struct {
MergeQueue bool
// CheckRunAndStatusContextCounts indicates whether the API supports
// the checkRunCount, checkRunCountsByState, statusContextCount and statusContextCountsByState
// fields on the StatusCheckRollupContextConnection
CheckRunAndStatusContextCounts bool
CheckRunEvent bool
}
var allPullRequestFeatures = PullRequestFeatures{
MergeQueue: true,
CheckRunAndStatusContextCounts: true,
CheckRunEvent: true,
}
type RepositoryFeatures struct {
PullRequestTemplateQuery bool
VisibilityField bool
AutoMerge bool
}
var allRepositoryFeatures = RepositoryFeatures{
PullRequestTemplateQuery: true,
VisibilityField: true,
AutoMerge: true,
}
type ProjectFeatures struct {
// ProjectItemQuery indicates support for the `query` argument on
// ProjectV2.items (supported on github.com and GHES 3.20+).
ProjectItemQuery bool
}
var allProjectFeatures = ProjectFeatures{
ProjectItemQuery: true,
}
type SearchFeatures struct {
// AdvancedIssueSearch indicates whether the host supports advanced issue
// search via API calls.
AdvancedIssueSearchAPI bool
// AdvancedIssueSearchOptIn indicates whether the host supports advanced
// issue search as an opt-in feature, which has to be explicitly enabled in
// API calls.
AdvancedIssueSearchAPIOptIn bool
// TODO advancedSearchFuture
// When advanced issue search is supported in Pull Requests tab, or in
// global search we can introduce more fields to reflect the support status.
}
// advancedIssueSearchNotSupported mimics GHE <3.18 where advanced issue search
// is either not supported or is not meant to be used due to not being stable
// enough (i.e. in preview).
var advancedIssueSearchNotSupported = SearchFeatures{
AdvancedIssueSearchAPI: false,
}
// advancedIssueSearchSupportedAsOptIn mimics github.com and GHE >=3.18 before
// the full cleanup of temp types (i.e. ISSUE_ADVANCED search type is still
// present on the schema).
var advancedIssueSearchSupportedAsOptIn = SearchFeatures{
AdvancedIssueSearchAPI: true,
AdvancedIssueSearchAPIOptIn: true,
}
// advancedIssueSearchSupportedAsOnlyBackend mimics github.com and GHE >=3.18
// after the full cleanup of temp types (i.e. ISSUE_ADVANCED search type is
// removed from the schema).
var advancedIssueSearchSupportedAsOnlyBackend = SearchFeatures{
AdvancedIssueSearchAPI: true,
AdvancedIssueSearchAPIOptIn: false,
}
type ReleaseFeatures struct {
ImmutableReleases bool
}
type ActionsFeatures struct {
// DispatchRunDetails indicates whether the API supports the `return_run_details`
// field in workflow dispatches that, when set to true, will return the details
// of the created workflow run in the response (with status code 200).
//
// On older API versions (e.g. GHES 3.20 or earlier), this new field is not
// supported and setting it will cause an error.
DispatchRunDetails bool
}
type detector struct {
host string
httpClient *http.Client
}
func NewDetector(httpClient *http.Client, host string) Detector {
return &detector{
httpClient: httpClient,
host: host,
}
}
func (d *detector) IssueFeatures() (IssueFeatures, error) {
if !ghauth.IsEnterprise(d.host) {
return allIssueFeatures, nil
}
features := IssueFeatures{
StateReason: false,
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
}
var featureDetection struct {
Issue struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Issue: __type(name: \"Issue\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
err := gql.Query(d.host, "Issue_fields", &featureDetection, nil)
if err != nil {
return features, err
}
for _, field := range featureDetection.Issue.Fields {
if field.Name == "stateReason" {
features.StateReason = true
}
}
return features, nil
}
func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {
// TODO: reinstate the short-circuit once the APIs are fully available on github.com
// https://github.com/cli/cli/issues/5778
//
// if !ghinstance.IsEnterprise(d.host) {
// return allPullRequestFeatures, nil
// }
var pullRequestFeatureDetection struct {
PullRequest struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"PullRequest: __type(name: \"PullRequest\")"`
StatusCheckRollupContextConnection struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"StatusCheckRollupContextConnection: __type(name: \"StatusCheckRollupContextConnection\")"`
}
// Break feature detection down into two separate queries because the platform
// only supports two `__type` expressions in one query.
var pullRequestFeatureDetection2 struct {
WorkflowRun struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"WorkflowRun: __type(name: \"WorkflowRun\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
var wg errgroup.Group
wg.Go(func() error {
return gql.Query(d.host, "PullRequest_fields", &pullRequestFeatureDetection, nil)
})
wg.Go(func() error {
return gql.Query(d.host, "PullRequest_fields2", &pullRequestFeatureDetection2, nil)
})
if err := wg.Wait(); err != nil {
return PullRequestFeatures{}, err
}
features := PullRequestFeatures{}
for _, field := range pullRequestFeatureDetection.PullRequest.Fields {
if field.Name == "isInMergeQueue" {
features.MergeQueue = true
}
}
for _, field := range pullRequestFeatureDetection.StatusCheckRollupContextConnection.Fields {
// We only check for checkRunCount here but it, checkRunCountsByState, statusContextCount and statusContextCountsByState
// were all introduced in the same version of the API.
if field.Name == "checkRunCount" {
features.CheckRunAndStatusContextCounts = true
}
}
for _, field := range pullRequestFeatureDetection2.WorkflowRun.Fields {
if field.Name == "event" {
features.CheckRunEvent = true
}
}
return features, nil
}
func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
if !ghauth.IsEnterprise(d.host) {
return allRepositoryFeatures, nil
}
features := RepositoryFeatures{}
var featureDetection struct {
Repository struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Repository: __type(name: \"Repository\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
err := gql.Query(d.host, "Repository_fields", &featureDetection, nil)
if err != nil {
return features, err
}
for _, field := range featureDetection.Repository.Fields {
if field.Name == "pullRequestTemplates" {
features.PullRequestTemplateQuery = true
}
if field.Name == "visibility" {
features.VisibilityField = true
}
if field.Name == "autoMergeAllowed" {
features.AutoMerge = true
}
}
return features, nil
}
const (
enterpriseProjectsV1Removed = "3.17.0"
)
func (d *detector) ProjectsV1() gh.ProjectsV1Support {
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 (d *detector) ProjectFeatures() (ProjectFeatures, error) {
if !ghauth.IsEnterprise(d.host) {
return allProjectFeatures, nil
}
var features ProjectFeatures
var featureDetection struct {
ProjectV2 struct {
Fields []struct {
Name string
Args []struct {
Name string
}
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"ProjectV2: __type(name: \"ProjectV2\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
err := gql.Query(d.host, "ProjectV2_fields", &featureDetection, nil)
if err != nil {
return features, err
}
for _, field := range featureDetection.ProjectV2.Fields {
if field.Name == "items" {
for _, arg := range field.Args {
if arg.Name == "query" {
features.ProjectItemQuery = true
break
}
}
break
}
}
return features, nil
}
const (
// enterpriseAdvancedIssueSearchSupport is the minimum version of GHES that
// supports advanced issue search and gh should use it.
//
// Note that advanced issue search is also available on GHES 3.17, but it's
// at the preview stage and is not as mature as it is on github.com or later
// GHES version.
enterpriseAdvancedIssueSearchSupport = "3.18.0"
)
func (d *detector) SearchFeatures() (SearchFeatures, error) {
// TODO advancedIssueSearchCleanup
// Once GHES 3.17 support ends, we don't need this and, probably, the entire search feature detection.
// Regarding the release of advanced issue search (AIS, for short), there
// are three time spans/periods:
//
// 1. Pre-deprecation: where both legacy search and AIS are available
// - GraphQL: `ISSUE` and `ISSUE_ADVANCED` search types in GraphQL behave differently
// - REST: `advance_search=true` query parameter can be used to switch to AIS
// 2. Deprecation: only AIS available
// - GraphQL: `ISSUE` and `ISSUE_ADVANCED` search types in GraphQL behave the same (AIS)
// - REST: `advance_search` query parameter has no effect (AIS)
// 3. Cleanup: only AIS available
// - GraphQL: `ISSUE` search type in GraphQL is the only available option (AIS)
// - REST: `advance_search` query parameter has no effect (AIS)
//
// Since there's no schema-wise difference between pre-deprecation and
// deprecation periods (i.e. `ISSUE_ADVANCED` is available during both),
// we cannot figure out the exact time period. The consensus is to to use
// the advanced search syntax during both periods.
var feature SearchFeatures
if ghauth.IsEnterprise(d.host) {
enterpriseAISSupportVersion, err := version.NewVersion(enterpriseAdvancedIssueSearchSupport)
if err != nil {
return SearchFeatures{}, err
}
hostVersion, err := resolveEnterpriseVersion(d.httpClient, d.host)
if err != nil {
return SearchFeatures{}, err
}
if hostVersion.GreaterThanOrEqual(enterpriseAISSupportVersion) {
// As of August 2025, advanced issue search is going to be available
// on GHES 3.18+, including Issues tabs in repositories.
feature.AdvancedIssueSearchAPI = true
// TODO advancedSearchFuture
// When the advanced search syntax is supported in global search or
// Pull Requests tabs (in repositories), we can add and enable the
// corresponding fields.
}
} else {
// As of August 2025, advanced issue search is available on github.com,
// including Issues tabs in repositories.
feature.AdvancedIssueSearchAPI = true
// TODO advancedSearchFuture
// When the advanced search syntax is supported in global search or
// Pull Requests tabs (in repositories), we can add and enable the
// corresponding fields.
}
if !feature.AdvancedIssueSearchAPI {
return feature, nil
}
var searchTypeFeatureDetection struct {
SearchType struct {
EnumValues []struct {
Name string
} `graphql:"enumValues(includeDeprecated: true)"`
} `graphql:"SearchType: __type(name: \"SearchType\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
if err := gql.Query(d.host, "SearchType_enumValues", &searchTypeFeatureDetection, nil); err != nil {
return SearchFeatures{}, err
}
for _, enumValue := range searchTypeFeatureDetection.SearchType.EnumValues {
if enumValue.Name == "ISSUE_ADVANCED" {
// As long as ISSUE_ADVANCED is present on the schema, we should
// explicitly opt-in when making API calls.
feature.AdvancedIssueSearchAPIOptIn = true
break
}
}
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
}
const (
enterpriseWorkflowDispatchRunDetailsSupport = "3.21.0"
)
func (d *detector) ActionsFeatures() (ActionsFeatures, error) {
// TODO workflowDispatchRunDetailsCleanup
// Once GHES 3.20 support ends, we don't need feature detection for workflow dispatch (i.e. run details support).
//
// On github.com, workflow dispatch API now supports a new field named `return_run_details` that enabling it will
// result in a 200 OK response with the details of the created workflow run. If not set (or set to false), the API
// will keep the old behavior of returning a 204 No Content response.
//
// On GHES (current latest at 3.20), this new field is not available, and setting it will cause a 400 response.
//
// Once GHES 3.20 support ends, we can remove the feature detection and start using the new field in API calls.
//
// IMPORTANT: In the future REST API versions (i.e. breaking changes), the workflow dispatch endpoint is going to
// always return the details of the created workflow run in the response, and the `return_run_details` field is
// going to be ignored/removed. So, once we are migrating to the new API version we should double check the status
// of the API.
if !ghauth.IsEnterprise(d.host) {
return ActionsFeatures{
DispatchRunDetails: true,
}, nil
}
minSupportedVersion, err := version.NewVersion(enterpriseWorkflowDispatchRunDetailsSupport)
if err != nil {
return ActionsFeatures{}, err
}
hostVersion, err := resolveEnterpriseVersion(d.httpClient, d.host)
if err != nil {
return ActionsFeatures{}, err
}
if hostVersion.GreaterThanOrEqual(minSupportedVersion) {
return ActionsFeatures{
DispatchRunDetails: true,
}, nil
}
return ActionsFeatures{
DispatchRunDetails: false,
}, nil
}
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)
}