Skip to content

Adds AttachWorkItemType mutation for lifecycles

What does this MR do and why?

This code implements a feature that allows attaching work item types to custom lifecycles in GitLab. The main changes include:

  1. Core Functionality: A new service (AttachWorkItemTypeService) was created that handles moving work item types from one lifecycle to another, with proper validation and error handling for cases like insufficient permissions, duplicate attachments, or invalid lifecycle types.
  2. Status Mapping: The system now supports mapping statuses between lifecycles when switching. This ensures that if a status exists in the old lifecycle but not the new one, users can specify which status should replace it. The mapping logic includes validation to ensure mapped statuses have the same state (open/closed) and prevents circular references.
  3. GraphQL API Updates: The GraphQL mutation was updated from a mock implementation to actually call the new service, and the status mapping input types were made more specific to only accept work item status IDs instead of generic global IDs. No multi-version-compatibility problems, because we're only getting more specific.
  4. Data Tracking: Analytics tracking was added to monitor when work item types are attached to custom lifecycles, including metrics for counting unique namespaces and total attachments.
  5. Improved Validation: Enhanced validation for edge cases in the update mutation.

Notes for reviewer

  1. A work item type always has a lifecycle that contains statuses that can be applied to work items
  2. A lifecycle can be attached to more than one work item type
  3. You can attach a work item type to a different lifecycle
  4. If statuses of the old lifecycle are in use you need to provide a mapping to statuses of the new lifecycle
  5. Mappings need to be within the same state "open/closed"

See the description of the first mapping management MR for additional details about the mapping logic.

This is part of a series of MRs to implement this feature:

  1. Adds work_item_custom_status_mapping table and ... (!203127 - merged)
  2. Add date range validation to work item status m... (!203961 - merged)
  3. Realtime status mapping resolution (!204229 - merged) (resolving work item statuses with mappings)
  4. Adding/managing mappings
    1. Lifecycle Update mutation
    2. 🎯 we're here Lifecycle AttachWorkItemType mutation
  5. Filtering with mapping

References

  1. BE: Change lifecycle (#557011 - closed)
  2. Change lifecycle (#557009 - closed)
  3. BE: Add mapping (#558275 - closed)
  4. Provide mapping in lifecycle mutations (#566529 - closed)
  5. Draft: POC for mappings approach for custom sta... (!199448)

Screenshots or screen recordings

Queries and query plans

Delete work item type from lifecycle

Query plan

DELETE FROM work_item_type_custom_lifecycles
WHERE
    work_item_type_custom_lifecycles.lifecycle_id = 25 AND
    work_item_type_custom_lifecycles.work_item_type_id = 5;

Expire unbounded mappings to new statuses

Query plan

UPDATE work_item_custom_status_mappings
SET
    valid_until = '2025-09-23 15:00:25.498402'
WHERE
    work_item_custom_status_mappings.namespace_id = 110 AND
    work_item_custom_status_mappings.work_item_type_id = 1 AND
    work_item_custom_status_mappings.new_status_id IN ( 102, 103, 104 ) AND
    work_item_custom_status_mappings.valid_until IS NULL;

Prevent self references when eliminating mapping chains

Query plan

DELETE FROM work_item_custom_status_mappings
WHERE
    work_item_custom_status_mappings.namespace_id = 110 AND
    work_item_custom_status_mappings.old_status_id = 103 AND
    work_item_custom_status_mappings.new_status_id = 102 AND
    work_item_custom_status_mappings.work_item_type_id = 1;

How to set up and validate locally

Note: We won't test all paths here but these steps will walk you through valid use cases (attach without mapping, attach with mapping):

  1. Enable the work_item_status_mvc2 feature flag:

    Feature.enable(:work_item_status_mvc2)
  2. Create a new group

  3. Add custom statuses for the group. Either through the UI via group settings --> issues --> edit statuses or

    group = Group.find(INSERT_GROUP_ID)
    lifecycle = FactoryBot.create(:work_item_custom_lifecycle, :for_issues, namespace: group)
  4. Add a second lifecycle for the group through the UI or

    another_lifecycle = FactoryBot.create(:work_item_custom_lifecycle, namespace: group)
  5. If you take a look at the settings page for statuses, it should look somewhat like this image

  6. If you created them from the console, you can get the Ids from the variables, if not you can inspect the GraphQL query to get the Ids of the lifecycles.

  7. Now use the AttachWorkItemType mutation to attach issues to the other lifecycle (see mutation and variables, add your paths and ids). This should work without a mapping because we don't have work item types in the group yet.

     mutation ($input: LifecycleAttachWorkItemTypeInput!) {
       lifecycleAttachWorkItemType(input: $input) {
         lifecycle {
           id
           name
         }
         errors
       }
     }
    
    
     {
       "input": {
         "namespacePath": "attach-work-item-type-mr",
         "workItemTypeId": "gid://gitlab/WorkItems::Type/1",
         "lifecycleId": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/26"
       }
     }
  8. The result should look somewhat like this. If you reload the settings page issues should now be attached to the other lifecycle

     {
       "data": {
         "lifecycleAttachWorkItemType": {
           "lifecycle": {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/26",
             "name": "Custom Lifecycle 2"
           },
           "errors": []
         }
       }
     }

    image

  9. Now create a new project and a new issue in that project and assign a status or use the default.

  10. Now try to change the lifecycle back to the original lifecycle (exchange the ids in the mutation to point to the "old" lifecycle) and see that it fails, because now a status is in use and doesn't have a mapping. These are the variables I used and the response I got:

     {
       "input": {
         "namespacePath": "attach-work-item-type-mr",
         "workItemTypeId": "gid://gitlab/WorkItems::Type/1",
         "lifecycleId": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/25"
       }
     }
     {
       "data": {
         "lifecycleAttachWorkItemType": {
           "lifecycle": null,
           "errors": [
             "Cannot remove status 'Mae White' from lifecycle because it is in use and no mapping is provided"
           ]
         }
       }
     }
  11. So now let's add a mapping from the status in question to the default status of the "original" lifecycle. If you don't have the Id's handy, use that snippet to find the ids for both default open statsues:

    group = Group.find(INSERT_GROUP_ID)
    group.custom_lifecycles.map { |l| l.default_open_status.to_gid }
  12. Send the mutation with this mapping (old is the higher ID, new is the lower ID). The request should be successful.

     {
       "input": {
         "namespacePath": "attach-work-item-type-mr",
         "workItemTypeId": "gid://gitlab/WorkItems::Type/1",
         "lifecycleId": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/25",
         "statusMappings": [
           {
             "oldStatusId": "gid://gitlab/WorkItems::Statuses::Custom::Status/105",
             "newStatusId": "gid://gitlab/WorkItems::Statuses::Custom::Status/102"
           }
         ]
       }
     }
  13. Reload the settings page. Issues should be assigned to the original lifecycle again.

  14. Reload the issue and see it now displays the status of the original lifecycle because of the mapping.

  15. You're a rockstar 🤘 You did it 🎸 🥁

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Marc Saleiko

Merge request reports

Loading