marlens-github-to-jira-comment 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +72 -0
- data/bin/github-to-jira-comment +6 -0
- data/lib/marlens/github_to_jira_comment/cli.rb +105 -0
- data/lib/marlens/github_to_jira_comment/comment_poster.rb +31 -0
- data/lib/marlens/github_to_jira_comment/github_client.rb +68 -0
- data/lib/marlens/github_to_jira_comment/github_markdown_resolver.rb +137 -0
- data/lib/marlens/github_to_jira_comment/github_url.rb +37 -0
- data/lib/marlens/github_to_jira_comment/jira_url.rb +23 -0
- data/lib/marlens/github_to_jira_comment/version.rb +7 -0
- data/lib/marlens/github_to_jira_comment.rb +14 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3d2671f36558d94fbc4e03f4b9fb6d94f406cdaf5cdf0d450ccf97e921446fc9
|
|
4
|
+
data.tar.gz: dab9a7f4e03050178663e4471ed8646648b477d4a16b3800318492f1906e358b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d3420e1f18718e6a3024cca6019ba055754940473e9890740b417fb3b8003c980824051e06b9f06c72761cd7ef156276be77245969424c4515f0ea5fd5ebc515
|
|
7
|
+
data.tar.gz: 0cac0986612cb0f105d57d221e102474c595fbab3e743f937377647f01d957bd0970bfa39241cd00a41c2c18882964ea3bf953ae4801483a9bd3e4b2a1a3efeb
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marlen Brunner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# GitHub to Jira Comment
|
|
2
|
+
|
|
3
|
+
Small Ruby CLI/gem for copying a GitHub issue or pull request body into a Jira issue comment with rich Jira formatting.
|
|
4
|
+
|
|
5
|
+
## Required contract
|
|
6
|
+
|
|
7
|
+
Both source and destination are URLs, not shorthand tags:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
github-to-jira-comment \
|
|
11
|
+
--github-url https://github.com/icefoganalytics/wrap/pull/405 \
|
|
12
|
+
--jira-url https://yg-hpw.atlassian.net/browse/WRAPX-388
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Credentials come from environment variables. `JIRA_BASE_URL` is optional; the CLI derives it from `--jira-url` unless you override it.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
GITHUB_TOKEN=...
|
|
19
|
+
JIRA_EMAIL=...
|
|
20
|
+
JIRA_API_TOKEN=...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "marlens-github-to-jira-comment", "~> 0.1"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## CLI
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
github-to-jira-comment \
|
|
33
|
+
--github-url https://github.com/icefoganalytics/wrap/pull/405 \
|
|
34
|
+
--jira-url https://yg-hpw.atlassian.net/browse/WRAPX-388
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Release cycle
|
|
38
|
+
|
|
39
|
+
1. Work from an issue branch and open a draft PR linked to the issue.
|
|
40
|
+
2. Run `bundle exec rspec`, `bin/github-to-jira-comment --help`, and `gem build marlens-github-to-jira-comment.gemspec`.
|
|
41
|
+
3. Delete the generated `.gem` file after the build check.
|
|
42
|
+
4. Mark the PR ready, merge it to `main`, then update local `main`.
|
|
43
|
+
5. Build the release artifact from `main` with `gem build marlens-github-to-jira-comment.gemspec`.
|
|
44
|
+
6. Publish with `gem push marlens-github-to-jira-comment-<version>.gem`.
|
|
45
|
+
7. Tag the release as `v<version>` and create the GitHub release.
|
|
46
|
+
8. Verify RubyGems lists the version with `gem list --remote marlens-github-to-jira-comment --exact --all`.
|
|
47
|
+
9. Install the published gem in a temporary `GEM_HOME` and smoke-check `github-to-jira-comment --help`.
|
|
48
|
+
10. Delete the local `.gem` artifact after verification.
|
|
49
|
+
|
|
50
|
+
## MVP scope
|
|
51
|
+
|
|
52
|
+
1. Accept a full GitHub pull request URL or GitHub issue URL.
|
|
53
|
+
2. Accept a full Jira issue URL.
|
|
54
|
+
3. Fetch the GitHub title/body through the GitHub API.
|
|
55
|
+
4. Convert/post the GitHub body to Jira using `marlens-jira-api`.
|
|
56
|
+
5. Resolve real GitHub `user-attachments` images while preserving normal links and code blocks.
|
|
57
|
+
6. Fail on image-upload degradation by using strict image handling.
|
|
58
|
+
7. Return or print the created Jira comment ID.
|
|
59
|
+
|
|
60
|
+
## Proved live example
|
|
61
|
+
|
|
62
|
+
- GitHub source: https://github.com/icefoganalytics/wrap/pull/405
|
|
63
|
+
- Jira destination: https://yg-hpw.atlassian.net/browse/WRAPX-388
|
|
64
|
+
- Existing script-created Jira comment: `69213`
|
|
65
|
+
- Created at: `2026-07-09T11:42:26.501-0700`
|
|
66
|
+
- Verified markers:
|
|
67
|
+
- `Fixes https://yg-hpw.atlassian.net/browse/WRAPX-388` rendered as text + Jira link mark.
|
|
68
|
+
- `# Context` rendered as an ADF heading.
|
|
69
|
+
- numbered Implementation/Testing lists rendered as ordered lists.
|
|
70
|
+
- testing commands rendered as code marks.
|
|
71
|
+
|
|
72
|
+
See `docs/HANDOFF.md` for implementation notes and source artifacts.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "marlens/jira_api"
|
|
5
|
+
|
|
6
|
+
module Marlens
|
|
7
|
+
module GithubToJiraComment
|
|
8
|
+
class CLI
|
|
9
|
+
def self.run(argv = ARGV, env: ENV, out: $stdout, err: $stderr, github_client_factory: nil, jira_client_factory: nil)
|
|
10
|
+
new(env:, out:, err:, github_client_factory:, jira_client_factory:).call(argv)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(env: ENV, out: $stdout, err: $stderr, github_client_factory: nil, jira_client_factory: nil, markdown_resolver_factory: nil, comment_poster_factory: nil)
|
|
14
|
+
@env = env
|
|
15
|
+
@out = out
|
|
16
|
+
@err = err
|
|
17
|
+
@github_client_factory = github_client_factory
|
|
18
|
+
@jira_client_factory = jira_client_factory
|
|
19
|
+
@markdown_resolver_factory = markdown_resolver_factory
|
|
20
|
+
@comment_poster_factory = comment_poster_factory
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(argv)
|
|
24
|
+
options = parse_options(argv.dup)
|
|
25
|
+
missing = %i[github_url jira_url].select { |key| options[key].to_s.empty? }
|
|
26
|
+
return fail_with("Missing required option(s): #{option_names(missing)}") unless missing.empty?
|
|
27
|
+
|
|
28
|
+
source = github_client.fetch(options.fetch(:github_url))
|
|
29
|
+
resolved = resolve_markdown(source.body, github_url: options.fetch(:github_url))
|
|
30
|
+
result = post_comment(options.fetch(:jira_url), resolved)
|
|
31
|
+
|
|
32
|
+
@out.puts(result.fetch("id") { result.fetch(:id) })
|
|
33
|
+
0
|
|
34
|
+
rescue ArgumentError, KeyError, OptionParser::ParseError, RuntimeError => error
|
|
35
|
+
fail_with(error.message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
ResolvedMarkdown = Struct.new(:markdown, :allowed_image_hosts, keyword_init: true)
|
|
41
|
+
|
|
42
|
+
def parse_options(argv)
|
|
43
|
+
options = {}
|
|
44
|
+
OptionParser.new do |parser|
|
|
45
|
+
parser.banner = "Usage: github-to-jira-comment --github-url URL --jira-url URL"
|
|
46
|
+
parser.on("--github-url URL", "GitHub pull request or issue URL") { |value| options[:github_url] = value }
|
|
47
|
+
parser.on("--jira-url URL", "Jira browse URL") { |value| options[:jira_url] = value }
|
|
48
|
+
end.parse!(argv)
|
|
49
|
+
options
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def resolve_markdown(markdown, github_url:)
|
|
53
|
+
resolver = markdown_resolver
|
|
54
|
+
result = if resolver.respond_to?(:resolve_with_metadata)
|
|
55
|
+
resolver.resolve_with_metadata(markdown, context: GithubUrl.parse(github_url).context)
|
|
56
|
+
else
|
|
57
|
+
resolver.resolve(markdown)
|
|
58
|
+
end
|
|
59
|
+
result.respond_to?(:markdown) ? result : ResolvedMarkdown.new(markdown: result, allowed_image_hosts: CommentPoster::DEFAULT_IMAGE_HOSTS)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def post_comment(jira_url, resolved)
|
|
63
|
+
poster = comment_poster(jira_url)
|
|
64
|
+
options = {
|
|
65
|
+
jira_url:,
|
|
66
|
+
markdown: resolved.markdown,
|
|
67
|
+
allowed_image_hosts: resolved.allowed_image_hosts,
|
|
68
|
+
}
|
|
69
|
+
poster.post(**options)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def github_client
|
|
73
|
+
@github_client ||= @github_client_factory&.call || GithubClient.new(token: @env.fetch("GITHUB_TOKEN"))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def markdown_resolver
|
|
77
|
+
@markdown_resolver ||= @markdown_resolver_factory&.call || GithubMarkdownResolver.new(github_client: github_client)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def comment_poster(jira_url)
|
|
81
|
+
@comment_poster ||= @comment_poster_factory&.call || CommentPoster.new(jira_client: jira_client(jira_url))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def jira_client(jira_url)
|
|
85
|
+
jira_issue = JiraUrl.parse(jira_url)
|
|
86
|
+
return @jira_client_factory.call(env: @env, base_url: @env["JIRA_BASE_URL"] || jira_issue.base_url) if @jira_client_factory
|
|
87
|
+
|
|
88
|
+
Marlens::JiraApi::Client.new(
|
|
89
|
+
base_url: @env["JIRA_BASE_URL"] || jira_issue.base_url,
|
|
90
|
+
email: @env.fetch("JIRA_EMAIL"),
|
|
91
|
+
api_token: @env.fetch("JIRA_API_TOKEN")
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def fail_with(message)
|
|
96
|
+
@err.puts(message)
|
|
97
|
+
1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def option_names(keys)
|
|
101
|
+
keys.map { |key| "--#{key.to_s.tr("_", "-")}" }.join(", ")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Marlens
|
|
4
|
+
module GithubToJiraComment
|
|
5
|
+
class CommentPoster
|
|
6
|
+
DEFAULT_IMAGE_HOSTS = ["private-user-images.githubusercontent.com"].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(jira_client:)
|
|
9
|
+
@jira_client = jira_client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def post(jira_url:, markdown:, allowed_image_hosts: DEFAULT_IMAGE_HOSTS)
|
|
13
|
+
create(
|
|
14
|
+
issue_key: JiraUrl.parse(jira_url).issue_key,
|
|
15
|
+
markdown:,
|
|
16
|
+
allowed_image_hosts:
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def create(issue_key:, markdown:, allowed_image_hosts: [])
|
|
21
|
+
@jira_client.create_markdown_comment(
|
|
22
|
+
issue_key:,
|
|
23
|
+
markdown:,
|
|
24
|
+
allowed_image_hosts:,
|
|
25
|
+
strict_images: true,
|
|
26
|
+
image_upload_failures: []
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Marlens
|
|
8
|
+
module GithubToJiraComment
|
|
9
|
+
class GithubClient
|
|
10
|
+
Content = Struct.new(:title, :body, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
API_ROOT = "https://api.github.com"
|
|
13
|
+
|
|
14
|
+
def initialize(token:, http: nil, requester: nil)
|
|
15
|
+
@token = token
|
|
16
|
+
@http = http
|
|
17
|
+
@requester = requester || method(:request)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fetch(source)
|
|
21
|
+
source = GithubUrl.parse(source) if source.is_a?(String)
|
|
22
|
+
json = get_json(source.api_path)
|
|
23
|
+
Content.new(title: json.fetch("title"), body: json["body"].to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_markdown(markdown, context:)
|
|
27
|
+
uri = URI("#{API_ROOT}/markdown")
|
|
28
|
+
request = Net::HTTP::Post.new(uri)
|
|
29
|
+
request.body = JSON.dump("text" => markdown, "mode" => "gfm", "context" => context)
|
|
30
|
+
response = perform(uri, request)
|
|
31
|
+
return response.body if response.code.to_i.between?(200, 299)
|
|
32
|
+
|
|
33
|
+
raise "GitHub Markdown render failed: #{response.code} #{response.body}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def get_json(path)
|
|
39
|
+
return @http.get(path, headers:) if @http
|
|
40
|
+
|
|
41
|
+
uri = URI("#{API_ROOT}#{path}")
|
|
42
|
+
response = perform(uri, Net::HTTP::Get.new(uri))
|
|
43
|
+
return JSON.parse(response.body) if response.code.to_i.between?(200, 299)
|
|
44
|
+
|
|
45
|
+
raise "GitHub API request failed: #{response.code} #{response.body}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def perform(uri, request)
|
|
49
|
+
headers.each { |key, value| request[key] = value }
|
|
50
|
+
request["Content-Type"] = "application/json" if request.request_body_permitted?
|
|
51
|
+
@requester.call(uri, request)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def headers
|
|
55
|
+
{
|
|
56
|
+
"Accept" => "application/vnd.github+json",
|
|
57
|
+
"Authorization" => "Bearer #{@token}",
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def request(uri, request)
|
|
62
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
63
|
+
http.request(request)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "commonmarker"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Marlens
|
|
7
|
+
module GithubToJiraComment
|
|
8
|
+
class GithubMarkdownResolver
|
|
9
|
+
Result = Struct.new(:markdown, :allowed_image_hosts, keyword_init: true)
|
|
10
|
+
Image = Struct.new(:url, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
MARKDOWN_IMAGE = /!\[[^\]]*\]\(([^)\s]+)(\s+"[^"]*")?\)/
|
|
13
|
+
HTML_IMAGE = /<img\b(?<attributes>[^>]+)>/i
|
|
14
|
+
|
|
15
|
+
def initialize(github_client: nil, renderer: nil)
|
|
16
|
+
@github_client = github_client
|
|
17
|
+
@renderer = renderer
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resolve(markdown, context: nil)
|
|
21
|
+
resolve_with_metadata(markdown, context:).markdown
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def resolve_with_metadata(markdown, context: nil)
|
|
25
|
+
images = image_occurrences(markdown)
|
|
26
|
+
attachment_images = images.select { |image| github_user_attachment?(image.url) }
|
|
27
|
+
return Result.new(markdown:, allowed_image_hosts: []) if attachment_images.empty?
|
|
28
|
+
|
|
29
|
+
rendered_images = rendered_image_urls(render_markdown(markdown, context:)).select { |url| rendered_attachment?(url) }
|
|
30
|
+
mappings = {}
|
|
31
|
+
attachment_images.each_with_index do |image, index|
|
|
32
|
+
rendered = rendered_images[index] || mappings[image.url]
|
|
33
|
+
raise "GitHub Markdown render did not return an image for #{image.url}" unless rendered
|
|
34
|
+
|
|
35
|
+
mappings[image.url] ||= rendered
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
resolved = rewrite_image_urls(markdown, mappings)
|
|
39
|
+
Result.new(markdown: resolved, allowed_image_hosts: mappings.values.filter_map { |url| host(url) }.uniq)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def render_markdown(markdown, context:)
|
|
45
|
+
return @renderer.render(markdown) if @renderer
|
|
46
|
+
|
|
47
|
+
@github_client.render_markdown(markdown, context:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def image_occurrences(markdown)
|
|
51
|
+
images = []
|
|
52
|
+
collect_images(markdown_document(markdown), images)
|
|
53
|
+
images
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def rewrite_image_urls(markdown, mappings)
|
|
57
|
+
fenced = false
|
|
58
|
+
markdown.each_line.map do |line|
|
|
59
|
+
if line.match?(/\A\s*(```|~~~)/)
|
|
60
|
+
fenced = !fenced
|
|
61
|
+
next line
|
|
62
|
+
end
|
|
63
|
+
next line if fenced
|
|
64
|
+
|
|
65
|
+
line
|
|
66
|
+
.gsub(MARKDOWN_IMAGE) { |match| mappings.key?($1) ? match.sub($1, mappings.fetch($1)) : match }
|
|
67
|
+
.gsub(HTML_IMAGE) { |match| rewrite_html_image(match, mappings) }
|
|
68
|
+
end.join
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def rewrite_html_image(tag, mappings)
|
|
72
|
+
url = html_image_url(tag)
|
|
73
|
+
return tag unless mappings.key?(url)
|
|
74
|
+
|
|
75
|
+
tag.sub(url, mappings.fetch(url))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def collect_images(node, images)
|
|
79
|
+
case node.type
|
|
80
|
+
when :image
|
|
81
|
+
images << Image.new(url: node.url)
|
|
82
|
+
when :html_inline, :html_block
|
|
83
|
+
html_image_url(node.to_commonmark)&.then { |url| images << Image.new(url:) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
node.each { |child| collect_images(child, images) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def markdown_document(markdown)
|
|
90
|
+
Commonmarker.parse(
|
|
91
|
+
markdown,
|
|
92
|
+
options: {
|
|
93
|
+
parse: { smart: true },
|
|
94
|
+
extension: { autolink: true, strikethrough: true, table: true, tagfilter: false },
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rendered_image_urls(html)
|
|
100
|
+
html.scan(HTML_IMAGE).filter_map do |match|
|
|
101
|
+
parse_html_attributes(match.first)["src"]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def html_image_url(html)
|
|
106
|
+
match = html.match(HTML_IMAGE)
|
|
107
|
+
return nil unless match
|
|
108
|
+
|
|
109
|
+
parse_html_attributes(match[:attributes])["src"]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_html_attributes(attributes)
|
|
113
|
+
attributes.scan(/([a-zA-Z:-]+)=["']([^"']+)["']/).to_h
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def rendered_attachment?(url)
|
|
118
|
+
uri = URI.parse(url)
|
|
119
|
+
uri.host == "private-user-images.githubusercontent.com" || github_user_attachment?(url)
|
|
120
|
+
rescue URI::InvalidURIError
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
def github_user_attachment?(url)
|
|
124
|
+
uri = URI.parse(url)
|
|
125
|
+
uri.scheme == "https" && uri.host == "github.com" && uri.path.start_with?("/user-attachments/assets/")
|
|
126
|
+
rescue URI::InvalidURIError
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def host(url)
|
|
131
|
+
URI.parse(url).host
|
|
132
|
+
rescue URI::InvalidURIError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Marlens
|
|
6
|
+
module GithubToJiraComment
|
|
7
|
+
class GithubUrl
|
|
8
|
+
Source = Struct.new(:owner, :repo, :type, :number, keyword_init: true) do
|
|
9
|
+
def api_path
|
|
10
|
+
collection = type == "pull" ? "pulls" : "issues"
|
|
11
|
+
"/repos/#{owner}/#{repo}/#{collection}/#{number}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def context
|
|
15
|
+
"#{owner}/#{repo}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.parse(url)
|
|
20
|
+
uri = URI.parse(url)
|
|
21
|
+
segments = uri.path.split("/").reject(&:empty?)
|
|
22
|
+
unless uri.is_a?(URI::HTTPS) && uri.host == "github.com" && segments.length == 4
|
|
23
|
+
raise ArgumentError, "GitHub URL must be https://github.com/<owner>/<repo>/pull/<number> or /issues/<number>"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
owner, repo, path_type, number = segments
|
|
27
|
+
unless %w[pull issues].include?(path_type) && number.match?(/\A\d+\z/)
|
|
28
|
+
raise ArgumentError, "GitHub URL must point to a pull request or issue number"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Source.new(owner:, repo:, type: path_type == "pull" ? "pull" : "issue", number:)
|
|
32
|
+
rescue URI::InvalidURIError
|
|
33
|
+
raise ArgumentError, "Invalid GitHub URL"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Marlens
|
|
6
|
+
module GithubToJiraComment
|
|
7
|
+
class JiraUrl
|
|
8
|
+
Issue = Struct.new(:base_url, :issue_key, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def self.parse(url)
|
|
11
|
+
uri = URI.parse(url)
|
|
12
|
+
match = uri.path.match(%r{\A/browse/([A-Z][A-Z0-9_]*-\d+)\z})
|
|
13
|
+
unless uri.is_a?(URI::HTTPS) && uri.host&.end_with?(".atlassian.net") && match
|
|
14
|
+
raise ArgumentError, "Jira URL must be https://<site>.atlassian.net/browse/<ISSUE-KEY>"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Issue.new(base_url: "#{uri.scheme}://#{uri.host}", issue_key: match[1])
|
|
18
|
+
rescue URI::InvalidURIError
|
|
19
|
+
raise ArgumentError, "Invalid Jira URL"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "github_to_jira_comment/version"
|
|
4
|
+
require_relative "github_to_jira_comment/github_url"
|
|
5
|
+
require_relative "github_to_jira_comment/jira_url"
|
|
6
|
+
require_relative "github_to_jira_comment/github_client"
|
|
7
|
+
require_relative "github_to_jira_comment/github_markdown_resolver"
|
|
8
|
+
require_relative "github_to_jira_comment/comment_poster"
|
|
9
|
+
require_relative "github_to_jira_comment/cli"
|
|
10
|
+
|
|
11
|
+
module Marlens
|
|
12
|
+
module GithubToJiraComment
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: marlens-github-to-jira-comment
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Marlen Brunner
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: marlens-jira-api
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.5'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: commonmarker
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.8'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.8'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.13'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.13'
|
|
54
|
+
description: Small CLI/gem bridge from full GitHub issue/pull request URLs to full
|
|
55
|
+
Jira issue URLs.
|
|
56
|
+
email:
|
|
57
|
+
- klondike.marlen@gmail.com
|
|
58
|
+
executables:
|
|
59
|
+
- github-to-jira-comment
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- LICENSE.txt
|
|
64
|
+
- README.md
|
|
65
|
+
- bin/github-to-jira-comment
|
|
66
|
+
- lib/marlens/github_to_jira_comment.rb
|
|
67
|
+
- lib/marlens/github_to_jira_comment/cli.rb
|
|
68
|
+
- lib/marlens/github_to_jira_comment/comment_poster.rb
|
|
69
|
+
- lib/marlens/github_to_jira_comment/github_client.rb
|
|
70
|
+
- lib/marlens/github_to_jira_comment/github_markdown_resolver.rb
|
|
71
|
+
- lib/marlens/github_to_jira_comment/github_url.rb
|
|
72
|
+
- lib/marlens/github_to_jira_comment/jira_url.rb
|
|
73
|
+
- lib/marlens/github_to_jira_comment/version.rb
|
|
74
|
+
homepage: https://github.com/klondikemarlen/github-to-jira-comment
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
homepage_uri: https://github.com/klondikemarlen/github-to-jira-comment
|
|
79
|
+
source_code_uri: https://github.com/klondikemarlen/github-to-jira-comment
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '3.2'
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 4.0.10
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Copy GitHub issue or pull request bodies into Jira comments.
|
|
97
|
+
test_files: []
|