datagrok-tools 6.4.5 → 6.4.7

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,14 @@
1
1
  # Datagrok-tools changelog
2
2
 
3
+ ## 6.4.7 (2026-07-22)
4
+
5
+ * GROK-20452: `grok publish` — generate `dockerfiles/celery` / `dockerfiles/queue` **before** gathering files for the zip. They were generated after, so the generated Dockerfile never reached the server: `meta.queue` functions fell back to a server-side container named `<pkg>-queue-celery` while the client had built `<pkg>-queue` — image validation 404'd and the worker container never started ("Container is not started" on every call).
6
+
7
+ ## 6.4.6 (2026-07-22)
8
+
9
+ * `grok test` — the whole-run Puppeteer cap (60 min) is now overridable via `GROK_TEST_INVOCATION_TIMEOUT_MS`. When the cap fires mid-pass all collected results are discarded (empty `test-report.csv`, "Passed tests: 0"), which CI reports as "no results produced"; loaded CI agents can now raise the cap instead.
10
+ * `grok test` — `--no-retry` is now honored for Playwright runs. minimist parsed `--no-retry` as `{retry:false}`, so the flag was silently dropped and failed specs were still retried once; normalized so `--retries=0` reaches Playwright.
11
+
3
12
  ## 6.4.5 (2026-06-23)
4
13
 
5
14
  * `grok test` — screen recording (`--record`) is now optional: if `page.screencast()` can't start (e.g. `ffmpeg` is missing on the runner, `spawnSync ffmpeg ENOENT`), the run warns and continues instead of failing the whole Puppeteer pass with 0 tests executed.
@@ -19,6 +19,7 @@ var utils = _interopRequireWildcard(require("../utils/utils"));
19
19
  var color = _interopRequireWildcard(require("../utils/color-utils"));
20
20
  var _check = require("./check");
21
21
  var _pythonCeleryGen = require("../utils/python-celery-gen");
22
+ var _queueWorkerGen = require("../utils/queue-worker-gen");
22
23
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
23
24
  // @ts-ignore
24
25
 
@@ -467,6 +468,14 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
467
468
  const chunks = [];
468
469
  zip.on('data', chunk => chunks.push(chunk));
469
470
 
471
+ // Generate Celery / Node-worker Docker artifacts BEFORE gathering files: the
472
+ // generated dockerfiles/<dir>/Dockerfile must land in the zip, or the server
473
+ // never binds the queue funcs to the client-built image and falls back to a
474
+ // differently-named server-side container that never starts
475
+ // (<pkg>-queue-celery vs the built <pkg>-queue).
476
+ (0, _pythonCeleryGen.generateCeleryArtifacts)(curDir);
477
+ (0, _queueWorkerGen.generateQueueArtifacts)(curDir);
478
+
470
479
  // Gather the files
471
480
  const localTimestamps = {};
