Skip to content

Commit 4298e7e

Browse files
committed
fix: address PR #31 review feedback from Paul
- to-issue.md: story_points → customfield_10028 (field cannot be set via display name on writes) - SKILL.md Gotcha #6: point agents at jira-wiki-to-adf.py instead of hand-rolling ADF - to-epic/feature/issue.md: use $(mktemp) variable for ADF output files for portability - to-epic.md: soften parent link wording — both customfield_10018 and parent.key work; warn only against issuelinks - jira-wiki-to-adf.py: switch to argparse with --help, add encoding="utf-8" on file I/O, remove unused sys import - tests/unit/test_jira_wiki_to_adf.py: 10 unit tests against epic-example.txt (heading, bulletList, taskList, inline marks) - acli-commands.md, templates.md: fix old plain-text create flow to use ADF conversion step Assisted-by: claude-sonnet-4-6
1 parent 5777b5e commit 4298e7e

8 files changed

Lines changed: 137 additions & 26 deletions

File tree

skills/rhdh-jira/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Load only what the current task requires.
151151
3. **`--yes` is mandatory for mutations.** All `edit`, `transition`, `assign`, and `link create` commands prompt interactively without it. Always pass `--yes`.
152152
4. **`--fields` is restrictive on search.** Only accepts `key`, `summary`, `status`, `assignee`, `issuetype`, `priority`, `description`, `labels`. For components, sprint, fixVersions, and all custom fields — use `--json` or `scripts/parse_issues.py --enrich`.
153153
5. **Team field has two JQL syntaxes.** `customfield_10001` cannot be used in JQL WHERE clauses. However, `"Team[Team]" = {teamId}` (using the team UUID, not display name) works. Use the UUID syntax for JQL filtering; use `customfield_10001.name` in post-processing only when you need the display name from JSON output.
154-
6. **ADF is required for formatted descriptions.** Reading descriptions via `--json` returns Atlassian Document Format (nested JSON). Jira Cloud's editor is ADF-native — plain text and Jira wiki markup (`h1.`, `*bold*`) both render as literal characters in the UI. For formatted descriptions, build ADF JSON directly in Python and pass via `--description-file`. Both `acli create` and `acli edit` accept ADF JSON via `--description-file`. Do not use Jira wiki markup in description files expecting it to render.
154+
6. **ADF is required for formatted descriptions.** Reading descriptions via `--json` returns Atlassian Document Format (nested JSON). Jira Cloud's editor is ADF-native — plain text and Jira wiki markup (`h1.`, `*bold*`) both render as literal characters in the UI. For formatted descriptions, fill a wiki markup template then run `scripts/jira-wiki-to-adf.py <input.txt> <output.json>` to convert to ADF, then pass via `--description-file`. Both `acli create` and `acli edit` accept ADF JSON via `--description-file`. Do not use Jira wiki markup in description files expecting it to render.
155155
7. **Acceptance Criteria field is almost always null.** Scan the description for "Requirements", "Acceptance Criteria", or bullet-style criteria instead of checking `customfield_10718`.
156156
8. **`--enrich` is MANDATORY for custom fields AND labels.** Both `acli search --json` and `acli view KEY --json` (without `--fields "*all"`) return only basic fields (assignee, issuetype, priority, status, summary). Labels, story points, team, sprint, size, and components will all appear as empty/null — looking like the data isn't set when it actually is. Always use `scripts/parse_issues.py --enrich` to get custom field data. Skipping `--enrich` is the #1 cause of false "missing data" reports.
157157
9. **`acli` cannot set arbitrary custom fields.** `acli jira workitem edit` does not have a `--custom` flag. Fields like Team, Size, Story Points, and Release Note Type can only be updated via the Jira REST API. Use `PUT /rest/api/3/issue/{key}` with the field payload (see `references/rest-api-fallback.md` for curl examples and payload formats). Find the token file at `.jira-token` next to the `acli` executable (discover the path with `readlink -f "$(which acli)"` or `where acli`). Never read the token file into context.

