payping-gitlab-triage 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +19 -0
  3. data/.gitignore +15 -0
  4. data/.gitlab/CODEOWNERS +2 -0
  5. data/.gitlab/changelog_config.yml +13 -0
  6. data/.gitlab/issue_templates/Default.md +13 -0
  7. data/.gitlab/merge_request_templates/Default.md +11 -0
  8. data/.gitlab/merge_request_templates/Release.md +13 -0
  9. data/.gitlab-ci.yml +146 -0
  10. data/.rubocop.yml +21 -0
  11. data/.rubocop_todo.yml +145 -0
  12. data/.ruby-version +1 -0
  13. data/.tool-versions +1 -0
  14. data/.yardopts +4 -0
  15. data/CONTRIBUTING.md +31 -0
  16. data/Dangerfile +5 -0
  17. data/Gemfile +15 -0
  18. data/Guardfile +70 -0
  19. data/LICENSE.md +25 -0
  20. data/README.md +1480 -0
  21. data/Rakefile +6 -0
  22. data/bin/gitlab-triage +19 -0
  23. data/gitlab-triage.gemspec +41 -0
  24. data/lib/gitlab/triage/action/base.rb +14 -0
  25. data/lib/gitlab/triage/action/comment.rb +104 -0
  26. data/lib/gitlab/triage/action/comment_on_summary.rb +83 -0
  27. data/lib/gitlab/triage/action/delete.rb +56 -0
  28. data/lib/gitlab/triage/action/issue.rb +64 -0
  29. data/lib/gitlab/triage/action/summarize.rb +82 -0
  30. data/lib/gitlab/triage/action.rb +36 -0
  31. data/lib/gitlab/triage/api_query_builders/base_query_param_builder.rb +27 -0
  32. data/lib/gitlab/triage/api_query_builders/date_query_param_builder.rb +42 -0
  33. data/lib/gitlab/triage/api_query_builders/multi_query_param_builder.rb +28 -0
  34. data/lib/gitlab/triage/api_query_builders/single_query_param_builder.rb +13 -0
  35. data/lib/gitlab/triage/command_builders/base_command_builder.rb +40 -0
  36. data/lib/gitlab/triage/command_builders/cc_command_builder.rb +19 -0
  37. data/lib/gitlab/triage/command_builders/comment_command_builder.rb +19 -0
  38. data/lib/gitlab/triage/command_builders/label_command_builder.rb +40 -0
  39. data/lib/gitlab/triage/command_builders/move_command_builder.rb +19 -0
  40. data/lib/gitlab/triage/command_builders/remove_label_command_builder.rb +15 -0
  41. data/lib/gitlab/triage/command_builders/status_command_builder.rb +23 -0
  42. data/lib/gitlab/triage/command_builders/text_content_builder.rb +138 -0
  43. data/lib/gitlab/triage/engine.rb +635 -0
  44. data/lib/gitlab/triage/entity_builders/issue_builder.rb +54 -0
  45. data/lib/gitlab/triage/entity_builders/summary_builder.rb +82 -0
  46. data/lib/gitlab/triage/errors/network.rb +11 -0
  47. data/lib/gitlab/triage/errors.rb +1 -0
  48. data/lib/gitlab/triage/expand_condition/expansion.rb +203 -0
  49. data/lib/gitlab/triage/expand_condition/list.rb +25 -0
  50. data/lib/gitlab/triage/expand_condition/sequence.rb +25 -0
  51. data/lib/gitlab/triage/expand_condition.rb +23 -0
  52. data/lib/gitlab/triage/filters/assignee_member_conditions_filter.rb +13 -0
  53. data/lib/gitlab/triage/filters/author_member_conditions_filter.rb +13 -0
  54. data/lib/gitlab/triage/filters/base_conditions_filter.rb +58 -0
  55. data/lib/gitlab/triage/filters/branch_date_filter.rb +73 -0
  56. data/lib/gitlab/triage/filters/branch_protected_filter.rb +26 -0
  57. data/lib/gitlab/triage/filters/discussions_conditions_filter.rb +58 -0
  58. data/lib/gitlab/triage/filters/issue_date_conditions_filter.rb +78 -0
  59. data/lib/gitlab/triage/filters/member_conditions_filter.rb +84 -0
  60. data/lib/gitlab/triage/filters/merge_request_date_conditions_filter.rb +13 -0
  61. data/lib/gitlab/triage/filters/name_conditions_filter.rb +26 -0
  62. data/lib/gitlab/triage/filters/no_additional_labels_conditions_filter.rb +30 -0
  63. data/lib/gitlab/triage/filters/ruby_conditions_filter.rb +33 -0
  64. data/lib/gitlab/triage/filters/votes_conditions_filter.rb +54 -0
  65. data/lib/gitlab/triage/graphql_network.rb +81 -0
  66. data/lib/gitlab/triage/graphql_queries/query_builder.rb +158 -0
  67. data/lib/gitlab/triage/graphql_queries/query_param_builders/base_param_builder.rb +30 -0
  68. data/lib/gitlab/triage/graphql_queries/query_param_builders/date_param_builder.rb +35 -0
  69. data/lib/gitlab/triage/graphql_queries/query_param_builders/labels_param_builder.rb +18 -0
  70. data/lib/gitlab/triage/limiters/base_limiter.rb +35 -0
  71. data/lib/gitlab/triage/limiters/date_field_limiter.rb +45 -0
  72. data/lib/gitlab/triage/network.rb +39 -0
  73. data/lib/gitlab/triage/network_adapters/base_adapter.rb +17 -0
  74. data/lib/gitlab/triage/network_adapters/graphql_adapter.rb +92 -0
  75. data/lib/gitlab/triage/network_adapters/httparty_adapter.rb +116 -0
  76. data/lib/gitlab/triage/network_adapters/test_adapter.rb +39 -0
  77. data/lib/gitlab/triage/option_parser.rb +105 -0
  78. data/lib/gitlab/triage/options.rb +30 -0
  79. data/lib/gitlab/triage/param_builders/date_param_builder.rb +64 -0
  80. data/lib/gitlab/triage/policies/base_policy.rb +80 -0
  81. data/lib/gitlab/triage/policies/rule_policy.rb +36 -0
  82. data/lib/gitlab/triage/policies/summary_policy.rb +29 -0
  83. data/lib/gitlab/triage/policies_resources/rule_resources.rb +11 -0
  84. data/lib/gitlab/triage/policies_resources/summary_resources.rb +11 -0
  85. data/lib/gitlab/triage/resource/base.rb +102 -0
  86. data/lib/gitlab/triage/resource/branch.rb +13 -0
  87. data/lib/gitlab/triage/resource/context.rb +47 -0
  88. data/lib/gitlab/triage/resource/epic.rb +20 -0
  89. data/lib/gitlab/triage/resource/instance_version.rb +35 -0
  90. data/lib/gitlab/triage/resource/issue.rb +52 -0
  91. data/lib/gitlab/triage/resource/label.rb +56 -0
  92. data/lib/gitlab/triage/resource/label_event.rb +48 -0
  93. data/lib/gitlab/triage/resource/linked_issue.rb +15 -0
  94. data/lib/gitlab/triage/resource/merge_request.rb +23 -0
  95. data/lib/gitlab/triage/resource/milestone.rb +98 -0
  96. data/lib/gitlab/triage/resource/shared/issuable.rb +119 -0
  97. data/lib/gitlab/triage/rest_api_network.rb +125 -0
  98. data/lib/gitlab/triage/retryable.rb +33 -0
  99. data/lib/gitlab/triage/ui.rb +23 -0
  100. data/lib/gitlab/triage/url_builders/url_builder.rb +54 -0
  101. data/lib/gitlab/triage/utils.rb +13 -0
  102. data/lib/gitlab/triage/validators/limiter_validator.rb +21 -0
  103. data/lib/gitlab/triage/validators/params_validator.rb +43 -0
  104. data/lib/gitlab/triage/version.rb +7 -0
  105. data/lib/gitlab/triage.rb +6 -0
  106. data/support/.gitlab-ci.example.yml +22 -0
  107. data/support/.triage-policies.example.yml +51 -0
  108. metadata +280 -0
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../url_builders/url_builder'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Resource
8
+ class Base
9
+ attr_reader :resource, :parent
10
+
11
+ CONFIDENTIAL_TEXT = '(confidential)'
12
+
13
+ def self.define_field(name, &block)
14
+ define_method(name) do
15
+ if redact_confidential_attributes?
16
+ CONFIDENTIAL_TEXT
17
+ else
18
+ instance_eval(&block)
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(
24
+ resource, parent: nil, network: nil, redact_confidentials: true)
25
+ @resource = resource
26
+ @parent = parent
27
+ @network = network
28
+ @redact_confidentials = redact_confidentials
29
+ end
30
+
31
+ protected
32
+
33
+ def redact_confidential_attributes?
34
+ parent&.redact_confidential_attributes? ||
35
+ (@redact_confidentials && resource[:confidential])
36
+ end
37
+
38
+ def network
39
+ parent&.network || @network
40
+ end
41
+
42
+ private
43
+
44
+ def expand_resource!(params: {})
45
+ resource.merge!(
46
+ network.query_api_cached(resource_url(params: params)).first)
47
+ end
48
+
49
+ def source_resource
50
+ @source_resource ||= network.query_api_cached(source_url).first
51
+ end
52
+
53
+ def source_url
54
+ build_url(options: { resource_type: nil })
55
+ end
56
+
57
+ def url(params = {})
58
+ build_url(params: params)
59
+ end
60
+
61
+ def resource_id
62
+ resource[:iid]
63
+ end
64
+
65
+ def resource_url(params: {}, sub_resource_type: nil)
66
+ build_url(
67
+ params: params,
68
+ options: {
69
+ resource_id: resource_id,
70
+ sub_resource_type: sub_resource_type
71
+ }
72
+ )
73
+ end
74
+
75
+ def build_url(params: {}, options: {})
76
+ UrlBuilders::UrlBuilder.new(
77
+ url_opts
78
+ .merge(options)
79
+ .merge(params: { per_page: 100 }.merge(params))
80
+ ).build
81
+ end
82
+
83
+ def url_opts
84
+ {
85
+ network_options: network.options,
86
+ resource_type: self.class.name.demodulize.underscore.pluralize,
87
+ source: source,
88
+ source_id: resource[:"#{source.singularize}_id"]
89
+ }
90
+ end
91
+
92
+ def source
93
+ if resource[:project_id]
94
+ 'projects'
95
+ elsif resource[:group_id]
96
+ 'groups'
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'shared/issuable'
5
+
6
+ module Gitlab
7
+ module Triage
8
+ module Resource
9
+ class Branch < Base
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'epic'
5
+ require_relative 'issue'
6
+ require_relative 'linked_issue'
7
+ require_relative 'merge_request'
8
+ require_relative 'instance_version'
9
+ require_relative 'branch'
10
+
11
+ module Gitlab
12
+ module Triage
13
+ module Resource
14
+ module Context
15
+ EvaluationError = Class.new(RuntimeError)
16
+
17
+ def self.build(resource, **options)
18
+ const_name = (resource[:type] || 'Base')
19
+ .to_s.singularize.camelcase
20
+
21
+ Resource.const_get(const_name).new(resource, **options).extend(self)
22
+ end
23
+
24
+ def eval(ruby)
25
+ instance_eval <<~RUBY, __FILE__, __LINE__ + 1
26
+ begin
27
+ #{ruby}
28
+ rescue StandardError, ScriptError => e
29
+ raise EvaluationError.new(e.message)
30
+ end
31
+ RUBY
32
+ rescue EvaluationError => e
33
+ # This way we could obtain the original backtrace and error
34
+ # If we just let instance_eval raise an error, the backtrace
35
+ # won't contain the actual line where it's giving an error.
36
+ raise e.cause
37
+ end
38
+
39
+ private
40
+
41
+ def instance_version
42
+ @instance_version ||= InstanceVersion.new(parent: self)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'shared/issuable'
5
+
6
+ module Gitlab
7
+ module Triage
8
+ module Resource
9
+ class Epic < Base
10
+ include Shared::Issuable
11
+
12
+ def project_path
13
+ @project_path ||=
14
+ request_group(resource[:group_id])[:full_path]
15
+ end
16
+ alias_method :group_path, :project_path
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require_relative 'base'
2
+
3
+ module Gitlab
4
+ module Triage
5
+ module Resource
6
+ class InstanceVersion < Base
7
+ def initialize(**options)
8
+ super({}, **options)
9
+ end
10
+
11
+ def version
12
+ response[:version]
13
+ end
14
+
15
+ def version_short
16
+ version[/^\d+\.\d+/]
17
+ end
18
+
19
+ def revision
20
+ response[:revision]
21
+ end
22
+
23
+ private
24
+
25
+ # See https://gitlab.com/api/v4/version
26
+ def response
27
+ @response ||=
28
+ network.query_api_cached(
29
+ "#{network.options.host_url}/api/#{network.options.api_version}/version")
30
+ .first
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'shared/issuable'
5
+
6
+ module Gitlab
7
+ module Triage
8
+ module Resource
9
+ class Issue < Base
10
+ include Shared::Issuable
11
+
12
+ DATE_FIELDS = %i[
13
+ due_date
14
+ ].freeze
15
+
16
+ DATE_FIELDS.each do |field|
17
+ define_field(field) do
18
+ value = resource[field]
19
+
20
+ Date.parse(value) if value
21
+ end
22
+ end
23
+
24
+ def merge_requests_count
25
+ @merge_requests_count ||= resource.dig(:merge_requests_count)
26
+ end
27
+
28
+ def related_merge_requests
29
+ @related_merge_requests ||= network.query_api_cached(
30
+ resource_url(sub_resource_type: 'related_merge_requests'))
31
+ .map { |merge_request| MergeRequest.new(merge_request, parent: self) }
32
+ end
33
+
34
+ def closed_by
35
+ @closed_by ||= network.query_api_cached(
36
+ resource_url(sub_resource_type: 'closed_by'))
37
+ .map { |merge_request| MergeRequest.new(merge_request, parent: self) }
38
+ end
39
+
40
+ def linked_issues
41
+ @linked_issues ||= network.query_api_cached(
42
+ resource_url(sub_resource_type: 'links'))
43
+ .map { |issue| LinkedIssue.new(issue, parent: self) }
44
+ end
45
+
46
+ def expired?(today = Date.today)
47
+ due_date && due_date < today
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'date'
5
+ require 'time'
6
+
7
+ module Gitlab
8
+ module Triage
9
+ module Resource
10
+ class Label < Base
11
+ LabelDoesntExistError = Class.new(StandardError)
12
+
13
+ FIELDS = %i[
14
+ id
15
+ project_id
16
+ group_id
17
+ name
18
+ description
19
+ color
20
+ priority
21
+ ].freeze
22
+
23
+ TIME_FIELDS = %i[
24
+ added_at
25
+ ].freeze
26
+
27
+ FIELDS.each do |field|
28
+ define_field(field) do
29
+ resource[field]
30
+ end
31
+ end
32
+
33
+ TIME_FIELDS.each do |field|
34
+ define_field(field) do
35
+ value = resource[field]
36
+
37
+ Time.parse(value) if value
38
+ end
39
+ end
40
+
41
+ def exist?
42
+ label = network.query_api_cached(resource_url).first
43
+ return false unless label
44
+
45
+ label[:name] == name
46
+ end
47
+
48
+ private
49
+
50
+ def resource_id
51
+ name
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'label'
5
+ require 'date'
6
+ require 'time'
7
+
8
+ module Gitlab
9
+ module Triage
10
+ module Resource
11
+ class LabelEvent < Base
12
+ FIELDS = %i[
13
+ id
14
+ user
15
+ resource_type
16
+ resource_id
17
+ action
18
+ ].freeze
19
+
20
+ TIME_FIELDS = %i[
21
+ created_at
22
+ ].freeze
23
+
24
+ FIELDS.each do |field|
25
+ define_field(field) do
26
+ resource[field]
27
+ end
28
+ end
29
+
30
+ TIME_FIELDS.each do |field|
31
+ define_field(field) do
32
+ value = resource[field]
33
+
34
+ Time.parse(value) if value
35
+ end
36
+ end
37
+
38
+ def label
39
+ return unless resource[:label]
40
+
41
+ @label ||= Label.new(
42
+ resource[:label].reverse_merge(added_at: resource[:created_at]),
43
+ parent: self)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'issue'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Resource
8
+ class LinkedIssue < Issue
9
+ def link_type
10
+ @link_type ||= resource.dig(:link_type)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'shared/issuable'
5
+
6
+ module Gitlab
7
+ module Triage
8
+ module Resource
9
+ class MergeRequest < Base
10
+ include Shared::Issuable
11
+
12
+ def first_contribution?
13
+ if resource.key?(:first_contribution)
14
+ resource[:first_contribution]
15
+ else
16
+ expanded = expand_resource!
17
+ expanded[:first_contribution]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ require_relative 'base'
2
+ require 'date'
3
+ require 'time'
4
+
5
+ module Gitlab
6
+ module Triage
7
+ module Resource
8
+ class Milestone < Base
9
+ FIELDS = %i[
10
+ id
11
+ iid
12
+ project_id
13
+ group_id
14
+ title
15
+ description
16
+ state
17
+ ].freeze
18
+
19
+ DATE_FIELDS = %i[
20
+ due_date
21
+ start_date
22
+ ].freeze
23
+
24
+ TIME_FIELDS = %i[
25
+ updated_at
26
+ created_at
27
+ ].freeze
28
+
29
+ FIELDS.each do |field|
30
+ define_field(field) do
31
+ resource[field]
32
+ end
33
+ end
34
+
35
+ DATE_FIELDS.each do |field|
36
+ define_field(field) do
37
+ value = resource[field]
38
+
39
+ Date.parse(value) if value
40
+ end
41
+ end
42
+
43
+ TIME_FIELDS.each do |field|
44
+ define_field(field) do
45
+ value = resource[field]
46
+
47
+ Time.parse(value) if value
48
+ end
49
+ end
50
+
51
+ def succ
52
+ index = current_index
53
+
54
+ all_active_with_start_date[index.succ] if index
55
+ end
56
+
57
+ def active?
58
+ state == 'active'
59
+ end
60
+
61
+ def closed?
62
+ state == 'closed'
63
+ end
64
+
65
+ def started?(today = Date.today)
66
+ start_date && start_date <= today
67
+ end
68
+
69
+ def expired?(today = Date.today)
70
+ due_date && due_date < today
71
+ end
72
+
73
+ def in_progress?(today = Date.today)
74
+ started?(today) && !expired?(today)
75
+ end
76
+
77
+ private
78
+
79
+ def current_index
80
+ all_active_with_start_date
81
+ .index { |milestone| milestone.id == id }
82
+ end
83
+
84
+ def all_active_with_start_date
85
+ @all_active_with_start_date ||=
86
+ all_active.select(&:start_date).sort_by(&:start_date)
87
+ end
88
+
89
+ def all_active
90
+ @all_active ||=
91
+ network
92
+ .query_api_cached(url(state: 'active'))
93
+ .map { |milestone| self.class.new(milestone, parent: self) }
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../label'
4
+ require_relative '../label_event'
5
+ require_relative '../milestone'
6
+
7
+ module Gitlab
8
+ module Triage
9
+ module Resource
10
+ module Shared
11
+ module Issuable
12
+ SourceTooDeep = Class.new(RuntimeError)
13
+ MAX_PARENT_LOOKUP = 10
14
+
15
+ def milestone
16
+ @milestone ||=
17
+ resource[:milestone] &&
18
+ Milestone.new(resource[:milestone], parent: self)
19
+ end
20
+
21
+ # This will be more useful when we have:
22
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/51011
23
+ def labels
24
+ @labels ||= resource[:labels] # an array of label names
25
+ .map { |label| Label.new({ name: label }, parent: self) }
26
+ end
27
+
28
+ # Make this an alias of `labels` when we have:
29
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/51011
30
+ def labels_with_details
31
+ # Labels can be deleted thus event.label can be nil
32
+ @labels_with_details ||= label_events
33
+ .select { |event| event.action == 'add' && event.label }
34
+ .map(&:label)
35
+ .sort_by(&:added_at)
36
+ .reverse
37
+ .uniq(&:name)
38
+ .select { |label| resource[:labels].include?(label.name) }
39
+ end
40
+
41
+ def label_events
42
+ @label_events ||= query_label_events
43
+ .map { |label_event| LabelEvent.new(label_event, parent: self) }
44
+ end
45
+
46
+ def labels_chronologically
47
+ @labels_chronologically ||= labels_with_details.sort_by(&:added_at)
48
+ end
49
+
50
+ def state
51
+ @state ||= resource.dig(:state)
52
+ end
53
+
54
+ def author
55
+ @author ||= resource.dig(:author, :username)
56
+ end
57
+
58
+ def project_path
59
+ @project_path ||=
60
+ request_project(resource[:project_id])[:path_with_namespace]
61
+ end
62
+
63
+ def full_resource_reference
64
+ @full_resource_reference ||= resource.dig(:references, :full)
65
+ end
66
+
67
+ def root_id(
68
+ resource: source_resource,
69
+ max_levels: MAX_PARENT_LOOKUP)
70
+ raise SourceTooDeep if max_levels <= 0
71
+
72
+ # In projects, the reference to the namespace's parent ID is `namespace.parent_id`
73
+ # but in groups, the reference is directly in `parent_id`
74
+ parent_id = resource.dig(:namespace, :parent_id) || resource.dig(:parent_id)
75
+
76
+ if parent_id
77
+ root_id(
78
+ resource: request_group(parent_id),
79
+ max_levels: max_levels - 1)
80
+ else
81
+ resource.dig(:namespace, :id) || resource[:id]
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def query_label_events
88
+ network.query_api_cached(
89
+ resource_url(sub_resource_type: 'resource_label_events'))
90
+ end
91
+
92
+ def request_project(project_id)
93
+ network.query_api_cached(project_url(project_id)).first
94
+ end
95
+
96
+ def request_group(group_id)
97
+ network.query_api_cached(group_url(group_id)).first
98
+ end
99
+
100
+ def group_url(group_id)
101
+ Gitlab::Triage::UrlBuilders::UrlBuilder.new(
102
+ network_options: network.options,
103
+ source: 'groups',
104
+ source_id: group_id
105
+ ).build
106
+ end
107
+
108
+ def project_url(project_id)
109
+ Gitlab::Triage::UrlBuilders::UrlBuilder.new(
110
+ network_options: network.options,
111
+ source: 'projects',
112
+ source_id: project_id
113
+ ).build
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end