commenter 0.7.0 → 0.7.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 +4 -4
- data/README.adoc +26 -7
- data/lib/commenter/cli.rb +12 -6
- data/lib/commenter/comment.rb +13 -11
- data/lib/commenter/github_integration.rb +27 -68
- data/lib/commenter/issue_marker.rb +34 -0
- data/lib/commenter/milestone_resolver.rb +50 -0
- data/lib/commenter/sheet_validator.rb +1 -1
- data/lib/commenter/version.rb +1 -1
- data/lib/commenter.rb +2 -0
- data/spec/commenter/cli_spec.rb +38 -0
- data/spec/commenter/comment_spec.rb +28 -0
- data/spec/commenter/github_integration_spec.rb +98 -8
- data/spec/commenter/sheet_validator_spec.rb +4 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dbcfbae1636268a1f6577337f999f47271e93b7943fd6e9a28a3ddb93603ad04
|
|
4
|
+
data.tar.gz: dcca1817d391d94ce98fbb1469499fd480944cac137ea86ef9c468f1873347fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0fc597c5fe0c6d0f9db6366569d20cad1c93a78170187d3d9a67ba0e8911ba64b355938d11a40fe752b6ccd79ea5e3d7f702af5b61c1848a996962df3d57b1e9
|
|
7
|
+
data.tar.gz: 50e641728f5d07c496388fdc073c2a659ff212f21e8d0d23c44de7c449270052f7bc56dba427bd35f1a68125151d62c61b95a913e7e1aad9e9f4ba4100aebf37
|
data/README.adoc
CHANGED
|
@@ -246,6 +246,11 @@ non-zero when errors are found, so it fits CI and pre-flight checks:
|
|
|
246
246
|
commenter validate ballot.yaml
|
|
247
247
|
----
|
|
248
248
|
|
|
249
|
+
Known stages are AWI, WD, CD, DIS, FDIS, PRF, and PUB. A sheet at an
|
|
250
|
+
unusual stage (or with an unrecognized comment type) produces a WARNING,
|
|
251
|
+
which is informational only — a non-zero exit always means errors were
|
|
252
|
+
found.
|
|
253
|
+
|
|
249
254
|
=== Comparing ballots between stages
|
|
250
255
|
|
|
251
256
|
The same comment ID (`DE-001`) recurs at every ballot stage; comparing two
|
|
@@ -331,6 +336,8 @@ github:
|
|
|
331
336
|
token: "ghp_xxxxxxxxxxxx"
|
|
332
337
|
milestone:
|
|
333
338
|
name: "ISO 80000-2 DIS Review"
|
|
339
|
+
# Creates the milestone when it does not exist yet
|
|
340
|
+
create_if_missing: true
|
|
334
341
|
default_labels: ["comment-review", "iso-standard"]
|
|
335
342
|
stage_labels:
|
|
336
343
|
WD: ["working-draft"]
|
|
@@ -350,7 +357,7 @@ templates are provided, but you can customize them.
|
|
|
350
357
|
|
|
351
358
|
The title template (`data/github_issue_title_template.liquid`) supports these variables:
|
|
352
359
|
|
|
353
|
-
* `stage` - Approval stage (WD/CD/DIS/FDIS/PRF/PUB)
|
|
360
|
+
* `stage` - Approval stage (AWI/WD/CD/DIS/FDIS/PRF/PUB)
|
|
354
361
|
* `document` - Document identifier
|
|
355
362
|
* `comment_id` - Comment identifier
|
|
356
363
|
* `brief_summary` - Generated summary combining locality and description
|
|
@@ -447,13 +454,22 @@ Every created issue body also carries a deterministic marker line, e.g.
|
|
|
447
454
|
+
|
|
448
455
|
[source]
|
|
449
456
|
----
|
|
450
|
-
`urn:commenter:owner/repo:dis:
|
|
457
|
+
`urn:commenter:owner/repo:dis:iso1234-2:2019:gb-001`
|
|
451
458
|
----
|
|
452
459
|
+
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
460
|
+
The marker includes the sheet's document (compacted and lowercased), so
|
|
461
|
+
the same comment ID (`GB-001`) posted for two different documents of one
|
|
462
|
+
repository never collides. Sheets without a `document` field keep the
|
|
463
|
+
older, document-less marker shape.
|
|
464
|
+
|
|
465
|
+
Duplicate detection searches the issue bodies for this marker first —
|
|
466
|
+
exact and template-independent. It then falls back to the document-less
|
|
467
|
+
marker for issues created before the document segment existed, and to the
|
|
468
|
+
title search for issues created before markers existed at all; those two
|
|
469
|
+
fallbacks are ambiguous when the same comment ID exists for several
|
|
470
|
+
documents, so a fallback hit counts only when the issue itself names the
|
|
471
|
+
sheet's document. Comment numbers already recorded in the comments YAML
|
|
472
|
+
are verified with a direct read before any search runs.
|
|
457
473
|
|
|
458
474
|
===== Unique ID template
|
|
459
475
|
|
|
@@ -542,7 +558,10 @@ By default an open issue whose comment has a recorded disposition
|
|
|
542
558
|
`github-retrieve` round-trip working, since observations are retrieved from
|
|
543
559
|
closed issues. Disable with `--no-close-on-disposition`.
|
|
544
560
|
|
|
545
|
-
The milestone is resolved once per run and reused for every issue.
|
|
561
|
+
The milestone is resolved once per run and reused for every issue. When
|
|
562
|
+
`create_if_missing` is set and no milestone carries that name, the
|
|
563
|
+
milestone is created on first use (honoring `description` and `due_date`
|
|
564
|
+
from the same config block).
|
|
546
565
|
|
|
547
566
|
==== Retrieving observations from GitHub issues
|
|
548
567
|
|
data/lib/commenter/cli.rb
CHANGED
|
@@ -91,17 +91,23 @@ module Commenter
|
|
|
91
91
|
comment_sheet = CommentSheet.from_yaml(File.read(input_yaml))
|
|
92
92
|
problems = SheetValidator.call(comment_sheet)
|
|
93
93
|
|
|
94
|
-
if problems.empty?
|
|
95
|
-
puts "OK: #{comment_sheet.comments.length} comments, no problems found"
|
|
96
|
-
return
|
|
97
|
-
end
|
|
98
|
-
|
|
99
94
|
problems.each do |problem|
|
|
100
95
|
where = problem[:comment] ? "#{problem[:comment]}: " : ""
|
|
101
96
|
puts "#{problem[:severity].to_s.upcase}: #{where}#{problem[:message]}"
|
|
102
97
|
end
|
|
98
|
+
|
|
103
99
|
errors = problems.count { |problem| problem[:severity] == :error }
|
|
104
|
-
|
|
100
|
+
warnings = problems.count { |problem| problem[:severity] == :warning }
|
|
101
|
+
|
|
102
|
+
# Warnings surface unusual-but-legal sheets (e.g. an early-stage draft)
|
|
103
|
+
# and must not fail CI on their own; only errors are fatal.
|
|
104
|
+
raise Commenter::Error, "#{errors} error(s), #{warnings} warning(s) found in #{input_yaml}" if errors.positive?
|
|
105
|
+
|
|
106
|
+
if problems.empty?
|
|
107
|
+
puts "OK: #{comment_sheet.comments.length} comments, no problems found"
|
|
108
|
+
else
|
|
109
|
+
puts "#{comment_sheet.comments.length} comments valid, #{warnings} warning(s)"
|
|
110
|
+
end
|
|
105
111
|
end
|
|
106
112
|
|
|
107
113
|
desc "fill INPUT.yaml", "Fill DOCX template from YAML comments"
|
data/lib/commenter/comment.rb
CHANGED
|
@@ -100,9 +100,21 @@ module Commenter
|
|
|
100
100
|
locality.element = value
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# Public so every consumer (summaries, issue templates) renders locality
|
|
104
|
+
# through one implementation.
|
|
105
|
+
def locality_summary
|
|
106
|
+
parts = []
|
|
107
|
+
if clause && !clause.strip.empty?
|
|
108
|
+
parts << (clause.strip.match?(/\Aclause\b/i) ? clause : "Clause #{clause}")
|
|
109
|
+
end
|
|
110
|
+
parts << element if element && !element.strip.empty?
|
|
111
|
+
parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
|
|
112
|
+
parts.join(", ")
|
|
113
|
+
end
|
|
114
|
+
|
|
103
115
|
def brief_summary(max_length = 60)
|
|
104
116
|
description = comments.to_s.split(/[.!?\n]/).map(&:strip).reject(&:empty?).first.to_s
|
|
105
|
-
locality_text =
|
|
117
|
+
locality_text = locality_summary
|
|
106
118
|
|
|
107
119
|
if description.empty?
|
|
108
120
|
locality_text.empty? ? "No description" : locality_text
|
|
@@ -143,15 +155,5 @@ module Commenter
|
|
|
143
155
|
self.github = GithubInfo.new(issue_number: issue_number, issue_url: issue_url,
|
|
144
156
|
status: status, created_at: created_at)
|
|
145
157
|
end
|
|
146
|
-
|
|
147
|
-
private
|
|
148
|
-
|
|
149
|
-
def format_locality
|
|
150
|
-
parts = []
|
|
151
|
-
parts << "Clause #{clause}" if clause && !clause.strip.empty?
|
|
152
|
-
parts << element if element && !element.strip.empty?
|
|
153
|
-
parts << "Line #{line_number}" if line_number && !line_number.strip.empty?
|
|
154
|
-
parts.join(", ")
|
|
155
|
-
end
|
|
156
158
|
end
|
|
157
159
|
end
|
|
@@ -10,7 +10,6 @@ module Commenter
|
|
|
10
10
|
THROTTLE_SECONDS = 1
|
|
11
11
|
RATE_LIMIT_RETRY_DELAY = 60
|
|
12
12
|
RATE_LIMIT_MAX_RETRIES = 3
|
|
13
|
-
MARKER_PREFIX = "urn:commenter"
|
|
14
13
|
|
|
15
14
|
def initialize(config_path, title_template_path = nil, body_template_path = nil, client: nil)
|
|
16
15
|
@session = GitHubSession.new(config_path)
|
|
@@ -108,7 +107,7 @@ module Commenter
|
|
|
108
107
|
"unique_id" => unique_id,
|
|
109
108
|
"has_observations" => !comment.observations.nil? && !comment.observations.strip.empty?,
|
|
110
109
|
"has_proposed_change" => !comment.proposed_change.nil? && !comment.proposed_change.strip.empty?,
|
|
111
|
-
"locality_summary" =>
|
|
110
|
+
"locality_summary" => comment.locality_summary
|
|
112
111
|
}
|
|
113
112
|
end
|
|
114
113
|
|
|
@@ -124,14 +123,6 @@ module Commenter
|
|
|
124
123
|
@unique_id_template.render(base_variables).strip
|
|
125
124
|
end
|
|
126
125
|
|
|
127
|
-
def format_locality(comment)
|
|
128
|
-
parts = []
|
|
129
|
-
parts << "Clause #{comment.clause}" if comment.clause && !comment.clause.strip.empty?
|
|
130
|
-
parts << comment.element if comment.element && !comment.element.strip.empty?
|
|
131
|
-
parts << "Line #{comment.line_number}" if comment.line_number && !comment.line_number.strip.empty?
|
|
132
|
-
parts.join(", ")
|
|
133
|
-
end
|
|
134
|
-
|
|
135
126
|
def create_issue(comment, comment_sheet, options = {})
|
|
136
127
|
puts "[GitHubIssueCreator] Creating issue for comment ID: #{comment.id}"
|
|
137
128
|
begin
|
|
@@ -181,16 +172,9 @@ module Commenter
|
|
|
181
172
|
end
|
|
182
173
|
end
|
|
183
174
|
|
|
184
|
-
# Deterministic marker embedded in every created issue body, independent
|
|
185
|
-
# of the user's templates, so duplicate detection never has to rely on
|
|
186
|
-
# title matching alone (#1).
|
|
187
|
-
def issue_marker(comment, comment_sheet)
|
|
188
|
-
"#{MARKER_PREFIX}:#{@repo.downcase}:#{comment_sheet.stage.to_s.downcase}:#{comment.id}"
|
|
189
|
-
end
|
|
190
|
-
|
|
191
175
|
def body_with_marker(comment, comment_sheet)
|
|
192
176
|
rendered = @body_template.render(template_variables(comment, comment_sheet))
|
|
193
|
-
"#{rendered}\n\n---\n`#{
|
|
177
|
+
"#{rendered}\n\n---\n`#{IssueMarker.for(comment, comment_sheet, @repo)}`"
|
|
194
178
|
end
|
|
195
179
|
|
|
196
180
|
def recorded_issue(comment, comment_sheet)
|
|
@@ -200,14 +184,14 @@ module Commenter
|
|
|
200
184
|
puts "[GitHubIssueCreator] Verifying recorded issue ##{number} for comment ID: #{comment.id}"
|
|
201
185
|
issue = @github_client.issue(@repo, number)
|
|
202
186
|
if issue.title.include?(render_unique_id(comment, comment_sheet)) ||
|
|
203
|
-
issue.body.to_s.include?(
|
|
187
|
+
issue.body.to_s.include?(IssueMarker.for(comment, comment_sheet, @repo))
|
|
204
188
|
issue
|
|
205
189
|
else
|
|
206
|
-
puts "[GitHubIssueCreator] Recorded issue ##{number} does not match, falling back to
|
|
190
|
+
puts "[GitHubIssueCreator] Recorded issue ##{number} does not match, falling back to search."
|
|
207
191
|
nil
|
|
208
192
|
end
|
|
209
193
|
rescue Octokit::NotFound
|
|
210
|
-
puts "[GitHubIssueCreator] Recorded issue ##{number} no longer exists, falling back to
|
|
194
|
+
puts "[GitHubIssueCreator] Recorded issue ##{number} no longer exists, falling back to search."
|
|
211
195
|
nil
|
|
212
196
|
end
|
|
213
197
|
|
|
@@ -257,19 +241,29 @@ module Commenter
|
|
|
257
241
|
}
|
|
258
242
|
end
|
|
259
243
|
|
|
244
|
+
# Duplicate detection runs in three tiers: the document-qualified marker
|
|
245
|
+
# is exact; the legacy marker and the title match are ambiguous when two
|
|
246
|
+
# documents share a comment id, so those hits count only when the issue
|
|
247
|
+
# itself names this document.
|
|
260
248
|
def find_existing_issue(comment, comment_sheet)
|
|
261
249
|
unique_id = render_unique_id(comment, comment_sheet)
|
|
262
250
|
puts "[GitHubIssueCreator] Searching for existing issue with unique_id: #{unique_id}"
|
|
263
251
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
return marker_results.items.first if marker_results.items.any?
|
|
252
|
+
search_issues("body", IssueMarker.for(comment, comment_sheet, @repo)) ||
|
|
253
|
+
search_issues("body", IssueMarker.legacy_for(comment, comment_sheet, @repo), verify: comment_sheet) ||
|
|
254
|
+
search_issues("title", unique_id, verify: comment_sheet)
|
|
255
|
+
end
|
|
269
256
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
257
|
+
def search_issues(field, term, verify: nil)
|
|
258
|
+
issues = @github_client.search_issues("repo:#{@repo} in:#{field} \"#{term}\"").items
|
|
259
|
+
return issues.first if verify.nil?
|
|
260
|
+
return nil if IssueMarker.document_segment(verify).nil?
|
|
261
|
+
|
|
262
|
+
issues.find { |issue| names_document?(issue, verify.document) }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def names_document?(issue, document)
|
|
266
|
+
issue.title.to_s.include?(document) || issue.body.to_s.include?(document)
|
|
273
267
|
end
|
|
274
268
|
|
|
275
269
|
def determine_labels(comment, comment_sheet)
|
|
@@ -307,48 +301,13 @@ module Commenter
|
|
|
307
301
|
end
|
|
308
302
|
|
|
309
303
|
def determine_milestone(_comment, _comment_sheet, _options)
|
|
310
|
-
# # Check for stage-specific milestone
|
|
311
|
-
# if comment_sheet.stage
|
|
312
|
-
# stage_milestone = @config.dig("github", "stage_milestones", comment_sheet.stage)
|
|
313
|
-
# if stage_milestone
|
|
314
|
-
# milestone_number = resolve_milestone_by_name_or_number(stage_milestone)
|
|
315
|
-
# return milestone_number if milestone_number
|
|
316
|
-
# end
|
|
317
|
-
# end
|
|
318
|
-
|
|
319
|
-
# Use configured milestone
|
|
320
304
|
milestone_config = @config.dig("github", "milestone")
|
|
321
305
|
return nil unless milestone_config
|
|
322
306
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
# Resolved once per run and memoized: resolving per comment caused
|
|
328
|
-
# duplicated milestone fetches (see 3a06c3d).
|
|
329
|
-
@milestone_number ||= begin
|
|
330
|
-
puts "[GitHubIssueCreator] Using milestone name: #{milestone_config["name"]}"
|
|
331
|
-
resolve_milestone_by_name_or_number(milestone_config["name"])
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
def resolve_milestone_by_name_or_number(milestone_identifier)
|
|
337
|
-
# If it's a number, return it directly
|
|
338
|
-
return milestone_identifier.to_i if milestone_identifier.to_s.match?(/^\d+$/)
|
|
339
|
-
|
|
340
|
-
# Otherwise, search by name
|
|
341
|
-
find_milestone_by_name(milestone_identifier)
|
|
342
|
-
end
|
|
343
|
-
|
|
344
|
-
def find_milestone_by_name(name)
|
|
345
|
-
milestones = @github_client.milestones(@repo, state: "all")
|
|
346
|
-
|
|
347
|
-
puts "[GitHubIssueCreator] Found #{milestones.size} milestones in repository #{@repo}" if milestones.any?
|
|
348
|
-
milestone = milestones.find { |m| m.title == name }
|
|
349
|
-
milestone&.number
|
|
350
|
-
rescue Octokit::Error
|
|
351
|
-
nil
|
|
307
|
+
# Memoized: resolving per comment caused duplicated milestone fetches
|
|
308
|
+
# (see 3a06c3d).
|
|
309
|
+
@milestone_number ||= MilestoneResolver.new(@github_client, @repo, milestone_config).number
|
|
310
|
+
@milestone_number
|
|
352
311
|
end
|
|
353
312
|
|
|
354
313
|
def update_yaml_with_github_info(yaml_file, comment_sheet, results, options)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Deterministic issue markers for duplicate detection: exact, independent
|
|
5
|
+
# of the user's templates, and stable across runs. The document segment
|
|
6
|
+
# keeps the marker unambiguous when the same comment id exists for several
|
|
7
|
+
# documents in one repository; sheets without a document keep the earlier
|
|
8
|
+
# marker shape.
|
|
9
|
+
module IssueMarker
|
|
10
|
+
PREFIX = "urn:commenter"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def for(comment, comment_sheet, repo)
|
|
15
|
+
segments = [PREFIX, repo.to_s.downcase, comment_sheet.stage.to_s.downcase]
|
|
16
|
+
document = document_segment(comment_sheet)
|
|
17
|
+
segments << document unless document.nil?
|
|
18
|
+
segments << comment.id
|
|
19
|
+
segments.join(":")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The marker shape used before markers carried the document. Ambiguous
|
|
23
|
+
# when two documents share a comment id, so callers must verify a hit
|
|
24
|
+
# against the sheet's document (see ExistingIssueFinder).
|
|
25
|
+
def legacy_for(comment, comment_sheet, repo)
|
|
26
|
+
"#{PREFIX}:#{repo.to_s.downcase}:#{comment_sheet.stage.to_s.downcase}:#{comment.id}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def document_segment(comment_sheet)
|
|
30
|
+
document = comment_sheet.document.to_s.gsub(/\s+/, "").downcase
|
|
31
|
+
document.empty? ? nil : document
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Commenter
|
|
4
|
+
# Resolves (and optionally creates) the GitHub milestone issues are
|
|
5
|
+
# assigned to: a `number` config passes through; a `name` resolves
|
|
6
|
+
# against the repository; `create_if_missing` mints the milestone on
|
|
7
|
+
# first use, honoring `description` and `due_date`.
|
|
8
|
+
class MilestoneResolver
|
|
9
|
+
def initialize(client, repo, config)
|
|
10
|
+
@client = client
|
|
11
|
+
@repo = repo
|
|
12
|
+
@config = config
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def number
|
|
16
|
+
return @config["number"] if @config["number"]
|
|
17
|
+
return nil unless @config["name"]
|
|
18
|
+
|
|
19
|
+
@number ||= resolve
|
|
20
|
+
@number ||= create if @config["create_if_missing"]
|
|
21
|
+
@number
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def resolve
|
|
27
|
+
return @config["name"].to_i if @config["name"].to_s.match?(/\A\d+\z/)
|
|
28
|
+
|
|
29
|
+
milestones = @client.milestones(@repo, state: "all")
|
|
30
|
+
puts "[GitHubIssueCreator] Found #{milestones.size} milestones in repository #{@repo}" if milestones.any?
|
|
31
|
+
milestones.find { |milestone| milestone.title == @config["name"] }&.number
|
|
32
|
+
rescue Octokit::Error
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create
|
|
37
|
+
options = {}
|
|
38
|
+
options[:description] = @config["description"] if @config["description"]
|
|
39
|
+
if (due = @config["due_date"])
|
|
40
|
+
options[:due_on] = due.include?("T") ? due : "#{due}T00:00:00Z"
|
|
41
|
+
end
|
|
42
|
+
milestone = @client.create_milestone(@repo, @config["name"], options)
|
|
43
|
+
puts "[GitHubIssueCreator] Created milestone ##{milestone.number}: #{@config["name"]}"
|
|
44
|
+
milestone.number
|
|
45
|
+
rescue Octokit::Error => e
|
|
46
|
+
puts "[GitHubIssueCreator] Could not create milestone #{@config["name"].inspect}: #{e.message}"
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/commenter/version.rb
CHANGED
data/lib/commenter.rb
CHANGED
|
@@ -21,6 +21,8 @@ module Commenter
|
|
|
21
21
|
autoload :Parser, "commenter/parser"
|
|
22
22
|
autoload :Filler, "commenter/filler"
|
|
23
23
|
autoload :GitHubIssueCreator, "commenter/github_integration"
|
|
24
|
+
autoload :IssueMarker, "commenter/issue_marker"
|
|
25
|
+
autoload :MilestoneResolver, "commenter/milestone_resolver"
|
|
24
26
|
autoload :GitHubIssueRetriever, "commenter/github_issue_retriever"
|
|
25
27
|
autoload :GitHubSync, "commenter/github_sync"
|
|
26
28
|
end
|
data/spec/commenter/cli_spec.rb
CHANGED
|
@@ -101,3 +101,41 @@ RSpec.describe Commenter::Cli do
|
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
|
+
|
|
105
|
+
RSpec.describe Commenter::Cli do
|
|
106
|
+
describe "#validate" do
|
|
107
|
+
def sheet_file(data)
|
|
108
|
+
file = Tempfile.new(["sheet", ".yaml"])
|
|
109
|
+
file.write(data.to_yaml)
|
|
110
|
+
file.close
|
|
111
|
+
file
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it "passes a sheet at the AWI stage" do
|
|
115
|
+
file = sheet_file({ "version" => "2012-03", "stage" => "AWI",
|
|
116
|
+
"comments" => [{ "id" => "DE-001", "body" => "DE", "comments" => "Text" }] })
|
|
117
|
+
|
|
118
|
+
expect { described_class.start(["validate", file.path]) }
|
|
119
|
+
.to output(a_string_including("OK:", "no problems found")).to_stdout
|
|
120
|
+
file.unlink
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "does not fail when only warnings are found" do
|
|
124
|
+
file = sheet_file({ "version" => "2012-03", "stage" => "CUSTOM",
|
|
125
|
+
"comments" => [{ "id" => "DE-001", "body" => "DE", "comments" => "Text" }] })
|
|
126
|
+
|
|
127
|
+
expect { described_class.start(["validate", file.path]) }
|
|
128
|
+
.to output(a_string_including('WARNING: unusual stage "CUSTOM"', "1 warning(s)")).to_stdout
|
|
129
|
+
file.unlink
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "fails when errors are found" do
|
|
133
|
+
file = sheet_file({ "version" => "2012-03",
|
|
134
|
+
"comments" => [{ "id" => "DE-001", "body" => "DE" }] })
|
|
135
|
+
|
|
136
|
+
expect { described_class.start(["validate", file.path]) }
|
|
137
|
+
.to raise_error(Commenter::Error, a_string_including("1 error(s)"))
|
|
138
|
+
file.unlink
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -81,6 +81,18 @@ RSpec.describe Commenter::Comment do
|
|
|
81
81
|
expect(summary).to include("Line 42")
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
it "does not prefix Clause when the clause already names it" do
|
|
85
|
+
comment = described_class.new(
|
|
86
|
+
locality: { clause: "Clause 4 and document" },
|
|
87
|
+
comments: "Comment text"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
summary = comment.brief_summary
|
|
91
|
+
|
|
92
|
+
expect(summary).to start_with("Clause 4 and document:")
|
|
93
|
+
expect(summary).not_to include("Clause Clause")
|
|
94
|
+
end
|
|
95
|
+
|
|
84
96
|
it "handles empty locality gracefully" do
|
|
85
97
|
comment = described_class.new(
|
|
86
98
|
locality: { clause: "" },
|
|
@@ -231,3 +243,19 @@ RSpec.describe Commenter::Comment do
|
|
|
231
243
|
end
|
|
232
244
|
end
|
|
233
245
|
end
|
|
246
|
+
|
|
247
|
+
RSpec.describe Commenter::Comment do
|
|
248
|
+
describe "#locality_summary" do
|
|
249
|
+
it "formats clause, element, and line number" do
|
|
250
|
+
comment = described_class.new(locality: { clause: "5.1", element: "Table 1", line_number: "42" })
|
|
251
|
+
|
|
252
|
+
expect(comment.locality_summary).to eq("Clause 5.1, Table 1, Line 42")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "does not double the Clause prefix when the clause already names it" do
|
|
256
|
+
comment = described_class.new(locality: { clause: "Clause 4 and document" })
|
|
257
|
+
|
|
258
|
+
expect(comment.locality_summary).to eq("Clause 4 and document")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -17,13 +17,14 @@ RetrieverComment = Struct.new(:body, keyword_init: true)
|
|
|
17
17
|
# exercised against an object with the same protocol as Octokit::Client.
|
|
18
18
|
class FakeGitHubClient
|
|
19
19
|
attr_accessor :rate_limit_failures, :search_error
|
|
20
|
-
attr_reader :created
|
|
20
|
+
attr_reader :created, :created_milestones
|
|
21
21
|
|
|
22
22
|
def initialize(existing_issues = [])
|
|
23
23
|
@issues = {}
|
|
24
24
|
existing_issues.each { |issue| @issues[issue.number] = issue }
|
|
25
25
|
@next_number = @issues.keys.max.to_i
|
|
26
26
|
@created = []
|
|
27
|
+
@created_milestones = []
|
|
27
28
|
@rate_limit_failures = 0
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -46,6 +47,12 @@ class FakeGitHubClient
|
|
|
46
47
|
[FakeMilestone.new(number: 1, title: "Test Milestone")]
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
def create_milestone(_repo, title, _options = {})
|
|
51
|
+
milestone = FakeMilestone.new(number: 100 + @created_milestones.length + 1, title: title)
|
|
52
|
+
@created_milestones << milestone
|
|
53
|
+
milestone
|
|
54
|
+
end
|
|
55
|
+
|
|
49
56
|
def create_issue(_repo, title, body, _options = {})
|
|
50
57
|
if @rate_limit_failures.positive?
|
|
51
58
|
@rate_limit_failures -= 1
|
|
@@ -175,6 +182,36 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
175
182
|
end
|
|
176
183
|
end
|
|
177
184
|
|
|
185
|
+
describe "milestone handling" do
|
|
186
|
+
let(:client) { FakeGitHubClient.new }
|
|
187
|
+
let(:creator) do
|
|
188
|
+
described_class.new(config_file.path, title_template_file.path, body_template_file.path, client: client)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "creates the milestone when create_if_missing is set" do
|
|
192
|
+
config_data["github"]["milestone"] = {
|
|
193
|
+
"name" => "Fresh Milestone",
|
|
194
|
+
"create_if_missing" => true,
|
|
195
|
+
"description" => "Created by specs",
|
|
196
|
+
"due_date" => "2026-12-31"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
result = creator.create_issues_from_yaml(yaml_file.path, dry_run: true).first
|
|
200
|
+
|
|
201
|
+
expect(result[:milestone]).to eq(101)
|
|
202
|
+
expect(client.created_milestones.map(&:title)).to eq(["Fresh Milestone"])
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "leaves the milestone unset when missing without create_if_missing" do
|
|
206
|
+
config_data["github"]["milestone"] = { "name" => "Absent Milestone" }
|
|
207
|
+
|
|
208
|
+
result = creator.create_issues_from_yaml(yaml_file.path, dry_run: true).first
|
|
209
|
+
|
|
210
|
+
expect(result[:milestone]).to be_nil
|
|
211
|
+
expect(client.created_milestones).to be_empty
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
178
215
|
describe "#create_issues_from_yaml" do
|
|
179
216
|
let(:creator) { described_class.new(config_file.path, title_template_file.path, body_template_file.path) }
|
|
180
217
|
|
|
@@ -240,7 +277,7 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
240
277
|
end
|
|
241
278
|
|
|
242
279
|
it "falls back to search when the recorded issue no longer exists" do
|
|
243
|
-
client = FakeGitHubClient.new([seeded_issue(5, "[DIS] US-001: posted earlier")])
|
|
280
|
+
client = FakeGitHubClient.new([seeded_issue(5, "[DIS] US-001: posted earlier (Test Document)")])
|
|
244
281
|
sheet = write_sheet([yaml_data["comments"].first.merge("github" => { "issue_number" => 99 })])
|
|
245
282
|
creator = described_class.new(config_file.path, title_template_file.path,
|
|
246
283
|
body_template_file.path, client: client)
|
|
@@ -256,7 +293,7 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
256
293
|
|
|
257
294
|
it "falls back to search when the recorded issue title does not match the stage" do
|
|
258
295
|
client = FakeGitHubClient.new([seeded_issue(7, "[WD] US-001: different stage"),
|
|
259
|
-
seeded_issue(5, "[DIS] US-001: posted earlier")])
|
|
296
|
+
seeded_issue(5, "[DIS] US-001: posted earlier (Test Document)")])
|
|
260
297
|
sheet = write_sheet([yaml_data["comments"].first.merge("github" => { "issue_number" => 7 })])
|
|
261
298
|
creator = described_class.new(config_file.path, title_template_file.path,
|
|
262
299
|
body_template_file.path, client: client)
|
|
@@ -270,7 +307,7 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
270
307
|
end
|
|
271
308
|
|
|
272
309
|
it "records the issue in the YAML when skipping via title search" do
|
|
273
|
-
client = FakeGitHubClient.new([seeded_issue(5, "[DIS] US-001: posted earlier")])
|
|
310
|
+
client = FakeGitHubClient.new([seeded_issue(5, "[DIS] US-001: posted earlier (Test Document)")])
|
|
274
311
|
sheet = write_sheet(yaml_data["comments"])
|
|
275
312
|
creator = described_class.new(config_file.path, title_template_file.path,
|
|
276
313
|
body_template_file.path, client: client)
|
|
@@ -356,6 +393,14 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
356
393
|
file
|
|
357
394
|
end
|
|
358
395
|
|
|
396
|
+
it "does not double the Clause prefix in locality_summary" do
|
|
397
|
+
yaml_data["comments"].first["locality"]["clause"] = "Clause 4 and document"
|
|
398
|
+
|
|
399
|
+
result = creator.create_issues_from_yaml(yaml_file.path, dry_run: true).first
|
|
400
|
+
|
|
401
|
+
expect(result[:title]).to include("Clause 4 and document|")
|
|
402
|
+
end
|
|
403
|
+
|
|
359
404
|
it "exposes sheet, comment, and computed variables to templates" do
|
|
360
405
|
result = creator.create_issues_from_yaml(yaml_file.path, dry_run: true).first
|
|
361
406
|
variables = result[:title].split("|")
|
|
@@ -505,12 +550,57 @@ RSpec.describe Commenter::GitHubIssueCreator do
|
|
|
505
550
|
file.unlink
|
|
506
551
|
end
|
|
507
552
|
|
|
508
|
-
|
|
553
|
+
it "embeds the document in the marker when the sheet has one" do
|
|
554
|
+
client = FakeGitHubClient.new
|
|
555
|
+
creator = described_class.new(config_file.path, client: client)
|
|
556
|
+
|
|
557
|
+
file = write_yaml_with_github(nil, document: "ISO 80000-2:2019")
|
|
558
|
+
results = creator.create_issues_from_yaml(file)
|
|
559
|
+
|
|
560
|
+
expect(results.first[:status]).to eq(:created)
|
|
561
|
+
expect(client.created.first.body)
|
|
562
|
+
.to match(%r{urn:commenter:test-org/test-repo:dis:iso80000-2:2019:US-001})
|
|
563
|
+
file.unlink
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
it "creates a new issue when a same-id issue exists for another document (legacy marker)" do
|
|
567
|
+
other = FakeIssue.new(number: 9, title: "[DIS] US-001 (Doc A)",
|
|
568
|
+
body: "x `urn:commenter:test-org/test-repo:dis:US-001`",
|
|
569
|
+
html_url: "u", state: "open", created_at: "2026-01-01T00:00:00Z")
|
|
570
|
+
client = FakeGitHubClient.new([other])
|
|
571
|
+
creator = described_class.new(config_file.path, client: client)
|
|
572
|
+
|
|
573
|
+
file = write_yaml_with_github(nil, document: "Doc B")
|
|
574
|
+
results = creator.create_issues_from_yaml(file)
|
|
575
|
+
|
|
576
|
+
expect(results.first[:status]).to eq(:created)
|
|
577
|
+
expect(results.first[:issue_number]).to eq(10)
|
|
578
|
+
file.unlink
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
it "skips via the legacy marker only when the existing issue names this document" do
|
|
582
|
+
legacy = FakeIssue.new(number: 9, title: "[DIS] US-001 Doc B",
|
|
583
|
+
body: "x `urn:commenter:test-org/test-repo:dis:US-001`",
|
|
584
|
+
html_url: "u", state: "open", created_at: "2026-01-01T00:00:00Z")
|
|
585
|
+
client = FakeGitHubClient.new([legacy])
|
|
586
|
+
creator = described_class.new(config_file.path, client: client)
|
|
587
|
+
|
|
588
|
+
file = write_yaml_with_github(nil, document: "Doc B")
|
|
589
|
+
results = creator.create_issues_from_yaml(file)
|
|
590
|
+
|
|
591
|
+
expect(results.first[:status]).to eq(:skipped)
|
|
592
|
+
expect(results.first[:issue_number]).to eq(9)
|
|
593
|
+
file.unlink
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def write_yaml_with_github(issue_number, document: nil)
|
|
509
597
|
github = issue_number ? { "issue_number" => issue_number, "status" => "open" } : nil
|
|
598
|
+
sheet = { "version" => "2012-03", "stage" => "DIS",
|
|
599
|
+
"comments" => [{ "id" => "US-001", "body" => "US", "comments" => "Text",
|
|
600
|
+
"github" => github }.compact] }
|
|
601
|
+
sheet["document"] = document unless document.nil?
|
|
510
602
|
file = Tempfile.new(["comments", ".yaml"])
|
|
511
|
-
file.write(
|
|
512
|
-
"comments" => [{ "id" => "US-001", "body" => "US", "comments" => "Text",
|
|
513
|
-
"github" => github }.compact] }.to_yaml)
|
|
603
|
+
file.write(sheet.to_yaml)
|
|
514
604
|
file.close
|
|
515
605
|
file
|
|
516
606
|
end
|
|
@@ -22,6 +22,10 @@ RSpec.describe Commenter::SheetValidator do
|
|
|
22
22
|
expect(problems).to include(severity: :warning, message: a_string_including("unusual stage"))
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
it "accepts the AWI stage without a warning" do
|
|
26
|
+
expect(described_class.call(sheet([comment("DE-001")], stage: "AWI"))).to eq([])
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
it "reports missing ids, missing text, and duplicate ids as errors" do
|
|
26
30
|
problems = described_class.call(sheet([
|
|
27
31
|
comment("", text: "No id"),
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: commenter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -166,6 +166,8 @@ files:
|
|
|
166
166
|
- lib/commenter/github_issue_retriever.rb
|
|
167
167
|
- lib/commenter/github_session.rb
|
|
168
168
|
- lib/commenter/github_sync.rb
|
|
169
|
+
- lib/commenter/issue_marker.rb
|
|
170
|
+
- lib/commenter/milestone_resolver.rb
|
|
169
171
|
- lib/commenter/parser.rb
|
|
170
172
|
- lib/commenter/parser/osd_xlsx_parser.rb
|
|
171
173
|
- lib/commenter/parser/track_change_docx_parser.rb
|