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 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.
@@ -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 and JIRA_TOKEN env vars are required for `grok report ticket`.');
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). Auth is HTTP
448
- // Basic with `JIRA_USER` (Atlassian email) + `JIRA_TOKEN` (API token from
449
- // id.atlassian.com/manage-profile/security/api-tokens). Base URL defaults to
450
- // the Datagrok org instance; override via --jira-url or $JIRA_URL.
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, `&nbsp;` shows up literally, etc.
456
469
  // `markdownToJiraWiki` below bridges the gap for Markdown-emitting callers.
457
470
 
458
- function resolveJiraBase(args) {
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 (!user || !token) return null;
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 and JIRA_TOKEN env vars are required for `grok report comment`.');
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
- const user = process.env.JIRA_USER;
618
- const token = process.env.JIRA_TOKEN;
619
- if (!user || !token) {
620
- color.error('JIRA_USER and JIRA_TOKEN env vars are required for `grok report attach`.');
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', '-u', `${user}:${token}`, '-H', 'X-Atlassian-Token: no-check', '-F', `file=@${filePath}`, '-w', '\n%{http_code}\n', url], {
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 and JIRA_TOKEN env vars are required for `grok report label`.');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.5.5",
3
+ "version": "6.5.6",
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": {