issue_scheduler 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +28 -0
- data/CHANGELOG.md +44 -0
- data/Dockerfile.changelog-rs +12 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +99 -0
- data/Rakefile +98 -0
- data/examples/create-issues +41 -0
- data/exe/issue-scheduler +8 -0
- data/exe/issue-scheduler-admin-ui +8 -0
- data/issue_scheduler.gemspec +56 -0
- data/lib/issue_scheduler/admin_ui.ru +23 -0
- data/lib/issue_scheduler/config.rb +233 -0
- data/lib/issue_scheduler/issue_template.rb +318 -0
- data/lib/issue_scheduler/server.rb +110 -0
- data/lib/issue_scheduler/version.rb +6 -0
- data/lib/issue_scheduler.rb +70 -0
- data/sig/issue_scheduler.rbs +4 -0
- metadata +265 -0
@@ -0,0 +1,233 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module IssueScheduler
|
6
|
+
# Manage the configuration for the YCA EOL Dashboard program
|
7
|
+
# @api public
|
8
|
+
class Config
|
9
|
+
# Create a new Config object from a given YAML config
|
10
|
+
# @example
|
11
|
+
# config_yaml = <<~CONFIG
|
12
|
+
# { username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic }
|
13
|
+
# CONFIG
|
14
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
15
|
+
#
|
16
|
+
# @param config_hash [Hash] the config object as a hash
|
17
|
+
#
|
18
|
+
def initialize(config_hash)
|
19
|
+
@config = DEFAULT_VALUES.merge(config_hash)
|
20
|
+
assert_no_unexpected_keys(config)
|
21
|
+
assert_all_values_are_non_nil(config)
|
22
|
+
end
|
23
|
+
|
24
|
+
# The username to use when authenticating with Jira
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# config_yaml = <<~CONFIG
|
28
|
+
# { username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic }
|
29
|
+
# CONFIG
|
30
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
31
|
+
# config.username #=> 'user'
|
32
|
+
#
|
33
|
+
# @return [String] the username
|
34
|
+
#
|
35
|
+
def username
|
36
|
+
config[:username]
|
37
|
+
end
|
38
|
+
|
39
|
+
# The password to use when authenticating with Jira
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# config_yaml = <<~CONFIG
|
43
|
+
# { username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic }
|
44
|
+
# CONFIG
|
45
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
46
|
+
# config.password #=> 'pass'
|
47
|
+
#
|
48
|
+
# @return [String] the password
|
49
|
+
#
|
50
|
+
def password
|
51
|
+
config[:password]
|
52
|
+
end
|
53
|
+
|
54
|
+
# The URL of the Jira server
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# config_yaml = <<~CONFIG
|
58
|
+
# { username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic }
|
59
|
+
# CONFIG
|
60
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
61
|
+
# config.site #=> 'https://jira.mydomain.com'
|
62
|
+
#
|
63
|
+
# @return [String] the Jira URL
|
64
|
+
#
|
65
|
+
def site
|
66
|
+
config[:site]
|
67
|
+
end
|
68
|
+
|
69
|
+
# The context path to append to the Jira server URL
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# config_yaml = <<~CONFIG
|
73
|
+
# { username: user, password: pass, site: https://jira.mydomain.com, context_path: /jira, auth_type: basic }
|
74
|
+
# CONFIG
|
75
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
76
|
+
# config.context_path #=> '/jira'
|
77
|
+
#
|
78
|
+
# @return [String] the context path
|
79
|
+
#
|
80
|
+
def context_path
|
81
|
+
config[:context_path]
|
82
|
+
end
|
83
|
+
|
84
|
+
# The authentication type to use when authenticating with Jira
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# config_yaml = <<~CONFIG
|
88
|
+
# { username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic }
|
89
|
+
# CONFIG
|
90
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
91
|
+
# config.auth_type #=> 'basic'
|
92
|
+
#
|
93
|
+
# @return ['basic', 'oauth'] the authentication type
|
94
|
+
#
|
95
|
+
def auth_type
|
96
|
+
config[:auth_type]
|
97
|
+
end
|
98
|
+
|
99
|
+
# The glob pattern to use to find issue files
|
100
|
+
#
|
101
|
+
# @example
|
102
|
+
# config_yaml = <<~CONFIG
|
103
|
+
# {
|
104
|
+
# username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic,
|
105
|
+
# issue_templates: 'config/**/*.yaml'
|
106
|
+
# }
|
107
|
+
# CONFIG
|
108
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
109
|
+
# config.issue_templates #=> 'config/**/*.yaml'
|
110
|
+
#
|
111
|
+
# @return [String] the glob pattern
|
112
|
+
#
|
113
|
+
# @see https://ruby-doc.org/core-3.1.2/Dir.html#method-c-glob Dir.glob
|
114
|
+
#
|
115
|
+
def issue_templates
|
116
|
+
config[:issue_templates]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Load the issue templates from the configured glob pattern(s) in issue_templates
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# config_yaml = <<~CONFIG
|
123
|
+
# {
|
124
|
+
# username: user, password: pass, site: https://jira.mydomain.com, auth_type: basic,
|
125
|
+
# issue_templates: 'config/**/*.yaml'
|
126
|
+
# }
|
127
|
+
# CONFIG
|
128
|
+
# config = IssueScheduler::Config.new(config_yaml)
|
129
|
+
# config.load_issue_templates
|
130
|
+
# IssueScheduler::IssueTemplate.size #=> number of issue templates loaded from config/**/*.yaml
|
131
|
+
#
|
132
|
+
# @return [void] as a side effect the issue templates are loaded
|
133
|
+
#
|
134
|
+
def load_issue_templates
|
135
|
+
Dir[*Array(issue_templates)].each do |file|
|
136
|
+
template_hash = { name: file, **IssueScheduler.load_yaml(file) }
|
137
|
+
template = IssueScheduler::IssueTemplate.new(template_hash)
|
138
|
+
if template.valid?
|
139
|
+
template.save
|
140
|
+
else
|
141
|
+
warn "Skipping invalid issue template #{file}: #{template.errors.full_messages.join(', ')}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# The config in the form of a Hash
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# config = IssueScheduler::Config.new(<<~YAML)
|
150
|
+
# username: jcouball
|
151
|
+
# password: my_password
|
152
|
+
# site: https://jira.example.com
|
153
|
+
# context_path: ''
|
154
|
+
# auth_type: basic
|
155
|
+
# issue_templates: '~/scheduled_issues/**/*.yaml'
|
156
|
+
# YAML
|
157
|
+
# config.to_h #=> { 'username' => 'jcouball', 'password' => 'my_password', ... }
|
158
|
+
#
|
159
|
+
# @return [Hash] the config
|
160
|
+
#
|
161
|
+
def to_h
|
162
|
+
config
|
163
|
+
end
|
164
|
+
|
165
|
+
# Creates an options hash suitable to pass to Jira::Client.new
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# config = IssueScheduler::Config.new(<<~YAML)
|
169
|
+
# username: jcouball
|
170
|
+
# password: my_password
|
171
|
+
# site: https://jira.example.com
|
172
|
+
# context_path: ''
|
173
|
+
# auth_type: basic
|
174
|
+
# issue_templates: '~/scheduled_issues/**/*.yaml'
|
175
|
+
# YAML
|
176
|
+
# config.to_jira_options #=> {
|
177
|
+
# username: 'jcouball',
|
178
|
+
# password: 'my_password',
|
179
|
+
# site: 'https://jira.example.com',
|
180
|
+
# context_path: '',
|
181
|
+
# auth_type: 'basic'
|
182
|
+
# }
|
183
|
+
#
|
184
|
+
# @return [Hash] the options hash
|
185
|
+
#
|
186
|
+
def to_jira_options
|
187
|
+
config.slice(:username, :password, :site, :context_path, :auth_type)
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
# Raise a RuntimeError if the config contains unexpected keys
|
193
|
+
# @raise [RuntimeError] if the config is invalid
|
194
|
+
# @return [Void]
|
195
|
+
# @api private
|
196
|
+
def assert_no_unexpected_keys(config_hash)
|
197
|
+
unexpected_keys = config_hash.keys - DEFAULT_VALUES.keys
|
198
|
+
raise "Unexpected configuration keys : #{unexpected_keys}" unless
|
199
|
+
unexpected_keys.empty?
|
200
|
+
end
|
201
|
+
|
202
|
+
# Raise a RuntimeError if the config contains a nil value
|
203
|
+
# @raise [RuntimeError] if the config is invalid
|
204
|
+
# @return [Void]
|
205
|
+
# @api private
|
206
|
+
def assert_all_values_are_non_nil(config_hash)
|
207
|
+
keys_of_missing_values = config_hash.select { |_k, v| v.nil? }.keys
|
208
|
+
raise "Missing configuration values: #{keys_of_missing_values}" unless
|
209
|
+
keys_of_missing_values.empty?
|
210
|
+
end
|
211
|
+
|
212
|
+
# The config hash from config_yaml
|
213
|
+
# @return [Hash] the config hash
|
214
|
+
# @api private
|
215
|
+
attr_reader :config
|
216
|
+
|
217
|
+
# Defines the allows keys and default values for each key
|
218
|
+
#
|
219
|
+
# A nil value indicates that the key does not have a default value and must
|
220
|
+
# be specified in the config_yaml.
|
221
|
+
#
|
222
|
+
# @return [Hash] the allows keys and their default values
|
223
|
+
# @api private
|
224
|
+
DEFAULT_VALUES = {
|
225
|
+
username: nil,
|
226
|
+
password: nil,
|
227
|
+
site: nil,
|
228
|
+
context_path: '',
|
229
|
+
auth_type: 'basic',
|
230
|
+
issue_templates: '~/.issue_scheduler/issue_templates/**/*.yaml'
|
231
|
+
}.freeze
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,318 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
require 'active_model_persistence'
|
5
|
+
|
6
|
+
require 'pp'
|
7
|
+
require 'fugit'
|
8
|
+
|
9
|
+
module IssueScheduler
|
10
|
+
# A template for an issue
|
11
|
+
#
|
12
|
+
# @example Create a new issue template specifying all attributes
|
13
|
+
# attributes = {
|
14
|
+
# name: 'weekly_status_report.yaml',
|
15
|
+
# cron: '0 7 * * 6', # every Friday at 7:00 AM
|
16
|
+
# project: 'MYJIRA',
|
17
|
+
# component: 'Management',
|
18
|
+
# type: 'Story',
|
19
|
+
# summary: 'Weekly status report',
|
20
|
+
# description: "Update the weekly status report\n\nhttp://mydomain.com/status",
|
21
|
+
# due_date: '2022-05-06'
|
22
|
+
# }
|
23
|
+
#
|
24
|
+
# template = IssueScheduler::IssueTemplate.new(attributes)
|
25
|
+
#
|
26
|
+
# @example Create a new issue template specifying only required attributes
|
27
|
+
# attributes = {
|
28
|
+
# name: 'weekly_status_report.yaml',
|
29
|
+
# cron: '0 7 * * 6', # every Friday at 7:00 AM
|
30
|
+
# project: 'MYJIRA',
|
31
|
+
# summary: 'Weekly status report',
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
# template = IssueScheduler::IssueTemplate.new(attributes)
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
class IssueTemplate
|
38
|
+
include ActiveModelPersistence::Persistence
|
39
|
+
include ActiveModel::Validations::Callbacks
|
40
|
+
|
41
|
+
# @!attribute [rw] name
|
42
|
+
# The template name
|
43
|
+
#
|
44
|
+
# * name must not be present
|
45
|
+
# * name must be unique across all IssueTemplate objects
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# template.name #=> "weekly_status_report"
|
49
|
+
#
|
50
|
+
# @return [String] the template name
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
|
54
|
+
attribute :name, :string
|
55
|
+
validates :name, presence: true
|
56
|
+
|
57
|
+
# @!attribute [rw] cron
|
58
|
+
# A cron string that specifies when the issue should be created
|
59
|
+
#
|
60
|
+
# * cron must be present
|
61
|
+
# * cron must be a valid cron string
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# # 7 AM Monday - Friday
|
65
|
+
# template.cron #=> "0 7 * * 1,2,3,4,5"
|
66
|
+
#
|
67
|
+
# @return [String] the cron string
|
68
|
+
#
|
69
|
+
# @see https://github.com/floraison/fugit The fugit gem
|
70
|
+
#
|
71
|
+
# @api public
|
72
|
+
|
73
|
+
attribute :cron, :string
|
74
|
+
validates :cron, presence: true
|
75
|
+
validates_each :cron do |record, attr, value|
|
76
|
+
record.errors.add(attr, 'is not a valid cron string') unless Fugit.parse_cron(value)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @!attribute [rw] project
|
80
|
+
# The JIRA project name
|
81
|
+
#
|
82
|
+
# * project must be present
|
83
|
+
# * project is upcased
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# template.project #=> "MYPROJECT"
|
87
|
+
#
|
88
|
+
# @return [String] the project name
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
|
92
|
+
attribute :project, :string
|
93
|
+
validates :project, presence: true
|
94
|
+
|
95
|
+
def project=(project)
|
96
|
+
super(project.is_a?(String) ? project.upcase : project)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @!attribute [rw] component
|
100
|
+
# The JIRA component name
|
101
|
+
#
|
102
|
+
# * component is optional and defaults to nil
|
103
|
+
# * component may be nil but not an empty string
|
104
|
+
#
|
105
|
+
# The component name is optional and will be set to nil if not given.
|
106
|
+
#
|
107
|
+
# @example
|
108
|
+
# template.component #=> "MYCOMPONENT"
|
109
|
+
#
|
110
|
+
# @return [String] the component name
|
111
|
+
#
|
112
|
+
# @api public
|
113
|
+
|
114
|
+
attribute :component, :string, default: nil
|
115
|
+
validates :component, presence: true, allow_nil: true
|
116
|
+
|
117
|
+
# @!attribute [rw] summary
|
118
|
+
# The JIRA issue summary
|
119
|
+
#
|
120
|
+
# * summary must be present
|
121
|
+
#
|
122
|
+
# @example
|
123
|
+
# template.summary #=> "Take out the trash"
|
124
|
+
#
|
125
|
+
# @return [String] the JIRA issue summary
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
|
129
|
+
attribute :summary, :string
|
130
|
+
validates :summary, presence: true
|
131
|
+
|
132
|
+
# @!attribute [rw] description
|
133
|
+
# The JIRA issue description
|
134
|
+
#
|
135
|
+
# * description is optional and defaults to nil
|
136
|
+
# * description may be nil but not an empty string
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# template.description #=> "Take out the trash in:\n- kitchen\n- bathroom\n- bedroom"
|
140
|
+
#
|
141
|
+
# @return [String] the JIRA issue description
|
142
|
+
#
|
143
|
+
# @api public
|
144
|
+
|
145
|
+
attribute :description, :string, default: nil
|
146
|
+
validates :description, presence: true, allow_nil: true
|
147
|
+
|
148
|
+
# @!attribute [rw] type
|
149
|
+
# The name of the JIRA issue type
|
150
|
+
#
|
151
|
+
# * type is optional and defaults to nil
|
152
|
+
# * type may be nil but not an empty string
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# template.type #=> "Story"
|
156
|
+
#
|
157
|
+
# @return [String] the JIRA issue type
|
158
|
+
#
|
159
|
+
# @api public
|
160
|
+
|
161
|
+
attribute :type, :string, default: nil
|
162
|
+
validates :type, presence: true, allow_nil: true
|
163
|
+
|
164
|
+
# @!attribute [rw] due_date
|
165
|
+
# The JIRA issue due date
|
166
|
+
#
|
167
|
+
# * due_date is optional and defaults to nil
|
168
|
+
# * If non-nil, due_date must be a Date object or parsable by Date.parse
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# template.due_date #=> #<Date: 2022-05-03 ((2459703j,0s,0n),+0s,2299161j)>
|
172
|
+
#
|
173
|
+
# @return [Date, nil] the JIRA issue description
|
174
|
+
#
|
175
|
+
# @api public
|
176
|
+
|
177
|
+
attribute :due_date, :date
|
178
|
+
validates :due_date, presence: true, allow_nil: true
|
179
|
+
|
180
|
+
validates_each :due_date do |record, attr, value|
|
181
|
+
before_type_cast = record.due_date_before_type_cast
|
182
|
+
record.errors.add(attr, "'#{before_type_cast}' is not a valid date") if value.nil? && !before_type_cast.nil?
|
183
|
+
end
|
184
|
+
|
185
|
+
def due_date=(date)
|
186
|
+
@due_date_before_type_cast = date
|
187
|
+
super(date)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Sets the primary key to `:name`
|
191
|
+
# @return [Symbol] the attribute name of the primary key
|
192
|
+
# @api private
|
193
|
+
def self.primary_key
|
194
|
+
:name
|
195
|
+
end
|
196
|
+
|
197
|
+
# The due_date supplied by the user since ActiveModel sets it to nil of invalid
|
198
|
+
# @return [Date, String, nil] the due_date before type cast
|
199
|
+
# @api private
|
200
|
+
attr_reader :due_date_before_type_cast
|
201
|
+
|
202
|
+
# private
|
203
|
+
|
204
|
+
# Create a new issue template from the given template_yaml
|
205
|
+
#
|
206
|
+
# @example
|
207
|
+
# template_yaml = <<~YAML
|
208
|
+
# template_name: Take out the trash
|
209
|
+
# cron: '0 7 * * 1,2,3,4,5 America/Los_Angeles' # Mon-Fri, 7 AM UTC
|
210
|
+
# project: 'MYPROJECT'
|
211
|
+
# component: 'Internal'
|
212
|
+
# summary: 'Take out the trash'
|
213
|
+
# description: |
|
214
|
+
# Take out the trash in the following rooms:
|
215
|
+
# - kitchen
|
216
|
+
# - bathroom
|
217
|
+
# - bedroom
|
218
|
+
# type: 'Story'
|
219
|
+
# due_date: '2022-05-03'
|
220
|
+
# YAML
|
221
|
+
#
|
222
|
+
# template = IssueScheduler::IssueTemplate.new(template_yaml)
|
223
|
+
# template.recurrance_rule #=> "FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR"
|
224
|
+
#
|
225
|
+
# @param template_yaml [String] the YAML string to parse
|
226
|
+
#
|
227
|
+
|
228
|
+
# Define getter methods for allowed keys
|
229
|
+
# ATTRIBUTES.each do |key, _value|
|
230
|
+
# define_method(key) do
|
231
|
+
# template[__method__]
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
|
235
|
+
# private
|
236
|
+
|
237
|
+
# The template Hash read from YAML
|
238
|
+
# @return [Hash]
|
239
|
+
# @api private
|
240
|
+
# attr_reader :template
|
241
|
+
|
242
|
+
# Reads the config file returning a hash
|
243
|
+
# @raise [RuntimeError] if the config file can not be read or is not valid
|
244
|
+
# @return [Hash<String, String] the config hash read from config_yaml
|
245
|
+
# @api private
|
246
|
+
# def parse_template(template_yaml)
|
247
|
+
# YAML.safe_load(template_yaml, permitted_classes: [Symbol, Date], aliases: true, symbolize_names: true)
|
248
|
+
# rescue Psych::SyntaxError => e
|
249
|
+
# raise "Error parsing YAML template: #{e.message}"
|
250
|
+
# end
|
251
|
+
|
252
|
+
# Set default values for keys that are not given in the template
|
253
|
+
#
|
254
|
+
# @param template_from_yaml [Hash] the template from YAML
|
255
|
+
# @return [Hash] the template with default values
|
256
|
+
# @api private
|
257
|
+
# def template_with_default_values(template_from_yaml)
|
258
|
+
# template = {}
|
259
|
+
# ATTRIBUTES.each { |k, v| template[k] = v[:default] if v.key?(:default) }
|
260
|
+
# template.merge!(template_from_yaml)
|
261
|
+
# end
|
262
|
+
|
263
|
+
# Run the conversion functions for values that are given in the template
|
264
|
+
#
|
265
|
+
# @return [Void] the template is modified in place
|
266
|
+
# @api private
|
267
|
+
# def run_value_conversions!
|
268
|
+
# template.each do |key, value|
|
269
|
+
# if (conversion = ATTRIBUTES.dig(key, :conversion))
|
270
|
+
# template[key] = conversion.call(value)
|
271
|
+
# end
|
272
|
+
# end
|
273
|
+
# end
|
274
|
+
|
275
|
+
# Raise a RuntimeError if any validation method returns false
|
276
|
+
#
|
277
|
+
# @raise [RuntimeError] if the config is invalid
|
278
|
+
# @return [Void]
|
279
|
+
# @api private
|
280
|
+
# def assert_all_values_are_valid
|
281
|
+
# template.each do |key, value|
|
282
|
+
# if (validation = ATTRIBUTES.dig(key, :validation)) && !validation.call(value)
|
283
|
+
# raise "Invalid value for #{key}: #{value.pretty_inspect}"
|
284
|
+
# end
|
285
|
+
# end
|
286
|
+
# end
|
287
|
+
|
288
|
+
# Raise a RuntimeError if the config is not a Hash
|
289
|
+
# @raise [RuntimeError] if the config is invalid
|
290
|
+
# @return [Void]
|
291
|
+
# @api private
|
292
|
+
# def assert_template_is_an_object(template_from_yaml)
|
293
|
+
# raise "YAML issue template is not an object, contained '#{template_from_yaml}'" unless
|
294
|
+
# template_from_yaml.is_a?(Hash)
|
295
|
+
# end
|
296
|
+
|
297
|
+
# Raise a RuntimeError if the config contains unexpected keys
|
298
|
+
# @raise [RuntimeError] if the config is invalid
|
299
|
+
# @return [Void]
|
300
|
+
# @api private
|
301
|
+
# def assert_no_unexpected_keys
|
302
|
+
# unexpected_keys = template.keys - ATTRIBUTES.keys
|
303
|
+
# raise "Unexpected issue template keys: #{unexpected_keys}" unless
|
304
|
+
# unexpected_keys.empty?
|
305
|
+
# end
|
306
|
+
|
307
|
+
# Raise a RuntimeError if the config contains a nil value
|
308
|
+
# @raise [RuntimeError] if the config is invalid
|
309
|
+
# @return [Void]
|
310
|
+
# @api private
|
311
|
+
# def assert_all_required_keys_are_given
|
312
|
+
# required_attributes = ATTRIBUTES.select { |_, v| v[:required] }.keys
|
313
|
+
# missing_keys = required_attributes - template.keys
|
314
|
+
# raise "Missing issue template keys: #{missing_keys}" unless
|
315
|
+
# missing_keys.empty?
|
316
|
+
# end
|
317
|
+
end
|
318
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pp'
|
4
|
+
require 'sidekiq'
|
5
|
+
require 'sidekiq-cron'
|
6
|
+
require 'issue_scheduler'
|
7
|
+
require 'jira-ruby'
|
8
|
+
|
9
|
+
issue_config = IssueScheduler::Config.new(IssueScheduler.load_yaml('config/config.yaml'))
|
10
|
+
issue_config.load_issue_templates
|
11
|
+
|
12
|
+
Sidekiq.configure_client do |config|
|
13
|
+
config.redis = { db: 1 }
|
14
|
+
end
|
15
|
+
|
16
|
+
Sidekiq.configure_server do |config|
|
17
|
+
config.redis = { db: 1 }
|
18
|
+
|
19
|
+
config.on(:startup) do
|
20
|
+
Sidekiq::Cron::Job.destroy_all!
|
21
|
+
|
22
|
+
IssueScheduler::IssueTemplate.all.each do |issue_template|
|
23
|
+
job_properties = {
|
24
|
+
'name' => issue_template.name,
|
25
|
+
'description' => issue_template.summary,
|
26
|
+
'cron' => issue_template.cron,
|
27
|
+
'class' => 'IssueWorker',
|
28
|
+
'args' => issue_template.name,
|
29
|
+
'date_as_argument' => true
|
30
|
+
}
|
31
|
+
|
32
|
+
job = Sidekiq::Cron::Job.new(job_properties)
|
33
|
+
raise "Error creating job: #{job.errors.pretty_inspect}" unless job.valid?
|
34
|
+
|
35
|
+
job.save
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# My worker class that creates issues
|
41
|
+
# @api private
|
42
|
+
class IssueWorker
|
43
|
+
include Sidekiq::Worker
|
44
|
+
|
45
|
+
# Create an issue with the given template name
|
46
|
+
# @param template_name [String] the name of the template to use
|
47
|
+
# @param time [Time] the time to use for the issue
|
48
|
+
# @return [void]
|
49
|
+
# @api private
|
50
|
+
def perform(template_name, time)
|
51
|
+
puts "**** IssueWorker#perform called with #{template_name.pretty_inspect.chomp}, #{time.pretty_inspect.chomp}"
|
52
|
+
|
53
|
+
template = load_issue_template(template_name)
|
54
|
+
return unless template
|
55
|
+
|
56
|
+
create_issue(template)
|
57
|
+
|
58
|
+
puts '**** IssueWorker#perform finished'
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Create a Jira issue based on the following template
|
64
|
+
#
|
65
|
+
# @param template [IssueScheduler::IssueTemplate] the template to base the issue from
|
66
|
+
# @return [void]
|
67
|
+
# @api private
|
68
|
+
def create_issue(template)
|
69
|
+
puts ">>>> Creating issue with template: #{template.name}"
|
70
|
+
|
71
|
+
issue = jira_client.Issue.build
|
72
|
+
|
73
|
+
fields = { 'project' => { 'key' => template.project }, 'summary' => template.summary }
|
74
|
+
|
75
|
+
# fields['component'] = { 'key' => template.component } if template.component
|
76
|
+
fields['issuetype'] = { 'name' => template.type } if template.type
|
77
|
+
# fields['description'] = template.description if template.description
|
78
|
+
# fields['duedate'] = template.due_date.strftime('%Y-%m-%d') if template.due_date
|
79
|
+
|
80
|
+
issue.save({ 'fields' => fields })
|
81
|
+
issue.fetch
|
82
|
+
|
83
|
+
puts ">>>> Created issue #{issue.key}"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create and memoize a Jira Client
|
87
|
+
#
|
88
|
+
# @return [JIRA::Client] the Jira client to be used to create issues
|
89
|
+
# @api private
|
90
|
+
def jira_client
|
91
|
+
@jira_client ||= JIRA::Client.new(issue_config.to_jira_options)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create and memoize a IssueScheduler::Config
|
95
|
+
#
|
96
|
+
# @return [IssueScheduler::Config] the config to be used to create issues
|
97
|
+
# @api private
|
98
|
+
def issue_config
|
99
|
+
@issue_config ||= IssueScheduler::Config.new(IssueScheduler.load_yaml('config/config.yaml'))
|
100
|
+
end
|
101
|
+
|
102
|
+
# Load the IssueScheduler::IssueTemplate with the given name
|
103
|
+
#
|
104
|
+
# @return [IssueScheuler::IssueTemplate] the template with the given name
|
105
|
+
# @api private
|
106
|
+
def load_issue_template(template_name)
|
107
|
+
issue_config.load_issue_templates
|
108
|
+
IssueScheduler::IssueTemplate.find(template_name)
|
109
|
+
end
|
110
|
+
end
|