ticket-replicator 0.1.1 → 1.1.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -0
- data/README.md +7 -8
- data/config/examples/ticket-replicator.mappings.yml +1 -28
- data/cucumber.yml +1 -1
- data/features/load_tickets_in_jira.feature +65 -69
- data/features/setup_ticket_replicator.feature +1 -28
- data/features/step_definitions/load_tickets_in_jira_steps.rb +1 -1
- data/features/transform-solution-manager-tickets-into-jira-loadable-tickets.feature +51 -104
- data/features/transform_and_load_extracted_ticket_queue.feature +23 -46
- data/lib/ticket/replicator/file_loader.rb +1 -1
- data/lib/ticket/replicator/jira_project.rb +4 -0
- data/lib/ticket/replicator/row_loader.rb +66 -3
- data/lib/ticket/replicator/row_transformer.rb +29 -6
- data/lib/ticket/replicator/ticket.rb +19 -0
- data/lib/ticket/replicator/version.rb +1 -1
- data/spec/ticket/replicator/file_loader_spec.rb +1 -1
- data/spec/ticket/replicator/file_replicator_spec.rb +0 -2
- data/spec/ticket/replicator/file_transformer_spec.rb +2 -1
- data/spec/ticket/replicator/jira_project_spec.rb +19 -0
- data/spec/ticket/replicator/row_loader_spec.rb +208 -15
- data/spec/ticket/replicator/row_transformer_spec.rb +72 -5
- data/spec/ticket/replicator/ticket_spec.rb +72 -2
- metadata +1 -1
@@ -8,6 +8,10 @@ Feature: Transform and load extracted ticket queue
|
|
8
8
|
| name |
|
9
9
|
| TICKET_REPLICATOR_JIRA_PROJECT_KEY |
|
10
10
|
| TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME |
|
11
|
+
And the following environment variables have been set:
|
12
|
+
| name | value |
|
13
|
+
| TICKET_REPLICATOR_SOURCE_TICKET_URL | http://url/to/source/ticket/<%= source_ticket_id %> |
|
14
|
+
|
11
15
|
And a file named "config/ticket-replicator.mappings.yml" with:
|
12
16
|
"""
|
13
17
|
field_mapping:
|
@@ -28,38 +32,11 @@ Feature: Transform and load extracted ticket queue
|
|
28
32
|
defaults_to: keep_original_value
|
29
33
|
|
30
34
|
resolution_mapping:
|
31
|
-
defaults_to:
|
32
|
-
"
|
33
|
-
"Open":
|
34
|
-
"In Process":
|
35
|
-
"In Review":
|
36
|
-
"On Hold":
|
37
|
-
Deferred:
|
38
|
-
Defect Correction in Process:
|
39
|
-
"Fixed": "Fixed"
|
35
|
+
defaults_to: blank_value
|
36
|
+
"Fixed": "Done"
|
40
37
|
"Closed": "Done"
|
41
|
-
"
|
42
|
-
"Resolved": "
|
43
|
-
"Confirmed":
|
44
|
-
"Forwarded":
|
45
|
-
"Information Required":
|
46
|
-
Wait for Defect Correction:
|
47
|
-
Solution Proposal:
|
48
|
-
Tester Action:
|
49
|
-
Wait on External:
|
50
|
-
|
51
|
-
team_mapping:
|
52
|
-
defaults_to: keep_original_value
|
53
|
-
"Frontend": "Web Team"
|
54
|
-
"Backend": "Server Team"
|
55
|
-
"Integration": "Integration Team"
|
56
|
-
"Mobile": "Mobile Team"
|
57
|
-
"Security": "Security Team"
|
58
|
-
"DevOps": "DevOps Team"
|
59
|
-
"QA": "Quality Assurance"
|
60
|
-
"Architecture": "Architecture Team"
|
61
|
-
"UX": "Design Team"
|
62
|
-
"Performance": "Performance Team"
|
38
|
+
"Withdrawn": "Won't Do"
|
39
|
+
"Resolved": "Done"
|
63
40
|
"""
|
64
41
|
And the project has no tickets
|
65
42
|
|
@@ -74,7 +51,7 @@ Feature: Transform and load extracted ticket queue
|
|
74
51
|
| 9400013816 | Summary | 2: High | Forwarded | A Team |
|
75
52
|
| 9400011382 | Summary | 3: Medium | In Process | A Team |
|
76
53
|
| 9400011393 | Summary | 3: Medium | Information Required | A Team |
|
77
|
-
| 9400011381 | Summary | 3: Medium |
|
54
|
+
| 9400011381 | Summary | 3: Medium | Withdrawn | A Team |
|
78
55
|
| 3000016617 | Summary | 3: Medium | No Error | A Team |
|
79
56
|
| 9400011372 | Summary | 1: Critical | Open | A Team |
|
80
57
|
| 9400011466 | Summary | 3: Medium | Withdrawn | A Team |
|
@@ -84,20 +61,20 @@ Feature: Transform and load extracted ticket queue
|
|
84
61
|
And the current date time is "2025-05-10 07:52:00 UTC"
|
85
62
|
When I successfully run `ticket-replicator --ticket-queue-transform-and-load`
|
86
63
|
Then the Jira project should only have the following tickets:
|
87
|
-
| status | resolution | priority |
|
88
|
-
| Closed | Done | Medium |
|
89
|
-
| Confirmed | | Low |
|
90
|
-
| Defect Correction in Process | | Medium |
|
91
|
-
| Deferred | | Medium |
|
92
|
-
| Forwarded | | High |
|
93
|
-
| In Process | | Medium |
|
94
|
-
| Information Required | | Medium |
|
95
|
-
|
|
96
|
-
| No Error | | Medium |
|
97
|
-
| Open | | Highest |
|
98
|
-
| Withdrawn | Won't Do | Medium |
|
99
|
-
| Closed | Done | Medium |
|
100
|
-
| No Error | | Medium |
|
64
|
+
| status | resolution | priority | summary | source_ticket_url |
|
65
|
+
| Closed | Done | Medium | SMAN-3000017049 \| Summary | http://url/to/source/ticket/3000017049 |
|
66
|
+
| Confirmed | | Low | SMAN-9400011377 \| Summary | http://url/to/source/ticket/9400011377 |
|
67
|
+
| Defect Correction in Process | | Medium | SMAN-3000016618 \| Summary | http://url/to/source/ticket/3000016618 |
|
68
|
+
| Deferred | | Medium | SMAN-9400013805 \| Summary | http://url/to/source/ticket/9400013805 |
|
69
|
+
| Forwarded | | High | SMAN-9400013816 \| Summary | http://url/to/source/ticket/9400013816 |
|
70
|
+
| In Process | | Medium | SMAN-9400011382 \| Summary | http://url/to/source/ticket/9400011382 |
|
71
|
+
| Information Required | | Medium | SMAN-9400011393 \| Summary | http://url/to/source/ticket/9400011393 |
|
72
|
+
| Withdrawn | Won't Do | Medium | SMAN-9400011381 \| Summary | http://url/to/source/ticket/9400011381 |
|
73
|
+
| No Error | | Medium | SMAN-3000016617 \| Summary | http://url/to/source/ticket/3000016617 |
|
74
|
+
| Open | | Highest | SMAN-9400011372 \| Summary | http://url/to/source/ticket/9400011372 |
|
75
|
+
| Withdrawn | Won't Do | Medium | SMAN-9400011466 \| Summary | http://url/to/source/ticket/9400011466 |
|
76
|
+
| Closed | Done | Medium | SMAN-3000017667 \| Summary | http://url/to/source/ticket/3000017667 |
|
77
|
+
| No Error | | Medium | SMAN-3000018423 \| Summary | http://url/to/source/ticket/3000018423 |
|
101
78
|
# TODO: And the source ticket URL is found in the ticket descriptions of those tickets
|
102
79
|
And a file named "queue/30.archived/2025-05-10.07h52m00.sap_solution_manager_defects.xlsx" should exist
|
103
80
|
And the file named "queue/10.extracted/sap_solution_manager_defects.xlsx" should not exist anymore
|
@@ -8,7 +8,7 @@ module Ticket
|
|
8
8
|
class LoadError < StandardError; end
|
9
9
|
|
10
10
|
def self.run_on(jira_project, file_path)
|
11
|
-
log.
|
11
|
+
log.debug { "Loading #{file_path} into #{jira_project.project_key}..." }
|
12
12
|
|
13
13
|
new(jira_project, file_path).run
|
14
14
|
end
|
@@ -30,6 +30,10 @@ module Ticket
|
|
30
30
|
@ticket_type_name ||= ENV.fetch("TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME")
|
31
31
|
end
|
32
32
|
|
33
|
+
def resolutions
|
34
|
+
@resolutions ||= jira_client.Resolution.all.to_h { |resolution| [resolution.name, resolution] }
|
35
|
+
end
|
36
|
+
|
33
37
|
def replicated_tickets
|
34
38
|
@replicated_tickets ||= all_replicated_ticket_pages.to_h { |ticket| [ticket.source_id, ticket] }
|
35
39
|
end
|
@@ -20,6 +20,7 @@ module Ticket
|
|
20
20
|
|
21
21
|
def run
|
22
22
|
save_ticket
|
23
|
+
update_source_ticket_remote_link
|
23
24
|
transition_ticket_to_the_expected_status
|
24
25
|
end
|
25
26
|
|
@@ -27,6 +28,7 @@ module Ticket
|
|
27
28
|
jira_project.replicated_tickets.key?(id)
|
28
29
|
end
|
29
30
|
|
31
|
+
# rubocop:disable Metrics/MethodLength
|
30
32
|
def save_ticket
|
31
33
|
return unless ticket_fields_need_to_be_updated?
|
32
34
|
|
@@ -34,9 +36,8 @@ module Ticket
|
|
34
36
|
fields: {
|
35
37
|
project: { key: jira_project.project_key },
|
36
38
|
issuetype: { name: jira_project.ticket_type_name },
|
37
|
-
|
39
|
+
resolution: jira_resolution_value,
|
38
40
|
priority: { name: priority },
|
39
|
-
# implementation_team: { name: team },
|
40
41
|
summary: summary
|
41
42
|
}
|
42
43
|
})
|
@@ -44,6 +45,20 @@ module Ticket
|
|
44
45
|
ticket.jira_ticket.fetch
|
45
46
|
end
|
46
47
|
|
48
|
+
def jira_resolution_value
|
49
|
+
return if resolution.blank?
|
50
|
+
return { name: resolution } if jira_project.resolutions.key?(resolution)
|
51
|
+
|
52
|
+
log.warn do
|
53
|
+
"Setting resolution to unset since " \
|
54
|
+
"resolution #{resolution.inspect} not found in #{jira_project.resolutions.inspect}!"
|
55
|
+
end
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# rubocop:enable Metrics/MethodLength
|
61
|
+
|
47
62
|
def ticket_fields_need_to_be_updated?
|
48
63
|
!ticket_previously_replicated? || ticket_fields_changed?
|
49
64
|
end
|
@@ -56,6 +71,53 @@ module Ticket
|
|
56
71
|
%i[summary priority]
|
57
72
|
end
|
58
73
|
|
74
|
+
def update_source_ticket_remote_link
|
75
|
+
return unless source_ticket_link_needs_update?
|
76
|
+
|
77
|
+
ticket.source_ticket_link&.delete
|
78
|
+
|
79
|
+
ticket.jira_ticket.remotelink.build.save!(ticket_link_attributes)
|
80
|
+
end
|
81
|
+
|
82
|
+
# rubocop:disable Metrics/MethodLength
|
83
|
+
def source_ticket_link_needs_update?
|
84
|
+
existing_ticket_link_attributes = ticket.source_ticket_link&.attrs
|
85
|
+
|
86
|
+
object = existing_ticket_link_attributes&.[]("object")
|
87
|
+
existing_url = object&.[]("url")
|
88
|
+
existing_title = object&.[]("title")
|
89
|
+
|
90
|
+
filtered_existing_ticket_link_attributes = ticket_link_attributes(existing_url, existing_title)
|
91
|
+
|
92
|
+
log.debug do
|
93
|
+
<<~EOLOG
|
94
|
+
|
95
|
+
existing_ticket_link_attributes = #{existing_ticket_link_attributes.inspect}
|
96
|
+
filtered_existing_ticket_link_attributes = #{filtered_existing_ticket_link_attributes}
|
97
|
+
ticket_link_attributes: #{ticket_link_attributes.inspect}
|
98
|
+
EOLOG
|
99
|
+
end
|
100
|
+
|
101
|
+
filtered_existing_ticket_link_attributes != ticket_link_attributes
|
102
|
+
end
|
103
|
+
|
104
|
+
# rubocop:enable Metrics/MethodLength
|
105
|
+
|
106
|
+
def source_ticket_link_title
|
107
|
+
"Source Ticket #{id}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def ticket_link_attributes(url = source_ticket_url, title = source_ticket_link_title)
|
111
|
+
self.class.build_ticket_link_attributes(url, title)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.build_ticket_link_attributes(
|
115
|
+
url, title, application_name = Ticket::SOURCE_TICKET_REMOTE_LINK_APPLICATION
|
116
|
+
)
|
117
|
+
{ "object" => { "url" => url, "title" => title } }
|
118
|
+
.merge(application_name ? { "application" => { "name" => application_name } } : {})
|
119
|
+
end
|
120
|
+
|
59
121
|
def transition_ticket_to_the_expected_status
|
60
122
|
ticket.transition_to(status)
|
61
123
|
end
|
@@ -101,7 +163,8 @@ module Ticket
|
|
101
163
|
|
102
164
|
class << self
|
103
165
|
def fields_to_load
|
104
|
-
@fields_to_load ||=
|
166
|
+
@fields_to_load ||=
|
167
|
+
RowTransformer.fields_to_transform.collect { |field| field.to_s.parameterize.underscore.to_sym }
|
105
168
|
end
|
106
169
|
end
|
107
170
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "ticket/replicator/replicated_summary"
|
4
|
+
require "erb"
|
4
5
|
|
5
6
|
module Ticket
|
6
7
|
class Replicator
|
@@ -18,13 +19,25 @@ module Ticket
|
|
18
19
|
end
|
19
20
|
|
20
21
|
def run
|
21
|
-
fields_to_transform.collect { |field| send("transformed_#{field.to_s.
|
22
|
+
fields_to_transform.collect { |field| send("transformed_#{field.to_s.parameterize.underscore}") }
|
22
23
|
end
|
23
24
|
|
24
25
|
def transformed_id
|
25
26
|
remapped_field_extracted_value_for :id
|
26
27
|
end
|
27
28
|
|
29
|
+
def transformed_source_ticket_url
|
30
|
+
source_ticket_url_builder(transformed_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def source_ticket_url_builder(source_ticket_id)
|
34
|
+
ERB.new(ticket_replicator_source_ticket_url).result(binding)
|
35
|
+
end
|
36
|
+
|
37
|
+
def ticket_replicator_source_ticket_url
|
38
|
+
ENV.fetch("TICKET_REPLICATOR_SOURCE_TICKET_URL") { |name| raise "#{name}: not set in environment!" }
|
39
|
+
end
|
40
|
+
|
28
41
|
def transformed_status
|
29
42
|
mapped_value_for :status
|
30
43
|
end
|
@@ -50,17 +63,27 @@ module Ticket
|
|
50
63
|
self.class.fields_to_transform
|
51
64
|
end
|
52
65
|
|
66
|
+
def fields_to_remap
|
67
|
+
self.class.fields_to_remap
|
68
|
+
end
|
69
|
+
|
53
70
|
def self.fields_to_transform
|
54
|
-
|
71
|
+
fields_to_remap + ["Source Ticket URL"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.fields_to_remap
|
75
|
+
%i[ID Status Resolution Priority Summary]
|
55
76
|
end
|
56
77
|
|
57
78
|
def mapped_value_for(field, extracted_value = remapped_field_extracted_value_for(field))
|
58
79
|
mapping_for(field).fetch(extracted_value) do |key|
|
59
|
-
|
80
|
+
case mapping_for(field)["defaults_to"]
|
81
|
+
when "keep_original_value" then extracted_value
|
82
|
+
when "unset_value" then nil
|
83
|
+
when "blank_value" then ""
|
84
|
+
else
|
60
85
|
raise "No mapping found for #{field.inspect} = #{key.inspect} in #{mapping_for(field).inspect}"
|
61
86
|
end
|
62
|
-
|
63
|
-
extracted_value
|
64
87
|
end
|
65
88
|
end
|
66
89
|
|
@@ -75,7 +98,7 @@ module Ticket
|
|
75
98
|
end
|
76
99
|
|
77
100
|
def remapped_field_extracted_row
|
78
|
-
@remapped_field_extracted_row ||=
|
101
|
+
@remapped_field_extracted_row ||= fields_to_remap.to_h do |field|
|
79
102
|
mapped_field_key = remapped_field_key(field)
|
80
103
|
[mapped_field_key, extracted_row_value_for(mapped_field_key)]
|
81
104
|
end
|
@@ -36,8 +36,25 @@ module Ticket
|
|
36
36
|
ReplicatedSummary.parse(summary).source_id
|
37
37
|
end
|
38
38
|
|
39
|
+
SOURCE_TICKET_REMOTE_LINK_APPLICATION = "Ticket Source"
|
40
|
+
|
41
|
+
def source_ticket_link
|
42
|
+
@source_ticket_link ||=
|
43
|
+
jira_ticket.remotelink.all.find do |link|
|
44
|
+
source_ticket_link_attributes(link)&.[]("application")&.[]("name") == SOURCE_TICKET_REMOTE_LINK_APPLICATION
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def source_ticket_url
|
49
|
+
source_ticket_link_attributes(source_ticket_link)&.[]("object")&.[]("url")
|
50
|
+
end
|
51
|
+
|
39
52
|
private
|
40
53
|
|
54
|
+
def source_ticket_link_attributes(link)
|
55
|
+
link&.attrs
|
56
|
+
end
|
57
|
+
|
41
58
|
def cmp_values(object)
|
42
59
|
object&.source_id
|
43
60
|
end
|
@@ -57,6 +74,8 @@ module Ticket
|
|
57
74
|
value.name
|
58
75
|
elsif value.respond_to?(:value)
|
59
76
|
value.value
|
77
|
+
elsif value.is_a?(Hash)
|
78
|
+
value.values_at("name", "value").compact.first
|
60
79
|
else
|
61
80
|
value.nil? ? "" : value.to_s
|
62
81
|
end
|
@@ -8,7 +8,7 @@ module Ticket
|
|
8
8
|
RSpec.describe FileLoader do
|
9
9
|
let(:loader) { described_class.send(:new, jira_project, "file.csv") }
|
10
10
|
let(:jira_project) { instance_double(JiraProject) }
|
11
|
-
let(:logger) { instance_double(Logger,
|
11
|
+
let(:logger) { instance_double(Logger, debug: nil) }
|
12
12
|
|
13
13
|
describe ".run_on" do
|
14
14
|
it "loads the given file" do
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
module Ticket
|
4
4
|
class Replicator
|
5
|
-
# rubocop:disable Metrics/ClassLength
|
6
5
|
class FileReplicator
|
7
6
|
RSpec.describe FileReplicator do
|
8
7
|
let(:file_replicator) { described_class.send(:new, replicator, "extracted.csv") }
|
@@ -148,6 +147,5 @@ module Ticket
|
|
148
147
|
end
|
149
148
|
end
|
150
149
|
end
|
151
|
-
# rubocop:enable Metrics/ClassLength
|
152
150
|
end
|
153
151
|
end
|
@@ -43,7 +43,8 @@ module Ticket
|
|
43
43
|
|
44
44
|
describe "#transformed_headers" do
|
45
45
|
it "returns the headers" do
|
46
|
-
expect(transformer.transformed_headers)
|
46
|
+
expect(transformer.transformed_headers)
|
47
|
+
.to eq(%w[ID Status Resolution Priority Summary] + ["Source Ticket URL"])
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
@@ -28,6 +28,24 @@ module Ticket
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
describe "#resolutions" do
|
32
|
+
def build_resolution(name, id)
|
33
|
+
double(JIRA::Resource::Resolution, name: name, id: id)
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:a_resolution) { build_resolution("a resolution", 1) }
|
37
|
+
let(:another_resolution) { build_resolution("another resolution", 2) }
|
38
|
+
|
39
|
+
let(:resolution_query) { double("jira_client.Resolution", all: [a_resolution, another_resolution]) }
|
40
|
+
|
41
|
+
before { allow(jira_client).to receive_messages(Resolution: resolution_query) }
|
42
|
+
|
43
|
+
it do
|
44
|
+
expect(project.resolutions)
|
45
|
+
.to eq({ "a resolution" => a_resolution, "another resolution" => another_resolution })
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
31
49
|
describe "#ticket_type_name" do
|
32
50
|
it do
|
33
51
|
expect(ENV).to receive(:fetch).with("TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME").and_return("Ticket Type Name")
|
@@ -83,6 +101,7 @@ module Ticket
|
|
83
101
|
build_expected_ticket("5", "SMAN-5 | a summary")
|
84
102
|
]
|
85
103
|
end
|
104
|
+
|
86
105
|
let(:actual_tickets) do
|
87
106
|
[
|
88
107
|
build_non_replicated_ticket("a non replicated ticket"),
|