marlens-jira-api 0.5.0 → 0.5.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/README.md +5 -1
- data/lib/marlens/jira_api/cli.rb +23 -6
- data/lib/marlens/jira_api/client.rb +13 -7
- data/lib/marlens/jira_api/markdown_to_adf.rb +66 -6
- data/lib/marlens/jira_api/remote_image_attachment_uploader.rb +19 -4
- data/lib/marlens/jira_api/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89d099771b89c4a51659eaca4c4242d7102762ec507e4d80265ce2e1612d756c
|
|
4
|
+
data.tar.gz: 93682e74950ac69b58316049b1251f61eb6f1a8eed75d7d14695585da4ad45d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20e4b6ecd4f692d4c9b699f1ed4d0a3fe2f3ae5be5de2bb7970c540efacdbd08ec45a9dd9384184fb38300b23d5ac07dd0adec482eb93025ec9f8d8d6e28ecfb
|
|
7
|
+
data.tar.gz: ad230b1e8235738fecc90762794c5e8ca2fd07f62fa3d551c2f47eca630177676c3d04e5af53e7d5f699f6ce1ce9ff511971b037cef1e01c663b21a08f2ae98c
|
data/README.md
CHANGED
|
@@ -50,6 +50,8 @@ jira-comment delete --issue-key WRAPX-123 --comment-id 10001
|
|
|
50
50
|
|
|
51
51
|
Remote Markdown image URLs are advanced opt-in behavior. Pass `allowed_image_hosts:` in Ruby or repeat `--image-host` in the CLI only when you intentionally want the gem to fetch images from trusted hosts, upload them to Jira, and convert them into Jira media attachments.
|
|
52
52
|
|
|
53
|
+
Ruby callers that need to detect degraded images can pass an array via `image_upload_failures:`. Add `strict_images: true` in Ruby or `--strict-images` in the CLI to raise `Marlens::JiraApi::ImageUploadError` instead of falling back to `Alt text: URL` when an image cannot be fetched or uploaded.
|
|
54
|
+
|
|
53
55
|
## Feature and Issue Workflow
|
|
54
56
|
|
|
55
57
|
Preferred flow for issue and feature work:
|
|
@@ -61,4 +63,6 @@ Preferred flow for issue and feature work:
|
|
|
61
63
|
5. Open a draft PR, link the issue, include the checks run, and mark it ready only after verification.
|
|
62
64
|
6. Merge the PR to `main` so GitHub records the review path.
|
|
63
65
|
7. After merge, build and publish the gem version to RubyGems with `gem build marlens-jira-api.gemspec` and `gem push marlens-jira-api-<version>.gem`.
|
|
64
|
-
8. Verify RubyGems lists the released version with `gem list --remote marlens-jira-api --exact --all
|
|
66
|
+
8. Verify RubyGems lists the released version with `gem list --remote marlens-jira-api --exact --all`; if that output is stale, verify the specific version API at `https://rubygems.org/api/v2/rubygems/marlens-jira-api/versions/<version>.json`.
|
|
67
|
+
9. Run an install or CLI smoke check against the published gem.
|
|
68
|
+
10. Keep one-off smoke cleanup helpers in `tmp/`, delete them before committing, and do not turn cleanup-only scripts into product API.
|
data/lib/marlens/jira_api/cli.rb
CHANGED
|
@@ -37,14 +37,14 @@ module Marlens
|
|
|
37
37
|
|
|
38
38
|
@out.puts JSON.pretty_generate(send(config.fetch(:action), options))
|
|
39
39
|
0
|
|
40
|
-
rescue OptionParser::ParseError, KeyError => error
|
|
40
|
+
rescue OptionParser::ParseError, KeyError, ImageUploadError => error
|
|
41
41
|
fail_with(error.message)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
46
|
def parse_options
|
|
47
|
-
options = { allowed_image_hosts: [] }
|
|
47
|
+
options = { allowed_image_hosts: [], strict_images: false }
|
|
48
48
|
parser = OptionParser.new do |parser|
|
|
49
49
|
parser.banner = "Usage: jira-comment [#{COMMANDS.keys.join("|")}] [options]"
|
|
50
50
|
parser.on("--issue-key ISSUE", "Jira issue key") { |value| options[:issue_key] = value }
|
|
@@ -53,6 +53,9 @@ module Marlens
|
|
|
53
53
|
parser.on("--image-host HOST", "Allowed remote image host; repeatable") do |value|
|
|
54
54
|
options[:allowed_image_hosts] << value
|
|
55
55
|
end
|
|
56
|
+
parser.on("--strict-images", "Fail create/update when a remote image cannot be uploaded") do
|
|
57
|
+
options[:strict_images] = true
|
|
58
|
+
end
|
|
56
59
|
end
|
|
57
60
|
parser.parse!(@argv)
|
|
58
61
|
options
|
|
@@ -67,20 +70,28 @@ module Marlens
|
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
def create_comment(options)
|
|
70
|
-
|
|
73
|
+
image_upload_failures = []
|
|
74
|
+
result = client.create_markdown_comment(
|
|
71
75
|
issue_key: options.fetch(:issue_key),
|
|
72
76
|
markdown: File.read(options.fetch(:markdown_file)),
|
|
73
|
-
allowed_image_hosts: options.fetch(:allowed_image_hosts)
|
|
77
|
+
allowed_image_hosts: options.fetch(:allowed_image_hosts),
|
|
78
|
+
strict_images: options.fetch(:strict_images),
|
|
79
|
+
image_upload_failures: image_upload_failures
|
|
74
80
|
)
|
|
81
|
+
result_with_image_failures(result, image_upload_failures)
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
def update_comment(options)
|
|
78
|
-
|
|
85
|
+
image_upload_failures = []
|
|
86
|
+
result = client.update_markdown_comment(
|
|
79
87
|
issue_key: options.fetch(:issue_key),
|
|
80
88
|
comment_id: options.fetch(:comment_id),
|
|
81
89
|
markdown: File.read(options.fetch(:markdown_file)),
|
|
82
|
-
allowed_image_hosts: options.fetch(:allowed_image_hosts)
|
|
90
|
+
allowed_image_hosts: options.fetch(:allowed_image_hosts),
|
|
91
|
+
strict_images: options.fetch(:strict_images),
|
|
92
|
+
image_upload_failures: image_upload_failures
|
|
83
93
|
)
|
|
94
|
+
result_with_image_failures(result, image_upload_failures)
|
|
84
95
|
end
|
|
85
96
|
|
|
86
97
|
def delete_comment(options)
|
|
@@ -88,6 +99,12 @@ module Marlens
|
|
|
88
99
|
{ deleted: true }
|
|
89
100
|
end
|
|
90
101
|
|
|
102
|
+
def result_with_image_failures(result, image_upload_failures)
|
|
103
|
+
return result if image_upload_failures.empty?
|
|
104
|
+
|
|
105
|
+
result.merge("image_upload_failures" => image_upload_failures)
|
|
106
|
+
end
|
|
107
|
+
|
|
91
108
|
def client
|
|
92
109
|
@client ||= @client_factory.call(env: @env)
|
|
93
110
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "base64"
|
|
4
4
|
require "json"
|
|
5
|
-
require "
|
|
5
|
+
require "net/http/post/multipart"
|
|
6
6
|
require "net/http"
|
|
7
7
|
require "uri"
|
|
8
8
|
|
|
@@ -54,20 +54,24 @@ module Marlens
|
|
|
54
54
|
raise "Failed to delete Jira comment #{comment_id} for #{issue_key}: #{response.code} #{response.body}"
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def create_markdown_comment(issue_key:, markdown:, allowed_image_hosts: [])
|
|
57
|
+
def create_markdown_comment(issue_key:, markdown:, allowed_image_hosts: [], strict_images: false, image_upload_failures: nil)
|
|
58
58
|
document = markdown_document(
|
|
59
59
|
issue_key: issue_key,
|
|
60
60
|
markdown: markdown,
|
|
61
|
-
allowed_image_hosts: allowed_image_hosts
|
|
61
|
+
allowed_image_hosts: allowed_image_hosts,
|
|
62
|
+
strict_images: strict_images,
|
|
63
|
+
image_upload_failures: image_upload_failures
|
|
62
64
|
)
|
|
63
65
|
create_comment(issue_key: issue_key, document: document)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
|
-
def update_markdown_comment(issue_key:, comment_id:, markdown:, allowed_image_hosts: [])
|
|
68
|
+
def update_markdown_comment(issue_key:, comment_id:, markdown:, allowed_image_hosts: [], strict_images: false, image_upload_failures: nil)
|
|
67
69
|
document = markdown_document(
|
|
68
70
|
issue_key: issue_key,
|
|
69
71
|
markdown: markdown,
|
|
70
|
-
allowed_image_hosts: allowed_image_hosts
|
|
72
|
+
allowed_image_hosts: allowed_image_hosts,
|
|
73
|
+
strict_images: strict_images,
|
|
74
|
+
image_upload_failures: image_upload_failures
|
|
71
75
|
)
|
|
72
76
|
update_comment(issue_key: issue_key, comment_id: comment_id, document: document)
|
|
73
77
|
end
|
|
@@ -94,11 +98,13 @@ module Marlens
|
|
|
94
98
|
|
|
95
99
|
private
|
|
96
100
|
|
|
97
|
-
def markdown_document(issue_key:, markdown:, allowed_image_hosts:)
|
|
101
|
+
def markdown_document(issue_key:, markdown:, allowed_image_hosts:, strict_images:, image_upload_failures:)
|
|
98
102
|
image_uploader = RemoteImageAttachmentUploader.new(
|
|
99
103
|
client: self,
|
|
100
104
|
issue_key: issue_key,
|
|
101
|
-
allowed_hosts: allowed_image_hosts
|
|
105
|
+
allowed_hosts: allowed_image_hosts,
|
|
106
|
+
strict: strict_images,
|
|
107
|
+
failures: image_upload_failures
|
|
102
108
|
)
|
|
103
109
|
MarkdownToAdf.call(markdown) do |image|
|
|
104
110
|
image_uploader.media_node_for(image)
|
|
@@ -31,6 +31,25 @@ module Marlens
|
|
|
31
31
|
}
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def self.external_media_single(url:, alt:, width:, height:)
|
|
35
|
+
{
|
|
36
|
+
"type" => "mediaSingle",
|
|
37
|
+
"attrs" => { "layout" => "center" },
|
|
38
|
+
"content" => [
|
|
39
|
+
{
|
|
40
|
+
"type" => "media",
|
|
41
|
+
"attrs" => {
|
|
42
|
+
"type" => "external",
|
|
43
|
+
"url" => url,
|
|
44
|
+
"alt" => alt,
|
|
45
|
+
"width" => width,
|
|
46
|
+
"height" => height,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
34
53
|
def self.paragraph(text)
|
|
35
54
|
new("").send(:paragraph_from_text, text)
|
|
36
55
|
end
|
|
@@ -79,7 +98,7 @@ module Marlens
|
|
|
79
98
|
when :heading
|
|
80
99
|
heading(node)
|
|
81
100
|
when :paragraph
|
|
82
|
-
|
|
101
|
+
paragraph_blocks(node)
|
|
83
102
|
when :code_block
|
|
84
103
|
code_block(node)
|
|
85
104
|
when :list
|
|
@@ -105,19 +124,23 @@ module Marlens
|
|
|
105
124
|
}
|
|
106
125
|
end
|
|
107
126
|
|
|
108
|
-
def
|
|
109
|
-
return image_block_for(children(node).first) if single_image_node?(node)
|
|
127
|
+
def paragraph_blocks(node)
|
|
128
|
+
return [image_block_for(children(node).first)] if single_image_node?(node)
|
|
129
|
+
|
|
130
|
+
split_paragraph_around_html_images(node) || [paragraph_from_inline_content(inline_content(node))]
|
|
131
|
+
end
|
|
110
132
|
|
|
133
|
+
def paragraph_from_text(text)
|
|
111
134
|
{
|
|
112
135
|
"type" => "paragraph",
|
|
113
|
-
"content" =>
|
|
136
|
+
"content" => [text_node(text)],
|
|
114
137
|
}
|
|
115
138
|
end
|
|
116
139
|
|
|
117
|
-
def
|
|
140
|
+
def paragraph_from_inline_content(content)
|
|
118
141
|
{
|
|
119
142
|
"type" => "paragraph",
|
|
120
|
-
"content" =>
|
|
143
|
+
"content" => content,
|
|
121
144
|
}
|
|
122
145
|
end
|
|
123
146
|
|
|
@@ -210,6 +233,43 @@ module Marlens
|
|
|
210
233
|
)
|
|
211
234
|
end
|
|
212
235
|
|
|
236
|
+
def split_paragraph_around_html_images(node)
|
|
237
|
+
blocks = []
|
|
238
|
+
pending_content = []
|
|
239
|
+
|
|
240
|
+
children(node).each do |child|
|
|
241
|
+
image_block = inline_html_image_block(child)
|
|
242
|
+
|
|
243
|
+
if image_block.nil?
|
|
244
|
+
append_inline_content(pending_content, render_inline(child, []))
|
|
245
|
+
else
|
|
246
|
+
append_paragraph_block(blocks, pending_content)
|
|
247
|
+
pending_content = []
|
|
248
|
+
blocks << image_block
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
return nil if blocks.empty?
|
|
252
|
+
|
|
253
|
+
append_paragraph_block(blocks, pending_content)
|
|
254
|
+
blocks
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def inline_html_image_block(node)
|
|
258
|
+
return nil unless node.type == :html_inline
|
|
259
|
+
|
|
260
|
+
html_image_block(node)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def append_paragraph_block(blocks, content)
|
|
264
|
+
blocks << paragraph_from_inline_content(content) unless content.empty?
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def append_inline_content(content, inline)
|
|
268
|
+
return if inline.nil?
|
|
269
|
+
|
|
270
|
+
inline.is_a?(Array) ? content.concat(inline.compact) : content << inline
|
|
271
|
+
end
|
|
272
|
+
|
|
213
273
|
def resolved_image_block(url:, alt: nil, width: nil, height: nil)
|
|
214
274
|
return nil if url.nil? || url.strip.empty?
|
|
215
275
|
return fallback_image_paragraph(url, alt) if @image_resolver.nil?
|
|
@@ -7,26 +7,41 @@ require "uri"
|
|
|
7
7
|
|
|
8
8
|
module Marlens
|
|
9
9
|
module JiraApi
|
|
10
|
+
class ImageUploadError < StandardError
|
|
11
|
+
attr_reader :failure
|
|
12
|
+
|
|
13
|
+
def initialize(failure)
|
|
14
|
+
@failure = failure
|
|
15
|
+
super("Failed to upload image #{failure.fetch(:url)}: #{failure.fetch(:error_class)}: #{failure.fetch(:error_message)}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
10
19
|
class RemoteImageAttachmentUploader
|
|
11
20
|
MAX_REDIRECTS = 5
|
|
12
21
|
|
|
13
|
-
def initialize(client:, issue_key:, allowed_hosts:)
|
|
22
|
+
def initialize(client:, issue_key:, allowed_hosts:, strict: false, failures: nil)
|
|
14
23
|
@allowed_hosts = allowed_hosts
|
|
15
24
|
@client = client
|
|
25
|
+
@failures = failures
|
|
16
26
|
@issue_key = issue_key
|
|
27
|
+
@strict = strict
|
|
17
28
|
end
|
|
18
29
|
|
|
19
30
|
def media_node_for(image)
|
|
20
31
|
attachment = upload_image_attachment(image.fetch(:url))
|
|
21
32
|
dimensions = attachment.fetch("dimensions")
|
|
22
33
|
|
|
23
|
-
MarkdownToAdf.
|
|
24
|
-
|
|
34
|
+
MarkdownToAdf.external_media_single(
|
|
35
|
+
url: attachment.fetch("content"),
|
|
25
36
|
alt: image[:alt].to_s.strip.empty? ? attachment.fetch("filename") : image[:alt],
|
|
26
37
|
width: image_dimension_value(image[:width], dimensions.fetch("width")),
|
|
27
38
|
height: image_dimension_value(image[:height], dimensions.fetch("height"))
|
|
28
39
|
)
|
|
29
40
|
rescue StandardError => error
|
|
41
|
+
failure = { url: image[:url], alt: image[:alt], error_class: error.class.name, error_message: error.message }
|
|
42
|
+
@failures << failure if @failures
|
|
43
|
+
raise ImageUploadError.new(failure) if @strict
|
|
44
|
+
|
|
30
45
|
warn "Failed to upload image #{image[:url]}: #{error.class}: #{error.message}"
|
|
31
46
|
MarkdownToAdf.paragraph("#{image[:alt] || "Image"}: #{image[:url]}")
|
|
32
47
|
end
|
|
@@ -55,7 +70,7 @@ module Marlens
|
|
|
55
70
|
)
|
|
56
71
|
|
|
57
72
|
return {
|
|
58
|
-
"
|
|
73
|
+
"content" => attachment.fetch("content").to_s,
|
|
59
74
|
"filename" => attachment.fetch("filename", filename),
|
|
60
75
|
"dimensions" => dimensions,
|
|
61
76
|
}
|