ticket-replicator 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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Guardfile +158 -0
- data/LICENSE.txt +21 -0
- data/README.md +136 -0
- data/Rakefile +23 -0
- data/bin/ticket-replicator +67 -0
- data/config/examples/ticket-replicator.mappings.yml +54 -0
- data/cucumber.yml +7 -0
- data/features/extract-sap-solution-manager-defect-tickets.feature +45 -0
- data/features/load_tickets_in_jira.feature +129 -0
- data/features/setup_ticket_replicator.feature +85 -0
- data/features/step_definitions/anonymized_sample.xlsx +0 -0
- data/features/step_definitions/anonymized_sample.xlsx:Zone.Identifier +3 -0
- data/features/step_definitions/execution_context_steps.rb +13 -0
- data/features/step_definitions/extract_defect_tickets_from_sap_solution_manager_steps.rb.rb +29 -0
- data/features/step_definitions/load_tickets_in_jira_steps.rb +47 -0
- data/features/step_definitions/transform_solution_manager_tickets_steps.rb +21 -0
- data/features/support/10.setup_cucumber.rb +10 -0
- data/features/support/env.rb +15 -0
- data/features/support/hooks.rb +13 -0
- data/features/support/manage_mock_sap_solution_manager.rb.DISABLED +12 -0
- data/features/support/mocks/mock_defect_ticket_server.rb.DISABLED +251 -0
- data/features/support/setup_rspec.rb +15 -0
- data/features/support/setup_simplecov.rb +5 -0
- data/features/transform-solution-manager-tickets-into-jira-loadable-tickets.feature +313 -0
- data/features/transform_and_load_extracted_ticket_queue.feature +121 -0
- data/lib/tasks/version.rake +55 -0
- data/lib/ticket/replicator/defect_export_automation.rb.DISABLED +128 -0
- data/lib/ticket/replicator/file_loader.rb +46 -0
- data/lib/ticket/replicator/file_replicator.rb +67 -0
- data/lib/ticket/replicator/file_transformer/for_csv.rb +22 -0
- data/lib/ticket/replicator/file_transformer/for_xlsx.rb +34 -0
- data/lib/ticket/replicator/file_transformer.rb +70 -0
- data/lib/ticket/replicator/jira_project.rb +65 -0
- data/lib/ticket/replicator/replicated_summary.rb +73 -0
- data/lib/ticket/replicator/row_loader.rb +109 -0
- data/lib/ticket/replicator/row_transformer.rb +126 -0
- data/lib/ticket/replicator/s_a_p_solution_manager_client.rb.DISABLED +169 -0
- data/lib/ticket/replicator/setup.rb +49 -0
- data/lib/ticket/replicator/ticket.rb +70 -0
- data/lib/ticket/replicator/ticket_status_transitioner.rb +45 -0
- data/lib/ticket/replicator/version.rb +7 -0
- data/lib/ticket/replicator.rb +90 -0
- data/sig/ticket/replicator.rbs +6 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/ticket/replicator/file_loader_spec.rb +77 -0
- data/spec/ticket/replicator/file_replicator_spec.rb +153 -0
- data/spec/ticket/replicator/file_transformer/for_csv_spec.rb +52 -0
- data/spec/ticket/replicator/file_transformer/for_xlsx_spec.rb +52 -0
- data/spec/ticket/replicator/file_transformer_spec.rb +83 -0
- data/spec/ticket/replicator/jira_project_spec.rb +127 -0
- data/spec/ticket/replicator/replicated_summary_spec.rb +70 -0
- data/spec/ticket/replicator/row_loader_spec.rb +245 -0
- data/spec/ticket/replicator/row_transformer_spec.rb +234 -0
- data/spec/ticket/replicator/setup_spec.rb +80 -0
- data/spec/ticket/replicator/ticket_spec.rb +244 -0
- data/spec/ticket/replicator/ticket_status_transitioner_spec.rb +123 -0
- data/spec/ticket/replicator_spec.rb +137 -0
- data/transformed_file1 +1 -0
- metadata +235 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ticket/replicator/jira_project"
|
4
|
+
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
class RowLoader
|
8
|
+
def self.run_on(jira_project, row)
|
9
|
+
new(jira_project, row).run
|
10
|
+
end
|
11
|
+
|
12
|
+
private_class_method :new
|
13
|
+
|
14
|
+
attr_reader :jira_project, :row
|
15
|
+
|
16
|
+
def initialize(jira_project, row)
|
17
|
+
@jira_project = jira_project
|
18
|
+
@row = row
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
save_ticket
|
23
|
+
transition_ticket_to_the_expected_status
|
24
|
+
end
|
25
|
+
|
26
|
+
def ticket_previously_replicated?
|
27
|
+
jira_project.replicated_tickets.key?(id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def save_ticket
|
31
|
+
return unless ticket_fields_need_to_be_updated?
|
32
|
+
|
33
|
+
ticket.jira_ticket.save!({
|
34
|
+
fields: {
|
35
|
+
project: { key: jira_project.project_key },
|
36
|
+
issuetype: { name: jira_project.ticket_type_name },
|
37
|
+
# resolution: { name: resolution },
|
38
|
+
priority: { name: priority },
|
39
|
+
# implementation_team: { name: team },
|
40
|
+
summary: summary
|
41
|
+
}
|
42
|
+
})
|
43
|
+
|
44
|
+
ticket.jira_ticket.fetch
|
45
|
+
end
|
46
|
+
|
47
|
+
def ticket_fields_need_to_be_updated?
|
48
|
+
!ticket_previously_replicated? || ticket_fields_changed?
|
49
|
+
end
|
50
|
+
|
51
|
+
def ticket_fields_changed?
|
52
|
+
fields_to_save.any? { |field| ticket.send(field) != send(field) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def fields_to_save
|
56
|
+
%i[summary priority]
|
57
|
+
end
|
58
|
+
|
59
|
+
def transition_ticket_to_the_expected_status
|
60
|
+
ticket.transition_to(status)
|
61
|
+
end
|
62
|
+
|
63
|
+
def ticket
|
64
|
+
@ticket ||=
|
65
|
+
if ticket_previously_replicated?
|
66
|
+
fetch_ticket
|
67
|
+
else
|
68
|
+
create_ticket
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def fetch_ticket
|
73
|
+
jira_project.replicated_tickets.fetch(id)
|
74
|
+
end
|
75
|
+
|
76
|
+
def create_ticket
|
77
|
+
Replicator::Ticket.new(jira_project.jira_auto_tool, jira_project.jira_client.Issue.build)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def method_missing(name, *args)
|
83
|
+
if field_to_load?(name)
|
84
|
+
row.fetch(name) { |key| raise "No value found for #{key.inspect} in #{row.inspect}" }
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def respond_to_missing?(name, *args)
|
91
|
+
field_to_load?(name) || super
|
92
|
+
end
|
93
|
+
|
94
|
+
def field_to_load?(field)
|
95
|
+
fields_to_load.include?(field)
|
96
|
+
end
|
97
|
+
|
98
|
+
def fields_to_load
|
99
|
+
self.class.fields_to_load
|
100
|
+
end
|
101
|
+
|
102
|
+
class << self
|
103
|
+
def fields_to_load
|
104
|
+
@fields_to_load ||= RowTransformer.fields_to_transform.collect { |field| field.to_s.downcase.to_sym }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ticket/replicator/replicated_summary"
|
4
|
+
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
class RowTransformer
|
8
|
+
def self.run_on(extracted_row)
|
9
|
+
new(extracted_row).run
|
10
|
+
end
|
11
|
+
|
12
|
+
private_class_method :new
|
13
|
+
|
14
|
+
attr_reader :extracted_row
|
15
|
+
|
16
|
+
def initialize(extracted_row)
|
17
|
+
@extracted_row = extracted_row
|
18
|
+
end
|
19
|
+
|
20
|
+
def run
|
21
|
+
fields_to_transform.collect { |field| send("transformed_#{field.to_s.downcase}") }
|
22
|
+
end
|
23
|
+
|
24
|
+
def transformed_id
|
25
|
+
remapped_field_extracted_value_for :id
|
26
|
+
end
|
27
|
+
|
28
|
+
def transformed_status
|
29
|
+
mapped_value_for :status
|
30
|
+
end
|
31
|
+
|
32
|
+
def transformed_resolution
|
33
|
+
mapped_value_for :resolution, remapped_field_extracted_value_for(:status)
|
34
|
+
end
|
35
|
+
|
36
|
+
def transformed_priority
|
37
|
+
mapped_value_for :priority
|
38
|
+
end
|
39
|
+
|
40
|
+
def transformed_team
|
41
|
+
mapped_value_for :team
|
42
|
+
end
|
43
|
+
|
44
|
+
def transformed_summary
|
45
|
+
ReplicatedSummary.build(remapped_field_extracted_value_for(:id),
|
46
|
+
remapped_field_extracted_value_for(:summary)).to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def fields_to_transform
|
50
|
+
self.class.fields_to_transform
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.fields_to_transform
|
54
|
+
%i[ID Status Resolution Priority Team Summary]
|
55
|
+
end
|
56
|
+
|
57
|
+
def mapped_value_for(field, extracted_value = remapped_field_extracted_value_for(field))
|
58
|
+
mapping_for(field).fetch(extracted_value) do |key|
|
59
|
+
unless mapping_for(field)["defaults_to"] == "keep_original_value"
|
60
|
+
raise "No mapping found for #{field.inspect} = #{key.inspect} in #{mapping_for(field).inspect}"
|
61
|
+
end
|
62
|
+
|
63
|
+
extracted_value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def remapped_field_extracted_value_for(field)
|
68
|
+
remapped_field_extracted_row.fetch(remapped_field_key(field)) do |key|
|
69
|
+
raise "No value found for #{key.inspect} in #{remapped_field_extracted_row.inspect}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def mappings
|
74
|
+
@mappings ||= YAML.safe_load_file(mapping_file_path, symbolize_names: false)
|
75
|
+
end
|
76
|
+
|
77
|
+
def remapped_field_extracted_row
|
78
|
+
@remapped_field_extracted_row ||= fields_to_transform.to_h do |field|
|
79
|
+
mapped_field_key = remapped_field_key(field)
|
80
|
+
[mapped_field_key, extracted_row_value_for(mapped_field_key)]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def extracted_field_mapping_for(field)
|
85
|
+
original_field = extracted_field_mapping.fetch(remapped_field_key(field)) do |key|
|
86
|
+
raise "No mapping found for #{key.inspect} in #{extracted_field_mapping.inspect}"
|
87
|
+
end
|
88
|
+
|
89
|
+
remapped_field_key(original_field)
|
90
|
+
end
|
91
|
+
|
92
|
+
def remapped_field_key(field)
|
93
|
+
field.to_s.downcase
|
94
|
+
end
|
95
|
+
|
96
|
+
def extracted_field_mapping
|
97
|
+
@extracted_field_mapping ||= mapping_for :field
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.mapping_file_path
|
101
|
+
File.join("config", "ticket-replicator.mappings.yml")
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def extracted_row_value_for(mapped_field_key)
|
107
|
+
extracted_field_mapping = extracted_field_mapping_for(mapped_field_key)
|
108
|
+
|
109
|
+
extracted_row.fetch(extracted_field_mapping) do
|
110
|
+
raise "No value found for #{mapped_field_key.inspect} as " \
|
111
|
+
"#{extracted_field_mapping.inspect} in #{extracted_row.inspect}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def mapping_for(field)
|
116
|
+
mappings.fetch("#{field.to_s.downcase}_mapping") do |key|
|
117
|
+
raise "No mapping found for #{key.inspect} in #{mappings.inspect}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def mapping_file_path
|
122
|
+
self.class.mapping_file_path
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
class SAPSolutionManagerClient
|
8
|
+
HTTP_POST = Net::HTTP::Post::METHOD
|
9
|
+
HTTP_GET = Net::HTTP::Get::METHOD
|
10
|
+
HTTP_PUT = Net::HTTP::Put::METHOD
|
11
|
+
HTTP_DELETE = Net::HTTP::Delete::METHOD
|
12
|
+
CONTENT_TYPE_JSON = "application/json"
|
13
|
+
HTTP_OK = Net::HTTPSuccess::CODE
|
14
|
+
HTTP_CREATED = Net::HTTPCreated::CODE
|
15
|
+
HTTP_NO_CONTENT = Net::HTTPNoContent::CODE
|
16
|
+
API_AUTH_PATH = "/api/auth"
|
17
|
+
API_TICKETS_PATH = "/api/defect_tickets"
|
18
|
+
API_EXPORT_PATH = "/api/export/defect_tickets"
|
19
|
+
DEFAULT_TIMEOUT = 30
|
20
|
+
DEFAULT_FORMAT = "json"
|
21
|
+
DEFAULT_BASE_URL = "http://localhost:4567"
|
22
|
+
|
23
|
+
attr_reader :base_url, :auth_token
|
24
|
+
|
25
|
+
def initialize(base_url = DEFAULT_BASE_URL)
|
26
|
+
@base_url = base_url
|
27
|
+
@auth_token = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def authenticate(username, password)
|
31
|
+
uri = URI("#{@base_url}#{API_AUTH_PATH}")
|
32
|
+
|
33
|
+
request = Net::HTTP::Post.new(uri)
|
34
|
+
request.content_type = CONTENT_TYPE_JSON
|
35
|
+
request.body = { username: username, password: password }.to_json
|
36
|
+
|
37
|
+
response = send_request(uri, request)
|
38
|
+
|
39
|
+
if response.code == HTTP_OK
|
40
|
+
data = JSON.parse(response.body)
|
41
|
+
@auth_token = data["token"]
|
42
|
+
true
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_tickets(params = {})
|
49
|
+
query_params = params.map { |k, v| "#{k}=#{v}" }.join("&")
|
50
|
+
uri = URI("#{@base_url}/api/defect_tickets")
|
51
|
+
uri.query = query_params unless query_params.empty?
|
52
|
+
|
53
|
+
request = Net::HTTP::Get.new(uri)
|
54
|
+
add_auth_header(request)
|
55
|
+
|
56
|
+
response = send_request(uri, request)
|
57
|
+
|
58
|
+
if response.code == "200"
|
59
|
+
JSON.parse(response.body)
|
60
|
+
else
|
61
|
+
handle_error_response(response)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_ticket(id)
|
66
|
+
uri = URI("#{@base_url}/api/defect_tickets/#{id}")
|
67
|
+
|
68
|
+
request = Net::HTTP.const_get(HTTP_GET).new(uri)
|
69
|
+
add_auth_header(request)
|
70
|
+
|
71
|
+
response = send_request(uri, request)
|
72
|
+
|
73
|
+
if response.code == HTTP_OK
|
74
|
+
JSON.parse(response.body)
|
75
|
+
else
|
76
|
+
handle_error_response(response)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def create_ticket(attributes)
|
81
|
+
uri = URI("#{@base_url}#{API_TICKETS_PATH}")
|
82
|
+
|
83
|
+
request = Net::HTTP::Post.new(uri)
|
84
|
+
request.content_type = "application/json"
|
85
|
+
request.body = attributes.to_json
|
86
|
+
add_auth_header(request)
|
87
|
+
|
88
|
+
response = send_request(uri, request)
|
89
|
+
|
90
|
+
if response.code == HTTP_CREATED
|
91
|
+
JSON.parse(response.body)
|
92
|
+
else
|
93
|
+
handle_error_response(response)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_ticket(id, attributes)
|
98
|
+
uri = URI("#{@base_url}/api/defect_tickets/#{id}")
|
99
|
+
|
100
|
+
request = Net::HTTP.const_get(HTTP_PUT).new(uri)
|
101
|
+
request.content_type = CONTENT_TYPE_JSON
|
102
|
+
request.body = attributes.to_json
|
103
|
+
add_auth_header(request)
|
104
|
+
|
105
|
+
response = send_request(uri, request)
|
106
|
+
|
107
|
+
if response.code == HTTP_OK
|
108
|
+
JSON.parse(response.body)
|
109
|
+
else
|
110
|
+
handle_error_response(response)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def delete_ticket(id)
|
115
|
+
uri = URI("#{@base_url}/api/defect_tickets/#{id}")
|
116
|
+
|
117
|
+
request = Net::HTTP::Delete.new(uri)
|
118
|
+
add_auth_header(request)
|
119
|
+
|
120
|
+
response = send_request(uri, request)
|
121
|
+
|
122
|
+
response.code == HTTP_NO_CONTENT
|
123
|
+
end
|
124
|
+
|
125
|
+
def export_tickets(format = DEFAULT_FORMAT, params = {})
|
126
|
+
query_params = params.map { |k, v| "#{k}=#{v}" }.join("&")
|
127
|
+
query_params += "&format=#{format}"
|
128
|
+
|
129
|
+
uri = URI("#{@base_url}#{API_EXPORT_PATH}")
|
130
|
+
uri.query = query_params
|
131
|
+
|
132
|
+
request = Net::HTTP::Get.new(uri)
|
133
|
+
add_auth_header(request)
|
134
|
+
|
135
|
+
response = send_request(uri, request)
|
136
|
+
|
137
|
+
if response.code == HTTP_OK
|
138
|
+
if format == "json"
|
139
|
+
JSON.parse(response.body)
|
140
|
+
else
|
141
|
+
response.body
|
142
|
+
end
|
143
|
+
else
|
144
|
+
handle_error_response(response)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def add_auth_header(request)
|
151
|
+
request["Authorization"] = "Bearer #{@auth_token}" if @auth_token
|
152
|
+
end
|
153
|
+
|
154
|
+
def send_request(uri, request)
|
155
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
156
|
+
http.read_timeout = DEFAULT_TIMEOUT
|
157
|
+
http.open_timeout = DEFAULT_TIMEOUT
|
158
|
+
http.use_ssl = (uri.scheme == "https")
|
159
|
+
|
160
|
+
http.request(request)
|
161
|
+
end
|
162
|
+
|
163
|
+
def handle_error_response(response)
|
164
|
+
error_data = JSON.parse(response.body)
|
165
|
+
raise "API Error (#{response.code}): #{error_data["error"]}"
|
166
|
+
rescue JSON::ParserError
|
167
|
+
raise "API Error (#{response.code}): #{response.body}"
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ticket
|
4
|
+
class Replicator
|
5
|
+
class Setup
|
6
|
+
SetupError = Class.new(StandardError)
|
7
|
+
|
8
|
+
attr_reader :replicator
|
9
|
+
|
10
|
+
def initialize(replicator)
|
11
|
+
@replicator = replicator
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
create_queue
|
16
|
+
create_initial_configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_queue
|
22
|
+
log.info "Created folder #{replicator.extracted_folder.inspect}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_initial_configuration
|
26
|
+
if File.exist?(config_file_path)
|
27
|
+
raise SetupError, "Not overwriting existing config file #{config_file_path.inspect}"
|
28
|
+
end
|
29
|
+
|
30
|
+
FileUtils.cp(example_config_file_path, config_file_path)
|
31
|
+
|
32
|
+
log.info { "Created file #{config_file_path.inspect}" }
|
33
|
+
end
|
34
|
+
|
35
|
+
def config_file_path
|
36
|
+
@config_file_path ||=
|
37
|
+
begin
|
38
|
+
path = RowTransformer.mapping_file_path
|
39
|
+
FileUtils.mkdir_p(File.dirname(path))
|
40
|
+
path
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def example_config_file_path
|
45
|
+
File.join(replicator.home_dir, "config", "examples", File.basename(config_file_path))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ticket_status_transitioner"
|
4
|
+
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
class Ticket
|
8
|
+
attr_reader :jira_auto_tool, :jira_ticket
|
9
|
+
|
10
|
+
def initialize(jira_auto_tool, jira_ticket)
|
11
|
+
@jira_auto_tool = jira_auto_tool
|
12
|
+
@jira_ticket = jira_ticket
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete
|
16
|
+
jira_ticket.delete
|
17
|
+
end
|
18
|
+
|
19
|
+
def jira_client
|
20
|
+
jira_auto_tool.jira_client
|
21
|
+
end
|
22
|
+
|
23
|
+
def transition_to(status)
|
24
|
+
TicketStatusTransitioner.new(self).transition_to(status)
|
25
|
+
end
|
26
|
+
|
27
|
+
def <=>(other)
|
28
|
+
cmp_values(self) <=> cmp_values(other)
|
29
|
+
end
|
30
|
+
|
31
|
+
def replicated?
|
32
|
+
ReplicatedSummary.match?(summary)
|
33
|
+
end
|
34
|
+
|
35
|
+
def source_id
|
36
|
+
ReplicatedSummary.parse(summary).source_id
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def cmp_values(object)
|
42
|
+
object&.source_id
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_missing(name, *args)
|
46
|
+
if jira_ticket.respond_to?(name)
|
47
|
+
value = jira_ticket.send(name)
|
48
|
+
|
49
|
+
sanitize(value)
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def sanitize(value)
|
56
|
+
if value.respond_to?(:name)
|
57
|
+
value.name
|
58
|
+
elsif value.respond_to?(:value)
|
59
|
+
value.value
|
60
|
+
else
|
61
|
+
value.nil? ? "" : value.to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def respond_to_missing?(name, *args)
|
66
|
+
jira_ticket.respond_to?(name) || super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ticket
|
4
|
+
class Replicator
|
5
|
+
class TicketStatusTransitioner
|
6
|
+
attr_reader :ticket
|
7
|
+
|
8
|
+
def initialize(ticket)
|
9
|
+
@ticket = ticket
|
10
|
+
end
|
11
|
+
|
12
|
+
def tool
|
13
|
+
@tool ||= ticket.jira_auto_tool
|
14
|
+
end
|
15
|
+
|
16
|
+
def transition_to(status)
|
17
|
+
return if status == ticket.status
|
18
|
+
|
19
|
+
ticket.jira_client.post(ticket.jira_auto_tool.jira_url("/rest/api/2/issue/#{ticket.key}/transitions"),
|
20
|
+
transition_payload_for(status),
|
21
|
+
http_headers)
|
22
|
+
end
|
23
|
+
|
24
|
+
def transition_payload_for(status)
|
25
|
+
transition = available_transitions.find { |transition| transition.fetch("to").fetch("name") == status } or
|
26
|
+
raise "No transition found for #{status.inspect} in " \
|
27
|
+
"#{available_transitions.collect { |t| "#{t["name"]} -> #{t["to"]["name"]}" }.inspect}."
|
28
|
+
|
29
|
+
{ transition: { id: transition["id"] } }.to_json
|
30
|
+
end
|
31
|
+
|
32
|
+
def available_transitions
|
33
|
+
response = tool.jira_client.get(tool.jira_url("/rest/api/2/issue/#{ticket.key}?expand=transitions"))
|
34
|
+
|
35
|
+
JSON.parse(response.body).fetch("transitions")
|
36
|
+
end
|
37
|
+
|
38
|
+
def http_headers
|
39
|
+
{
|
40
|
+
"Content-Type" => "application/json"
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jira/auto/tool"
|
4
|
+
|
5
|
+
require_relative "replicator/file_replicator"
|
6
|
+
require_relative "replicator/file_loader"
|
7
|
+
require_relative "replicator/file_transformer"
|
8
|
+
require_relative "replicator/jira_project"
|
9
|
+
require_relative "replicator/setup"
|
10
|
+
require_relative "replicator/version"
|
11
|
+
|
12
|
+
module Ticket
|
13
|
+
class Replicator
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
def ticket_queue_transform_and_load
|
17
|
+
extracted_files.each { |file| FileReplicator.run_on(self, file) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def jira_client
|
21
|
+
jira_auto_tool.jira_client
|
22
|
+
end
|
23
|
+
|
24
|
+
def jira_auto_tool
|
25
|
+
@jira_auto_tool ||= Jira::Auto::Tool.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def jira_project
|
29
|
+
@jira_project = JiraProject.new(jira_auto_tool)
|
30
|
+
end
|
31
|
+
|
32
|
+
def transformed_queue_clear
|
33
|
+
FileUtils.rm_rf(transformed_folder)
|
34
|
+
end
|
35
|
+
|
36
|
+
def load
|
37
|
+
transformed_files.each { |file| FileReplicator.load(self, file) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def transform
|
41
|
+
extracted_files.each { |file| FileReplicator.transform(self, file) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def extracted_files
|
45
|
+
queue_files(extracted_folder)
|
46
|
+
end
|
47
|
+
|
48
|
+
def extracted_folder
|
49
|
+
queue_folder("10.extracted")
|
50
|
+
end
|
51
|
+
|
52
|
+
def transformed_files
|
53
|
+
queue_files(transformed_folder)
|
54
|
+
end
|
55
|
+
|
56
|
+
def transformed_folder
|
57
|
+
queue_folder("20.transformed")
|
58
|
+
end
|
59
|
+
|
60
|
+
def archived_folder
|
61
|
+
queue_folder("30.archived")
|
62
|
+
end
|
63
|
+
|
64
|
+
def queue_folder(*path_components)
|
65
|
+
path = File.join(queue_dir, *path_components)
|
66
|
+
|
67
|
+
FileUtils.mkdir_p(path)
|
68
|
+
|
69
|
+
path
|
70
|
+
end
|
71
|
+
|
72
|
+
def setup
|
73
|
+
Setup.new(self)
|
74
|
+
end
|
75
|
+
|
76
|
+
def home_dir
|
77
|
+
File.expand_path(File.join("..", ".."), __dir__)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def queue_dir
|
83
|
+
"queue"
|
84
|
+
end
|
85
|
+
|
86
|
+
def queue_files(queue_folder)
|
87
|
+
Dir.glob(File.join(queue_folder, "**", "*"))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ticket/replicator"
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
# Enable flags like --only-failures and --next-failure
|
7
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
8
|
+
|
9
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
10
|
+
config.disable_monkey_patching!
|
11
|
+
|
12
|
+
config.expect_with :rspec do |c|
|
13
|
+
c.syntax = :expect
|
14
|
+
end
|
15
|
+
|
16
|
+
config.mock_with :rspec do |mocks|
|
17
|
+
mocks.verify_partial_doubles = true
|
18
|
+
end
|
19
|
+
end
|