payping-gitlab-triage 0.1.1

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 (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