jira-ruby 3.1.0 → 3.2.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codeql.yml +3 -3
  3. data/.github/workflows/{rubocop.yml → lint.yml} +3 -2
  4. data/.github/workflows/{CI.yml → test.yml} +2 -2
  5. data/README.md +2 -1
  6. data/jira-ruby.gemspec +1 -1
  7. data/lib/jira/atlassian/jwt.rb +76 -0
  8. data/lib/jira/base.rb +1 -0
  9. data/lib/jira/client.rb +10 -2
  10. data/lib/jira/jwt_client.rb +2 -2
  11. data/lib/jira/resource/attachment.rb +17 -2
  12. data/lib/jira/resource/comment.rb +12 -0
  13. data/lib/jira/resource/createmeta.rb +4 -4
  14. data/lib/jira/resource/issue.rb +1 -0
  15. data/lib/jira/resource/properties.rb +56 -0
  16. data/lib/jira/resource/worklog.rb +12 -0
  17. data/lib/jira/version.rb +1 -1
  18. data/lib/jira-ruby.rb +1 -0
  19. data/spec/data/files/jwt-signed-urls.json +317 -0
  20. data/spec/integration/properties_spec.rb +45 -0
  21. data/spec/integration/transition_spec.rb +3 -2
  22. data/spec/integration/webhook_spec.rb +4 -2
  23. data/spec/jira/atlassian/jwt_spec.rb +59 -0
  24. data/spec/jira/base_spec.rb +21 -0
  25. data/spec/jira/resource/issue_spec.rb +5 -1
  26. data/spec/mock_responses/issue/10002/properties/foo.json +4 -0
  27. data/spec/mock_responses/issue/10002/properties/xyz.json +4 -0
  28. data/spec/mock_responses/issue/10002/properties/xyz.put.json +4 -0
  29. data/spec/mock_responses/issue/10002/properties.json +12 -0
  30. data/spec/support/clients_helper.rb +3 -3
  31. data/spec/support/shared_examples/integration.rb +34 -38
  32. metadata +18 -11
  33. data/spec/mock_responses/jira/rest/webhooks/1.0/webhook.json +0 -11
  34. data/spec/mock_responses/webhook/webhook.json +0 -11
  35. /data/spec/mock_responses/{jira/rest/webhooks/1.0/webhook → webhook}/2.json +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d81df9752f33a1c48332b6793d3b6596359b77b1a79575b57382553c1e04400
4
- data.tar.gz: 5d6d16a50ccd3293b893068c303405a67450006197bd1d5aec85d75a9d3179e4
3
+ metadata.gz: 4f0687ac3651feaafae6addd37de84cf36bc0b39b25bb9e476ae20cc58c88c83
4
+ data.tar.gz: 0dcfa1a76303b6502a9ce22eb74d62c80070b3a859fd0f61a88cfdff91e6cc16
5
5
  SHA512:
6
- metadata.gz: e4dc469b98fac2378bf647cd33e433a33ddc17ddf83d2e6fe835d21eda2239c6e3899fe9a838ebb4b1130356fa011302dd7f463ce9f4a0f660d4f66c5258e547
7
- data.tar.gz: 2a09541af9b0028a16338dd13d31014e1e919e730e86ff370150536f1a2e4a8911532283dea8f7d57571010fffa5c2986097fda8d4da7034b35a3afbc806fcfa
6
+ metadata.gz: 3a41e5b87a3f9854a8bdebef3fcf5127d25ba00afe7a7c35e223c9de311004d50351eafd0437f3e8901a792b5cfbfc7a318cf55ec1c8faa32dd4e8180c286700
7
+ data.tar.gz: 953c1f03222337d49f0c5868c25ab226a1bbdef70f5b09063cfec6f90a0eb9d01b1f96de5f606b02d58d88f33c648738d2d640c77550f85ad4f705e9c929db14
@@ -60,11 +60,11 @@ jobs:
60
60
  # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
61
61
  steps:
62
62
  - name: Checkout repository
