jirametrics 2.12.1 → 2.14pre2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7565c5b6320b9c4375a3feb27fcaf80f96d9c058041b71584f287e6a3d7eb399
4
- data.tar.gz: 4554c7edbc7c42e01ad4838094ab922819c2e71818a46907f25740e35c327aa6
3
+ metadata.gz: ddc2265d15bc52422942f009082fd92fea0b296860297b6a6fa5fd0aee1788f4
4
+ data.tar.gz: e76799a3576b4e1e667268b823c99d6be8068ee196a76766e07fb646a44b17ee
5
5
  SHA512:
6
- metadata.gz: 021c1f989117b1fbb8708183d9ad8e79b049060989c1f20d31121a486d25f1ae2d1c036e013279479cd3ac6f5b101a48d8c9f4943570107c9cc4b40485ea6a4f
7
- data.tar.gz: 7f7e334a2161e23ef5ae8a754aa3f12524c1bb130893ca3df3e8574f7a44286ef33c8a03b2ac2790ccb8e8b792ce0fb6c17b1d69d2da068ea48dbce1b4c514a1
6
+ metadata.gz: 0d60ce44257fcd466d092963ec7a58e86d9cf9252bc83262ac3c3b3621941df44b5c686b71e57be68bef06352d964f369b3cd9281324e3235cdad2c94e2f1869
7
+ data.tar.gz: 217c6e8a37ad3b5ae4e25d454a27b77b023ccd87e73ece9a1d3853f55e9c1330e24e64a7578f99289007925c2310482f200ac4f5d5affae6dd0ccb649978269b
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AtlassianDocumentFormat
4
+ attr_reader :users
5
+
6
+ def initialize users:, timezone_offset:
7
+ @users = users
8
+ end
9
+
10
+ def to_html input
11
+ if input.is_a? String
12
+ input
13
+ .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
14
+ .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
15
+ .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
16
+ .gsub("\n", '<br />')
17
+ else
18
+ input['content'].collect { |element| adf_node_to_html element }.join("\n")
19
+ end
20
+ end
21
+
22
+ # ADF is Atlassian Document Format
23
+ # https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
24
+ def adf_node_to_html node
25
+ closing_tag = nil
26
+ node_attrs = node['attrs']
27
+
28
+ result = +''
29
+ case node['type']
30
+ when 'blockquote'
31
+ result << '<blockquote>'
32
+ closing_tag = '</blockquote>'
33
+ when 'bulletList'
34
+ result << '<ul>'
35
+ closing_tag = '</ul>'
36
+ when 'codeBlock'
37
+ result << '<code>'
38
+ closing_tag = '</code>'
39
+ when 'date'
40
+ result << Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s
41
+ when 'decisionItem'
42
+ result << '<li>'
43
+ closing_tag = '</li>'
44
+ when 'decisionList'
45
+ result << '<div>Decisions<ul>'
46
+ closing_tag = '</ul></div>'
47
+ when 'emoji'
48
+ result << node_attrs['text']
49
+ when 'expand'
50
+ # TODO: Maybe, someday, make this actually expandable. For now it's always open
51
+ result << "<div>#{node_attrs['title']}</div>"
52
+ when 'hardBreak'
53
+ result << '<br />'
54
+ when 'heading'
55
+ level = node_attrs['level']
56
+ result << "<h#{level}>"
57
+ closing_tag = "</h#{level}>"
58
+ when 'inlineCard'
59
+ url = node_attrs['url']
60
+ result << "[Inline card]: <a href='#{url}'>#{url}</a>"
61
+ when 'listItem'
62
+ result << '<li>'
63
+ closing_tag = '</li>'
64
+ when 'media'
65
+ text = node_attrs['alt'] || node_attrs['id']
66
+ result << "Media: #{text}"
67
+ when 'mediaSingle', 'mediaGroup'
68
+ result << '<div>'
69
+ closing_tag = '</div>'
70
+ when 'mention'
71
+ user = node_attrs['text']
72
+ result << "<b>#{user}</b>"
73
+ when 'orderedList'
74
+ result << '<ol>'
75
+ closing_tag = '</ol>'
76
+ when 'panel'
77
+ type = node_attrs['panelType']
78
+ result << "<div>#{type.upcase}</div>"
79
+ when 'paragraph'
80
+ result << '<p>'
81
+ closing_tag = '</p>'
82
+ when 'rule'
83
+ result << '<hr />'
84
+ when 'status'
85
+ text = node_attrs['text']
86
+ result << text
87
+ when 'table'
88
+ result << '<table>'
89
+ closing_tag = '</table>'
90
+ when 'tableCell'
91
+ result << '<td>'
92
+ closing_tag = '</td>'
93
+ when 'tableHeader'
94
+ result << '<th>'
95
+ closing_tag = '</th>'
96
+ when 'tableRow'
97
+ result << '<tr>'
98
+ closing_tag = '</tr>'
99
+ when 'text'
100
+ marks = adf_marks_to_html node['marks']
101
+ result << marks.collect(&:first).join
102
+ result << node['text']
103
+ result << marks.collect(&:last).join
104
+ when 'taskItem'
105
+ state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
106
+ result << "<li>#{state} "
107
+ closing_tag = '</li>'
108
+ when 'taskList'
109
+ result << "<ul class='taskList'>"
110
+ closing_tag = '</ul>'
111
+ else
112
+ result << "<p>Unparseable section: #{node['type']}</p>"
113
+ end
114
+
115
+ node['content']&.each do |child|
116
+ result << adf_node_to_html(child)
117
+ end
118
+
119
+ result << closing_tag if closing_tag
120
+ result
121
+ end
122
+
123
+ def adf_marks_to_html list
124
+ return [] if list.nil?
125
+
126
+ mappings = [
127
+ ['strong', '<b>', '</b>'],
128
+ ['code', '<code>', '</code>'],
129
+ ['em', '<em>', '</em>'],
130
+ ['strike', '<s>', '</s>'],
131
+ ['underline', '<u>', '</u>']
132
+ ]
133
+
134
+ list.filter_map do |mark|
135
+ type = mark['type']
136
+ if type == 'textColor'
137
+ color = mark['attrs']['color']
138
+ ["<span style='color: #{color}'>", '</span>']
139
+ elsif type == 'link'
140
+ href = mark['attrs']['href']
141
+ title = mark['attrs']['title']
142
+ ["<a href='#{href}' title='#{title}'>", '</a>']
143
+ else
144
+ line = mappings.find { |key, _open, _close| key == type }
145
+ [line[1], line[2]] if line
146
+ end
147
+ end
148
+ end
149
+
150
+ def expand_account_id account_id
151
+ user = @users.find { |u| u.account_id == account_id }
152
+ text = account_id
153
+ text = "@#{user.display_name}" if user
154
+ "<span class='account_id'>#{text}</span>"
155
+ end
156
+ end
@@ -11,6 +11,7 @@ class BoardConfig
11
11
 
