gitlab-triage 1.53.1 → 1.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +4 -3
  4. data/README.md +159 -1
  5. data/lib/gitlab/triage/action/base.rb +62 -0
  6. data/lib/gitlab/triage/action/comment.rb +1 -9
  7. data/lib/gitlab/triage/action/comment_on_summary.rb +0 -6
  8. data/lib/gitlab/triage/action/issue.rb +31 -3
  9. data/lib/gitlab/triage/action/summarize.rb +21 -1
  10. data/lib/gitlab/triage/action/update.rb +432 -0
  11. data/lib/gitlab/triage/action/work_item.rb +0 -27
  12. data/lib/gitlab/triage/action.rb +18 -2
  13. data/lib/gitlab/triage/command_builders/cc_command_builder.rb +0 -4
  14. data/lib/gitlab/triage/entity_builders/issue_builder.rb +8 -0
  15. data/lib/gitlab/triage/entity_builders/issue_fields.rb +52 -0
  16. data/lib/gitlab/triage/entity_builders/summary_builder.rb +8 -0
  17. data/lib/gitlab/triage/graphql_network.rb +12 -2
  18. data/lib/gitlab/triage/network.rb +4 -0
  19. data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +24 -0
  20. data/lib/gitlab/triage/network_adapters/test_adapter.rb +8 -0
  21. data/lib/gitlab/triage/policies/base_policy.rb +10 -2
  22. data/lib/gitlab/triage/policies/rule_policy.rb +18 -8
  23. data/lib/gitlab/triage/policies/summary_policy.rb +9 -0
  24. data/lib/gitlab/triage/rest_api_network.rb +22 -0
  25. data/lib/gitlab/triage/validators/policy_validator.rb +1 -3
  26. data/lib/gitlab/triage/version.rb +1 -1
  27. metadata +4 -6
  28. data/lib/gitlab/triage/command_builders/label_command_builder.rb +0 -42
  29. data/lib/gitlab/triage/command_builders/move_command_builder.rb +0 -21
  30. data/lib/gitlab/triage/command_builders/remove_label_command_builder.rb +0 -17
  31. data/lib/gitlab/triage/command_builders/status_command_builder.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0b9314a216bceaf2701553359d22450e602b1d1964c7cf44b5fad5b2f932395
4
- data.tar.gz: 841fbddcf617cb4e14ef9679e8a56887bc5f6ddb3741232f9e79219589b5ae8c
3
+ metadata.gz: c2e36c5b7ecfbf719e3a7ac2282fc01d0e2f0af214eaeead7d60c913456090f8
4
+ data.tar.gz: 3da3785a4e850948e45ce922f6169a1dc9faa2d817eb3ebbd5285476c0290aee
5
5
  SHA512:
6
- metadata.gz: f84a9f71e56ea5a9de1c329161eb0858dc970694e0b319200ba951c85ede770a3d24053ce93f68f5fe77119e2243ba696a27c539670b2fc79f13f7ff7025ea66
7
- data.tar.gz: b44869dfd04f5419fbe7e418fec0e700e52366ae20fa6f7190d8c27fda4c95c7d2946c7716cd82db675f023eaf2b475b04b8b212f6725b5c760520f06b244f72
6
+ metadata.gz: e4b4c9e46c33d13f835e62e3c0638aa0663d4ab5f0ff9d64b0d404fb64cf738895cc1708346c92a8103cba4b41a089f7c746292baab3c46e04518ac09339aac9
7
+ data.tar.gz: cd9dd8c10b7381c41858c1f0de7a64205c87c79669e2e475e2748c40300536469369e05744545f5b55b2aed89d1384b329abaf733bbe4db837bb0a5f0d9732a5
data/Gemfile CHANGED
@@ -4,3 +4,6 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in gitlab-rubocop.gemspec
6
6
  gemspec
7
+
8
+ # minitest 5.26.2+ requires Ruby 3.1+; pin to exact version for Ruby 3.0 CI
9
+ gem 'minitest', '5.26.1'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- gitlab-triage (1.53.1)
4
+ gitlab-triage (1.54.0)
5
5
  activesupport (>= 5.1)
6
6
  globalid (~> 1.0, >= 1.0.1)
7
7
  graphql (< 2.1.0)
@@ -58,7 +58,7 @@ GEM
58
58
  danger
59
59
  gitlab (~> 4.2, >= 4.2.0)
