
Update Issue
- 1 installs
- 1 repo stars
- Updated April 10, 2026
- ericfisherdev/claude-plugins
Updates Jira issues via a Python script supporting status transitions, assignee, labels, priority, and comments with cached lookups.
About
Updates existing Jira issues through a bundled Python script that caches users, priorities, and components, replacing direct Atlassian MCP edit calls. A developer uses it to change status, assignee, labels, priority, or add comments on a ticket.
- Updates Jira issues via a Python script with cached users/priorities
- Supports status transitions, assignee, labels, priority, and comments
Update Issue by the numbers
- 1 all-time installs (skills.sh)
- Ranked #2,476 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 8, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ericfisherdev/claude-plugins --skill update-issueAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | April 10, 2026 |
| Repository | ericfisherdev/claude-plugins ↗ |
What it does
Updates Jira issues via a Python script supporting status transitions, assignee, labels, priority, and comments with cached lookups.
Files
Jira Issue Update
IMPORTANT: Always use this skill's Python script for updating Jira issues. Do NOT use mcp__atlassian__editJiraIssue or mcp__atlassian__transitionJiraIssue - this skill uses a shared cache for users, priorities, and components, and provides better error messages.
Quick Start
Use the Python script at scripts/update_jira_issue.py:
# Update status
python scripts/update_jira_issue.py PROJ-123 --status "In Progress"
# Update assignee
python scripts/update_jira_issue.py PROJ-123 --assignee "John Smith"
# Add a comment
python scripts/update_jira_issue.py PROJ-123 --comment "Working on this now"
# Multiple updates at once
python scripts/update_jira_issue.py PROJ-123 \
--status "In Progress" \
--assignee "Jane Doe" \
--priority High \
--add-labels "urgent"Update Options
| Option | Description |
|---|---|
--summary, -s | Update issue title |
--description, -d | Update issue description |
--status | Transition to new status |
--priority | Update priority (High, Medium, Low, etc.) |
--assignee, -a | Update assignee (partial name match) |
--unassign | Remove assignee |
--labels, -l | Set labels (replaces existing) |
--add-labels | Add labels to existing |
--remove-labels | Remove specific labels |
--components, -c | Set components (replaces existing) |
--comment | Add a comment |
--format, -f | Output: compact (default), text, json |
Status Transitions
Jira issues follow workflows. To change status, use --status:
# Transition to "In Progress"
python scripts/update_jira_issue.py PROJ-123 --status "In Progress"
# List available transitions
python scripts/update_jira_issue.py PROJ-123 --list-transitionsThe script matches both transition names (e.g., "Start Progress") and target status names (e.g., "In Progress").
Common Workflows
Start Working on an Issue
python scripts/update_jira_issue.py PROJ-123 \
--status "In Progress" \
--assignee "me" \
--comment "Starting work on this"Complete an Issue
python scripts/update_jira_issue.py PROJ-123 \
--status "Done" \
--comment "Completed and deployed"Reassign and Reprioritize
python scripts/update_jira_issue.py PROJ-123 \
--assignee "Jane Doe" \
--priority Critical \
--add-labels "escalated"Add Labels Without Replacing
# Add new labels while keeping existing ones
python scripts/update_jira_issue.py PROJ-123 --add-labels "reviewed,approved"
# Remove specific labels
python scripts/update_jira_issue.py PROJ-123 --remove-labels "needs-review"Update Description
python scripts/update_jira_issue.py PROJ-123 \
--description "Updated requirements: must support dark mode"Output Formats
compact (default):
UPDATED|PROJ-123|Fix login bug|In Progress|Bug|P:High|@jsmith
Changes:status->In Progress,comment
URL:https://yoursite.atlassian.net/browse/PROJ-123text:
Issue Updated: PROJ-123
Summary: Fix login bug
Status: In Progress
Type: Bug
Priority: High
Assignee: John Smith
Changes: status->In Progress, comment
URL: https://yoursite.atlassian.net/browse/PROJ-123json:
{"key":"PROJ-123","summary":"Fix login bug","status":"In Progress","changes":["status->In Progress","comment"],"url":"..."}Shared Cache
This skill shares a cache (~/.jira-tools-cache.json) with other jira-tools skills:
- Users (for assignee lookups)
- Priorities
- Components
Manage cache via:
python shared/jira_cache.py info # View cache status
python shared/jira_cache.py clear # Clear cacheEnvironment Setup
Requires three environment variables:
JIRA_BASE_URL- e.g.,https://yoursite.atlassian.netJIRA_EMAIL- Your Jira account emailJIRA_API_TOKEN- API token from Atlassian account settings
Why Not Use Atlassian MCP Directly?
mcp__atlassian__editJiraIssue and mcp__atlassian__transitionJiraIssue:
- Require looking up user account IDs separately
- Require looking up transition IDs separately
- No caching - repeated API calls for metadata
- Verbose output wastes tokens
This skill's script:
- Caches user/priority/component metadata
- Accepts human-readable names (not IDs)
- Automatically finds matching transitions
- Returns token-efficient output
Always prefer this skill over direct MCP calls for Jira updates.
Reference
For detailed field options and error codes, see references/options-reference.md.
Update Issue Options Reference
Field Updates
Summary
--summary "New issue title"Updates the issue title/summary.
Description
--description "Full description text here"Replaces the entire description. Plain text is converted to Atlassian Document Format.
Priority
--priority HighCommon priorities (varies by Jira configuration):
- Highest
- High
- Medium
- Low
- Lowest
Assignee
--assignee "John Smith" # Partial match on display name
--assignee "jsmith" # Also matches if in display name
--unassign # Remove assigneeUses partial, case-insensitive matching on display names.
Labels
Replace All Labels
--labels "bug,critical,needs-review" # Set these labels
--labels "" # Remove all labelsAdd Labels (Keep Existing)
--add-labels "reviewed,approved"Adds to existing labels without removing any.
Remove Specific Labels
--remove-labels "needs-review,draft"Removes only the specified labels, keeps others.
Components
--components "Frontend,API" # Set these components
--components "" # Remove all componentsComponent names must match exactly (case-insensitive).
Status Transitions
Transition to Status
--status "In Progress"
--status "Done"
--status "Closed"The script matches against: 1. Transition name (e.g., "Start Progress") 2. Target status name (e.g., "In Progress")
List Available Transitions
python update_jira_issue.py PROJ-123 --list-transitionsOutput:
Available transitions for PROJ-123:
- Start Progress -> In Progress
- Close Issue -> Closed
- Resolve -> DoneCommon Workflow Transitions
| From Status | Common Transitions |
|---|---|
| Open/Backlog | Start Progress, In Progress |
| In Progress | Done, Closed, Review, Blocked |
| Review | Done, In Progress, Rejected |
| Done | Reopen, Closed |
Note: Available transitions depend on your Jira workflow configuration.
Comments
--comment "This is my comment text"Adds a new comment to the issue. Plain text only.
Multiple Updates
Combine multiple options in one command:
python update_jira_issue.py PROJ-123 \
--summary "Updated title" \
--status "In Progress" \
--assignee "Jane Doe" \
--priority High \
--add-labels "urgent,sprint-42" \
--comment "Taking ownership of this issue"Order of operations: 1. Field updates (summary, description, priority, assignee, labels, components) 2. Status transition 3. Comment added
Error Handling
Common Errors
| Error | Cause | Solution |
|---|---|---|
| User not found | Assignee name doesn't match | Check spelling or use --list-users in create-issue |
| Cannot transition | Status not reachable | Use --list-transitions to see available options |
| Component not found | Component doesn't exist | Check project components in Jira |
| Priority not found | Invalid priority name | Use standard names: Highest, High, Medium, Low, Lowest |
Permission Errors
- 403: User lacks permission to edit the issue
- 404: Issue doesn't exist or user can't view it
API Endpoints Used
PUT /rest/api/3/issue/{issueKey}- Update fieldsPOST /rest/api/3/issue/{issueKey}/transitions- Change statusPOST /rest/api/3/issue/{issueKey}/comment- Add commentGET /rest/api/3/issue/{issueKey}/transitions- List available transitions
Output Changes Tracking
The changes field in output shows what was modified:
summary- Summary was updateddescription- Description was updatedpriority- Priority was changedassignee- Assignee was changedunassigned- Assignee was removedlabels- Labels were replacedlabels+- Labels were addedlabels-- Labels were removedcomponents- Components were changedstatus->StatusName- Transitioned to new statuscomment- Comment was added
#!/usr/bin/env python3
"""
Update Jira issues with token-efficient output.
Uses shared cache for project metadata, users, priorities, etc.
Supports field updates, status transitions, and adding comments.
Environment Variables:
JIRA_BASE_URL: Jira instance URL (e.g., https://yoursite.atlassian.net)
JIRA_EMAIL: User email for authentication
JIRA_API_TOKEN: API token for authentication
Usage:
python update_jira_issue.py PROJ-123 [options]
Options:
--summary TEXT Update issue summary/title
--description TEXT Update issue description
--status STATUS Transition to new status
--priority NAME Update priority
--assignee NAME Update assignee (partial match)
--labels L1,L2 Set labels (replaces existing)
--add-labels L1,L2 Add labels to existing
--remove-labels L1,L2 Remove labels from existing
--components C1,C2 Set components (replaces existing)
--comment TEXT Add a comment to the issue
--format FORMAT Output: compact (default), text, json
--list-transitions List available status transitions
"""
import argparse
import json
import os
import sys
from pathlib import Path
from typing import Optional
from urllib.parse import urljoin
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
import base64
# Add shared module to path
SCRIPT_DIR = Path(__file__).parent
SHARED_DIR = SCRIPT_DIR.parent.parent.parent / "shared"
sys.path.insert(0, str(SHARED_DIR))
from jira_cache import JiraCache
from markdown_to_adf import markdown_to_adf
def get_auth_header() -> str:
"""Generate Basic Auth header."""
email = os.environ.get("JIRA_EMAIL")
token = os.environ.get("JIRA_API_TOKEN")
if not email or not token:
raise EnvironmentError(
"JIRA_EMAIL and JIRA_API_TOKEN environment variables required"
)
credentials = f"{email}:{token}"
encoded = base64.b64encode(credentials.encode()).decode()
return f"Basic {encoded}"
def api_request(path: str, method: str = "GET", data: Optional[dict] = None) -> dict:
"""Make authenticated API request to Jira."""
base_url = os.environ.get("JIRA_BASE_URL")
if not base_url:
raise EnvironmentError("JIRA_BASE_URL environment variable required")
url = urljoin(base_url, path)
req = Request(url, method=method)
req.add_header("Authorization", get_auth_header())
req.add_header("Accept", "application/json")
if data is not None:
req.add_header("Content-Type", "application/json")
req.data = json.dumps(data).encode()
try:
with urlopen(req, timeout=30) as response:
content = response.read().decode()
return json.loads(content) if content else {}
except HTTPError as e:
error_body = e.read().decode() if e.fp else ""
try:
error_json = json.loads(error_body)
errors = error_json.get("errors", {})
error_messages = error_json.get("errorMessages", [])
details = "; ".join(error_messages + [f"{k}: {v}" for k, v in errors.items()])
raise RuntimeError(f"Jira API error: {details}")
except json.JSONDecodeError:
raise RuntimeError(f"Jira API error {e.code}: {error_body}")
except URLError as e:
raise ConnectionError(f"Failed to connect to Jira: {e.reason}")
def get_issue(issue_key: str) -> dict:
"""Fetch issue details."""
return api_request(
f"/rest/api/3/issue/{issue_key}?fields=summary,status,issuetype,priority,assignee,labels,components,project"
)
def get_transitions(issue_key: str) -> list[dict]:
"""Get available transitions for an issue."""
result = api_request(f"/rest/api/3/issue/{issue_key}/transitions")
transitions = []
for t in result.get("transitions", []):
transitions.append({
"id": t["id"],
"name": t["name"],
"to_status": t.get("to", {}).get("name", ""),
})
return transitions
def update_issue_fields(issue_key: str, fields: dict) -> None:
"""Update issue fields."""
if not fields:
return
api_request(f"/rest/api/3/issue/{issue_key}", method="PUT", data={"fields": fields})
def transition_issue(issue_key: str, transition_id: str) -> None:
"""Transition issue to new status."""
api_request(
f"/rest/api/3/issue/{issue_key}/transitions",
method="POST",
data={"transition": {"id": transition_id}}
)
def add_comment(issue_key: str, comment_text: str) -> dict:
"""Add a comment to an issue."""
body = {"body": markdown_to_adf(comment_text)}
return api_request(f"/rest/api/3/issue/{issue_key}/comment", method="POST", data=body)
def format_output(issue_key: str, issue_data: dict, changes: list[str], output_format: str) -> str:
"""Format the updated issue output."""
fields = issue_data.get("fields", {})
summary = fields.get("summary", "")
status = fields.get("status", {}).get("name", "")
issue_type = fields.get("issuetype", {}).get("name", "")
priority = fields.get("priority", {}).get("name", "") if fields.get("priority") else ""
assignee = fields.get("assignee", {}).get("displayName", "Unassigned") if fields.get("assignee") else "Unassigned"
labels = fields.get("labels", [])
base_url = os.environ.get("JIRA_BASE_URL", "")
browse_url = f"{base_url}/browse/{issue_key}" if base_url else ""
if output_format == "compact":
parts = [f"UPDATED|{issue_key}|{summary}|{status}"]
if issue_type:
parts[0] += f"|{issue_type}"
if priority:
parts[0] += f"|P:{priority}"
if assignee != "Unassigned":
parts[0] += f"|@{assignee}"
if changes:
parts.append(f"Changes:{','.join(changes)}")
if browse_url:
parts.append(f"URL:{browse_url}")
return "\n".join(parts)
elif output_format == "json":
data = {
"key": issue_key,
"summary": summary,
"status": status,
"changes": changes,
"url": browse_url
}
if issue_type:
data["type"] = issue_type
if priority:
data["priority"] = priority
if assignee != "Unassigned":
data["assignee"] = assignee
if labels:
data["labels"] = labels
return json.dumps(data, separators=(',', ':'))
else: # text
lines = [
f"Issue Updated: {issue_key}",
f"Summary: {summary}",
f"Status: {status}",
]
if issue_type:
lines.append(f"Type: {issue_type}")
if priority:
lines.append(f"Priority: {priority}")
lines.append(f"Assignee: {assignee}")
if labels:
lines.append(f"Labels: {', '.join(labels)}")
if changes:
lines.append(f"Changes: {', '.join(changes)}")
if browse_url:
lines.append(f"URL: {browse_url}")
return "\n".join(lines)
def list_transitions_output(issue_key: str, transitions: list[dict]) -> None:
"""List available transitions for an issue."""
if not transitions:
print(f"No transitions available for {issue_key}", file=sys.stderr)
sys.exit(1)
print(f"Available transitions for {issue_key}:")
for t in transitions:
print(f" - {t['name']} -> {t['to_status']}")
def main():
parser = argparse.ArgumentParser(
description="Update Jira issues with token-efficient output"
)
parser.add_argument(
"issue_key",
help="Jira issue key (e.g., PROJ-123)"
)
parser.add_argument(
"--summary", "-s",
help="Update issue summary/title"
)
parser.add_argument(
"--description", "-d",
help="Update issue description"
)
parser.add_argument(
"--status",
help="Transition to new status"
)
parser.add_argument(
"--priority",
help="Update priority (e.g., High, Medium, Low)"
)
parser.add_argument(
"--assignee", "-a",
help="Update assignee (display name, partial match)"
)
parser.add_argument(
"--unassign",
action="store_true",
help="Remove assignee from issue"
)
parser.add_argument(
"--labels", "-l",
help="Set labels (comma-separated, replaces existing)"
)
parser.add_argument(
"--add-labels",
help="Add labels (comma-separated)"
)
parser.add_argument(
"--remove-labels",
help="Remove labels (comma-separated)"
)
parser.add_argument(
"--components", "-c",
help="Set components (comma-separated, replaces existing)"
)
parser.add_argument(
"--comment",
help="Add a comment to the issue"
)
parser.add_argument(
"--format", "-f",
choices=["compact", "text", "json"],
default="compact",
help="Output format (default: compact)"
)
parser.add_argument(
"--list-transitions",
action="store_true",
help="List available status transitions"
)
args = parser.parse_args()
try:
cache = JiraCache()
# Handle list transitions command
if args.list_transitions:
transitions = get_transitions(args.issue_key)
list_transitions_output(args.issue_key, transitions)
return
# Get current issue to determine project
current_issue = get_issue(args.issue_key)
project_key = current_issue.get("fields", {}).get("project", {}).get("key", "")
current_labels = current_issue.get("fields", {}).get("labels", [])
fields_to_update = {}
changes = []
# Summary
if args.summary:
fields_to_update["summary"] = args.summary
changes.append("summary")
# Description
if args.description:
fields_to_update["description"] = markdown_to_adf(args.description)
changes.append("description")
# Priority
if args.priority:
priority = cache.get_priority_by_name(args.priority)
if not priority:
print(f"Error: Priority '{args.priority}' not found", file=sys.stderr)
sys.exit(1)
fields_to_update["priority"] = {"id": priority["id"]}
changes.append("priority")
# Assignee
if args.unassign:
fields_to_update["assignee"] = None
changes.append("unassigned")
elif args.assignee:
if not project_key:
print("Error: Could not determine project key", file=sys.stderr)
sys.exit(1)
user = cache.get_user_by_name(project_key, args.assignee)
if not user:
print(f"Error: User '{args.assignee}' not found in project {project_key}", file=sys.stderr)
sys.exit(1)
fields_to_update["assignee"] = {"accountId": user["accountId"]}
changes.append("assignee")
# Labels - set (replace)
if args.labels is not None:
if args.labels == "":
fields_to_update["labels"] = []
else:
fields_to_update["labels"] = [l.strip() for l in args.labels.split(",")]
changes.append("labels")
# Labels - add
if args.add_labels:
new_labels = [l.strip() for l in args.add_labels.split(",")]
combined = list(set(current_labels + new_labels))
fields_to_update["labels"] = combined
changes.append("labels+")
# Labels - remove
if args.remove_labels:
remove_set = {l.strip().lower() for l in args.remove_labels.split(",")}
remaining = [l for l in current_labels if l.lower() not in remove_set]
fields_to_update["labels"] = remaining
changes.append("labels-")
# Components
if args.components is not None:
if not project_key:
print("Error: Could not determine project key", file=sys.stderr)
sys.exit(1)
if args.components == "":
fields_to_update["components"] = []
else:
component_names = [c.strip() for c in args.components.split(",")]
components = cache.get_components(project_key)
component_ids = []
for name in component_names:
name_lower = name.lower()
found = None
for c in components:
if c["name"].lower() == name_lower:
found = c
break
if not found:
print(f"Error: Component '{name}' not found", file=sys.stderr)
sys.exit(1)
component_ids.append({"id": found["id"]})
fields_to_update["components"] = component_ids
changes.append("components")
# Apply field updates
if fields_to_update:
update_issue_fields(args.issue_key, fields_to_update)
# Status transition
if args.status:
transitions = get_transitions(args.issue_key)
target_status = args.status.lower()
transition = None
for t in transitions:
if t["name"].lower() == target_status or t["to_status"].lower() == target_status:
transition = t
break
if not transition:
print(f"Error: Cannot transition to '{args.status}'", file=sys.stderr)
print("Available transitions:", file=sys.stderr)
for t in transitions:
print(f" - {t['name']} -> {t['to_status']}", file=sys.stderr)
sys.exit(1)
transition_issue(args.issue_key, transition["id"])
changes.append(f"status->{transition['to_status']}")
# Add comment
if args.comment:
add_comment(args.issue_key, args.comment)
changes.append("comment")
# Check if any changes were made
if not changes:
print("Error: No updates specified. Use --help for options.", file=sys.stderr)
sys.exit(1)
# Fetch updated issue and output
updated_issue = get_issue(args.issue_key)
output = format_output(args.issue_key, updated_issue, changes, args.format)
print(output)
# Update cache with new issue data
updated_fields = updated_issue.get("fields", {})
cache_data = {
"id": updated_issue.get("id", ""),
"key": args.issue_key,
"summary": updated_fields.get("summary", ""),
"status": updated_fields.get("status", {}).get("name", ""),
"labels": updated_fields.get("labels", []),
}
# Try to update existing cached issue, or add new if not present
if cache.get_cached_issue(args.issue_key):
cache.update_cached_issue_fields(args.issue_key, cache_data)
else:
cache.set_cached_issue(args.issue_key, cache_data)
except (EnvironmentError, ValueError, PermissionError, ConnectionError, RuntimeError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()