12
12
  def run
13
13
  @board = @project_config.all_boards[id]
14
+ raise "Can't find board #{id.inspect} in #{@project_config.all_boards.keys.inspect}" unless @board
14
15
 
15
16
  instance_eval(&@block)
16
17
  raise "Must specify a cycletime for board #{@id}" if @board.cycletime.nil?
@@ -30,6 +30,7 @@ class ChangeItem
30
30
  def artificial? = @artificial
31
31
  def assignee? = (field == 'assignee')
32
32
  def comment? = (field == 'comment')
33
+ def description? = (field == 'description')
33
34
  def due_date? = (field == 'duedate')
34
35
  def flagged? = (field == 'Flagged')
35
36
  def issue_type? = field == 'issuetype'
@@ -66,6 +66,8 @@ class ChartBase
66
66
  end
67
67
 
68
68
  def label_days days
69
+ return 'unknown' if days.nil?
70
+
69
71
  "#{days} day#{'s' unless days == 1}"
70
72
  end
71
73
 
@@ -33,6 +33,10 @@ class DailyView < ChartBase
33
33
  result
34
34
  end
35
35
 
36
+ def atlassian_document_format
37
+ @atlassian_document_format ||= AtlassianDocumentFormat.new(users: users, timezone_offset: timezone_offset)
38
+ end
39
+
36
40
  def select_aging_issues
37
41
  aging_issues = issues.select do |issue|
38
42
  started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
@@ -78,7 +82,7 @@ class DailyView < ChartBase
78
82
  blocked_stalled = issue.blocked_stalled_by_date(
79
83
  date_range: today..today, chart_end_time: time_range.end, settings: settings
80
84
  )[today]
81
- return [] unless blocked_stalled
85
+ return [] if blocked_stalled.active?
82
86
 
83
87
  lines = []
84
88
  if blocked_stalled.blocked?
@@ -98,15 +102,18 @@ class DailyView < ChartBase
98
102
  lines
99
103
  end
100
104
 
