datagrok-tools 6.1.12 → 6.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## 6.1.14 (2026-05-01)
4
+
5
+ * Reports: `grok report comment` now converts Markdown body to JIRA wiki markup before POSTing, fixing rendered headings/list/HTML-entity mismatches in JIRA UI.
6
+
7
+ ## 6.1.13 (2026-04-30)
8
+
9
+ * Reports: `grok report ticket` now uses direct JIRA REST honoring `$JIRA_PROJECT`, replacing the Datlas-mediated path that hardcoded GROK.
10
+
3
11
  ## 6.1.11 (2026-04-27)
4
12
 
5
13
  * `grok report read <path | instance number>` — normalize a report zip/json into one JSON object on stdout (envelope unwrap, `_meta.json` merge, optional `--extract-screenshot` / `--extract-d42` / `--extract-actions`)
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+
3
+ var _vitest = require("vitest");
4
+ var _report = require("../commands/report");
5
+ (0, _vitest.describe)('markdownToJiraWiki — basic rules', () => {
6
+ (0, _vitest.it)('converts H1', () => {
7
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('# Title')).toBe('h1. Title');
8
+ });
9
+ (0, _vitest.it)('converts H2 / H3 / H6', () => {
10
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('## Sub')).toBe('h2. Sub');
11
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('### Sub-sub')).toBe('h3. Sub-sub');
12
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('###### Deep')).toBe('h6. Deep');
13
+ });
14
+ (0, _vitest.it)('converts bold', () => {
15
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('hello **world** foo')).toBe('hello *world* foo');
16
+ });
17
+ (0, _vitest.it)('converts italic with single asterisks', () => {
18
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('hello *world* foo')).toBe('hello _world_ foo');
19
+ });
20
+ (0, _vitest.it)('converts strikethrough', () => {
21
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('~~gone~~')).toBe('-gone-');
22
+ });
23
+ (0, _vitest.it)('converts links', () => {
24
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('see [docs](https://x.example/y)')).toBe('see [docs|https://x.example/y]');
25
+ });
26
+ (0, _vitest.it)('converts blockquote', () => {
27
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('> quoted line')).toBe('bq. quoted line');
28
+ });
29
+ (0, _vitest.it)('converts unordered list', () => {
30
+ const md = '- one\n- two\n- three';
31
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('* one\n* two\n* three');
32
+ });
33
+ (0, _vitest.it)('converts one level of nested unordered list', () => {
34
+ const md = '- top\n - sub\n- back';
35
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('* top\n** sub\n* back');
36
+ });
37
+ (0, _vitest.it)('converts ordered list', () => {
38
+ const md = '1. one\n2. two\n3. three';
39
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('# one\n# two\n# three');
40
+ });
41
+ (0, _vitest.it)('converts inline code', () => {
42
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('use `foo()` here')).toBe('use {{foo()}} here');
43
+ });
44
+ (0, _vitest.it)('converts plain code fence to noformat', () => {
45
+ const md = '```\nraw code\nmore\n```';
46
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('{noformat}\nraw code\nmore\n{noformat}');
47
+ });
48
+ (0, _vitest.it)('converts code fence with language tag to {code:lang}', () => {
49
+ const md = '```js\nconst x = 1;\n```';
50
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('{code:js}\nconst x = 1;\n{code}');
51
+ });
52
+ (0, _vitest.it)('converts HTML entities &nbsp; / &amp; / &lt; / &gt;', () => {
53
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('a&nbsp;b')).toBe('a b');
54
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('a&amp;b')).toBe('a&b');
55
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('a&lt;b&gt;c')).toBe('a<b>c');
56
+ });
57
+ });
58
+ (0, _vitest.describe)('markdownToJiraWiki — content-protection inside code fences', () => {
59
+ (0, _vitest.it)('does not transform `{{plates}}` inside a fenced block (run-#4 false-positive case)', () => {
60
+ const md = ['Before', '```', 'context: {{plates}} should stay literal', '# not a heading', '- not a list', '```', 'After'].join('\n');
61
+ const out = (0, _report.markdownToJiraWiki)(md);
62
+ (0, _vitest.expect)(out).toContain('{noformat}\ncontext: {{plates}} should stay literal');
63
+ (0, _vitest.expect)(out).toContain('# not a heading');
64
+ (0, _vitest.expect)(out).toContain('- not a list');
65
+ (0, _vitest.expect)(out).not.toContain('h1.');
66
+ });
67
+ (0, _vitest.it)('does not transform headings/links/lists inside an inline code span', () => {
68
+ const md = 'use `# not a heading` and `[ref](u)` here';
69
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('use {{# not a heading}} and {{[ref](u)}} here');
70
+ });
71
+ });
72
+ (0, _vitest.describe)('markdownToJiraWiki — edge cases', () => {
73
+ (0, _vitest.it)('handles bold containing italic: **bold *inside* bold**', () => {
74
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('**bold *inside* bold**')).toBe('*bold _inside_ bold*');
75
+ });
76
+ (0, _vitest.it)('preserves bold and italic on the same line', () => {
77
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('**a** and *b*')).toBe('*a* and _b_');
78
+ });
79
+ (0, _vitest.it)('handles a heading whose text contains backticks', () => {
80
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('## use `foo()` for x')).toBe('h2. use {{foo()}} for x');
81
+ });
82
+ (0, _vitest.it)('passes plain text through unchanged', () => {
83
+ const plain = 'Just a normal sentence with no markdown.';
84
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)(plain)).toBe(plain);
85
+ });
86
+ (0, _vitest.it)('returns empty string unchanged', () => {
87
+ (0, _vitest.expect)((0, _report.markdownToJiraWiki)('')).toBe('');
88
+ });
89
+ (0, _vitest.it)('handles a multi-block document end-to-end', () => {
90
+ const md = ['# Handoff', '', 'Some **bold** intro and a [link](https://x.example).', '', '## Findings', '', '- first', '- second', '', '> note: this matters', '', '```js', 'const x = 1;', '```', '', 'Done&nbsp;here.'].join('\n');
91
+ const out = (0, _report.markdownToJiraWiki)(md);
92
+ (0, _vitest.expect)(out).toContain('h1. Handoff');
93
+ (0, _vitest.expect)(out).toContain('h2. Findings');
94
+ (0, _vitest.expect)(out).toContain('Some *bold* intro and a [link|https://x.example].');
95
+ (0, _vitest.expect)(out).toContain('* first\n* second');
96
+ (0, _vitest.expect)(out).toContain('bq. note: this matters');
97
+ (0, _vitest.expect)(out).toContain('{code:js}\nconst x = 1;\n{code}');
98
+ (0, _vitest.expect)(out).toContain('Done here.');
99
+ (0, _vitest.expect)(out).not.toContain('&nbsp;');
100
+ });
101
+ });
@@ -0,0 +1,150 @@
1
+ import {describe, it, expect} from 'vitest';
2
+ import {markdownToJiraWiki} from '../commands/report';
3
+
4
+ describe('markdownToJiraWiki — basic rules', () => {
5
+ it('converts H1', () => {
6
+ expect(markdownToJiraWiki('# Title')).toBe('h1. Title');
7
+ });
8
+
9
+ it('converts H2 / H3 / H6', () => {
10
+ expect(markdownToJiraWiki('## Sub')).toBe('h2. Sub');
11
+ expect(markdownToJiraWiki('### Sub-sub')).toBe('h3. Sub-sub');
12
+ expect(markdownToJiraWiki('###### Deep')).toBe('h6. Deep');
13
+ });
14
+
15
+ it('converts bold', () => {
16
+ expect(markdownToJiraWiki('hello **world** foo')).toBe('hello *world* foo');
17
+ });
18
+
19
+ it('converts italic with single asterisks', () => {
20
+ expect(markdownToJiraWiki('hello *world* foo')).toBe('hello _world_ foo');
21
+ });
22
+
23
+ it('converts strikethrough', () => {
24
+ expect(markdownToJiraWiki('~~gone~~')).toBe('-gone-');
25
+ });
26
+
27
+ it('converts links', () => {
28
+ expect(markdownToJiraWiki('see [docs](https://x.example/y)'))
29
+ .toBe('see [docs|https://x.example/y]');
30
+ });
31
+
32
+ it('converts blockquote', () => {
33
+ expect(markdownToJiraWiki('> quoted line')).toBe('bq. quoted line');
34
+ });
35
+
36
+ it('converts unordered list', () => {
37
+ const md = '- one\n- two\n- three';
38
+ expect(markdownToJiraWiki(md)).toBe('* one\n* two\n* three');
39
+ });
40
+
41
+ it('converts one level of nested unordered list', () => {
42
+ const md = '- top\n - sub\n- back';
43
+ expect(markdownToJiraWiki(md)).toBe('* top\n** sub\n* back');
44
+ });
45
+
46
+ it('converts ordered list', () => {
47
+ const md = '1. one\n2. two\n3. three';
48
+ expect(markdownToJiraWiki(md)).toBe('# one\n# two\n# three');
49
+ });
50
+
51
+ it('converts inline code', () => {
52
+ expect(markdownToJiraWiki('use `foo()` here')).toBe('use {{foo()}} here');
53
+ });
54
+
55
+ it('converts plain code fence to noformat', () => {
56
+ const md = '```\nraw code\nmore\n```';
57
+ expect(markdownToJiraWiki(md)).toBe('{noformat}\nraw code\nmore\n{noformat}');
58
+ });
59
+
60
+ it('converts code fence with language tag to {code:lang}', () => {
61
+ const md = '```js\nconst x = 1;\n```';
62
+ expect(markdownToJiraWiki(md)).toBe('{code:js}\nconst x = 1;\n{code}');
63
+ });
64
+
65
+ it('converts HTML entities &nbsp; / &amp; / &lt; / &gt;', () => {
66
+ expect(markdownToJiraWiki('a&nbsp;b')).toBe('a b');
67
+ expect(markdownToJiraWiki('a&amp;b')).toBe('a&b');
68
+ expect(markdownToJiraWiki('a&lt;b&gt;c')).toBe('a<b>c');
69
+ });
70
+ });
71
+
72
+ describe('markdownToJiraWiki — content-protection inside code fences', () => {
73
+ it('does not transform `{{plates}}` inside a fenced block (run-#4 false-positive case)', () => {
74
+ const md = [
75
+ 'Before',
76
+ '```',
77
+ 'context: {{plates}} should stay literal',
78
+ '# not a heading',
79
+ '- not a list',
80
+ '```',
81
+ 'After',
82
+ ].join('\n');
83
+ const out = markdownToJiraWiki(md);
84
+ expect(out).toContain('{noformat}\ncontext: {{plates}} should stay literal');
85
+ expect(out).toContain('# not a heading');
86
+ expect(out).toContain('- not a list');
87
+ expect(out).not.toContain('h1.');
88
+ });
89
+
90
+ it('does not transform headings/links/lists inside an inline code span', () => {
91
+ const md = 'use `# not a heading` and `[ref](u)` here';
92
+ expect(markdownToJiraWiki(md))
93
+ .toBe('use {{# not a heading}} and {{[ref](u)}} here');
94
+ });
95
+ });
96
+
97
+ describe('markdownToJiraWiki — edge cases', () => {
98
+ it('handles bold containing italic: **bold *inside* bold**', () => {
99
+ expect(markdownToJiraWiki('**bold *inside* bold**'))
100
+ .toBe('*bold _inside_ bold*');
101
+ });
102
+
103
+ it('preserves bold and italic on the same line', () => {
104
+ expect(markdownToJiraWiki('**a** and *b*')).toBe('*a* and _b_');
105
+ });
106
+
107
+ it('handles a heading whose text contains backticks', () => {
108
+ expect(markdownToJiraWiki('## use `foo()` for x'))
109
+ .toBe('h2. use {{foo()}} for x');
110
+ });
111
+
112
+ it('passes plain text through unchanged', () => {
113
+ const plain = 'Just a normal sentence with no markdown.';
114
+ expect(markdownToJiraWiki(plain)).toBe(plain);
115
+ });
116
+
117
+ it('returns empty string unchanged', () => {
118
+ expect(markdownToJiraWiki('')).toBe('');
119
+ });
120
+
121
+ it('handles a multi-block document end-to-end', () => {
122
+ const md = [
123
+ '# Handoff',
124
+ '',
125
+ 'Some **bold** intro and a [link](https://x.example).',
126
+ '',
127
+ '## Findings',
128
+ '',
129
+ '- first',
130
+ '- second',
131
+ '',
132
+ '> note: this matters',
133
+ '',
134
+ '```js',
135
+ 'const x = 1;',
136
+ '```',
137
+ '',
138
+ 'Done&nbsp;here.',
139
+ ].join('\n');
140
+ const out = markdownToJiraWiki(md);
141
+ expect(out).toContain('h1. Handoff');
142
+ expect(out).toContain('h2. Findings');
143
+ expect(out).toContain('Some *bold* intro and a [link|https://x.example].');
144
+ expect(out).toContain('* first\n* second');
145
+ expect(out).toContain('bq. note: this matters');
146
+ expect(out).toContain('{code:js}\nconst x = 1;\n{code}');
147
+ expect(out).toContain('Done here.');
148
+ expect(out).not.toContain('&nbsp;');
149
+ });
150
+ });
@@ -4,6 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
+ exports.markdownToJiraWiki = markdownToJiraWiki;
7
8
  exports.report = report;