63
- uses: actions/checkout@v4
63
+ uses: actions/checkout@v7
64
64
 
65
65
  # Initializes the CodeQL tools for scanning.
66
66
  - name: Initialize CodeQL
67
- uses: github/codeql-action/init@v3
67
+ uses: github/codeql-action/init@v4
68
68
  with:
69
69
  languages: ${{ matrix.language }}
70
70
  build-mode: ${{ matrix.build-mode }}
@@ -91,6 +91,6 @@ jobs:
91
91
  exit 1
92
92
 
93
93
  - name: Perform CodeQL Analysis
94
- uses: github/codeql-action/analyze@v3
94
+ uses: github/codeql-action/analyze@v4
95
95
  with:
96
96
  category: "/language:${{matrix.language}}"
@@ -1,13 +1,14 @@
1
- name: Rubocop
1
+ name: Lint
2
2
 
3
3
  on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  rubocop:
7
+ name: Lint
7
8
  runs-on: ubuntu-latest
8
9
 
9
10
  steps:
10
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v7
11
12
  - name: Set up Ruby
12
13
  uses: ruby/setup-ruby@v1
13
14
  with:
@@ -1,4 +1,4 @@
1
- name: Ruby
1
+ name: Test
2
2
 
3
3
  on: [push, pull_request]
4
4
 
@@ -16,7 +16,7 @@ jobs:
16
16
  - '3.2'
17
17
  - '3.1'
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v7
20
20
 
21
21
  - name: Set up Ruby
22
22
  uses: ruby/setup-ruby@v1
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # JIRA API Gem
2
2
 
