Skip to content

Draft: POC Custom status management API

What does this MR do and why?

The purpose of this MR is to build a POC for the management part of custom statuses. In particular the following:

  1. 🚧 Retrieve system-defined lifecycles and statuses
  2. Retrieve custom lifecycles and statuses
  3. Create and manage lifecycles with statuses
  4. Create and update statuses as standalone entities
  5. 🚧 migration from system-defined statuses to custom statuses
  6. 🚧 general migration of statuses due to either
    1. status is deleted from lifecycle (map to replacement)
    2. work item type is applied to different lifecycle (map statuses that don't exist in old lifecycle to replacement)

Contributes to #536365 (closed)

View lifecycles and statuses

image

No matter whether the namespace uses system-defined statuses or custom statuses there should be a single endpoint that returns all necessary data.

The current state of the POC only returns custom statuses. That will be changed shortly.

Use this GraphQL query:

Click to expand
query getLifecycles {
  namespace(fullPath: "flightjs") {
    id
    lifecycles {
      nodes {
        id
        name
        defaultOpenStatus {
          id
        }
				defaultClosedStatus {
          id
        }
				defaultDuplicateStatus {
          id
        }
        workItemTypes {
          id
          name
          iconName
        }
        statuses {
          id
					name
          iconName
          color
        }
      }
    }
  }
}

See example output with system-defined lifecycles/statuses:

Click to expand
{
  "data": {
    "namespace": {
      "id": "gid://gitlab/Group/24",
      "lifecycles": {
        "nodes": [
          {
            "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Lifecycle/1",
            "name": "Default",
            "defaultOpenStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1"
            },
            "defaultClosedStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3"
            },
            "defaultDuplicateStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5"
            },
            "workItemTypes": [
              {
                "id": "gid://gitlab/WorkItems::Type/1",
                "name": "Issue",
                "iconName": "issue-type-issue"
              },
              {
                "id": "gid://gitlab/WorkItems::Type/5",
                "name": "Task",
                "iconName": "issue-type-task"
              }
            ],
            "statuses": [
              {
                "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1",
                "name": "To do",
                "iconName": "status-waiting",
                "color": "#737278"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2",
                "name": "In progress",
                "iconName": "status-running",
                "color": "#1f75cb"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3",
                "name": "Done",
                "iconName": "status-success",
                "color": "#108548"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/4",
                "name": "Won't do",
                "iconName": "status-cancelled",
                "color": "#DD2B0E"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5",
                "name": "Duplicate",
                "iconName": "status-cancelled",
                "color": "#DD2B0E"
              }
            ]
          }
        ]
      }
    }
  },
  "correlationId": "01JSEWPVBZMKRQFZPFF0FHW09J"
}

image

See example output with custom statuses:

Click to expand
{
  "data": {
    "namespace": {
      "id": "gid://gitlab/Group/33",
      "lifecycles": {
        "nodes": [
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/12",
            "name": "Another lifecycle",
            "defaultOpenStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
            },
            "defaultClosedStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
            },
            "defaultDuplicateStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/49"
            },
            "workItemTypes": [
              {
                "id": "gid://gitlab/WorkItems::Type/5",
                "name": "Task",
                "iconName": "issue-type-task"
              }
            ],
            "statuses": [
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
                "name": "Neu",
                "iconName": "status-waiting",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
                "name": "In Arbeit",
                "iconName": "status-running",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/49",
                "name": "Nö",
                "iconName": "status-cancelled",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
                "name": "Erledigttt",
                "iconName": "status-success",
                "color": "#ff3300"
              }
            ]
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/11",
            "name": "API Lifecycle for the win",
            "defaultOpenStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
            },
            "defaultClosedStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50"
            },
            "defaultDuplicateStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
            },
            "workItemTypes": [
              {
                "id": "gid://gitlab/WorkItems::Type/5",
                "name": "Task",
                "iconName": "issue-type-task"
              }
            ],
            "statuses": [
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
                "name": "Neu",
                "iconName": "status-waiting",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
                "name": "In Arbeit",
                "iconName": "status-running",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
                "name": "Erledigttt",
                "iconName": "status-success",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50",
                "name": "Machenwanich",
                "iconName": "status-cancelled",
                "color": "#00ff00"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/51",
                "name": "Unused",
                "iconName": "status-neutral",
                "color": "#000000"
              }
            ]
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/9",
            "name": "Super duper lifecycle",
            "defaultOpenStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
            },
            "defaultClosedStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
            },
            "defaultDuplicateStatus": {
              "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
            },
            "workItemTypes": [
              {
                "id": "gid://gitlab/WorkItems::Type/5",
                "name": "Task",
                "iconName": "issue-type-task"
              }
            ],
            "statuses": [
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
                "name": "Neu",
                "iconName": "status-waiting",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
                "name": "In Arbeit",
                "iconName": "status-running",
                "color": "#ff3300"
              },
              {
                "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
                "name": "Erledigttt",
                "iconName": "status-success",
                "color": "#ff3300"
              }
            ]
          }
        ]
      }
    }
  },
  "correlationId": "01JS23BHJ4QSD6CEYN9CX0VEQH"
}