60
60
  diff-lcs (1.5.1)
61
- drb (2.2.1)
61
+ drb (2.2.3)
62
62
  faraday (2.9.1)
63
63
  faraday-net_http (>= 2.0, < 3.2)
64
64
  faraday-http-cache (2.5.1)
@@ -124,7 +124,7 @@ GEM
124
124
  lumberjack (1.2.10)
125
125
  method_source (1.1.0)
126
126
  mini_mime (1.1.5)
127
- minitest (5.23.1)
127
+ minitest (5.26.1)
128
128
  multi_xml (0.6.0)
129
129
  mutex_m (0.2.0)
130
130
  nap (1.1.0)
@@ -230,6 +230,7 @@ DEPENDENCIES
230
230
  gitlab-triage!
231
231
  guard-rspec (~> 4.7.3)
232
232
  lefthook (~> 1.3)
233
+ minitest (= 5.26.1)
233
234
  rake (~> 13.2)
234
235
  rspec (~> 3.8)
235
236
  webmock (~> 3.4)
data/README.md CHANGED
@@ -903,6 +903,15 @@ Used to declare an action to be carried out on a resource if **all** conditions
903
903
  Available action types:
904
904
  - [`labels` action](#labels-action)
905
905
  - [`remove_labels` action](#remove-labels-action)
906
+ - [`assignees` action](#assignees-action)
907
+ - [`reviewers` action](#reviewers-action)
908
+ - [`remove_assignees` and `remove_reviewers` actions](#remove-assignees-and-remove-reviewers-actions)
909
+ - [`milestone` action](#milestone-action)
910
+ - [`due_date` action](#due-date-action)
911
+ - [`weight` action](#weight-action)
912
+ - [`health_status` action](#health-status-action)
913
+ - [`title` action](#title-action)
914
+ - [`discussion_locked` action](#discussion-locked-action)
906
915
  - [`status` action](#status-action)
907
916
  - [`work_item_status` action](#work-item-status-action)
908
917
  - [`mention` action](#mention-action)
@@ -934,6 +943,16 @@ actions:
934
943
  - awaiting feedback
935
944
  ```
936
945
 
946
+ Each entry supports Ruby string interpolation. An entry that interpolates to an
947
+ empty string is dropped, so a label can be made conditional on the resource:
948
+
949
+ ```yml
950
+ actions:
951
+ labels:
952
+ - Community contribution
953
+ - "#{'1st contribution' if first_contribution?}"
954
+ ```
955
+
937
956
  ##### Remove labels action
938
957
 
939
958
  Removes a number of labels from the resource.
@@ -953,6 +972,142 @@ actions:
953
972
  - awaiting feedback
954
973
  ```
955
974
 
975
+ ##### Assignees action
976
+
977
+ Sets the assignees on the resource. Applies to issues and merge requests.
978
+
979
+ Accepts an array of usernames (without the leading `@`). If any username does not exist, the automation stops immediately.
980
+
981
+ Setting an empty array (`[]`) removes all assignees.
982
+
983
+ Example:
984
+
985
+ ```yml
986
+ actions:
987
+ assignees:
988
+ - username1
989
+ - username2
990
+ ```
991
+
992
+ ##### Reviewers action
993
+
994
+ Sets the reviewers on the resource. Applies to merge requests only.
995
+
996
+ Accepts an array of usernames (without the leading `@`). If any username does not exist, the automation stops immediately.
997
+
998
+ Setting an empty array (`[]`) removes all reviewers.
999
+
1000
+ Example:
1001
+
1002
+ ```yml
1003
+ actions:
1004
+ reviewers:
1005
+ - username1
1006
+ ```
1007
+
1008
+ Both actions replace the whole list rather than adding to it, and both support Ruby string interpolation in each entry. An entry that interpolates to a comma-separated list is expanded, so a policy can compute the list from the resource.
1009
+
1010
+ Entries that interpolate to nothing are dropped. If every entry drops out, the action is skipped rather than clearing the list, so a conditional assignee cannot unassign by accident. Only a literal `[]` clears.
1011
+
1012
+ ##### Remove assignees and remove reviewers actions
1013
+
1014
+ Removes the named users and keeps the rest. `remove_assignees` applies to issues and merge requests, `remove_reviewers` to merge requests only.
1015
+
1016
+ Accepts an array of usernames (without the leading `@`), with the same interpolation and comma expansion as the actions above. Names that are not on the resource are ignored, and a resource with nobody assigned is left alone.
1017
+
1018
+ Example:
1019
+
1020
+ ```yml
1021
+ actions:
1022
+ remove_reviewers:
1023
+ - some-bot
1024
+ ```
1025
+
1026
+ Prefer these over computing the surviving list yourself: they read the ids straight off the resource, so they cost no extra API call and cannot clear the list by accident.
1027
+
1028
+ ##### Milestone action
1029
+
1030
+ Sets the milestone on the resource. Applies to issues and merge requests.
1031
+
1032
+ Accepts a string containing the milestone title. If the milestone does not exist, the automation stops immediately.
1033
+
1034
+ Supports Ruby string interpolation in the value.
1035
+
1036
+ Example:
1037
+
1038
+ ```yml
1039
+ actions:
1040
+ milestone: "17.0"
1041
+ ```
1042
+
1043
+ ##### Due date action
1044
+
1045
+ Sets the due date on the resource. Applies to issues only. The epics endpoint takes `due_date_fixed` and `due_date_is_fixed` instead, and merge requests have no due date.
1046
+
1047
+ Accepts a string in `YYYY-MM-DD` format. Supports Ruby string interpolation.
1048
+
1049
+ Example:
1050
+
1051
+ ```yml
1052
+ actions:
1053
+ due_date: "#{(Date.today + 7).iso8601}"
1054
+ ```
1055
+
1056
+ ##### Weight action
1057
+
1058
+ Sets the weight on an issue. Issues only.
1059
+
1060
+ Accepts an integer, or `~` (YAML null) to clear the weight.
1061
+
1062
+ Example:
1063
+
1064
+ ```yml
1065
+ actions:
1066
+ weight: 3
1067
+ ```
1068
+
1069
+ ##### Health status action
1070
+
1071
+ Sets the health status on an issue. Issues only.
1072
+
1073
+ Accepts one of `on_track`, `needs_attention`, or `at_risk`, or `~` (YAML null) to clear it.
1074
+
1075
+ This one goes through the work item GraphQL API, because the REST issue update
1076
+ endpoint does not accept a health status parameter.
1077
+
1078
+ Example:
1079
+
1080
+ ```yml
1081
+ actions:
1082
+ health_status: needs_attention
1083
+ ```
1084
+
1085
+ ##### Title action
1086
+
1087
+ Updates the title of the resource.
1088
+
1089
+ Accepts a string. Supports Ruby string interpolation.
1090
+
1091
+ Example:
1092
+
1093
+ ```yml
1094
+ actions:
1095
+ title: "#{resource[:title]} [archived]"
1096
+ ```
1097
+
1098
+ ##### Discussion locked action
1099
+
1100
+ Locks or unlocks discussions on the resource. Applies to issues and merge requests.
1101
+
1102
+ Accepts `true` or `false`.
1103
+
1104
+ Example:
1105
+
1106
+ ```yml
1107
+ actions:
1108
+ discussion_locked: true
1109
+ ```
1110
+
956
1111
  ##### Status action
957
1112
 
958
1113
  Changes the open or closed state of the resource.
@@ -999,7 +1154,7 @@ actions:
999
1154
 
1000
1155
  ##### Mention action
1001
1156
 
1002
- Mentions a number of users.
1157
+ Mentions a number of users. The usernames are appended to the comment body as `@username` references. Prior to 1.54.0 they were emitted as `/cc @username`; the mention effect is identical but the rendered text differs.
1003
1158
 
1004
1159
  Accepts an array of strings. Each element is the username of a user to mention.
1005
1160
 
@@ -1018,6 +1173,9 @@ Moves an issue (merge request is not supported yet) to the specified project.
1018
1173
 
1019
1174
  Accepts a string containing the target project path.
1020
1175
 
1176
+ When combined with other update actions, the move runs last, because it gives the
1177
+ issue a new internal ID at the destination.
1178
+
1021
1179
  Example:
1022
1180
 
1023
1181
  ```yml
@@ -1,15 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+
5
+ require_relative '../url_builders/url_builder'
6
+ require_relative '../resource/label'
7
+
3
8
  module Gitlab
4
9
  module Triage
5
10
  module Action
6
11
  class Base
12
+ AssigneeDoesntExistError = Class.new(StandardError)
13
+ MilestoneDoesntExistError = Class.new(StandardError)
14
+
7
15
  attr_reader :policy, :network
8
16
 
9
17
  def initialize(**args)
10
18
  @policy = args[:policy]
11
19
  @network = args[:network]
12
20
  end
21
+
22
+ # Action.process calls this on every active action before any of them
23
+ # writes. Actions with nothing to look up have nothing to check.
24
+ def verify!; end
25
+
26
+ private
27
+
28
+ def ensure_labels_exist!(labels, source_id, source_id_key: :project_id)
29
+ labels.each do |label|
30
+ label_opts = { source_id_key => source_id, name: label }
31
+ next if Resource::Label.new(label_opts, network: network).exist?
32
+
33
+ raise Resource::Label::LabelDoesntExistError, "Label `#{label}` doesn't exist!"
34
+ end
35
+ end
36
+
37
+ def resolve_user_ids(usernames, error_class = AssigneeDoesntExistError)
38
+ Array(usernames).map do |username|
39
+ id = resolve_user_id(username)
40
+ raise error_class, "User `#{username}` doesn't exist!" unless id
41
+
42
+ id
43
+ end
44
+ end
45
+ alias_method :resolve_assignee_ids, :resolve_user_ids
46
+
47
+ def resolve_user_id(username)
48
+ users = network.query_api_cached(users_url(username))
49
+ users.first&.dig(:id)
50
+ end
51
+
52
+ def users_url(username)
53
+ opts = network.options
54
+ "#{opts.host_url}/api/#{opts.api_version}/users?username=#{CGI.escape(username)}"
55
+ end
56
+
57
+ # include_ancestors matches the /milestone quick action, which also resolves
58
+ # milestones defined on a parent group.
59
+ def milestones_url(source_id, title, source: 'projects')
60
+ UrlBuilders::UrlBuilder.new(
61
+ network_options: network.options,
62
+ source: source,
63
+ source_id: source_id,
64
+ resource_type: 'milestones',
65
+ params: { title: title, include_ancestors: true }
66
+ ).build
67
+ end
68
+
69
+ def resolve_milestone_id(source_id, title, source: 'projects')
70
+ milestone = network.query_api_cached(milestones_url(source_id, title, source: source)).first
71
+ raise MilestoneDoesntExistError, "Milestone `#{title}` doesn't exist!" unless milestone
72
+
73
+ milestone[:id]
74
+ end
13
75
  end
14
76
  end
15
77
  end
@@ -3,11 +3,7 @@
3
3
  require_relative 'base'
4
4
  require_relative '../command_builders/text_content_builder'
5
5
  require_relative '../command_builders/comment_command_builder'
6
- require_relative '../command_builders/label_command_builder'
7
- require_relative '../command_builders/remove_label_command_builder'
8
6
  require_relative '../command_builders/cc_command_builder'
9
- require_relative '../command_builders/status_command_builder'
10
- require_relative '../command_builders/move_command_builder'
11
7
 
12
8
  module Gitlab
13
9
  module Triage
@@ -51,11 +47,7 @@ module Gitlab
51
47
  network: network,
52
48
  redact_confidentials: policy.actions.fetch(:redact_confidential_resources, true)
53
49
  ).build_command,
54
- CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels], resource: resource, network: network).build_command,
55
- CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels], resource: resource, network: network).build_command,
56
- CommandBuilders::CcCommandBuilder.new(policy.actions[:mention]).build_command,
57
- CommandBuilders::MoveCommandBuilder.new(policy.actions[:move]).build_command,
58
- CommandBuilders::StatusCommandBuilder.new(policy.actions[:status]).build_command
50
+ CommandBuilders::CcCommandBuilder.new(policy.actions[:mention]).build_command
59
51
  ]
60
52
  ).build_command
61
53
  end
@@ -2,12 +2,6 @@
2
2
 
3
3
  require_relative 'base'
4
4
  require_relative '../command_builders/text_content_builder'
5
- require_relative '../command_builders/comment_command_builder'
6
- require_relative '../command_builders/label_command_builder'
7
- require_relative '../command_builders/remove_label_command_builder'
8
- require_relative '../command_builders/cc_command_builder'
9
- require_relative '../command_builders/status_command_builder'
10
- require_relative '../command_builders/move_command_builder'
11
5
 
12
6
  module Gitlab
13
7
  module Triage
@@ -19,11 +19,30 @@ module Gitlab
19
19
  puts ">>>"
20
20
  puts "* Project: #{issue.destination || resource[policy.source_id_sym]}"
21
21
  puts "* Title: #{issue.title}"
22
+ puts "* Labels: #{issue.labels.join(', ')}" if issue.labels.any?
23
+ puts "* Assignees: #{issue.assignee_usernames.join(', ')}" if issue.assignee_usernames&.any?
24
+ puts "* Due date: #{issue.due_date}" if issue.due_date
25
+ puts "* Milestone: #{issue.milestone_title}" if issue.milestone_title
22
26
  puts "* Description: #{issue.description}"
23
27
  puts ">>>"
24
28
  end
25
29
  end
26
30
 
31
+ # Resolving the assignees and milestone here too keeps a rule naming an
32
+ # unknown one from failing after Comment has already posted.
33
+ def verify!
34
+ policy.resources.each do |resource|
35
+ issue = policy.build_issue(resource)
36
+ next unless issue.valid?
37
+
38
+ destination_id = issue.destination || resource[policy.source_id_sym]
39
+
40
+ ensure_labels_exist!(issue.labels, destination_id)
41
+ resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
42
+ resolve_milestone_id(destination_id, issue.milestone_title) if issue.milestone_title
43
+ end
44
+ end
45
+
27
46
  def act
28
47
  policy.resources.each do |resource|
29
48
  issue = policy.build_issue(resource)
@@ -35,7 +54,7 @@ module Gitlab
35
54
  private
36
55
 
37
56
  def perform(resource, issue)
38
- network.post_api(build_post_url(resource, issue), post_issue_body(issue))
57
+ network.post_api(build_post_url(resource, issue), post_issue_body(resource, issue))
39
58
  end
40
59
 
41
60
  def build_post_url(resource, issue)
@@ -52,11 +71,20 @@ module Gitlab
52
71
  post_url
53
72
  end
54
73
 
55
- def post_issue_body(issue)
56
- {
74
+ def post_issue_body(resource, issue)
75
+ destination_id = issue.destination || resource[policy.source_id_sym]
76
+
77
+ body = {
57
78
  title: issue.title,
58
79
  description: issue.description
59
80
  }
81
+
82
+ body[:labels] = issue.labels.join(',') if issue.labels.any?
83
+ body[:assignee_ids] = resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
84
+ body[:due_date] = issue.due_date if issue.due_date
85
+ body[:milestone_id] = resolve_milestone_id(destination_id, issue.milestone_title) if issue.milestone_title
86
+
87
+ body
60
88
  end
61
89
  end
62
90
  end
@@ -27,11 +27,24 @@ module Gitlab
27
27
  puts "The following issue would be created in project `#{destination}` for the rule **#{policy.name}**:\n\n"
28
28
  puts ">>>"
29
29
  puts "* Title: #{issue.title}"
30
+ puts "* Labels: #{issue.labels.join(', ')}" if issue.labels.any?
31
+ puts "* Assignees: #{issue.assignee_usernames.join(', ')}" if issue.assignee_usernames&.any?
32
+ puts "* Due date: #{issue.due_date}" if issue.due_date
33
+ puts "* Milestone: #{issue.milestone_title}" if issue.milestone_title
30
34
  puts "* Description: #{issue.description}"
31
35
  puts ">>>"
32
36
  end
33
37
  end
34
38
 
39
+ def verify!
40
+ return unless issue.valid?
41
+ return if group_summary_without_destination?
42
+
43
+ ensure_labels_exist!(issue.labels, destination)
44
+ resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
45
+ resolve_milestone_id(destination, issue.milestone_title) if issue.milestone_title
46
+ end
47
+
35
48
  def act
36
49
  perform if issue.valid?
37
50
  end
@@ -75,10 +88,17 @@ module Gitlab
75
88
  end
76
89
 
77
90
  def post_issue_body
78
- {
91
+ body = {
79
92
  title: issue.title,
80
93
  description: issue.description
81
94
  }
95
+
96
+ body[:labels] = issue.labels.join(',') if issue.labels.any?
97
+ body[:assignee_ids] = resolve_assignee_ids(issue.assignee_usernames) if issue.assignee_usernames.any?
98
+ body[:due_date] = issue.due_date if issue.due_date
99
+ body[:milestone_id] = resolve_milestone_id(destination, issue.milestone_title) if issue.milestone_title
100
+
101
+ body
82
102
  end
83
103
  end
84
104
  end