101
- def make_issue_label issue
102
- "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> " \
103
- "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
105
+ def make_issue_label issue:, done:
106
+ label = "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
107
+ label << '<s>' if done
108
+ label << "<b><a href='#{issue.url}'>#{issue.key}</a></b> &nbsp;<i>#{issue.summary}</i>"
109
+ label << '</s>' if done
110
+ label
104
111
  end
105
112
 
106
- def make_title_line issue
113
+ def make_title_line issue:, done:
107
114
  title_line = +''
108
115
  title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
109
- title_line << make_issue_label(issue)
116
+ title_line << make_issue_label(issue: issue, done: done)
110
117
  title_line
111
118
  end
112
119
 
@@ -115,20 +122,25 @@ class DailyView < ChartBase
115
122
  parent_key = issue.parent_key
116
123
  if parent_key
117
124
  parent = issues.find_by_key key: parent_key, include_hidden: true
118
- text = parent ? make_issue_label(parent) : parent_key
125
+ text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
119
126
  lines << ["Parent: #{text}"]
120
127
  end
121
128
  lines
122
129
  end
123
130
 
124
- def make_stats_lines issue
131
+ def make_stats_lines issue:, done:
125
132
  line = []
126
133
 
127
134
  line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
128
135
 
129
- age = issue.board.cycletime.age(issue, today: date_range.end)
130
- line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
136
+ if done
137
+ cycletime = issue.board.cycletime.cycletime(issue)
131
138
 
139
+ line << "Cycletime: <b>#{label_days cycletime}</b>"
140
+ else
141
+ age = issue.board.cycletime.age(issue, today: date_range.end)
142
+ line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
143
+ end
132
144
  line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
133
145
 
134
146
  column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
@@ -154,29 +166,23 @@ class DailyView < ChartBase
154
166
 
155
167
  def make_child_lines issue
156
168
  lines = []
157
- subtasks = issue.subtasks.reject { |i| i.done? }
169
+ subtasks = issue.subtasks
158
170
 
159
- unless subtasks.empty?
160
- icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
161
- lines << (icon_urls << 'Incomplete child issues')
162
- lines += subtasks
163
- end
164
- lines
165
- end
171
+ return lines if subtasks.empty?
166
172
 
167
- def jira_rich_text_to_html text
168
- text
169
- .gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
170
- .gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
171
- .gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
172
- .gsub("\n", '<br />')
173
- end
173
+ id = next_id
174
+ lines <<
175
+ "<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'section#{id}');\">" \
176
+ "<span id='open#{id}' style='display: none'>▶ Child issues</span>" \
177
+ "<span id='close#{id}'>▼ Child issues</span></a>"
178
+ lines << "<section id='section#{id}'>"
179
+
180
+ # icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
181
+ # lines << (icon_urls << 'Child issues')
182
+ lines += subtasks
183
+ lines << '</section>'
174
184
 
175
- def expand_account_id account_id
176
- user = @users.find { |u| u.account_id == account_id }
177
- text = account_id
178
- text = "@#{user.display_name}" if user
179
- "<span class='account_id'>#{text}</span>"
185
+ lines
180
186
  end
181
187
 
182
188
  def make_history_lines issue
@@ -207,8 +213,8 @@ class DailyView < ChartBase
207
213
  end
208
214
 
209
215
  def history_text change:, board:
210
- if change.comment?
211
- jira_rich_text_to_html(change.value)
216
+ if change.comment? || change.description?
217
+ atlassian_document_format.to_html(change.value)
212
218
  elsif change.status?
213
219
  convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
214
220
  to = convertor.call(change.value_id)
@@ -245,15 +251,27 @@ class DailyView < ChartBase
245
251
  .join(' ')]]
246
252
  end
247
253
 
254
+ def make_description_lines issue
255
+ description = issue.raw['fields']['description']
256
+ result = []
257
+ result << [atlassian_document_format.to_html(description)] if description
258
+ result
259
+ end
260
+
248
261
  def assemble_issue_lines issue, child:
262
+ done = issue.done?
263
+
249
264
  lines = []
250
- lines << [make_title_line(issue)]
265
+ lines << [make_title_line(issue: issue, done: done)]
251
266
  lines += make_parent_lines(issue) unless child
