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,121 @@
|
|
1
|
+
Feature: Transform and load extracted ticket queue
|
2
|
+
In order to track the pending work in a unified view
|
3
|
+
As a stakeholder
|
4
|
+
I need the ability to transform and load extracted tickets to a Jira project
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given the following environment variables have already been set with a value:
|
8
|
+
| name |
|
9
|
+
| TICKET_REPLICATOR_JIRA_PROJECT_KEY |
|
10
|
+
| TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME |
|
11
|
+
And a file named "config/ticket-replicator.mappings.yml" with:
|
12
|
+
"""
|
13
|
+
field_mapping:
|
14
|
+
id: Defect
|
15
|
+
summary: Defect (2)
|
16
|
+
priority: Defect Priority
|
17
|
+
resolution: Defect Status
|
18
|
+
status: Defect Status
|
19
|
+
team: Defect Support Team (2)
|
20
|
+
|
21
|
+
priority_mapping:
|
22
|
+
"1: Critical": "Highest"
|
23
|
+
"2: High": "High"
|
24
|
+
"3: Medium": "Medium"
|
25
|
+
"4: Low": "Low"
|
26
|
+
|
27
|
+
status_mapping:
|
28
|
+
defaults_to: keep_original_value
|
29
|
+
|
30
|
+
resolution_mapping:
|
31
|
+
defaults_to: keep_original_value
|
32
|
+
"New":
|
33
|
+
"Open":
|
34
|
+
"In Process":
|
35
|
+
"In Review":
|
36
|
+
"On Hold":
|
37
|
+
Deferred:
|
38
|
+
Defect Correction in Process:
|
39
|
+
"Fixed": "Fixed"
|
40
|
+
"Closed": "Done"
|
41
|
+
"Rejected": "Won't Do"
|
42
|
+
"Resolved": "Fixed"
|
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"
|
63
|
+
"""
|
64
|
+
And the project has no tickets
|
65
|
+
|
66
|
+
Scenario: Process Ticket Queue successfully
|
67
|
+
Given an Excel file named "queue/10.extracted/sap_solution_manager_defects.xlsx"
|
68
|
+
And it has a tab named "SAP Document Export" with the following rows:
|
69
|
+
| Defect | Defect (2) | Defect Priority | Defect Status | Defect Support Team (2) |
|
70
|
+
| 3000017049 | Summary | 3: Medium | Closed | A Team |
|
71
|
+
| 9400011377 | Summary | 4: Low | Confirmed | A Team |
|
72
|
+
| 3000016618 | Summary | 3: Medium | Defect Correction in Process | A Team |
|
73
|
+
| 9400013805 | Summary | 3: Medium | Deferred | A Team |
|
74
|
+
| 9400013816 | Summary | 2: High | Forwarded | A Team |
|
75
|
+
| 9400011382 | Summary | 3: Medium | In Process | A Team |
|
76
|
+
| 9400011393 | Summary | 3: Medium | Information Required | A Team |
|
77
|
+
| 9400011381 | Summary | 3: Medium | New | A Team |
|
78
|
+
| 3000016617 | Summary | 3: Medium | No Error | A Team |
|
79
|
+
| 9400011372 | Summary | 1: Critical | Open | A Team |
|
80
|
+
| 9400011466 | Summary | 3: Medium | Withdrawn | A Team |
|
81
|
+
| 3000017667 | Summary | 3: Medium | Closed | Another Team |
|
82
|
+
| 3000018423 | Summary | 3: Medium | No Error | A Team |
|
83
|
+
Then a file named "queue/10.extracted/sap_solution_manager_defects.xlsx" should exist
|
84
|
+
And the current date time is "2025-05-10 07:52:00 UTC"
|
85
|
+
When I successfully run `ticket-replicator --ticket-queue-transform-and-load`
|
86
|
+
Then the Jira project should only have the following tickets:
|
87
|
+
| status | resolution | priority | team | summary | source ticket url |
|
88
|
+
| Closed | Done | Medium | A Team | SMAN-3000017049 \| Summary | https://example.com/defect/3000017049 |
|
89
|
+
| Confirmed | | Low | A Team | SMAN-9400011377 \| Summary | https://example.com/defect/9400011377 |
|
90
|
+
| Defect Correction in Process | | Medium | A Team | SMAN-3000016618 \| Summary | https://example.com/defect/3000016618 |
|
91
|
+
| Deferred | | Medium | A Team | SMAN-9400013805 \| Summary | https://example.com/defect/9400013805 |
|
92
|
+
| Forwarded | | High | A Team | SMAN-9400013816 \| Summary | https://example.com/defect/9400013816 |
|
93
|
+
| In Process | | Medium | A Team | SMAN-9400011382 \| Summary | https://example.com/defect/9400011382 |
|
94
|
+
| Information Required | | Medium | A Team | SMAN-9400011393 \| Summary | https://example.com/defect/9400011393 |
|
95
|
+
| New | | Medium | A Team | SMAN-9400011381 \| Summary | https://example.com/defect/9400011381 |
|
96
|
+
| No Error | | Medium | A Team | SMAN-3000016617 \| Summary | https://example.com/defect/3000016617 |
|
97
|
+
| Open | | Highest | A Team | SMAN-9400011372 \| Summary | https://example.com/defect/9400011372 |
|
98
|
+
| Withdrawn | Won't Do | Medium | A Team | SMAN-9400011466 \| Summary | https://example.com/defect/9400011466 |
|
99
|
+
| Closed | Done | Medium | Another Team | SMAN-3000017667 \| Summary | https://example.com/defect/3000017667 |
|
100
|
+
| No Error | | Medium | A Team | SMAN-3000018423 \| Summary | https://example.com/defect/3000018423 |
|
101
|
+
# TODO: And the source ticket URL is found in the ticket descriptions of those tickets
|
102
|
+
And a file named "queue/30.archived/2025-05-10.07h52m00.sap_solution_manager_defects.xlsx" should exist
|
103
|
+
And the file named "queue/10.extracted/sap_solution_manager_defects.xlsx" should not exist anymore
|
104
|
+
And a file named "queue/20.transformed/sap_solution_manager_defects.csv" should not exist anymore
|
105
|
+
|
106
|
+
Scenario: Extracted files remain available for reprocessing in case of failure
|
107
|
+
Given an Excel file named "queue/10.extracted/sap_solution_manager_defects.xlsx"
|
108
|
+
And it has a tab named "SAP Document Export" with the following rows:
|
109
|
+
| Defect | Defect (2) | Defect Priority | Defect Status | Defect Support Team (2) |
|
110
|
+
| 3000017049 | Summary | 3: Medium | __UNKNOWN STATUS Causing Error___ | A Team |
|
111
|
+
| 3000017667 | Summary | 3: Medium | Closed | Another Team |
|
112
|
+
| 3000018423 | Summary | 3: Medium | No Error | A Team |
|
113
|
+
Then a file named "queue/10.extracted/sap_solution_manager_defects.xlsx" should exist
|
114
|
+
When I run `ticket-replicator --tqtal`
|
115
|
+
Then it should fail with:
|
116
|
+
"""
|
117
|
+
ERROR Object : Ticket::Replicator::FileLoader::LoadError: queue/20.transformed/sap_solution_manager_defects.csv:2: error while loading row:
|
118
|
+
No transition found for "__UNKNOWN STATUS Causing Error___" in
|
119
|
+
"""
|
120
|
+
And the file named "queue/10.extracted/sap_solution_manager_defects.xlsx" should exist
|
121
|
+
And a file named "queue/20.transformed/sap_solution_manager_defects.csv" should exist
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
def validate_version_type(type)
|
4
|
+
valid_types = %w[patch minor major]
|
5
|
+
return if valid_types.include?(type)
|
6
|
+
|
7
|
+
puts "Error: Version type must be one of: #{valid_types.join(", ")}"
|
8
|
+
exit 1
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute_version_bump(type)
|
12
|
+
puts "Bumping #{type} version..."
|
13
|
+
bump_result = system("bundle exec gem bump --version #{type}")
|
14
|
+
return if bump_result
|
15
|
+
|
16
|
+
puts "Error: Failed to bump version"
|
17
|
+
exit 1
|
18
|
+
end
|
19
|
+
|
20
|
+
def update_gemfile_lock
|
21
|
+
puts "Updating Gemfile.lock..."
|
22
|
+
bundle_result = system("bundle install")
|
23
|
+
return if bundle_result
|
24
|
+
|
25
|
+
puts "Error: Failed to update Gemfile.lock"
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def amend_commit_to_include_gemfile_lock_changes
|
30
|
+
puts "Amending commit to include Gemfile.lock update..."
|
31
|
+
system("git add .")
|
32
|
+
system("git commit --amend --no-edit")
|
33
|
+
end
|
34
|
+
|
35
|
+
namespace :version do
|
36
|
+
desc "Bump version (patch, minor, major) and update Gemfile.lock in a single step. Default: patch"
|
37
|
+
task :bump, [:type] do |_t, args|
|
38
|
+
args.with_defaults(type: "patch")
|
39
|
+
|
40
|
+
validate_version_type(args.type)
|
41
|
+
|
42
|
+
execute_version_bump(args.type)
|
43
|
+
update_gemfile_lock
|
44
|
+
amend_commit_to_include_gemfile_lock_changes
|
45
|
+
|
46
|
+
puts <<~EOEM
|
47
|
+
Version successfully bumped and committed!
|
48
|
+
|
49
|
+
Run 'git push' to push the changes to your remote repository.
|
50
|
+
EOEM
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "Alias for version:bump"
|
55
|
+
task :bump, [:type] => ["version:bump"]
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "mock_sap_client"
|
4
|
+
require "logger"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
class DefectExportAutomation
|
8
|
+
def initialize
|
9
|
+
@client = SAPSolutionManagerClient.new("http://localhost:4567")
|
10
|
+
@logger = Logger.new($stdout)
|
11
|
+
@logger.level = Logger::INFO
|
12
|
+
|
13
|
+
# Create exports directory if it doesn't exist
|
14
|
+
FileUtils.mkdir_p("exports")
|
15
|
+
end
|
16
|
+
|
17
|
+
def run_all_scenarios
|
18
|
+
# Authenticate
|
19
|
+
authenticate
|
20
|
+
|
21
|
+
# Run scenarios
|
22
|
+
export_all_defects
|
23
|
+
export_filtered_defects
|
24
|
+
|
25
|
+
@logger.info { "All export scenarios completed successfully!" }
|
26
|
+
rescue StandardError => e
|
27
|
+
@logger.error("Automation failed: #{e.message}")
|
28
|
+
@logger.error(e.backtrace.join("\n"))
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def authenticate
|
34
|
+
@logger.info { "Authenticating with SAP Solution Manager..." }
|
35
|
+
raise "Authentication failed" unless @client.authenticate("test_user", "test_password")
|
36
|
+
|
37
|
+
@logger.info { "Authentication successful" }
|
38
|
+
end
|
39
|
+
|
40
|
+
def export_all_defects
|
41
|
+
@logger.info { "Scenario 1: Exporting all defect tickets..." }
|
42
|
+
export_all_defects_json
|
43
|
+
export_all_defects_csv
|
44
|
+
end
|
45
|
+
|
46
|
+
def export_all_defects_json
|
47
|
+
@logger.info { "Exporting all tickets as JSON..." }
|
48
|
+
result_json = @client.export_tickets("json")
|
49
|
+
File.write("exports/all_defects.json", result_json.to_json)
|
50
|
+
@logger.info { "Exported #{result_json['tickets'].size} tickets to exports/all_defects.json" }
|
51
|
+
end
|
52
|
+
|
53
|
+
def export_all_defects_csv
|
54
|
+
@logger.info { "Exporting all tickets as CSV..." }
|
55
|
+
result_csv = @client.export_tickets("csv")
|
56
|
+
File.write("exports/all_defects.csv", result_csv)
|
57
|
+
lines_count = result_csv.split("\n").size - 1
|
58
|
+
@logger.info { "Exported #{lines_count} tickets to exports/all_defects.csv" }
|
59
|
+
end
|
60
|
+
|
61
|
+
def export_filtered_defects
|
62
|
+
@logger.info { "Scenario 2: Exporting filtered defect tickets..." }
|
63
|
+
create_test_tickets
|
64
|
+
|
65
|
+
export_status_filtered
|
66
|
+
export_priority_filtered
|
67
|
+
export_complex_filtered
|
68
|
+
export_csv_filtered
|
69
|
+
end
|
70
|
+
|
71
|
+
def export_status_filtered
|
72
|
+
@logger.info { "Exporting tickets with status 'IN_PROGRESS'..." }
|
73
|
+
status_result = @client.export_tickets("json", { status: "IN_PROGRESS" })
|
74
|
+
File.write("exports/in_progress_defects.json", status_result.to_json)
|
75
|
+
@logger.info { "Exported #{status_result['tickets'].size} tickets with IN_PROGRESS status" }
|
76
|
+
end
|
77
|
+
|
78
|
+
def export_priority_filtered
|
79
|
+
@logger.info { "Exporting tickets with HIGH priority..." }
|
80
|
+
priority_result = @client.export_tickets("json", { priority: "HIGH" })
|
81
|
+
File.write("exports/high_priority_defects.json", priority_result.to_json)
|
82
|
+
@logger.info { "Exported #{priority_result['tickets'].size} tickets with HIGH priority" }
|
83
|
+
end
|
84
|
+
|
85
|
+
def export_complex_filtered
|
86
|
+
@logger.info { "Exporting tickets with multiple filters..." }
|
87
|
+
complex_result = @client.export_tickets("json", {
|
88
|
+
status: "NEW",
|
89
|
+
priority: "CRITICAL",
|
90
|
+
assigned_to: "URGENT_TEAM"
|
91
|
+
})
|
92
|
+
File.write("exports/critical_new_defects.json", complex_result.to_json)
|
93
|
+
@logger.info { "Exported #{complex_result['tickets'].size} tickets matching complex criteria" }
|
94
|
+
end
|
95
|
+
|
96
|
+
def export_csv_filtered
|
97
|
+
@logger.info { "Exporting filtered tickets as CSV..." }
|
98
|
+
csv_result = @client.export_tickets("csv", { priority: "HIGH" })
|
99
|
+
File.write("exports/high_priority_defects.csv", csv_result)
|
100
|
+
lines_count = csv_result.split("\n").size - 1
|
101
|
+
@logger.info { "Exported #{lines_count} tickets to exports/high_priority_defects.csv" }
|
102
|
+
end
|
103
|
+
|
104
|
+
def create_test_tickets
|
105
|
+
@logger.info { "Creating test tickets for filtering scenarios..." }
|
106
|
+
|
107
|
+
# Create a few tickets with different attributes for testing filters
|
108
|
+
statuses = %w[NEW IN_PROGRESS RESOLVED CLOSED]
|
109
|
+
priorities = %w[LOW MEDIUM HIGH CRITICAL]
|
110
|
+
|
111
|
+
10.times do |i|
|
112
|
+
status = statuses[i % statuses.length]
|
113
|
+
priority = priorities[i % priorities.length]
|
114
|
+
|
115
|
+
ticket = @client.create_ticket({
|
116
|
+
description: "Test ticket #{i + 1} for filtering",
|
117
|
+
status: status,
|
118
|
+
priority: priority,
|
119
|
+
assigned_to: priority == "CRITICAL" ? "URGENT_TEAM" : "USER_#{i + 1}"
|
120
|
+
})
|
121
|
+
|
122
|
+
@logger.info { "Created ticket #{ticket['ticket_id']} with status #{status}, priority #{priority}" }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Run the automation if this file is executed directly
|
128
|
+
DefectExportAutomation.new.run_all_scenarios if __FILE__ == $PROGRAM_NAME
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ticket/replicator/row_loader"
|
4
|
+
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
class FileLoader
|
8
|
+
class LoadError < StandardError; end
|
9
|
+
|
10
|
+
def self.run_on(jira_project, file_path)
|
11
|
+
log.info { "Loading #{file_path} into #{jira_project.project_key}..." }
|
12
|
+
|
13
|
+
new(jira_project, file_path).run
|
14
|
+
end
|
15
|
+
|
16
|
+
private_class_method :new
|
17
|
+
|
18
|
+
attr_reader :jira_project, :extracted_path
|
19
|
+
|
20
|
+
def initialize(jira_project, extracted_path)
|
21
|
+
@jira_project = jira_project
|
22
|
+
@extracted_path = extracted_path
|
23
|
+
end
|
24
|
+
|
25
|
+
FIRST_DATA_ROW_LINE_NUMBER = 2
|
26
|
+
def run
|
27
|
+
rows.each_with_index do |row, index|
|
28
|
+
line_number = index + FIRST_DATA_ROW_LINE_NUMBER
|
29
|
+
RowLoader.run_on(jira_project, row)
|
30
|
+
rescue StandardError => e
|
31
|
+
message = <<~EOERROR
|
32
|
+
#{extracted_path}:#{line_number}: error while loading row:
|
33
|
+
#{e.message}:
|
34
|
+
#{row.inspect}
|
35
|
+
EOERROR
|
36
|
+
|
37
|
+
raise LoadError, message
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def rows
|
42
|
+
CSV.read(extracted_path, headers: true, header_converters: :symbol)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ticket/replicator"
|
4
|
+
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
class FileReplicator
|
8
|
+
def self.run_on(replicator, extracted_path)
|
9
|
+
new(replicator, extracted_path).run
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load(replicator, transformed_path)
|
13
|
+
FileLoader.run_on(replicator.jira_project, transformed_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.transform(replicator, extracted_path)
|
17
|
+
new(replicator, extracted_path).transform
|
18
|
+
end
|
19
|
+
|
20
|
+
private_class_method :new
|
21
|
+
|
22
|
+
attr_reader :replicator, :extracted_path
|
23
|
+
|
24
|
+
def initialize(replicator, extracted_path)
|
25
|
+
@replicator = replicator
|
26
|
+
@extracted_path = extracted_path
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
transform
|
31
|
+
load
|
32
|
+
delete_transformed_file
|
33
|
+
archive_extracted_file
|
34
|
+
end
|
35
|
+
|
36
|
+
def load
|
37
|
+
self.class.load(replicator, transformed_path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def transform
|
41
|
+
FileTransformer.run_on(extracted_path, transformed_path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_transformed_file
|
45
|
+
FileUtils.rm(transformed_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def archive_extracted_file
|
49
|
+
FileUtils.mv(extracted_path, archived_path)
|
50
|
+
end
|
51
|
+
|
52
|
+
def transformed_path
|
53
|
+
@transformed_path ||=
|
54
|
+
extracted_path.sub(/#{Regexp.escape(replicator.extracted_folder)}/, replicator.transformed_folder)
|
55
|
+
.sub(/\.xlsx$/, ".csv")
|
56
|
+
end
|
57
|
+
|
58
|
+
def archived_path
|
59
|
+
File.join(replicator.archived_folder, "#{timestamp}.#{File.basename(extracted_path)}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def timestamp
|
63
|
+
Jira::Auto::Tool::Helpers::OverridableTime.now.strftime("%Y-%m-%d.%Hh%Mm%S")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ticket
|
4
|
+
class Replicator
|
5
|
+
class FileTransformer
|
6
|
+
class ForCSV < FileTransformer
|
7
|
+
CSVReaderError = Class.new(StandardError)
|
8
|
+
|
9
|
+
CSV_FIRST_LINE_NUMBER = 1
|
10
|
+
|
11
|
+
def extracted_rows
|
12
|
+
CSV.read(extracted_path, headers: true, header_converters: :downcase)
|
13
|
+
rescue StandardError => e
|
14
|
+
raise CSVReaderError, <<~EOERRORMSG
|
15
|
+
#{extracted_path}:#{CSV_FIRST_LINE_NUMBER}: error while reading CSV file:
|
16
|
+
#{e.message}
|
17
|
+
EOERRORMSG
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "creek"
|
4
|
+
|
5
|
+
module Ticket
|
6
|
+
class Replicator
|
7
|
+
class FileTransformer
|
8
|
+
class ForXLSX < FileTransformer
|
9
|
+
class XLSXReaderError < StandardError; end
|
10
|
+
|
11
|
+
XLS_FIRST_LINE_NUMBER = 1
|
12
|
+
|
13
|
+
def extracted_rows
|
14
|
+
rows = sheet.simple_rows.to_a
|
15
|
+
|
16
|
+
headers = rows.shift.values.collect(&:downcase)
|
17
|
+
|
18
|
+
rows.collect { |row| headers.zip(row.values).to_h }
|
19
|
+
rescue StandardError => e
|
20
|
+
raise XLSXReaderError, <<~EOERRORMSG
|
21
|
+
#{extracted_path}:#{XLS_FIRST_LINE_NUMBER}: error while reading XLSX file:
|
22
|
+
#{e.message}
|
23
|
+
EOERRORMSG
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def sheet
|
29
|
+
Creek::Book.new(extracted_path, convert_numerics: false).sheets.first
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "row_transformer"
|
4
|
+
require "csv"
|
5
|
+
require "fileutils"
|
6
|
+
require "ticket/replicator"
|
7
|
+
require_relative "file_transformer/for_csv"
|
8
|
+
require_relative "file_transformer/for_xlsx"
|
9
|
+
|
10
|
+
module Ticket
|
11
|
+
class Replicator
|
12
|
+
class FileTransformer
|
13
|
+
class TransformError < StandardError; end
|
14
|
+
|
15
|
+
def self.run_on(extracted_path, transformed_path)
|
16
|
+
run_class(extracted_path).send(:new, extracted_path, transformed_path).run
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.run_class(extracted_path)
|
20
|
+
"#{name}::For#{file_extension_without_dot(extracted_path).upcase}".constantize
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.file_extension_without_dot(extracted_path)
|
24
|
+
File.extname(extracted_path).gsub(".", "")
|
25
|
+
end
|
26
|
+
|
27
|
+
private_class_method :new
|
28
|
+
|
29
|
+
attr_reader :extracted_path, :transformed_path
|
30
|
+
|
31
|
+
def initialize(extracted_path, transformed_path)
|
32
|
+
@extracted_path = extracted_path
|
33
|
+
@transformed_path = transformed_path
|
34
|
+
end
|
35
|
+
|
36
|
+
def run
|
37
|
+
CSV.open(transformed_path, "wb", force_quotes: true) do |csv|
|
38
|
+
csv << transformed_headers
|
39
|
+
transformed_rows.each { |row| csv << row }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def extracted_rows
|
44
|
+
raise NotImplementedError, "You must implement this method in your subclass"
|
45
|
+
end
|
46
|
+
|
47
|
+
FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER = 2
|
48
|
+
private_constant :FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER
|
49
|
+
|
50
|
+
def transformed_rows
|
51
|
+
row_index = FIRST_EXTRACTED_DATA_ROW_LINE_NUMBER
|
52
|
+
extracted_rows.collect do |row|
|
53
|
+
RowTransformer.run_on(row)
|
54
|
+
rescue StandardError => e
|
55
|
+
raise TransformError, <<~EOERRORMSG
|
56
|
+
#{extracted_path}:#{row_index}: error while transforming row:
|
57
|
+
#{e.message}:
|
58
|
+
#{row.inspect}
|
59
|
+
EOERRORMSG
|
60
|
+
ensure
|
61
|
+
row_index += 1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def transformed_headers
|
66
|
+
RowTransformer.fields_to_transform.collect(&:to_s)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "jira/resource/issue"
|
4
|
+
require "jira/auto/tool/helpers/pagination"
|
5
|
+
require "ticket/replicator/row_transformer"
|
6
|
+
require "ticket/replicator/ticket"
|
7
|
+
|
8
|
+
module Ticket
|
9
|
+
class Replicator
|
10
|
+
class JiraProject
|
11
|
+
attr_reader :jira_auto_tool
|
12
|
+
|
13
|
+
def initialize(jira_auto_tool)
|
14
|
+
@jira_auto_tool = jira_auto_tool
|
15
|
+
end
|
16
|
+
|
17
|
+
def jira_client
|
18
|
+
jira_auto_tool.jira_client
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete_all_tickets_from_the_expected_type
|
22
|
+
replicated_tickets.each(&:delete)
|
23
|
+
end
|
24
|
+
|
25
|
+
def project_key
|
26
|
+
@project_key ||= ENV.fetch("TICKET_REPLICATOR_JIRA_PROJECT_KEY")
|
27
|
+
end
|
28
|
+
|
29
|
+
def ticket_type_name
|
30
|
+
@ticket_type_name ||= ENV.fetch("TICKET_REPLICATOR_JIRA_TICKET_TYPE_NAME")
|
31
|
+
end
|
32
|
+
|
33
|
+
def replicated_tickets
|
34
|
+
@replicated_tickets ||= all_replicated_ticket_pages.to_h { |ticket| [ticket.source_id, ticket] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def all_tickets(all_ticket_jql = "project = #{project_key}")
|
38
|
+
all_jira_tickets(all_ticket_jql).collect { |ticket| Replicator::Ticket.new(jira_auto_tool, ticket) }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def all_jira_tickets(all_ticket_jql)
|
44
|
+
Jira::Auto::Tool::Helpers::Pagination.fetch_all_object_pages(:snake_case) do |pagination_options|
|
45
|
+
jira_client.Issue.jql(all_ticket_jql, pagination_options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def all_replicated_ticket_pages
|
50
|
+
all_tickets(replicated_ticket_jql).find_all(&:replicated?).sort_by(&:source_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def replicated_ticket_jql
|
54
|
+
<<-EOJQL
|
55
|
+
project = #{project_key}
|
56
|
+
AND issuetype = #{ticket_type_name.inspect}
|
57
|
+
EOJQL
|
58
|
+
end
|
59
|
+
|
60
|
+
def jira_client_project
|
61
|
+
@jira_client_project ||= jira_client.Project.find(project_key)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require "ticket/replicator"
|
5
|
+
|
6
|
+
module Ticket
|
7
|
+
class Replicator
|
8
|
+
class ReplicatedSummary
|
9
|
+
class ParseError < StandardError; end
|
10
|
+
|
11
|
+
TICKET_ID_PREFIX = "SMAN-"
|
12
|
+
TICKET_ID_PREFIX_SEPARATOR = " | "
|
13
|
+
TICKET_ID_PREFIX_REGEX =
|
14
|
+
/^#{Regexp.escape(TICKET_ID_PREFIX)}(?<source_id>.+?)#{Regexp.escape(TICKET_ID_PREFIX_SEPARATOR)}
|
15
|
+
(?<source_summary>.+)$/x
|
16
|
+
|
17
|
+
def self.build(source_id, source_summary)
|
18
|
+
new(source_id, source_summary)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.match?(replicated_summary_string)
|
22
|
+
TICKET_ID_PREFIX_REGEX.match(replicated_summary_string)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse(replicated_summary_string)
|
26
|
+
match_result = match?(replicated_summary_string)
|
27
|
+
|
28
|
+
unless match_result
|
29
|
+
raise ParseError,
|
30
|
+
"#{replicated_summary_string.inspect}: missing expected prefix format: " \
|
31
|
+
"not matching #{TICKET_ID_PREFIX_REGEX.inspect}."
|
32
|
+
end
|
33
|
+
|
34
|
+
new(match_result[:source_id], match_result[:source_summary])
|
35
|
+
end
|
36
|
+
|
37
|
+
MATCH_ANY_CHARACTER_IN_JQL = "%"
|
38
|
+
|
39
|
+
def self.jql_pattern
|
40
|
+
[
|
41
|
+
TICKET_ID_PREFIX,
|
42
|
+
TICKET_ID_PREFIX_SEPARATOR
|
43
|
+
].join(MATCH_ANY_CHARACTER_IN_JQL)
|
44
|
+
end
|
45
|
+
|
46
|
+
private_class_method :new
|
47
|
+
|
48
|
+
attr_reader :source_summary, :source_id
|
49
|
+
|
50
|
+
def initialize(source_id, source_summary)
|
51
|
+
@source_id = source_id
|
52
|
+
@source_summary = source_summary
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
[
|
57
|
+
transformed_summary_prefix,
|
58
|
+
source_summary
|
59
|
+
].join
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def transformed_summary_prefix
|
65
|
+
[
|
66
|
+
TICKET_ID_PREFIX,
|
67
|
+
source_id,
|
68
|
+
TICKET_ID_PREFIX_SEPARATOR
|
69
|
+
].join
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|