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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/Guardfile +158 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +136 -0
  9. data/Rakefile +23 -0
  10. data/bin/ticket-replicator +67 -0
  11. data/config/examples/ticket-replicator.mappings.yml +54 -0
  12. data/cucumber.yml +7 -0
  13. data/features/extract-sap-solution-manager-defect-tickets.feature +45 -0
  14. data/features/load_tickets_in_jira.feature +129 -0
  15. data/features/setup_ticket_replicator.feature +85 -0
  16. data/features/step_definitions/anonymized_sample.xlsx +0 -0
  17. data/features/step_definitions/anonymized_sample.xlsx:Zone.Identifier +3 -0
  18. data/features/step_definitions/execution_context_steps.rb +13 -0
  19. data/features/step_definitions/extract_defect_tickets_from_sap_solution_manager_steps.rb.rb +29 -0
  20. data/features/step_definitions/load_tickets_in_jira_steps.rb +47 -0
  21. data/features/step_definitions/transform_solution_manager_tickets_steps.rb +21 -0
  22. data/features/support/10.setup_cucumber.rb +10 -0
  23. data/features/support/env.rb +15 -0
  24. data/features/support/hooks.rb +13 -0
  25. data/features/support/manage_mock_sap_solution_manager.rb.DISABLED +12 -0
  26. data/features/support/mocks/mock_defect_ticket_server.rb.DISABLED +251 -0
  27. data/features/support/setup_rspec.rb +15 -0
  28. data/features/support/setup_simplecov.rb +5 -0
  29. data/features/transform-solution-manager-tickets-into-jira-loadable-tickets.feature +313 -0
  30. data/features/transform_and_load_extracted_ticket_queue.feature +121 -0
  31. data/lib/tasks/version.rake +55 -0
  32. data/lib/ticket/replicator/defect_export_automation.rb.DISABLED +128 -0
  33. data/lib/ticket/replicator/file_loader.rb +46 -0
  34. data/lib/ticket/replicator/file_replicator.rb +67 -0
  35. data/lib/ticket/replicator/file_transformer/for_csv.rb +22 -0
  36. data/lib/ticket/replicator/file_transformer/for_xlsx.rb +34 -0
  37. data/lib/ticket/replicator/file_transformer.rb +70 -0
  38. data/lib/ticket/replicator/jira_project.rb +65 -0
  39. data/lib/ticket/replicator/replicated_summary.rb +73 -0
  40. data/lib/ticket/replicator/row_loader.rb +109 -0
  41. data/lib/ticket/replicator/row_transformer.rb +126 -0
  42. data/lib/ticket/replicator/s_a_p_solution_manager_client.rb.DISABLED +169 -0
  43. data/lib/ticket/replicator/setup.rb +49 -0
  44. data/lib/ticket/replicator/ticket.rb +70 -0
  45. data/lib/ticket/replicator/ticket_status_transitioner.rb +45 -0
  46. data/lib/ticket/replicator/version.rb +7 -0
  47. data/lib/ticket/replicator.rb +90 -0
  48. data/sig/ticket/replicator.rbs +6 -0
  49. data/spec/spec_helper.rb +19 -0
  50. data/spec/ticket/replicator/file_loader_spec.rb +77 -0
  51. data/spec/ticket/replicator/file_replicator_spec.rb +153 -0
  52. data/spec/ticket/replicator/file_transformer/for_csv_spec.rb +52 -0
  53. data/spec/ticket/replicator/file_transformer/for_xlsx_spec.rb +52 -0
  54. data/spec/ticket/replicator/file_transformer_spec.rb +83 -0
  55. data/spec/ticket/replicator/jira_project_spec.rb +127 -0
  56. data/spec/ticket/replicator/replicated_summary_spec.rb +70 -0
  57. data/spec/ticket/replicator/row_loader_spec.rb +245 -0
  58. data/spec/ticket/replicator/row_transformer_spec.rb +234 -0
  59. data/spec/ticket/replicator/setup_spec.rb +80 -0
  60. data/spec/ticket/replicator/ticket_spec.rb +244 -0
  61. data/spec/ticket/replicator/ticket_status_transitioner_spec.rb +123 -0
  62. data/spec/ticket/replicator_spec.rb +137 -0
  63. data/transformed_file1 +1 -0
  64. 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