252
- lines += make_stats_lines(issue)
253
- lines += make_sprints_lines(issue)
254
- lines += make_blocked_stalled_lines(issue)
255
- lines += make_child_lines(issue)
256
- lines += make_history_lines(issue)
267
+ lines += make_stats_lines(issue: issue, done: done)
268
+ unless done
269
+ lines += make_description_lines(issue)
270
+ lines += make_sprints_lines(issue)
271
+ lines += make_blocked_stalled_lines(issue)
272
+ lines += make_child_lines(issue)
273
+ lines += make_history_lines(issue)
274
+ end
257
275
  lines
258
276
  end
259
277
 
@@ -264,6 +282,8 @@ class DailyView < ChartBase
264
282
  assemble_issue_lines(issue, child: child).each do |row|
265
283
  if row.is_a? Issue
266
284
  result << render_issue(row, child: true)
285
+ elsif row.is_a?(String)
286
+ result << row
267
287
  else
268
288
  result << '<div class="heading">'
269
289
  row.each do |chunk|
@@ -97,30 +97,59 @@ class Downloader
97
97
  log " JQL: #{jql}"
98
98
  escaped_jql = CGI.escape jql
99
99
 
100
- max_results = 100
101
- start_at = 0
102
- total = 1
103
- while start_at < total
104
- json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
105
- "?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
106
-
107
- json['issues'].each do |issue_json|
108
- issue_json['exporter'] = {
109
- 'in_initial_query' => initial_query
110
- }
111
- identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
112
- file = "#{issue_json['key']}-#{board.id}.json"
113
-
114
- @file_system.save_json(json: issue_json, filename: File.join(path, file))
100
+ if @jira_gateway.cloud?
101
+ max_results = 5_000 # The maximum allowed by Jira
102
+ next_page_token = nil
103
+ issue_count = 0
104
+
105
+ loop do
106
+ json = @jira_gateway.call_url relative_url: '/rest/api/3/search/jql' \
107
+ "?jql=#{escaped_jql}&maxResults=#{max_results}&" \
108
+ "nextPageToken=#{next_page_token}&expand=changelog&fields=*all"
109
+ next_page_token = json['nextPageToken']
110
+
111
+ json['issues'].each do |issue_json|
112
+ issue_json['exporter'] = {
113
+ 'in_initial_query' => initial_query
114
+ }
115
+ identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
116
+ file = "#{issue_json['key']}-#{board.id}.json"
117
+
118
+ @file_system.save_json(json: issue_json, filename: File.join(path, file))
119
+ issue_count += 1
120
+ end
121
+
122
+ message = " Downloaded #{issue_count} issues"
123
+ log message, both: true
124
+
125
+ break unless next_page_token
126
+ end
127
+ else
128
+ max_results = 100
129
+ start_at = 0
130
+ total = 1
131
+ while start_at < total
132
+ json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
133
+ "?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
134
+
135
+ json['issues'].each do |issue_json|
136
+ issue_json['exporter'] = {
137
+ 'in_initial_query' => initial_query
138
+ }
139
+ identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
140
+ file = "#{issue_json['key']}-#{board.id}.json"
141
+
142
+ @file_system.save_json(json: issue_json, filename: File.join(path, file))
143
+ end
144
+
145
+ total = json['total'].to_i
146
+ max_results = json['maxResults']
147
+
148
+ message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
149
+ log message, both: true
150
+
151
+ start_at += json['issues'].size
115
152
  end
116
-
117
- total = json['total'].to_i
118
- max_results = json['maxResults']
119
-
120
- message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
121
- log message, both: true
122
-
123
- start_at += json['issues'].size
124
153
  end
125
154
  end
126
155
 
@@ -191,6 +191,11 @@ div.daily_issue {
191
191
  padding-right: 0.2em;
192
192
  border-radius: 0.2em;
193
193
  }
194
+ h1 {
195
+ border: none;
196
+ background: none;
197
+ padding-left: 0;
198
+ }
194
199
  margin-bottom: 0.5em;
195
200
  }
