Class: Gitlab::Triage::Action::WorkItem

Inherits:
Base
  • Object
show all
Defined in:
lib/gitlab/triage/action/work_item.rb

Overview

Applies work_items actions through the Work Items REST API update endpoint (PATCH) rather than the issues note/quick-action path.

- labels    -> features.labels.add_label_ids  (resolved from names)
- assignees -> features.assignees.assignee_ids (resolved from usernames)

Missing labels or usernames stop the automation immediately (matching the labels action behaviour on the issues type) rather than silently applying nothing - which is especially important because clearing the assignees array would unassign everyone.

Defined Under Namespace

Classes: Dry

Constant Summary collapse

AssigneeDoesntExistError =
Class.new(StandardError)
UpdateNotAppliedError =
Class.new(StandardError)

Instance Attribute Summary

Attributes inherited from Base

#network, #policy

Instance Method Summary collapse

Methods inherited from Base

#initialize

Constructor Details

This class inherits a constructor from Gitlab::Triage::Action::Base

Instance Method Details

#actObject



45
46
47
48
49
50
51
# File 'lib/gitlab/triage/action/work_item.rb', line 45

def act
  return unless requested_labels.any? || requested_assignees.any?

  policy.resources.each do |resource|
    perform(resource)
  end
end

#assignee_idsObject (private)



96
97
98
# File 'lib/gitlab/triage/action/work_item.rb', line 96

def assignee_ids
  @assignee_ids ||= resolve_assignee_ids(requested_assignees)
end

#build_features(resource) ⇒ Object (private)



64
65
66
67
68
69
70
71
# File 'lib/gitlab/triage/action/work_item.rb', line 64

def build_features(resource)
  features = {}

  features[:labels] = { add_label_ids: label_ids_to_add(resource) } if requested_labels.any?
  features[:assignees] = { assignee_ids: assignee_ids } if requested_assignees.any?

  features
end

#label_ids_to_add(resource) ⇒ Object (private)

Label ids are namespace-specific, so resolve them against the resource's own namespace and cache per scope - a group-spanning run then makes one label lookup per distinct project/group.



89
90
91
92
93
94
# File 'lib/gitlab/triage/action/work_item.rb', line 89

def label_ids_to_add(resource)
  scope = source.scope_for(resource)

  @label_ids_by_scope ||= {}
  @label_ids_by_scope[scope] ||= resolve_label_ids(requested_labels, scope)
end

#labels_url(scope) ⇒ Object (private)



163
164
165
166
167
168
169
170
171
172
173
# File 'lib/gitlab/triage/action/work_item.rb', line 163

def labels_url(scope)
  scope_source, scope_id = scope

  UrlBuilders::UrlBuilder.new(
    network_options: network.options,
    source: scope_source,
    source_id: scope_id,
    resource_type: 'labels',
    params: { per_page: 100 }
  ).build
end

#perform(resource) ⇒ Object (private)



55
56
57
58
59
60
61
62
# File 'lib/gitlab/triage/action/work_item.rb', line 55

def perform(resource)
  features = build_features(resource)
  return if features.empty?

  response = source.update(resource, features)

  verify_applied!(resource, response)
end

#requested_assigneesObject (private)



77
78
79
# File 'lib/gitlab/triage/action/work_item.rb', line 77

def requested_assignees
  @requested_assignees ||= Array(policy.actions[:assignees])
end

#requested_labelsObject (private)



73
74
75
# File 'lib/gitlab/triage/action/work_item.rb', line 73

def requested_labels
  @requested_labels ||= Array(policy.actions[:labels])
end

#resolve_assignee_ids(usernames) ⇒ Object (private)

Resolve every requested username to an id. A missing username raises rather than being dropped, because an empty assignee_ids array would clear all assignees on the work item.



119
120
121
122
123
124
125
126
127
128
# File 'lib/gitlab/triage/action/work_item.rb', line 119

def resolve_assignee_ids(usernames)
  return [] if usernames.empty?

  usernames.map do |username|
    id = resolve_user_id(username)
    raise AssigneeDoesntExistError, "User `#{username}` doesn't exist!" unless id

    id
  end
end

#resolve_label_ids(names, scope) ⇒ Object (private)

Resolve every requested label name to an id. Any name that does not resolve raises, so the run stops instead of PATCHing a partial (or empty) label set.



103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/gitlab/triage/action/work_item.rb', line 103

def resolve_label_ids(names, scope)
  return [] if names.empty?

  by_name = network.query_api(labels_url(scope)).index_by { |label| label[:name] }

  names.map do |name|
    label = by_name[name]
    raise Resource::Label::LabelDoesntExistError, "Label `#{name}` doesn't exist!" unless label

    label[:id]
  end
end

#resolve_user_id(username) ⇒ Object (private)



130
131
132
133
134
# File 'lib/gitlab/triage/action/work_item.rb', line 130

def resolve_user_id(username)
  users = network.query_api(users_url(username))

  users.first&.dig(:id)
end

#sourceObject (private)



81
82
83
84
# File 'lib/gitlab/triage/action/work_item.rb', line 81

def source
  @source ||= Sources::WorkItemsRestSource.new(
    network: network, options: network.options)
end

#users_url(username) ⇒ Object (private)



175
176
177
178
179
# File 'lib/gitlab/triage/action/work_item.rb', line 175

def users_url(username)
  options = network.options

  "#{options.host_url}/api/#{options.api_version}/users?username=#{CGI.escape(username)}"
end

#verify_applied!(resource, response) ⇒ Object (private)

The Work Items REST update endpoint can return success without persisting a change when the token lacks permission. The response echoes the resulting widgets, so confirm the requested ids landed and raise otherwise rather than reporting a silent no-op as success.



140
141
142
143
# File 'lib/gitlab/triage/action/work_item.rb', line 140

def verify_applied!(resource, response)
  verify_labels_applied!(resource, response) if requested_labels.any?
  verify_assignees_applied!(resource, response) if requested_assignees.any?
end

#verify_assignees_applied!(resource, response) ⇒ Object (private)



154
155
156
157
158
159
160
161
# File 'lib/gitlab/triage/action/work_item.rb', line 154

def verify_assignees_applied!(resource, response)
  applied_ids = [*response.dig(:features, :assignees)].pluck(:id)
  missing = assignee_ids - applied_ids
  return if missing.empty?

  raise UpdateNotAppliedError,
    "Assignees were not applied to #{resource[:web_url]}; the token may lack permission."
end

#verify_labels_applied!(resource, response) ⇒ Object (private)



145
146
147
148
149
150
151
152
# File 'lib/gitlab/triage/action/work_item.rb', line 145

def verify_labels_applied!(resource, response)
  applied_ids = [*response.dig(:features, :labels, :labels)].pluck(:id)
  missing = label_ids_to_add(resource) - applied_ids
  return if missing.empty?

  raise UpdateNotAppliedError,
    "Labels were not applied to #{resource[:web_url]}; the token may lack permission."
end