Fix project mutation query variable usage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
William Martin 2026-02-23 07:04:33 -07:00
parent 6c3e39ffc4
commit 4dd50cf60c
8 changed files with 122 additions and 13 deletions

View file

@ -30,7 +30,7 @@ type closeConfig struct {
// the close command relies on the updateProjectV2 mutation
type updateProjectMutation struct {
UpdateProjectV2 struct {
ProjectV2 queries.Project `graphql:"projectV2"`
ProjectV2 queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"updateProjectV2(input:$input)"`
}
@ -123,7 +123,7 @@ func closeArgs(config closeConfig) (*updateProjectMutation, map[string]interface
}
}
func printResults(config closeConfig, project queries.Project) error {
func printResults(config closeConfig, project queries.ProjectMutationQuery) error {
if !config.io.IsStdoutTTY() {
return nil
}

View file

@ -32,7 +32,7 @@ type copyConfig struct {
type copyProjectMutation struct {
CopyProjectV2 struct {
ProjectV2 queries.Project `graphql:"projectV2"`
ProjectV2 queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"copyProjectV2(input:$input)"`
}
@ -134,7 +134,7 @@ func copyArgs(config copyConfig) (*copyProjectMutation, map[string]interface{})
}
}
func printResults(config copyConfig, project queries.Project) error {
func printResults(config copyConfig, project queries.ProjectMutationQuery) error {
if !config.io.IsStdoutTTY() {
return nil
}

View file

@ -27,7 +27,7 @@ type createConfig struct {
type createProjectMutation struct {
CreateProjectV2 struct {
ProjectV2 queries.Project `graphql:"projectV2"`
ProjectV2 queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"createProjectV2(input:$input)"`
}
@ -104,7 +104,7 @@ func createArgs(config createConfig) (*createProjectMutation, map[string]interfa
}
}
func printResults(config createConfig, project queries.Project) error {
func printResults(config createConfig, project queries.ProjectMutationQuery) error {
if !config.io.IsStdoutTTY() {
return nil
}

View file

@ -28,7 +28,7 @@ type deleteConfig struct {
type deleteProjectMutation struct {
DeleteProject struct {
Project queries.Project `graphql:"projectV2"`
Project queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"deleteProjectV2(input:$input)"`
}
@ -115,7 +115,7 @@ func deleteItemArgs(config deleteConfig) (*deleteProjectMutation, map[string]int
}
}
func printResults(config deleteConfig, project queries.Project) error {
func printResults(config deleteConfig, project queries.ProjectMutationQuery) error {
if !config.io.IsStdoutTTY() {
return nil
}

View file

@ -32,7 +32,7 @@ type editConfig struct {
type updateProjectMutation struct {
UpdateProjectV2 struct {
ProjectV2 queries.Project `graphql:"projectV2"`
ProjectV2 queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"updateProjectV2(input:$input)"`
}
@ -144,7 +144,7 @@ func editArgs(config editConfig) (*updateProjectMutation, map[string]interface{}
}
}
func printResults(config editConfig, project queries.Project) error {
func printResults(config editConfig, project queries.ProjectMutationQuery) error {
if !config.io.IsStdoutTTY() {
return nil
}

View file

@ -29,12 +29,12 @@ type markTemplateConfig struct {
type markProjectTemplateMutation struct {
TemplateProject struct {
Project queries.Project `graphql:"projectV2"`
Project queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"markProjectV2AsTemplate(input:$input)"`
}
type unmarkProjectTemplateMutation struct {
TemplateProject struct {
Project queries.Project `graphql:"projectV2"`
Project queries.ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"unmarkProjectV2AsTemplate(input:$input)"`
}
@ -150,7 +150,7 @@ func unmarkTemplateArgs(config markTemplateConfig) (*unmarkProjectTemplateMutati
}
}
func printResults(config markTemplateConfig, project queries.Project) error {
func printResults(config markTemplateConfig, project queries.ProjectMutationQuery) error {
if !config.io.IsStdoutTTY() {
return nil
}

View file

@ -147,6 +147,34 @@ type Project struct {
}
}
// ProjectMutationQuery is a ProjectV2 response shape for mutation payloads.
// It intentionally avoids the queryable items connection to prevent requiring a $query variable.
type ProjectMutationQuery struct {
Number int32
URL string
ShortDescription string
Public bool
Closed bool
Title string
ID string
Readme string
Items struct {
TotalCount int
} `graphql:"items(first: $firstItems, after: $afterItems)"`
Fields struct {
TotalCount int
} `graphql:"fields(first: $firstFields, after: $afterFields)"`
Owner struct {
TypeName string `graphql:"__typename"`
User struct {
Login string
} `graphql:"... on User"`
Organization struct {
Login string
} `graphql:"... on Organization"`
}
}
// Below, you will find the query structs to represent fetching a project via the GraphQL API.
// Prior to GHES 3.20, the query argument did not exist on the items connection, so we have
// one base struct and two structs that embed and add the Items connection with and without the query argument.
@ -265,6 +293,40 @@ func (p Project) OwnerLogin() string {
return p.Owner.Organization.Login
}
func (p ProjectMutationQuery) ExportData(_ []string) map[string]interface{} {
return map[string]interface{}{
"number": p.Number,
"url": p.URL,
"shortDescription": p.ShortDescription,
"public": p.Public,
"closed": p.Closed,
"title": p.Title,
"id": p.ID,
"readme": p.Readme,
"items": map[string]interface{}{
"totalCount": p.Items.TotalCount,
},
"fields": map[string]interface{}{
"totalCount": p.Fields.TotalCount,
},
"owner": map[string]interface{}{
"type": p.OwnerType(),
"login": p.OwnerLogin(),
},
}
}
func (p ProjectMutationQuery) OwnerType() string {
return p.Owner.TypeName
}
func (p ProjectMutationQuery) OwnerLogin() string {
if p.OwnerType() == "User" {
return p.Owner.User.Login
}
return p.Owner.Organization.Login
}
type Projects struct {
Nodes []Project
TotalCount int

View file

@ -8,6 +8,7 @@ import (
"testing"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/shurcooL/githubv4"
"github.com/stretchr/testify/assert"
"gopkg.in/h2non/gock.v1"
)
@ -18,6 +19,52 @@ func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func TestProjectMutationQuery_DoesNotRequireQueryVariable(t *testing.T) {
ios, _, _, _ := iostreams.Test()
httpClient := &http.Client{
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
assert.NoError(t, err)
assert.NotContains(t, string(body), "$query")
return &http.Response{
StatusCode: 200,
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: io.NopCloser(strings.NewReader(`{
"data": {
"updateProjectV2": {
"projectV2": {
"id": "project ID",
"url": "http://example.com"
}
}
}
}`)),
}, nil
}),
}
client := NewClient(httpClient, "github.com", ios)
mutation := struct {
UpdateProjectV2 struct {
ProjectV2 ProjectMutationQuery `graphql:"projectV2"`
} `graphql:"updateProjectV2(input:$input)"`
}{}
err := client.Mutate("UpdateProjectV2", &mutation, map[string]interface{}{
"input": githubv4.UpdateProjectV2Input{
ProjectID: githubv4.ID("project ID"),
},
"firstItems": githubv4.Int(0),
"afterItems": (*githubv4.String)(nil),
"firstFields": githubv4.Int(0),
"afterFields": (*githubv4.String)(nil),
})
assert.NoError(t, err)
}
func TestProjectItems_DefaultLimit(t *testing.T) {
defer gock.Off()
gock.Observe(gock.DumpRequest)