196
201
  div.child_issue:hover {
@@ -50,8 +50,8 @@
50
50
  </head>
51
51
  <body>
52
52
  <noscript>
53
- <div style="padding: 1em; background: gray; color: white; font-size: 2em;">
54
- Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'd loaded this from a folder on SharePoint then save it locally and load it again.
53
+ <div style="padding: 1em; background: red; color: white; font-size: 2em;">
54
+ Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you've loaded this from a folder on SharePoint then save it locally and load it again.
55
55
  </div>
56
56
  </noscript>
57
57
  <%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
@@ -345,7 +345,7 @@ class Issue
345
345
  end
346
346
  elsif change.link?
347
347
  # Example: "This issue is satisfied by ANON-30465"
348
- unless /^This issue (?<link_text>.+) (?<issue_key>.+)$/ =~ (change.value || change.old_value)
348
+ unless /^This (?<_>issue|work item) (?<link_text>.+) (?<issue_key>.+)$/ =~ (change.value || change.old_value)
349
349
  puts "Issue(#{key}) Can't parse link text: #{change.value || change.old_value}"
350
350
  next
351
351
  end
@@ -681,9 +681,8 @@ class Issue
681
681
  def done?
682
682
  if artificial? || board.cycletime.nil?
683
683
  # This was probably loaded as a linked issue, which means we don't know what board it really
684
- # belonged to. The best we can do is look at the status category. This case should be rare but
685
- # it can happen.
686
- status.category.name == 'Done'
684
+ # belonged to. The best we can do is look at the status key
685
+ status.category.done?
687
686
  else
688
687
  board.cycletime.done? self
689
688
  end
@@ -77,4 +77,8 @@ class JiraGateway
77
77
 
78
78
  true
79
79
  end
80
+
81
+ def cloud?
82
+ @jira_url.downcase.end_with? '.atlassian.net'
83
+ end
80
84
  end
@@ -114,10 +114,14 @@ class ProjectConfig
114
114
  def file_prefix prefix
115
115
  # The file_prefix has to be set before almost everything else. It really should have been an attribute
116
116
  # on the project declaration itself. Hindsight is 20/20.
117
+
118
+ # There can only be one of these
117
119
  if @file_prefix
118
- raise "file_prefix should only be set once. Was #{@file_prefix.inspect} and now changed to #{prefix.inspect}."
120
+ raise "file_prefix can only be set once. Was #{@file_prefix.inspect} and now changed to #{prefix.inspect}."
119
121
  end
120
122
 
123
+ raise_if_prefix_already_used(prefix)
124
+
121
125
  @file_prefix = prefix
122
126
 
123
127
  # Yes, this is a wierd place to be initializing this. Unfortunately, it has to happen after the file_prefix
@@ -130,8 +134,21 @@ class ProjectConfig
130
134
  @file_prefix
131
135
  end
132
136
 
133
- def get_file_prefix # rubocop:disable Naming/AccessorMethodName
134
- raise 'file_prefix has not been set yet. Move it to the top of the project declaration.' unless @file_prefix
137
+ def raise_if_prefix_already_used prefix
138
+ @exporter.project_configs.each do |project|
139
+ next unless project.get_file_prefix(raise_if_not_set: false) == prefix && project.target_path == target_path
140
+
141
+ raise "Project #{name.inspect} specifies file prefix #{prefix.inspect}, " \
142
+ "but that is already used by project #{project.name.inspect} in the same target path #{target_path.inspect}. " \
143
+ 'This is almost guaranteed to be too much copy and paste in your configuration. ' \
144
+ 'File prefixes must be unique within a directory.'
145
+ end
146
+ end
147
+
148
+ def get_file_prefix raise_if_not_set: true
149
+ if @file_prefix.nil? && raise_if_not_set
150
+ raise 'file_prefix has not been set yet. Move it to the top of the project declaration.'
151
+ end
135
152
 
136
153
  @file_prefix
137
154
  end
@@ -19,6 +19,7 @@ class StatusCollection
19
19
  def find_by_id! id
20
20
  status = @list.find { |status| status.id == id }
21
21
  raise "Can't find any status for id #{id} in #{self}" unless status
22
+
22
23
  status
23
24
  end
24
25
 
data/lib/jirametrics.rb CHANGED
@@ -117,6 +117,7 @@ class JiraMetrics < Thor
117
117
  require 'jirametrics/board'
118
118
  require 'jirametrics/daily_view'
119
119
  require 'jirametrics/user'
120
+ require 'jirametrics/atlassian_document_format'
120
121
  load config_file
121
122
  end
122
123
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.1
4
+ version: 2.14pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-07-13 00:00:00.000000000 Z
10
+ date: 2025-08-11 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: random-word
@@ -65,6 +65,7 @@ files:
65
65
  - lib/jirametrics/aging_work_in_progress_chart.rb
66
66
  - lib/jirametrics/aging_work_table.rb
67
67
  - lib/jirametrics/anonymizer.rb
68
+ - lib/jirametrics/atlassian_document_format.rb
68
69
  - lib/jirametrics/blocked_stalled_change.rb
69
70
  - lib/jirametrics/board.rb
70
71
  - lib/jirametrics/board_column.rb