jira-auto-tool 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 (139) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +291 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/Guardfile +105 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +159 -0
  9. data/Rakefile +20 -0
  10. data/bin/jira-auto-tool +57 -0
  11. data/bin/jira-auto-tool.bat +2 -0
  12. data/bin/setup +8 -0
  13. data/bin/setup-dev-win.bat +3 -0
  14. data/cucumber.yml +7 -0
  15. data/features/align_sprint_time_in_dates.feature +33 -0
  16. data/features/assign_tickets_to_team_sprints.feature +73 -0
  17. data/features/cache_boards.feature +24 -0
  18. data/features/control_http_request_rate_limit.feature +17 -0
  19. data/features/create_sprints_using_existing_ones_as_reference.feature +79 -0
  20. data/features/list_boards.feature +12 -0
  21. data/features/list_project_fields.feature +89 -0
  22. data/features/list_sprint_prefixes.feature +77 -0
  23. data/features/quarterly_add_sprints_using_existing_ones_as_a_reference.feature +71 -0
  24. data/features/quarterly_create_sprints_until_specific_date.feature +75 -0
  25. data/features/quarterly_rename_sprints.feature +179 -0
  26. data/features/rename_sprints.feature +203 -0
  27. data/features/self_documented_command_line.feature +15 -0
  28. data/features/sprint_filtering.feature +111 -0
  29. data/features/step_definitions/execution_context_steps.rb +33 -0
  30. data/features/step_definitions/jira_board_steps.rb +102 -0
  31. data/features/step_definitions/jira_ticket_steps.rb +63 -0
  32. data/features/support/10.setup_cucumber.rb +10 -0
  33. data/features/support/env.rb +25 -0
  34. data/features/support/hooks.rb +25 -0
  35. data/features/support/setup_rspec.rb +14 -0
  36. data/features/support/setup_simplecov.rb +5 -0
  37. data/features/update_sprint_end_date_and_shift_following_ones.feature +52 -0
  38. data/lib/jira/auto/tool/board/cache.rb +67 -0
  39. data/lib/jira/auto/tool/board/unavailable_board.rb +36 -0
  40. data/lib/jira/auto/tool/board.rb +105 -0
  41. data/lib/jira/auto/tool/board_controller/options.rb +32 -0
  42. data/lib/jira/auto/tool/board_controller.rb +88 -0
  43. data/lib/jira/auto/tool/common_options.rb +37 -0
  44. data/lib/jira/auto/tool/config/options.rb +19 -0
  45. data/lib/jira/auto/tool/config.rb +64 -0
  46. data/lib/jira/auto/tool/fetch_custom_field_options.rb +47 -0
  47. data/lib/jira/auto/tool/field.rb +59 -0
  48. data/lib/jira/auto/tool/field_controller.rb +50 -0
  49. data/lib/jira/auto/tool/field_option.rb +35 -0
  50. data/lib/jira/auto/tool/get_createmeta_for_project.rb +24 -0
  51. data/lib/jira/auto/tool/helpers/environment_based_value.rb +96 -0
  52. data/lib/jira/auto/tool/helpers/option_parser.rb +16 -0
  53. data/lib/jira/auto/tool/helpers/overridable_time.rb +18 -0
  54. data/lib/jira/auto/tool/helpers/pagination.rb +50 -0
  55. data/lib/jira/auto/tool/jira_http_options.rb +20 -0
  56. data/lib/jira/auto/tool/next_sprint_creator.rb +60 -0
  57. data/lib/jira/auto/tool/performer/options.rb +76 -0
  58. data/lib/jira/auto/tool/performer/planning_increment_sprint_creator.rb +42 -0
  59. data/lib/jira/auto/tool/performer/prefix_sprint_updater.rb +42 -0
  60. data/lib/jira/auto/tool/performer/quarterly_sprint_renamer/next_name_generator.rb +60 -0
  61. data/lib/jira/auto/tool/performer/quarterly_sprint_renamer.rb +19 -0
  62. data/lib/jira/auto/tool/performer/sprint_end_date_updater.rb +55 -0
  63. data/lib/jira/auto/tool/performer/sprint_renamer/keep_same_name_generator.rb +19 -0
  64. data/lib/jira/auto/tool/performer/sprint_renamer/next_name_generator.rb +47 -0
  65. data/lib/jira/auto/tool/performer/sprint_renamer.rb +55 -0
  66. data/lib/jira/auto/tool/performer/sprint_time_in_dates_aligner.rb +52 -0
  67. data/lib/jira/auto/tool/project/options.rb +22 -0
  68. data/lib/jira/auto/tool/project/ticket_fields.rb +70 -0
  69. data/lib/jira/auto/tool/project.rb +40 -0
  70. data/lib/jira/auto/tool/rate_limited_jira_client.rb +50 -0
  71. data/lib/jira/auto/tool/request_builder/field_context_fetcher.rb +78 -0
  72. data/lib/jira/auto/tool/request_builder/field_option_fetcher.rb +54 -0
  73. data/lib/jira/auto/tool/request_builder/get.rb +29 -0
  74. data/lib/jira/auto/tool/request_builder/sprint_creator.rb +112 -0
  75. data/lib/jira/auto/tool/request_builder/sprint_state_updater.rb +60 -0
  76. data/lib/jira/auto/tool/request_builder.rb +89 -0
  77. data/lib/jira/auto/tool/setup_logging.rb +35 -0
  78. data/lib/jira/auto/tool/sprint/name.rb +105 -0
  79. data/lib/jira/auto/tool/sprint/prefix.rb +66 -0
  80. data/lib/jira/auto/tool/sprint.rb +183 -0
  81. data/lib/jira/auto/tool/sprint_controller/options.rb +61 -0
  82. data/lib/jira/auto/tool/sprint_controller.rb +152 -0
  83. data/lib/jira/auto/tool/sprint_state_controller.rb +58 -0
  84. data/lib/jira/auto/tool/team.rb +23 -0
  85. data/lib/jira/auto/tool/team_sprint_prefix_mapper/options.rb +27 -0
  86. data/lib/jira/auto/tool/team_sprint_prefix_mapper.rb +62 -0
  87. data/lib/jira/auto/tool/team_sprint_ticket_dispatcher.rb +76 -0
  88. data/lib/jira/auto/tool/ticket.rb +110 -0
  89. data/lib/jira/auto/tool/until_date.rb +68 -0
  90. data/lib/jira/auto/tool/version.rb +9 -0
  91. data/lib/jira/auto/tool.rb +216 -0
  92. data/sig/jira/sprint/tool.rbs +8 -0
  93. data/spec/jira/auto/tool/board/cache_spec.rb +179 -0
  94. data/spec/jira/auto/tool/board/unavailable_board_spec.rb +34 -0
  95. data/spec/jira/auto/tool/board_controller/options_spec.rb +52 -0
  96. data/spec/jira/auto/tool/board_controller_spec.rb +154 -0
  97. data/spec/jira/auto/tool/board_spec.rb +163 -0
  98. data/spec/jira/auto/tool/common_options_spec.rb +49 -0
  99. data/spec/jira/auto/tool/config_spec.rb +108 -0
  100. data/spec/jira/auto/tool/field_controller_spec.rb +121 -0
  101. data/spec/jira/auto/tool/field_option_spec.rb +42 -0
  102. data/spec/jira/auto/tool/field_spec.rb +99 -0
  103. data/spec/jira/auto/tool/helpers/environment_based_value_spec.rb +21 -0
  104. data/spec/jira/auto/tool/helpers/option_parser_spec.rb +21 -0
  105. data/spec/jira/auto/tool/helpers/overridable_time_spec.rb +43 -0
  106. data/spec/jira/auto/tool/helpers/pagination_spec.rb +72 -0
  107. data/spec/jira/auto/tool/jira_http_options_spec.rb +32 -0
  108. data/spec/jira/auto/tool/next_sprint_creator_spec.rb +85 -0
  109. data/spec/jira/auto/tool/performer/option_spec.rb +55 -0
  110. data/spec/jira/auto/tool/performer/planning_increment_sprint_creator_spec.rb +62 -0
  111. data/spec/jira/auto/tool/performer/prefix_sprint_updater_spec.rb +35 -0
  112. data/spec/jira/auto/tool/performer/quarterly_sprint_renamer/next_name_generator_spec.rb +175 -0
  113. data/spec/jira/auto/tool/performer/quarterly_sprint_renamer_spec.rb +239 -0
  114. data/spec/jira/auto/tool/performer/sprint_end_date_updater_spec.rb +90 -0
  115. data/spec/jira/auto/tool/performer/sprint_renamer/keep_same_name_generator_spec.rb +12 -0
  116. data/spec/jira/auto/tool/performer/sprint_renamer/next_name_generator_spec.rb +129 -0
  117. data/spec/jira/auto/tool/performer/sprint_renamer_spec.rb +240 -0
  118. data/spec/jira/auto/tool/performer/sprint_time_in_dates_aligner_spec.rb +132 -0
  119. data/spec/jira/auto/tool/project/ticket_fields_spec.rb +390 -0
  120. data/spec/jira/auto/tool/project_spec.rb +31 -0
  121. data/spec/jira/auto/tool/rate_limited_jira_client_spec.rb +82 -0
  122. data/spec/jira/auto/tool/request_builder/field_context_fetcher_spec.rb +54 -0
  123. data/spec/jira/auto/tool/request_builder/field_option_fetcher_spec.rb +64 -0
  124. data/spec/jira/auto/tool/request_builder/get_spec.rb +40 -0
  125. data/spec/jira/auto/tool/request_builder/sprint_creator_spec.rb +179 -0
  126. data/spec/jira/auto/tool/request_builder/sprint_state_updater_spec.rb +31 -0
  127. data/spec/jira/auto/tool/request_builder_spec.rb +73 -0
  128. data/spec/jira/auto/tool/sprint/name_spec.rb +101 -0
  129. data/spec/jira/auto/tool/sprint/prefix_spec.rb +207 -0
  130. data/spec/jira/auto/tool/sprint_controller_spec.rb +406 -0
  131. data/spec/jira/auto/tool/sprint_spec.rb +309 -0
  132. data/spec/jira/auto/tool/team_spec.rb +21 -0
  133. data/spec/jira/auto/tool/team_sprint_prefix_mapper_spec.rb +97 -0
  134. data/spec/jira/auto/tool/team_sprint_ticket_dispatcher_spec.rb +232 -0
  135. data/spec/jira/auto/tool/ticket_spec.rb +116 -0
  136. data/spec/jira/auto/tool/until_date_spec.rb +80 -0
  137. data/spec/jira/auto/tool_spec.rb +458 -0
  138. data/spec/spec_helper.rb +42 -0
  139. metadata +368 -0
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "jira-ruby"
5
+
6
+ require_relative "sprint_state_controller"
7
+
8
+ module Jira
9
+ module Auto
10
+ class Tool
11
+ class Sprint < SimpleDelegator
12
+ include Comparable
13
+
14
+ def self.date_for_save(date)
15
+ case date
16
+ when Time, Date, DateTime
17
+ date
18
+ when String
19
+ Time.parse(date)
20
+ else
21
+ raise ArgumentError, "#{date.inspect} (#{date.class}), date must be a Time, Date, DateTime or a String"
22
+ end
23
+ .utc.iso8601
24
+ end
25
+
26
+ attr_reader :jira_sprint, :tool
27
+
28
+ def initialize(tool, jira_sprint)
29
+ super(jira_sprint)
30
+ @tool = tool
31
+ @jira_sprint = jira_sprint
32
+ end
33
+
34
+ def id
35
+ @jira_sprint.id
36
+ end
37
+
38
+ def name
39
+ @jira_sprint.name
40
+ end
41
+
42
+ def length_in_days
43
+ (end_date - start_date) / 1.day
44
+ end
45
+
46
+ def start_date
47
+ get_optional_date :startDate
48
+ end
49
+
50
+ def start_date=(date)
51
+ jira_sprint.attrs["startDate"] = self.class.date_for_save(date)
52
+ end
53
+
54
+ def start_date?
55
+ start_date != UNDEFINED_DATE
56
+ end
57
+
58
+ def end_date
59
+ get_optional_date :endDate
60
+ end
61
+
62
+ def end_date=(date)
63
+ jira_sprint.attrs["endDate"] = self.class.date_for_save(date)
64
+ end
65
+
66
+ def end_date?
67
+ end_date != UNDEFINED_DATE
68
+ end
69
+
70
+ UNDEFINED_DATE = Time.new(1970, 1, 1, 0, 0, 0, "UTC")
71
+
72
+ def get_optional_date(jira_field_id)
73
+ return UNDEFINED_DATE unless jira_sprint.respond_to?(jira_field_id)
74
+
75
+ parse_date(jira_sprint.send(jira_field_id))
76
+ end
77
+
78
+ def missing_dates?
79
+ start_date == UNDEFINED_DATE || end_date == UNDEFINED_DATE
80
+ end
81
+
82
+ def rename_to(new_name)
83
+ return if new_name == name || closed?
84
+
85
+ jira_sprint.attrs["name"] = new_name
86
+
87
+ save
88
+ end
89
+
90
+ def save
91
+ remove_attributes_that_causes_errors_on_save
92
+
93
+ jira_sprint.save!
94
+ end
95
+
96
+ def remove_attributes_that_causes_errors_on_save
97
+ jira_sprint.attrs.delete("rapidview_id")
98
+ end
99
+
100
+ def state
101
+ @jira_sprint.state
102
+ end
103
+
104
+ def closed?
105
+ state == SprintStateController::SprintState::CLOSED
106
+ end
107
+
108
+ def origin_board_id
109
+ @jira_sprint.originBoardId
110
+ end
111
+
112
+ def index_in_quarter
113
+ parsed_name.index_in_quarter
114
+ end
115
+
116
+ def board
117
+ @board ||= Board.find_by_id(tool, origin_board_id)
118
+ end
119
+
120
+ def jira_client
121
+ @jira_sprint.client
122
+ end
123
+
124
+ def name_prefix
125
+ parsed_name.prefix
126
+ end
127
+
128
+ def parsed_name
129
+ Name.parse(name)
130
+ end
131
+
132
+ def <=>(other)
133
+ comparison_values(self) <=> comparison_values(other)
134
+ end
135
+
136
+ def to_s
137
+ "name = #{name}, start_date = #{start_date}, end_date = #{end_date}, length_in_days = #{length_in_days}"
138
+ end
139
+
140
+ def self.to_table_row_field_names
141
+ %i[id name length_in_days start_date end_date]
142
+ end
143
+
144
+ def self.to_table_row_header(without_board_information: false)
145
+ header = to_table_row_field_names.collect { |field| field.to_s.titleize }
146
+
147
+ header.concat(Board.to_table_row_header.collect { |field| "Board #{field}" }) unless without_board_information
148
+
149
+ header
150
+ end
151
+
152
+ def to_table_row(without_board_information: false)
153
+ row = self.class.to_table_row_field_names.collect { |field| send(field) }
154
+
155
+ row.concat(board.to_table_row) unless without_board_information
156
+
157
+ row
158
+ rescue StandardError => e
159
+ raise e.class, "#{e.class}: sprint #{name.inspect}: #{e.message}:\n#{inspect}"
160
+ end
161
+
162
+ private
163
+
164
+ def comparison_values(object)
165
+ comparison_fields(object).collect { |field| object.send(field) }
166
+ end
167
+
168
+ def comparison_fields(object)
169
+ %i[start_date end_date] +
170
+ if Name.respects_naming_convention?(object.name)
171
+ [:parsed_name]
172
+ else
173
+ [:name]
174
+ end
175
+ end
176
+
177
+ def parse_date(date)
178
+ Time.parse(date).utc
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira/auto/tool/until_date"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class SprintController
9
+ class Options
10
+ def self.add(tool, parser)
11
+ add_quarterly_sprint_adding_options(parser, tool)
12
+ add_sprint_listing_options(parser, tool)
13
+ add_sprint_prefix_listing_options(parser, tool)
14
+ end
15
+
16
+ def self.add_quarterly_sprint_adding_options(parser, tool)
17
+ parser.on("--quarterly-sprint-add-one", "--qsao",
18
+ "Create a follow up sprint for each of the existing unclosed sprint prefixes") do
19
+ tool.sprint_controller.quarterly_add_one_sprint_for_each_unclosed_sprint_prefix
20
+ end
21
+
22
+ parser.accept(UntilDate) { |until_date_string| UntilDate.new(until_date_string) }
23
+
24
+ parser.on("--quarterly-sprint-add-until=DATE", "--qsau", UntilDate,
25
+ "Create sprints until date is included for each of the unclosed sprint prefixes") do |until_date|
26
+ tool.sprint_controller.quarterly_add_sprints_until(until_date)
27
+ end
28
+ end
29
+
30
+ def self.add_sprint_listing_options(parser, tool)
31
+ parser.on("--sprint-list",
32
+ "List sprints. The output may be controlled via the ART_PREFIX_REGEX environment variable") do
33
+ tool.sprint_controller.list_sprints
34
+ end
35
+
36
+ parser.on("--sprint-list-without-board-info",
37
+ "List sprints without the board information. " \
38
+ "The output may be controlled via the ART_PREFIX_REGEX environment variable") do
39
+ tool.sprint_controller.list_sprints(without_board_information: true)
40
+ end
41
+ end
42
+
43
+ def self.add_sprint_prefix_listing_options(parser, tool)
44
+ parser.section_header "Sprint Prefix"
45
+
46
+ parser.on("--sprint-prefix-list",
47
+ "List sprints. The output may be controlled via the ART_PREFIX_REGEX environment variable") do
48
+ tool.sprint_controller.list_sprint_prefixes
49
+ end
50
+
51
+ parser.on("--sprint-prefix-list-without-board-info",
52
+ "List sprints without the board information. " \
53
+ "The output may be controlled via the ART_PREFIX_REGEX environment variable") do
54
+ tool.sprint_controller.list_sprint_prefixes(without_board_information: true)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "next_sprint_creator"
4
+ require_relative "sprint"
5
+ require_relative "sprint/prefix"
6
+ require_relative "sprint_state_controller"
7
+ require_relative "helpers/pagination"
8
+
9
+ module Jira
10
+ module Auto
11
+ class Tool
12
+ class SprintController
13
+ attr_accessor :tool, :board
14
+
15
+ def initialize(tool, board)
16
+ @tool = tool
17
+ @board = board
18
+ end
19
+
20
+ def quarterly_add_sprints_until(until_date)
21
+ unclosed_sprint_prefixes.each { |prefix| prefix.quarterly_add_sprints_until(until_date) }
22
+ end
23
+
24
+ def quarterly_add_one_sprint_for_each_unclosed_sprint_prefix
25
+ exit_with_board_warning "No sprint added since no reference sprint was found!" unless sprint_exist?
26
+ unless unclosed_sprint_exist?
27
+ exit_with_board_warning "No sprint added since no unclosed reference sprint was found!"
28
+ end
29
+
30
+ unclosed_sprint_prefixes.each(&:add_sprint_following_last_one)
31
+ end
32
+
33
+ SUCCESSFUL_EXECUTION_EXIT_CODE = 0
34
+ UNSUCCESSFUL_EXECUTION_EXIT_CODE = 1
35
+
36
+ def exit_with_board_warning(message, exit_code = SUCCESSFUL_EXECUTION_EXIT_CODE)
37
+ log.warn { "Jira board '#{board.name}': #{message}" }
38
+ exit(exit_code)
39
+ end
40
+
41
+ def unclosed_sprint_prefixes
42
+ @unclosed_sprint_prefixes ||= calculate_unclosed_sprint_prefixes.sort_by(&:name)
43
+ end
44
+
45
+ def unclosed_sprint_exist?
46
+ !unclosed_sprints.empty?
47
+ end
48
+
49
+ def sprint_exist?
50
+ !sprints.empty?
51
+ end
52
+
53
+ def list_sprint_prefixes(without_board_information: false)
54
+ list_object(Sprint::Prefix, :unclosed_sprint_prefixes,
55
+ "Sprint Prefixes With Corresponding Last Sprints",
56
+ without_board_information: without_board_information)
57
+ end
58
+
59
+ def list_sprints(without_board_information: false)
60
+ list_object(Sprint, :unclosed_sprints, "Matching Sprints",
61
+ without_board_information: without_board_information)
62
+ end
63
+
64
+ def list_object(object_class, object_method, title, without_board_information: false)
65
+ table_row_header = object_class.to_table_row_header(without_board_information: without_board_information)
66
+
67
+ rows = send(object_method).collect do |sprint|
68
+ sprint.to_table_row(without_board_information: without_board_information)
69
+ end
70
+
71
+ table = Terminal::Table.new(
72
+ title: title,
73
+ headings: table_row_header,
74
+ rows: rows
75
+ )
76
+
77
+ puts table
78
+ end
79
+
80
+ def sprints
81
+ jira_sprints.collect { |jira_sprint| Sprint.new(tool, jira_sprint) }
82
+ .uniq(&:id)
83
+ .sort
84
+ end
85
+
86
+ def jira_sprints
87
+ sprint_filter_string = tool.art_sprint_regex_defined? ? tool.art_sprint_regex : ""
88
+ sprint_filter_regex = Regexp.new(sprint_filter_string)
89
+
90
+ unfiltered_board_sprints.find_all { |sprint| sprint.name =~ sprint_filter_regex }
91
+ end
92
+
93
+ def unfiltered_board_sprints
94
+ all_board_sprints = []
95
+
96
+ sprint_compatible_boards.each do |board|
97
+ all_board_sprints.concat(unfiltered_jira_sprints(board))
98
+ end
99
+
100
+ all_board_sprints
101
+ end
102
+
103
+ def sprint_compatible_boards
104
+ boards.find_all(&:sprint_compatible?)
105
+ end
106
+
107
+ def boards
108
+ tool.board_controller.boards
109
+ end
110
+
111
+ def unfiltered_jira_sprints(board)
112
+ Helpers::Pagination.fetch_all_object_pages do |pagination_options|
113
+ fetch_jira_sprints(board, pagination_options)
114
+ end
115
+ end
116
+
117
+ def fetch_jira_sprints(board, options)
118
+ log.debug { "Fetching sprints from Jira (#{options.inspect})" }
119
+
120
+ fetched_sprints = board.jira_board.sprints(options)
121
+
122
+ log.debug do
123
+ "Board: #{board.name}: Fetched #sprints #{fetched_sprints.size} sprints from Jira: " \
124
+ "#{fetched_sprints.map(&:name).join(", ")}"
125
+ end
126
+
127
+ fetched_sprints
128
+ end
129
+
130
+ def unclosed_sprints
131
+ sprints.find_all { |sprint| sprint.state != SprintStateController::SprintState::CLOSED }
132
+ end
133
+
134
+ private
135
+
136
+ def calculate_unclosed_sprint_prefixes
137
+ unclosed_sprints.each_with_object({}) do |sprint, prefixes|
138
+ if Sprint::Name.respects_naming_convention?(sprint.name)
139
+ prefix = prefixes[sprint.name_prefix] ||= Sprint::Prefix.new(sprint.name_prefix)
140
+ prefix << sprint
141
+ else
142
+ log.warn do
143
+ "IGNORING sprint '#{sprint.name}' (board '#{board.name}'): " \
144
+ "it does not respect the naming conventions #{Sprint::Name::SPRINT_NAME_REGEX}."
145
+ end
146
+ end
147
+ end.values
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ module Auto
5
+ class Tool
6
+ class SprintStateController
7
+ attr_reader :jira_client, :sprint
8
+
9
+ def initialize(jira_client, sprint)
10
+ @jira_client = jira_client
11
+ @sprint = sprint
12
+ end
13
+
14
+ def transition_to(desired_state)
15
+ transition_state(desired_state)
16
+ end
17
+
18
+ module SprintState
19
+ ACTIVE = "active"
20
+ FUTURE = "future"
21
+ CLOSED = "closed"
22
+ end
23
+
24
+ STATE_TRANSITIONS =
25
+ {
26
+ SprintState::FUTURE => SprintState::ACTIVE,
27
+ SprintState::ACTIVE => SprintState::CLOSED
28
+ }.freeze
29
+
30
+ # TODO: - write unit tests
31
+ # TODO - fix infinite loop in case of invalid/in-existing state
32
+ def transition_state(desired_state)
33
+ current_state = sprint.state
34
+
35
+ loop do
36
+ log.debug do
37
+ "sprint_to_update = #{sprint.name}, current_state = #{current_state} desired_state = #{desired_state}"
38
+ end
39
+
40
+ break if current_state == desired_state
41
+
42
+ new_state = STATE_TRANSITIONS[current_state]
43
+
44
+ update_sprint_state(new_state)
45
+
46
+ current_state = new_state
47
+ end
48
+ end
49
+
50
+ def update_sprint_state(new_state)
51
+ RequestBuilder::SprintStateUpdater
52
+ .new(jira_client, sprint: sprint, new_state: new_state)
53
+ .run
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira/auto/tool"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class Team
9
+ def initialize(field_option)
10
+ @field_option = field_option
11
+ end
12
+
13
+ def name
14
+ @field_option.value
15
+ end
16
+
17
+ def id
18
+ @field_option.id
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jira/auto/tool/until_date"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class TeamSprintPrefixMapper
9
+ class Options
10
+ def self.add(tool, parser)
11
+ parser.section_header "Team Sprint Prefix Mapping"
12
+
13
+ parser.on("--team-sprint-mapping-list", "--tsm-list",
14
+ "List the sprint and team owning their content") do
15
+ tool.team_sprint_ticket_dispatcher.team_sprint_prefix_mapper.list_mappings
16
+ end
17
+
18
+ parser.on("--team-sprint-mapping-dispatch-tickets", "--tsm-dispatch",
19
+ "Dispatch tickets to sprint so the teams start timely working on them") do
20
+ tool.team_sprint_ticket_dispatcher.dispatch_tickets
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "terminal-table"
4
+
5
+ module Jira
6
+ module Auto
7
+ class Tool
8
+ class TeamSprintPrefixMapper
9
+ class NoMatchingTeamError < StandardError; end
10
+
11
+ class NoMatchingSprintPrefixError < StandardError; end
12
+
13
+ attr_reader :teams, :sprint_prefixes
14
+
15
+ def initialize(teams, sprint_prefixes)
16
+ @sprint_prefixes = sprint_prefixes
17
+ @teams = teams
18
+ end
19
+
20
+ def list_mappings
21
+ table = Terminal::Table.new(
22
+ title: "Team Sprint Mappings",
23
+ headings: ["Team", "Sprint Prefix"],
24
+ rows: teams.collect do |team|
25
+ [team, team_sprint_prefix_mappings.fetch(team, "!!__no matching sprint prefix__!!")]
26
+ end
27
+ )
28
+
29
+ puts table
30
+ end
31
+
32
+ def fetch_for(team_name)
33
+ team_sprint_prefix_mappings[team_name]
34
+ end
35
+
36
+ def fetch_for!(team_name)
37
+ fetch_for(team_name) or
38
+ raise NoMatchingSprintPrefixError,
39
+ no_matching_sprint_prefix_for_team_message(team_name)
40
+ end
41
+
42
+ def team_sprint_prefix_mappings
43
+ @team_sprint_prefix_mappings ||=
44
+ sprint_prefixes.collect(&:name).each_with_object({}) do |prefix_name, mappings|
45
+ found_team = map_prefix_name_to_team_name(prefix_name)
46
+ mappings[prefix_name] = found_team if found_team
47
+ end.invert
48
+ end
49
+
50
+ def map_prefix_name_to_team_name(prefix_name)
51
+ sub_team_in_prefix = prefix_name.split(Sprint::Name::SPRINT_PREFIX_SEPARATOR).last
52
+
53
+ teams.find { |team| team.end_with?(sub_team_in_prefix) }
54
+ end
55
+
56
+ def no_matching_sprint_prefix_for_team_message(team_name)
57
+ "No matching sprint prefix for team '#{team_name}' in #{team_sprint_prefix_mappings.inspect}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jira
4
+ module Auto
5
+ class Tool
6
+ class TeamSprintTicketDispatcher
7
+ attr_reader :jira_client, :tickets, :sprint_prefixes
8
+
9
+ def initialize(jira_client, tickets, sprint_prefixes)
10
+ @jira_client = jira_client
11
+ @tickets = tickets
12
+ @sprint_prefixes = sprint_prefixes
13
+ end
14
+
15
+ def dispatch_tickets
16
+ per_team_tickets do |team, tickets|
17
+ log.debug { "#{team}: dispatching tickets #{tickets.collect(&:key).join(", ")}" }
18
+
19
+ sprint_prefix_matching_team = sprint_prefix_for(team)
20
+
21
+ if sprint_prefix_matching_team.nil?
22
+ log.warn { team_sprint_prefix_mapper.no_matching_sprint_prefix_for_team_message(team) }
23
+ else
24
+ dispatch_tickets_to_prefix_sprints(sprint_prefix_matching_team, tickets)
25
+ end
26
+ end
27
+ end
28
+
29
+ def sprint_prefix_for(team)
30
+ team_sprint_prefix_mapper.fetch_for(team)
31
+ end
32
+
33
+ def dispatch_tickets_to_prefix_sprints(prefix_name, tickets)
34
+ prefix = sprint_prefixes.find { |sprint_prefix| sprint_prefix.name == prefix_name }
35
+
36
+ tickets.each do |ticket|
37
+ matched_sprint = match_ticket_to_prefix_sprint(prefix, ticket)
38
+
39
+ ticket.sprint = matched_sprint if matched_sprint
40
+ end
41
+ end
42
+
43
+ def match_ticket_to_prefix_sprint(prefix, ticket)
44
+ ticket_start_time = Time.parse(ticket.expected_start_date).end_of_day
45
+
46
+ first_sprint_when_overdue_ticket(ticket_start_time, prefix) ||
47
+ prefix.sprints.find do |sprint|
48
+ ticket_start_time.between?(sprint.start_date, sprint.end_date)
49
+ end
50
+ end
51
+
52
+ def first_sprint_when_overdue_ticket(ticket_start_time, prefix)
53
+ first_sprint = prefix.sprints.first
54
+
55
+ ticket_start_time < first_sprint.start_date ? first_sprint : nil
56
+ end
57
+
58
+ def per_team_tickets(&)
59
+ return enum_for(:per_team_tickets) unless block_given?
60
+
61
+ team_ticket_map = tickets.group_by(&:implementation_team)
62
+
63
+ team_ticket_map.each(&)
64
+ end
65
+
66
+ def team_sprint_prefix_mapper
67
+ TeamSprintPrefixMapper.new(teams, sprint_prefixes)
68
+ end
69
+
70
+ def teams
71
+ tickets.collect(&:implementation_team).uniq.sort
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end