Skip to content

Adds mapping in lifecycle mutations

What does this MR do and why?

Status Mapping for Work Item Lifecycles

This MR implements mapping logic for work item statuses, allowing seamless transitions when statuses are removed or changed. Key features include:

  • Status Preservation: When removing statuses that are in use, the system creates mappings to preserve work item state integrity
  • Time-Bound Mappings: Mappings include validity periods (valid_from/valid_until) to track status changes over time
  • Chain Prevention: Automatically prevents mapping chains (A→B→C) by updating existing mappings to point directly to final targets
  • System-to-Custom Conversion: Handles conversion from system-defined statuses to custom statuses with proper mapping preservation
  • Conflict Resolution: Automatically expires conflicting mappings when new ones are created
  • Namespace Scoping: All mappings are scoped to specific namespaces and work item types

This implementation ensures work items maintain appropriate statuses even when lifecycle configurations change, providing a robust solution for status management across the application.

This is a visualization of how time-bound mappings work:

image

  1. If you remove a status, a mapping will be added which is valid for all work items that currently have this status
  2. If for some reason you add the same status again to the lifecycle, we'll limit the validity of the mapping, so it doesn't affect items from now on.
  3. If you remove the status again, a new mapping will be added which is valid after the old mapping.
  4. This logic goes on an on, but should be sufficient to illustrate how it works.

This is MR four in 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. 🎯 we're here (adding/managing mappings)
  5. Filtering with mapping

References

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

Screenshots or screen recordings

Queries and query plans

Adding bounds to unbound mappings

as a safeguard to prevent invalid mappings.

Query plan

UPDATE
    "work_item_custom_status_mappings"
SET
    "valid_until" = '2025-09-18 09:22:04.407028'
WHERE
    "work_item_custom_status_mappings"."namespace_id" = 109
    AND "work_item_custom_status_mappings"."old_status_id" = 92
    AND "work_item_custom_status_mappings"."work_item_type_id" IN (
        SELECT
            "work_item_types"."id"
        FROM
            "work_item_types"
            INNER JOIN "work_item_type_custom_lifecycles" ON "work_item_types"."id" = "work_item_type_custom_lifecycles"."work_item_type_id"
        WHERE
            "work_item_type_custom_lifecycles"."lifecycle_id" = 23)
    AND "work_item_custom_status_mappings"."valid_until" IS NULL

Prevent mapping chains

Query plan

UPDATE
    "work_item_custom_status_mappings"
SET
    "new_status_id" = 92
WHERE
    "work_item_custom_status_mappings"."namespace_id" = 109
    AND "work_item_custom_status_mappings"."new_status_id" = 95
    AND "work_item_custom_status_mappings"."work_item_type_id" = 1

Scope originating_from_status

Query plan

SELECT
    "work_item_custom_status_mappings".*
FROM
    "work_item_custom_status_mappings"
WHERE
    "work_item_custom_status_mappings"."namespace_id" = 22
    AND "work_item_custom_status_mappings"."old_status_id" = 68
    AND "work_item_custom_status_mappings"."work_item_type_id" = 1

Exists checks

Query plan

SELECT
    1 AS one
FROM
    "work_item_custom_status_mappings"
WHERE
    "work_item_custom_status_mappings"."namespace_id" = 22
    AND "work_item_custom_status_mappings"."new_status_id" = 68
LIMIT 1

Query plan

SELECT
    1 AS one
FROM
    work_item_custom_lifecycle_statuses
WHERE
    work_item_custom_lifecycle_statuses.namespace_id = 22 AND
    work_item_custom_lifecycle_statuses.status_id = 68
LIMIT 1;

How to set up and validate locally

