gitlab-triage 0.14.1 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -24
- data/bin/gitlab-triage +2 -74
- data/lib/gitlab/triage/action/base.rb +3 -3
- data/lib/gitlab/triage/action/comment.rb +5 -7
- data/lib/gitlab/triage/action/summarize.rb +4 -5
- data/lib/gitlab/triage/command_builders/base_command_builder.rb +1 -1
- data/lib/gitlab/triage/command_builders/text_content_builder.rb +54 -16
- data/lib/gitlab/triage/engine.rb +27 -34
- data/lib/gitlab/triage/entity_builders/issue_builder.rb +27 -17
- data/lib/gitlab/triage/filters/member_conditions_filter.rb +6 -8
- data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +5 -3
- data/lib/gitlab/triage/network.rb +20 -8
- data/lib/gitlab/triage/network_adapters/base_adapter.rb +1 -1
- data/lib/gitlab/triage/option_parser.rb +72 -0
- data/lib/gitlab/triage/options.rb +21 -0
- data/lib/gitlab/triage/policies/base_policy.rb +21 -2
- data/lib/gitlab/triage/policies/rule_policy.rb +7 -1
- data/lib/gitlab/triage/policies/summary_policy.rb +25 -2
- data/lib/gitlab/triage/resource/base.rb +49 -10
- data/lib/gitlab/triage/resource/context.rb +13 -8
- data/lib/gitlab/triage/resource/instance_version.rb +3 -4
- data/lib/gitlab/triage/resource/issue.rb +14 -0
- data/lib/gitlab/triage/resource/label.rb +41 -0
- data/lib/gitlab/triage/resource/label_event.rb +45 -0
- data/lib/gitlab/triage/resource/merge_request.rb +14 -0
- data/lib/gitlab/triage/resource/milestone.rb +5 -5
- data/lib/gitlab/triage/resource/shared/issuable.rb +54 -0
- data/lib/gitlab/triage/url_builders/url_builder.rb +3 -2
- data/lib/gitlab/triage/version.rb +1 -1
- data/support/.triage-policies.example.yml +4 -4
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2282112f85b0f8af0edd3fd6c553848768e15c2b39402b1e9344564626086a9c
|
4
|
+
data.tar.gz: 55f84c356fd84e7cd35aea63d2f6c377a96a82b13f23322dece9d9eb077c7f98
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c5989be48592d0f1d6110c2e0f752bcb3a492a20027845829a3b97c21b421d61548007f443f01e7b85c5136f47b081909b2f4470a7f142d4de7ec4abc4f121a
|
7
|
+
data.tar.gz: 994b0b296c5382f2c402b4a42c9ffac7678777051b8dd6ba28bd6e30360f297139d2ab8ec88edbf26ef919b436303ce62bdd0fddddb9358e35177aceb59971e4
|
data/README.md
CHANGED
@@ -66,9 +66,11 @@ resource_rules:
|
|
66
66
|
comment: |
|
67
67
|
{{author}} This issue is unlabelled after 5 days. It needs attention. Please take care of this before the end of #{2.days.from_now.strftime('%Y-%m-%d')}
|
68
68
|
summarize:
|
69
|
-
title:
|
69
|
+
title: |
|
70
|
+
#{resource[:type].capitalize} require labels
|
70
71
|
item: |
|
71
72
|
- [ ] [{{title}}]({{web_url}}) {{labels}}
|
73
|
+
redact_confidential_resources: false
|
72
74
|
summary: |
|
73
75
|
The following issues require labels:
|
74
76
|
|
@@ -560,6 +562,8 @@ The following placeholders are supported:
|
|
560
562
|
- `downvotes`: the resources's downvotes count
|
561
563
|
- `title`: the resource's title
|
562
564
|
- `web_url`: the web URL pointing to the resource
|
565
|
+
- `type`: the type of the resources. For now, only `issues` and
|
566
|
+
`merge_requests` are supported.
|
563
567
|
|
564
568
|
If the resource doesn't respond to the placeholder, or if the field is `nil`,
|
565
569
|
the placeholder is not replaced.
|
@@ -611,42 +615,56 @@ Generates an issue summarizing what was triaged.
|
|
611
615
|
|
612
616
|
Accepts a hash of fields.
|
613
617
|
|
614
|
-
| Field | Type | Description | Required | Placeholders | Ruby expression |
|
615
|
-
| ---- | ---- | ---- | ---- | ---- | ---- |
|
616
|
-
| `title` | string | The title of the generated issue | yes |
|
617
|
-
| `item` | string | Template representing each triaged resource | no | yes | yes |
|
618
|
-
| `summary` | string | The description of the generated issue | no | Only `{{items}}
|
618
|
+
| Field | Type | Description | Required | Placeholders | Ruby expression | Default |
|
619
|
+
| ---- | ---- | ---- | ---- | ---- | ---- | ---- |
|
620
|
+
| `title` | string | The title of the generated issue | yes | yes | no | |
|
621
|
+
| `item` | string | Template representing each triaged resource | no | yes | yes | |
|
622
|
+
| `summary` | string | The description of the generated issue | no | Only `{{title}}`, `{{items}}`, `{{type}}` | yes | |
|
623
|
+
| `redact_confidential_resources` | boolean | Whether redact fields for confidential resources | no | no | no | true |
|
619
624
|
|
620
625
|
**Note:**: Both `item` and `summary` fields act like a
|
621
626
|
[comment action](#comment-action), therefore
|
622
627
|
[Ruby expression](#ruby-expression) is supported.
|
623
628
|
Placeholders work regularly for `item`, but for `summary` only
|
624
|
-
`{{items}}
|
629
|
+
`{{title}}`, `{{items}}`, `{{type}}` are supported because it's not tied to a
|
625
630
|
particular resource like the comment action.
|
626
631
|
|
632
|
+
**Note:**: `redact_confidential_resources` defaults to `true`, so fields on
|
633
|
+
confidential resources will be converted to `(confidential)` except for
|
634
|
+
`{{web_url}}`. Setting it to `false` will reveal the confidential fields.
|
635
|
+
This will be useful if the summary is confidential itself (not implemented
|
636
|
+
yet), or if we're posting to another private project (not implemented yet).
|
637
|
+
|
627
638
|
The following placeholders are supported for `summary`:
|
628
639
|
|
629
|
-
- `items`: Concatenated markdown separated by a newline for each `item`
|
630
640
|
- `title`: The title of the generated issue
|
641
|
+
- `items`: Concatenated markdown separated by a newline for each `item`
|
642
|
+
- `type`: The resource type for the summary. For now `issues` or
|
643
|
+
`merge_requests`
|
631
644
|
|
632
645
|
Example:
|
633
646
|
|
634
647
|
```yml
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
648
|
+
resource_rules:
|
649
|
+
issues:
|
650
|
+
rules:
|
651
|
+
- name: Issues require labels
|
652
|
+
limits:
|
653
|
+
most_recent: 15
|
654
|
+
actions:
|
655
|
+
summarize:
|
656
|
+
title: |
|
657
|
+
#{resource[:type].capitalize} require labels
|
658
|
+
item: |
|
659
|
+
- [ ] [{{title}}]({{web_url}}) {{labels}}
|
660
|
+
summary: |
|
661
|
+
The following {{type}} require labels:
|
644
662
|
|
645
|
-
|
663
|
+
{{items}}
|
646
664
|
|
647
|
-
|
665
|
+
Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
|
648
666
|
|
649
|
-
|
667
|
+
/label ~"needs attention"
|
650
668
|
```
|
651
669
|
|
652
670
|
Which could generate an issue like:
|
@@ -688,9 +706,9 @@ resource_rules:
|
|
688
706
|
- name: Newest and oldest issues summary
|
689
707
|
actions:
|
690
708
|
summarize:
|
691
|
-
title: "Newest and oldest
|
709
|
+
title: "Newest and oldest {{type}} summary"
|
692
710
|
summary: |
|
693
|
-
Please triage the following
|
711
|
+
Please triage the following {{type}}:
|
694
712
|
|
695
713
|
{{items}}
|
696
714
|
|
@@ -707,7 +725,7 @@ resource_rules:
|
|
707
725
|
summarize:
|
708
726
|
item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
|
709
727
|
summary: |
|
710
|
-
Please triage the following new
|
728
|
+
Please triage the following new {{type}}:
|
711
729
|
|
712
730
|
{{items}}
|
713
731
|
- name: Old issues
|
@@ -719,7 +737,7 @@ resource_rules:
|
|
719
737
|
summarize:
|
720
738
|
item: "- [ ] [{{title}}]({{web_url}}) {{labels}}"
|
721
739
|
summary: |
|
722
|
-
Please triage the following old
|
740
|
+
Please triage the following old {{type}}:
|
723
741
|
|
724
742
|
{{items}}
|
725
743
|
```
|
@@ -759,6 +777,10 @@ Here's a list of currently available Ruby expression API:
|
|
759
777
|
| ---- | ---- | ---- |
|
760
778
|
| resource | Hash | The hash containing the raw data of the resource |
|
761
779
|
| milestone | Milestone | The milestone attached to the resource |
|
780
|
+
| labels | [Label] | A list of labels, having only names |
|
781
|
+
| labels_with_details | [Label] | A list of labels which has more information loaded from another API request |
|
782
|
+
| labels_chronologically | [Label] | Same as `labels_with_details` but sorted chronologically |
|
783
|
+
| label_events | [LabelEvent] | A list of label events on the resource |
|
762
784
|
| instance_version | InstanceVersion | The version for the GitLab instance we're triaging with |
|
763
785
|
|
764
786
|
##### Methods for `Milestone`
|
@@ -779,6 +801,29 @@ Here's a list of currently available Ruby expression API:
|
|
779
801
|
| succ | Milestone | The next active milestone beside this milestone |
|
780
802
|
| active? | Boolean | `true` if `state` is `active`; `false` otherwise |
|
781
803
|
|
804
|
+
##### Methods for `Label`
|
805
|
+
|
806
|
+
| Method | Return type | Description |
|
807
|
+
| ---- | ---- | ---- |
|
808
|
+
| id | Integer | The id of the label |
|
809
|
+
| project_id | Integer | The project id of the label if available |
|
810
|
+
| group_id | Integer | The group id of the label if available |
|
811
|
+
| name | String | The name of the label |
|
812
|
+
| description | String | The description of the label |
|
813
|
+
| color | String | The color of the label in RGB |
|
814
|
+
| priority | Integer | The priority of the label |
|
815
|
+
| added_at | Time | When the label was added to the resource |
|
816
|
+
|
817
|
+
##### Methods for `LabelEvent`
|
818
|
+
|
819
|
+
| Method | Return type | Description |
|
820
|
+
| ---- | ---- | ---- |
|
821
|
+
| id | Integer | The id of the label event |
|
822
|
+
| resource_type | String | The resource type of the event. Could be `Issue` or `MergeRequest` |
|
823
|
+
| resource_id | Integer | The id of the resource |
|
824
|
+
| action | String | The action of the event. Could be `add` or `remove` |
|
825
|
+
| created_at | Time | When the event happened |
|
826
|
+
|
782
827
|
##### Methods for `InstanceVersion`
|
783
828
|
|
784
829
|
| Method | Return type | Description |
|
data/bin/gitlab-triage
CHANGED
@@ -1,82 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'optparse'
|
4
3
|
require 'yaml'
|
4
|
+
require_relative '../lib/gitlab/triage/option_parser'
|
5
5
|
require_relative '../lib/gitlab/triage/engine'
|
6
6
|
|
7
|
-
|
8
|
-
:dry_run,
|
9
|
-
:policies_file,
|
10
|
-
:project_id,
|
11
|
-
:token,
|
12
|
-
:debug,
|
13
|
-
:host_url
|
14
|
-
)
|
15
|
-
|
16
|
-
class TriageOptionParser
|
17
|
-
class << self
|
18
|
-
def parse(argv)
|
19
|
-
options = Options.new
|
20
|
-
options.host_url = 'https://gitlab.com'
|
21
|
-
|
22
|
-
parser = OptionParser.new do |opts|
|
23
|
-
opts.banner = "Usage: #{__FILE__} [options]\n\n"
|
24
|
-
|
25
|
-
opts.on('-n', '--dry-run', "Don't actually update anything, just print") do |value|
|
26
|
-
options.dry_run = value
|
27
|
-
end
|
28
|
-
|
29
|
-
opts.on('-f', '--policies-file [string]', String, 'A valid policies YML file') do |value|
|
30
|
-
options.policies_file = value
|
31
|
-
end
|
32
|
-
|
33
|
-
opts.on('-p', '--project-id [string]', String, 'A project ID or path') do |value|
|
34
|
-
options.project_id = value
|
35
|
-
end
|
36
|
-
|
37
|
-
opts.on('-t', '--token [string]', String, 'A valid API token') do |value|
|
38
|
-
options.token = value
|
39
|
-
end
|
40
|
-
|
41
|
-
opts.on('-H', '--host-url [string]', String, 'A valid host url') do |value|
|
42
|
-
options.host_url = value
|
43
|
-
end
|
44
|
-
|
45
|
-
opts.on('-d', '--debug', 'Print debug information') do |value|
|
46
|
-
options.debug = value
|
47
|
-
end
|
48
|
-
|
49
|
-
opts.on('-h', '--help', 'Print help message') do
|
50
|
-
$stdout.puts opts
|
51
|
-
exit
|
52
|
-
end
|
53
|
-
|
54
|
-
opts.on('--init', 'Initialize the project with a policy file') do
|
55
|
-
example_path =
|
56
|
-
File.expand_path('../support/.triage-policies.example.yml', __dir__)
|
57
|
-
|
58
|
-
FileUtils.cp(example_path, '.triage-policies.yml')
|
59
|
-
exit
|
60
|
-
end
|
61
|
-
|
62
|
-
opts.on('--init-ci', 'Initialize the project with a .gitlab-ci.yml file') do
|
63
|
-
example_path =
|
64
|
-
File.expand_path('../support/.gitlab-ci.example.yml', __dir__)
|
65
|
-
|
66
|
-
FileUtils.cp(example_path, '.gitlab-ci.yml')
|
67
|
-
exit
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
parser.parse!(argv)
|
72
|
-
|
73
|
-
options
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
options = TriageOptionParser.parse(ARGV)
|
79
|
-
|
7
|
+
options = Gitlab::Triage::OptionParser.parse(ARGV)
|
80
8
|
policies_file = options.policies_file || '.triage-policies.yml'
|
81
9
|
policies = HashWithIndifferentAccess.new(YAML.load_file(policies_file))
|
82
10
|
|
@@ -2,11 +2,11 @@ module Gitlab
|
|
2
2
|
module Triage
|
3
3
|
module Action
|
4
4
|
class Base
|
5
|
-
attr_reader :policy, :
|
5
|
+
attr_reader :policy, :network
|
6
6
|
|
7
|
-
def initialize(policy:,
|
7
|
+
def initialize(policy:, network:)
|
8
8
|
@policy = policy
|
9
|
-
@
|
9
|
+
@network = network
|
10
10
|
end
|
11
11
|
end
|
12
12
|
end
|
@@ -39,7 +39,7 @@ module Gitlab
|
|
39
39
|
def build_comment(resource)
|
40
40
|
CommandBuilders::CommentCommandBuilder.new(
|
41
41
|
[
|
42
|
-
CommandBuilders::TextContentBuilder.new(policy.actions[:comment], resource: resource,
|
42
|
+
CommandBuilders::TextContentBuilder.new(policy.actions[:comment], resource: resource, network: network).build_command,
|
43
43
|
CommandBuilders::LabelCommandBuilder.new(policy.actions[:labels]).build_command,
|
44
44
|
CommandBuilders::RemoveLabelCommandBuilder.new(policy.actions[:remove_labels]).build_command,
|
45
45
|
CommandBuilders::CcCommandBuilder.new(policy.actions[:mention]).build_command,
|
@@ -49,8 +49,7 @@ module Gitlab
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def perform(resource, comment)
|
52
|
-
|
53
|
-
net[:token],
|
52
|
+
network.post_api(
|
54
53
|
build_post_url(resource),
|
55
54
|
body: comment)
|
56
55
|
end
|
@@ -58,15 +57,14 @@ module Gitlab
|
|
58
57
|
def build_post_url(resource)
|
59
58
|
# POST /projects/:id/issues/:issue_iid/notes
|
60
59
|
post_url = UrlBuilders::UrlBuilder.new(
|
61
|
-
|
62
|
-
|
63
|
-
source_id: net[:source_id],
|
60
|
+
network_options: network.options,
|
61
|
+
source_id: network.options.project_id,
|
64
62
|
resource_type: policy.type,
|
65
63
|
resource_id: resource['iid'],
|
66
64
|
sub_resource_type: 'notes'
|
67
65
|
).build
|
68
66
|
|
69
|
-
puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if
|
67
|
+
puts Gitlab::Triage::UI.debug "post_url: #{post_url}" if network.options.debug
|
70
68
|
|
71
69
|
post_url
|
72
70
|
end
|
@@ -25,7 +25,7 @@ module Gitlab
|
|
25
25
|
private
|
26
26
|
|
27
27
|
def perform
|
28
|
-
|
28
|
+
network.post_api(post_issue_url, post_issue_body)
|
29
29
|
end
|
30
30
|
|
31
31
|
def issue
|
@@ -36,13 +36,12 @@ module Gitlab
|
|
36
36
|
# POST /projects/:id/issues
|
37
37
|
# https://docs.gitlab.com/ee/api/issues.html#new-issue
|
38
38
|
post_url = UrlBuilders::UrlBuilder.new(
|
39
|
-
|
40
|
-
|
41
|
-
source_id: net[:source_id],
|
39
|
+
network_options: network.options,
|
40
|
+
source_id: network.options.project_id,
|
42
41
|
resource_type: 'issues'
|
43
42
|
).build
|
44
43
|
|
45
|
-
puts Gitlab::Triage::UI.debug "post_issue_url: #{post_url}" if
|
44
|
+
puts Gitlab::Triage::UI.debug "post_issue_url: #{post_url}" if network.options.debug
|
46
45
|
|
47
46
|
post_url
|
48
47
|
end
|
@@ -26,16 +26,19 @@ module Gitlab
|
|
26
26
|
downvotes: "{{downvotes}}",
|
27
27
|
title: "{{title}}",
|
28
28
|
web_url: "{{web_url}}",
|
29
|
+
type: "{{type}}",
|
29
30
|
items: "{{items}}"
|
30
31
|
}.freeze
|
31
32
|
PLACEHOLDER_REGEX = /{{([\w\.]+)}}/
|
32
33
|
|
33
|
-
attr_reader :resource, :
|
34
|
+
attr_reader :resource, :network
|
34
35
|
|
35
|
-
def initialize(
|
36
|
+
def initialize(
|
37
|
+
items, resource: nil, network: nil, redact_confidentials: true)
|
36
38
|
super(items)
|
37
|
-
@resource = resource
|
38
|
-
@
|
39
|
+
@resource = resource&.with_indifferent_access
|
40
|
+
@network = network
|
41
|
+
@redact_confidentials = redact_confidentials
|
39
42
|
end
|
40
43
|
|
41
44
|
private
|
@@ -53,30 +56,65 @@ module Gitlab
|
|
53
56
|
def eval_interpolation(item)
|
54
57
|
quoted_comment = "%Q{#{item}}"
|
55
58
|
|
56
|
-
Resource::Context.
|
59
|
+
Resource::Context.build(
|
60
|
+
resource,
|
61
|
+
network: network,
|
62
|
+
redact_confidentials: @redact_confidentials
|
63
|
+
).eval(quoted_comment)
|
57
64
|
end
|
58
65
|
|
59
66
|
def replace_placeholders(item)
|
60
|
-
SUPPORTED_PLACEHOLDERS.inject(item) do |comment, (placeholder,
|
67
|
+
SUPPORTED_PLACEHOLDERS.inject(item) do |comment, (placeholder, template)|
|
61
68
|
next comment unless comment.include?("{{#{placeholder}}}")
|
62
69
|
|
63
|
-
|
64
|
-
|
70
|
+
path = template[/.*#{PLACEHOLDER_REGEX}.*/, 1]
|
71
|
+
attributes = extract_attributes(path)
|
65
72
|
|
66
|
-
|
73
|
+
formatted_text = attributes.map do |attribute|
|
74
|
+
template.sub(PLACEHOLDER_REGEX, attribute.to_s)
|
75
|
+
end.join(', ')
|
67
76
|
|
68
|
-
comment.gsub("{{#{placeholder}}}",
|
77
|
+
comment.gsub("{{#{placeholder}}}", formatted_text)
|
69
78
|
end
|
70
79
|
end
|
71
80
|
|
72
|
-
def
|
73
|
-
|
81
|
+
def extract_attributes(path)
|
82
|
+
redact_attributes(path, resource_dig_and_map(path.split('.')))
|
83
|
+
end
|
84
|
+
|
85
|
+
# If we don't have to map arrays, we can simply do:
|
86
|
+
#
|
87
|
+
# resource.dig(*indices)
|
88
|
+
#
|
89
|
+
# Thus this method name. The only array here is `assignees`
|
90
|
+
def resource_dig_and_map(indices)
|
91
|
+
attributes = indices.inject(resource) do |result, index|
|
92
|
+
break unless result
|
93
|
+
|
94
|
+
case result
|
95
|
+
when Array
|
96
|
+
result.flat_map { |sub_resource| sub_resource[index] }
|
97
|
+
else
|
98
|
+
result[index]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
Array.wrap(attributes)
|
103
|
+
end
|
104
|
+
|
105
|
+
def redact_attributes(path, attributes)
|
106
|
+
return attributes unless redact_confidential_attributes?
|
74
107
|
|
75
|
-
|
76
|
-
|
108
|
+
case path
|
109
|
+
when 'web_url', 'items', 'type'
|
110
|
+
attributes # No need to redact them
|
111
|
+
else
|
112
|
+
[Resource::Base::CONFIDENTIAL_TEXT]
|
113
|
+
end
|
114
|
+
end
|
77
115
|
|
78
|
-
|
79
|
-
|
116
|
+
def redact_confidential_attributes?
|
117
|
+
@redact_confidentials && resource[:confidential]
|
80
118
|
end
|
81
119
|
end
|
82
120
|
end
|