datagrok-tools 6.1.11 → 6.1.13
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 +4 -0
- package/bin/commands/report.js +201 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.1.13 (2026-04-30)
|
|
4
|
+
|
|
5
|
+
* Reports: `grok report ticket` now uses direct JIRA REST honoring `$JIRA_PROJECT`, replacing the Datlas-mediated path that hardcoded GROK.
|
|
6
|
+
|
|
3
7
|
## 6.1.11 (2026-04-27)
|
|
4
8
|
|
|
5
9
|
* `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`)
|
package/bin/commands/report.js
CHANGED
|
@@ -24,6 +24,10 @@ async function report(args) {
|
|
|
24
24
|
return await handleResolve(args);
|
|
25
25
|
case 'ticket':
|
|
26
26
|
return await handleTicket(args);
|
|
27
|
+
case 'comment':
|
|
28
|
+
return await handleComment(args);
|
|
29
|
+
case 'label':
|
|
30
|
+
return await handleLabel(args);
|
|
27
31
|
default:
|
|
28
32
|
return false;
|
|
29
33
|
}
|
|
@@ -103,7 +107,20 @@ async function handleFetch(args) {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
function looksLikePath(s) {
|
|
106
|
-
|
|
110
|
+
if (s.includes('/') || s.includes('\\')) return true;
|
|
111
|
+
if (/\.(zip|json)$/i.test(s)) return true;
|
|
112
|
+
// Only treat a bare name as a path if it's an actual file. A *directory*
|
|
113
|
+
// collision (e.g. running `grok report read public 2147` from a checkout
|
|
114
|
+
// that has a `public/` submodule next to it) must not flip into the
|
|
115
|
+
// single-arg path branch — that would EISDIR on `readFileSync(p)`.
|
|
116
|
+
if (_fs.default.existsSync(s)) {
|
|
117
|
+
try {
|
|
118
|
+
return _fs.default.statSync(s).isFile();
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
107
124
|
}
|
|
108
125
|
function loadFromZip(z) {
|
|
109
126
|
const entries = z.getEntries();
|
|
@@ -202,7 +219,7 @@ function extractD42(zip, names, outDir) {
|
|
|
202
219
|
recursive: true
|
|
203
220
|
});
|
|
204
221
|
const written = [];
|
|
205
|
-
for (
|
|
222
|
+
for (const name of names) {
|
|
206
223
|
const entry = zip.getEntry(name);
|
|
207
224
|
if (entry == null) continue;
|
|
208
225
|
const out = _path.default.join(outDir, _path.default.basename(name));
|
|
@@ -325,46 +342,79 @@ async function handleTicket(args) {
|
|
|
325
342
|
const instance = args._[2];
|
|
326
343
|
const reportId = args._[3];
|
|
327
344
|
if (!instance || !reportId) {
|
|
328
|
-
color.error('Usage: grok report ticket <instance> <report-id>');
|
|
345
|
+
color.error('Usage: grok report ticket <instance> <report-id> [--project <KEY>] [--type <Bug>] [--jira-url <url>]');
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
const projectKey = args['project'] || process.env.JIRA_PROJECT || '';
|
|
349
|
+
if (!projectKey) {
|
|
350
|
+
color.error('--project or $JIRA_PROJECT is required (no GROK default)');
|
|
329
351
|
return false;
|
|
330
352
|
}
|
|
353
|
+
const issueType = args['type'] || 'Bug';
|
|
354
|
+
const auth = jiraAuthHeader();
|
|
355
|
+
if (auth == null) {
|
|
356
|
+
color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report ticket`.');
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const jiraBase = resolveJiraBase(args);
|
|
331
360
|
try {
|
|
332
361
|
const {
|
|
333
362
|
url,
|
|
334
363
|
key
|
|
335
364
|
} = (0, _testUtils.getDevKey)(instance);
|
|
336
365
|
const token = await (0, _testUtils.getToken)(url, key);
|
|
337
|
-
console.log(
|
|
338
|
-
const
|
|
366
|
+
console.log(`Fetching report ${reportId}...`);
|
|
367
|
+
const reportResp = await fetch(`${url}/reports/${encodeURIComponent(reportId)}`, {
|
|
339
368
|
headers: {
|
|
340
369
|
Authorization: token
|
|
341
370
|
}
|
|
342
371
|
});
|
|
343
|
-
if (!
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
const user = await userResp.json();
|
|
348
|
-
const userId = user.id || user.Id;
|
|
349
|
-
if (!userId) {
|
|
350
|
-
color.error('No user id in response');
|
|
372
|
+
if (!reportResp.ok) {
|
|
373
|
+
const body = await reportResp.text();
|
|
374
|
+
color.error(`Failed to fetch report (HTTP ${reportResp.status}): ${body.slice(0, 400)}`);
|
|
351
375
|
return false;
|
|
352
376
|
}
|
|
353
|
-
|
|
354
|
-
|
|
377
|
+
const body = await reportResp.json();
|
|
378
|
+
// The REST endpoint returns a flat UserReport: top-level `#type`,
|
|
379
|
+
// `number`, `errorMessage`, etc. The `data` field is a ref to the
|
|
380
|
+
// related data-table entity, not a body wrapper — do not unwrap.
|
|
381
|
+
const number = body && (body.number != null ? body.number : body.Number);
|
|
382
|
+
const errorMessage = (body && (body.errorMessage || body.ErrorMessage) || '').toString().trim();
|
|
383
|
+
let summary = number != null ? errorMessage ? `Report #${number}: ${errorMessage}` : `Report #${number}` : errorMessage || `Report ${reportId}`;
|
|
384
|
+
if (summary.length > 200) summary = summary.slice(0, 200);
|
|
385
|
+
// JIRA rejects newlines in summary.
|
|
386
|
+
summary = summary.replace(/[\r\n]+/g, ' ').trim();
|
|
387
|
+
const webRoot = url.replace(/\/api\/?$/, '');
|
|
388
|
+
const reportLink = number != null ? `${webRoot}/apps/usage/reports/${number}` : `${webRoot}/apps/usage/reports/`;
|
|
389
|
+
const description = `Auto-created from ${reportLink}`;
|
|
390
|
+
console.log(`Creating JIRA ticket in ${projectKey} (${issueType})...`);
|
|
391
|
+
const createResp = await fetch(`${jiraBase}/rest/api/2/issue/`, {
|
|
355
392
|
method: 'POST',
|
|
356
393
|
headers: {
|
|
357
|
-
Authorization:
|
|
358
|
-
'Content-Type': 'application/json'
|
|
359
|
-
|
|
394
|
+
Authorization: auth,
|
|
395
|
+
'Content-Type': 'application/json',
|
|
396
|
+
Accept: 'application/json'
|
|
397
|
+
},
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
fields: {
|
|
400
|
+
project: {
|
|
401
|
+
key: projectKey
|
|
402
|
+
},
|
|
403
|
+
summary,
|
|
404
|
+
issuetype: {
|
|
405
|
+
name: issueType
|
|
406
|
+
},
|
|
407
|
+
description
|
|
408
|
+
}
|
|
409
|
+
})
|
|
360
410
|
});
|
|
361
|
-
if (
|
|
362
|
-
const
|
|
363
|
-
color.error(`JIRA
|
|
411
|
+
if (createResp.status !== 200 && createResp.status !== 201) {
|
|
412
|
+
const errBody = await createResp.text();
|
|
413
|
+
color.error(`JIRA issue creation failed (HTTP ${createResp.status}): ${errBody}`);
|
|
364
414
|
return false;
|
|
365
415
|
}
|
|
366
|
-
const result = await
|
|
367
|
-
const ticketKey = result.key;
|
|
416
|
+
const result = await createResp.json();
|
|
417
|
+
const ticketKey = result && result.key;
|
|
368
418
|
if (!ticketKey) {
|
|
369
419
|
color.error(`No ticket key in response: ${JSON.stringify(result).slice(0, 200)}`);
|
|
370
420
|
return false;
|
|
@@ -376,4 +426,132 @@ async function handleTicket(args) {
|
|
|
376
426
|
color.error(`Error: ${err.message}`);
|
|
377
427
|
return false;
|
|
378
428
|
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── JIRA REST helpers (used by `grok report comment` / `grok report label`) ─
|
|
432
|
+
//
|
|
433
|
+
// These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok). Auth is HTTP
|
|
434
|
+
// Basic with `JIRA_USER` (Atlassian email) + `JIRA_TOKEN` (API token from
|
|
435
|
+
// id.atlassian.com/manage-profile/security/api-tokens). Base URL defaults to
|
|
436
|
+
// the Datagrok org instance; override via --jira-url or $JIRA_URL.
|
|
437
|
+
//
|
|
438
|
+
// Why v2 and not v3: v3 requires comment bodies in ADF (Atlassian Document
|
|
439
|
+
// Format) JSON; v2 accepts plain text / wiki-markup. The handoff prompt emits
|
|
440
|
+
// markdown, which JIRA's plain-text path renders acceptably.
|
|
441
|
+
|
|
442
|
+
function resolveJiraBase(args) {
|
|
443
|
+
const cli = args['jira-url'] || '';
|
|
444
|
+
const env = process.env.JIRA_URL || '';
|
|
445
|
+
return (cli || env || 'https://reddata.atlassian.net').replace(/\/+$/, '');
|
|
446
|
+
}
|
|
447
|
+
function jiraAuthHeader() {
|
|
448
|
+
const user = process.env.JIRA_USER;
|
|
449
|
+
const token = process.env.JIRA_TOKEN;
|
|
450
|
+
if (!user || !token) return null;
|
|
451
|
+
return 'Basic ' + Buffer.from(`${user}:${token}`).toString('base64');
|
|
452
|
+
}
|
|
453
|
+
async function handleComment(args) {
|
|
454
|
+
const ticket = args._[2];
|
|
455
|
+
if (!ticket) {
|
|
456
|
+
color.error('Usage: grok report comment <ticket-key> [--body <text> | --body-file <path>] [--jira-url <url>]');
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const auth = jiraAuthHeader();
|
|
460
|
+
if (auth == null) {
|
|
461
|
+
color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report comment`.');
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
let body;
|
|
465
|
+
if (typeof args['body-file'] === 'string') {
|
|
466
|
+
const p = args['body-file'];
|
|
467
|
+
try {
|
|
468
|
+
body = _fs.default.readFileSync(p, 'utf-8');
|
|
469
|
+
} catch (e) {
|
|
470
|
+
color.error(`Failed to read --body-file ${p}: ${e.message}`);
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
} else if (typeof args['body'] === 'string') {
|
|
474
|
+
body = args['body'];
|
|
475
|
+
} else {
|
|
476
|
+
// Read from stdin if neither --body nor --body-file is provided.
|
|
477
|
+
body = _fs.default.readFileSync(0, 'utf-8');
|
|
478
|
+
}
|
|
479
|
+
if (!body || body.trim().length === 0) {
|
|
480
|
+
color.error('Comment body is empty (use --body, --body-file, or pipe to stdin).');
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
const base = resolveJiraBase(args);
|
|
484
|
+
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/comment`;
|
|
485
|
+
try {
|
|
486
|
+
const resp = await fetch(url, {
|
|
487
|
+
method: 'POST',
|
|
488
|
+
headers: {
|
|
489
|
+
Authorization: auth,
|
|
490
|
+
'Content-Type': 'application/json',
|
|
491
|
+
Accept: 'application/json'
|
|
492
|
+
},
|
|
493
|
+
body: JSON.stringify({
|
|
494
|
+
body
|
|
495
|
+
})
|
|
496
|
+
});
|
|
497
|
+
if (resp.status !== 200 && resp.status !== 201) {
|
|
498
|
+
const text = await resp.text();
|
|
499
|
+
color.error(`JIRA comment POST failed (HTTP ${resp.status}): ${text.slice(0, 400)}`);
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const result = await resp.json();
|
|
503
|
+
const id = result && (result.id || result.Id);
|
|
504
|
+
if (!id) {
|
|
505
|
+
color.error(`JIRA returned no comment id: ${JSON.stringify(result).slice(0, 200)}`);
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
color.success(`Posted comment ${id} on ${ticket}`);
|
|
509
|
+
console.log(id);
|
|
510
|
+
return true;
|
|
511
|
+
} catch (err) {
|
|
512
|
+
color.error(`Error: ${err.message}`);
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function handleLabel(args) {
|
|
517
|
+
const ticket = args._[2];
|
|
518
|
+
const labels = args._.slice(3).filter(s => s.length > 0);
|
|
519
|
+
if (!ticket || labels.length === 0) {
|
|
520
|
+
color.error('Usage: grok report label <ticket-key> <label> [<label2> ...] [--jira-url <url>]');
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
const auth = jiraAuthHeader();
|
|
524
|
+
if (auth == null) {
|
|
525
|
+
color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report label`.');
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
const base = resolveJiraBase(args);
|
|
529
|
+
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}`;
|
|
530
|
+
const update = {
|
|
531
|
+
labels: labels.map(l => ({
|
|
532
|
+
add: l
|
|
533
|
+
}))
|
|
534
|
+
};
|
|
535
|
+
try {
|
|
536
|
+
const resp = await fetch(url, {
|
|
537
|
+
method: 'PUT',
|
|
538
|
+
headers: {
|
|
539
|
+
Authorization: auth,
|
|
540
|
+
'Content-Type': 'application/json'
|
|
541
|
+
},
|
|
542
|
+
body: JSON.stringify({
|
|
543
|
+
update
|
|
544
|
+
})
|
|
545
|
+
});
|
|
546
|
+
if (resp.status !== 204 && resp.status !== 200) {
|
|
547
|
+
const text = await resp.text();
|
|
548
|
+
color.error(`JIRA label PUT failed (HTTP ${resp.status}): ${text.slice(0, 400)}`);
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
color.success(`Applied labels to ${ticket}: ${labels.join(', ')}`);
|
|
552
|
+
return true;
|
|
553
|
+
} catch (err) {
|
|
554
|
+
color.error(`Error: ${err.message}`);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
379
557
|
}
|
package/package.json
CHANGED