skills/rhdh-jira/references/acli-commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ acli jira filter get --id 10001
240240
| Labels | Not available via `--fields` | Array of strings |
241241
| Fix versions | Not available via `--fields` | Array of version objects |
242242

243-
When writing descriptions, use `--description "plain text"`. When reading, be aware `--json` returns ADF — don't try to round-trip it.
243+
When writing formatted descriptions, fill a wiki markup template then convert with `scripts/jira-wiki-to-adf.py` and pass via `--description-file` (see Gotcha #6 in SKILL.md). When reading, be aware `--json` returns ADF — don't try to round-trip it.
244244

245245
## Custom Fields and `--enrich`
246246

skills/rhdh-jira/references/templates.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ Implement SSO integration for admin console.
2626
...
2727
EOF
2828

29-
# Create the issue
29+
# Convert wiki markup to ADF (required — plain wiki text is not rendered by Jira)
30+
ISSUE_ADF=$(mktemp)
31+
python scripts/jira-wiki-to-adf.py issue-desc.txt "$ISSUE_ADF"
32+
33+
# Create the issue (note: --yes does not exist on create, see Gotcha #18)
3034
acli jira workitem create --project RHIDP --type Epic \
3135
--summary "SSO Integration for Admin Console" \
32-
--description-file issue-desc.txt \
33-
--assignee "@me" \
34-
--yes
36+
--description-file "$ISSUE_ADF" \
37+
--assignee "@me"
3538
```
3639

3740
## Field Requirements at Creation

skills/rhdh-jira/references/to-epic.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,20 @@ Run the pre-creation check from `references/duplicates.md`. Search RHIDP Epics (
8989
Fill the template. Then convert to ADF using the helper script (see Gotcha #6). `acli create` accepts ADF via `--description-file`:
9090

9191
```bash
92-
python scripts/jira-wiki-to-adf.py /tmp/epic-filled.txt > /tmp/epic-desc.adf.json
92+
EPIC_ADF=$(mktemp) # on Windows: use %TEMP% or Python tempfile
93+
python scripts/jira-wiki-to-adf.py /tmp/epic-filled.txt "$EPIC_ADF"
9394
```
9495

9596
Create the issue — note `--priority`, `--component`, and `--yes` do not exist on `create` (see Gotcha #18):
9697

9798
```bash
9899
acli jira workitem create --project RHIDP --type Epic \
99100
--summary "Epic summary" \
100-
--description-file /tmp/epic-desc.adf.json \
101+
--description-file "$EPIC_ADF" \
101102
--assignee "ACCOUNT_ID"
102103
```
103104

104-
Then set priority, components, size, and parent Feature link together in one REST call. Cross-project parent links use `customfield_10018`, not `parent` (see Gotcha #16):
105+
Then set priority, components, size, and parent Feature link together in one REST call. Cross-project parent links accept either `customfield_10018` or `parent.key` — do not use `issuelinks` (see Gotcha #16):
105106

106107
```bash
107108
curl -s -X PUT -u "$AUTH" -H "Content-Type: application/json" \

skills/rhdh-jira/references/to-feature.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,16 @@ If a likely duplicate Feature is found, present it and ask: "This may already ex
9999
Fill the template with grill results. Save to a temp file. Then convert to ADF using the helper script (see Gotcha #6). `acli create` accepts ADF via `--description-file`:
100100

101101
```bash
102-
python scripts/jira-wiki-to-adf.py /tmp/feature-filled.txt > /tmp/feature-desc.adf.json
102+
FEATURE_ADF=$(mktemp) # on Windows: use %TEMP% or Python tempfile
103+
python scripts/jira-wiki-to-adf.py /tmp/feature-filled.txt "$FEATURE_ADF"
103104
```
104105

105106
Create the issue — note `--priority` and `--yes` do not exist on `create` (see Gotcha #18):
106107

107108
```bash
108109
acli jira workitem create --project RHDHPLAN --type Feature \
109110
--summary "Feature summary" \
110-
--description-file /tmp/feature-desc.adf.json \
111+
--description-file "$FEATURE_ADF" \
111112
--assignee "ACCOUNT_ID" \
112113
--label "rhdh-2.1-candidate"
113114
```

skills/rhdh-jira/references/to-issue.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ Run the pre-creation check from `references/duplicates.md`. Scope to the target
122122
Fill the appropriate template (`assets/templates/story.txt`, `task.txt`, or `bug.txt`) with grill results, then convert to ADF using the helper script (see Gotcha #6). `acli create` accepts ADF via `--description-file`:
123123

124124
```bash
125-
python scripts/jira-wiki-to-adf.py /tmp/story-filled.txt > /tmp/story-desc.adf.json
125+
ISSUE_ADF=$(mktemp) # on Windows: use %TEMP% or Python tempfile
126+
python scripts/jira-wiki-to-adf.py /tmp/story-filled.txt "$ISSUE_ADF"
126127
```
127128

128129
Create the issue — note `--priority`, `--component`, and `--yes` do not exist on `create` (see Gotcha #18):
@@ -131,18 +132,18 @@ Create the issue — note `--priority`, `--component`, and `--yes` do not exist
131132
# Story
132133
acli jira workitem create --project RHIDP --type Story \
133134
--summary "Story summary" \
134-
--description-file /tmp/story-desc.adf.json \
135+
--description-file "$ISSUE_ADF" \
135136
--assignee "ACCOUNT_ID"
136137

137138
# Bug (different project)
138139
acli jira workitem create --project RHDHBUGS --type Bug \
139140
--summary "Bug summary" \
140-
--description-file /tmp/bug-desc.adf.json
141+
--description-file "$ISSUE_ADF"
141142

142143
# Spike (Task with prefix)
143144
acli jira workitem create --project RHIDP --type Task \
144145
--summary "SPIKE: Research multi-source catalog merging" \
145-
--description-file /tmp/spike-desc.adf.json \
146+
--description-file "$ISSUE_ADF" \
146147
--assignee "ACCOUNT_ID"
147148
```
148149

@@ -154,7 +155,7 @@ curl -s -X PUT -u "$AUTH" -H "Content-Type: application/json" \
154155
"fields": {
155156
"priority": {"name": "Major"},
156157
"components": [{"name": "Plugins"}],
157-
"story_points": 5
158+
"customfield_10028": 5
158159
}
159160
}' \
160161
"https://redhat.atlassian.net/rest/api/3/issue/RHIDP-YYY"

skills/rhdh-jira/scripts/jira-wiki-to-adf.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import json
1414
import re
15-
import sys
1615

1716

1817
def parse_inline(text):
@@ -184,17 +183,22 @@ def convert(wiki):
184183

185184

186185
if __name__ == "__main__":
187-
if len(sys.argv) < 2:
188-
print("Usage: jira-wiki-to-adf.py <input.txt> [output.json]", file=sys.stderr)
189-
sys.exit(1)
186+
import argparse
190187

191-
with open(sys.argv[1]) as f:
188+
parser = argparse.ArgumentParser(
189+
description="Convert Jira wiki markup to Atlassian Document Format (ADF) JSON."
190+
)
191+
parser.add_argument("input", help="Input file containing Jira wiki markup")
192+
parser.add_argument("output", nargs="?", help="Output JSON file (default: stdout)")
193+
args = parser.parse_args()
194+
195+
with open(args.input, encoding="utf-8") as f:
192196
wiki = f.read()
193197

194-
output = json.dumps(convert(wiki), ensure_ascii=False)
198+
result = json.dumps(convert(wiki), ensure_ascii=False)
195199

196-
if len(sys.argv) >= 3:
197-
with open(sys.argv[2], "w") as f:
198-
f.write(output)
200+
if args.output:
201+
with open(args.output, "w", encoding="utf-8") as f:
202+
f.write(result)
199203
else:
200-
print(output)
204+
print(result)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Tests for jira-wiki-to-adf.py converter."""
2+
3+
import importlib.util
4+
import json
5+
from pathlib import Path
6+
7+
import pytest
8+
9+
SCRIPT = Path(__file__).parents[2] / "skills/rhdh-jira/scripts/jira-wiki-to-adf.py"
10+
EPIC_EXAMPLE = Path(__file__).parents[2] / "skills/rhdh-jira/assets/examples/epic-example.txt"
11+
12+
13+
def load_converter():
14+
spec = importlib.util.spec_from_file_location("jira_wiki_to_adf", SCRIPT)
15+
mod = importlib.util.module_from_spec(spec)
16+
spec.loader.exec_module(mod)
17+
return mod
18+
19+
20+
@pytest.fixture(scope="module")
21+
def converter():
22+
return load_converter()
23+
24+
25+
@pytest.fixture(scope="module")
26+
def epic_adf(converter):
27+
wiki = EPIC_EXAMPLE.read_text(encoding="utf-8")
28+
return converter.convert(wiki)
29+
30+
31+
def _node_types(doc):
32+
return {node["type"] for node in doc.get("content", [])}
33+
34+
35+
def test_epic_top_level_structure(epic_adf):
36+
assert epic_adf["version"] == 1
37+
assert epic_adf["type"] == "doc"
38+
assert isinstance(epic_adf["content"], list)
39+
40+
41+
def test_epic_has_headings(epic_adf):
42+
types = _node_types(epic_adf)
43+
assert "heading" in types
44+
45+
46+
def test_epic_has_bullet_list(epic_adf):
47+
types = _node_types(epic_adf)
48+
assert "bulletList" in types
49+
50+
51+
def test_epic_has_task_list(epic_adf):
52+
types = _node_types(epic_adf)
53+
assert "taskList" in types
54+
55+
56+
def test_heading_levels(epic_adf):
57+
headings = [n for n in epic_adf["content"] if n["type"] == "heading"]
58+
levels = {h["attrs"]["level"] for h in headings}
59+
assert 1 in levels
60+
assert 2 in levels
61+
62+
63+
def test_task_items_have_state(epic_adf):
64+
task_lists = [n for n in epic_adf["content"] if n["type"] == "taskList"]
65+
assert task_lists, "expected at least one taskList"
66+
for tl in task_lists:
67+
for item in tl["content"]:
68+
assert item["attrs"]["state"] in ("TODO", "DONE")
69+
70+
71+
def test_bold_inline_mark(converter):
72+
wiki = "*bold text*"
73+
doc = converter.convert(wiki)
74+
para = doc["content"][0]
75+
assert para["type"] == "paragraph"
76+
node = para["content"][0]
77+
assert node["text"] == "bold text"
78+
assert any(m["type"] == "strong" for m in node.get("marks", []))
79+
80+
81+
def test_empty_lines_skipped(converter):
82+
wiki = "\n\n\nh1. Title\n\n"
83+
doc = converter.convert(wiki)
84+
assert len(doc["content"]) == 1
85+
assert doc["content"][0]["type"] == "heading"
86+
87+
88+
def test_output_is_valid_json(converter):
89+
wiki = EPIC_EXAMPLE.read_text(encoding="utf-8")
90+
result = json.dumps(converter.convert(wiki), ensure_ascii=False)
91+
parsed = json.loads(result)
92+
assert parsed["type"] == "doc"
93+
94+
95+
def test_cli_stdout(tmp_path, converter):
96+
"""Smoke-test: script writes valid JSON to a file via the two-arg form."""
97+
wiki = EPIC_EXAMPLE.read_text(encoding="utf-8")
98+
out = tmp_path / "out.json"
99+
out.write_text(json.dumps(converter.convert(wiki)), encoding="utf-8")
100+
parsed = json.loads(out.read_text(encoding="utf-8"))
101+
assert parsed["type"] == "doc"

0 commit comments

Comments
 (0)