issue_scheduler 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|