Class: Gitlab::Triage::Sources::WorkItemsRestSource

Inherits:
Object
  • Object
show all
Defined in:
lib/gitlab/triage/sources/work_items_rest_source.rb

Overview

Fetches and updates work items through the GitLab Work Items REST API.

Unlike the legacy issues REST API, the Work Items REST list endpoint applies no default base-type filter, so work items of any type - including custom, namespace-defined types - are returned and can be matched by name via the work_item_type_names filter.

The source is a self-contained peer to the engine's inline issues / merge_requests / branches fetch: it owns list URL building, filter push-down, normalization, and the PATCH update path. Comment creation is not yet available on the Work Items REST API, so the comment action is rejected at load time by Validators::PolicyValidator.

Constant Summary collapse

UnavailableError =
Class.new(StandardError)
ScopeError =
Class.new(StandardError)
LIST_FIELDS =

Base fields opted into on the list endpoint. The endpoint is sparse by default (id, iid, global_id, title only); every field a filter or template reads must be requested here or it comes back nil.

%w[
  state
  confidential
  created_at
  updated_at
  web_url
  author
  namespace
  work_item_type
].freeze
LIST_FEATURES =

Widget payloads opted into via features.

%w[
  labels
  assignees
  milestone
  status
  start_and_due_date
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(network:, options:, per_page: 100, normalizer: Normalizers::WorkItemNormalizer.new) ⇒ WorkItemsRestSource

Returns a new instance of WorkItemsRestSource.



52
53
54
55
56
57
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 52

def initialize(network:, options:, per_page: 100, normalizer: Normalizers::WorkItemNormalizer.new)
  @network = network
  @options = options
  @per_page = per_page
  @normalizer = normalizer
end

Instance Method Details

#apply_date_filter(params, date_condition) ⇒ Object (private)



199
200
201
202
203
204
205
206
207
208
209
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 199

def apply_date_filter(params, date_condition)
  attribute = date_condition[:attribute]
  condition = date_condition[:condition]
  return unless %w[created_at updated_at].include?(attribute)

  boundary = date_boundary(date_condition)
  return unless boundary

  direction = condition == 'older_than' ? 'before' : 'after'
  params["#{attribute.delete_suffix('_at')}_#{direction}"] = boundary.iso8601
end

#apply_filters(params, conditions) ⇒ Object (private)

Push-down: map work_items conditions to Work Items REST query params. type uses work_item_type_names, which resolves names (including custom types) case-insensitively server-side - no id resolution needed.



187
188
189
190
191
192
193
194
195
196
197
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 187

def apply_filters(params, conditions)
  params[:iids] = Array(conditions[:iids]).join(',') if conditions[:iids].present?
  params[:work_item_type_names] = Array(conditions[:type]).join(',') if conditions[:type]
  params[:state] = conditions[:state] if conditions[:state]
  params[:label_name] = Array(conditions[:labels]).join(',') if conditions[:labels]
  params['not[label_name]'] = Array(conditions[:forbidden_labels]).join(',') if conditions[:forbidden_labels]
  params[:assignee_usernames] = Array(conditions[:assignees]).join(',') if conditions[:assignees]
  params['status[name]'] = conditions[:status] if conditions[:status]

  apply_date_filter(params, conditions[:date]) if conditions[:date]
end

#build_list_url(conditions) ⇒ Object (private)



149
150
151
152
153
154
155
156
157
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 149

def build_list_url(conditions)
  UrlBuilders::UrlBuilder.new(
    network_options: @options,
    source: @options.source,
    source_id: @options.source_id,
    resource_type: '-/work_items',
    params: list_params(conditions)
  ).build
end

#date_boundary(date_condition) ⇒ Object (private)



211
212
213
214
215
216
217
218
219
220
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 211

def date_boundary(date_condition)
  interval = date_condition[:interval]
  interval_type = date_condition[:interval_type]
  return unless interval && interval_type

  # A malformed interval_type raises here rather than silently dropping
  # the date bound, which would widen the matched set - dangerous in an
  # automation tool. interval_type is also validated by the date filter.
  interval.public_send(interval_type).ago # rubocop:disable GitlabSecurity/PublicSend -- interval_type constrained to a known set
end

#list(conditions) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 59

def list(conditions)
  raw = @network.query_api(build_list_url(conditions))

  raise_if_unavailable!(raw)

  raw.map { |item| resolve_project_id!(@normalizer.call(item)) }
end

#list_params(conditions) ⇒ Object (private)



171
172
173
174
175
176
177
178
179
180
181
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 171

def list_params(conditions)
  params = {
    per_page: @per_page,
    fields: LIST_FIELDS.join(','),
    features: LIST_FEATURES.join(',')
  }

  apply_filters(params, conditions)

  params
end

#project_id_by_full_path(full_path) ⇒ Object (private)



114
115
116
117
118
119
120
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 114

def project_id_by_full_path(full_path)
  @project_ids_by_full_path ||= {}
  return @project_ids_by_full_path[full_path] if @project_ids_by_full_path.key?(full_path)

  project = @network.query_api_cached(project_url(full_path)).first
  @project_ids_by_full_path[full_path] = project && project[:id]
end

#project_url(full_path) ⇒ Object (private)



122
123
124
125
126
127
128
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 122

def project_url(full_path)
  UrlBuilders::UrlBuilder.new(
    network_options: @options,
    source: 'projects',
    source_id: full_path
  ).build
end

#raise_if_unavailable!(raw) ⇒ Object (private)

The Work Items REST API is behind the :work_item_rest_api feature flag. When it is off, or the token lacks access, or the endpoint is absent, the response is an error hash (e.g. a 403 "feature flag is disabled" or a 404). The adapter does not raise on those, so without this guard the error hash would be normalized into a bogus resource and the run would silently match nothing - which in an automation tool looks like "all clear". Raise a clear, actionable error instead.

Raises:



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 137

def raise_if_unavailable!(raw)
  error = raw.find { |item| item.is_a?(Hash) && item[:iid].blank? && (item[:message] || item[:error]) }
  return unless error

  message = error[:message] || error[:error]

  raise UnavailableError,
    "The Work Items REST API is not available (#{message}). " \
      'Ensure the :work_item_rest_api feature flag is enabled and the ' \
      'token has sufficient access.'
end

#resolve_project_id!(resource) ⇒ Object (private)

The normalizer surfaces group_id directly for group-namespace items (namespace.id == legacy group id) but cannot supply a project_id for project-namespace items, because the list payload only carries the project-namespace id and full path, not the legacy project id that the gem's project-scoped resource helpers need. Resolve it here from the full path, cached per run, so that a group-spanning run makes at most one lookup per distinct project - and only for project items.



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 101

def resolve_project_id!(resource)
  namespace = resource[:namespace]
  return resource unless namespace && namespace[:kind] == 'project'

  full_path = namespace[:full_path]
  return resource if full_path.blank?

  project_id = project_id_by_full_path(full_path)
  resource[:project_id] = project_id if project_id

  resource
end

#scope_for(resource) ⇒ Object

Returns [source, source_id] for the namespace the resource lives in. A group run can return work items from descendant projects, and an iid is only unique within its own namespace, so writes and label lookups must target the resource's namespace, not the run-level source.



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 76

def scope_for(resource)
  return ['projects', resource[:project_id]] if resource[:project_id]
  return ['groups', resource[:group_id]] if resource[:group_id]

  # A project item whose legacy project id could not be resolved must
  # not fall back to the run-level scope: on a group run the same iid
  # can name a different, visible work item there.
  if resource.dig(:namespace, :kind) == 'project'
    raise ScopeError,
      "Cannot resolve the project for work item #{resource[:web_url] || resource[:iid]}; " \
        'the token may lack access to its project.'
  end

  [@options.source, @options.source_id]
end

#update(resource, features) ⇒ Object



67
68
69
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 67

def update(resource, features)
  @network.patch_api(work_item_url(resource), features: features)
end

#work_item_url(resource) ⇒ Object (private)



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/gitlab/triage/sources/work_items_rest_source.rb', line 159

def work_item_url(resource)
  scope, scope_id = scope_for(resource)

  UrlBuilders::UrlBuilder.new(
    network_options: @options,
    source: scope,
    source_id: scope_id,
    resource_type: '-/work_items',
    resource_id: resource[:iid]
  ).build
end