From 33252fa63205833af403c969fa9844d77ebbdeea Mon Sep 17 00:00:00 2001 From: Brian Burt Date: Fri, 24 Apr 2026 15:42:32 -0400 Subject: [PATCH 1/3] feat(docs-tools): migrate jira-reader from REST API v2 to v3 The jira Python library defaults to Atlassian REST API v2. Atlassian's current version is v3 and v2 may be deprecated. This migrates the jira-reader skill by passing options={"rest_api_version": "3"} to both JIRA() constructor paths and adding an adf_to_text() converter for the description and comment body fields, which v3 returns as Atlassian Document Format (ADF) JSON instead of wiki markup. Also fixes SKILL.md to use relative script paths per repo conventions. Closes #111 --- plugins/docs-tools/.claude-plugin/plugin.json | 2 +- .../docs-tools/skills/jira-reader/SKILL.md | 16 +-- .../skills/jira-reader/scripts/jira_reader.py | 108 ++++++++++++++++-- 3 files changed, 107 insertions(+), 19 deletions(-) diff --git a/plugins/docs-tools/.claude-plugin/plugin.json b/plugins/docs-tools/.claude-plugin/plugin.json index 513bfab5..fa02ea28 100644 --- a/plugins/docs-tools/.claude-plugin/plugin.json +++ b/plugins/docs-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "docs-tools", - "version": "0.0.58", + "version": "0.0.59", "description": "Documentation review, writing, and workflow tools for Red Hat AsciiDoc and Markdown documentation.", "author": { "name": "Red Hat Documentation Team", diff --git a/plugins/docs-tools/skills/jira-reader/SKILL.md b/plugins/docs-tools/skills/jira-reader/SKILL.md index 3885655d..b1d36097 100644 --- a/plugins/docs-tools/skills/jira-reader/SKILL.md +++ b/plugins/docs-tools/skills/jira-reader/SKILL.md @@ -7,7 +7,7 @@ allowed-tools: Read, Bash, Grep, Glob # JIRA Reader Skill -This skill provides read-only access to JIRA issues on Red Hat Issue Tracker (https://redhat.atlassian.net). +This skill provides read-only access to JIRA issues on Red Hat Issue Tracker (https://redhat.atlassian.net). It uses the Atlassian REST API v3, which returns rich text fields (description, comments) in Atlassian Document Format (ADF). The skill automatically converts ADF to plain text. ## Prerequisites @@ -47,37 +47,37 @@ The script loads `~/.env` automatically — do **not** prepend `source ~/.env` t **Fetch a single issue:** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 +python3 scripts/jira_reader.py --issue INFERENG-5233 ``` **Fetch issue with comments:** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 --include-comments +python3 scripts/jira_reader.py --issue INFERENG-5233 --include-comments ``` **Fetch multiple issues:** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 --issue INFERENG-5049 +python3 scripts/jira_reader.py --issue INFERENG-5233 --issue INFERENG-5049 ``` **Search issues by JQL (FAST - returns summaries):** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" +python3 scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" ``` **Search with full details (SLOW - fetches all fields):** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" --fetch-details +python3 scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" --fetch-details ``` **Traverse the ticket graph:** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --graph INFERENG-5233 +python3 scripts/jira_reader.py --graph INFERENG-5233 ``` **Graph with custom limits:** ```bash -python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --graph INFERENG-5233 --max-children 10 --max-siblings 10 --max-links 20 +python3 scripts/jira_reader.py --graph INFERENG-5233 --max-children 10 --max-siblings 10 --max-links 20 ``` ## Performance Modes diff --git a/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py b/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py index 09ddba9e..a1dcc1ae 100755 --- a/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py +++ b/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py @@ -6,11 +6,14 @@ It fetches issue details, comments, custom fields, related Git links, and traverses the ticket graph (parent, children, siblings, issue links, web links). +Uses Atlassian REST API v3. Description and comment fields are returned in +Atlassian Document Format (ADF) and automatically converted to plain text. + Usage: - python jira_reader.py --issue INFERENG-5233 - python jira_reader.py --issue INFERENG-5233 --include-comments - python jira_reader.py --jql "project=INFERENG AND fixVersion='3.4'" - python jira_reader.py --graph INFERENG-5233 + python3 scripts/jira_reader.py --issue INFERENG-5233 + python3 scripts/jira_reader.py --issue INFERENG-5233 --include-comments + python3 scripts/jira_reader.py --jql "project=INFERENG AND fixVersion='3.4'" + python3 scripts/jira_reader.py --graph INFERENG-5233 """ import argparse @@ -30,6 +33,88 @@ sys.exit(1) +def adf_to_text(node): + """ + Convert an Atlassian Document Format (ADF) node to plain text. + + API v3 returns description and comment body fields as ADF JSON (a nested + document structure) instead of wiki markup. This function recursively + extracts readable text, preserving paragraph breaks, list structure, and + code block formatting. + + Args: + node: An ADF node (dict with 'type' and optional 'content'), + a plain string (returned as-is), or None. + + Returns: + Plain text representation of the ADF content. + """ + if node is None: + return "" + if isinstance(node, str): + return node + + if not isinstance(node, dict): + return "" + + node_type = node.get("type", "") + content = node.get("content", []) + + if node_type == "text": + return node.get("text", "") + + if node_type == "hardBreak": + return "\n" + + if node_type == "mention": + return node.get("attrs", {}).get("text", "") + + if node_type == "emoji": + return node.get("attrs", {}).get("shortName", "") + + if node_type == "codeBlock": + code_text = "".join(adf_to_text(child) for child in content) + return f"\n```\n{code_text}\n```\n" + + if node_type in ("bulletList", "orderedList"): + lines = [] + for i, item in enumerate(content): + prefix = "- " if node_type == "bulletList" else f"{i + 1}. " + item_text = adf_to_text(item).strip() + lines.append(f"{prefix}{item_text}") + return "\n".join(lines) + "\n" + + if node_type == "listItem": + return "".join(adf_to_text(child) for child in content) + + if node_type in ("heading", "paragraph"): + text = "".join(adf_to_text(child) for child in content) + return text + "\n" + + if node_type == "blockquote": + inner = "".join(adf_to_text(child) for child in content).strip() + return "> " + inner.replace("\n", "\n> ") + "\n" + + if node_type == "rule": + return "\n---\n" + + if node_type == "table": + rows = [] + for row_node in content: + cells = [] + for cell_node in row_node.get("content", []): + cell_text = "".join( + adf_to_text(child) for child in cell_node.get("content", []) + ).strip() + cells.append(cell_text) + rows.append(" | ".join(cells)) + return "\n".join(rows) + "\n" + + # doc, panel, expand, mediaSingle, etc.: recurse into children + parts = [adf_to_text(child) for child in content] + return "".join(parts) + + def load_env_file(): """Load environment variables from ~/.env file.""" env_file = os.path.expanduser("~/.env") @@ -55,6 +140,8 @@ def __init__(self, server=None): server = server or os.environ.get("JIRA_URL", "https://redhat.atlassian.net") + options = {"rest_api_version": "3"} + if "atlassian.net" in server: email = os.environ.get("JIRA_EMAIL") if not email: @@ -62,9 +149,9 @@ def __init__(self, server=None): "JIRA_EMAIL environment variable not set. " "Required for Atlassian Cloud. Add it to ~/.env" ) - self.jira = JIRA(server=server, basic_auth=(email, token)) + self.jira = JIRA(server=server, basic_auth=(email, token), options=options) else: - self.jira = JIRA(server=server, token_auth=token) + self.jira = JIRA(server=server, token_auth=token, options=options) self.server = server self._epic_link_field = None @@ -113,8 +200,9 @@ def process_comments(self, comments): except (ValueError, TypeError): formatted_time = comment.created[:16].replace("T", " ") - # Clean comment body - comment_body = comment.body.strip() if comment.body else "" + # Clean comment body (v3 API returns ADF; v2 returns plain text) + raw_body = comment.body if comment.body else "" + comment_body = adf_to_text(raw_body).strip() if comment_body: processed_comments.append( @@ -239,7 +327,7 @@ def get_issue_data(self, jira_id, include_comments=False, git_link_types="all"): "status": str(issue.fields.status), "assignee": assignee, "summary": issue.fields.summary, - "description": issue.fields.description or "", + "description": adf_to_text(issue.fields.description), "created": issue.fields.created, "updated": issue.fields.updated, "comments": comments_data, @@ -371,7 +459,7 @@ def _fetch_issue_summary(self, jira_id): "assignee": fields.assignee.displayName if fields.assignee and hasattr(fields.assignee, "displayName") else None, - "description": fields.description or "", + "description": adf_to_text(fields.description), } except Exception as e: if "403" in str(e) or "Forbidden" in str(e): From 78499cf0a0c523a545d5228c6e48dcadc6166980 Mon Sep 17 00:00:00 2001 From: Brian Burt Date: Fri, 24 Apr 2026 15:50:36 -0400 Subject: [PATCH 2/3] reverts back to `${CLAUDE_SKILL_DIR}/` --- .../docs-tools/skills/jira-reader/SKILL.md | 14 ++++++------- .../skills/jira-reader/scripts/jira_reader.py | 21 ++++++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/plugins/docs-tools/skills/jira-reader/SKILL.md b/plugins/docs-tools/skills/jira-reader/SKILL.md index b1d36097..b479d007 100644 --- a/plugins/docs-tools/skills/jira-reader/SKILL.md +++ b/plugins/docs-tools/skills/jira-reader/SKILL.md @@ -47,37 +47,37 @@ The script loads `~/.env` automatically — do **not** prepend `source ~/.env` t **Fetch a single issue:** ```bash -python3 scripts/jira_reader.py --issue INFERENG-5233 +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 ``` **Fetch issue with comments:** ```bash -python3 scripts/jira_reader.py --issue INFERENG-5233 --include-comments +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 --include-comments ``` **Fetch multiple issues:** ```bash -python3 scripts/jira_reader.py --issue INFERENG-5233 --issue INFERENG-5049 +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 --issue INFERENG-5049 ``` **Search issues by JQL (FAST - returns summaries):** ```bash -python3 scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" ``` **Search with full details (SLOW - fetches all fields):** ```bash -python3 scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" --fetch-details +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --jql "project=INFERENG AND status='In Progress'" --fetch-details ``` **Traverse the ticket graph:** ```bash -python3 scripts/jira_reader.py --graph INFERENG-5233 +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --graph INFERENG-5233 ``` **Graph with custom limits:** ```bash -python3 scripts/jira_reader.py --graph INFERENG-5233 --max-children 10 --max-siblings 10 --max-links 20 +python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --graph INFERENG-5233 --max-children 10 --max-siblings 10 --max-links 20 ``` ## Performance Modes diff --git a/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py b/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py index a1dcc1ae..718e3bc4 100755 --- a/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py +++ b/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py @@ -10,10 +10,10 @@ Atlassian Document Format (ADF) and automatically converted to plain text. Usage: - python3 scripts/jira_reader.py --issue INFERENG-5233 - python3 scripts/jira_reader.py --issue INFERENG-5233 --include-comments - python3 scripts/jira_reader.py --jql "project=INFERENG AND fixVersion='3.4'" - python3 scripts/jira_reader.py --graph INFERENG-5233 + python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 + python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --issue INFERENG-5233 --include-comments + python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --jql "project=INFERENG AND fixVersion='3.4'" + python3 ${CLAUDE_SKILL_DIR}/scripts/jira_reader.py --graph INFERENG-5233 """ import argparse @@ -61,7 +61,13 @@ def adf_to_text(node): content = node.get("content", []) if node_type == "text": - return node.get("text", "") + text = node.get("text", "") + for mark in node.get("marks", []): + if mark.get("type") == "link": + href = mark.get("attrs", {}).get("href", "") + if href and href != text: + return f"{text} ({href})" + return text if node_type == "hardBreak": return "\n" @@ -140,9 +146,8 @@ def __init__(self, server=None): server = server or os.environ.get("JIRA_URL", "https://redhat.atlassian.net") - options = {"rest_api_version": "3"} - if "atlassian.net" in server: + options = {"rest_api_version": "3"} email = os.environ.get("JIRA_EMAIL") if not email: raise ValueError( @@ -151,7 +156,7 @@ def __init__(self, server=None): ) self.jira = JIRA(server=server, basic_auth=(email, token), options=options) else: - self.jira = JIRA(server=server, token_auth=token, options=options) + self.jira = JIRA(server=server, token_auth=token) self.server = server self._epic_link_field = None From 480ce4bf3b9a3de256dbdb82b0bc2d54ef14b778 Mon Sep 17 00:00:00 2001 From: Brian Burt Date: Mon, 27 Apr 2026 11:56:27 -0400 Subject: [PATCH 3/3] fix(docs-tools): handle inlineCard ADF nodes, update deps.json Add inlineCard handler to adf_to_text so smart-link URLs are preserved in plain-text output. Regenerate deps.json after rebase picked up create_mr.sh -> create_mr.py migration. --- plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py b/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py index 718e3bc4..53b66825 100755 --- a/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py +++ b/plugins/docs-tools/skills/jira-reader/scripts/jira_reader.py @@ -116,6 +116,9 @@ def adf_to_text(node): rows.append(" | ".join(cells)) return "\n".join(rows) + "\n" + if node_type == "inlineCard": + return node.get("attrs", {}).get("url", "") + # doc, panel, expand, mediaSingle, etc.: recurse into children parts = [adf_to_text(child) for child in content] return "".join(parts)