ruby-jira 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/CHANGELOG.md +14 -0
- data/LICENSE.txt +24 -0
- data/README.md +273 -0
- data/lib/jira/api.rb +22 -0
- data/lib/jira/client/issues.rb +35 -0
- data/lib/jira/client/project_permission_schemes.rb +37 -0
- data/lib/jira/client/projects.rb +33 -0
- data/lib/jira/client.rb +43 -0
- data/lib/jira/configuration.rb +101 -0
- data/lib/jira/error.rb +211 -0
- data/lib/jira/objectified_hash.rb +66 -0
- data/lib/jira/pagination/cursor_paginated_response.rb +92 -0
- data/lib/jira/pagination/paginated_response.rb +96 -0
- data/lib/jira/request/authentication.rb +180 -0
- data/lib/jira/request/paginated_response.rb +4 -0
- data/lib/jira/request/rate_limiting.rb +126 -0
- data/lib/jira/request/request_building.rb +78 -0
- data/lib/jira/request/response_parsing.rb +45 -0
- data/lib/jira/request.rb +153 -0
- data/lib/jira/version.rb +5 -0
- data/lib/jira.rb +65 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a560af049fc2790fd46fcfa42a8913ff6a3e35fb6da619d0dbee7873a872fdec
|
|
4
|
+
data.tar.gz: d5e1af098eac6a126ca0643545effb8c9f420ef30349c716d127d7018ff3e8c5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c144da359a042b32d600661c40eb4d8588eac575ded10b1a00cec642958631fe50a878f240a97c2d8e6220824aa49973f49103fa1e4cac0f0774f6f39d754886
|
|
7
|
+
data.tar.gz: 15b14457c90a78271c0af4644276d945dd3cea0d980919967f24081d11a53b5b0b8a710204aeaa71757c68b89798a63d5f83ca3301543d79e3f2e9d02d8f92d5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-03-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Add initial ruby-jira Jira API client gem ([09c7c53](https://github.com/macio/ruby-jira/commit/09c7c5302cec9ccd079d85f0c46466f53775e9da))
|
|
13
|
+
|
|
14
|
+
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Maciej Kozak
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# ruby-jira
|
|
2
|
+
|
|
3
|
+
Ruby client for the [Jira Cloud REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/).
|
|
4
|
+
|
|
5
|
+
> Inspired by and based on the architecture of [NARKOZ/gitlab](https://github.com/NARKOZ/gitlab) — a Ruby wrapper for the GitLab API. Many thanks for the solid foundation.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
Ruby **3.2** or newer. Tested on 3.2, 3.3, and 3.4. Ruby 3.1 and older are not supported (EOL).
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add to your Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem "ruby-jira"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Authentication
|
|
20
|
+
|
|
21
|
+
Two auth methods are supported: **Basic** (email + API token) and **OAuth 2.0**.
|
|
22
|
+
|
|
23
|
+
### Basic auth
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
Jira.configure do |config|
|
|
27
|
+
config.endpoint = "https://your-domain.atlassian.net"
|
|
28
|
+
config.auth_type = :basic
|
|
29
|
+
config.email = "you@example.com"
|
|
30
|
+
config.api_token = "your-api-token"
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or via environment variables:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
JIRA_ENDPOINT=https://your-domain.atlassian.net
|
|
38
|
+
JIRA_EMAIL=you@example.com
|
|
39
|
+
JIRA_API_TOKEN=your-api-token
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### OAuth 2.0 — pre-fetched access token
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
Jira.configure do |config|
|
|
46
|
+
config.endpoint = "https://your-domain.atlassian.net"
|
|
47
|
+
config.auth_type = :oauth2
|
|
48
|
+
config.cloud_id = "your-cloud-id"
|
|
49
|
+
config.oauth_access_token = "your-access-token"
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### OAuth 2.0 — automatic token refresh (`refresh_token` grant)
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
Jira.configure do |config|
|
|
57
|
+
config.endpoint = "https://your-domain.atlassian.net"
|
|
58
|
+
config.auth_type = :oauth2
|
|
59
|
+
config.cloud_id = "your-cloud-id"
|
|
60
|
+
config.oauth_grant_type = "refresh_token"
|
|
61
|
+
config.oauth_client_id = "your-client-id"
|
|
62
|
+
config.oauth_client_secret = "your-client-secret"
|
|
63
|
+
config.oauth_refresh_token = "your-refresh-token"
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### OAuth 2.0 — service account (`client_credentials` grant)
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
Jira.configure do |config|
|
|
71
|
+
config.endpoint = "https://your-domain.atlassian.net"
|
|
72
|
+
config.auth_type = :oauth2
|
|
73
|
+
config.cloud_id = "your-cloud-id"
|
|
74
|
+
config.oauth_grant_type = "client_credentials"
|
|
75
|
+
config.oauth_client_id = "your-client-id"
|
|
76
|
+
config.oauth_client_secret = "your-client-secret"
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### Client
|
|
83
|
+
|
|
84
|
+
Create a one-off client or use the global `Jira` facade:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
client = Jira.client # uses global configuration
|
|
88
|
+
# or
|
|
89
|
+
client = Jira::Client.new(endpoint: "...", email: "...", api_token: "...")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
All methods are also available directly on the `Jira` module:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Jira.projects
|
|
96
|
+
Jira.issue("TEST-1")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Response objects
|
|
100
|
+
|
|
101
|
+
All responses are returned as `Jira::ObjectifiedHash` instances, supporting both dot-notation and bracket access:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
issue = Jira.issue("TEST-1")
|
|
105
|
+
issue.key # => "TEST-1"
|
|
106
|
+
issue[:key] # => "TEST-1"
|
|
107
|
+
issue.fields.summary
|
|
108
|
+
issue.dig(:fields, :summary)
|
|
109
|
+
issue.to_h # => original Hash
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Projects
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# Search projects (offset-paginated)
|
|
116
|
+
projects = Jira.projects(status: "live", maxResults: 50)
|
|
117
|
+
projects.total # => 42
|
|
118
|
+
projects.next_page? # => true
|
|
119
|
+
projects.map(&:key) # => ["TEST", "DEMO", ...]
|
|
120
|
+
|
|
121
|
+
# Auto-paginate all projects
|
|
122
|
+
all = projects.auto_paginate
|
|
123
|
+
all = projects.paginate_with_limit(100)
|
|
124
|
+
|
|
125
|
+
# Get a single project
|
|
126
|
+
project = Jira.project("TEST")
|
|
127
|
+
project.name
|
|
128
|
+
project.lead.displayName
|
|
129
|
+
|
|
130
|
+
# Archive a project
|
|
131
|
+
Jira.archive_project("TEST")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Issues
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Get a single issue
|
|
138
|
+
issue = Jira.issue("TEST-1")
|
|
139
|
+
issue = Jira.issue("TEST-1", expand: "names,renderedFields")
|
|
140
|
+
|
|
141
|
+
# Create an issue
|
|
142
|
+
issue = Jira.create_issue({
|
|
143
|
+
fields: {
|
|
144
|
+
project: { key: "TEST" },
|
|
145
|
+
summary: "Something is broken",
|
|
146
|
+
issuetype: { id: "10001" }
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
# Update an issue
|
|
151
|
+
Jira.edit_issue("TEST-1", { fields: { summary: "Updated summary" } })
|
|
152
|
+
Jira.edit_issue("TEST-1", { fields: { summary: "Silent update" } }, notifyUsers: false)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Permission schemes
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
Jira.permission_scheme("TEST")
|
|
159
|
+
Jira.issue_security_level_scheme("TEST")
|
|
160
|
+
Jira.assign_permission_scheme("TEST", scheme_id: 101)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Pagination
|
|
164
|
+
|
|
165
|
+
Offset-paginated responses (`GET /project/search`, `GET /workflow/search`, etc.) return `Jira::PaginatedResponse`:
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
page = Jira.projects
|
|
169
|
+
page.total # total count
|
|
170
|
+
page.start_at # current offset
|
|
171
|
+
page.max_results # page size
|
|
172
|
+
page.last_page? # isLast flag
|
|
173
|
+
page.next_page?
|
|
174
|
+
page.next_page # fetches the next page
|
|
175
|
+
page.auto_paginate # fetches all pages, returns flat Array
|
|
176
|
+
page.paginate_with_limit(200)
|
|
177
|
+
page.each_page { |p| process(p) }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Cursor-paginated responses (`POST /search/jql`, etc.) return `Jira::CursorPaginatedResponse`:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
results = Jira.search_issues(jql: "project = TEST ORDER BY created DESC")
|
|
184
|
+
results.next_page_token # raw token
|
|
185
|
+
results.next_page?
|
|
186
|
+
results.next_page # fetches next page automatically
|
|
187
|
+
results.auto_paginate # fetches all pages
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Rate limiting
|
|
191
|
+
|
|
192
|
+
> Atlassian enforces a new points-based and tiered quota rate limiting policy for Jira Cloud apps since **March 2, 2026**.
|
|
193
|
+
> This gem follows the current [official Jira Cloud Rate Limiting guide](https://developer.atlassian.com/cloud/jira/platform/rate-limiting/).
|
|
194
|
+
|
|
195
|
+
The client automatically retries `429 Too Many Requests` and `503 Service Unavailable` (when rate-limit headers are present) on idempotent requests (`GET`, `PUT`, `DELETE`).
|
|
196
|
+
|
|
197
|
+
**Supported response headers** (as enforced by Jira Cloud):
|
|
198
|
+
|
|
199
|
+
| Header | Format | Description |
|
|
200
|
+
| ----------------------- | ------------------ | ------------------------------------------------------------------------------ |
|
|
201
|
+
| `Retry-After` | integer seconds | How long to wait before retrying (429 and some 503) |
|
|
202
|
+
| `X-RateLimit-Reset` | ISO 8601 timestamp | When the rate-limit window resets (429 only) |
|
|
203
|
+
| `X-RateLimit-Limit` | integer | Max request rate for the current scope |
|
|
204
|
+
| `X-RateLimit-Remaining` | integer | Remaining capacity in the current window |
|
|
205
|
+
| `X-RateLimit-NearLimit` | `"true"` | Signals < 20% capacity remains — consider throttling proactively |
|
|
206
|
+
| `RateLimit-Reason` | string | Which limit was exceeded (`jira-burst-based`, `jira-quota-tenant-based`, etc.) |
|
|
207
|
+
|
|
208
|
+
**Retry strategy:** exponential backoff with proportional jitter (`delay × rand(0.7..1.3)`), respecting `Retry-After` and `X-RateLimit-Reset` headers. Falls back to backoff when no header is present.
|
|
209
|
+
|
|
210
|
+
Default configuration (aligned with Atlassian recommendations):
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
Jira.configure do |config|
|
|
214
|
+
config.ratelimit_retries = 4 # max retry attempts
|
|
215
|
+
config.ratelimit_base_delay = 2.0 # seconds, base for exponential backoff
|
|
216
|
+
config.ratelimit_max_delay = 30.0 # seconds, cap on backoff
|
|
217
|
+
end
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Proxy
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
Jira.http_proxy("proxy.example.com", 8080, "user", "pass")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Configuration reference
|
|
227
|
+
|
|
228
|
+
| Key | ENV variable | Default |
|
|
229
|
+
| ---------------------- | --------------------------- | ---------------------------------------- |
|
|
230
|
+
| `endpoint` | `JIRA_ENDPOINT` | — |
|
|
231
|
+
| `auth_type` | `JIRA_AUTH_TYPE` | `:basic` |
|
|
232
|
+
| `email` | `JIRA_EMAIL` | — |
|
|
233
|
+
| `api_token` | `JIRA_API_TOKEN` | — |
|
|
234
|
+
| `oauth_access_token` | `JIRA_OAUTH_ACCESS_TOKEN` | — |
|
|
235
|
+
| `oauth_client_id` | `JIRA_OAUTH_CLIENT_ID` | — |
|
|
236
|
+
| `oauth_client_secret` | `JIRA_OAUTH_CLIENT_SECRET` | — |
|
|
237
|
+
| `oauth_refresh_token` | `JIRA_OAUTH_REFRESH_TOKEN` | — |
|
|
238
|
+
| `oauth_grant_type` | `JIRA_OAUTH_GRANT_TYPE` | — |
|
|
239
|
+
| `oauth_token_endpoint` | `JIRA_OAUTH_TOKEN_ENDPOINT` | `https://auth.atlassian.com/oauth/token` |
|
|
240
|
+
| `cloud_id` | `JIRA_CLOUD_ID` | — |
|
|
241
|
+
| `ratelimit_retries` | `JIRA_RATELIMIT_RETRIES` | `4` |
|
|
242
|
+
| `ratelimit_base_delay` | `JIRA_RATELIMIT_BASE_DELAY` | `2.0` |
|
|
243
|
+
| `ratelimit_max_delay` | `JIRA_RATELIMIT_MAX_DELAY` | `30.0` |
|
|
244
|
+
|
|
245
|
+
## Error handling
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
rescue Jira::Error::Unauthorized # 401
|
|
249
|
+
rescue Jira::Error::Forbidden # 403
|
|
250
|
+
rescue Jira::Error::NotFound # 404
|
|
251
|
+
rescue Jira::Error::TooManyRequests # 429
|
|
252
|
+
rescue Jira::Error::ResponseError # any other 4xx/5xx
|
|
253
|
+
rescue Jira::Error::Base # all gem errors
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`Jira::Error::ResponseError` exposes:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
e.response_status # HTTP status code
|
|
260
|
+
e.response_message # parsed message from response body
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Running the example script
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
JIRA_ENDPOINT=https://your-domain.atlassian.net \
|
|
267
|
+
JIRA_EMAIL=you@example.com \
|
|
268
|
+
JIRA_API_TOKEN=your-api-token \
|
|
269
|
+
JIRA_PROJECT_KEY=TEST \
|
|
270
|
+
bundle exec ruby examples/basic_usage.rb
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
See [examples/basic_usage.rb](examples/basic_usage.rb) for all supported environment variables.
|
data/lib/jira/api.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jira
|
|
4
|
+
# @private
|
|
5
|
+
class API < Request
|
|
6
|
+
attr_accessor(*Configuration::VALID_OPTIONS_KEYS)
|
|
7
|
+
|
|
8
|
+
# Creates a new API.
|
|
9
|
+
# @raise [Jira::Error::MissingCredentials]
|
|
10
|
+
# rubocop:disable Lint/MissingSuper
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
options = Jira.options.merge(options)
|
|
13
|
+
Configuration::VALID_OPTIONS_KEYS.each do |key|
|
|
14
|
+
send("#{key}=", options[key]) if options.key?(key)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
request_defaults
|
|
18
|
+
self.class.headers "User-Agent" => user_agent
|
|
19
|
+
end
|
|
20
|
+
# rubocop:enable Lint/MissingSuper
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jira
|
|
4
|
+
class Client
|
|
5
|
+
# Defines methods related to issues.
|
|
6
|
+
module Issues
|
|
7
|
+
# Creates a new issue
|
|
8
|
+
#
|
|
9
|
+
# @param payload [Hash] Issue payload
|
|
10
|
+
# @return [Hash]
|
|
11
|
+
def create_issue(payload = {})
|
|
12
|
+
post("/issue", body: payload)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Gets a single issue
|
|
16
|
+
#
|
|
17
|
+
# @param issue_id_or_key [Integer, String] The ID or key of an issue
|
|
18
|
+
# @param options [Hash] Query parameters
|
|
19
|
+
# @return [Hash]
|
|
20
|
+
def issue(issue_id_or_key, options = {})
|
|
21
|
+
get("/issue/#{url_encode(issue_id_or_key)}", query: options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Updates an existing issue
|
|
25
|
+
#
|
|
26
|
+
# @param issue_id_or_key [Integer, String] The ID or key of an issue
|
|
27
|
+
# @param payload [Hash] Issue payload
|
|
28
|
+
# @param options [Hash] Query parameters
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
def edit_issue(issue_id_or_key, payload = {}, options = {})
|
|
31
|
+
put("/issue/#{url_encode(issue_id_or_key)}", body: payload, query: options)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jira
|
|
4
|
+
class Client
|
|
5
|
+
# Defines methods related to project permission schemes.
|
|
6
|
+
module ProjectPermissionSchemes
|
|
7
|
+
# Gets assigned issue security level scheme for a project
|
|
8
|
+
#
|
|
9
|
+
# @param project_key_or_id [Integer, String] Project ID or key
|
|
10
|
+
# @param options [Hash] Query parameters
|
|
11
|
+
# @return [Hash]
|
|
12
|
+
def issue_security_level_scheme(project_key_or_id, options = {})
|
|
13
|
+
get("/project/#{url_encode(project_key_or_id)}/issuesecuritylevelscheme", query: options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Gets assigned permission scheme for a project
|
|
17
|
+
#
|
|
18
|
+
# @param project_key_or_id [Integer, String] Project ID or key
|
|
19
|
+
# @param options [Hash] Query parameters
|
|
20
|
+
# @return [Hash]
|
|
21
|
+
def permission_scheme(project_key_or_id, options = {})
|
|
22
|
+
get("/project/#{url_encode(project_key_or_id)}/permissionscheme", query: options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Assigns permission scheme to a project
|
|
26
|
+
#
|
|
27
|
+
# @param project_key_or_id [Integer, String] Project ID or key
|
|
28
|
+
# @param scheme_id [Integer] Permission scheme ID
|
|
29
|
+
# @param options [Hash] Additional payload
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
def assign_permission_scheme(project_key_or_id, scheme_id:, options: {})
|
|
32
|
+
body = { id: scheme_id }.merge(options)
|
|
33
|
+
put("/project/#{url_encode(project_key_or_id)}/permissionscheme", body: body)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jira
|
|
4
|
+
class Client
|
|
5
|
+
# Defines methods related to projects.
|
|
6
|
+
module Projects
|
|
7
|
+
# Search projects
|
|
8
|
+
#
|
|
9
|
+
# @param options [Hash] Query parameters
|
|
10
|
+
# @return [Jira::Request::PaginatedResponse]
|
|
11
|
+
def projects(options = {})
|
|
12
|
+
get("/project/search", query: options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Gets a single project
|
|
16
|
+
#
|
|
17
|
+
# @param project_id_or_key [Integer, String] Project ID or key
|
|
18
|
+
# @param options [Hash] Query parameters
|
|
19
|
+
# @return [Hash]
|
|
20
|
+
def project(project_id_or_key, options = {})
|
|
21
|
+
get("/project/#{url_encode(project_id_or_key)}", query: options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Archives a project
|
|
25
|
+
#
|
|
26
|
+
# @param project_id_or_key [Integer, String] Project ID or key
|
|
27
|
+
# @return [Hash]
|
|
28
|
+
def archive_project(project_id_or_key)
|
|
29
|
+
post("/project/#{url_encode(project_id_or_key)}/archive")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/jira/client.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jira
|
|
4
|
+
class Client < API
|
|
5
|
+
Dir[File.expand_path("client/*.rb", __dir__)].each { |file| require file }
|
|
6
|
+
|
|
7
|
+
include Issues
|
|
8
|
+
include ProjectPermissionSchemes
|
|
9
|
+
include Projects
|
|
10
|
+
|
|
11
|
+
# Text representation of the client, masking auth secrets.
|
|
12
|
+
#
|
|
13
|
+
# @return [String]
|
|
14
|
+
def inspect
|
|
15
|
+
inspected = super
|
|
16
|
+
inspected = redact_secret(inspected, :api_token, @api_token) if @api_token
|
|
17
|
+
inspected = redact_secret(inspected, :oauth_access_token, @oauth_access_token) if @oauth_access_token
|
|
18
|
+
inspected = redact_secret(inspected, :oauth_client_secret, @oauth_client_secret) if @oauth_client_secret
|
|
19
|
+
inspected = redact_secret(inspected, :oauth_refresh_token, @oauth_refresh_token) if @oauth_refresh_token
|
|
20
|
+
inspected
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Utility method for URL encoding of a string.
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
def url_encode(url)
|
|
27
|
+
url.to_s.b.gsub(/[^a-zA-Z0-9_\-.~]/n) { |match| format("%%%02X", match.unpack1("C")) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def redact_secret(inspected, key, secret)
|
|
33
|
+
redacted = only_show_last_four_chars(secret)
|
|
34
|
+
inspected.sub %(@#{key}="#{secret}"), %(@#{key}="#{redacted}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def only_show_last_four_chars(token)
|
|
38
|
+
return "****" if token.size <= 4
|
|
39
|
+
|
|
40
|
+
"#{"*" * (token.size - 4)}#{token[-4..]}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Jira
|
|
6
|
+
module Configuration
|
|
7
|
+
VALID_OPTIONS_KEYS = %i[
|
|
8
|
+
endpoint
|
|
9
|
+
user_agent
|
|
10
|
+
httparty
|
|
11
|
+
auth_type
|
|
12
|
+
email
|
|
13
|
+
api_token
|
|
14
|
+
oauth_access_token
|
|
15
|
+
oauth_client_id
|
|
16
|
+
oauth_client_secret
|
|
17
|
+
oauth_refresh_token
|
|
18
|
+
oauth_grant_type
|
|
19
|
+
oauth_token_endpoint
|
|
20
|
+
cloud_id
|
|
21
|
+
ratelimit_retries
|
|
22
|
+
ratelimit_base_delay
|
|
23
|
+
ratelimit_max_delay
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
DEFAULT_USER_AGENT = "Ruby Jira Gem #{Jira::VERSION}".freeze
|
|
27
|
+
DEFAULT_AUTH_TYPE = :basic
|
|
28
|
+
DEFAULT_RATELIMIT_RETRIES = 4
|
|
29
|
+
DEFAULT_RATELIMIT_BASE_DELAY = 2.0
|
|
30
|
+
DEFAULT_RATELIMIT_MAX_DELAY = 30.0
|
|
31
|
+
DEFAULT_OAUTH_TOKEN_ENDPOINT = "https://auth.atlassian.com/oauth/token"
|
|
32
|
+
|
|
33
|
+
attr_accessor(*VALID_OPTIONS_KEYS)
|
|
34
|
+
|
|
35
|
+
def self.extended(base)
|
|
36
|
+
base.reset
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def configure
|
|
40
|
+
yield self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def options
|
|
44
|
+
VALID_OPTIONS_KEYS.to_h do |key|
|
|
45
|
+
[key, send(key)]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reset # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
50
|
+
self.endpoint = ENV.fetch("JIRA_ENDPOINT", nil)
|
|
51
|
+
self.auth_type = (ENV["JIRA_AUTH_TYPE"] || DEFAULT_AUTH_TYPE).to_sym
|
|
52
|
+
self.email = ENV.fetch("JIRA_EMAIL", nil)
|
|
53
|
+
self.api_token = ENV.fetch("JIRA_API_TOKEN", nil)
|
|
54
|
+
self.oauth_access_token = ENV.fetch("JIRA_OAUTH_ACCESS_TOKEN", nil)
|
|
55
|
+
self.oauth_client_id = ENV.fetch("JIRA_OAUTH_CLIENT_ID", nil)
|
|
56
|
+
self.oauth_client_secret = ENV.fetch("JIRA_OAUTH_CLIENT_SECRET", nil)
|
|
57
|
+
self.oauth_refresh_token = ENV.fetch("JIRA_OAUTH_REFRESH_TOKEN", nil)
|
|
58
|
+
self.oauth_grant_type = ENV.fetch("JIRA_OAUTH_GRANT_TYPE", nil)
|
|
59
|
+
self.oauth_token_endpoint = ENV.fetch("JIRA_OAUTH_TOKEN_ENDPOINT", DEFAULT_OAUTH_TOKEN_ENDPOINT)
|
|
60
|
+
self.cloud_id = ENV.fetch("JIRA_CLOUD_ID", nil)
|
|
61
|
+
self.ratelimit_retries = integer_env("JIRA_RATELIMIT_RETRIES", DEFAULT_RATELIMIT_RETRIES)
|
|
62
|
+
self.ratelimit_base_delay = float_env("JIRA_RATELIMIT_BASE_DELAY", DEFAULT_RATELIMIT_BASE_DELAY)
|
|
63
|
+
self.ratelimit_max_delay = float_env("JIRA_RATELIMIT_MAX_DELAY", DEFAULT_RATELIMIT_MAX_DELAY)
|
|
64
|
+
self.httparty = get_httparty_config(ENV.fetch("JIRA_HTTPARTY_OPTIONS", nil))
|
|
65
|
+
self.user_agent = DEFAULT_USER_AGENT
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def get_httparty_config(options)
|
|
71
|
+
return nil if options.nil? || options.empty?
|
|
72
|
+
|
|
73
|
+
config = YAML.safe_load(options, permitted_classes: [Symbol], aliases: false)
|
|
74
|
+
raise ArgumentError, "HTTParty config should be a Hash." unless config.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
symbolize_keys(config)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def symbolize_keys(value)
|
|
80
|
+
return value unless value.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
value.each_with_object({}) do |(key, nested_value), output|
|
|
83
|
+
output[key.to_sym] = nested_value.is_a?(Hash) ? symbolize_keys(nested_value) : nested_value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def integer_env(key, default)
|
|
88
|
+
value = ENV.fetch(key, nil)
|
|
89
|
+
value ? Integer(value, 10) : default
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
default
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def float_env(key, default)
|
|
95
|
+
value = ENV.fetch(key, nil)
|
|
96
|
+
value ? Float(value) : default
|
|
97
|
+
rescue ArgumentError
|
|
98
|
+
default
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|