Project

ruby-jira

0.0
No release in over 3 years
A Ruby wrapper for Jira Cloud API
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.24
 Project Readme

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"
end

Or 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"
end

OAuth 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"
end

OAuth 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"
end

Usage

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 Hash

Projects

# 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
end

Logging

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)
end

Sample 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 errors

Jira::Error::ResponseError exposes:

e.response_status   # HTTP status code
e.response_message  # parsed message from response body

Running 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.rb

See examples/basic_usage.rb for all supported environment variables.