Help us learn about your current experience with the documentation. Take the survey.

工作项和工作项类型

工作项引入了一个灵活的模型,用于标准化和扩展 GitLab 中的问题跟踪功能。 通过工作项,您可以定义不同类型的工作项,并使用各种小部件进行自定义, 以满足特定需求 - 无论您是在跟踪缺陷、事件、测试用例还是其他工作单元。 这份架构文档涵盖了工作项和工作项类型的开发细节和实现策略。

挑战

问题有望成为协作的中心枢纽。 我们需要接受这样一个事实:不同的问题类型需要不同的字段和上下文, 具体取决于它们被用来完成什么工作。例如:

  • 缺陷需要列出重现步骤。
  • 事件需要引用堆栈跟踪以及其他仅与该事件相关的上下文信息。

与其让每个对象类型分化为独立的模型,我们可以基于一个底层的通用模型进行标准化, 该模型可以通过其包含的小部件(一个或多个属性)进行自定义。

以下是当前问题使用中的一些问题以及我们研究工作项的原因:

  • 使用标签来显示问题类型很繁琐,并且使报告视图更加复杂。

  • 问题类型是标签的前两大用例之一,因此为它们提供一流的支持是合理的。

  • 随着我们向问题添加更多功能,问题开始变得杂乱,而且它们并不完美:

    • 没有一致的模式来展示与其他对象的关系。
    • 由于我们使用标签来区分不同类型的问题,因此不同类型问题之间缺乏一致的交互模型。
    • 各种问题类型的实现缺乏灵活性和可扩展性。
  • Epic、问题、需求等在常见交互中都有相似但足够细微的差异, 用户需要掌握一个复杂的心理模型来理解它们各自的行为方式。

  • 问题不足以支持它们需要促进的所有新兴工作。

  • 随着我们扩展问题类型,使其超越问题跟踪的核心角色, 转而支持不同的工作项类型并处理逻辑和结构差异, 代码库的可维护性和功能开发变得越来越具挑战性。

  • 新功能通常通过一流的对象实现,这些对象通过共享的关注点从问题导入行为。 这导致了重复的工作,并最终导致常见交互之间存在微小差异。 这导致了不一致的用户体验。

工作项术语

为了避免混淆并确保沟通高效, 我们在讨论工作项时将 exclusively 使用以下术语。此列表是工作项术语的单一事实来源 (SSoT)

术语 描述 错误使用示例 应该使用
work item type 工作项的类别;例如:issue、requirement、test case、incident 或 task Epics 将最终成为 issues Epics 将最终成为 work item type
work item work item type 的实例
work item view 渲染任何类型工作项的新前端视图 这应该在新视图中渲染 这应该在 work item view 中渲染
legacy object 已被或将被转换为 Work Item Type 的对象 Epics 将从独立/旧式/前对象迁移到 work item type Epics 将从 legacy object 转换为 work item type
legacy issue view 用于渲染问题和事件的现有视图 问题继续在旧视图中渲染 问题继续在 legacy issue view 中渲染
issue 现有的问题模型
issuable 任何当前使用 issuable 模型的模型(issues、epics 和 MRs) Incidents 是一个 issuable Incidents 是一个 work item type
widget 用于展示或允许与特定工作项数据交互的 UI 元素

一些术语过去曾被使用,但现已变得令人困惑,现已不再推荐使用。

术语 描述 错误使用示例 应该使用
issue type 过去用于指代工作项类别的一种方式 Tasks 是一个 issue type Tasks 是一个 work item type

工作项开发

在开发过程中,工作项通过三个阶段推进,通过使用功能标志进行管理:

  1. work_items_alpha 用于内部团队测试(gitlab-org/plan-stage)。
  2. work_items_beta 用于更广泛的内部 GitLab 测试(gitlab-orggitlab-com)。
  3. work_items,默认为 SaaS 和自托管环境启用。

其他组可能包含在内。有关最新信息,请在 chatops 中查询功能标志。

有关这些功能标志的更多信息,请参见 工作项架构蓝图

迁移策略

