jirametrics 1.4 → 2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|