datagrok-tools 6.5.5 → 6.5.6
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 +68 -19
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.5.6 (WIP)
|
|
4
|
+
|
|
5
|
+
* `grok report` (`ticket`, `comment`, `label`, `attach`) — support Atlassian **service-account** tokens alongside user API tokens, chosen by the token itself. A user token (`ATATT...`) keeps HTTP Basic with `JIRA_USER` + `JIRA_TOKEN` against the site. A service-account token (`ATSTT...`, minted at admin.atlassian.com) is a scoped token that the site host will not accept — Basic returns 401 and Bearer returns 403 `"Failed to parse Connect Session Auth Token"` — so it is sent as Bearer to the API gateway at `api.atlassian.com/ex/jira/<cloudId>`, with the cloud id read once from the site's public `/_edge/tenant_info` (override with `$JIRA_CLOUD_ID`). `$JIRA_URL` and `--jira-url` still mean the SITE; the gateway address is derived from it, never substituted for it. A service token needs no `JIRA_USER`, so the credential checks no longer demand one.
|
|
6
|
+
|
|
3
7
|
## 6.5.5 (WIP)
|
|
4
8
|
|
|
5
9
|
* GROK-20298: `grok api --ui` — a relation-bearing table's `<Table>Ui` handle carries `<Table>Update` as its fifth generic too (`<Table>Ui.client`, `.table()` and the `<Schema>UiDb` property), so `<table>Ui.client.update(id, {labels: [...]})` compiles instead of failing the excess-property check.
|
package/bin/commands/report.js
CHANGED
|
@@ -367,10 +367,10 @@ async function handleTicket(args) {
|
|
|
367
367
|
const issueType = args['type'] || 'Bug';
|
|
368
368
|
const auth = jiraAuthHeader();
|
|
369
369
|
if (auth == null) {
|
|
370
|
-
color.error('JIRA_USER
|
|
370
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report ticket`.');
|
|
371
371
|
return false;
|
|
372
372
|
}
|
|
373
|
-
const jiraBase = resolveJiraBase(args);
|
|
373
|
+
const jiraBase = await resolveJiraBase(args);
|
|
374
374
|
try {
|
|
375
375
|
const {
|
|
376
376
|
url,
|
|
@@ -444,10 +444,23 @@ async function handleTicket(args) {
|
|
|
444
444
|
|
|
445
445
|
// ─── JIRA REST helpers (used by `grok report comment` / `grok report label`) ─
|
|
446
446
|
//
|
|
447
|
-
// These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok).
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
447
|
+
// These talk DIRECTLY to Atlassian Cloud REST v2 (not Datagrok). Two auth schemes, picked from
|
|
448
|
+
// the token itself:
|
|
449
|
+
//
|
|
450
|
+
// * user API token (`ATATT...`, from id.atlassian.com/manage-profile/security/api-tokens) —
|
|
451
|
+
// HTTP Basic with `JIRA_USER` + `JIRA_TOKEN`, against the site. The original path.
|
|
452
|
+
// * service-account token (`ATSTT...`, from admin.atlassian.com) — a SCOPED token. The site
|
|
453
|
+
// host will not take it: Basic gives 401, and Bearer gives 403 "Failed to parse Connect
|
|
454
|
+
// Session Auth Token" because Jira tries to read it as a Connect session. Only the API
|
|
455
|
+
// gateway accepts it, addressed by cloud id, with Bearer. No JIRA_USER is involved.
|
|
456
|
+
//
|
|
457
|
+
// Both are supported on purpose. Automation moved to a service account when its bot mailbox
|
|
458
|
+
// became a Google group (a group holds no Atlassian identity and cannot mint tokens), while
|
|
459
|
+
// release-notes CI and every developer's local setup still use user tokens.
|
|
460
|
+
//
|
|
461
|
+
// Base URL defaults to the Datagrok org instance; override via --jira-url or $JIRA_URL. That
|
|
462
|
+
// value stays the SITE — the gateway address is derived from it, never substituted for it, so
|
|
463
|
+
// human-facing links elsewhere keep working.
|
|
451
464
|
//
|
|
452
465
|
// Why v2 and not v3: v3 requires comment bodies in ADF (Atlassian Document
|
|
453
466
|
// Format) JSON, which is much heavier to construct. v2 accepts a plain string
|
|
@@ -455,15 +468,49 @@ async function handleTicket(args) {
|
|
|
455
468
|
// becomes a top-level ordered-list item, ` ` shows up literally, etc.
|
|
456
469
|
// `markdownToJiraWiki` below bridges the gap for Markdown-emitting callers.
|
|
457
470
|
|
|
458
|
-
|
|
471
|
+
const JIRA_SERVICE_TOKEN_RE = /^ATSTT/;
|
|
472
|
+
function isJiraServiceToken() {
|
|
473
|
+
return JIRA_SERVICE_TOKEN_RE.test(process.env.JIRA_TOKEN || '');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// The site, as configured. Human-facing links are built from this.
|
|
477
|
+
function resolveJiraSite(args) {
|
|
459
478
|
const cli = args['jira-url'] || '';
|
|
460
479
|
const env = process.env.JIRA_URL || '';
|
|
461
480
|
return (cli || env || 'https://reddata.atlassian.net').replace(/\/+$/, '');
|
|
462
481
|
}
|
|
482
|
+
let cloudIdCache = null;
|
|
483
|
+
|
|
484
|
+
// The gateway addresses a site by cloud id rather than hostname. Public endpoint, no credentials
|
|
485
|
+
// needed, cached for the process.
|
|
486
|
+
async function resolveJiraCloudId(site) {
|
|
487
|
+
if (process.env.JIRA_CLOUD_ID) return process.env.JIRA_CLOUD_ID.trim();
|
|
488
|
+
if (cloudIdCache !== null) return cloudIdCache;
|
|
489
|
+
try {
|
|
490
|
+
const r = await fetch(`${site}/_edge/tenant_info`);
|
|
491
|
+
const j = r.ok ? await r.json() : {};
|
|
492
|
+
cloudIdCache = (j.cloudId || '').trim();
|
|
493
|
+
} catch {
|
|
494
|
+
cloudIdCache = '';
|
|
495
|
+
}
|
|
496
|
+
return cloudIdCache;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Where REST calls go. A service token needs the gateway; anything else keeps the site. If the
|
|
500
|
+
// cloud id cannot be resolved, fall back to the site rather than failing outright — the request
|
|
501
|
+
// then reports a normal auth error instead of a confusing lookup one.
|
|
502
|
+
async function resolveJiraBase(args) {
|
|
503
|
+
const site = resolveJiraSite(args);
|
|
504
|
+
if (!isJiraServiceToken()) return site;
|
|
505
|
+
const cloudId = await resolveJiraCloudId(site);
|
|
506
|
+
return cloudId ? `https://api.atlassian.com/ex/jira/${cloudId}` : site;
|
|
507
|
+
}
|
|
463
508
|
function jiraAuthHeader() {
|
|
464
|
-
const user = process.env.JIRA_USER;
|
|
465
509
|
const token = process.env.JIRA_TOKEN;
|
|
466
|
-
if (!
|
|
510
|
+
if (!token) return null;
|
|
511
|
+
if (isJiraServiceToken()) return `Bearer ${token}`;
|
|
512
|
+
const user = process.env.JIRA_USER;
|
|
513
|
+
if (!user) return null;
|
|
467
514
|
return 'Basic ' + Buffer.from(`${user}:${token}`).toString('base64');
|
|
468
515
|
}
|
|
469
516
|
|
|
@@ -544,7 +591,7 @@ async function handleComment(args) {
|
|
|
544
591
|
}
|
|
545
592
|
const auth = jiraAuthHeader();
|
|
546
593
|
if (auth == null) {
|
|
547
|
-
color.error('JIRA_USER
|
|
594
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report comment`.');
|
|
548
595
|
return false;
|
|
549
596
|
}
|
|
550
597
|
let body;
|
|
@@ -566,7 +613,7 @@ async function handleComment(args) {
|
|
|
566
613
|
color.error('Comment body is empty (use --body, --body-file, or pipe to stdin).');
|
|
567
614
|
return false;
|
|
568
615
|
}
|
|
569
|
-
const base = resolveJiraBase(args);
|
|
616
|
+
const base = await resolveJiraBase(args);
|
|
570
617
|
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/comment`;
|
|
571
618
|
// Callers (especially the dg-fix-reports M2 handoff) emit Markdown, but JIRA
|
|
572
619
|
// REST v2 renders the body as wiki markup. Convert before posting so headings,
|
|
@@ -614,13 +661,15 @@ async function handleAttach(args) {
|
|
|
614
661
|
color.error(`File not found: ${filePath}`);
|
|
615
662
|
return false;
|
|
616
663
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
664
|
+
|
|
665
|
+
// Through the same helper as every other call: a service token authenticates with Bearer and
|
|
666
|
+
// has no JIRA_USER at all, so checking for one here would refuse a working setup.
|
|
667
|
+
const auth = jiraAuthHeader();
|
|
668
|
+
if (auth == null) {
|
|
669
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report attach`.');
|
|
621
670
|
return false;
|
|
622
671
|
}
|
|
623
|
-
const base = resolveJiraBase(args);
|
|
672
|
+
const base = await resolveJiraBase(args);
|
|
624
673
|
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}/attachments`;
|
|
625
674
|
|
|
626
675
|
// node-fetch v2 has no built-in FormData and the codebase doesn't depend on
|
|
@@ -630,7 +679,7 @@ async function handleAttach(args) {
|
|
|
630
679
|
const {
|
|
631
680
|
spawnSync
|
|
632
681
|
} = require('child_process');
|
|
633
|
-
const r = spawnSync('curl', ['-sS', '-X', 'POST', '-
|
|
682
|
+
const r = spawnSync('curl', ['-sS', '-X', 'POST', '-H', `Authorization: ${auth}`, '-H', 'X-Atlassian-Token: no-check', '-F', `file=@${filePath}`, '-w', '\n%{http_code}\n', url], {
|
|
634
683
|
encoding: 'utf8',
|
|
635
684
|
timeout: 120_000,
|
|
636
685
|
maxBuffer: 4 * 1024 * 1024
|
|
@@ -671,10 +720,10 @@ async function handleLabel(args) {
|
|
|
671
720
|
}
|
|
672
721
|
const auth = jiraAuthHeader();
|
|
673
722
|
if (auth == null) {
|
|
674
|
-
color.error('JIRA_USER
|
|
723
|
+
color.error('Set JIRA_TOKEN (plus JIRA_USER for a user API token) for `grok report label`.');
|
|
675
724
|
return false;
|
|
676
725
|
}
|
|
677
|
-
const base = resolveJiraBase(args);
|
|
726
|
+
const base = await resolveJiraBase(args);
|
|
678
727
|
const url = `${base}/rest/api/2/issue/${encodeURIComponent(ticket)}`;
|
|
679
728
|
const update = {
|
|
680
729
|
labels: labels.map(l => ({
|
package/package.json
CHANGED