ruby-jira
Ruby client for the Jira Cloud REST API v3 and the Jira Software Cloud REST API (agile — sprints, boards).
Inspired by and based on the architecture of NARKOZ/gitlab — a Ruby wrapper for the GitLab API. Many thanks for the solid foundation.
Requirements
Ruby 3.2 or newer. Tested on 3.2, 3.3, and 3.4. Ruby 3.1 and older are not supported (EOL).
Installation
Add to your Gemfile:
gem "ruby-jira"Authentication
Two auth methods are supported: Basic (email + API token) and OAuth 2.0.
Basic auth
Jira.configure do |config|
config.endpoint = "https://your-domain.atlassian.net"
config.auth_type = :basic
config.email = "you@example.com"
config.api_token = "your-api-token"
endOr via environment variables:
JIRA_ENDPOINT=https://your-domain.atlassian.net
JIRA_EMAIL=you@example.com
JIRA_API_TOKEN=your-api-token
OAuth 2.0 - pre-fetched access token
Jira.configure do |config|
config.endpoint = "https://your-domain.atlassian.net"
config.auth_type = :oauth2
config.cloud_id = "your-cloud-id"
config.oauth_access_token = "your-access-token"
endOAuth 2.0 - automatic token refresh (refresh_token grant)
Jira.configure do |config|
config.endpoint = "https://your-domain.atlassian.net"
config.auth_type = :oauth2
config.cloud_id = "your-cloud-id"
config.oauth_grant_type = "refresh_token"
config.oauth_client_id = "your-client-id"
config.oauth_client_secret = "your-client-secret"
config.oauth_refresh_token = "your-refresh-token"
endOAuth 2.0 - service account (client_credentials grant)
Jira.configure do |config|
config.endpoint = "https://your-domain.atlassian.net"
config.auth_type = :oauth2
config.cloud_id = "your-cloud-id"
config.oauth_grant_type = "client_credentials"
config.oauth_client_id = "your-client-id"
config.oauth_client_secret = "your-client-secret"
endUsage
Client
Create a one-off client or use the global Jira facade:
client = Jira.client # uses global configuration
# or
client = Jira::Client.new(endpoint: "...", email: "...", api_token: "...")All methods are also available directly on the Jira module:
Jira.projects
Jira.issue("TEST-1")Response objects
All responses are returned as Jira::ObjectifiedHash instances, supporting both dot-notation and bracket access:
issue = Jira.issue("TEST-1")
issue.key # => "TEST-1"
issue[:key] # => "TEST-1"
issue.fields.summary
issue.dig(:fields, :summary)
issue.to_h # => original HashProjects
# Search projects (offset-paginated)
projects = Jira.projects(status: "live", maxResults: 50)
projects.total # => 42
projects.next_page? # => true
projects.map(&:key) # => ["TEST", "DEMO", ...]
# Auto-paginate all projects
all = projects.auto_paginate
all = projects.paginate_with_limit(100)
# Get a single project
project = Jira.project("TEST")
project.name
project.lead.displayName
# Archive a project
Jira.archive_project("TEST")Issues
# Get a single issue
issue = Jira.issue("TEST-1")
issue = Jira.issue("TEST-1", expand: "names,renderedFields")
# Create an issue
issue = Jira.create_issue({
fields: {
project: { key: "TEST" },
summary: "Something is broken",
issuetype: { id: "10001" }
}
})
# Update an issue
Jira.edit_issue("TEST-1", { fields: { summary: "Updated summary" } })
Jira.edit_issue("TEST-1", { fields: { summary: "Silent update" } }, notifyUsers: false)Permission schemes
Jira.permission_scheme("TEST")
Jira.issue_security_level_scheme("TEST")
Jira.assign_permission_scheme("TEST", scheme_id: 101)Boards (Jira Software Cloud API)
Board methods use the Jira Software Cloud REST API (/rest/agile/1.0) transparently - no extra configuration needed.
# List all boards (offset-paginated)
boards = Jira.boards(type: "scrum", maxResults: 50)
boards.total
boards.first[:name] # => "scrum board"
boards.auto_paginate
# Boards for a filter
Jira.boards_for_filter(1001)
# Get / create / delete a board
board = Jira.board(84)
board.name # => "scrum board"
board.type # => "scrum"
Jira.create_board({ name: "New Board", type: "scrum", filterId: 1001 })
Jira.delete_board(84)
# Board configuration
config = Jira.board_configuration(84)
config[:columnConfig][:columns].map { |c| c[:name] } # => ["To Do", "In Progress", "Done"]
# Features (enable/disable board panels)
Jira.board_features(84)
Jira.toggle_board_feature(84, { feature: "BACKLOG", state: "ENABLED" })
# Backlog and issues
Jira.board_backlog(84, maxResults: 50)
Jira.board_issues(84, jql: "assignee = currentUser()")
Jira.move_issues_to_board(84, issues: ["TEST-1", "TEST-2"])
# Epics
Jira.board_epics(84, done: false)
Jira.board_issues_without_epic(84)
Jira.board_epic_issues(84, 37)
# Sprints
sprints = Jira.board_sprints(84, state: "active")
Jira.board_sprint_issues(84, 37)
# Projects and versions
Jira.board_projects(84)
Jira.board_projects_full(84)
Jira.board_versions(84, released: false)
# Quick filters
Jira.board_quick_filters(84)
Jira.board_quick_filter(84, 1)
# Reports
Jira.board_reports(84)
# Board properties
Jira.board_property_keys(84)
Jira.board_property(84, "my.key")
Jira.set_board_property(84, "my.key", { foo: "bar" })
Jira.delete_board_property(84, "my.key")Sprints (Jira Software Cloud API)
Sprint methods use the Jira Software Cloud REST API (/rest/agile/1.0) transparently — no extra configuration needed.
# Get a sprint
sprint = Jira.sprint(37)
sprint.name # => "sprint 1"
sprint.state # => "future"
# Create / update / delete
Jira.create_sprint({ name: "Sprint 5", originBoardId: 5, goal: "ship it" })
Jira.update_sprint(37, { name: "Sprint 5 — revised" }) # partial update (POST)
Jira.replace_sprint(37, { name: "Sprint 5", originBoardId: 5, state: "active" }) # full update (PUT)
Jira.delete_sprint(37)
# Issues in a sprint (offset-paginated)
issues = Jira.sprint_issues(37, maxResults: 50)
issues.total
issues.auto_paginate
issues.map { |i| i[:key] }
# Move issues into a sprint
Jira.move_issues_to_sprint(37, issues: ["TEST-1", "TEST-2"])
# Swap sprint position with another sprint
Jira.swap_sprint(37, sprint_to_swap_with: 42)
# Sprint properties
Jira.sprint_property_keys(37)
Jira.sprint_property(37, "my.key")
Jira.set_sprint_property(37, "my.key", { foo: "bar" })
Jira.delete_sprint_property(37, "my.key")Pagination
Jira Cloud uses multiple pagination shapes across endpoints. This gem unifies them with auto_paginate, each_page,
and paginate_with_limit.
Offset-paginated responses return Jira::PaginatedResponse - includes GET /project/search, GET /issue/{key}/comment,
GET /issue/{key}/worklog, and others:
page = Jira.projects
page.total # total count
page.start_at # current offset
page.max_results # page size
page.last_page? # isLast flag
page.next_page?
page.next_page # fetches the next page
page.auto_paginate # fetches all pages, returns flat Array
page.paginate_with_limit(200)
page.each_page { |p| process(p) }Cursor-paginated responses (GET /search/jql, POST /search/jql) return Jira::CursorPaginatedResponse:
# GET /search/jql returns minimal issue payload by default (id only).
# Pass fields/expand to fetch richer issue data.
results = Jira.search_issues_jql(
jql: "project = TEST ORDER BY created DESC",
fields: "key,summary"
)
results.next_page_token # raw token
results.next_page?
results.next_page # fetches next page automatically
results.auto_paginate # fetches all pages
results.paginate_with_limit(200)Rate limiting
Atlassian enforces a new points-based and tiered quota rate limiting policy for Jira Cloud apps since March 2, 2026. This gem follows the current official Jira Cloud Rate Limiting guide.
The client automatically retries 429 Too Many Requests and 503 Service Unavailable (when rate-limit headers are present) on idempotent requests (GET, PUT, DELETE).
Supported response headers (as enforced by Jira Cloud):
| Header | Format | Description |
|---|---|---|
Retry-After |
integer seconds | How long to wait before retrying (429 and some 503) |
X-RateLimit-Reset |
ISO 8601 timestamp | When the rate-limit window resets (429 only) |
X-RateLimit-Limit |
integer | Max request rate for the current scope |
X-RateLimit-Remaining |
integer | Remaining capacity in the current window |
X-RateLimit-NearLimit |
"true" |
Signals < 20% capacity remains - consider throttling proactively |
RateLimit-Reason |
string | Which limit was exceeded (jira-burst-based, jira-quota-tenant-based, etc.) |
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.
Default configuration (aligned with Atlassian recommendations):
Jira.configure do |config|
config.ratelimit_retries = 4 # max retry attempts
config.ratelimit_base_delay = 2.0 # seconds, base for exponential backoff
config.ratelimit_max_delay = 30.0 # seconds, cap on backoff
endLogging
Pass any Logger-compatible object to enable debug logging. All requests, detected response types, and rate-limit retries are logged at DEBUG level.
require "logger"
Jira.configure do |config|
config.logger = Logger.new($stdout)
endSample output:
GET /project/search {query: {maxResults: 50}}
→ Jira::PaginatedResponse
GET /search/jql {query: {jql: "project=TEST", nextPageToken: "..."}}
→ Jira::CursorPaginatedResponse
rate limited (HTTP 429), retrying in 5.0s (3 retries left)
Logging is disabled by default (config.logger = nil).
Proxy
Jira.http_proxy("proxy.example.com", 8080, "user", "pass")Configuration reference
| Key | ENV variable | Default |
|---|---|---|
endpoint |
JIRA_ENDPOINT |
— |
auth_type |
JIRA_AUTH_TYPE |
:basic |
email |
JIRA_EMAIL |
— |
api_token |
JIRA_API_TOKEN |
— |
oauth_access_token |
JIRA_OAUTH_ACCESS_TOKEN |
— |
oauth_client_id |
JIRA_OAUTH_CLIENT_ID |
— |
oauth_client_secret |
JIRA_OAUTH_CLIENT_SECRET |
— |
oauth_refresh_token |
JIRA_OAUTH_REFRESH_TOKEN |
— |
oauth_grant_type |
JIRA_OAUTH_GRANT_TYPE |
— |
oauth_token_endpoint |
JIRA_OAUTH_TOKEN_ENDPOINT |
https://auth.atlassian.com/oauth/token |
cloud_id |
JIRA_CLOUD_ID |
— |
ratelimit_retries |
JIRA_RATELIMIT_RETRIES |
4 |
ratelimit_base_delay |
JIRA_RATELIMIT_BASE_DELAY |
2.0 |
ratelimit_max_delay |
JIRA_RATELIMIT_MAX_DELAY |
30.0 |
logger |
— | nil |
Error handling
rescue Jira::Error::Unauthorized # 401
rescue Jira::Error::Forbidden # 403
rescue Jira::Error::NotFound # 404
rescue Jira::Error::TooManyRequests # 429
rescue Jira::Error::ResponseError # any other 4xx/5xx
rescue Jira::Error::Base # all gem errorsJira::Error::ResponseError exposes:
e.response_status # HTTP status code
e.response_message # parsed message from response bodyRunning the example script
JIRA_ENDPOINT=https://your-domain.atlassian.net \
JIRA_EMAIL=you@example.com \
JIRA_API_TOKEN=your-api-token \
JIRA_PROJECT_KEY=TEST \
bundle exec ruby examples/basic_usage.rbSee examples/basic_usage.rb for all supported environment variables.