Loading gitlab_test/workitems_integration_test.go +67 −14 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ import ( gitlab "gitlab.com/gitlab-org/api/client-go/v2" ) func TestCreateWorkItem(t *testing.T) { func TestCreateGetUpdateWorkItem(t *testing.T) { t.Parallel() client := SetupIntegrationClient(t) Loading @@ -19,22 +19,75 @@ func TestCreateWorkItem(t *testing.T) { // GIVEN a test group group := CreateTestGroup(t, client) // AND a work item creation options opt := gitlab.CreateWorkItemOptions{ Title: "Test Work Item", Description: gitlab.Ptr("This is a test work item"), Weight: gitlab.Ptr(int64(100)), // STEP 1: Create a work item // WHEN creating a new work item with initial options createOpt := gitlab.CreateWorkItemOptions{ Title: "Integration Test Work Item", Description: gitlab.Ptr("Initial description"), HealthStatus: gitlab.Ptr("onTrack"), Color: gitlab.Ptr("green"), } // WHEN creating a new work item with the given options wi, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &opt) createdWI, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &createOpt) require.NoError(t, err, "CreateWorkItem failed") require.NotNil(t, createdWI) // THEN the work item should be created successfully assert.NotNil(t, wi) // THEN the work item should have the provided fields set correctly assert.Equal(t, "Integration Test Work Item", createdWI.Title, "Field: Title") assert.Equal(t, "Initial description", createdWI.Description, "Field: Description") assert.Equal(t, "onTrack", deref(t, createdWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, "#008000", deref(t, createdWI.Color), "Field: Color") // AND all provided fields should be set correctly assert.Equal(t, "Test Work Item", wi.Title) assert.Equal(t, "This is a test work item", wi.Description) // assert.Equal(t, int64(100), wi.Weight) // STEP 2: Get the work item // WHEN retrieving the work item by full path and IID gotWI, _, err := client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID) require.NoError(t, err, "GetWorkItem failed") require.NotNil(t, gotWI) // THEN the retrieved work item should have the same provided fields assert.Equal(t, createdWI.Title, gotWI.Title, "Field: Title") assert.Equal(t, createdWI.Description, gotWI.Description, "Field: Description") assert.Equal(t, deref(t, createdWI.HealthStatus), deref(t, gotWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, deref(t, createdWI.Color), deref(t, gotWI.Color), "Field: Color") // STEP 3: Update the work item // WHEN updating the work item with new values updateOpt := gitlab.UpdateWorkItemOptions{ Title: gitlab.Ptr("Updated Work Item Title"), Description: gitlab.Ptr("Updated description"), HealthStatus: gitlab.Ptr("needsAttention"), Color: gitlab.Ptr("red"), } updatedWI, _, err := client.WorkItems.UpdateWorkItem(group.FullPath, createdWI.IID, &updateOpt) require.NoError(t, err, "UpdateWorkItem failed") require.NotNil(t, updatedWI) // THEN the work item should have the updated fields set correctly assert.Equal(t, "Updated Work Item Title", updatedWI.Title, "Field: Title") assert.Equal(t, "Updated description", updatedWI.Description, "Field: Description") assert.Equal(t, "needsAttention", deref(t, updatedWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, "#FF0000", deref(t, updatedWI.Color), "Field: Color") // STEP 4: Get the work item again // WHEN retrieving the work item after update finalWI, _, err := client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID) require.NoError(t, err, "GetWorkItem after update failed") require.NotNil(t, finalWI) // THEN the retrieved work item should have the same updated fields assert.Equal(t, updatedWI.Title, finalWI.Title, "Field: Title") assert.Equal(t, updatedWI.Description, finalWI.Description, "Field: Description") assert.Equal(t, deref(t, updatedWI.HealthStatus), deref(t, finalWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, deref(t, updatedWI.Color), deref(t, finalWI.Color), "Field: Color") } func deref(t *testing.T, ptr *string) string { t.Helper() if ptr == nil { t.Fatal("pointer is nil") } return *ptr } graphql.go +4 −0 Original line number Diff line number Diff line Loading @@ -146,6 +146,10 @@ func (id *gidGQL) UnmarshalJSON(b []byte) error { return nil } func (id gidGQL) IsZero() bool { return id.Type == "" && id.Int64 == 0 } func (id gidGQL) String() string { return fmt.Sprintf("gid://gitlab/%s/%d", id.Type, id.Int64) } Loading testdata/update_workitem.json 0 → 100644 +26 −0 Original line number Diff line number Diff line { "data": { "workItemUpdate": { "workItem": { "id": "gid://gitlab/WorkItem/179785913", "iid": "756", "workItemType": { "name": "Task" }, "state": "OPEN", "title": "test title update", "description": "## Overview\n\nUpdate Runway Helm charts to generate Argo Rollout resources ...", "author": { "id": "gid://gitlab/User/5532616", "username": "swainaina", "name": "Silvester Wainaina", "state": "active", "createdAt": "2020-03-02T06:29:14Z", "avatarUrl": "/uploads/-/system/user/avatar/5532616/avatar.png", "webUrl": "https://gitlab.com/swainaina" } }, "errors": [] } } } testing/workitems_mock.go +45 −0 Original line number Diff line number Diff line Loading @@ -173,3 +173,48 @@ func (c *MockWorkItemsServiceInterfaceListWorkItemsCall) DoAndReturn(f func(stri c.Call = c.Call.DoAndReturn(f) return c } // UpdateWorkItem mocks base method. func (m *MockWorkItemsServiceInterface) UpdateWorkItem(fullPath string, iid int64, opt *gitlab.UpdateWorkItemOptions, options ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error) { m.ctrl.T.Helper() varargs := []any{fullPath, iid, opt} for _, a := range options { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "UpdateWorkItem", varargs...) ret0, _ := ret[0].(*gitlab.WorkItem) ret1, _ := ret[1].(*gitlab.Response) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // UpdateWorkItem indicates an expected call of UpdateWorkItem. func (mr *MockWorkItemsServiceInterfaceMockRecorder) UpdateWorkItem(fullPath, iid, opt any, options ...any) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { mr.mock.ctrl.T.Helper() varargs := append([]any{fullPath, iid, opt}, options...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkItem", reflect.TypeOf((*MockWorkItemsServiceInterface)(nil).UpdateWorkItem), varargs...) return &MockWorkItemsServiceInterfaceUpdateWorkItemCall{Call: call} } // MockWorkItemsServiceInterfaceUpdateWorkItemCall wrap *gomock.Call type MockWorkItemsServiceInterfaceUpdateWorkItemCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) Return(arg0 *gitlab.WorkItem, arg1 *gitlab.Response, arg2 error) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { c.Call = c.Call.Return(arg0, arg1, arg2) return c } // Do rewrite *gomock.Call.Do func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) Do(f func(string, int64, *gitlab.UpdateWorkItemOptions, ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error)) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) DoAndReturn(f func(string, int64, *gitlab.UpdateWorkItemOptions, ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error)) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { c.Call = c.Call.DoAndReturn(f) return c } workitems.go +364 −1 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ package gitlab import ( "errors" "fmt" "strconv" "strings" "text/template" Loading @@ -15,6 +16,7 @@ type ( CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) } // WorkItemsService handles communication with the work item related methods Loading Loading @@ -89,6 +91,14 @@ type LinkedWorkItem struct { LinkType string } // WorkItemStateEvent represents a state change event for a work item. type WorkItemStateEvent string const ( WorkItemStateEventClose WorkItemStateEvent = "CLOSE" WorkItemStateEventReopen WorkItemStateEvent = "REOPEN" ) // workItemTemplate defines the common fields for a work item in GraphQL queries. // It's chained from userCoreBasicTemplate so nested templates work. var workItemTemplate = template.Must(template.Must(userCoreBasicTemplate.Clone()).New("WorkItem").Parse(` Loading Loading @@ -249,7 +259,7 @@ func (s *WorkItemsService) GetWorkItem(fullPath string, iid int64, options ...Re // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitems // // Experimental: The Work Item API is work in progress and subject to change even between minor versions. // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type ListWorkItemsOptions struct { AssigneeUsernames []string AssigneeWildcardID *string Loading Loading @@ -505,6 +515,8 @@ func (s *WorkItemsService) ListWorkItems(fullPath string, opt *ListWorkItemsOpti // // GitLab API docs: // https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type CreateWorkItemOptions struct { // Title of the work item. Required. Title string Loading Loading @@ -558,6 +570,9 @@ type CreateWorkItemOptions struct { Color *string } // CreateWorkItemOptionsLinkedItems represents linked items to be added to a work item. // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type CreateWorkItemOptionsLinkedItems struct { LinkType *string // enum: BLOCKED_BY, BLOCKS, RELATED WorkItemIDs []int64 Loading Loading @@ -650,6 +665,37 @@ type workItemWidgetColorInputGQL struct { Color *string `json:"color,omitempty"` } // workItemWidgetCRMContactsUpdateInputGQL represents the CRM contacts widget input for updates. type workItemWidgetCRMContactsUpdateInputGQL struct { ContactIDs []string `json:"contactIds,omitempty"` OperationMode *string `json:"operationMode,omitempty"` } // workItemWidgetHierarchyUpdateInputGQL represents the hierarchy widget input for updates. type workItemWidgetHierarchyUpdateInputGQL struct { ParentID *string `json:"parentId,omitempty"` AdjacentWorkItemID *string `json:"adjacentWorkItemId,omitempty"` ChildrenIDs []string `json:"childrenIds,omitempty"` RelativePosition *string `json:"relativePosition,omitempty"` } // workItemWidgetLabelsUpdateInputGQL represents the labels widget input for updates. type workItemWidgetLabelsUpdateInputGQL struct { AddLabelIDs []string `json:"addLabelIds,omitempty"` RemoveLabelIDs []string `json:"removeLabelIds,omitempty"` } // workItemWidgetStartAndDueDateUpdateInputGQL represents the start and due date widget input for updates. type workItemWidgetStartAndDueDateUpdateInputGQL struct { StartDate *string `json:"startDate,omitempty"` DueDate *string `json:"dueDate,omitempty"` } // workItemWidgetStatusInputGQL represents the status widget input. type workItemWidgetStatusInputGQL struct { Status *WorkItemStatusID `json:"status,omitempty"` } // newWorkItemCreateInput converts the user-facing CreateWorkItemOptions to the // backend-facing GraphQL-aligned workItemCreateInputGQL struct. func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID WorkItemTypeID) *workItemCreateInputGQL { Loading Loading @@ -751,6 +797,31 @@ func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID Work return input } // updateWorkItemTemplate is chained from workItemTemplate so it has access to both // UserCoreBasic and WorkItem templates. var updateWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("UpdateWorkItem").Parse(` mutation UpdateWorkItem($input: WorkItemUpdateInput!) { workItemUpdate(input: $input) { workItem { {{ template "WorkItem" }} } errors } } `)) const ( getWorkItemIDQuery = ` query GetWorkItemID($fullPath: ID!, $iid: String!) { namespace(fullPath: $fullPath) { workItem(iid: $iid) { id } } } ` ) // createWorkItemTemplate is chained from workItemTemplate so it has access to both // UserCoreBasic and WorkItem templates. var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("CreateWorkItem").Parse(` Loading @@ -772,6 +843,8 @@ var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone( // // GitLab API docs: // https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) { var queryBuilder strings.Builder if err := createWorkItemTemplate.Execute(&queryBuilder, nil); err != nil { Loading Loading @@ -826,6 +899,280 @@ func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkIt return wiQL.unwrap(), resp, nil } // UpdateWorkItemOptions represents the available UpdateWorkItem() options. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type UpdateWorkItemOptions struct { // Title of the work item. Title *string // State event for the work item. Possible values: CLOSE, REOPEN StateEvent *WorkItemStateEvent // Description of the work item. Description *string // Global IDs of assignees. An empty (non-nil) slice can be used to remove all assignees. AssigneeIDs []int64 // Global ID of the milestone to assign to the work item. MilestoneID *int64 // CRM contact IDs to set. An empty (non-nil) slice can be used to remove all contacts. CRMContactIDs []int64 // Global ID of the parent work item. ParentID *int64 // Global IDs of labels to be added to the work item. AddLabelIDs []int64 // Global IDs of labels to be removed from the work item. RemoveLabelIDs []int64 // Start date for the work item. StartDate *ISOTime // Due date for the work item. DueDate *ISOTime // Weight of the work item. Weight *int64 // Health status to be assigned to the work item. Possible values: onTrack, needsAttention, atRisk HealthStatus *string // Global ID of the iteration to assign to the work item. IterationID *int64 // Color of the work item, represented as a hex code or named color. Example: "#fefefe" Color *string // Global ID of the work item status. Status *WorkItemStatusID } func (opt *UpdateWorkItemOptions) wrap(gid gidGQL) *workItemUpdateInputGQL { if opt == nil { return &workItemUpdateInputGQL{ ID: gid.String(), } } input := &workItemUpdateInputGQL{ ID: gid.String(), Title: opt.Title, StateEvent: opt.StateEvent, } if opt.Description != nil { input.DescriptionWidget = &workItemWidgetDescriptionInputGQL{ Description: opt.Description, } } if len(opt.AssigneeIDs) > 0 { input.AssigneesWidget = &workItemWidgetAssigneesInputGQL{ AssigneeIDs: newGIDStrings("User", opt.AssigneeIDs...), } } if opt.MilestoneID != nil { input.MilestoneWidget = &workItemWidgetMilestoneInputGQL{ MilestoneID: Ptr(gidGQL{"Milestone", *opt.MilestoneID}.String()), } } if len(opt.CRMContactIDs) > 0 { input.CRMContactsWidget = &workItemWidgetCRMContactsUpdateInputGQL{ ContactIDs: newGIDStrings("CustomerRelations::Contact", opt.CRMContactIDs...), OperationMode: Ptr("REPLACE"), } } if opt.ParentID != nil { input.HierarchyWidget = &workItemWidgetHierarchyUpdateInputGQL{ ParentID: Ptr(gidGQL{"WorkItem", *opt.ParentID}.String()), } } if len(opt.AddLabelIDs) > 0 || len(opt.RemoveLabelIDs) > 0 { widget := &workItemWidgetLabelsUpdateInputGQL{} if len(opt.AddLabelIDs) > 0 { widget.AddLabelIDs = newGIDStrings("Label", opt.AddLabelIDs...) } if len(opt.RemoveLabelIDs) > 0 { widget.RemoveLabelIDs = newGIDStrings("Label", opt.RemoveLabelIDs...) } input.LabelsWidget = widget } if opt.StartDate != nil || opt.DueDate != nil { widget := &workItemWidgetStartAndDueDateUpdateInputGQL{} if opt.StartDate != nil { widget.StartDate = Ptr(opt.StartDate.String()) } if opt.DueDate != nil { widget.DueDate = Ptr(opt.DueDate.String()) } input.StartAndDueDateWidget = widget } if opt.Weight != nil { input.WeightWidget = &workItemWidgetWeightInputGQL{ Weight: opt.Weight, } } if opt.HealthStatus != nil { input.HealthStatusWidget = &workItemWidgetHealthStatusInputGQL{ HealthStatus: opt.HealthStatus, } } if opt.IterationID != nil { input.IterationWidget = &workItemWidgetIterationInputGQL{ IterationID: Ptr(gidGQL{"Iteration", *opt.IterationID}.String()), } } if opt.Color != nil { input.ColorWidget = &workItemWidgetColorInputGQL{ Color: opt.Color, } } if opt.Status != nil { input.StatusWidget = &workItemWidgetStatusInputGQL{ Status: opt.Status, } } return input } // UpdateWorkItem updates a work item by fullPath and iid. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. func (s *WorkItemsService) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) { gid, resp, err := s.workItemGID(fullPath, iid, options...) if err != nil { return nil, resp, err } var queryBuilder strings.Builder if err := updateWorkItemTemplate.Execute(&queryBuilder, nil); err != nil { return nil, nil, err } q := GraphQLQuery{ Query: queryBuilder.String(), Variables: map[string]any{ "input": opt.wrap(gid), }, } var result struct { Data struct { WorkItemUpdate struct { WorkItem *workItemGQL `json:"workItem"` Errors []string `json:"errors"` } `json:"workItemUpdate"` } GenericGraphQLErrors } resp, err = s.client.GraphQL.Do(q, &result, options...) if err != nil { return nil, resp, err } if len(result.Errors) != 0 { return nil, resp, &GraphQLResponseError{ Err: errors.New("GraphQL query failed"), Errors: result.GenericGraphQLErrors, } } if len(result.Data.WorkItemUpdate.Errors) != 0 { return nil, resp, errors.New(result.Data.WorkItemUpdate.Errors[0]) } wiQL := result.Data.WorkItemUpdate.WorkItem if wiQL == nil { return nil, resp, ErrNotFound } return wiQL.unwrap(), resp, nil } // workItemUpdateInputGQL represents the GraphQL input structure for updating a work item. type workItemUpdateInputGQL struct { ID string `json:"id"` Title *string `json:"title,omitempty"` StateEvent *WorkItemStateEvent `json:"stateEvent,omitempty"` DescriptionWidget *workItemWidgetDescriptionInputGQL `json:"descriptionWidget,omitempty"` AssigneesWidget *workItemWidgetAssigneesInputGQL `json:"assigneesWidget,omitempty"` MilestoneWidget *workItemWidgetMilestoneInputGQL `json:"milestoneWidget,omitempty"` CRMContactsWidget *workItemWidgetCRMContactsUpdateInputGQL `json:"crmContactsWidget,omitempty"` HierarchyWidget *workItemWidgetHierarchyUpdateInputGQL `json:"hierarchyWidget,omitempty"` LabelsWidget *workItemWidgetLabelsUpdateInputGQL `json:"labelsWidget,omitempty"` StartAndDueDateWidget *workItemWidgetStartAndDueDateUpdateInputGQL `json:"startAndDueDateWidget,omitempty"` WeightWidget *workItemWidgetWeightInputGQL `json:"weightWidget,omitempty"` HealthStatusWidget *workItemWidgetHealthStatusInputGQL `json:"healthStatusWidget,omitempty"` IterationWidget *workItemWidgetIterationInputGQL `json:"iterationWidget,omitempty"` ColorWidget *workItemWidgetColorInputGQL `json:"colorWidget,omitempty"` StatusWidget *workItemWidgetStatusInputGQL `json:"statusWidget,omitempty"` } // workItemGID queries for a work item's Global ID using fullPath and iid. // Returns the Global ID string or ErrNotFound if the work item doesn't exist. // // API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitem func (s *WorkItemsService) workItemGID(fullPath string, iid int64, options ...RequestOptionFunc) (gidGQL, *Response, error) { q := GraphQLQuery{ Query: getWorkItemIDQuery, Variables: map[string]any{ "fullPath": fullPath, "iid": strconv.FormatInt(iid, 10), }, } var result struct { Data struct { Namespace struct { WorkItem struct { ID gidGQL `json:"id"` } `json:"workItem"` } } GenericGraphQLErrors } resp, err := s.client.GraphQL.Do(q, &result, options...) if err != nil { return gidGQL{}, resp, err } if len(result.Errors) != 0 { return gidGQL{}, resp, &GraphQLResponseError{ Err: fmt.Errorf("looking up global ID of %s#%d failed", fullPath, iid), Errors: result.GenericGraphQLErrors, } } id := result.Data.Namespace.WorkItem.ID if id.IsZero() { return gidGQL{}, resp, fmt.Errorf("looking up global ID of %s#%d failed: %w", fullPath, iid, ErrEmptyResponse) } return id, resp, nil } // workItemGQL represents the JSON structure returned by the GraphQL query. // It is used to parse the response and convert it to the more user-friendly WorkItem type. type workItemGQL struct { Loading Loading @@ -1152,3 +1499,19 @@ const ( WorkItemTypeEpic WorkItemTypeID = `gid://gitlab/WorkItems::Type/8` WorkItemTypeTicket WorkItemTypeID = `gid://gitlab/WorkItems::Type/9` ) // WorkItemStatusID represents the global ID of a work item status. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemsstatus // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type WorkItemStatusID string // WorkItemStatusID constants for the system-defined work item statuses. const ( WorkItemStatusToDo WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1` WorkItemStatusInProgress WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2` WorkItemStatusDone WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3` WorkItemStatusWontDo WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/4` WorkItemStatusDuplicate WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5` ) Loading
gitlab_test/workitems_integration_test.go +67 −14 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ import ( gitlab "gitlab.com/gitlab-org/api/client-go/v2" ) func TestCreateWorkItem(t *testing.T) { func TestCreateGetUpdateWorkItem(t *testing.T) { t.Parallel() client := SetupIntegrationClient(t) Loading @@ -19,22 +19,75 @@ func TestCreateWorkItem(t *testing.T) { // GIVEN a test group group := CreateTestGroup(t, client) // AND a work item creation options opt := gitlab.CreateWorkItemOptions{ Title: "Test Work Item", Description: gitlab.Ptr("This is a test work item"), Weight: gitlab.Ptr(int64(100)), // STEP 1: Create a work item // WHEN creating a new work item with initial options createOpt := gitlab.CreateWorkItemOptions{ Title: "Integration Test Work Item", Description: gitlab.Ptr("Initial description"), HealthStatus: gitlab.Ptr("onTrack"), Color: gitlab.Ptr("green"), } // WHEN creating a new work item with the given options wi, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &opt) createdWI, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &createOpt) require.NoError(t, err, "CreateWorkItem failed") require.NotNil(t, createdWI) // THEN the work item should be created successfully assert.NotNil(t, wi) // THEN the work item should have the provided fields set correctly assert.Equal(t, "Integration Test Work Item", createdWI.Title, "Field: Title") assert.Equal(t, "Initial description", createdWI.Description, "Field: Description") assert.Equal(t, "onTrack", deref(t, createdWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, "#008000", deref(t, createdWI.Color), "Field: Color") // AND all provided fields should be set correctly assert.Equal(t, "Test Work Item", wi.Title) assert.Equal(t, "This is a test work item", wi.Description) // assert.Equal(t, int64(100), wi.Weight) // STEP 2: Get the work item // WHEN retrieving the work item by full path and IID gotWI, _, err := client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID) require.NoError(t, err, "GetWorkItem failed") require.NotNil(t, gotWI) // THEN the retrieved work item should have the same provided fields assert.Equal(t, createdWI.Title, gotWI.Title, "Field: Title") assert.Equal(t, createdWI.Description, gotWI.Description, "Field: Description") assert.Equal(t, deref(t, createdWI.HealthStatus), deref(t, gotWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, deref(t, createdWI.Color), deref(t, gotWI.Color), "Field: Color") // STEP 3: Update the work item // WHEN updating the work item with new values updateOpt := gitlab.UpdateWorkItemOptions{ Title: gitlab.Ptr("Updated Work Item Title"), Description: gitlab.Ptr("Updated description"), HealthStatus: gitlab.Ptr("needsAttention"), Color: gitlab.Ptr("red"), } updatedWI, _, err := client.WorkItems.UpdateWorkItem(group.FullPath, createdWI.IID, &updateOpt) require.NoError(t, err, "UpdateWorkItem failed") require.NotNil(t, updatedWI) // THEN the work item should have the updated fields set correctly assert.Equal(t, "Updated Work Item Title", updatedWI.Title, "Field: Title") assert.Equal(t, "Updated description", updatedWI.Description, "Field: Description") assert.Equal(t, "needsAttention", deref(t, updatedWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, "#FF0000", deref(t, updatedWI.Color), "Field: Color") // STEP 4: Get the work item again // WHEN retrieving the work item after update finalWI, _, err := client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID) require.NoError(t, err, "GetWorkItem after update failed") require.NotNil(t, finalWI) // THEN the retrieved work item should have the same updated fields assert.Equal(t, updatedWI.Title, finalWI.Title, "Field: Title") assert.Equal(t, updatedWI.Description, finalWI.Description, "Field: Description") assert.Equal(t, deref(t, updatedWI.HealthStatus), deref(t, finalWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, deref(t, updatedWI.Color), deref(t, finalWI.Color), "Field: Color") } func deref(t *testing.T, ptr *string) string { t.Helper() if ptr == nil { t.Fatal("pointer is nil") } return *ptr }
graphql.go +4 −0 Original line number Diff line number Diff line Loading @@ -146,6 +146,10 @@ func (id *gidGQL) UnmarshalJSON(b []byte) error { return nil } func (id gidGQL) IsZero() bool { return id.Type == "" && id.Int64 == 0 } func (id gidGQL) String() string { return fmt.Sprintf("gid://gitlab/%s/%d", id.Type, id.Int64) } Loading
testdata/update_workitem.json 0 → 100644 +26 −0 Original line number Diff line number Diff line { "data": { "workItemUpdate": { "workItem": { "id": "gid://gitlab/WorkItem/179785913", "iid": "756", "workItemType": { "name": "Task" }, "state": "OPEN", "title": "test title update", "description": "## Overview\n\nUpdate Runway Helm charts to generate Argo Rollout resources ...", "author": { "id": "gid://gitlab/User/5532616", "username": "swainaina", "name": "Silvester Wainaina", "state": "active", "createdAt": "2020-03-02T06:29:14Z", "avatarUrl": "/uploads/-/system/user/avatar/5532616/avatar.png", "webUrl": "https://gitlab.com/swainaina" } }, "errors": [] } } }
testing/workitems_mock.go +45 −0 Original line number Diff line number Diff line Loading @@ -173,3 +173,48 @@ func (c *MockWorkItemsServiceInterfaceListWorkItemsCall) DoAndReturn(f func(stri c.Call = c.Call.DoAndReturn(f) return c } // UpdateWorkItem mocks base method. func (m *MockWorkItemsServiceInterface) UpdateWorkItem(fullPath string, iid int64, opt *gitlab.UpdateWorkItemOptions, options ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error) { m.ctrl.T.Helper() varargs := []any{fullPath, iid, opt} for _, a := range options { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "UpdateWorkItem", varargs...) ret0, _ := ret[0].(*gitlab.WorkItem) ret1, _ := ret[1].(*gitlab.Response) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // UpdateWorkItem indicates an expected call of UpdateWorkItem. func (mr *MockWorkItemsServiceInterfaceMockRecorder) UpdateWorkItem(fullPath, iid, opt any, options ...any) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { mr.mock.ctrl.T.Helper() varargs := append([]any{fullPath, iid, opt}, options...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkItem", reflect.TypeOf((*MockWorkItemsServiceInterface)(nil).UpdateWorkItem), varargs...) return &MockWorkItemsServiceInterfaceUpdateWorkItemCall{Call: call} } // MockWorkItemsServiceInterfaceUpdateWorkItemCall wrap *gomock.Call type MockWorkItemsServiceInterfaceUpdateWorkItemCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) Return(arg0 *gitlab.WorkItem, arg1 *gitlab.Response, arg2 error) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { c.Call = c.Call.Return(arg0, arg1, arg2) return c } // Do rewrite *gomock.Call.Do func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) Do(f func(string, int64, *gitlab.UpdateWorkItemOptions, ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error)) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) DoAndReturn(f func(string, int64, *gitlab.UpdateWorkItemOptions, ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error)) *MockWorkItemsServiceInterfaceUpdateWorkItemCall { c.Call = c.Call.DoAndReturn(f) return c }
workitems.go +364 −1 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ package gitlab import ( "errors" "fmt" "strconv" "strings" "text/template" Loading @@ -15,6 +16,7 @@ type ( CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) } // WorkItemsService handles communication with the work item related methods Loading Loading @@ -89,6 +91,14 @@ type LinkedWorkItem struct { LinkType string } // WorkItemStateEvent represents a state change event for a work item. type WorkItemStateEvent string const ( WorkItemStateEventClose WorkItemStateEvent = "CLOSE" WorkItemStateEventReopen WorkItemStateEvent = "REOPEN" ) // workItemTemplate defines the common fields for a work item in GraphQL queries. // It's chained from userCoreBasicTemplate so nested templates work. var workItemTemplate = template.Must(template.Must(userCoreBasicTemplate.Clone()).New("WorkItem").Parse(` Loading Loading @@ -249,7 +259,7 @@ func (s *WorkItemsService) GetWorkItem(fullPath string, iid int64, options ...Re // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitems // // Experimental: The Work Item API is work in progress and subject to change even between minor versions. // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type ListWorkItemsOptions struct { AssigneeUsernames []string AssigneeWildcardID *string Loading Loading @@ -505,6 +515,8 @@ func (s *WorkItemsService) ListWorkItems(fullPath string, opt *ListWorkItemsOpti // // GitLab API docs: // https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type CreateWorkItemOptions struct { // Title of the work item. Required. Title string Loading Loading @@ -558,6 +570,9 @@ type CreateWorkItemOptions struct { Color *string } // CreateWorkItemOptionsLinkedItems represents linked items to be added to a work item. // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type CreateWorkItemOptionsLinkedItems struct { LinkType *string // enum: BLOCKED_BY, BLOCKS, RELATED WorkItemIDs []int64 Loading Loading @@ -650,6 +665,37 @@ type workItemWidgetColorInputGQL struct { Color *string `json:"color,omitempty"` } // workItemWidgetCRMContactsUpdateInputGQL represents the CRM contacts widget input for updates. type workItemWidgetCRMContactsUpdateInputGQL struct { ContactIDs []string `json:"contactIds,omitempty"` OperationMode *string `json:"operationMode,omitempty"` } // workItemWidgetHierarchyUpdateInputGQL represents the hierarchy widget input for updates. type workItemWidgetHierarchyUpdateInputGQL struct { ParentID *string `json:"parentId,omitempty"` AdjacentWorkItemID *string `json:"adjacentWorkItemId,omitempty"` ChildrenIDs []string `json:"childrenIds,omitempty"` RelativePosition *string `json:"relativePosition,omitempty"` } // workItemWidgetLabelsUpdateInputGQL represents the labels widget input for updates. type workItemWidgetLabelsUpdateInputGQL struct { AddLabelIDs []string `json:"addLabelIds,omitempty"` RemoveLabelIDs []string `json:"removeLabelIds,omitempty"` } // workItemWidgetStartAndDueDateUpdateInputGQL represents the start and due date widget input for updates. type workItemWidgetStartAndDueDateUpdateInputGQL struct { StartDate *string `json:"startDate,omitempty"` DueDate *string `json:"dueDate,omitempty"` } // workItemWidgetStatusInputGQL represents the status widget input. type workItemWidgetStatusInputGQL struct { Status *WorkItemStatusID `json:"status,omitempty"` } // newWorkItemCreateInput converts the user-facing CreateWorkItemOptions to the // backend-facing GraphQL-aligned workItemCreateInputGQL struct. func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID WorkItemTypeID) *workItemCreateInputGQL { Loading Loading @@ -751,6 +797,31 @@ func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID Work return input } // updateWorkItemTemplate is chained from workItemTemplate so it has access to both // UserCoreBasic and WorkItem templates. var updateWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("UpdateWorkItem").Parse(` mutation UpdateWorkItem($input: WorkItemUpdateInput!) { workItemUpdate(input: $input) { workItem { {{ template "WorkItem" }} } errors } } `)) const ( getWorkItemIDQuery = ` query GetWorkItemID($fullPath: ID!, $iid: String!) { namespace(fullPath: $fullPath) { workItem(iid: $iid) { id } } } ` ) // createWorkItemTemplate is chained from workItemTemplate so it has access to both // UserCoreBasic and WorkItem templates. var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("CreateWorkItem").Parse(` Loading @@ -772,6 +843,8 @@ var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone( // // GitLab API docs: // https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) { var queryBuilder strings.Builder if err := createWorkItemTemplate.Execute(&queryBuilder, nil); err != nil { Loading Loading @@ -826,6 +899,280 @@ func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkIt return wiQL.unwrap(), resp, nil } // UpdateWorkItemOptions represents the available UpdateWorkItem() options. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type UpdateWorkItemOptions struct { // Title of the work item. Title *string // State event for the work item. Possible values: CLOSE, REOPEN StateEvent *WorkItemStateEvent // Description of the work item. Description *string // Global IDs of assignees. An empty (non-nil) slice can be used to remove all assignees. AssigneeIDs []int64 // Global ID of the milestone to assign to the work item. MilestoneID *int64 // CRM contact IDs to set. An empty (non-nil) slice can be used to remove all contacts. CRMContactIDs []int64 // Global ID of the parent work item. ParentID *int64 // Global IDs of labels to be added to the work item. AddLabelIDs []int64 // Global IDs of labels to be removed from the work item. RemoveLabelIDs []int64 // Start date for the work item. StartDate *ISOTime // Due date for the work item. DueDate *ISOTime // Weight of the work item. Weight *int64 // Health status to be assigned to the work item. Possible values: onTrack, needsAttention, atRisk HealthStatus *string // Global ID of the iteration to assign to the work item. IterationID *int64 // Color of the work item, represented as a hex code or named color. Example: "#fefefe" Color *string // Global ID of the work item status. Status *WorkItemStatusID } func (opt *UpdateWorkItemOptions) wrap(gid gidGQL) *workItemUpdateInputGQL { if opt == nil { return &workItemUpdateInputGQL{ ID: gid.String(), } } input := &workItemUpdateInputGQL{ ID: gid.String(), Title: opt.Title, StateEvent: opt.StateEvent, } if opt.Description != nil { input.DescriptionWidget = &workItemWidgetDescriptionInputGQL{ Description: opt.Description, } } if len(opt.AssigneeIDs) > 0 { input.AssigneesWidget = &workItemWidgetAssigneesInputGQL{ AssigneeIDs: newGIDStrings("User", opt.AssigneeIDs...), } } if opt.MilestoneID != nil { input.MilestoneWidget = &workItemWidgetMilestoneInputGQL{ MilestoneID: Ptr(gidGQL{"Milestone", *opt.MilestoneID}.String()), } } if len(opt.CRMContactIDs) > 0 { input.CRMContactsWidget = &workItemWidgetCRMContactsUpdateInputGQL{ ContactIDs: newGIDStrings("CustomerRelations::Contact", opt.CRMContactIDs...), OperationMode: Ptr("REPLACE"), } } if opt.ParentID != nil { input.HierarchyWidget = &workItemWidgetHierarchyUpdateInputGQL{ ParentID: Ptr(gidGQL{"WorkItem", *opt.ParentID}.String()), } } if len(opt.AddLabelIDs) > 0 || len(opt.RemoveLabelIDs) > 0 { widget := &workItemWidgetLabelsUpdateInputGQL{} if len(opt.AddLabelIDs) > 0 { widget.AddLabelIDs = newGIDStrings("Label", opt.AddLabelIDs...) } if len(opt.RemoveLabelIDs) > 0 { widget.RemoveLabelIDs = newGIDStrings("Label", opt.RemoveLabelIDs...) } input.LabelsWidget = widget } if opt.StartDate != nil || opt.DueDate != nil { widget := &workItemWidgetStartAndDueDateUpdateInputGQL{} if opt.StartDate != nil { widget.StartDate = Ptr(opt.StartDate.String()) } if opt.DueDate != nil { widget.DueDate = Ptr(opt.DueDate.String()) } input.StartAndDueDateWidget = widget } if opt.Weight != nil { input.WeightWidget = &workItemWidgetWeightInputGQL{ Weight: opt.Weight, } } if opt.HealthStatus != nil { input.HealthStatusWidget = &workItemWidgetHealthStatusInputGQL{ HealthStatus: opt.HealthStatus, } } if opt.IterationID != nil { input.IterationWidget = &workItemWidgetIterationInputGQL{ IterationID: Ptr(gidGQL{"Iteration", *opt.IterationID}.String()), } } if opt.Color != nil { input.ColorWidget = &workItemWidgetColorInputGQL{ Color: opt.Color, } } if opt.Status != nil { input.StatusWidget = &workItemWidgetStatusInputGQL{ Status: opt.Status, } } return input } // UpdateWorkItem updates a work item by fullPath and iid. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. func (s *WorkItemsService) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) { gid, resp, err := s.workItemGID(fullPath, iid, options...) if err != nil { return nil, resp, err } var queryBuilder strings.Builder if err := updateWorkItemTemplate.Execute(&queryBuilder, nil); err != nil { return nil, nil, err } q := GraphQLQuery{ Query: queryBuilder.String(), Variables: map[string]any{ "input": opt.wrap(gid), }, } var result struct { Data struct { WorkItemUpdate struct { WorkItem *workItemGQL `json:"workItem"` Errors []string `json:"errors"` } `json:"workItemUpdate"` } GenericGraphQLErrors } resp, err = s.client.GraphQL.Do(q, &result, options...) if err != nil { return nil, resp, err } if len(result.Errors) != 0 { return nil, resp, &GraphQLResponseError{ Err: errors.New("GraphQL query failed"), Errors: result.GenericGraphQLErrors, } } if len(result.Data.WorkItemUpdate.Errors) != 0 { return nil, resp, errors.New(result.Data.WorkItemUpdate.Errors[0]) } wiQL := result.Data.WorkItemUpdate.WorkItem if wiQL == nil { return nil, resp, ErrNotFound } return wiQL.unwrap(), resp, nil } // workItemUpdateInputGQL represents the GraphQL input structure for updating a work item. type workItemUpdateInputGQL struct { ID string `json:"id"` Title *string `json:"title,omitempty"` StateEvent *WorkItemStateEvent `json:"stateEvent,omitempty"` DescriptionWidget *workItemWidgetDescriptionInputGQL `json:"descriptionWidget,omitempty"` AssigneesWidget *workItemWidgetAssigneesInputGQL `json:"assigneesWidget,omitempty"` MilestoneWidget *workItemWidgetMilestoneInputGQL `json:"milestoneWidget,omitempty"` CRMContactsWidget *workItemWidgetCRMContactsUpdateInputGQL `json:"crmContactsWidget,omitempty"` HierarchyWidget *workItemWidgetHierarchyUpdateInputGQL `json:"hierarchyWidget,omitempty"` LabelsWidget *workItemWidgetLabelsUpdateInputGQL `json:"labelsWidget,omitempty"` StartAndDueDateWidget *workItemWidgetStartAndDueDateUpdateInputGQL `json:"startAndDueDateWidget,omitempty"` WeightWidget *workItemWidgetWeightInputGQL `json:"weightWidget,omitempty"` HealthStatusWidget *workItemWidgetHealthStatusInputGQL `json:"healthStatusWidget,omitempty"` IterationWidget *workItemWidgetIterationInputGQL `json:"iterationWidget,omitempty"` ColorWidget *workItemWidgetColorInputGQL `json:"colorWidget,omitempty"` StatusWidget *workItemWidgetStatusInputGQL `json:"statusWidget,omitempty"` } // workItemGID queries for a work item's Global ID using fullPath and iid. // Returns the Global ID string or ErrNotFound if the work item doesn't exist. // // API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitem func (s *WorkItemsService) workItemGID(fullPath string, iid int64, options ...RequestOptionFunc) (gidGQL, *Response, error) { q := GraphQLQuery{ Query: getWorkItemIDQuery, Variables: map[string]any{ "fullPath": fullPath, "iid": strconv.FormatInt(iid, 10), }, } var result struct { Data struct { Namespace struct { WorkItem struct { ID gidGQL `json:"id"` } `json:"workItem"` } } GenericGraphQLErrors } resp, err := s.client.GraphQL.Do(q, &result, options...) if err != nil { return gidGQL{}, resp, err } if len(result.Errors) != 0 { return gidGQL{}, resp, &GraphQLResponseError{ Err: fmt.Errorf("looking up global ID of %s#%d failed", fullPath, iid), Errors: result.GenericGraphQLErrors, } } id := result.Data.Namespace.WorkItem.ID if id.IsZero() { return gidGQL{}, resp, fmt.Errorf("looking up global ID of %s#%d failed: %w", fullPath, iid, ErrEmptyResponse) } return id, resp, nil } // workItemGQL represents the JSON structure returned by the GraphQL query. // It is used to parse the response and convert it to the more user-friendly WorkItem type. type workItemGQL struct { Loading Loading @@ -1152,3 +1499,19 @@ const ( WorkItemTypeEpic WorkItemTypeID = `gid://gitlab/WorkItems::Type/8` WorkItemTypeTicket WorkItemTypeID = `gid://gitlab/WorkItems::Type/9` ) // WorkItemStatusID represents the global ID of a work item status. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemsstatus // // Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions. type WorkItemStatusID string // WorkItemStatusID constants for the system-defined work item statuses. const ( WorkItemStatusToDo WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1` WorkItemStatusInProgress WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2` WorkItemStatusDone WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3` WorkItemStatusWontDo WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/4` WorkItemStatusDuplicate WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5` )