commenter 0.2.1 → 0.2.2
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/.github/workflows/release.yml +0 -1
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +174 -0
- data/README.adoc +439 -18
- data/commenter.gemspec +4 -0
- data/data/github_config_sample.yaml +78 -0
- data/data/github_issue_body_template.liquid +35 -0
- data/data/github_issue_title_template.liquid +1 -0
- data/exe/commenter +1 -1
- data/lib/commenter/cli.rb +137 -4
- data/lib/commenter/comment.rb +62 -3
- data/lib/commenter/comment_sheet.rb +10 -8
- data/lib/commenter/filler.rb +74 -20
- data/lib/commenter/github_integration.rb +452 -0
- data/lib/commenter/parser.rb +10 -6
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +1 -0
- data/schema/iso_comment_2012-03.yaml +38 -0
- data/spec/commenter/comment_sheet_spec.rb +193 -0
- data/spec/commenter/comment_spec.rb +220 -0
- data/spec/commenter/github_integration_spec.rb +183 -0
- data/spec/commenter_spec.rb +0 -1
- metadata +66 -2
@@ -0,0 +1,452 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "octokit"
|
4
|
+
require "liquid"
|
5
|
+
require "yaml"
|
6
|
+
require "dotenv/load"
|
7
|
+
|
8
|
+
module Commenter
|
9
|
+
class GitHubIssueCreator
|
10
|
+
def initialize(config_path, title_template_path = nil, body_template_path = nil)
|
11
|
+
@config = load_config(config_path)
|
12
|
+
@github_client = create_github_client
|
13
|
+
@repo = @config.dig("github", "repository")
|
14
|
+
|
15
|
+
raise "GitHub repository not specified in config" unless @repo
|
16
|
+
|
17
|
+
@title_template = load_liquid_template(title_template_path || default_title_template_path)
|
18
|
+
@body_template = load_liquid_template(body_template_path || default_body_template_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_issues_from_yaml(yaml_file, options = {})
|
22
|
+
data = YAML.load_file(yaml_file)
|
23
|
+
comment_sheet = CommentSheet.from_hash(data)
|
24
|
+
|
25
|
+
# Override stage if provided
|
26
|
+
comment_sheet.stage = options[:stage] if options[:stage]
|
27
|
+
|
28
|
+
results = []
|
29
|
+
comment_sheet.comments.each do |comment|
|
30
|
+
results << if options[:dry_run]
|
31
|
+
preview_issue(comment, comment_sheet)
|
32
|
+
else
|
33
|
+
create_issue(comment, comment_sheet, options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Update YAML with GitHub info after creation (unless dry run)
|
38
|
+
update_yaml_with_github_info(yaml_file, comment_sheet, results, options) unless options[:dry_run]
|
39
|
+
|
40
|
+
results
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def load_config(config_path)
|
46
|
+
YAML.load_file(config_path)
|
47
|
+
rescue Errno::ENOENT
|
48
|
+
raise "Configuration file not found: #{config_path}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_github_client
|
52
|
+
token = @config.dig("github", "token") || ENV["GITHUB_TOKEN"]
|
53
|
+
raise "GitHub token not found. Set GITHUB_TOKEN environment variable or specify in config file." unless token
|
54
|
+
|
55
|
+
Octokit::Client.new(access_token: token)
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_title_template_path
|
59
|
+
File.join(__dir__, "../../data/github_issue_title_template.liquid")
|
60
|
+
end
|
61
|
+
|
62
|
+
def default_body_template_path
|
63
|
+
File.join(__dir__, "../../data/github_issue_body_template.liquid")
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_liquid_template(template_path)
|
67
|
+
content = File.read(template_path)
|
68
|
+
Liquid::Template.parse(content)
|
69
|
+
rescue Errno::ENOENT
|
70
|
+
raise "Template file not found: #{template_path}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def template_variables(comment, comment_sheet)
|
74
|
+
{
|
75
|
+
# Comment sheet variables
|
76
|
+
"stage" => comment_sheet.stage || "",
|
77
|
+
"document" => comment_sheet.document || "",
|
78
|
+
"project" => comment_sheet.project || "",
|
79
|
+
"date" => comment_sheet.date || "",
|
80
|
+
"version" => comment_sheet.version || "",
|
81
|
+
|
82
|
+
# Comment variables
|
83
|
+
"comment_id" => comment.id || "",
|
84
|
+
"body" => comment.body || "",
|
85
|
+
"type" => comment.type || "",
|
86
|
+
"type_full_name" => expand_comment_type(comment.type),
|
87
|
+
"comments" => comment.comments || "",
|
88
|
+
"proposed_change" => comment.proposed_change || "",
|
89
|
+
"observations" => comment.observations || "",
|
90
|
+
"brief_summary" => comment.brief_summary,
|
91
|
+
|
92
|
+
# Locality variables
|
93
|
+
"clause" => comment.clause || "",
|
94
|
+
"element" => comment.element || "",
|
95
|
+
"line_number" => comment.line_number || "",
|
96
|
+
|
97
|
+
# Computed variables
|
98
|
+
"has_observations" => !comment.observations.nil? && !comment.observations.strip.empty?,
|
99
|
+
"has_proposed_change" => !comment.proposed_change.nil? && !comment.proposed_change.strip.empty?,
|
100
|
+
"locality_summary" => format_locality(comment)
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def expand_comment_type(type)
|
105
|
+
case type&.downcase
|
106
|
+
when "ge" then "General"
|
107
|
+
when "te" then "Technical"
|
108
|
+
when "ed" then "Editorial"
|
109
|
+
else type || "Unknown"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def format_locality(comment)
|
114
|
+
parts = []
|
115
|
+
parts << "Clause #{comment.clause}" if comment.clause && !comment.clause.strip.empty?
|
116
|
+
parts << comment.element if comment.element && !comment.element.strip.empty?
|
117
|
+
parts << "Line #{comment.line_number}" if comment.line_number && !comment.line_number.strip.empty?
|
118
|
+
parts.join(", ")
|
119
|
+
end
|
120
|
+
|
121
|
+
def create_issue(comment, comment_sheet, options = {})
|
122
|
+
# Check if issue already exists
|
123
|
+
existing_issue = find_existing_issue(comment)
|
124
|
+
if existing_issue
|
125
|
+
return {
|
126
|
+
comment_id: comment.id,
|
127
|
+
status: :skipped,
|
128
|
+
message: "Issue already exists",
|
129
|
+
issue_url: existing_issue.html_url
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
title = @title_template.render(template_variables(comment, comment_sheet))
|
134
|
+
body = @body_template.render(template_variables(comment, comment_sheet))
|
135
|
+
|
136
|
+
issue_options = {
|
137
|
+
labels: determine_labels(comment, comment_sheet),
|
138
|
+
assignees: determine_assignees(comment, comment_sheet, options),
|
139
|
+
milestone: determine_milestone(comment, comment_sheet, options)
|
140
|
+
}.compact
|
141
|
+
|
142
|
+
begin
|
143
|
+
issue = @github_client.create_issue(@repo, title, body, issue_options)
|
144
|
+
{
|
145
|
+
comment_id: comment.id,
|
146
|
+
status: :created,
|
147
|
+
issue_number: issue.number,
|
148
|
+
issue_url: issue.html_url
|
149
|
+
}
|
150
|
+
rescue Octokit::Error => e
|
151
|
+
{
|
152
|
+
comment_id: comment.id,
|
153
|
+
status: :error,
|
154
|
+
message: e.message
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def preview_issue(comment, comment_sheet)
|
160
|
+
title = @title_template.render(template_variables(comment, comment_sheet))
|
161
|
+
body = @body_template.render(template_variables(comment, comment_sheet))
|
162
|
+
|
163
|
+
{
|
164
|
+
comment_id: comment.id,
|
165
|
+
title: title,
|
166
|
+
body: body,
|
167
|
+
labels: determine_labels(comment, comment_sheet),
|
168
|
+
assignees: determine_assignees(comment, comment_sheet, {}),
|
169
|
+
milestone: determine_milestone(comment, comment_sheet, {})
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
def find_existing_issue(comment)
|
174
|
+
# Search for existing issues with the comment ID in the title
|
175
|
+
query = "repo:#{@repo} in:title #{comment.id}"
|
176
|
+
results = @github_client.search_issues(query)
|
177
|
+
results.items.first
|
178
|
+
rescue Octokit::Error
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
|
182
|
+
def determine_labels(comment, comment_sheet)
|
183
|
+
labels = []
|
184
|
+
|
185
|
+
# Add default labels
|
186
|
+
labels.concat(@config.dig("github", "default_labels") || [])
|
187
|
+
|
188
|
+
# Add stage-specific labels
|
189
|
+
if comment_sheet.stage
|
190
|
+
stage_labels = @config.dig("github", "stage_labels", comment_sheet.stage)
|
191
|
+
labels.concat(stage_labels) if stage_labels
|
192
|
+
end
|
193
|
+
|
194
|
+
# Add comment type label
|
195
|
+
labels << comment.type if comment.type
|
196
|
+
|
197
|
+
labels.uniq
|
198
|
+
end
|
199
|
+
|
200
|
+
def determine_assignees(_comment, _comment_sheet, options)
|
201
|
+
assignees = []
|
202
|
+
|
203
|
+
# Check for override in options
|
204
|
+
if options[:assignee]
|
205
|
+
assignees << options[:assignee]
|
206
|
+
else
|
207
|
+
# Use default assignee from config
|
208
|
+
default_assignee = @config.dig("github", "default_assignee")
|
209
|
+
assignees << default_assignee if default_assignee
|
210
|
+
end
|
211
|
+
|
212
|
+
assignees.compact.uniq
|
213
|
+
end
|
214
|
+
|
215
|
+
def determine_milestone(_comment, comment_sheet, options)
|
216
|
+
# Check for override in options
|
217
|
+
return resolve_milestone_by_name_or_number(options[:milestone]) if options[:milestone]
|
218
|
+
|
219
|
+
# Check for stage-specific milestone
|
220
|
+
if comment_sheet.stage
|
221
|
+
stage_milestone = @config.dig("github", "stage_milestones", comment_sheet.stage)
|
222
|
+
if stage_milestone
|
223
|
+
milestone_number = resolve_milestone_by_name_or_number(stage_milestone)
|
224
|
+
return milestone_number if milestone_number
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Use configured milestone
|
229
|
+
milestone_config = @config.dig("github", "milestone")
|
230
|
+
return nil unless milestone_config
|
231
|
+
|
232
|
+
if milestone_config["number"]
|
233
|
+
milestone_config["number"]
|
234
|
+
elsif milestone_config["name"]
|
235
|
+
resolve_milestone_by_name_or_number(milestone_config["name"])
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def resolve_milestone_by_name_or_number(milestone_identifier)
|
240
|
+
# If it's a number, return it directly
|
241
|
+
return milestone_identifier.to_i if milestone_identifier.to_s.match?(/^\d+$/)
|
242
|
+
|
243
|
+
# Otherwise, search by name
|
244
|
+
find_milestone_by_name(milestone_identifier)
|
245
|
+
end
|
246
|
+
|
247
|
+
def find_milestone_by_name(name)
|
248
|
+
milestones = @github_client.milestones(@repo, state: "all")
|
249
|
+
milestone = milestones.find { |m| m.title == name }
|
250
|
+
milestone&.number
|
251
|
+
rescue Octokit::Error
|
252
|
+
nil
|
253
|
+
end
|
254
|
+
|
255
|
+
def update_yaml_with_github_info(yaml_file, comment_sheet, results, options)
|
256
|
+
# Update comments with GitHub information
|
257
|
+
results.each do |result|
|
258
|
+
next unless result[:status] == :created
|
259
|
+
|
260
|
+
comment = comment_sheet.comments.find { |c| c.id == result[:comment_id] }
|
261
|
+
next unless comment
|
262
|
+
|
263
|
+
# Add GitHub information to the comment
|
264
|
+
comment.github[:issue_number] = result[:issue_number]
|
265
|
+
comment.github[:issue_url] = result[:issue_url]
|
266
|
+
comment.github[:status] = "open"
|
267
|
+
comment.github[:created_at] = Time.now.utc.iso8601
|
268
|
+
end
|
269
|
+
|
270
|
+
# Write updated YAML
|
271
|
+
output_file = options[:output] || yaml_file
|
272
|
+
yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h)
|
273
|
+
File.write(output_file, yaml_content)
|
274
|
+
end
|
275
|
+
|
276
|
+
def generate_yaml_with_header(data)
|
277
|
+
header = "# yaml-language-server: $schema=schema/iso_comment_2012-03.yaml\n\n"
|
278
|
+
header + data.to_yaml
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
class GitHubIssueRetriever
|
283
|
+
def initialize(config_path)
|
284
|
+
@config = load_config(config_path)
|
285
|
+
@github_client = create_github_client
|
286
|
+
@repo = @config.dig("github", "repository")
|
287
|
+
|
288
|
+
raise "GitHub repository not specified in config" unless @repo
|
289
|
+
end
|
290
|
+
|
291
|
+
def retrieve_observations_from_yaml(yaml_file, options = {})
|
292
|
+
data = YAML.load_file(yaml_file)
|
293
|
+
comment_sheet = CommentSheet.from_hash(data)
|
294
|
+
|
295
|
+
results = []
|
296
|
+
comment_sheet.comments.each do |comment|
|
297
|
+
next unless comment.has_github_issue?
|
298
|
+
|
299
|
+
result = if options[:dry_run]
|
300
|
+
preview_observation_retrieval(comment, options)
|
301
|
+
else
|
302
|
+
retrieve_observation(comment, options)
|
303
|
+
end
|
304
|
+
results << result
|
305
|
+
end
|
306
|
+
|
307
|
+
# Update YAML with observations (unless dry run)
|
308
|
+
update_yaml_with_observations(yaml_file, comment_sheet, options) unless options[:dry_run]
|
309
|
+
|
310
|
+
results
|
311
|
+
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def load_config(config_path)
|
316
|
+
YAML.load_file(config_path)
|
317
|
+
rescue Errno::ENOENT
|
318
|
+
raise "Configuration file not found: #{config_path}"
|
319
|
+
end
|
320
|
+
|
321
|
+
def create_github_client
|
322
|
+
token = @config.dig("github", "token") || ENV["GITHUB_TOKEN"]
|
323
|
+
raise "GitHub token not found. Set GITHUB_TOKEN environment variable or specify in config file." unless token
|
324
|
+
|
325
|
+
Octokit::Client.new(access_token: token)
|
326
|
+
end
|
327
|
+
|
328
|
+
def retrieve_observation(comment, options)
|
329
|
+
issue_number = comment.github_issue_number
|
330
|
+
|
331
|
+
begin
|
332
|
+
issue = @github_client.issue(@repo, issue_number)
|
333
|
+
|
334
|
+
# Skip open issues unless explicitly included
|
335
|
+
if issue.state == "open" && !options[:include_open]
|
336
|
+
return {
|
337
|
+
comment_id: comment.id,
|
338
|
+
issue_number: issue_number,
|
339
|
+
status: :skipped,
|
340
|
+
message: "Issue is still open"
|
341
|
+
}
|
342
|
+
end
|
343
|
+
|
344
|
+
# Extract observation from issue comments
|
345
|
+
observation = extract_observation_from_issue(issue_number)
|
346
|
+
|
347
|
+
if observation
|
348
|
+
# Update comment with observation and current status
|
349
|
+
comment.observations = observation
|
350
|
+
comment.github[:status] = issue.state
|
351
|
+
comment.github[:updated_at] = Time.now.utc.iso8601
|
352
|
+
|
353
|
+
{
|
354
|
+
comment_id: comment.id,
|
355
|
+
issue_number: issue_number,
|
356
|
+
status: :retrieved,
|
357
|
+
observation: observation
|
358
|
+
}
|
359
|
+
else
|
360
|
+
{
|
361
|
+
comment_id: comment.id,
|
362
|
+
issue_number: issue_number,
|
363
|
+
status: :skipped,
|
364
|
+
message: "No observation found in issue"
|
365
|
+
}
|
366
|
+
end
|
367
|
+
rescue Octokit::Error => e
|
368
|
+
{
|
369
|
+
comment_id: comment.id,
|
370
|
+
issue_number: issue_number,
|
371
|
+
status: :error,
|
372
|
+
message: e.message
|
373
|
+
}
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
def preview_observation_retrieval(comment, _options)
|
378
|
+
issue_number = comment.github_issue_number
|
379
|
+
|
380
|
+
begin
|
381
|
+
issue = @github_client.issue(@repo, issue_number)
|
382
|
+
observation = extract_observation_from_issue(issue_number)
|
383
|
+
|
384
|
+
{
|
385
|
+
comment_id: comment.id,
|
386
|
+
issue_number: issue_number,
|
387
|
+
status: issue.state,
|
388
|
+
observation: observation
|
389
|
+
}
|
390
|
+
rescue Octokit::Error => e
|
391
|
+
{
|
392
|
+
comment_id: comment.id,
|
393
|
+
issue_number: issue_number,
|
394
|
+
status: :error,
|
395
|
+
message: e.message
|
396
|
+
}
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def extract_observation_from_issue(issue_number)
|
401
|
+
comments = @github_client.issue_comments(@repo, issue_number)
|
402
|
+
|
403
|
+
# Look for magic comments with observation markers
|
404
|
+
observation_markers = @config.dig("github", "retrieval", "observation_markers") ||
|
405
|
+
["**OBSERVATION:**", "**COMMENTER OBSERVATION:**"]
|
406
|
+
|
407
|
+
# Search comments in reverse order (newest first)
|
408
|
+
comments.reverse_each do |comment|
|
409
|
+
observation = parse_observation_from_comment(comment.body, observation_markers)
|
410
|
+
return observation if observation
|
411
|
+
end
|
412
|
+
|
413
|
+
# Fallback to last comment if configured and no magic comment found
|
414
|
+
return comments.last.body.strip if @config.dig("github", "retrieval", "fallback_to_last_comment") && !comments.empty?
|
415
|
+
|
416
|
+
nil
|
417
|
+
rescue Octokit::Error
|
418
|
+
nil
|
419
|
+
end
|
420
|
+
|
421
|
+
def parse_observation_from_comment(comment_body, markers)
|
422
|
+
markers.each do |marker|
|
423
|
+
# Look for markdown blockquote with the marker
|
424
|
+
pattern = /^>\s*#{Regexp.escape(marker)}\s*\n((?:^>.*\n?)*)/m
|
425
|
+
match = comment_body.match(pattern)
|
426
|
+
|
427
|
+
next unless match
|
428
|
+
|
429
|
+
# Extract the blockquote content and clean it up
|
430
|
+
observation = match[1]
|
431
|
+
.split("\n")
|
432
|
+
.map { |line| line.sub(/^>\s?/, "") }
|
433
|
+
.join("\n")
|
434
|
+
.strip
|
435
|
+
return observation unless observation.empty?
|
436
|
+
end
|
437
|
+
|
438
|
+
nil
|
439
|
+
end
|
440
|
+
|
441
|
+
def update_yaml_with_observations(yaml_file, comment_sheet, options)
|
442
|
+
output_file = options[:output] || yaml_file
|
443
|
+
yaml_content = generate_yaml_with_header(comment_sheet.to_yaml_h)
|
444
|
+
File.write(output_file, yaml_content)
|
445
|
+
end
|
446
|
+
|
447
|
+
def generate_yaml_with_header(data)
|
448
|
+
header = "# yaml-language-server: $schema=schema/iso_comment_2012-03.yaml\n\n"
|
449
|
+
header + data.to_yaml
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
data/lib/commenter/parser.rb
CHANGED
@@ -36,9 +36,9 @@ module Commenter
|
|
36
36
|
id: id,
|
37
37
|
body: body,
|
38
38
|
locality: {
|
39
|
-
line_number: cells[1]
|
40
|
-
clause: cells[2]
|
41
|
-
element: cells[3]
|
39
|
+
line_number: cells[1] && cells[1].empty? ? nil : cells[1],
|
40
|
+
clause: cells[2] && cells[2].empty? ? nil : cells[2],
|
41
|
+
element: cells[3] && cells[3].empty? ? nil : cells[3]
|
42
42
|
},
|
43
43
|
type: cells[4] || "",
|
44
44
|
comments: cells[5] || "",
|
@@ -47,7 +47,7 @@ module Commenter
|
|
47
47
|
|
48
48
|
# Handle observations column
|
49
49
|
unless options[:exclude_observations]
|
50
|
-
comment_attrs[:observations] = cells[7]
|
50
|
+
comment_attrs[:observations] = cells[7] && cells[7].empty? ? nil : cells[7]
|
51
51
|
end
|
52
52
|
|
53
53
|
comments << Comment.new(comment_attrs)
|
@@ -71,9 +71,13 @@ module Commenter
|
|
71
71
|
# Try to extract metadata from document properties first
|
72
72
|
begin
|
73
73
|
if doc.respond_to?(:created) && doc.created
|
74
|
-
metadata[:date] =
|
74
|
+
metadata[:date] = begin
|
75
|
+
doc.created.strftime("%Y-%m-%d")
|
76
|
+
rescue StandardError
|
77
|
+
nil
|
78
|
+
end
|
75
79
|
end
|
76
|
-
rescue
|
80
|
+
rescue StandardError
|
77
81
|
# Ignore errors accessing document properties
|
78
82
|
end
|
79
83
|
|
data/lib/commenter/version.rb
CHANGED
data/lib/commenter.rb
CHANGED
@@ -9,6 +9,20 @@ properties:
|
|
9
9
|
type: string
|
10
10
|
const: "2012-03"
|
11
11
|
description: Version of the ISO commenting template
|
12
|
+
date:
|
13
|
+
type: ["string", "null"]
|
14
|
+
description: Date of the comment sheet
|
15
|
+
format: date
|
16
|
+
document:
|
17
|
+
type: ["string", "null"]
|
18
|
+
description: Document identifier being reviewed
|
19
|
+
project:
|
20
|
+
type: ["string", "null"]
|
21
|
+
description: Project name
|
22
|
+
stage:
|
23
|
+
type: ["string", "null"]
|
24
|
+
description: Approval stage (WD/CD/DIS/FDIS/PRF/PUB)
|
25
|
+
enum: [null, "WD", "CD", "DIS", "FDIS", "PRF", "PUB"]
|
12
26
|
comments:
|
13
27
|
type: array
|
14
28
|
description: Array of comment entries from the ISO comment sheet
|
@@ -65,5 +79,29 @@ properties:
|
|
65
79
|
observations:
|
66
80
|
type: ["string", "null"]
|
67
81
|
description: "Observations of the Secretariat (optional)"
|
82
|
+
github:
|
83
|
+
type: ["object", "null"]
|
84
|
+
description: "GitHub integration information"
|
85
|
+
properties:
|
86
|
+
issue_number:
|
87
|
+
type: integer
|
88
|
+
description: "GitHub issue number"
|
89
|
+
issue_url:
|
90
|
+
type: string
|
91
|
+
format: uri
|
92
|
+
description: "GitHub issue URL"
|
93
|
+
status:
|
94
|
+
type: string
|
95
|
+
enum: ["open", "closed"]
|
96
|
+
description: "GitHub issue status"
|
97
|
+
created_at:
|
98
|
+
type: string
|
99
|
+
format: date-time
|
100
|
+
description: "Issue creation timestamp"
|
101
|
+
updated_at:
|
102
|
+
type: string
|
103
|
+
format: date-time
|
104
|
+
description: "Issue last update timestamp"
|
105
|
+
required: ["issue_number", "issue_url", "status"]
|
68
106
|
required: ["id", "body", "locality", "type", "comments", "proposed_change"]
|
69
107
|
required: ["version", "comments"]
|