jirametrics 1.5 → 2.0
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 +4 -4
- data/bin/jirametrics +1 -0
- data/lib/jirametrics/aggregate_config.rb +2 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +3 -3
- data/lib/jirametrics/aging_work_in_progress_chart.rb +6 -6
- data/lib/jirametrics/anonymizer.rb +3 -3
- data/lib/jirametrics/blocked_stalled_change.rb +5 -10
- data/lib/jirametrics/board.rb +11 -13
- data/lib/jirametrics/chart_base.rb +5 -5
- data/lib/jirametrics/cycletime_histogram.rb +2 -2
- data/lib/jirametrics/cycletime_scatterplot.rb +5 -2
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +4 -4
- data/lib/jirametrics/dependency_chart.rb +5 -4
- data/lib/jirametrics/download_config.rb +0 -19
- data/lib/jirametrics/downloader.rb +32 -74
- data/lib/jirametrics/examples/aggregated_project.rb +61 -3
- data/lib/jirametrics/examples/standard_project.rb +3 -3
- data/lib/jirametrics/expedited_chart.rb +4 -4
- data/lib/jirametrics/experimental/generator.rb +5 -4
- data/lib/jirametrics/experimental/info.rb +2 -2
- data/lib/jirametrics/exporter.rb +16 -30
- data/lib/jirametrics/file_system.rb +36 -0
- data/lib/jirametrics/groupable_issue_chart.rb +0 -9
- data/lib/jirametrics/hierarchy_table.rb +1 -1
- data/lib/jirametrics/html_report_config.rb +5 -30
- data/lib/jirametrics/issue.rb +1 -7
- data/lib/jirametrics/issue_link.rb +0 -7
- data/lib/jirametrics/jira_gateway.rb +59 -0
- data/lib/jirametrics/project_config.rb +78 -88
- data/lib/jirametrics/rules.rb +1 -20
- data/lib/jirametrics/sprint_burndown.rb +7 -6
- data/lib/jirametrics/sprint_issue_change_data.rb +4 -9
- data/lib/jirametrics/status.rb +24 -20
- data/lib/jirametrics/status_collection.rb +2 -2
- data/lib/jirametrics/story_point_accuracy_chart.rb +2 -7
- data/lib/jirametrics/throughput_chart.rb +2 -2
- data/lib/jirametrics/trend_line_calculator.rb +4 -4
- data/lib/jirametrics/value_equality.rb +23 -0
- data/lib/jirametrics.rb +3 -1
- metadata +5 -17
- data/lib/jirametrics/json_file_loader.rb +0 -9
@@ -1,9 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# This file is really intended to give you ideas about how you might configure your own reports, not
|
4
|
-
# as a complete setup that will work in every case.
|
4
|
+
# as a complete setup that will work in every case.
|
5
5
|
#
|
6
|
-
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for
|
6
|
+
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more details
|
7
|
+
#
|
8
|
+
# The point of an AGGREGATED report is that we're now looking at a higher level. We might use this in a
|
9
|
+
# S2 meeting (Scrum of Scrums) to talk about the things that are happening across teams, not within a
|
10
|
+
# single team. For that reason, we look at slightly different things that we would on a single team board.
|
11
|
+
|
7
12
|
class Exporter
|
8
13
|
def aggregated_project name:, project_names:
|
9
14
|
project name: name do
|
@@ -29,10 +34,63 @@ class Exporter
|
|
29
34
|
rules.label = issue.board.name
|
30
35
|
end
|
31
36
|
end
|
32
|
-
aging_work_in_progress_chart
|
37
|
+
# aging_work_in_progress_chart
|
38
|
+
daily_wip_chart do
|
39
|
+
header_text 'Daily WIP by Parent'
|
40
|
+
description_text <<-TEXT
|
41
|
+
<p>How much work is in progress, grouped by the parent of the issue. This will give us an
|
42
|
+
indication of how focused we are on higher level objectives. If there are many parent
|
43
|
+
tickets in progress at the same time, either this team has their focus scattered or we
|
44
|
+
aren't doing a good job of
|
45
|
+
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
46
|
+
tickets</a>. Neither of those is desirable.</p>
|
47
|
+
<p>If you're expecting all work items to have parents and there are a lot that don't,
|
48
|
+
that's also something to look at. Consider whether there is even value in aggregating
|
49
|
+
these projects if they don't share parent dependencies. Aggregation helps us when we're
|
50
|
+
looking at related work and if there aren't parent dependencies then the work may not
|
51
|
+
be related.</p>
|
52
|
+
TEXT
|
53
|
+
grouping_rules do |issue, rules|
|
54
|
+
rules.label = issue.parent&.key || 'No parent'
|
55
|
+
rules.color = 'white' if rules.label == 'No parent'
|
56
|
+
end
|
57
|
+
end
|
33
58
|
aging_work_table do
|
59
|
+
# In an aggregated report, we likely only care about items that are old so exclude anything
|
60
|
+
# under 21 days.
|
34
61
|
age_cutoff 21
|
35
62
|
end
|
63
|
+
|
64
|
+
dependency_chart do
|
65
|
+
header_text 'Dependencies across boards'
|
66
|
+
description_text 'We are only showing dependencies across boards.'
|
67
|
+
|
68
|
+
# By default, the issue doesn't show what board it's on and this is important for an
|
69
|
+
# aggregated view
|
70
|
+
issue_rules do |issue, rules|
|
71
|
+
key = issue.key
|
72
|
+
key = "<S>#{key} </S> " if issue.status.category_name == 'Done'
|
73
|
+
rules.label = "<#{key} [#{issue.type}]<BR/>#{issue.board.name}<BR/>#{word_wrap issue.summary}>"
|
74
|
+
end
|
75
|
+
|
76
|
+
link_rules do |link, rules|
|
77
|
+
# By default, the dependency chart shows everything. Clean it up a bit.
|
78
|
+
case link.name
|
79
|
+
when 'Cloners'
|
80
|
+
# We don't want to see any clone links at all.
|
81
|
+
rules.ignore
|
82
|
+
when 'Blocks'
|
83
|
+
# For blocks, by default Jira will have links going both
|
84
|
+
# ways and we want them only going one way. Also make the
|
85
|
+
# link red.
|
86
|
+
rules.merge_bidirectional keep: 'outward'
|
87
|
+
rules.line_color = 'red'
|
88
|
+
end
|
89
|
+
|
90
|
+
# Because this is the aggregated view, let's hide any link that doesn't cross boards.
|
91
|
+
rules.ignore if link.origin.board == link.other_issue.board
|
92
|
+
end
|
93
|
+
end
|
36
94
|
end
|
37
95
|
end
|
38
96
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# This file is really intended to give you ideas about how you might configure your own reports, not
|
4
|
-
# as a complete setup that will work in every case.
|
4
|
+
# as a complete setup that will work in every case.
|
5
5
|
#
|
6
6
|
# See https://github.com/mikebowler/jirametrics/wiki/Examples-folder for more
|
7
7
|
class Exporter
|
@@ -83,13 +83,13 @@ class Exporter
|
|
83
83
|
How much work is in progress, grouped by the parent of the issue. This will give us an
|
84
84
|
indication of how focused we are on higher level objectives. If there are many parent
|
85
85
|
tickets in progress at the same time, either this team has their focus scattered or we
|
86
|
-
aren't doing a good job of
|
86
|
+
aren't doing a good job of
|
87
87
|
<a href="https://improvingflow.com/2024/02/21/slicing-epics.html">splitting those parent
|
88
88
|
tickets</a>. Neither of those is desirable.
|
89
89
|
TEXT
|
90
90
|
grouping_rules do |issue, rules|
|
91
91
|
rules.label = issue.parent&.key || 'No parent'
|
92
|
-
rules.color = 'white' if rules.label == 'No parent'
|
92
|
+
rules.color = 'white' if rules.label == 'No parent'
|
93
93
|
end
|
94
94
|
end
|
95
95
|
expedited_chart
|
@@ -39,9 +39,9 @@ class ExpeditedChart < ChartBase
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def run
|
42
|
-
data_sets = find_expedited_issues.
|
42
|
+
data_sets = find_expedited_issues.filter_map do |issue|
|
43
43
|
make_expedite_lines_data_set(issue: issue, expedite_data: prepare_expedite_data(issue))
|
44
|
-
end
|
44
|
+
end
|
45
45
|
|
46
46
|
if data_sets.empty?
|
47
47
|
'<h1>Expedited work</h1>There is no expedited work in this time period.'
|
@@ -84,7 +84,7 @@ class ExpeditedChart < ChartBase
|
|
84
84
|
prepare_expedite_data(issue).empty?
|
85
85
|
end
|
86
86
|
|
87
|
-
expedited_issues.
|
87
|
+
expedited_issues.sort_by(&:key_as_i)
|
88
88
|
end
|
89
89
|
|
90
90
|
def later_date date1, date2
|
@@ -110,7 +110,7 @@ class ExpeditedChart < ChartBase
|
|
110
110
|
|
111
111
|
expedite_data << [started_time, :issue_started] if started_time
|
112
112
|
expedite_data << [stopped_time, :issue_stopped] if stopped_time
|
113
|
-
expedite_data.
|
113
|
+
expedite_data.sort_by! { |a| a[0] }
|
114
114
|
|
115
115
|
# If none of the data would be visible on the chart then skip it.
|
116
116
|
return nil unless expedite_data.any? { |time, _action| time.to_date >= date_range.begin }
|
@@ -38,7 +38,7 @@ class FakeIssue
|
|
38
38
|
priority: {
|
39
39
|
name: ''
|
40
40
|
},
|
41
|
-
summary: RandomWord.phrases.next.gsub(
|
41
|
+
summary: RandomWord.phrases.next.gsub(_, ' '),
|
42
42
|
issuelinks: [],
|
43
43
|
fixVersions: []
|
44
44
|
}
|
@@ -137,7 +137,7 @@ class Generator
|
|
137
137
|
remove_old_files
|
138
138
|
@date_range.each_with_index do |date, day|
|
139
139
|
yield date, day if block_given?
|
140
|
-
process_date(date, day) if (1..5).
|
140
|
+
process_date(date, day) if (1..5).cover? date.wday # Weekday
|
141
141
|
end
|
142
142
|
|
143
143
|
@issues.each do |issue|
|
@@ -158,7 +158,7 @@ class Generator
|
|
158
158
|
def remove_old_files
|
159
159
|
path = "#{@target_path}#{@file_prefix}_issues"
|
160
160
|
Dir.foreach path do |file|
|
161
|
-
next unless file
|
161
|
+
next unless file.match?(/-\d+\.json$/)
|
162
162
|
|
163
163
|
filename = "#{path}/#{file}"
|
164
164
|
File.unlink filename
|
@@ -191,8 +191,9 @@ class Generator
|
|
191
191
|
end
|
192
192
|
end
|
193
193
|
|
194
|
+
possible_capacities = [0, 1, 1, 1, 2]
|
194
195
|
@workers.each do |worker|
|
195
|
-
worker_capacity =
|
196
|
+
worker_capacity = possible_capacities.sample
|
196
197
|
if worker.issue.nil? || worker.issue.done?
|
197
198
|
type = lucky?(89) ? 'Story' : 'Bug'
|
198
199
|
worker.issue = next_issue_for worker: worker, date: date, type: type
|
@@ -13,7 +13,7 @@ class InfoDumper
|
|
13
13
|
path = "#{@target_dir}#{prefix}_issues/#{key}.json"
|
14
14
|
path = "#{@target_dir}#{prefix}_issues"
|
15
15
|
Dir.foreach path do |file|
|
16
|
-
if file
|
16
|
+
if file.match?(/^#{key}.+\.json$/)
|
17
17
|
issue = Issue.new raw: JSON.parse(File.read(File.join(path, file))), board: nil
|
18
18
|
dump issue
|
19
19
|
end
|
@@ -74,4 +74,4 @@ if __FILE__ == $PROGRAM_NAME
|
|
74
74
|
ARGV.each do |key|
|
75
75
|
InfoDumper.new.run key
|
76
76
|
end
|
77
|
-
end
|
77
|
+
end
|
data/lib/jirametrics/exporter.rb
CHANGED
@@ -3,37 +3,17 @@
|
|
3
3
|
require 'fileutils'
|
4
4
|
|
5
5
|
class Object
|
6
|
-
def deprecated message
|
7
|
-
text =
|
8
|
-
text << 'Deprecated'
|
9
|
-
text << "(#{date})"
|
10
|
-
text << ': '
|
6
|
+
def deprecated message:
|
7
|
+
text = +''
|
8
|
+
text << 'Deprecated:'
|
11
9
|
text << message
|
12
|
-
text << "\n-> Called from #{caller
|
13
|
-
warn text
|
14
|
-
end
|
15
|
-
|
16
|
-
def assert_jira_behaviour_true condition, &block
|
17
|
-
block.call if ENV['RACK_ENV'] == 'test' # Always expand the block if we're running in a test
|
18
|
-
failed_jira_behaviour(block) unless condition
|
19
|
-
end
|
20
|
-
|
21
|
-
def assert_jira_behaviour_false condition, &block
|
22
|
-
block.call if ENV['RACK_ENV'] == 'test' # Always expand the block if we're running in a test
|
23
|
-
failed_jira_behaviour(block) if condition
|
24
|
-
end
|
25
|
-
|
26
|
-
def failed_jira_behaviour block
|
27
|
-
text = String.new
|
28
|
-
text << 'Jira not doing what we expected. Results may be incorrect: '
|
29
|
-
text << block.call
|
30
|
-
text << "\n-> Called from #{caller[1]}"
|
10
|
+
text << "\n-> Called from #{caller(1..1).first}"
|
31
11
|
warn text
|
32
12
|
end
|
33
13
|
end
|
34
14
|
|
35
15
|
class Exporter
|
36
|
-
attr_reader :project_configs
|
16
|
+
attr_reader :project_configs, :file_system
|
37
17
|
|
38
18
|
def self.configure &block
|
39
19
|
exporter = Exporter.new
|
@@ -43,12 +23,13 @@ class Exporter
|
|
43
23
|
|
44
24
|
def self.instance = @@instance
|
45
25
|
|
46
|
-
def initialize
|
26
|
+
def initialize file_system: FileSystem.new
|
47
27
|
@project_configs = []
|
48
28
|
@timezone_offset = '+00:00'
|
49
29
|
@target_path = '.'
|
50
30
|
@holiday_dates = []
|
51
31
|
@downloading = false
|
32
|
+
@file_system = file_system
|
52
33
|
end
|
53
34
|
|
54
35
|
def export name_filter:
|
@@ -62,14 +43,19 @@ class Exporter
|
|
62
43
|
@downloading = true
|
63
44
|
logfile_name = 'downloader.log'
|
64
45
|
File.open logfile_name, 'w' do |logfile|
|
46
|
+
file_system.logfile = logfile
|
47
|
+
file_system.logfile_name = logfile_name
|
48
|
+
|
65
49
|
each_project_config(name_filter: name_filter) do |project|
|
66
50
|
project.evaluate_next_level
|
67
51
|
next if project.aggregated_project?
|
68
52
|
|
69
53
|
project.download_config.run
|
70
|
-
downloader = Downloader.new(
|
71
|
-
|
72
|
-
|
54
|
+
downloader = Downloader.new(
|
55
|
+
download_config: project.download_config,
|
56
|
+
file_system: file_system,
|
57
|
+
jira_gateway: JiraGateway.new(file_system: file_system)
|
58
|
+
)
|
73
59
|
downloader.run
|
74
60
|
end
|
75
61
|
end
|
@@ -107,7 +93,7 @@ class Exporter
|
|
107
93
|
end
|
108
94
|
|
109
95
|
def jira_config filename = nil
|
110
|
-
@jira_config =
|
96
|
+
@jira_config = file_system.load_json(filename) unless filename.nil?
|
111
97
|
@jira_config
|
112
98
|
end
|
113
99
|
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class FileSystem
|
6
|
+
attr_accessor :logfile, :logfile_name
|
7
|
+
|
8
|
+
def load_json filename, fail_on_error: true
|
9
|
+
return nil if fail_on_error == false && File.exist?(filename) == false
|
10
|
+
|
11
|
+
JSON.parse File.read(filename)
|
12
|
+
end
|
13
|
+
|
14
|
+
def save_json json:, filename:
|
15
|
+
file_path = File.dirname(filename)
|
16
|
+
FileUtils.mkdir_p file_path unless File.exist?(file_path)
|
17
|
+
|
18
|
+
File.write(filename, JSON.pretty_generate(compress json))
|
19
|
+
end
|
20
|
+
|
21
|
+
def log message
|
22
|
+
logfile.puts message
|
23
|
+
end
|
24
|
+
|
25
|
+
# In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
|
26
|
+
# cases where this simple compression will drop the filesize by half.
|
27
|
+
def compress node
|
28
|
+
if node.is_a? Hash
|
29
|
+
node.reject! { |_key, value| value.nil? || (value.is_a?(Array) && value.empty?) }
|
30
|
+
node.each_value { |value| compress value }
|
31
|
+
elsif node.is_a? Array
|
32
|
+
node.each { |a| compress a }
|
33
|
+
end
|
34
|
+
node
|
35
|
+
end
|
36
|
+
end
|
@@ -5,15 +5,6 @@ require 'jirametrics/grouping_rules'
|
|
5
5
|
|
6
6
|
module GroupableIssueChart
|
7
7
|
def init_configuration_block user_provided_block, &default_block
|
8
|
-
# The user provided a block but it's using the old deprecated style
|
9
|
-
if user_provided_block && user_provided_block.arity == 1
|
10
|
-
deprecated date: '2022-10-02', message: "#{self.class}: Use the new grouping_rules syntax"
|
11
|
-
grouping_rules do |issue, rules|
|
12
|
-
rules.label, rules.color = user_provided_block.call(issue)
|
13
|
-
end
|
14
|
-
return
|
15
|
-
end
|
16
|
-
|
17
8
|
if user_provided_block
|
18
9
|
instance_eval(&user_provided_block)
|
19
10
|
return if @group_by_block
|
@@ -17,7 +17,7 @@ class HierarchyTable < ChartBase
|
|
17
17
|
def run
|
18
18
|
tree_organizer = TreeOrganizer.new issues: @issues
|
19
19
|
unless tree_organizer.cyclical_links.empty?
|
20
|
-
message =
|
20
|
+
message = +''
|
21
21
|
message << '<p>Found cyclical links in the parent hierarchy. This is an error and should be '
|
22
22
|
message << 'fixed.</p><ul>'
|
23
23
|
tree_organizer.cyclical_links.each do |link|
|
@@ -19,7 +19,7 @@ class HtmlReportConfig
|
|
19
19
|
def cycletime label = nil, &block
|
20
20
|
# TODO: This is about to become deprecated
|
21
21
|
|
22
|
-
@file_config.project_config.all_boards.
|
22
|
+
@file_config.project_config.all_boards.each_value do |board|
|
23
23
|
raise 'Multiple cycletimes not supported yet' if board.cycletime
|
24
24
|
|
25
25
|
board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
|
@@ -68,12 +68,7 @@ class HtmlReportConfig
|
|
68
68
|
execute_chart AgingWorkBarChart.new(block)
|
69
69
|
end
|
70
70
|
|
71
|
-
def aging_work_table
|
72
|
-
if priority_name
|
73
|
-
deprecated message: 'priority name should no longer be passed into the chart. Specify it in the ' \
|
74
|
-
'board declaration. See https://github.com/mikebowler/jira-export/wiki/Deprecated',
|
75
|
-
date: '2022-12-26'
|
76
|
-
end
|
71
|
+
def aging_work_table &block
|
77
72
|
execute_chart AgingWorkTable.new(block)
|
78
73
|
end
|
79
74
|
|
@@ -81,11 +76,6 @@ class HtmlReportConfig
|
|
81
76
|
execute_chart CycletimeScatterplot.new block
|
82
77
|
end
|
83
78
|
|
84
|
-
def total_wip_over_time_chart &block
|
85
|
-
puts 'Deprecated(total_wip_over_time_chart). Use daily_wip_by_age_chart instead.'
|
86
|
-
execute_chart DailyWipByAgeChart.new block
|
87
|
-
end
|
88
|
-
|
89
79
|
def daily_wip_chart &block
|
90
80
|
execute_chart DailyWipChart.new(block)
|
91
81
|
end
|
@@ -106,17 +96,7 @@ class HtmlReportConfig
|
|
106
96
|
execute_chart ThroughputChart.new(block)
|
107
97
|
end
|
108
98
|
|
109
|
-
def
|
110
|
-
puts 'Deprecated(blocked_stalled_chart). Use daily_wip_by_blocked_stalled_chart instead.'
|
111
|
-
execute_chart DailyWipByBlockedStalledChart.new
|
112
|
-
end
|
113
|
-
|
114
|
-
def expedited_chart priority_name = nil
|
115
|
-
if priority_name
|
116
|
-
deprecated message: 'priority name should no longer be passed into the chart. Specify it in the ' \
|
117
|
-
'board declaration. See https://github.com/mikebowler/jira-export/wiki/Deprecated',
|
118
|
-
date: '2022-12-26'
|
119
|
-
end
|
99
|
+
def expedited_chart
|
120
100
|
execute_chart ExpeditedChart.new
|
121
101
|
end
|
122
102
|
|
@@ -125,11 +105,11 @@ class HtmlReportConfig
|
|
125
105
|
end
|
126
106
|
|
127
107
|
def random_color
|
128
|
-
"
|
108
|
+
"##{Random.bytes(3).unpack1('H*')}"
|
129
109
|
end
|
130
110
|
|
131
111
|
def html string, type: :body
|
132
|
-
raise "Unexpected type: #{type}" unless [
|
112
|
+
raise "Unexpected type: #{type}" unless %i[body header].include? type
|
133
113
|
|
134
114
|
@sections << [string, type]
|
135
115
|
end
|
@@ -161,11 +141,6 @@ class HtmlReportConfig
|
|
161
141
|
end
|
162
142
|
end
|
163
143
|
|
164
|
-
def discarded_changes_report
|
165
|
-
puts 'Deprecated(discarded_changes_report) No need to specify this anymore as this information is ' \
|
166
|
-
'now included in the data quality checks.'
|
167
|
-
end
|
168
|
-
|
169
144
|
def dependency_chart &block
|
170
145
|
execute_chart DependencyChart.new block
|
171
146
|
end
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -54,11 +54,6 @@ class Issue
|
|
54
54
|
Status.new raw: @raw['fields']['status']
|
55
55
|
end
|
56
56
|
|
57
|
-
def status_id
|
58
|
-
puts 'DEPRECATED(Issue.status_id) Call Issue.status.id instead'
|
59
|
-
status.id
|
60
|
-
end
|
61
|
-
|
62
57
|
def labels = @raw['fields']['labels'] || []
|
63
58
|
|
64
59
|
def author = @raw['fields']['creator']&.[]('displayName') || ''
|
@@ -270,7 +265,6 @@ class Issue
|
|
270
265
|
# By doing this, we're able to eliminate a lot of duplicated code in charts.
|
271
266
|
mock_change = ChangeItem.new time: end_time, author: '', artificial: true, raw: { 'field' => '' }
|
272
267
|
(changes + [mock_change]).each do |change|
|
273
|
-
|
274
268
|
previous_was_active = false if check_for_stalled(
|
275
269
|
change_time: change.time,
|
276
270
|
previous_change_time: previous_change_time,
|
@@ -388,7 +382,7 @@ class Issue
|
|
388
382
|
if expedited_names.include? change.value
|
389
383
|
expedited_start = change.time.to_date if expedited_start.nil?
|
390
384
|
else
|
391
|
-
return true if expedited_start && (expedited_start..change.time.to_date).
|
385
|
+
return true if expedited_start && (expedited_start..change.time.to_date).cover?(date)
|
392
386
|
|
393
387
|
expedited_start = nil
|
394
388
|
end
|
@@ -19,13 +19,6 @@ class IssueLink
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def direction
|
22
|
-
assert_jira_behaviour_false(raw['inwardIssue'].nil? && raw['outwardIssue'].nil?) do
|
23
|
-
"Found an issue link with neither inward nor outward references: #{raw}"
|
24
|
-
end
|
25
|
-
assert_jira_behaviour_false(raw['inwardIssue'] && raw['outwardIssue']) do
|
26
|
-
"Found an issue link that has both inward and outward references in the same link: #{raw}"
|
27
|
-
end
|
28
|
-
|
29
22
|
if raw['inwardIssue']
|
30
23
|
:inward
|
31
24
|
else
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'json'
|
5
|
+
require 'English'
|
6
|
+
|
7
|
+
class JiraGateway
|
8
|
+
attr_accessor :ignore_ssl_errors, :jira_url
|
9
|
+
|
10
|
+
def initialize file_system:
|
11
|
+
@file_system = file_system
|
12
|
+
end
|
13
|
+
|
14
|
+
def call_url relative_url:
|
15
|
+
command = make_curl_command url: "#{@jira_url}#{relative_url}"
|
16
|
+
JSON.parse call_command command
|
17
|
+
end
|
18
|
+
|
19
|
+
def call_command command
|
20
|
+
@file_system.log " #{command.gsub(/\s+/, ' ')}"
|
21
|
+
result = `#{command}`
|
22
|
+
@file_system.log result unless $CHILD_STATUS.success?
|
23
|
+
return result if $CHILD_STATUS.success?
|
24
|
+
|
25
|
+
@file_system.log "Failed call with exit status #{$CHILD_STATUS.exitstatus}."
|
26
|
+
raise "Failed call with exit status #{$CHILD_STATUS.exitstatus}. " \
|
27
|
+
"See #{@file_system.logfile_name} for details"
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_jira_config jira_config
|
31
|
+
@jira_url = jira_config['url']
|
32
|
+
raise "Must specify URL in config" if @jira_url.nil?
|
33
|
+
|
34
|
+
@jira_email = jira_config['email']
|
35
|
+
@jira_api_token = jira_config['api_token']
|
36
|
+
@jira_personal_access_token = jira_config['personal_access_token']
|
37
|
+
|
38
|
+
raise 'When specifying an api-token, you must also specify email' if @jira_api_token && !@jira_email
|
39
|
+
|
40
|
+
if @jira_api_token && @jira_personal_access_token
|
41
|
+
raise "You can't specify both an api-token and a personal-access-token. They don't work together."
|
42
|
+
end
|
43
|
+
|
44
|
+
@cookies = (jira_config['cookies'] || []).collect { |key, value| "#{key}=#{value}" }.join(';')
|
45
|
+
end
|
46
|
+
|
47
|
+
def make_curl_command url:
|
48
|
+
command = 'curl'
|
49
|
+
command += ' -s'
|
50
|
+
command += ' -k' if @ignore_ssl_errors
|
51
|
+
command += " --cookie #{@cookies.inspect}" unless @cookies.empty?
|
52
|
+
command += " --user #{@jira_email}:#{@jira_api_token}" if @jira_api_token
|
53
|
+
command += " -H \"Authorization: Bearer #{@jira_personal_access_token}\"" if @jira_personal_access_token
|
54
|
+
command += ' --request GET'
|
55
|
+
command += ' --header "Accept: application/json"'
|
56
|
+
command += " --url \"#{url}\""
|
57
|
+
command
|
58
|
+
end
|
59
|
+
end
|