472
481
  const files = await (0, _ignoreWalk.default)({
@@ -557,9 +566,6 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
557
566
  return 1;
558
567
  }
559
568
 
560
- // Generate Celery Docker artifacts from python/ if present
561
- (0, _pythonCeleryGen.generateCeleryArtifacts)(curDir);
562
-
563
569
  // Process Docker images and inject image.json into the ZIP
564
570
  let dockerVersion = json.version;
565
571
  if (debug) {
@@ -25,7 +25,10 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
25
25
 
26
26
  const execAsync = (0, _util.promisify)(_child_process.exec);
27
27
  const execFileAsync = (0, _util.promisify)(_child_process.execFile);
28
- const testInvocationTimeout = 3600000;
28
+ // Whole-run cap for the Puppeteer pass; when it fires, results collected so far
29
+ // are discarded (they live in the page until a pass completes). CI runners on
30
+ // loaded agents can exceed the 1h default — override via env.
31
+ const testInvocationTimeout = parseInt(process.env['GROK_TEST_INVOCATION_TIMEOUT_MS'] ?? '', 10) || 3600000;
29
32
  const availableCommandOptions = ['host', 'package', 'csv', 'gui', 'catchUnhandled', 'platform', 'core', 'report', 'skip-build', 'skip-publish', 'path', 'record', 'verbose', 'benchmark', 'category', 'test', 'stress-test', 'link', 'tag', 'ci-cd', 'debug', 'no-retry', 'dartium', 'f', 'params', 'logfailed', 'skip-playwright', 'skip-puppeteer'];
30
33
  const curDir = process.cwd();
31
34
 
package/bin/grok.js CHANGED
@@ -3,6 +3,10 @@ const argv = require('minimist')(process.argv.slice(2), {
3
3
  alias: {k: 'key', h: 'help', r: 'recursive'},
4
4
  boolean: ['dartium'],
5
5
  });
6
+ // minimist maps `--no-retry` to `{retry: false}`, so the `args['no-retry']` checks in
7
+ // test.ts / playwright-runner.ts never fired and `--no-retry` was silently ignored
8
+ // (Playwright kept retrying failed specs). Normalize back to the flag the commands read.
9
+ if (argv.retry === false) argv['no-retry'] = true;
6
10
  const help = require('./commands/help').help;
7
11
  const runAllCommand = require('./utils/utils').runAll;
8
12
 
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.generateQueueArtifacts = generateQueueArtifacts;
8
+ var _fs = _interopRequireDefault(require("fs"));
9
+ var _path = _interopRequireDefault(require("path"));
10
+ var color = _interopRequireWildcard(require("./color-utils"));
11
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
+ // Matches `//meta.queue: true` (or its alias `//meta.server: true`) in package.ts —
13
+ // functions executed as celery tasks in the Node worker
14
+ // (see help/develop/how-to/packages/js-server-functions.md).
15
+ const queueTrueRegex = /^\s*\/\/\s*meta\.(queue|server)\s*:\s*true\s*$/m;
16
+ const QUEUE_DIR = 'queue';
17
+ const TEMPLATE_NODE_DOCKER = `FROM datagrok/celery_node_worker:bleeding-edge
18
+ EXPOSE 8000
19
+ `;
20
+
21
+ /** Generates dockerfiles/queue/ for packages with \`meta.queue: true\` functions so the
22
+ * standard docker build/push/image.json flow covers the auto-created Node worker
23
+ * container (mirror of generateCeleryArtifacts for python/). The worker fetches the
24
+ * package bundle at runtime, so the image is just the stock worker base. */
25
+ function generateQueueArtifacts(packageDir) {
26
+ const candidates = ['package.ts', 'package.g.ts'].map(f => _path.default.join(packageDir, 'src', f)).filter(f => _fs.default.existsSync(f));
27
+ const hasQueueFuncs = candidates.some(f => queueTrueRegex.test(_fs.default.readFileSync(f, 'utf-8')));
28
+ if (!hasQueueFuncs) return false;
29
+ const dockerfilesDir = _path.default.join(packageDir, 'dockerfiles', QUEUE_DIR);
30
+ const dockerfilePath = _path.default.join(dockerfilesDir, 'Dockerfile');
31
+ _fs.default.mkdirSync(dockerfilesDir, {
32
+ recursive: true
33
+ });
34
+ if (!_fs.default.existsSync(dockerfilePath)) _fs.default.writeFileSync(dockerfilePath, TEMPLATE_NODE_DOCKER);
35
+
36
+ // Resource config lives in the reserved queue/ package folder
37
+ const containerJsonSrc = _path.default.join(packageDir, QUEUE_DIR, 'container.json');
38
+ const containerJsonDest = _path.default.join(dockerfilesDir, 'container.json');
39
+ if (_fs.default.existsSync(containerJsonSrc) && !_fs.default.existsSync(containerJsonDest)) _fs.default.copyFileSync(containerJsonSrc, containerJsonDest);
40
+ color.log(`Generated Node worker Docker artifacts in dockerfiles/${QUEUE_DIR}/`);
41
+ return true;
42
+ }
@@ -46,17 +46,44 @@ const defaultLaunchParameters = exports.defaultLaunchParameters = {
46
46
  protocolTimeout: 0
47
47
  };
48
48
  async function getToken(url, key) {
49
- let response;
50
- try {
51
- response = await fetch(`${url}/users/login/dev/${key}`, {
52
- method: 'POST'
53
- });
54
- } catch (error) {
55
- if (utils.isConnectivityError(error)) color.error(`Server is possibly offline: ${url}`);
56
- throw error;
49
+ // Retry transient failures: on a freshly-provisioned CI stack the datlas behind nginx may not
50
+ // be serving /api as JSON yet, so the dev-key exchange briefly returns an HTML error page
51
+ // (`Unexpected token '<'` from response.json()) or the node is momentarily unreachable. These
52
+ // are stack-readiness races, not bad credentials — re-poll for ~45s before giving up. A real
53
+ // auth failure (valid JSON with isSuccess=false) is returned immediately, no retry.
54
+ const maxAttempts = 15;
55
+ const delayMs = 3000;
56
+ let lastError;
57
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
58
+ try {
59
+ const response = await fetch(`${url}/users/login/dev/${key}`, {
60
+ method: 'POST'
61
+ });
62
+ const text = await response.text();
63
+ let json;
64
+ try {
65
+ json = JSON.parse(text);
66
+ } catch {
67
+ // Non-JSON body (HTML error page) => stack not ready yet; retry.
68
+ lastError = new Error(`non-JSON response from ${url}/users/login/dev (status ${response.status})`);
69
+ if (attempt < maxAttempts) {
70
+ color.warn(`Playwright: token exchange not ready (attempt ${attempt}/${maxAttempts}), retrying...`);
71
+ await new Promise(r => setTimeout(r, delayMs));
72
+ continue;
73
+ }
74
+ throw lastError;
75
+ }
76
+ if (json.isSuccess == true) return json.token;
77
+ // Valid JSON but login rejected => definitive failure, do not retry.
78
+ throw new Error('Unable to login to server. Check your dev key');
79
+ } catch (error) {
80
+ if (error?.message === 'Unable to login to server. Check your dev key') throw error;
81
+ lastError = error;
82
+ if (utils.isConnectivityError(error)) color.warn(`Playwright: server not reachable yet (attempt ${attempt}/${maxAttempts}): ${url}`);
83
+ if (attempt < maxAttempts) await new Promise(r => setTimeout(r, delayMs));
84
+ }
57
85
  }
58
- const json = await response.json();
59
- if (json.isSuccess == true) return json.token;else throw new Error('Unable to login to server. Check your dev key');
86
+ throw lastError ?? new Error(`Unable to exchange dev key for token at ${url}`);
60
87
  }
61
88
  async function isPackageOnServer(hostKey, packageName) {
62
89
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.4.5",
3
+ "version": "6.4.7",
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": {
@@ -4,4 +4,4 @@
4
4
  //sample: cars.csv
5
5
  //input: dataframe table [Data table]
6
6
  //output: int count [Number of cells in table]
7
- const count = table.dim()[0] * table.dim()[1];
7
+ const count = table.rowCount * table.columns.length;