3
- [![Build Status](https://github.com/sumoheavy/jira-ruby/actions/workflows/CI.yml/badge.svg)](https://github.com/sumoheavy/jira-ruby/actions/workflows/CI.yml)
3
+ [![Build Status](https://github.com/sumoheavy/jira-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/sumoheavy/jira-ruby/actions/workflows/test.yml)
4
+ [![CodeQL](https://github.com/sumoheavy/jira-ruby/actions/workflows/codeql.yml/badge.svg)](https://github.com/sumoheavy/jira-ruby/actions/workflows/codeql.yml)
4
5
 
5
6
  This gem provides access to the Atlassian JIRA REST API.
6
7
 
data/jira-ruby.gemspec CHANGED
@@ -23,8 +23,8 @@ Gem::Specification.new do |s|
23
23
  s.require_paths = ['lib']
24
24
 
25
25
  s.add_dependency 'activesupport'
26
- s.add_dependency 'atlassian-jwt'
27
26
  s.add_dependency 'cgi'
27
+ s.add_dependency 'jwt', '>= 2.1'
28
28
  s.add_dependency 'multipart-post'
29
29
  s.add_dependency 'oauth', '~> 1.0'
30
30
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2013 Atlassian Pty Ltd.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'jwt'
18
+ require 'uri'
19
+ require 'cgi'
20
+
21
+ module JIRA
22
+ module Atlassian
23
+ module Jwt
24
+ class << self
25
+ CANONICAL_QUERY_SEPARATOR = '&'
26
+ ESCAPED_CANONICAL_QUERY_SEPARATOR = '%26'
27
+
28
+ def create_canonical_request(uri, http_method, base_uri)
29
+ uri = URI.parse(uri) unless uri.is_a? URI
30
+ base_uri = URI.parse(base_uri) unless base_uri.is_a? URI
31
+
32
+ [
33
+ http_method.upcase,
34
+ canonicalize_uri(uri, base_uri),
35
+ canonicalize_query_string(uri.query)
36
+ ].join(CANONICAL_QUERY_SEPARATOR)
37
+ end
38
+
39
+ def build_claims(issuer, url, http_method, base_url = '', issued_at = nil, expires = nil, attributes = {}) # rubocop:disable Metrics/ParameterLists
40
+ issued_at ||= Time.now.to_i
41
+ expires ||= issued_at + 60
42
+ qsh = Digest::SHA256.hexdigest(create_canonical_request(url, http_method, base_url))
43
+
44
+ {
45
+ iss: issuer,
46
+ iat: issued_at,
47
+ exp: expires,
48
+ qsh: qsh
49
+ }.merge(attributes)
50
+ end
51
+
52
+ def canonicalize_uri(uri, base_uri)
53
+ path = uri.path.sub(/^#{base_uri.path}/, '')
54
+ path = '/' if path.nil? || path.empty?
55
+ path = "/#{path}" unless path.start_with? '/'
56
+ path.chomp!('/') if path.length > 1
57
+ path.gsub(CANONICAL_QUERY_SEPARATOR, ESCAPED_CANONICAL_QUERY_SEPARATOR)
58
+ end
59
+
60
+ def canonicalize_query_string(query)
61
+ return '' if query.nil? || query.empty?
62
+
63
+ query = CGI.parse(query)
64
+ query.delete('jwt')
65
+ query.each do |k, v|
66
+ query[k] = v.map { |a| CGI.escape a }.join(',') if v.is_a? Array
67
+ query[k].gsub!('+', '%20') # Use %20, not CGI.escape default of "+"
68
+ query[k].gsub!('%7E', '~') # Unescape "~" per JS tests
69
+ end
70
+ query = query.sort.to_h
71
+ query.map { |k, v| "#{CGI.escape k}=#{v}" }.join(CANONICAL_QUERY_SEPARATOR)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
data/lib/jira/base.rb CHANGED
@@ -396,6 +396,7 @@ module JIRA
396
396
  else
397
397
  hash.each do |k, v|
398
398
  if v.is_a?(Hash)
399
+ target[k] = {} unless target[k].is_a?(Hash)
399
400
  set_attrs(v, clobber, target[k])
400
401
  else
401
402
  target[k] = v
data/lib/jira/client.rb CHANGED
@@ -302,6 +302,10 @@ module JIRA
302
302
  JIRA::Resource::AgileFactory.new(self)
303
303
  end
304
304
 
305
+ def Properties
306
+ JIRA::Resource::PropertiesFactory.new(self)
307
+ end
308
+
305
309
  # HTTP methods without a body
306
310
 
307
311
  # Make an HTTP DELETE request
@@ -351,7 +355,9 @@ module JIRA
351
355
  # @raise [JIRA::HTTPError] If the response is not an HTTP success code
352
356
  def post_multipart(path, file, headers = {})
353
357
  puts "post multipart: #{path} - [#{file}]" if @http_debug
354
- @request_client.request_multipart(path, file, merge_default_headers(headers))
358
+ res = @request_client.request_multipart(path, file, merge_default_headers(headers))
359
+ puts "response: #{res}" if @http_debug
360
+ res
355
361
  end
356
362
 
357
363
  # Make an HTTP PUT request
@@ -375,7 +381,9 @@ module JIRA
375
381
  # @raise [JIRA::HTTPError] If the response is not an HTTP success code
376
382
  def request(http_method, path, body = '', headers = {})
377
383
  puts "#{http_method}: #{path} - [#{body}]" if @http_debug
378
- @request_client.request(http_method, path, body, headers)
384
+ res = @request_client.request(http_method, path, body, headers)
385
+ puts "response: #{res}" if @http_debug
386
+ res
379
387
  end
380
388
 
381
389
  # @private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'atlassian/jwt'
3
+ require 'jira/atlassian/jwt'
4
4
 
5
5
  module JIRA
6
6
  class JwtClient < HttpClient
@@ -29,7 +29,7 @@ module JIRA
29
29
  end
30
30
 
31
31
  def build_jwt(url)
32
- claim = Atlassian::Jwt.build_claims \
32
+ claim = JIRA::Atlassian::Jwt.build_claims \
33
33
  @options[:issuer],
34
34
  url,
35
35
  http_method.to_s,
@@ -146,10 +146,14 @@ module JIRA
146
146
  # @raise [JIRA::HTTPError] if failed
147
147
  def save!(attrs, path = url)
148
148
  file = attrs['file'] || attrs[:file] # Keep supporting 'file' parameter as a string for backward compatibility
149
- mime_type = attrs[:mimeType] || 'application/binary'
149
+ # If :filename does not exist or is nil, that is fine as it will force
150
+ # Upload to determine the filename automatically from file.
151
+ # Breaking the filename out allows this to support any IO-based file parameter.
152
+ fname = attrs['filename'] || attrs[:filename]
153
+ mime_type = attrs['mimeType'] || attrs[:mimeType] || 'application/binary'
150
154
 
151
155
  headers = { 'X-Atlassian-Token' => 'nocheck' }
152
- data = { 'file' => Multipart::Post::UploadIO.new(file, mime_type, file) }
156
+ data = { 'file' => Multipart::Post::UploadIO.new(file, mime_type, fname) }
153
157
 
154
158
  response = client.post_multipart(path, data, headers)
155
159
 
@@ -159,6 +163,17 @@ module JIRA
159
163
  true
160
164
  end
161
165
 
166
+ def http_download
167
+ # Actually fetch the attachment
168
+ # Note: Jira handles attachment's weird!
169
+ # Typically, they respond with a redirect location that should not have the same authentication
170
+ client.get(attrs['content'])
171
+ rescue JIRA::HTTPError => e
172
+ raise e unless e.response.code =~ /\A3\d\d\z/ && e.response['location'].present?
173
+
174
+ Net::HTTP.get_response(URI(e.response['location']))
175
+ end
176
+
162
177
  private
163
178
 
164
179
  def set_attributes(attributes, response)
@@ -9,6 +9,18 @@ module JIRA
9
9
  belongs_to :issue
10
10
 
11
11
  nested_collections true
12
+
13
+ def self.all(client, options = {})
14
+ issue = options[:issue]
15
+ raise ArgumentError, 'parent issue is required' unless issue
16
+
17
+ response = client.get("#{issue.url}/#{endpoint_name}")
18
+ json = parse_json(response.body)
19
+ json = json[endpoint_name.pluralize]
20
+ json.map do |attrs|
21
+ new(client, { attrs: }.merge(options))
22
+ end
23
+ end
12
24
  end
13
25
  end
14
26
  end
@@ -12,22 +12,22 @@ module JIRA
12
12
 
13
13
  def self.all(client, params = {})
14
14
  if params.key?(:projectKeys)
15
- values = Array(params[:projectKeys]).map { |i| (i.is_a?(JIRA::Resource::Project) ? i.key : i) }
15
+ values = Array(params[:projectKeys]).map { |i| i.is_a?(JIRA::Resource::Project) ? i.key : i }
16
16
  params[:projectKeys] = values.join(',')
17
17
  end
18
18
 
19
19
  if params.key?(:projectIds)
20
- values = Array(params[:projectIds]).map { |i| (i.is_a?(JIRA::Resource::Project) ? i.id : i) }
20
+ values = Array(params[:projectIds]).map { |i| i.is_a?(JIRA::Resource::Project) ? i.id : i }
21
21
  params[:projectIds] = values.join(',')
22
22
  end
23
23
 
24
24
  if params.key?(:issuetypeNames)
25
- values = Array(params[:issuetypeNames]).map { |i| (i.is_a?(JIRA::Resource::Issuetype) ? i.name : i) }
25
+ values = Array(params[:issuetypeNames]).map { |i| i.is_a?(JIRA::Resource::Issuetype) ? i.name : i }
26
26
  params[:issuetypeNames] = values.join(',')
27
27
  end
28
28
 
29
29
  if params.key?(:issuetypeIds)
30
- values = Array(params[:issuetypeIds]).map { |i| (i.is_a?(JIRA::Resource::Issuetype) ? i.id : i) }
30
+ values = Array(params[:issuetypeIds]).map { |i| i.is_a?(JIRA::Resource::Issuetype) ? i.id : i }
31
31
  params[:issuetypeIds] = values.join(',')
32
32
  end
33
33
 
@@ -57,6 +57,7 @@ module JIRA
57
57
  has_many :issuelinks, nested_under: 'fields'
58
58
  has_many :remotelink, class: JIRA::Resource::Remotelink
59
59
  has_many :watchers, attribute_key: 'watches', nested_under: %w[fields watches]
60
+ has_many :properties, class: JIRA::Resource::Properties
60
61
 
61
62
  # Get collection of issues.
62
63
  # @param client [JIRA::Client]
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/inflector'
4
+
5
+ module JIRA
6
+ module Resource
7
+ class PropertiesFactory < JIRA::BaseFactory # :nodoc:
8
+ end
9
+
10
+ class Properties < JIRA::Base
11
+ belongs_to :issue
12
+
13
+ def self.key_attribute
14
+ :key
15
+ end
16
+
17
+ def self.all(client, options = {})
18
+ issue = options[:issue]
19
+ raise ArgumentError, 'parent issue is required' unless issue
20
+
21
+ response = client.get("#{issue.url}/#{endpoint_name}")
22
+ json = parse_json(response.body)
23
+ json[key_attribute.to_s.pluralize].map do |attrs|
24
+ ## Net get the individual property
25
+ self_response = client.get(attrs['self'])
26
+ property = parse_json(self_response.body)
27
+ ## Make sure to build the new resource via the issue.properties in order to support the has_many proxy
28
+ issue.properties.build(property)
29
+ end
30
+ end
31
+
32
+ ## Override save so we can handle the required attrs (and default 'value' when appropriate)
33
+ def save!(attrs = {}, path = nil)
34
+ if attrs.is_a?(Hash) && attrs.key?(self.class.key_attribute.to_s)
35
+ raise ArgumentError, "Use of 'value' is required when '#{self.class.key_attribute}' is provided" \
36
+ unless attrs.key?('value')
37
+
38
+ set_attrs(self.class.key_attribute.to_s => attrs[self.class.key_attribute.to_s])
39
+ end
40
+
41
+ raise ArgumentError, "'key' is required on a new record" if new_record?
42
+
43
+ path ||= patched_url
44
+ ## We can take either the 'value' element from the hash, OR use the entire attrs as the value
45
+ value = attrs.is_a?(Hash) && attrs.key?('value') ? attrs['value'] : attrs
46
+ value = '' if value.nil?
47
+ ## Note: this API endpoint requires a non-empty JSON body for the value of the property
48
+ ## Note2: this API endpoint does not return a body, so no need to call set_attrs_from_response
49
+ client.send(:put, path, value.to_json)
50
+ set_attrs({ 'value' => value }, false)
51
+ @expanded = false
52
+ true
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,6 +10,18 @@ module JIRA
10
10
  has_one :update_author, class: JIRA::Resource::User, attribute_key: 'updateAuthor'
11
11
  belongs_to :issue
12
12
  nested_collections true
13
+
14
+ def self.all(client, options = {})
15
+ issue = options[:issue]
16
+ raise ArgumentError, 'parent issue is required' unless issue
17
+
18
+ response = client.get("#{issue.url}/#{endpoint_name}")
19
+ json = parse_json(response.body)
20
+ json = json[endpoint_name.pluralize]
21
+ json.map do |attrs|
22
+ new(client, { attrs: }.merge(options))
23
+ end
24
+ end
13
25
  end
14
26
  end
15
27
  end
data/lib/jira/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JIRA
4
- VERSION = '3.1.0'
4
+ VERSION = '3.2.1'
5
5
  end
data/lib/jira-ruby.rb CHANGED
@@ -23,6 +23,7 @@ require 'jira/resource/status'
23
23
  require 'jira/resource/status_category'
24
24
  require 'jira/resource/transition'
25
25
  require 'jira/resource/project'
26
+ require 'jira/resource/properties'
26
27
  require 'jira/resource/priority'
27
28
  require 'jira/resource/comment'
28
29
  require 'jira/resource/worklog'