Class: Exporter
- Inherits:
-
Object
- Object
- Exporter
- Defined in:
- lib/jirametrics/examples/standard_project.rb,
lib/jirametrics/exporter.rb,
lib/jirametrics/examples/aggregated_project.rb
Overview
This file is really intended to give you ideas about how you might configure your own reports, not as a complete setup that will work in every case.
The point of an AGGREGATED report is that we're now looking at a higher level. We might use this in a S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a single team. For that reason, we look at slightly different things that we would on a single team board.
Constant Summary collapse
- EXAMPLE_NAMES =
Nothing in examples/ is loaded with the rest of the library, because an example is something you opt into rather than something the product provides. Each file there defines the method named after it. Derived from the directory rather than listed, so a new example is covered by existing.
Dir[File.join(__dir__, 'examples', '*.rb')] .collect { |file| File.basename(file, '.rb').to_sym } .freeze
- IGNORED_ISSUE_NOTE =
Only the unexpected case is announced. Not being filtered is the common one, and saying so every time would be noise on top of the dump that was actually asked for. Note the claim is about the filter, not about charts: an issue that survives filtering may still be absent from a given chart for its own reasons, so there is no matching note for the other case.
'IGNORED by a filter such as ignore_types or ignore_issues. No chart uses it.'
Class Attribute Summary collapse
Instance Attribute Summary collapse
-
#file_system ⇒ Object
Returns the value of attribute file_system.
-
#project_configs ⇒ Object
readonly
Returns the value of attribute project_configs.
Class Method Summary collapse
Instance Method Summary collapse
- #aggregated_project(name:, project_names:, settings: {}) ⇒ Object
-
#board_features(gateway, board_id, raw) ⇒ Object
Only a team-managed ("simple") board needs the features lookup to tell sprints from kanban; for classic scrum/kanban the board type alone settles it, so we skip the extra request.
-
#board_kind_label(board) ⇒ Object
A team-managed ("simple") board can be scrum- or kanban-flavoured depending on its sprints feature; the bare "simple" from Jira doesn't say which, so spell it out.
- #boards(board_id:, name_filter: nil) ⇒ Object
- #boards_error_message(board_id) ⇒ Object
- #describe_board(gateway, board_id) ⇒ Object
-
#describe_search(project:, matches:) ⇒ Object
One line per project saying where its issues were read from and what was there, so that a missing issue can be traced to the wrong directory rather than guessed at.
-
#describe_where_we_looked(key, searched) ⇒ Object
Verbose in the log, sparse in the terminal.
- #download(name_filter:) ⇒ Object
- #downloading? ⇒ Boolean
- #each_project_config(name_filter:) ⇒ Object
- #export(name_filter:) ⇒ Object
- #fetch_all_boards(gateway) ⇒ Object
-
#filter_issues(issues, ignore_issues) ⇒ Object
Extracted as a separate method so it can be tested independently, without needing to invoke the full standard_project DSL setup.
- #format_board(board, board_id) ⇒ Object
- #format_status(status) ⇒ Object
- #holiday_dates(*args) ⇒ Object
- #info(key, name_filter:) ⇒ Object
-
#initialize(file_system: FileSystem.new) ⇒ Exporter
constructor
A new instance of Exporter.
- #jira_config(filename = nil) ⇒ Object
-
#jira_connections_to_verify(name_filter:) ⇒ Object
The [jira_config, settings] pairs to verify, deduplicated by URL.
- #list_boards(gateway, name_filter) ⇒ Object
-
#matching_issues_in(project, key) ⇒ Object
Asking about a specific issue should always show that issue, whether or not the report used it.
-
#method_missing(name, *args, **keywords, &block) ⇒ Object
Deliberately method_missing rather than stub methods that the real ones overwrite.
- #project(name: nil, &block) ⇒ Object
- #report_info_results(key:, selected:, where_we_looked:) ⇒ Object
-
#respond_to_missing?(name, include_private = false) ⇒ Boolean
These are not things we respond to.
- #standard_project(name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {}, default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}, rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],, show_experimental_charts: false, github_repos: nil) ⇒ Object
- #stitch(stitch_file) ⇒ Object
- #target_path(path = nil) ⇒ Object
- #timezone_offset(offset = nil) ⇒ Object
- #verify_jira_connections(name_filter:) ⇒ Object
- #xproject(*args) ⇒ Object
Constructor Details
#initialize(file_system: FileSystem.new) ⇒ Exporter
Returns a new instance of Exporter.
65 66 67 68 69 70 71 72 73 |
# File 'lib/jirametrics/exporter.rb', line 65 def initialize file_system: FileSystem.new @project_configs = [] @target_path = '.' @holiday_dates = [] @downloading = false @file_system = file_system timezone_offset '+00:00' end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(name, *args, **keywords, &block) ⇒ Object
Deliberately method_missing rather than stub methods that the real ones overwrite. Stubs depend on this file loading before the example, which require_all does not guarantee, and when it lost that race the stub replaced the working method.
19 20 21 22 23 24 25 |
# File 'lib/jirametrics/exporter.rb', line 19 def method_missing name, *args, **keywords, &block return super unless EXAMPLE_NAMES.include? name raise "#{name} is an example rather than part of jirametrics, so your config has to ask for " \ "it. Add this line to the top of your config file:\n\n" \ " require 'jirametrics/examples/#{name}'\n" end |
Class Attribute Details
.logfile_name ⇒ Object
33 34 35 |
# File 'lib/jirametrics/exporter.rb', line 33 def self.logfile_name @logfile_name ||= 'jirametrics.log' end |
Instance Attribute Details
#file_system ⇒ Object
Returns the value of attribute file_system.
7 8 9 |
# File 'lib/jirametrics/exporter.rb', line 7 def file_system @file_system end |
#project_configs ⇒ Object (readonly)
Returns the value of attribute project_configs.
6 7 8 |
# File 'lib/jirametrics/exporter.rb', line 6 def project_configs @project_configs end |
Class Method Details
.configure(&block) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/jirametrics/exporter.rb', line 43 def self.configure &block # No block form: FileSystem holds this descriptor open for the whole run and writes to it as we # go, so it must outlive this method. logfile = File.open(logfile_name, 'w') # rubocop:disable Style/FileOpen rescue Errno::EACCES # FileSystem can't be used here - it hasn't been created yet (it depends on this logfile). warn "Error: Cannot write to #{File.(logfile_name)}. " \ 'Please ensure the current directory is writable.' exit 1 else file_system = FileSystem.new file_system.logfile = logfile file_system.logfile_name = logfile_name exporter = Exporter.new file_system: file_system exporter.instance_eval(&block) @@instance = exporter end |
.instance ⇒ Object
63 |
# File 'lib/jirametrics/exporter.rb', line 63 def self.instance = @@instance |
Instance Method Details
#aggregated_project(name:, project_names:, settings: {}) ⇒ Object
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/jirametrics/examples/aggregated_project.rb', line 11 def aggregated_project name:, project_names:, settings: {} project name: name do file_system.log name file_prefix name self.settings.merge! stringify_keys(settings) aggregate do project_names.each do |project_name| include_issues_from project_name end end file do file_suffix '.html' issues.reject! do |issue| %w[Sub-task Epic].include? issue.type end html_report do html '<h1>Boards included in this report</h1><ul>', type: :header board_lines = [] included_projects.each do |project| project.all_boards.each_value do |board| board_lines << "<a href='#{project.get_file_prefix}.html'>#{board.name}</a> from project #{project.name}" end end board_lines.sort.each { |line| html "<li>#{line}</li>", type: :header } html '</ul>', type: :header cycletime_scatterplot do show_trend_lines # For an aggregated report we group by board rather than by type grouping_rules do |issue, rules| rules.label = issue.board.name end end # aging_work_in_progress_chart daily_wip_by_parent_chart do # When aggregating, the chart tends to need more vertical space canvas height: 400, width: 800 end aging_work_table do # In an aggregated report, we likely only care about items that are old so exclude anything # under 21 days. age_cutoff 21 end dependency_chart do header_text 'Dependencies across boards' description_text 'We are only showing dependencies across boards.' # By default, the issue doesn't show what board it's on and this is important for an # aggregated view chart = self issue_rules do |issue, rules| chart.default_issue_rules.call(issue, rules) rules.label = rules.label.split('<BR/>').insert(1, "Board: #{issue.board.name}").join('<BR/>') end link_rules do |link, rules| chart.default_link_rules.call(link, rules) # Because this is the aggregated view, let's hide any link that doesn't cross boards. rules.ignore if link.origin.board == link.other_issue.board end end end end end end |
#board_features(gateway, board_id, raw) ⇒ Object
Only a team-managed ("simple") board needs the features lookup to tell sprints from kanban; for classic scrum/kanban the board type alone settles it, so we skip the extra request.
231 232 233 234 235 |
# File 'lib/jirametrics/exporter.rb', line 231 def board_features gateway, board_id, raw return [] unless raw['type'] == 'simple' BoardFeature.from_raw gateway.call_url(relative_url: "/rest/agile/1.0/board/#{board_id}/features") end |
#board_kind_label(board) ⇒ Object
A team-managed ("simple") board can be scrum- or kanban-flavoured depending on its sprints feature; the bare "simple" from Jira doesn't say which, so spell it out.
272 273 274 275 276 |
# File 'lib/jirametrics/exporter.rb', line 272 def board_kind_label board return board.board_type unless board.board_type == 'simple' board.scrum? ? 'team-managed, uses sprints' : 'team-managed, no sprints' end |
#boards(board_id:, name_filter: nil) ⇒ Object
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/jirametrics/exporter.rb', line 152 def boards board_id:, name_filter: nil gateway = JiraGateway.new(file_system: file_system, jira_config: jira_config, settings: {}) # Keep the gateway's raw curl chatter in the logfile; list_boards/describe_board switch this off # right before they print their own clean output, and the rescue turns a failed Jira call into a # one-line message with next steps instead of a stack trace. file_system.log_only = true if board_id.nil? list_boards gateway, name_filter else describe_board gateway, board_id end true rescue StandardError file_system.log_only = false file_system.error (board_id) false ensure file_system.log_only = false end |
#boards_error_message(board_id) ⇒ Object
172 173 174 175 176 177 178 179 180 |
# File 'lib/jirametrics/exporter.rb', line 172 def board_id if board_id "Couldn't read board #{board_id} from Jira. Check the id (run `jirametrics boards` to list them) " \ "and your credentials (`jirametrics verify`). Details in #{file_system.logfile_name}." else "Couldn't list boards from Jira. Check your credentials with `jirametrics verify`. " \ "Details in #{file_system.logfile_name}." end end |
#describe_board(gateway, board_id) ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/jirametrics/exporter.rb', line 217 def describe_board gateway, board_id statuses = StatusCollection.new gateway.call_url(relative_url: '/rest/api/2/status').each do |snippet| statuses << Status.from_raw(snippet) end raw = gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration" features = board_features(gateway, board_id, raw) file_system.log_only = false # gateway calls done; turn logging back on to print results board = Board.new raw: raw, possible_statuses: statuses, features: features file_system.log format_board(board, board_id), also_write_to_stderr: true end |
#describe_search(project:, matches:) ⇒ Object
One line per project saying where its issues were read from and what was there, so that a missing issue can be traced to the wrong directory rather than guessed at.
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/jirametrics/exporter.rb', line 338 def describe_search project:, matches: prefix = project.get_file_prefix raise_if_not_set: false path = prefix.nil? ? '(no file_prefix set)' : File.join(project.target_path.to_s, "#{prefix}_issues") counts = begin collection = project.issues excluded = collection.hidden.size detail = "#{collection.size} issues loaded" detail << ", #{excluded} excluded by filters" if excluded.positive? "#{detail}, #{matches} matched" rescue StandardError 'could not be read' end " #{project.name.inspect}: #{path} (#{counts})" end |
#describe_where_we_looked(key, searched) ⇒ Object
Verbose in the log, sparse in the terminal. Where we looked is almost always what you need when an issue you know was downloaded cannot be found, and it is noise the rest of the time.
307 308 309 310 311 312 |
# File 'lib/jirametrics/exporter.rb', line 307 def describe_where_we_looked key, searched return "Searched these projects for #{key.inspect}:\n#{searched.join "\n"}" unless searched.empty? 'No project configurations were searched at all. Either none are defined in the config file ' \ 'or none of them matched the name filter.' end |
#download(name_filter:) ⇒ Object
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/jirametrics/exporter.rb', line 82 def download name_filter: @downloading = true github_pr_cache = {} each_project_config(name_filter: name_filter) do |project| project.evaluate_next_level next if project.aggregated_project? unless project.download_config raise "Project #{project.name.inspect} is missing a download section in the config. " \ 'That is required in order to download' end project.download_config.run gateway = JiraGateway.new( file_system: file_system, jira_config: project.jira_config, settings: project.settings ) downloader = Downloader.create( download_config: project.download_config, file_system: file_system, jira_gateway: gateway, github_pr_cache: github_pr_cache ) downloader.run end puts "Full output from downloader in #{file_system.logfile_name}" end |
#downloading? ⇒ Boolean
382 383 384 |
# File 'lib/jirametrics/exporter.rb', line 382 def downloading? @downloading end |
#each_project_config(name_filter:) ⇒ Object
376 377 378 379 380 |
# File 'lib/jirametrics/exporter.rb', line 376 def each_project_config name_filter: @project_configs.each do |project| yield project if project.name.nil? || File.fnmatch(name_filter, project.name) end end |
#export(name_filter:) ⇒ Object
75 76 77 78 79 80 |
# File 'lib/jirametrics/exporter.rb', line 75 def export name_filter: each_project_config(name_filter: name_filter) do |project| project.evaluate_next_level project.run end end |
#fetch_all_boards(gateway) ⇒ Object
203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/jirametrics/exporter.rb', line 203 def fetch_all_boards gateway boards = [] start_at = 0 loop do json = gateway.call_url relative_url: "/rest/agile/1.0/board?startAt=#{start_at}&maxResults=50" values = json['values'] || [] boards.concat values break if json['isLast'] || values.empty? start_at += values.length end boards end |
#filter_issues(issues, ignore_issues) ⇒ Object
Extracted as a separate method so it can be tested independently, without needing to invoke the full standard_project DSL setup.
104 105 106 107 108 109 110 |
# File 'lib/jirametrics/examples/standard_project.rb', line 104 def filter_issues issues, ignore_issues return unless ignore_issues issues.reject! do |issue| ignore_issues.is_a?(Proc) ? ignore_issues.call(issue) : ignore_issues.include?(issue.key) end end |
#format_board(board, board_id) ⇒ Object
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/jirametrics/exporter.rb', line 237 def format_board board, board_id lines = ["Board #{board_id}: #{board.name.inspect} (#{board_kind_label board})", ''] backlog = board.backlog_statuses unless backlog.empty? lines << 'Not shown on the board (treated as not started):' backlog.each { |status| lines << " - #{format_status status}" } lines << '' end lines << 'Columns, left to right, with the statuses in each (shown as "name":id with its category):' lines << '' board.visible_columns.each do |column| lines << " #{column.name}" column.status_ids.each do |id| status = board.possible_statuses.find_by_id id lines << " - #{status ? format_status(status) : "unknown status id #{id}"}" end end lines << '' lines << 'To set cycle time, choose the column where work is "started" and where it is "finished":' lines << " start_at first_time_in_or_right_of_column '<started column>'" lines << " stop_at first_time_in_or_right_of_column '<finished column>'" if board.scrum? lines << '' lines << 'This board uses sprints. If no column cleanly marks when work starts, you can start the' lines << 'clock when an item is first added to a sprint instead:' lines << ' start_at first_time_added_to_active_sprint' end lines.join "\n" end |
#format_status(status) ⇒ Object
278 279 280 |
# File 'lib/jirametrics/exporter.rb', line 278 def format_status status "#{status.name.inspect}:#{status.id} (#{status.category.name})" end |
#holiday_dates(*args) ⇒ Object
433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/jirametrics/exporter.rb', line 433 def holiday_dates *args unless args.empty? dates = [] args.each do |arg| if /^(?<from>\d{4}-\d{2}-\d{2})\.\.(?<to>\d{4}-\d{2}-\d{2})$/ =~ arg Date.parse(from).upto(Date.parse(to)).each { |date| dates << date } else dates << Date.parse(arg) end end @holiday_dates = dates end @holiday_dates end |
#info(key, name_filter:) ⇒ Object
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 |
# File 'lib/jirametrics/exporter.rb', line 282 def info key, name_filter: selected = [] searched = [] file_system.log_only = true each_project_config(name_filter: name_filter) do |project| project.evaluate_next_level project.run load_only: true matches = matching_issues_in(project, key) searched << describe_search(project: project, matches: matches.size) selected.concat matches rescue => e # rubocop:disable Style/RescueStandardError # This happens when we're attempting to load an aggregated project because it hasn't been # properly initialized. Since we don't care about aggregated projects, we just ignore it. raise unless e..start_with? 'This is an aggregated project and issues should have been included' searched << " #{project.name.inspect}: skipped, this is an aggregated project" end file_system.log_only = false report_info_results key: key, selected: selected, where_we_looked: describe_where_we_looked(key, searched) end |
#jira_config(filename = nil) ⇒ Object
417 418 419 420 421 422 423 424 425 426 |
# File 'lib/jirametrics/exporter.rb', line 417 def jira_config filename = nil if filename @jira_config = file_system.load_json(filename, fail_on_error: false) raise "Unable to load Jira configuration file and cannot continue: #{filename.inspect}" if @jira_config.nil? match = %r{^(?<base_url>.+)/+$}.match(@jira_config['url']) @jira_config['url'] = match[:base_url] if match end @jira_config end |
#jira_connections_to_verify(name_filter:) ⇒ Object
The [jira_config, settings] pairs to verify, deduplicated by URL. We do NOT evaluate_next_level: running a project block (e.g. standard_project) forces an issue-data load that doesn't exist before the first download, and verify must work pre-download. When no project supplied a connection we fall back to the top-level jira_config, so verify can run as a first setup step on just the credentials file. (The fallback has no per-project settings, so a config-block-only setting like ignore_ssl_errors isn't applied. That only affects self-signed Data Center instances.)
138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/jirametrics/exporter.rb', line 138 def jira_connections_to_verify name_filter: connections = [] seen_urls = [] each_project_config(name_filter: name_filter) do |project| url = project.jira_config && project.jira_config['url'] next if url.nil? || seen_urls.include?(url) seen_urls << url connections << [project.jira_config, project.settings] end connections << [jira_config, {}] if connections.empty? && jira_config connections end |
#list_boards(gateway, name_filter) ⇒ Object
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/jirametrics/exporter.rb', line 182 def list_boards gateway, name_filter boards = fetch_all_boards gateway boards.select! { |board| File.fnmatch(name_filter, board['name'].to_s, File::FNM_CASEFOLD) } if name_filter boards.sort_by! { |board| board['name'].to_s.strip.downcase } file_system.log_only = false # gateway calls done; turn logging back on to print results if boards.empty? file_system.log( name_filter ? "No boards match #{name_filter.inspect}." : 'No boards found for this Jira connection.', also_write_to_stderr: true ) return end lines = ['Boards you can access:', ''] boards.each { |board| lines << " #{board['id']}: #{board['name'].inspect} (#{board['type']})" } lines << '' lines << "Run `jirametrics boards <id>` to see a board's columns and choose cycletime points." file_system.log lines.join("\n"), also_write_to_stderr: true end |
#matching_issues_in(project, key) ⇒ Object
Asking about a specific issue should always show that issue, whether or not the report used it. An issue removed by something like ignore_types is exactly when somebody runs info to find out what happened, and reporting it as missing sends them hunting for a download problem that is not there. The hidden list is searched first. Returns [project, issue, ignored] triples.
358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/jirametrics/exporter.rb', line 358 def matching_issues_in project, key matches = [] collection = project.issues [[collection.hidden, true], [collection, false]].each do |issues, ignored| issues.each do |issue| matches << [project, issue, ignored] if key == issue.key issue.subtasks.each do |subtask| matches << [project, subtask, ignored] if key == subtask.key end end end matches end |
#project(name: nil, &block) ⇒ Object
386 387 388 389 390 391 392 |
# File 'lib/jirametrics/exporter.rb', line 386 def project name: nil, &block raise 'jira_config not set' if @jira_config.nil? @project_configs << ProjectConfig.new( exporter: self, target_path: @target_path, jira_config: @jira_config, block: block, name: name ) end |
#report_info_results(key:, selected:, where_we_looked:) ⇒ Object
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/jirametrics/exporter.rb', line 314 def report_info_results key:, selected:, where_we_looked: if selected.empty? file_system.log( "No issues found to match #{key.inspect}", more: where_we_looked, also_write_to_stderr: true ) return end file_system.log where_we_looked selected.each do |project, issue, ignored| file_system.log "\nProject #{project.name}", also_write_to_stderr: true file_system.log(IGNORED_ISSUE_NOTE, also_write_to_stderr: true) if ignored file_system.log issue.dump, also_write_to_stderr: true end end |
#respond_to_missing?(name, include_private = false) ⇒ Boolean
These are not things we respond to. Calling one raises, so respond_to? should say false and the message above should be the only way anyone learns they exist.
29 30 31 |
# File 'lib/jirametrics/exporter.rb', line 29 def respond_to_missing? name, include_private = false super end |
#standard_project(name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {}, default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}, rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic],, show_experimental_charts: false, github_repos: nil) ⇒ Object
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/jirametrics/examples/standard_project.rb', line 6 def standard_project name:, file_prefix:, ignore_issues: nil, starting_status: nil, boards: {}, default_board: nil, anonymize: false, settings: {}, status_category_mappings: {}, rolling_date_count: 90, no_earlier_than: nil, ignore_types: %w[Sub-task Subtask Epic], show_experimental_charts: false, github_repos: nil exporter = self project name: name do file_system.log name, also_write_to_stderr: true file_prefix file_prefix self.anonymize if anonymize self.settings.merge! stringify_keys(settings) boards.each_key do |board_id| block = boards[board_id] if block == :default block = lambda do |_| start_at first_time_in_status_category(:indeterminate) stop_at still_in_status_category(:done) end end board id: board_id do cycletime(&block) end end status_category_mappings.each do |status, category| status_category_mapping status: status, category: category end download do self.rolling_date_count(rolling_date_count) if rolling_date_count self.no_earlier_than(no_earlier_than) if no_earlier_than github_repo(*github_repos) if github_repos end issues.reject! do |issue| ignore_types.include? issue.type end exporter.filter_issues issues, ignore_issues discard_changes_before status_becomes: (starting_status || :backlog) file do file_suffix '.html' html_report do board_id default_board if default_board html "<H1>#{name}</H1>", type: :header boards.each_key do |id| board = find_board id html "<div><a href='#{board.url}'>#{id} #{board.name}</a> (#{board.board_type})</div>", type: :header end daily_view cumulative_flow_diagram cycletime_scatterplot do show_trend_lines end cycletime_histogram throughput_chart do description_text <<~TEXT <div>Throughput data is very useful for#{' '} <a href="https://blog.mikebowler.ca/2024/06/02/probabilistic-forecasting/">probabilistic forecasting</a>, to determine when we'll be done. Try it now with the <a href="<%= throughput_forecaster_url %>" target="_blank" rel="noopener noreferrer"> Focused Objective throughput forecaster,</a> to see how long it would take to complete all of the <%= @not_started_count %> items you currently have in your backlog. </div> <h2>Number of items completed, grouped by issue type</h2>' TEXT end throughput_by_completed_resolution_chart do description_text '<h2>Number of items completed, grouped by completion status and resolution</h2>' end aging_work_in_progress_chart wip_by_column_chart do show_recommendations end aging_work_table daily_wip_by_age_chart daily_wip_by_blocked_stalled_chart daily_wip_by_parent_chart flow_efficiency_scatterplot if show_experimental_charts sprint_burndown estimate_accuracy_chart dependency_chart end end end end |
#stitch(stitch_file) ⇒ Object
372 373 374 |
# File 'lib/jirametrics/exporter.rb', line 372 def stitch stitch_file Stitcher.new(file_system: file_system).run(stitch_file: stitch_file) end |
#target_path(path = nil) ⇒ Object
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
# File 'lib/jirametrics/exporter.rb', line 396 def target_path path = nil unless path.nil? previous = @target_path @target_path = path @target_path += File::SEPARATOR unless @target_path.end_with? File::SEPARATOR FileUtils.mkdir_p @target_path # A config is allowed to switch target directories partway through, and when it does, an # issue can be downloaded into one and looked for in another. Recording every change makes # that visible rather than something to guess at. See GitHub issue 77. # The constructor seeds this with a bare '.', and the setter always appends a separator, so # an unseparated '.' is the untouched default rather than something the config chose. Setting # the same path twice is not a change and saying so would be actively misleading. if previous == '.' file_system.diagnostic "target_path set to #{@target_path.inspect}" elsif previous != @target_path file_system.diagnostic "target_path changed from #{previous.inspect} to #{@target_path.inspect}" end end @target_path end |
#timezone_offset(offset = nil) ⇒ Object
428 429 430 431 |
# File 'lib/jirametrics/exporter.rb', line 428 def timezone_offset offset = nil @timezone_offset = offset unless offset.nil? @timezone_offset end |
#verify_jira_connections(name_filter:) ⇒ Object
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/jirametrics/exporter.rb', line 109 def verify_jira_connections name_filter: results = [] begin # Probe under log_only so the gateway's raw curl chatter (and its error dumps) stay in the # logfile rather than cluttering the console; we surface a clean summary line below. file_system.log_only = true jira_connections_to_verify(name_filter: name_filter).each do |config, settings| gateway = JiraGateway.new(file_system: file_system, jira_config: config, settings: settings) results << gateway.verify_connection end ensure file_system.log_only = false end if results.empty? file_system.error 'No Jira connection found in the configuration to verify' return results end results.each { |result| file_system.log result., also_write_to_stderr: true } results end |
#xproject(*args) ⇒ Object
394 |
# File 'lib/jirametrics/exporter.rb', line 394 def xproject *args; end |