8
9
  var _fs = _interopRequireDefault(require("fs"));
9
10
  var _os = _interopRequireDefault(require("os"));
@@ -342,46 +343,79 @@ async function handleTicket(args) {
342
343
  const instance = args._[2];
343
344
  const reportId = args._[3];
344
345
  if (!instance || !reportId) {
345
- color.error('Usage: grok report ticket <instance> <report-id>');
346
+ color.error('Usage: grok report ticket <instance> <report-id> [--project <KEY>] [--type <Bug>] [--jira-url <url>]');
346
347
  return false;
347
348
  }
349
+ const projectKey = args['project'] || process.env.JIRA_PROJECT || '';
350
+ if (!projectKey) {
351
+ color.error('--project or $JIRA_PROJECT is required (no GROK default)');
352
+ return false;
353
+ }
354
+ const issueType = args['type'] || 'Bug';
355
+ const auth = jiraAuthHeader();
356
+ if (auth == null) {
357
+ color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report ticket`.');
358
+ return false;
359
+ }
360
+ const jiraBase = resolveJiraBase(args);
348
361
  try {
349
362
  const {
350
363
  url,
351
364
  key
352
365
  } = (0, _testUtils.getDevKey)(instance);
353
366
  const token = await (0, _testUtils.getToken)(url, key);
354
- console.log('Getting current user...');
355
- const userResp = await fetch(`${url}/users/current`, {
367
+ console.log(`Fetching report ${reportId}...`);
368
+ const reportResp = await fetch(`${url}/reports/${encodeURIComponent(reportId)}`, {
356
369
  headers: {
357
370
  Authorization: token
358
371
  }
359
372
  });
360
- if (!userResp.ok) {
361
- color.error(`Failed to get current user (HTTP ${userResp.status})`);
362
- return false;
363
- }
364
- const user = await userResp.json();
365
- const userId = user.id || user.Id;
366
- if (!userId) {
367
- color.error('No user id in response');
373
+ if (!reportResp.ok) {
374
+ const body = await reportResp.text();
375
+ color.error(`Failed to fetch report (HTTP ${reportResp.status}): ${body.slice(0, 400)}`);
368
376
  return false;
369
377
  }
370
- console.log(`Creating JIRA ticket for report ${reportId}...`);
371
- const ticketResp = await fetch(`${url}/reports/${reportId}/jira?assigneeId=${userId}`, {
378
+ const body = await reportResp.json();
379
+ // The REST endpoint returns a flat UserReport: top-level `#type`,
380
+ // `number`, `errorMessage`, etc. The `data` field is a ref to the
381
+ // related data-table entity, not a body wrapper — do not unwrap.
382
+ const number = body && (body.number != null ? body.number : body.Number);
383
+ const errorMessage = (body && (body.errorMessage || body.ErrorMessage) || '').toString().trim();
384
+ let summary = number != null ? errorMessage ? `Report #${number}: ${errorMessage}` : `Report #${number}` : errorMessage || `Report ${reportId}`;
385
+ if (summary.length > 200) summary = summary.slice(0, 200);
386
+ // JIRA rejects newlines in summary.
387
+ summary = summary.replace(/[\r\n]+/g, ' ').trim();
388
+ const webRoot = url.replace(/\/api\/?$/, '');
389
+ const reportLink = number != null ? `${webRoot}/apps/usage/reports/${number}` : `${webRoot}/apps/usage/reports/`;
390
+ const description = `Auto-created from ${reportLink}`;
391
+ console.log(`Creating JIRA ticket in ${projectKey} (${issueType})...`);
392
+ const createResp = await fetch(`${jiraBase}/rest/api/2/issue/`, {
372
393
  method: 'POST',
373
394
  headers: {
374
- Authorization: token,
375
- 'Content-Type': 'application/json'
376
- }
395
+ Authorization: auth,
396
+ 'Content-Type': 'application/json',
397
+ Accept: 'application/json'
398
+ },
399
+ body: JSON.stringify({
400
+ fields: {
401
+ project: {
402
+ key: projectKey
403
+ },
404
+ summary,
405
+ issuetype: {
406
+ name: issueType
407
+ },
408
+ description
409
+ }
410
+ })
377
411
  });
378
- if (ticketResp.status !== 200 && ticketResp.status !== 201) {
379
- const body = await ticketResp.text();
380
- color.error(`JIRA ticket creation failed (HTTP ${ticketResp.status}): ${body}`);
412
+ if (createResp.status !== 200 && createResp.status !== 201) {
413
+ const errBody = await createResp.text();
414
+ color.error(`JIRA issue creation failed (HTTP ${createResp.status}): ${errBody}`);
381
415
  return false;
382
416
  }
383
- const result = await ticketResp.json();
384
- const ticketKey = result.key;
417
+ const result = await createResp.json();
418
+ const ticketKey = result && result.key;
385
419
  if (!ticketKey) {
386
420
  color.error(`No ticket key in response: ${JSON.stringify(result).slice(0, 200)}`);
387
421
  return false;
@@ -403,8 +437,10 @@ async function handleTicket(args) {
403
437
  // the Datagrok org instance; override via --jira-url or $JIRA_URL.
404
438
  //
405
439
  // Why v2 and not v3: v3 requires comment bodies in ADF (Atlassian Document
406
- // Format) JSON; v2 accepts plain text / wiki-markup. The handoff prompt emits
407
- // markdown, which JIRA's plain-text path renders acceptably.
440
+ // Format) JSON, which is much heavier to construct. v2 accepts a plain string
441
+ // body, which JIRA renders as wiki markup — NOT Markdown. So a `# heading`
442
+ // becomes a top-level ordered-list item, `&nbsp;` shows up literally, etc.
443
+ // `markdownToJiraWiki` below bridges the gap for Markdown-emitting callers.
408
444
 
409
445
  function resolveJiraBase(args) {
410
446
  const cli = args['jira-url'] || '';
@@ -417,6 +453,76 @@ function jiraAuthHeader() {
417
453
  if (!user || !token) return null;
418
454
  return 'Basic ' + Buffer.from(`${user}:${token}`).toString('base64');
419
455
  }
456
+
457
+ // Convert a Markdown string to JIRA wiki markup so that REST v2 renders it the
458
+ // way the author intended. Handles the common cases that diverge between the
459
+ // two dialects; for anything not listed, the input is passed through unchanged.
460
+ //
461
+ // Deferred for v1 (left as-is, may render imperfectly):
462
+ // - tables (Markdown and JIRA wiki use very similar pipe syntax)
463
+ // - nested lists deeper than two levels
464
+ // - footnotes, definition lists, raw HTML
465
+ function markdownToJiraWiki(md) {
466
+ if (!md) return md;
467
+
468
+ // 1. Code fences first: extract their content into placeholders so that
469
+ // later rules cannot rewrite anything inside `{noformat}` / `{code}`.
470
+ const placeholders = [];
471
+ const protect = s => {
472
+ placeholders.push(s);
473
+ return `\x00P${placeholders.length - 1}\x00`;
474
+ };
475
+ let out = md;
476
+ out = out.replace(/```([A-Za-z0-9_+\-]*)\r?\n([\s\S]*?)```/g, (_m, lang, code) => {
477
+ const wiki = lang ? `{code:${lang}}\n${code}{code}` : `{noformat}\n${code}{noformat}`;
478
+ return protect(wiki);
479
+ });
480
+
481
+ // 2. Inline code spans (after fences, before anything else).
482
+ out = out.replace(/`([^`\n]+)`/g, (_m, code) => protect(`{{${code}}}`));
483
+
484
+ // 3. Headings — h6 → h1 so that the `###` prefix doesn't get matched as `##`.
485
+ // Must run before list rules (where `#` would otherwise be ambiguous).
486
+ out = out.replace(/^###### (.+)$/gm, 'h6. $1');
487
+ out = out.replace(/^##### (.+)$/gm, 'h5. $1');
488
+ out = out.replace(/^#### (.+)$/gm, 'h4. $1');
489
+ out = out.replace(/^### (.+)$/gm, 'h3. $1');
490
+ out = out.replace(/^## (.+)$/gm, 'h2. $1');
491
+ out = out.replace(/^# (.+)$/gm, 'h1. $1');
492
+
493
+ // 4. Bold then italic. We stash bold runs behind a sentinel first so that
494
+ // the inner italic pass can't mistake the leftover single `*` markers for
495
+ // italic (and vice-versa for `**bold *italic* bold**`).
496
+ out = out.replace(/\*\*([\s\S]+?)\*\*/g, '\x01$1\x01');
497
+ out = out.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, '_$1_');
498
+ out = out.replace(/\x01/g, '*');
499
+
500
+ // 5. Strikethrough.
501
+ out = out.replace(/~~([^~\n]+)~~/g, '-$1-');
502
+
503
+ // 6. Links: [text](url) → [text|url].
504
+ out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]');
505
+
506
+ // 7. Blockquote.
507
+ out = out.replace(/^> (.+)$/gm, 'bq. $1');
508
+
509
+ // 8. Unordered lists (one level of nesting). Two-space indent → second level.
510
+ out = out.replace(/^ - /gm, '** ');
511
+ out = out.replace(/^- /gm, '* ');
512
+
513
+ // 9. Ordered lists. After the heading pass so we don't clobber `# Title`.
514
+ out = out.replace(/^\d+\. /gm, '# ');
515
+
516
+ // 10. HTML entities that JIRA wiki shows literally.
517
+ out = out.replace(/&nbsp;/g, ' ');
518
+ out = out.replace(/&amp;/g, '&');
519
+ out = out.replace(/&lt;/g, '<');
520
+ out = out.replace(/&gt;/g, '>');
521
+
522
+ // Restore protected fences / inline code last.
523
+ out = out.replace(/\x00P(\d+)\x00/g, (_m, idx) => placeholders[Number(idx)]);
524
+ return out;
525
+ }
420
526
  async function handleComment(args) {
421
527
  const ticket = args._[2];
422
528
  if (!ticket) {
@@ -449,6 +555,10 @@ async function handleComment(args) {
449
555
  }
450
556
  const base = resolveJiraBase(args);
451
557
  const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/comment`;
558
+ // Callers (especially the dg-fix-reports M2 handoff) emit Markdown, but JIRA
559
+ // REST v2 renders the body as wiki markup. Convert before posting so headings,
560
+ // lists, and HTML entities don't render as garbage in the JIRA UI.
561
+ const wikiBody = markdownToJiraWiki(body);
452
562
  try {
453
563
  const resp = await fetch(url, {
454
564
  method: 'POST',
@@ -458,7 +568,7 @@ async function handleComment(args) {
458
568
  Accept: 'application/json'
459
569
  },
460
570
  body: JSON.stringify({
461
- body
571
+ body: wikiBody
462
572
  })
463
573
  });
464
574
  if (resp.status !== 200 && resp.status !== 201) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.1.12",
3
+ "version": "6.1.14",
4
4
  "description": "Utility to upload and publish packages to Datagrok",
5
5
  "homepage": "https://github.com/datagrok-ai/public/tree/master/tools#readme",
6
6
  "dependencies": {
@@ -14,13 +14,12 @@
14
14
  "archiver-promise": "^1.0.0",
15
15
  "datagrok-api": "^1.26.0",
16
16
  "estraverse": "^5.3.0",
17
- "glob": "^11.0.2",
17
+ "glob": "^13.0.6",
18
18
  "ignore-walk": "^3.0.4",
19
19
  "inquirer": "^7.3.3",
20
20
  "js-yaml": "^4.1.0",
21
21
  "minimist": "^1.2.8",
22
22
  "node-fetch": "^2.7.0",
23
- "node-recursive-directory": "^1.2.0",
24
23
  "os": "^0.1.2",
25
24
  "papaparse": "^5.4.1",
26
25
  "path": "^0.12.7",