image

Get statuses of namespace

Depending on how we decide to create/update statuses in the management UI, we might need to fetch a list of existing statuses to attach to a lifecycle. We could maybe mask that by letting the update mutation handle that. So still TBD.

Example query:

Click to expand
query getStatuses {
  namespace(fullPath: "flightjs") {
    id
		statuses {
      nodes {
        id
        name
        iconName
        color
        category
        description
      }
    }
  }
}

Example output:

Click to expand
{
  "data": {
    "namespace": {
      "id": "gid://gitlab/Group/33",
      "statuses": {
        "nodes": [
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/51",
            "name": "Unused",
            "iconName": "status-neutral",
            "color": "#000000",
            "category": "TRIAGE",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50",
            "name": "Machenwanich",
            "iconName": "status-cancelled",
            "color": "#00ff00",
            "category": "CANCELLED",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/49",
            "name": "Nö",
            "iconName": "status-cancelled",
            "color": "#ff3300",
            "category": "CANCELLED",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
            "name": "Erledigttt",
            "iconName": "status-success",
            "color": "#ff3300",
            "category": "DONE",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
            "name": "In Arbeit",
            "iconName": "status-running",
            "color": "#ff3300",
            "category": "IN_PROGRESS",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
            "name": "Neu",
            "iconName": "status-waiting",
            "color": "#ff3300",
            "category": "TO_DO",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/6",
            "name": "Updated via API",
            "iconName": "status-waiting",
            "color": "#5CB85C",
            "category": "TO_DO",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/5",
            "name": "Created via API second edition",
            "iconName": "status-waiting",
            "color": "#5CB85C",
            "category": "TO_DO",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/4",
            "name": "Created via API",
            "iconName": "status-waiting",
            "color": "#5CB85C",
            "category": "TO_DO",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/3",
            "name": "Duplicate",
            "iconName": "status-cancelled",
            "color": "#DD2B0E",
            "category": "CANCELLED",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/2",
            "name": "Done",
            "iconName": "status-success",
            "color": "#108548",
            "category": "DONE",
            "description": null
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/1",
            "name": "To do",
            "iconName": "status-waiting",
            "color": "#737278",
            "category": "TO_DO",
            "description": null
          }
        ]
      }
    }
  },
  "correlationId": "01JS23HSDHTGMXKFQ852WTPNEM"
}

image

Create status

Again depending on which option we want to use, we might want to create statuses using a dedicated endpoint or create them with updating/creating lifecycles. This showcases how a status create could look like:

Example mutation:

Click to expand
mutation createStatus {
  statusesCreate(input: {
    groupPath: "flightjs",
    name: "Created via API fourth edition",
    category: TO_DO,
    color: "#5CB85C",
    description: "This is a good status."
  }) {
    status {
      id
      name
      category
      color
      iconName
      description
    }
    errors
  }
}

Example output:

{
  "data": {
    "statusesCreate": {
      "status": {
        "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/52",
        "name": "Created via API fourth edition",
        "category": "TO_DO",
        "color": "#5CB85C",
        "iconName": "status-waiting",
        "description": "This is a good status."
      },
      "errors": []
    }
  },
  "correlationId": "01JS23Q1RD50S752BVKC43RHBK"
}

image

Update status

Example mutation:

