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:
- If you remove a status, a mapping will be added which is valid for all work items that currently have this status
- 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.
- If you remove the status again, a new mapping will be added which is valid after the old mapping.
- 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:
- Adds work_item_custom_status_mapping table and ... (!203127 - merged)
- Add date range validation to work item status m... (!203961 - merged)
- Realtime status mapping resolution (!204229 - merged) (resolving work item statuses with mappings)
-
🎯 we're here (adding/managing mappings) - Filtering with mapping
References
- Provide mapping in lifecycle mutations (#566529 - closed)
- BE: Add mapping (#558275 - closed)
- 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.
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
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
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
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
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:
-
Enable the
work_item_status_mvc2
feature flag:Feature.enable(:work_item_status_mvc2)
-
Create a new group
-
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)
-
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 } } } } }
-
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" } ] } ] } } } }
-
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" } ] } }
-
The result should show your newly added status.
-
Now create a project in that group and an issue and assign the "To be deleted" status.
-
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" } ] } }
-
You should get an error saying the status is in use or no mapping provided.
-
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" } ]
-
The request should be successful and show three statuses.
-
Now go back to the issue and reload it. It should show the default open status and not the status you set before.
-
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" } ] } }
-
The request should be successful and you should see four statuses again.
-
Reload the issue and see it still has the mapped status although you "readded" the status.
-
Create a new issue with the status to be deleted.
-
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" } ] } }
-
The response should be successful and show you four statuses.
-
Now reload the first issue which should still point to the first status because of the first mapping.
-
Now reload the second issue which should now point to the "Another new status" status because of the new mapping.
-
🎸 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.