Skip to content

Commit 9ae764b

Browse files
authored
Merge pull request cli#11514 from cli/github-cli-937-v1-project-ghes-deprecation
Ensure users can see v2 projects when viewing issues and PRs, avoid v1 projects on GHES 3.17 and newer
2 parents 5ae174b + 4a2abf7 commit 9ae764b

9 files changed

Lines changed: 268 additions & 62 deletions

File tree

api/queries_issue.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ type ProjectCards struct {
166166
}
167167

168168
type ProjectItems struct {
169-
Nodes []*ProjectV2Item
169+
Nodes []*ProjectV2Item
170+
TotalCount int
170171
}
171172

172173
type ProjectInfo struct {

api/queries_projects_v2.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ func ProjectsV2ItemsForIssue(client *Client, repo ghrepo.Interface, issue *Issue
8282
Repository struct {
8383
Issue struct {
8484
ProjectItems struct {
85-
Nodes []*projectV2Item
86-
PageInfo struct {
85+
TotalCount int
86+
Nodes []*projectV2Item
87+
PageInfo struct {
8788
HasNextPage bool
8889
EndCursor string
8990
}
@@ -149,8 +150,9 @@ func ProjectsV2ItemsForPullRequest(client *Client, repo ghrepo.Interface, pr *Pu
149150
Repository struct {
150151
PullRequest struct {
151152
ProjectItems struct {
152-
Nodes []*projectV2Item
153-
PageInfo struct {
153+
TotalCount int
154+
Nodes []*projectV2Item
155+
PageInfo struct {
154156
HasNextPage bool
155157
EndCursor string
156158
}

internal/featuredetection/feature_detection.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/cli/cli/v2/api"
77
"github.com/cli/cli/v2/internal/gh"
8+
"github.com/hashicorp/go-version"
89
"golang.org/x/sync/errgroup"
910

1011
ghauth "github.com/cli/go-gh/v2/pkg/auth"
@@ -205,12 +206,35 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
205206
return features, nil
206207
}
207208

209+
const (
210+
enterpriseProjectsV1Removed = "3.17.0"
211+
)
212+
208213
func (d *detector) ProjectsV1() gh.ProjectsV1Support {
209-
// Currently, projects v1 support is entirely dependent on the host. As this is deprecated in GHES,
210-
// we will do feature detection on whether the GHES version has support.
211-
if ghauth.IsEnterprise(d.host) {
214+
if !ghauth.IsEnterprise(d.host) {
215+
return gh.ProjectsV1Unsupported
216+
}
217+
218+
hostVersion, hostVersionErr := resolveEnterpriseVersion(d.httpClient, d.host)
219+
v1ProjectCutoffVersion, v1ProjectCutoffVersionErr := version.NewVersion(enterpriseProjectsV1Removed)
220+
221+
if hostVersionErr == nil && v1ProjectCutoffVersionErr == nil && hostVersion.LessThan(v1ProjectCutoffVersion) {
212222
return gh.ProjectsV1Supported
213223
}
214224

215225
return gh.ProjectsV1Unsupported
216226
}
227+
228+
func resolveEnterpriseVersion(httpClient *http.Client, host string) (*version.Version, error) {
229+
var metaResponse struct {
230+
InstalledVersion string `json:"installed_version"`
231+
}
232+
233+
apiClient := api.NewClientFromHTTP(httpClient)
234+
err := apiClient.REST(host, "GET", "meta", nil, &metaResponse)
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
return version.NewVersion(metaResponse.InstalledVersion)
240+
}

internal/featuredetection/feature_detection_test.go

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -373,17 +373,69 @@ func TestRepositoryFeatures(t *testing.T) {
373373
}
374374

375375
func TestProjectV1Support(t *testing.T) {
376-
t.Parallel()
376+
tests := []struct {
377+
name string
378+
hostname string
379+
httpStubs func(*httpmock.Registry)
380+
wantFeatures gh.ProjectsV1Support
381+
}{
382+
{
383+
name: "github.com",
384+
hostname: "github.com",
385+
wantFeatures: gh.ProjectsV1Unsupported,
386+
},
387+
{
388+
name: "ghec data residency (ghe.com)",
389+
hostname: "stampname.ghe.com",
390+
wantFeatures: gh.ProjectsV1Unsupported,
391+
},
392+
{
393+
name: "GHE 3.16.0",
394+
hostname: "git.my.org",
395+
httpStubs: func(reg *httpmock.Registry) {
396+
reg.Register(
397+
httpmock.REST("GET", "api/v3/meta"),
398+
httpmock.StringResponse(`{"installed_version":"3.16.0"}`),
399+
)
400+
},
401+
wantFeatures: gh.ProjectsV1Supported,
402+
},
403+
{
404+
name: "GHE 3.16.1",
405+
hostname: "git.my.org",
406+
httpStubs: func(reg *httpmock.Registry) {
407+
reg.Register(
408+
httpmock.REST("GET", "api/v3/meta"),
409+
httpmock.StringResponse(`{"installed_version":"3.16.1"}`),
410+
)
411+
},
412+
wantFeatures: gh.ProjectsV1Supported,
413+
},
414+
{
415+
name: "GHE 3.17",
416+
hostname: "git.my.org",
417+
httpStubs: func(reg *httpmock.Registry) {
418+
reg.Register(
419+
httpmock.REST("GET", "api/v3/meta"),
420+
httpmock.StringResponse(`{"installed_version":"3.17.0"}`),
421+
)
422+
},
423+
wantFeatures: gh.ProjectsV1Unsupported,
424+
},
425+
}
377426

378-
t.Run("when the host is enterprise, project v1 is supported", func(t *testing.T) {
379-
detector := detector{host: "my.ghes.com"}
380-
isProjectV1Supported := detector.ProjectsV1()
381-
require.Equal(t, gh.ProjectsV1Supported, isProjectV1Supported)
382-
})
427+
for _, tt := range tests {
428+
t.Run(tt.name, func(t *testing.T) {
429+
t.Parallel()
430+
reg := &httpmock.Registry{}
431+
if tt.httpStubs != nil {
432+
tt.httpStubs(reg)
433+
}
434+
httpClient := &http.Client{}
435+
httpmock.ReplaceTripper(httpClient, reg)
383436

384-
t.Run("when the host is not enterprise, project v1 is not supported", func(t *testing.T) {
385-
detector := detector{host: "github.com"}
386-
isProjectV1Supported := detector.ProjectsV1()
387-
require.Equal(t, gh.ProjectsV1Unsupported, isProjectV1Supported)
388-
})
437+
detector := NewDetector(httpClient, tt.hostname)
438+
require.Equal(t, tt.wantFeatures, detector.ProjectsV1())
439+
})
440+
}
389441
}

pkg/cmd/issue/view/view.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ func viewRun(opts *ViewOptions) error {
124124
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
125125
}
126126

127+
lookupFields.Add("projectItems")
127128
projectsV1Support := opts.Detector.ProjectsV1()
128129
if projectsV1Support == gh.ProjectsV1Supported {
129130
lookupFields.Add("projectCards")
@@ -310,11 +311,24 @@ func issueAssigneeList(issue api.Issue) string {
310311
}
311312

312313
func issueProjectList(issue api.Issue) string {
313-
if len(issue.ProjectCards.Nodes) == 0 {
314+
totalCount := issue.ProjectCards.TotalCount + issue.ProjectItems.TotalCount
315+
count := len(issue.ProjectCards.Nodes) + len(issue.ProjectItems.Nodes)
316+
317+
if count == 0 {
314318
return ""
315319
}
316320

317-
projectNames := make([]string, 0, len(issue.ProjectCards.Nodes))
321+
projectNames := make([]string, 0, count)
322+
323+
for _, project := range issue.ProjectItems.Nodes {
324+
colName := project.Status.Name
325+
if colName == "" {
326+
colName = "No Status"
327+
}
328+
projectNames = append(projectNames, fmt.Sprintf("%s (%s)", project.Project.Title, colName))
329+
}
330+
331+
// TODO: Remove v1 classic project logic when completely deprecated
318332
for _, project := range issue.ProjectCards.Nodes {
319333
colName := project.Column.Name
320334
if colName == "" {
@@ -324,7 +338,7 @@ func issueProjectList(issue api.Issue) string {
324338
}
325339

326340
list := strings.Join(projectNames, ", ")
327-
if issue.ProjectCards.TotalCount > len(issue.ProjectCards.Nodes) {
341+
if totalCount > count {
328342
list += ", …"
329343
}
330344
return list

0 commit comments

Comments
 (0)