Click to expand
mutation updateStatus {
  statusesUpdate(input: {
    groupPath: "flightjs",
    id: "gid://gitlab/WorkItems::Statuses::Custom::Status/6"
    name: "Updated via APIII",
    category: TO_DO,
    color: "#5CB85C",
    description: "This is a good status."
  }) {
    status {
      id
      name
      category
      color
      iconName
      description
    }
    errors
  }
}

Example output:

Click to expand
{
  "data": {
    "statusesUpdate": {
      "status": {
        "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/6",
        "name": "Updated via APIII",
        "category": "TO_DO",
        "color": "#5CB85C",
        "iconName": "status-waiting",
        "description": "This is a good status."
      },
      "errors": []
    }
  },
  "correlationId": "01JS23RT2T577C93R9RVGV42WH"
}

image

Create lifecycle

This is an alternative proposal. Instead of creating and updating statuses using dedicated endpoints, we could also create and update them together with the lifecycle.

This would map nicely with the structure of the query and allow the frontend to just push updates that have been done for the lifecycle without the need to perform separate mutations. More on that on the update mutation.

image

Please note that the default statuses are indexes of the given status array! More on that on the update mutation.

Example mutation:

Click to expand
mutation createLifecycle {
  lifecyclesCreate(input: {
    groupPath: "flightjs",
    name: "Super duper lifecycle",
    statuses: [
      {
        name: "Neu",
        category: TO_DO,
        color: "#ff3300"
      },
      {
        # Can also be referenced by name. And update a single attribute (color).
        name: "In Arbeit",
        color: "#ff3300"
      },
      {
        # If name doesn't reference an existing status, it'll create a new one.
        name: "Erledigt",
        category: :DONE
        color: "#ff3300"
      },
      {
        # Can also be an existing status referenced by ID
        id: "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
      },
    ],
		workItemTypeIds: [
      "gid://gitlab/WorkItems::Type/5",
    ],
    defaultOpenStatusIndex: 0,
    defaultClosedStatusIndex: 2,
    defaultDuplicateStatusIndex: 2
  }) {
    lifecycle {
      id
        name
        defaultOpenStatus {
          id
        }
				defaultClosedStatus {
          id
        }
				defaultDuplicateStatus {
          id
        }
        workItemTypes {
          id
          name
          iconName
        }
        statuses {
          id
					name
          iconName
          color
        }
    }
    errors
  }
}

Example output:

Click to expand
{
  "data": {
    "lifecyclesCreate": {
      "lifecycle": {
        "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/9",
        "name": "Super duper lifecycle",
        "defaultOpenStatus": {
          "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
        },
        "defaultClosedStatus": {
          "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
        },
        "defaultDuplicateStatus": {
          "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
        },
        "workItemTypes": [
          {
            "id": "gid://gitlab/WorkItems::Type/5",
            "name": "Task",
            "iconName": "issue-type-task"
          }
        ],
        "statuses": [
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
            "name": "Neu",
            "iconName": "status-waiting",
            "color": "#ff3300"
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
            "name": "In Arbeit",
            "iconName": "status-running",
            "color": "#ff3300"
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
            "name": "Erledigt",
            "iconName": "status-success",
            "color": "#ff3300"
          }
        ]
      },
      "errors": []
    }
  },
  "correlationId": "01JRZEQXDY93HHX834Q63DJ57D"
}

image

Update lifecycle and statuses

Add statuses to lifecycle (existing and new and update attributes)

From the frontend perspective you can just send everything over that you have and the backend will handle the rest. For example:

  1. Reference a status by ID, BE assume it exists. You provide additional attributes? BE assumes you want to change those.
  2. Reference status by name, BE checks whether it exists (if yes, also updates given attributes), if not creates it.
  3. You don't provide any statuses? Statuses remain unchanged. But you cannot set default (open/closed/duplicate) statuses.

Delete a status

Simply pass the status array without that status. The status will be unassigned from the lifecycle.

Status reordering and position

🚧 Concept but unimplemented yet: I imagine it to work like this to keep it super simple. You send a list of statuses and we take that list in the given order. In the BE we can simply delete the lifecycle/status associations and add them again. This way we might get around setting a position explicitly.

Default (open/closed/duplicate) statuses

They are defined by it's index in the statuses array. Why? Because this way we can also set a non existing status as open default.