Note: We won't test all paths here especially the system defined paths because you'd need a new group for each try. These steps will walk you through the add/removal/add/removal scenarios:

  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.find(INSERT_GROUP_ID)
    lifecycle = FactoryBot.create(:work_item_custom_lifecycle, :for_issues, namespace: group)
  4. Open the GraphQL Explorer or any other tool and get the lifecycle of the group:

     query {
       namespace(fullPath: "mr-verification") {
         id
         lifecycles {
           nodes {
             id
             name
             statuses {
               id
               name
             }
           }
         }
       }
     }
  5. Result should look somewhat like this:

     {
       "data": {
         "namespace": {
           "id": "gid://gitlab/Group/105",
           "lifecycles": {
             "nodes": [
               {
                 "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/20",
                 "name": "Custom Lifecycle 1",
                 "statuses": [
                   {
                     "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/81",
                     "name": "Tyree Gerlach"
                   },
                   {
                     "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/82",
                     "name": "Janet Schmeler"
                   },
                   {
                     "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/83",
                     "name": "Garrett Schiller"
                   }
                 ]
               }
             ]
           }
         }
       }
     }
  6. Now add a status using this mutation and input variables with your own group path and status ids:

     mutation LifecycleUpdate($input: LifecycleUpdateInput!) {
       lifecycleUpdate(input: $input) {
         lifecycle {
           id
           name
           statuses {
             id
             name
           }
         }
         errors
       }
     }
    
     {
       "input": {
         "namespacePath": "mr-verification",
         "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/20",
         "statuses": [
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/81"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/82"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/83"
           },
           {
             "name": "To be deleted",
             "color": "#737278",
             "category": "TO_DO"
           }
         ]
       }
     }
  7. The result should show your newly added status.

  8. Now create a project in that group and an issue and assign the "To be deleted" status.

  9. Now try to remove the created status using this mutation and variables using your group path and status ids:

     mutation LifecycleUpdate($input: LifecycleUpdateInput!) {
       lifecycleUpdate(input: $input) {
         lifecycle {
           id
           name
           statuses {
             id
             name
           }
         }
         errors
       }
     }
    
     {
       "input": {
         "namespacePath": "mr-verification",
         "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/20",
         "statuses": [
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/81"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/82"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/83"
           }
         ]
       }
     }
  10. You should get an error saying the status is in use or no mapping provided.

  11. Now provide a mapping. Using the same mutation and variables plus this mapping. Use the the to be deleted status as old_status_id and the default open status (first) as the target.

     "statusMappings": [
       {
         "oldStatusId": "gid://gitlab/WorkItems::Statuses::Custom::Status/84",
         "newStatusId": "gid://gitlab/WorkItems::Statuses::Custom::Status/81"
       }
     ]
  12. The request should be successful and show three statuses.

  13. Now go back to the issue and reload it. It should show the default open status and not the status you set before.

  14. Now add the status again and add another new status. Use the mutation from above but include the deleted status by Id or name again:

     {
       "input": {
         "namespacePath": "mr-verification",
         "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/20",
         "statuses": [
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/81"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/82"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/83"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/84"
           },
           {
             "name": "Another new status",
             "color": "#737278",
             "category": "TO_DO"
           }
         ]
       }
     }
  15. The request should be successful and you should see four statuses again.

  16. Reload the issue and see it still has the mapped status although you "readded" the status.

  17. Create a new issue with the status to be deleted.

  18. Now remove the "To be deleted" status again and provide a mapping to the "Another new status". You can copy the IDs from the response.

     {
       "input": {
         "namespacePath": "mr-verification",
         "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/20",
         "statuses": [
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/81"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/82"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/83"
           },
           {
             "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/91"
           }
         ],
         "statusMappings": [
           {
             "oldStatusId": "gid://gitlab/WorkItems::Statuses::Custom::Status/84",
             "newStatusId": "gid://gitlab/WorkItems::Statuses::Custom::Status/91"
           }
         ]
       }
     }
  19. The response should be successful and show you four statuses.

  20. Now reload the first issue which should still point to the first status because of the first mapping.

  21. Now reload the second issue which should now point to the "Another new status" status because of the new mapping.

  22. 🎸 Congrats you finished all steps 🤘 Thanks 🙇

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