ticket-replicator 1.2.2 → 1.2.3
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/README.md +2 -0
- data/features/load_tickets_in_jira.feature +32 -7
- data/features/transform_and_load_extracted_ticket_queue.feature +1 -1
- data/lib/ticket/replicator/file_loader.rb +1 -1
- data/lib/ticket/replicator/file_transformer.rb +7 -1
- data/lib/ticket/replicator/mappings.rb +15 -0
- data/lib/ticket/replicator/row_loader.rb +21 -0
- data/lib/ticket/replicator/row_transformer.rb +6 -12
- data/lib/ticket/replicator/setup.rb +1 -1
- data/lib/ticket/replicator/ticket.rb +13 -1
- data/lib/ticket/replicator/ticket_status_transitioner.rb +18 -2
- data/lib/ticket/replicator/version.rb +1 -1
- data/spec/ticket/replicator/file_transformer_spec.rb +8 -8
- data/spec/ticket/replicator/mappings_spec.rb +61 -0
- data/spec/ticket/replicator/row_loader_spec.rb +170 -37
- data/spec/ticket/replicator/row_transformer_spec.rb +9 -45
- data/spec/ticket/replicator/ticket_spec.rb +36 -1
- data/spec/ticket/replicator/ticket_status_transitioner_spec.rb +49 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f526835796d74716dc38bff10d6be52892b547ad78c089b8a7e77e5fc080ffde
|
4
|
+
data.tar.gz: 0f004dbeda462964f94aa14f8ddaf51edca62dc1ac73d3f1fb89fab209e3a5d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34c1f9ad5dd0f245d72f483c507515c44055f2489039320667ad5430aeea3a58585b2be0e2c4075eebccc98ee2924495bf07afa1f78c94463d27c14566eba58f
|
7
|
+
data.tar.gz: dcd4d81a0ce4361f906e4fe921421035987db61693ed224fbbcf14bd2d3d6e8f3eaa534d1b90c26f2f8d1958af34050ddfd4dffb947021f1c51652c73e8ac705
|
data/README.md
CHANGED
@@ -52,6 +52,8 @@ Optional environment variables:
|
|
52
52
|
- `JAT_RATE_INTERVAL_IN_SECONDS` - Interval for rate limiting in seconds (e.g., "1").
|
53
53
|
- `JAT_RATE_LIMIT_PER_INTERVAL` - Rate limit per interval for Jira API calls (e.g., "1")
|
54
54
|
- `TICKET_REPLICATOR_DISABLE_RESOLUTION_LOADING` - To deal with tickets not having a resolution field.
|
55
|
+
- `TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS` - Limit the amount of requests
|
56
|
+
to modified tickets only (default to "true" when unset). Set to "false" to disable this feature.
|
55
57
|
|
56
58
|
### Setup Queue Folder And Field Mapping
|
57
59
|
|
@@ -112,12 +112,37 @@ Feature: Load tickets into Jira
|
|
112
112
|
"""
|
113
113
|
And I successfully run `ticket-replicator --jira-http-debug --load`
|
114
114
|
Then the Jira project should only have the following tickets:
|
115
|
-
| example purpose
|
116
|
-
| one update due to status change
|
117
|
-
|
|
118
|
-
| three updates due to new ticket created (fields, source ticket link and status)
|
119
|
-
| no update since no status or field change
|
120
|
-
And only
|
115
|
+
| example purpose | status | resolution | priority | summary | source_ticket_url |
|
116
|
+
| one update due to status change | Closed | | Highest | SMAN-10001 \| Login page randomly fails to load CSS assets (10001) | http://url/to/source/ticket/10001 |
|
117
|
+
| two updates due to field updated (priority, resolution and summary) and updating the remote link | In Process | | High | SMAN-10002 \| *Recurring* database deadlock during order processing (10002) | http://url/to/source/ticket/10002 |
|
118
|
+
| three updates due to new ticket created (fields, source ticket link and status) | Open | | Highest | SMAN-10008 \| Memory leak in caching implementation (10008) | http://url/to/source/ticket/10008 |
|
119
|
+
| no update since no status or field change | Confirmed | Done | High | SMAN-10016 \| Slow response time on product search API (10016) | http://url/to/source/ticket/10016 |
|
120
|
+
And only 6 Jira update requests were emitted
|
121
|
+
|
122
|
+
Scenario: Updating fields and statuses of closed and withdrawn tickets is possible
|
123
|
+
Given a file named "queue/20.transformed/10.initial_replication.csv" with:
|
124
|
+
"""
|
125
|
+
"ID","Status","Resolution","Priority","Summary","Source Ticket URL"
|
126
|
+
"10001","Closed","","Highest","SMAN-10001 | Possible to update summary and resolution of closed ticket (100001)","http://url/to/source/ticket/10001"
|
127
|
+
"10002","Closed","","Low","SMAN-10002 | Reopening ticket while updating priority (10002)","http://url/to/source/ticket/10002"
|
128
|
+
"10003","Withdrawn","Won't Do","Medium","SMAN-10003 | Possible to update a resolved ticket (10003)","http://url/to/source/ticket/10003"
|
129
|
+
"10004","Withdrawn","Done","Low","SMAN-10004 | Re-opening resolved ticket (10004)","http://url/to/source/ticket/10004"
|
130
|
+
"""
|
131
|
+
And a file named "queue/20.transformed/20.update_replication.csv" with:
|
132
|
+
"""
|
133
|
+
"ID","Status","Resolution","Priority","Summary","Source Ticket URL"
|
134
|
+
"10001","Closed","Done","Highest","SMAN-10001 | Successfully updated summary and resolution of the closed ticket (100001)","http://url/to/source/ticket/10001"
|
135
|
+
"10002","Open","","Highest","SMAN-10002 | Successfully reopened ticket while its updating priority and summary (10002)","http://url/to/source/ticket/10002"
|
136
|
+
"10003","Withdrawn","Done","High","SMAN-10003 | Successfully updated resolved ticket (10003)","http://url/to/source/ticket/10003"
|
137
|
+
"10004","Open","","Low","SMAN-10004 | Successfully re-opened and updated the ticket (10004)","http://url/to/source/ticket/10004"
|
138
|
+
"""
|
139
|
+
When I successfully run `ticket-replicator --load`
|
140
|
+
Then the Jira project should only have the following tickets:
|
141
|
+
| status | resolution | priority | summary | source_ticket_url |
|
142
|
+
| Closed | Done | Highest | SMAN-10001 \| Successfully updated summary and resolution of the closed ticket (100001) | http://url/to/source/ticket/10001 |
|
143
|
+
| Open | | Highest | SMAN-10002 \| Successfully reopened ticket while its updating priority and summary (10002) | http://url/to/source/ticket/10002 |
|
144
|
+
| Withdrawn | Done | High | SMAN-10003 \| Successfully updated resolved ticket (10003) | http://url/to/source/ticket/10003 |
|
145
|
+
| Open | | Low | SMAN-10004 \| Successfully re-opened and updated the ticket (10004) | http://url/to/source/ticket/10004 |
|
121
146
|
|
122
147
|
Scenario: Attempting to set an unexpected status generates information about the current row being loaded
|
123
148
|
Given a file named "queue/20.transformed/sap_solution_manager_defects.csv" with:
|
@@ -137,7 +162,7 @@ Feature: Load tickets into Jira
|
|
137
162
|
Then it should fail with:
|
138
163
|
"""
|
139
164
|
ERROR Object : Ticket::Replicator::FileLoader::LoadError: queue/20.transformed/sap_solution_manager_defects.csv:5: error while loading row:
|
140
|
-
No transition found for "____ INEXISTING STATUS ____" in ["No Error
|
165
|
+
No transition found for "____ INEXISTING STATUS ____" in ["No Error => No Error", "Tester Action => Tester Action", "Close Bug => Closed", "Confirmed => Confirmed", "Defect Correction in Process => Defect Correction in Process", "Deferred => Deferred", "Forwarded => Forwarded", "Information Required => Information Required", "New => New", "Open => Open", "Solution Proposal => Solution Proposal", "Wait for Defect Correction => Wait for Defect Correction", "Wait on External => Wait on External", "Withdrawn => Withdrawn", "In Process => In Process"].:
|
141
166
|
#<CSV::Row id:"10004" status:"____ INEXISTING STATUS ____" resolution:"Done" priority:"Low" summary:"SMAN-10004 | App crashes when offline on Android 12 (10004)" source_ticket_url:"http://url/to/source/ticket/10004">
|
142
167
|
"""
|
143
168
|
|
@@ -9,7 +9,7 @@ Feature: Transform and load extracted ticket queue
|
|
9
9
|
| TICKET_REPLICATOR_JIRA_PROJECT_KEY |
|
10
10
|
| TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME |
|
11
11
|
And the following environment variables have been set:
|
12
|
-
| name | value
|
12
|
+
| name | value |
|
13
13
|
| TICKET_REPLICATOR_SOURCE_TICKET_URL | http://url/to/source/ticket/<%= source_ticket_id %> |
|
14
14
|
|
15
15
|
And a file named "config/ticket-replicator.mappings.yml" with:
|
@@ -40,17 +40,23 @@ module Ticket
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
private
|
44
|
+
|
43
45
|
def extracted_rows
|
44
46
|
raise NotImplementedError, "You must implement this method in your subclass"
|
45
47
|
end
|
46
48
|
|
49
|
+
def mappings
|
50
|
+
@mappings ||= Mappings.new
|
51
|
+
end
|
52
|
+
|
47
53
|
FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER = 2
|
48
54
|
private_constant :FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER
|
49
55
|
|
50
56
|
def transformed_rows
|
51
57
|
row_index = FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER
|
52
58
|
extracted_rows.collect do |row|
|
53
|
-
RowTransformer.run_on(row)
|
59
|
+
RowTransformer.run_on(row, mappings)
|
54
60
|
rescue StandardError => e
|
55
61
|
raise TransformError, <<~EOERRORMSG
|
56
62
|
#{extracted_path}:#{row_index}: error while transforming row:
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ticket
|
4
|
+
class Replicator
|
5
|
+
class Mappings
|
6
|
+
def self.file_path
|
7
|
+
File.join("config", "ticket-replicator.mappings.yml")
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_h
|
11
|
+
@to_h = YAML.safe_load_file(self.class.file_path, symbolize_names: false)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -38,10 +38,14 @@ module Ticket
|
|
38
38
|
def save_ticket
|
39
39
|
return unless ticket_fields_need_to_be_updated?
|
40
40
|
|
41
|
+
reopen_ticket_if_status_in_done_category
|
42
|
+
|
41
43
|
ticket.jira_ticket.save!(attributes_for_save)
|
42
44
|
|
43
45
|
ticket.jira_ticket.fetch
|
44
46
|
|
47
|
+
@ticket_was_just_updated = true
|
48
|
+
|
45
49
|
jira_project.replicated_tickets[id] = ticket
|
46
50
|
end
|
47
51
|
|
@@ -78,6 +82,12 @@ module Ticket
|
|
78
82
|
nil
|
79
83
|
end
|
80
84
|
|
85
|
+
def reopen_ticket_if_status_in_done_category
|
86
|
+
return unless ticket_previously_replicated? && ticket.status_in_done_category?
|
87
|
+
|
88
|
+
ticket.transition_to_not_done
|
89
|
+
end
|
90
|
+
|
81
91
|
def ticket_fields_need_to_be_updated?
|
82
92
|
!ticket_previously_replicated? || ticket_fields_changed?
|
83
93
|
end
|
@@ -100,6 +110,8 @@ module Ticket
|
|
100
110
|
|
101
111
|
# rubocop:disable Metrics/MethodLength
|
102
112
|
def source_ticket_link_needs_update?
|
113
|
+
return ticket_was_just_updated? if only_update_links_for_modified_ticket?
|
114
|
+
|
103
115
|
existing_ticket_link_attributes = ticket.source_ticket_link&.attrs
|
104
116
|
|
105
117
|
object = existing_ticket_link_attributes&.[]("object")
|
@@ -120,6 +132,10 @@ module Ticket
|
|
120
132
|
filtered_existing_ticket_link_attributes != ticket_link_attributes
|
121
133
|
end
|
122
134
|
|
135
|
+
def only_update_links_for_modified_ticket?
|
136
|
+
ENV.fetch("TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS", "true") != "false"
|
137
|
+
end
|
138
|
+
|
123
139
|
# rubocop:enable Metrics/MethodLength
|
124
140
|
|
125
141
|
def source_ticket_link_title
|
@@ -139,10 +155,15 @@ module Ticket
|
|
139
155
|
if ticket_previously_replicated?
|
140
156
|
fetch_ticket
|
141
157
|
else
|
158
|
+
@ticket_was_just_updated = true
|
142
159
|
create_ticket
|
143
160
|
end
|
144
161
|
end
|
145
162
|
|
163
|
+
def ticket_was_just_updated?
|
164
|
+
@ticket_was_just_updated
|
165
|
+
end
|
166
|
+
|
146
167
|
def fetch_ticket
|
147
168
|
jira_project.replicated_tickets.fetch(id)
|
148
169
|
end
|
@@ -2,21 +2,23 @@
|
|
2
2
|
|
3
3
|
require "ticket/replicator/replicated_summary"
|
4
4
|
require "ticket/replicator/file_transformer"
|
5
|
+
require "ticket/replicator/mappings"
|
5
6
|
require "erb"
|
6
7
|
|
7
8
|
module Ticket
|
8
9
|
class Replicator
|
9
10
|
class RowTransformer
|
10
|
-
def self.run_on(extracted_row)
|
11
|
-
new(extracted_row).run
|
11
|
+
def self.run_on(extracted_row, mappings)
|
12
|
+
new(extracted_row, mappings).run
|
12
13
|
end
|
13
14
|
|
14
15
|
private_class_method :new
|
15
16
|
|
16
|
-
attr_reader :extracted_row
|
17
|
+
attr_reader :extracted_row, :mappings
|
17
18
|
|
18
|
-
def initialize(extracted_row)
|
19
|
+
def initialize(extracted_row, mappings)
|
19
20
|
@extracted_row = extracted_row
|
21
|
+
@mappings = mappings.to_h
|
20
22
|
end
|
21
23
|
|
22
24
|
def run
|
@@ -115,10 +117,6 @@ module Ticket
|
|
115
117
|
end
|
116
118
|
end
|
117
119
|
|
118
|
-
def mappings
|
119
|
-
@mappings ||= YAML.safe_load_file(mapping_file_path, symbolize_names: false)
|
120
|
-
end
|
121
|
-
|
122
120
|
def remapped_field_extracted_row
|
123
121
|
@remapped_field_extracted_row ||= fields_to_remap.to_h do |field|
|
124
122
|
mapped_field_key = remapped_field_key(field)
|
@@ -142,10 +140,6 @@ module Ticket
|
|
142
140
|
@extracted_field_mapping ||= mapping_for :field
|
143
141
|
end
|
144
142
|
|
145
|
-
def self.mapping_file_path
|
146
|
-
File.join("config", "ticket-replicator.mappings.yml")
|
147
|
-
end
|
148
|
-
|
149
143
|
private
|
150
144
|
|
151
145
|
def extracted_row_value_for(mapped_field_key)
|
@@ -21,7 +21,7 @@ module Ticket
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def transition_to(status)
|
24
|
-
|
24
|
+
status_transitioner.transition_to(status)
|
25
25
|
end
|
26
26
|
|
27
27
|
def <=>(other)
|
@@ -49,8 +49,20 @@ module Ticket
|
|
49
49
|
source_ticket_link_attributes(source_ticket_link)&.[]("object")&.[]("url")
|
50
50
|
end
|
51
51
|
|
52
|
+
def status_in_done_category?
|
53
|
+
jira_ticket.status.attrs.fetch("statusCategory").fetch("key") == "done"
|
54
|
+
end
|
55
|
+
|
56
|
+
def transition_to_not_done
|
57
|
+
status_transitioner.transition_to_not_done
|
58
|
+
end
|
59
|
+
|
52
60
|
private
|
53
61
|
|
62
|
+
def status_transitioner
|
63
|
+
TicketStatusTransitioner.new(self)
|
64
|
+
end
|
65
|
+
|
54
66
|
def source_ticket_link_attributes(link)
|
55
67
|
link&.attrs
|
56
68
|
end
|
@@ -2,7 +2,10 @@
|
|
2
2
|
|
3
3
|
module Ticket
|
4
4
|
class Replicator
|
5
|
+
# Assumption: The ticket workflow allows for transitioning to any other status.
|
5
6
|
class TicketStatusTransitioner
|
7
|
+
StatusTransitionerError = Class.new(StandardError)
|
8
|
+
|
6
9
|
attr_reader :ticket
|
7
10
|
|
8
11
|
def initialize(ticket)
|
@@ -21,14 +24,27 @@ module Ticket
|
|
21
24
|
http_headers)
|
22
25
|
end
|
23
26
|
|
27
|
+
def transition_to_not_done
|
28
|
+
not_done_status =
|
29
|
+
available_transitions.find do |transition|
|
30
|
+
transition.fetch("to").fetch("statusCategory").fetch("key") != "done"
|
31
|
+
end or
|
32
|
+
raise StatusTransitionerError, build_error_message("No non-done transition found")
|
33
|
+
|
34
|
+
transition_to(not_done_status.fetch("to").fetch("name"))
|
35
|
+
end
|
36
|
+
|
24
37
|
def transition_payload_for(status)
|
25
38
|
transition = available_transitions.find { |transition| transition.fetch("to").fetch("name") == status } or
|
26
|
-
raise "No transition found for #{status.inspect}
|
27
|
-
"#{available_transitions.collect { |t| "#{t["name"]} -> #{t["to"]["name"]}" }.inspect}."
|
39
|
+
raise StatusTransitionerError, build_error_message("No transition found for #{status.inspect}")
|
28
40
|
|
29
41
|
{ transition: { id: transition["id"] } }.to_json
|
30
42
|
end
|
31
43
|
|
44
|
+
def build_error_message(message)
|
45
|
+
message + " in #{available_transitions.collect { |t| "#{t["name"]} => #{t["to"]["name"]}" }.inspect}."
|
46
|
+
end
|
47
|
+
|
32
48
|
def available_transitions
|
33
49
|
response = tool.jira_client.get(tool.jira_url("/rest/api/2/issue/#{ticket.key}?expand=transitions"))
|
34
50
|
|
@@ -43,34 +43,34 @@ 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.send(:transformed_headers))
|
47
47
|
.to eq(%w[ID Status Resolution Priority Summary] + ["Source Ticket URL"])
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
describe "#transformed_rows" do
|
52
52
|
before do
|
53
|
-
allow(transformer).to receive_messages(extracted_rows: %i[a_row another_row])
|
53
|
+
allow(transformer).to receive_messages(extracted_rows: %i[a_row another_row], mappings: :mappings)
|
54
54
|
end
|
55
55
|
|
56
56
|
it "returns the rows" do
|
57
|
-
allow(RowTransformer).to receive(:run_on).with(:a_row).and_return(:a_tranformed_row)
|
58
|
-
allow(RowTransformer).to receive(:run_on).with(:another_row).and_return(:another_transformed_row)
|
57
|
+
allow(RowTransformer).to receive(:run_on).with(:a_row, :mappings).and_return(:a_tranformed_row)
|
58
|
+
allow(RowTransformer).to receive(:run_on).with(:another_row, :mappings).and_return(:another_transformed_row)
|
59
59
|
|
60
|
-
expect(transformer.transformed_rows).to eq(%i[a_tranformed_row another_transformed_row])
|
60
|
+
expect(transformer.send(:transformed_rows)).to eq(%i[a_tranformed_row another_transformed_row])
|
61
61
|
end
|
62
62
|
|
63
63
|
context "when a row transformation error occurs" do
|
64
64
|
before do
|
65
|
-
allow(RowTransformer).to receive(:run_on).with(:a_row).and_return(:a_tranformed_row)
|
65
|
+
allow(RowTransformer).to receive(:run_on).with(:a_row, :mappings).and_return(:a_tranformed_row)
|
66
66
|
|
67
67
|
allow(RowTransformer)
|
68
|
-
.to receive(:run_on).with(:another_row)
|
68
|
+
.to receive(:run_on).with(:another_row, :mappings)
|
69
69
|
.and_raise(TransformError, "transformation error")
|
70
70
|
end
|
71
71
|
|
72
72
|
it "raises an error message including the file name and line number of the row that was processed" do
|
73
|
-
expect { transformer.transformed_rows }
|
73
|
+
expect { transformer.send(:transformed_rows) }
|
74
74
|
.to raise_error(TransformError,
|
75
75
|
/source\.csv:3: error while transforming row:\ntransformation error:\n:another_row\n/)
|
76
76
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ticket
|
4
|
+
class Replicator
|
5
|
+
class Mappings
|
6
|
+
RSpec.describe Mappings do
|
7
|
+
let(:mappings) { described_class.new }
|
8
|
+
|
9
|
+
describe ".file_path" do
|
10
|
+
it { expect(described_class.file_path).to eq("config/ticket-replicator.mappings.yml") }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#to_h" do
|
14
|
+
let(:mappings_yaml) do
|
15
|
+
<<~'YAML'
|
16
|
+
field_mapping:
|
17
|
+
id: Defect
|
18
|
+
summary: Defect (2)
|
19
|
+
id_extraction_regex: "\\((?<id>\\d+)\\)$"
|
20
|
+
status_mapping:
|
21
|
+
defaults_to: keep_original_value
|
22
|
+
Open: Open
|
23
|
+
Closed: Closed
|
24
|
+
resolution_mapping:
|
25
|
+
Closed: Done
|
26
|
+
Rejected: Won't Fix
|
27
|
+
team_mapping:
|
28
|
+
Source Team: Transformed Team
|
29
|
+
Front End: Web Team
|
30
|
+
priority_mapping:
|
31
|
+
1 - Critical: Critical
|
32
|
+
2 - High: High
|
33
|
+
3 - Medium: Medium
|
34
|
+
4 - Low: Low
|
35
|
+
YAML
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:file_io) { instance_double(IO) }
|
39
|
+
|
40
|
+
it "loads the mappings from the mappings.yml file" do
|
41
|
+
expect(IO).to receive(:open).with("config/ticket-replicator.mappings.yml",
|
42
|
+
"r:bom|utf-8").and_yield(mappings_yaml)
|
43
|
+
|
44
|
+
expect(mappings.to_h)
|
45
|
+
.to eq(
|
46
|
+
"field_mapping" => { "id" => "Defect", "summary" => "Defect (2)" },
|
47
|
+
"id_extraction_regex" => "\\((?<id>\\d+)\\)$",
|
48
|
+
"status_mapping" => { "defaults_to" => "keep_original_value", "Open" => "Open",
|
49
|
+
"Closed" => "Closed" },
|
50
|
+
"resolution_mapping" => { "Closed" => "Done", "Rejected" => "Won't Fix" },
|
51
|
+
"team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" },
|
52
|
+
"priority_mapping" =>
|
53
|
+
{ "1 - Critical" => "Critical", "2 - High" => "High", "3 - Medium" => "Medium",
|
54
|
+
"4 - Low" => "Low" }
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -158,8 +158,9 @@ module Ticket
|
|
158
158
|
|
159
159
|
it do
|
160
160
|
allow(loader).to receive_messages(attributes_for_save: { fields: { a: "b" } })
|
161
|
-
expect(jira_ticket).to receive(:save!).with({ fields: { a: "b" } })
|
162
161
|
|
162
|
+
expect(loader).to receive(:reopen_ticket_if_status_in_done_category)
|
163
|
+
expect(jira_ticket).to receive(:save!).with({ fields: { a: "b" } })
|
163
164
|
expect(jira_ticket).to receive(:fetch)
|
164
165
|
|
165
166
|
# TODO: fix this check that should work
|
@@ -181,6 +182,52 @@ module Ticket
|
|
181
182
|
end
|
182
183
|
end
|
183
184
|
|
185
|
+
describe "#reopen_ticket_if_status_in_done_category" do
|
186
|
+
let(:loader) { described_class.send(:new, jira_project, double("row", id: "123", status: "Closed")) }
|
187
|
+
let(:ticket) { instance_double(Ticket) }
|
188
|
+
|
189
|
+
before do
|
190
|
+
allow(loader).to receive_messages(ticket: ticket,
|
191
|
+
:ticket_previously_replicated? => actual_ticket_previously_replicated)
|
192
|
+
end
|
193
|
+
|
194
|
+
context "when the ticket was not previously replicated" do
|
195
|
+
let(:actual_ticket_previously_replicated) { false }
|
196
|
+
|
197
|
+
it do
|
198
|
+
expect(ticket).not_to receive(:transition_to_not_done)
|
199
|
+
|
200
|
+
loader.send(:reopen_ticket_if_status_in_done_category)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context "when the ticket was previously replicated" do
|
205
|
+
let(:actual_ticket_previously_replicated) { true }
|
206
|
+
|
207
|
+
before { allow(ticket).to receive_messages(status_in_done_category?: actual_status_in_done_category) }
|
208
|
+
|
209
|
+
context "when the ticket is not closed" do
|
210
|
+
let(:actual_status_in_done_category) { false }
|
211
|
+
|
212
|
+
it do
|
213
|
+
expect(ticket).not_to receive(:transition_to_not_done)
|
214
|
+
|
215
|
+
loader.send(:reopen_ticket_if_status_in_done_category)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
context "when the ticket is closed" do
|
220
|
+
let(:actual_status_in_done_category) { true }
|
221
|
+
|
222
|
+
it do
|
223
|
+
expect(ticket).to receive(:transition_to_not_done)
|
224
|
+
|
225
|
+
loader.send(:reopen_ticket_if_status_in_done_category)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
184
231
|
describe "#attributes_for_save" do
|
185
232
|
let(:loader) { described_class.send(:new, jira_project, row) }
|
186
233
|
|
@@ -410,6 +457,24 @@ module Ticket
|
|
410
457
|
it { expect(loader.send(:source_ticket_link_title)).to eq("Source Ticket 123") }
|
411
458
|
end
|
412
459
|
|
460
|
+
describe "#only_update_links_for_modified_ticket?" do
|
461
|
+
it "defaults to true when unset" do
|
462
|
+
expect(ENV)
|
463
|
+
.to receive(:fetch).with("TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS",
|
464
|
+
"true").and_return("true")
|
465
|
+
|
466
|
+
expect(loader.send(:only_update_links_for_modified_ticket?)).to be_truthy
|
467
|
+
end
|
468
|
+
|
469
|
+
it "when TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS set to false" do
|
470
|
+
expect(ENV)
|
471
|
+
.to receive(:fetch).with("TICKET_REPLICATOR_ONLY_UPDATE_LINKS_FOR_MODIFIED_TICKETS", "true")
|
472
|
+
.and_return("false")
|
473
|
+
|
474
|
+
expect(loader.send(:only_update_links_for_modified_ticket?)).to be_falsey
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
413
478
|
describe "#update_source_ticket_remote_link" do
|
414
479
|
before do
|
415
480
|
allow(loader).to receive_messages(source_ticket_link_needs_update?: needs_update)
|
@@ -454,57 +519,87 @@ module Ticket
|
|
454
519
|
|
455
520
|
describe "#source_ticket_link_needs_update?" do
|
456
521
|
before do
|
457
|
-
allow(
|
522
|
+
allow(loader).to receive_messages(only_update_links_for_modified_ticket?: only_update_links_for_new_ticket)
|
458
523
|
end
|
459
524
|
|
460
|
-
context "when
|
461
|
-
let(:
|
525
|
+
context "when only for modified tickets" do
|
526
|
+
let(:only_update_links_for_new_ticket) { true }
|
462
527
|
|
463
|
-
|
528
|
+
before { allow(loader).to receive_messages(ticket_was_just_updated?: actual_was_updated) }
|
529
|
+
|
530
|
+
context "when the ticket has already been replicated (i.e., exists)" do
|
531
|
+
let(:actual_was_updated) { false }
|
532
|
+
|
533
|
+
it "does not request the remote link information" do
|
534
|
+
expect(ticket).not_to receive(:source_ticket_link)
|
535
|
+
|
536
|
+
expect(loader.send(:source_ticket_link_needs_update?)).to be_falsey
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
context "when the ticket is new" do
|
541
|
+
let(:actual_was_updated) { true }
|
542
|
+
|
543
|
+
it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
|
544
|
+
end
|
464
545
|
end
|
465
546
|
|
466
|
-
context "when
|
467
|
-
let(:
|
468
|
-
|
469
|
-
|
470
|
-
|
547
|
+
context "when not only for new tickets" do
|
548
|
+
let(:only_update_links_for_new_ticket) { false }
|
549
|
+
|
550
|
+
before do
|
551
|
+
allow(ticket).to receive_messages(source_ticket_link: remote_link)
|
471
552
|
end
|
472
553
|
|
473
|
-
context "when
|
474
|
-
let(:
|
554
|
+
context "when link has yet to be created" do
|
555
|
+
let(:remote_link) { nil }
|
475
556
|
|
476
|
-
|
477
|
-
|
478
|
-
let(:existing_title) { "Source Ticket 123" }
|
557
|
+
it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
|
558
|
+
end
|
479
559
|
|
480
|
-
|
560
|
+
context "when link already exists" do
|
561
|
+
let(:remote_link) do
|
562
|
+
instance_double(JIRA::Resource::Remotelink,
|
563
|
+
attrs: source_ticket_link,
|
564
|
+
inspect: "<JIRA::Resource::Remotelink attrs: #{source_ticket_link}>")
|
481
565
|
end
|
482
566
|
|
483
|
-
context "when
|
484
|
-
let(:
|
485
|
-
let(:existing_title) { "__OLD TITLE__Source Ticket 1234" }
|
567
|
+
context "when the link is not up to date" do
|
568
|
+
let(:source_ticket_link) { build_link(existing_url, existing_title) }
|
486
569
|
|
487
|
-
|
488
|
-
|
489
|
-
|
570
|
+
context "when url has changed" do
|
571
|
+
let(:existing_url) { "https://__OLD_URL__/to/source/ticket/1234" }
|
572
|
+
let(:existing_title) { "Source Ticket 123" }
|
573
|
+
|
574
|
+
it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
|
575
|
+
end
|
490
576
|
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
"title" => "Source Ticket 123",
|
498
|
-
"icon" => { "title" => "link", "url16x16" => "https://url/to/source/ticket/123/icon" }
|
499
|
-
},
|
500
|
-
"application" => { "name" => "Ticket Source" },
|
501
|
-
"extra_attributes" =>
|
502
|
-
{ "id" => 10_443, "self" => "https://url/to/source/ticket/123/remotelink/10443" } }
|
577
|
+
context "when title has changed" do
|
578
|
+
let(:existing_url) { "https://url/to/source/ticket/123" }
|
579
|
+
let(:existing_title) { "__OLD TITLE__Source Ticket 1234" }
|
580
|
+
|
581
|
+
it { expect(loader.send(:source_ticket_link_needs_update?)).to be_truthy }
|
582
|
+
end
|
503
583
|
end
|
504
584
|
|
505
|
-
|
506
|
-
|
507
|
-
|
585
|
+
context "when the link is up to date" do
|
586
|
+
let(:existing_url) { "https://url/to/source/ticket/123" }
|
587
|
+
let(:existing_title) { "Source Ticket 123" }
|
588
|
+
let(:source_ticket_link) do
|
589
|
+
{ "object" => {
|
590
|
+
"url" => "https://url/to/source/ticket/123",
|
591
|
+
"title" => "Source Ticket 123",
|
592
|
+
"icon" => { "title" => "link", "url16x16" => "https://url/to/source/ticket/123/icon" }
|
593
|
+
},
|
594
|
+
"application" => { "name" => "Ticket Source" },
|
595
|
+
"extra_attributes" =>
|
596
|
+
{ "id" => 10_443, "self" => "https://url/to/source/ticket/123/remotelink/10443" } }
|
597
|
+
end
|
598
|
+
|
599
|
+
it("only checks the needed attributes") {
|
600
|
+
expect(loader.send(:source_ticket_link_needs_update?)).to be_falsey
|
601
|
+
}
|
602
|
+
end
|
508
603
|
end
|
509
604
|
end
|
510
605
|
end
|
@@ -524,6 +619,44 @@ module Ticket
|
|
524
619
|
end
|
525
620
|
end
|
526
621
|
end
|
622
|
+
|
623
|
+
describe "#ticket_was_just_updated?" do
|
624
|
+
let(:loader) { described_class.send(:new, jira_project, { id: 789 }) }
|
625
|
+
|
626
|
+
context "when the ticket was just created" do
|
627
|
+
it do
|
628
|
+
allow(loader).to receive_messages(ticket_previously_replicated?: false)
|
629
|
+
allow(loader).to receive_messages(create_ticket: nil)
|
630
|
+
|
631
|
+
loader.send(:ticket)
|
632
|
+
|
633
|
+
expect(loader.send(:ticket_was_just_updated?)).to be_truthy
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
context "when the ticket had modified fields" do
|
638
|
+
let(:jira_ticket) { double(JIRA::Resource::Issue, save!: nil, fetch: nil) }
|
639
|
+
let(:ticket) { instance_double(Ticket, jira_ticket: jira_ticket) }
|
640
|
+
|
641
|
+
it do
|
642
|
+
allow(loader)
|
643
|
+
.to receive_messages(ticket_fields_need_to_be_updated?: true,
|
644
|
+
reopen_ticket_if_status_in_done_category: nil,
|
645
|
+
ticket: ticket,
|
646
|
+
attributes_for_save: {})
|
647
|
+
|
648
|
+
allow(jira_project).to receive_messages(replicated_tickets: { "789" => "a_ticket" })
|
649
|
+
|
650
|
+
loader.send(:save_ticket)
|
651
|
+
|
652
|
+
expect(loader.send(:ticket_was_just_updated?)).to be_truthy
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
context "when the ticket was not new or had modified fields" do
|
657
|
+
it { expect(loader.send(:ticket_was_just_updated?)).to be_falsey }
|
658
|
+
end
|
659
|
+
end
|
527
660
|
end
|
528
661
|
# rubocop:enable Metrics/ClassLength
|
529
662
|
end
|
@@ -7,17 +7,18 @@ module Ticket
|
|
7
7
|
# rubocop:disable Metrics/ClassLength
|
8
8
|
class RowTransformer
|
9
9
|
RSpec.describe RowTransformer do
|
10
|
-
let(:transformer) { described_class.send(:new, extracted_row) }
|
10
|
+
let(:transformer) { described_class.send(:new, extracted_row, mappings) }
|
11
|
+
let(:mappings) { instance_double(Mappings, to_h: {}) }
|
11
12
|
|
12
13
|
describe ".run_on" do
|
13
14
|
let(:extracted_row) { :a_row_to_transform }
|
14
15
|
|
15
16
|
it "transforms the given file" do
|
16
|
-
expect(described_class).to receive(:new).with(:a_row_to_transform).and_return(transformer)
|
17
|
+
expect(described_class).to receive(:new).with(:a_row_to_transform, mappings).and_return(transformer)
|
17
18
|
|
18
19
|
expect(transformer).to receive(:run)
|
19
20
|
|
20
|
-
described_class.run_on(:a_row_to_transform)
|
21
|
+
described_class.run_on(:a_row_to_transform, mappings)
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
@@ -46,50 +47,13 @@ module Ticket
|
|
46
47
|
end
|
47
48
|
|
48
49
|
describe "#mappings" do
|
49
|
-
let(:mappings_yaml) do
|
50
|
-
<<~'YAML'
|
51
|
-
field_mapping:
|
52
|
-
id: Defect
|
53
|
-
summary: Defect (2)
|
54
|
-
id_extraction_regex: "\\((?<id>\\d+)\\)$"
|
55
|
-
status_mapping:
|
56
|
-
defaults_to: keep_original_value
|
57
|
-
Open: Open
|
58
|
-
Closed: Closed
|
59
|
-
resolution_mapping:
|
60
|
-
Closed: Done
|
61
|
-
Rejected: Won't Fix
|
62
|
-
team_mapping:
|
63
|
-
Source Team: Transformed Team
|
64
|
-
Front End: Web Team
|
65
|
-
priority_mapping:
|
66
|
-
1 - Critical: Critical
|
67
|
-
2 - High: High
|
68
|
-
3 - Medium: Medium
|
69
|
-
4 - Low: Low
|
70
|
-
YAML
|
71
|
-
end
|
72
|
-
|
73
50
|
let(:extracted_row) { {} }
|
51
|
+
let(:mappings) { instance_double(Mappings, to_h: { "field_mapping" => {} }) }
|
52
|
+
|
53
|
+
it "uses the mapping object" do
|
54
|
+
RowTransformer.send(:new, extracted_row, mappings)
|
74
55
|
|
75
|
-
|
76
|
-
|
77
|
-
it "loads the mappings from the mappings.yml file" do
|
78
|
-
expect(IO).to receive(:open).with("config/ticket-replicator.mappings.yml",
|
79
|
-
"r:bom|utf-8").and_yield(mappings_yaml)
|
80
|
-
|
81
|
-
expect(transformer.mappings)
|
82
|
-
.to eq(
|
83
|
-
"field_mapping" => { "id" => "Defect", "summary" => "Defect (2)" },
|
84
|
-
"id_extraction_regex" => "\\((?<id>\\d+)\\)$",
|
85
|
-
"status_mapping" => { "defaults_to" => "keep_original_value", "Open" => "Open",
|
86
|
-
"Closed" => "Closed" },
|
87
|
-
"resolution_mapping" => { "Closed" => "Done", "Rejected" => "Won't Fix" },
|
88
|
-
"team_mapping" => { "Source Team" => "Transformed Team", "Front End" => "Web Team" },
|
89
|
-
"priority_mapping" =>
|
90
|
-
{ "1 - Critical" => "Critical", "2 - High" => "High", "3 - Medium" => "Medium",
|
91
|
-
"4 - Low" => "Low" }
|
92
|
-
)
|
56
|
+
expect(transformer.mappings).to eq({ "field_mapping" => {} })
|
93
57
|
end
|
94
58
|
end
|
95
59
|
|
@@ -217,6 +217,28 @@ module Ticket
|
|
217
217
|
it { expect(ticket.status).to eq("To Do") }
|
218
218
|
end
|
219
219
|
|
220
|
+
describe "#status_in_done_category?" do
|
221
|
+
let(:jira_ticket) do
|
222
|
+
double(JIRA::Resource::Issue, status: actual_status)
|
223
|
+
end
|
224
|
+
|
225
|
+
let(:actual_status) do
|
226
|
+
double(JIRA::Resource::Status, attrs: { "name" => "A status", "statusCategory" => actual_status_category })
|
227
|
+
end
|
228
|
+
|
229
|
+
context "when status is in done category" do
|
230
|
+
let(:actual_status_category) { { "key" => "done", "name" => "Done" } }
|
231
|
+
|
232
|
+
it { expect(ticket.status_in_done_category?).to be_truthy }
|
233
|
+
end
|
234
|
+
|
235
|
+
context "when status is not in done category" do
|
236
|
+
let(:actual_status_category) { { "key" => "new", "name" => "To Do" } }
|
237
|
+
|
238
|
+
it { expect(ticket.status_in_done_category?).to be_falsy }
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
220
242
|
describe "#resolution" do
|
221
243
|
it { expect(ticket.resolution).to eq("") }
|
222
244
|
end
|
@@ -280,7 +302,8 @@ module Ticket
|
|
280
302
|
|
281
303
|
describe "#transition_to" do
|
282
304
|
let(:transitioner) { instance_double(TicketStatusTransitioner) }
|
283
|
-
|
305
|
+
|
306
|
+
it "uses a status transitioner" do
|
284
307
|
expect(TicketStatusTransitioner).to receive(:new).with(ticket).and_return(transitioner)
|
285
308
|
expect(transitioner).to receive(:transition_to).with("Testing")
|
286
309
|
|
@@ -288,6 +311,17 @@ module Ticket
|
|
288
311
|
end
|
289
312
|
end
|
290
313
|
|
314
|
+
describe "#transition_to_not_done" do
|
315
|
+
let(:transitioner) { instance_double(TicketStatusTransitioner) }
|
316
|
+
|
317
|
+
it "uses a status transitioner" do
|
318
|
+
expect(TicketStatusTransitioner).to receive(:new).with(ticket).and_return(transitioner)
|
319
|
+
expect(transitioner).to receive(:transition_to_not_done)
|
320
|
+
|
321
|
+
ticket.transition_to_not_done
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
291
325
|
describe "#sanitize" do
|
292
326
|
let(:expectations) do
|
293
327
|
[
|
@@ -311,4 +345,5 @@ module Ticket
|
|
311
345
|
end
|
312
346
|
end
|
313
347
|
end
|
348
|
+
|
314
349
|
# rubocop:enable Metrics/ClassLength
|
@@ -6,7 +6,7 @@ require "ticket/replicator/ticket_status_transitioner"
|
|
6
6
|
module Ticket
|
7
7
|
class Replicator
|
8
8
|
RSpec.describe TicketStatusTransitioner do
|
9
|
-
let(:transitioner) { described_class.
|
9
|
+
let(:transitioner) { described_class.new(ticket) }
|
10
10
|
|
11
11
|
let(:ticket) do
|
12
12
|
double(Ticket, key: "PROJKEY-16", status: "Todo", jira_auto_tool: jira_auto_tool, jira_client: jira_client)
|
@@ -15,7 +15,7 @@ module Ticket
|
|
15
15
|
let(:jira_auto_tool) { instance_double(Jira::Auto::Tool, jira_client: jira_client) }
|
16
16
|
let(:jira_client) { instance_double(JIRA::Client) }
|
17
17
|
|
18
|
-
describe "#
|
18
|
+
describe "#transition_to" do
|
19
19
|
let(:expected_payload) { { transition: { id: 131 } }.to_json }
|
20
20
|
let(:expected_headers) { { "Content-Type" => "application/json" } }
|
21
21
|
|
@@ -48,6 +48,53 @@ module Ticket
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
describe "#transition_to_not_done" do
|
52
|
+
let(:available_transitions) do
|
53
|
+
[
|
54
|
+
{ "name" => "Withdraw",
|
55
|
+
"to" => { "name" => "Withdrawn",
|
56
|
+
"statusCategory" => { "key" => "done", "colorName" => "green", "name" => "Done" } } },
|
57
|
+
{ "name" => "Set As Defect Correction in Process",
|
58
|
+
"to" => {
|
59
|
+
"name" => "Defect Correction in Process",
|
60
|
+
"statusCategory" => { "key" => "indeterminate", "colorName" => "yellow", "name" => "In Progress" }
|
61
|
+
} },
|
62
|
+
{ "name" => "Confirm",
|
63
|
+
"to" => { "name" => "Confirmed",
|
64
|
+
"statusCategory" => { "key" => "new", "colorName" => "blue-gray", "name" => "To Do" } } }
|
65
|
+
]
|
66
|
+
end
|
67
|
+
|
68
|
+
before do
|
69
|
+
expect(transitioner).to receive(:available_transitions).at_least(:once).and_return(available_transitions)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "finds a non-done transition and transitions to it" do
|
73
|
+
expect(transitioner).to receive(:transition_to).with("Defect Correction in Process")
|
74
|
+
|
75
|
+
transitioner.transition_to_not_done
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when no non-done transition is found" do
|
79
|
+
let(:available_transitions) do
|
80
|
+
[
|
81
|
+
{ "name" => "Withdraw",
|
82
|
+
"to" => { "name" => "Withdrawn",
|
83
|
+
"statusCategory" => { "key" => "done", "colorName" => "green", "name" => "Done" } } },
|
84
|
+
{ "name" => "Set To Done",
|
85
|
+
"to" => { "name" => "Done",
|
86
|
+
"statusCategory" => { "key" => "done", "colorName" => "green", "name" => "Done" } } }
|
87
|
+
]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "transitions to the first done transition" do
|
91
|
+
expect { transitioner.transition_to_not_done }
|
92
|
+
.to raise_error(TicketStatusTransitioner::StatusTransitionerError,
|
93
|
+
/No non-done transition found in .+/)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
51
98
|
describe "#transition_payload_for" do
|
52
99
|
let(:available_transitions) do
|
53
100
|
[
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ticket-replicator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christophe Broult
|
@@ -183,6 +183,7 @@ files:
|
|
183
183
|
- lib/ticket/replicator/file_transformer/for_csv.rb
|
184
184
|
- lib/ticket/replicator/file_transformer/for_xlsx.rb
|
185
185
|
- lib/ticket/replicator/jira_project.rb
|
186
|
+
- lib/ticket/replicator/mappings.rb
|
186
187
|
- lib/ticket/replicator/replicated_summary.rb
|
187
188
|
- lib/ticket/replicator/row_loader.rb
|
188
189
|
- lib/ticket/replicator/row_transformer.rb
|
@@ -199,6 +200,7 @@ files:
|
|
199
200
|
- spec/ticket/replicator/file_transformer/for_xlsx_spec.rb
|
200
201
|
- spec/ticket/replicator/file_transformer_spec.rb
|
201
202
|
- spec/ticket/replicator/jira_project_spec.rb
|
203
|
+
- spec/ticket/replicator/mappings_spec.rb
|
202
204
|
- spec/ticket/replicator/replicated_summary_spec.rb
|
203
205
|
- spec/ticket/replicator/row_loader_spec.rb
|
204
206
|
- spec/ticket/replicator/row_transformer_spec.rb
|
@@ -230,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
230
232
|
- !ruby/object:Gem::Version
|
231
233
|
version: '0'
|
232
234
|
requirements: []
|
233
|
-
rubygems_version: 3.6.
|
235
|
+
rubygems_version: 3.6.8
|
234
236
|
specification_version: 4
|
235
237
|
summary: Automate replicating tickets from one system to a Jira project
|
236
238
|
test_files: []
|