Work item types

You just define the attached work item types.

🚧 Concept but unimplemented yet: Because a work item type cannot be without a lifecycle I'd imagine when you assign a WIT to a lifecycle we'll remove that type from the other lifecycle. Note that there might be a mapping necessary. More on that below.

Map old to new statuses

🚧 Concept but unimplemented yet:

There are two actions that may make a migration necessary:

  1. status is deleted from lifecycle (map to replacement)
  2. work item type is applied to different lifecycle (map statuses that don't exist in old lifecycle to replacement)

You could add a mapping (from old status to new status) to the update mutation and only if that exists, those actions above would be performed. The mapping enqueues a status migration which will be handled in a background job.

Migrate from system-defined statuses to custom statuses

🚧 Concept but unimplemented yet:

To reduce the complexity for the frontend, it should handle GIDs of system defined statuses as if they were custom statuses (same for lifecycles). If the BE receives a mutation that a) contains a system-defined status/lifecycle GID or b) is for a namespace that doesn't have custom statuses, it would first migrate system-defined statuses to custom statuses and them process the request. To make sure this works properly we'd then change the params of the request from the system-defined GIDs to the now newly created custom GIDs and then continue as normal.

This way the flow stays the same and system-defined GIDs can be treated the same. When the frontend then applies the result to it's data tree, it'll only use custom entities.

Click to expand
mutation updateLifecycle {
  lifecyclesUpdate(input: {
    groupPath: "flightjs",
    id: "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/11"
    statuses: [
      {
        id: "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
      },
      {
        id: "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
      },
      {
        id: "gid://gitlab/WorkItems::Statuses::Custom::Status/47"
      },
      {
        id: "gid://gitlab/WorkItems::Statuses::Custom::Status/50"
      },
      {
        name: "Unused"
        category: TRIAGE
        color: "#000000"
      }
    ]
    defaultOpenStatusIndex: 0,
    defaultClosedStatusIndex: 2,
    defaultDuplicateStatusIndex: 3
    workItemTypeIds: [
      "gid://gitlab/WorkItems::Type/5"
    ]
  }) {
    lifecycle {
      id
        name
        defaultOpenStatus {
          id
        }
				defaultClosedStatus {
          id
        }
				defaultDuplicateStatus {
          id
        }
        workItemTypes {
          id
          name
          iconName
        }
        statuses {
          id
					name
          iconName
          color
        }
    }
    errors
  }
}

Example output:

Click to expand
{
  "data": {
    "lifecyclesUpdate": {
      "lifecycle": {
        "id": "gid://gitlab/WorkItems::Statuses::Custom::Lifecycle/11",
        "name": "API Lifecycle for the win",
        "defaultOpenStatus": {
          "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46"
        },
        "defaultClosedStatus": {
          "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50"
        },
        "defaultDuplicateStatus": {
          "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48"
        },
        "workItemTypes": [
          {
            "id": "gid://gitlab/WorkItems::Type/5",
            "name": "Task",
            "iconName": "issue-type-task"
          }
        ],
        "statuses": [
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/46",
            "name": "Neu",
            "iconName": "status-waiting",
            "color": "#ff3300"
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/47",
            "name": "In Arbeit",
            "iconName": "status-running",
            "color": "#ff3300"
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/48",
            "name": "Erledigttt",
            "iconName": "status-success",
            "color": "#ff3300"
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/50",
            "name": "Machenwanich",
            "iconName": "status-cancelled",
            "color": "#00ff00"
          },
          {
            "id": "gid://gitlab/WorkItems::Statuses::Custom::Status/51",
            "name": "Unused",
            "iconName": "status-neutral",
            "color": "#000000"
          }
        ]
      },
      "errors": []
    }
  },
  "correlationId": "01JS21XR76W8VYEB52QPM1CG9N"
}

image

References

How to set up and validate locally

  1. Enable the work_item_status_feature_flag
  2. Open http://127.0.0.1:3000/-/graphql-explorer
  3. Start trying out the queries and mutations. Because they only work for custom entities right now, I'd suggest to start with creating statuses/lifecycles first.
  4. Please also try out the several options of the lifecyclesUpdate mutation.
Edited by Marc Saleiko

Merge request reports

Loading