gitlab-triage 0.14.1 → 0.15.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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -24
  3. data/bin/gitlab-triage +2 -74
  4. data/lib/gitlab/triage/action/base.rb +3 -3
  5. data/lib/gitlab/triage/action/comment.rb +5 -7
  6. data/lib/gitlab/triage/action/summarize.rb +4 -5
  7. data/lib/gitlab/triage/command_builders/base_command_builder.rb +1 -1
  8. data/lib/gitlab/triage/command_builders/text_content_builder.rb +54 -16
  9. data/lib/gitlab/triage/engine.rb +27 -34
  10. data/lib/gitlab/triage/entity_builders/issue_builder.rb +27 -17
  11. data/lib/gitlab/triage/filters/member_conditions_filter.rb +6 -8
  12. data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +5 -3
  13. data/lib/gitlab/triage/network.rb +20 -8
  14. data/lib/gitlab/triage/network_adapters/base_adapter.rb +1 -1
  15. data/lib/gitlab/triage/option_parser.rb +72 -0
  16. data/lib/gitlab/triage/options.rb +21 -0
  17. data/lib/gitlab/triage/policies/base_policy.rb +21 -2
  18. data/lib/gitlab/triage/policies/rule_policy.rb +7 -1
  19. data/lib/gitlab/triage/policies/summary_policy.rb +25 -2
  20. data/lib/gitlab/triage/resource/base.rb +49 -10
  21. data/lib/gitlab/triage/resource/context.rb +13 -8
  22. data/lib/gitlab/triage/resource/instance_version.rb +3 -4
  23. data/lib/gitlab/triage/resource/issue.rb +14 -0
  24. data/lib/gitlab/triage/resource/label.rb +41 -0
  25. data/lib/gitlab/triage/resource/label_event.rb +45 -0
  26. data/lib/gitlab/triage/resource/merge_request.rb +14 -0
  27. data/lib/gitlab/triage/resource/milestone.rb +5 -5
  28. data/lib/gitlab/triage/resource/shared/issuable.rb +54 -0
  29. data/lib/gitlab/triage/url_builders/url_builder.rb +3 -2
  30. data/lib/gitlab/triage/version.rb +1 -1
  31. data/support/.triage-policies.example.yml +4 -4
  32. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a4644219bf9e18ad39bca39fcac9a122f46d69f18dfb618ad441e364200e53a
4
- data.tar.gz: '032997cbb0b32059455367606d8647e532c3a440c07289d329b9453e2ef29b39'
3
+ metadata.gz: 2282112f85b0f8af0edd3fd6c553848768e15c2b39402b1e9344564626086a9c
4
+ data.tar.gz: 55f84c356fd84e7cd35aea63d2f6c377a96a82b13f23322dece9d9eb077c7f98
5
5
  SHA512:
6
- metadata.gz: b141fff9291004febcb944b829285358dd8f0c791e044015a3108c259ba95456bb67ae1ac7c0c8f6b02f72faa04a2b725559a6a9803651fecb92ce7668f9d3cc
7
- data.tar.gz: 9871a88ac9ecd507d65dd26343e971caa548976bc67c3c7b8533e61dd5f07e93bd98620b9da94539237116d20ae5cf3eafb3bf5dc5ad15781488f9e0fe3fa48e
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: Issues require labels
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 | no | no |
617
- | `item` | string | Template representing each triaged resource | no | yes | yes |
618
- | `summary` | string | The description of the generated issue | no | Only `{{items}}` and `{{title}}` | yes |
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}}` and `{{title}}` are supported because it's not tied to a
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
- limits:
636
- most_recent: 15
637
- actions:
638
- summarize:
639
- title: Issues require labels
640
- item: |
641
- - [ ] [{{title}}]({{web_url}}) {{labels}}
642
- summary: |
643
- The following issues require labels:
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
- {{items}}
663
+ {{items}}
646
664
 
647
- Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
665
+ Please take care of them before the end of #{7.days.from_now.strftime('%Y-%m-%d')}
648
666
 
649
- /label ~"needs attention"
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 issues summary"
709
+ title: "Newest and oldest {{type}} summary"
692
710
  summary: |
693
- Please triage the following issues:
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 issues:
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 issues:
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 |
@@ -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
- Options = Struct.new(
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, :net
5
+ attr_reader :policy, :network
6
6
 
7
- def initialize(policy:, net:)
7
+ def initialize(policy:, network:)
8
8
  @policy = policy
9
- @net = net
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, net: net).build_command,
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
- net[:network].post_api(
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
- host_url: net[:host_url],
62
- api_version: net[:api_version],
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 net[:debug]
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
- net[:network].post_api(net[:token], post_issue_url, post_issue_body)
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
- host_url: net[:host_url],
40
- api_version: net[:api_version],
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 net[:debug]
44
+ puts Gitlab::Triage::UI.debug "post_issue_url: #{post_url}" if network.options.debug
46
45
 
47
46
  post_url
48
47
  end
@@ -3,7 +3,7 @@ module Gitlab
3
3
  module CommandBuilders
4
4
  class BaseCommandBuilder
5
5
  def initialize(items)
6
- @items = Array(items)
6
+ @items = Array.wrap(items)
7
7
  @items.delete('')
8
8
  end
9
9
 
@@ -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, :net
34
+ attr_reader :resource, :network
34
35
 
35
- def initialize(items, resource: nil, net: {})
36
+ def initialize(
37
+ items, resource: nil, network: nil, redact_confidentials: true)
36
38
  super(items)
37
- @resource = resource
38
- @net = net
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.new(resource, net).eval(quoted_comment)
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, formatted_text)|
67
+ SUPPORTED_PLACEHOLDERS.inject(item) do |comment, (placeholder, template)|
61
68
  next comment unless comment.include?("{{#{placeholder}}}")
62
69
 
63
- methods = formatted_text.match(/.*#{PLACEHOLDER_REGEX}.*/)[1].split('.')
64
- fields = resource_fields(resource, methods)
70
+ path = template[/.*#{PLACEHOLDER_REGEX}.*/, 1]
71
+ attributes = extract_attributes(path)
65
72
 
66
- final_fields = fields.map { |field| formatted_text.sub(PLACEHOLDER_REGEX, field.to_s) }
73
+ formatted_text = attributes.map do |attribute|
74
+ template.sub(PLACEHOLDER_REGEX, attribute.to_s)
75
+ end.join(', ')
67
76
 
68
- comment.gsub("{{#{placeholder}}}", final_fields.join(', '))
77
+ comment.gsub("{{#{placeholder}}}", formatted_text)
69
78
  end
70
79
  end
71
80
 
72
- def resource_fields(resource, methods)
73
- method = methods.shift
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
- return Array(resource) unless method && resource
76
- return [] unless resource.key?(method)
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
- sub_resources = resource[method]
79
- Array.wrap(sub_resources).flat_map { |res| resource_fields(res, methods.dup) }
116
+ def redact_confidential_attributes?
117
+ @redact_confidentials && resource[:confidential]
80
118
  end
81
119
  end
82
120
  end