WI 模型将构建在现有的 Issue 模型之上,我们将逐步将 Issue 模型代码迁移到 WI 模型。

一种实现方法是:

class WorkItems::WorkItem < ApplicationRecord
  self.table_name = 'issues'

  # ... 所有当前 issue.rb 的代码
end

class Issue < WorkItems::WorkItem
  # 不要在此类中添加代码,请添加到 WorkItems::WorkItem
end

我们已经在 issues 表中通过 issue_type 列使用了 WITs 的概念。 有 issueincidenttest_case 问题类型。为了扩展这一点, 以便将来允许用户定义自定义 WITs,我们将把 issue_type 移动到一个单独的表中:work_item_typesissue_typework_item_types 的迁移过程将包括为所有顶级组创建一组 WITs, 如 this epic 中所述。

起初,定义 WIT 只能在顶级组中进行,然后由子组继承。 我们将在后续迭代中研究在子组级别定义新 WITs 的可能性。

引入 work_item_types

例如,假设有三个顶级组,ID 分别为:111213。同时, 假设有以下基础类型:issue: 0incident: 1、`test_case: 2。

相应的 work_item_types 记录:

namespace_id base_type title
11 0 Issue
11 1 Incident
11 2 Test Case
12 0 Issue
12 1 Incident
12 2 Test Case
13 0 Issue
13 1 Incident
13 2 Test Case

我们将执行以下操作来实现这一点:

  1. issues 表添加一个 work_item_type_id 列。

  2. 确保在创建或更新问题时,我们同时写入 issues#issue_typeissues#work_item_type_id 列。

  3. 回填 work_item_type_id 列,使其指向与问题项目顶级组对应的 work_item_types#id。例如:

    issue.project.root_group.work_item_types.where(base_type: issue.issue_type).first.id.
  4. issues#work_item_type_id 填充完成后,我们可以将查询从使用 issue_type 切换到使用 work_item_type_id

要引入新的 WIT 有两种选择:

  • 遵循上述过程的第一步。我们仍然需要运行一个迁移, 为所有顶级组添加一个新的 WIT,以便所有用户都可以使用该 WIT。 除了长时间运行的迁移外,我们还需要向 work_item_types 插入数百万条记录。 对于不希望或不需要在其工作流程中添加额外 WITs 的用户来说,这可能是不受欢迎的。
  • 创建一个选择加入的流程,这样只有当客户选择加入时, 才会为特定顶级组创建 work_item_types 中的记录。 然而,这意味着新引入的工作项类型的可发现性较低。

工作项类型小部件

小部件是可以在工作项上存在的单个组件。该组件可以用于一个或多个工作项类型, 并在实施时可以进行轻微的自定义。

小部件包含前端 UI(如果存在)以及用于呈现和管理小部件使用的任何数据的关联逻辑。 数据模型和小部件之间可以存在一对多的连接。这意味着可以有多个小部件使用或管理相同的数据, 并且它们可以同时存在(例如,只读摘要小部件和可编辑详情小部件, 或显示同一模型两个不同过滤视图的两个小部件)。

小部件应通过其目的进行区分。在可能的情况下,这个目的应该被抽象到最高合理的级别, 以最大化可重用性。例如,用于管理"任务"的小部件被构建为"子项"。 与其管理一种类型的子项,它被抽象为管理任何子项。

所有 WITs 将共享相同的预定义小部件池,并通过在特定 WIT 上激活哪些小部件来进行自定义。 每个属性(列或关联)都将成为具有自封装功能的小部件,无论它属于哪个 WIT。 因为任何 WIT 都可以有任意小部件,我们只需要定义哪些小部件对特定 WIT 是激活的。 因此,在切换特定工作项的类型后,我们会显示一组不同的小部件。

阅读更多关于工作项小部件的信息以及如何创建新的小部件。

小部件元数据

为了使用相应的激活小部件自定义每个 WIT,我们需要一个数据结构来将每个 WIT 映射到特定的小部件。

目的是让工作项类型具有高度可配置性,既可以由 GitLab 为客户实现各种工作项方案 (一个有主见的 GitLab 工作流程,或 SAFe 5 等),最终也可以让客户自定义自己的工作流程。

在这种情况下,工作项方案将被定义为一组具有特定特征(某些小部件启用,其他不启用)的类型, 例如 Epic、Story、Bug 和 Task 等。

随着我们构建新的工作项架构,我们希望能够以非常灵活的方式定义这些各种类型。 让 GitLab 首先使用这个系统(不引入客户自定义)可以让我们更好地构建初始系统。

工作项的 base_type 用于定义每种类型可用小部件的静态映射(当前状态), 这个定义应该存储在数据库表中。WIT 小部件元数据的精确结构 仍在定义中。 添加 base_type 是为了帮助将其他类型的资源(需求和事件)转换为工作项。 最终(当这些资源成为常规工作项时),base_type 将被移除。

在 WIT 小部件架构最终确定之前,我们暂停创建新的工作项类型。 如果绝对需要新的工作项类型,请联系 Project Management Engineering Team 的成员。

在数据库中创建新的工作项类型

我们已经完成了从 issues 表中移除 issue_type 列的工作,转而使用新的 work_item_types 表, 如 this epic 中所述。

在引入 work_item_types 表后,我们添加了更多的 work_item_types,我们希望让其他团队能更容易地做到这一点。 要引入一个新的 work_item_type,您必须:

  1. 编写数据库迁移以在 work_item_types 表中创建新记录。
  2. 更新 Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter

以下 MR 演示了如何引入新的 work_item_types

编写数据库迁移

首先,编写一个数据库迁移,在 work_item_types 表中创建新记录。

编写迁移时请注意以下几点:

  • 重要:将新类型排除在现有 API 之外。

    • 我们可能希望将此类型新创建的工作项排除在现有功能(如问题列表)之外, 直到我们完全发布该功能。因此,我们必须将新类型添加到 此排除列表 中, 除非预期用户可以在迁移执行后立即使用新类型创建新问题和工作项。
  • 使用常规迁移,而不是部署后迁移。

    • 我们认为使用 常规迁移 来添加新的工作项类型而不是 部署后迁移 会更有益。这样,依赖于类型创建的后续 MR 可以立即假定它存在, 而不必等待下一个发布。

      重要:因为我们使用常规迁移,我们需要确保它做两件事:

      1. 不要超过常规迁移的时间指南
      2. 确保迁移是向后兼容的。 这意味着即使引入此迁移的 MR 被回滚且迁移未执行, 已部署的代码也应继续工作。
  • 迁移应避免失败。

    • 我们期望在创建新类型的迁移运行时,与 work_item_types 相关的数据处于特定状态。 目前,我们编写检查数据的迁移,并在发现数据处于不一致状态时不失败。 关于我们可以在多大程度上依赖基于种子和迁移的数据状态, 有一个讨论在 this issue 中。 我们只有在编写迁移使其在数据处于不一致状态时不会失败的情况下, 才能有成功的流水线。我们可能需要更新一些数据库作业来改变这一点。
  • 为新类型添加小部件定义。

    • 迁移添加了新的工作项类型以及每个工作项所需的小部件定义。 您选择的小部件取决于新工作项支持的功能,但有些可能是所有新工作项都需要的,如 Description
  • 可选。创建层次结构限制。

    • 在其中一个示例 MR 中,我们还向 work_item_hierarchy_restrictions 表插入了记录。 这只有在新的工作项类型要使用 Hierarchy 小部件时才需要。 在此表中,您必须添加什么工作项类型可以有子项以及子项的类型。 您还应指定相同类型工作项的层次结构深度。 默认情况下,创建新限制时禁用跨层次结构(跨组或项目)关系, 但可以通过为 cross_hierarchy_enabled 指定值来启用。 由于限制被缓存用于工作项类型,还需要在相关的工作项类型上调用 clear_reactive_cache!
  • 可选。创建链接项限制。

    • Hierarchy 小部件类似,Linked items 小部件也支持定义哪些工作项类型可以链接到其他类型的规则。 限制可以指定源类型是否可以关联到或阻止目标类型。当前限制:
      类型 可以关联到 可以阻止 可以被阻止
      Epic Epic, issue, task, objective, key result Epic, issue, task, objective, key result Epic, issue, task
      Issue Epic, issue, task, objective, key result Epic, issue, task, objective, key result Epic, issue, task
      Task Epic, issue, task, objective, key result Epic, issue, task, objective, key result Epic, issue, task
      Objective Epic, issue, task, objective, key result Objective, key result Epic, issue, task, objective, key result
      Key result Epic, issue, task, objective, key result Objective, key result Epic, issue, task, objective, key result
  • 为迁移规范使用共享示例。

    您应该为不同迁移类型(新工作项类型、新小部件定义等)使用不同的共享示例, 在 add_work_item_widget_shared_examples.rb 中。

添加票证工作项示例

Ticket 工作项类型已存在于数据库中,但我们将使用它作为示例迁移。 请注意,对于新类型,您需要使用新名称和 ENUM 值。

class AddTicketWorkItemType < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!
  restrict_gitlab_migration gitlab_schema: :gitlab_main

  ISSUE_ENUM_VALUE = 0
  # Enum 值来自定义枚举的模型
  # https://gitlab.com/gitlab-org/gitlab/-/blob/1253f12abddb69cd1418c9e13e289d828b489f36/app/models/work_items/type.rb#L30.
  # 新的工作项类型应选择下一个整数值。
  TICKET_ENUM_VALUE = 8
  TICKET_NAME = 'Ticket'
  # 小部件定义也在
  # https://gitlab.com/gitlab-org/gitlab/-/blob/1253f12abddb69cd1418c9e13e289d828b489f36/app/models/work_items/widget_definition.rb#L17.
  # 中定义了枚举。
  # 我们需要同时提供枚举和名称,因为我们计划在未来支持自定义小部件名称。
  TICKET_WIDGETS = {
    'Assignees' => 0,
    'Description' => 1,
    'Hierarchy' => 2,
    'Labels' => 3,
    'Milestone' => 4,
    'Notes' => 5,
    'Start and due date' => 6,
    'Health status' => 7,
    'Weight' => 8,
    'Iteration' => 9,
    'Notifications' => 14,
    'Current user todos' => 15,
    'Award emoji' => 16
  }.freeze

  class MigrationWorkItemType < MigrationRecord
    self.table_name = 'work_item_types'
  end

  class MigrationWidgetDefinition < MigrationRecord
    self.table_name = 'work_item_widget_definitions'
  end

  class MigrationHierarchyRestriction < MigrationRecord
    self.table_name = 'work_item_hierarchy_restrictions'
  end

  def up
    existing_ticket_work_item_type = MigrationWorkItemType.find_by(base_type: TICKET_ENUM_VALUE, namespace_id: nil)

    return say('Ticket work item type record exists, skipping creation') if existing_ticket_work_item_type

    new_ticket_work_item_type = MigrationWorkItemType.create(
      name: TICKET_NAME,
      namespace_id: nil,
      base_type: TICKET_ENUM_VALUE,
      icon_name: 'issue-type-issue'
    )

    return say('Ticket work item type create record failed, skipping creation') if new_ticket_work_item_type.new_record?

    widgets = TICKET_WIDGETS.map do |widget_name, widget_enum_value|
      {
        work_item_type_id: new_ticket_work_item_type.id,
        name: widget_name,
        widget_type: widget_enum_value
      }
    end

    MigrationWidgetDefinition.upsert_all(
      widgets,
      unique_by: :index_work_item_widget_definitions_on_default_witype_and_name
    )

    issue_type = MigrationWorkItemType.find_by(base_type: ISSUE_ENUM_VALUE, namespace_id: nil)
    return say('Issue work item type not found, skipping hierarchy restrictions creation') unless issue_type

    # 这部分迁移只有在新类型使用 `Hierarchy` 小部件时才需要。
    restrictions = [
      { parent_type_id: new_ticket_work_item_type.id, child_type_id: new_ticket_work_item_type.id, maximum_depth: 1 },
      { parent_type_id: new_ticket_work_item_type.id, child_type_id: issue_type.id, maximum_depth: 1 }
    ]

    MigrationHierarchyRestriction.upsert_all(
      restrictions,
      unique_by: :index_work_item_hierarchy_restrictions_on_parent_and_child
    )
  end

  def down
    # 有极小的可能性问题可能已经在使用此问题类型,
    # 并且有严格的外键约束。
    # 因此我们不会尝试删除任何数据。
  end
end

更新 Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter

BaseTypeImporter 是我们可以清晰可视化我们拥有的类型结构以及每个类型关联的小部件的地方。 BaseTypeImporter 是全新 GitLab 安装的单一事实来源,也是我们的测试套件。 这应该始终反映我们通过迁移所做的更改。

类似地,层次结构和链接项限制的单一事实来源分别定义在 HierarchyRestrictionsImporterRelatedLinksRestrictionsImporter 中。

重要:每当相应的数据库表被修改时,这些导入器都应该被更新。

自定义工作项类型

有了 WIT 小部件元数据以及将 WIT 映射到特定小部件的工作流程, 我们将能够向用户暴露自定义的 WITs。用户将能够创建自己的 WITs, 并使用预定义池中的小部件进行自定义。

自定义小部件

最终目标是允许用户定义自定义小部件并在任何 WIT 上使用这些自定义小部件。 但这是一个更远的迭代,需要额外的调查来确定要使用的数据和应用架构。

将需求和 Epic 迁移到工作项类型

我们将把需求和 Epic 迁移到工作项类型,并拥有自己的小部件集合。 为此,我们将数据迁移到 issues 表, 并保留当前的 requirementsepics 表用作旧引用的代理, 以确保与现有引用的向后兼容性。

将需求迁移到工作项类型

目前 Requirement 属性是 Issue 属性的子集,因此迁移主要包括:

  • 数据迁移。
  • 在 API 级别保持向后兼容性。
  • 确保旧引用继续有效。

迁移到不同的底层数据结构应该对最终用户是无缝的。

将 Epic 迁移到工作项类型

Epic 具有一些当前 Issue WIT 没有的额外功能。 因此,将 Epic 迁移到工作项类型需要在当前的 Epic 对象和 WITs 之间提供功能对等性。

主要缺失的功能是:

  • 将工作项获取到组级别。这依赖于 Consolidate Groups and Projects 计划。
  • 层次结构小部件:将工作项结构化为层次结构的能力。
  • 继承日期小部件。

为了避免扰乱已经在使用 Epic 的用户的工作流程,我们将引入一个名为 Feature 的新 WIT, 它将在项目级别提供与 Epic 功能对等的功能。结合 Consolidate Groups and Projects 前端的进展, 这将帮助我们为 Epic 到 WIT 的迁移提供平滑的路径,最大限度地减少对用户工作流程的干扰。

工作项、工作项类型和小部件路线图

我们将通过迭代过程逐步推进工作项、工作项类型和自定义小部件 (CW)。 有关我们未来工作的粗略概述,请参见 epic 6033

Redis HLL 计数器架构

我们需要一个更具可扩展性的 Redis 计数器架构用于工作项,该架构包含 Plan xMAU、Project Management xMAU、Certify xMAU 和 Product Planning xMAU。 使用我们当前的 Redis 插槽架构,我们无法在组级别或阶段级别内聚合和去重跨功能的事件。

所有三个 Plan 产品组都将使用相同的基对象(work item)。每个产品组仍然需要跟踪 MAU。

建议的聚合计数器架构

graph TD
    Event[特定交互计数器] --> AC[聚合计数器]
    AC --> Plan[Plan xMAU]
    AC --> PM[Project Management xMAU]
    AC --> PP[Product Planning xMAU]
    AC --> Cer[Certify xMAU]
    AC --> WI[工作项用户]

实现

新的聚合架构已经实现,我们已经在 GitLab.com 中跟踪工作项的唯一操作。

有关实现细节,此 MR 可用作参考。 该 MR 涵盖了新唯一操作的定义、代码中的事件跟踪以及将新唯一操作添加到所需的聚合计数器中。

相关主题