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