datagrok-tools 6.4.4 → 6.4.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 +9 -0
- package/bin/commands/publish.js +4 -0
- package/bin/commands/test.js +4 -1
- package/bin/grok.js +4 -0
- package/bin/utils/queue-worker-gen.js +41 -0
- package/bin/utils/test-utils.js +56 -17
- package/package.json +1 -1
- package/script-template/node.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
+
## 6.4.6 (2026-07-22)
|
|
4
|
+
|
|
5
|
+
* `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.
|
|
6
|
+
* `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.
|
|
7
|
+
|
|
8
|
+
## 6.4.5 (2026-06-23)
|
|
9
|
+
|
|
10
|
+
* `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.
|
|
11
|
+
|
|
3
12
|
## 6.4.4 (2026-06-22)
|
|
4
13
|
|
|
5
14
|
* `grok publish` — fixed every publish failing with a silent `exit 1` after a successful upload: a stray `fs.unlinkSync('zip')` threw `ENOENT` (the archive is streamed in-memory, no `zip` file is ever written), and the surrounding `catch` only logged under `--verbose`. The errant unlink is removed and publish errors are now always surfaced.
|
package/bin/commands/publish.js
CHANGED
|
@@ -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
|
|
|
@@ -560,6 +561,9 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
|
|
|
560
561
|
// Generate Celery Docker artifacts from python/ if present
|
|
561
562
|
(0, _pythonCeleryGen.generateCeleryArtifacts)(curDir);
|
|
562
563
|
|
|
564
|
+
// Generate the Node worker container for meta.queue JS functions if present
|
|
565
|
+
(0, _queueWorkerGen.generateQueueArtifacts)(curDir);
|
|
566
|
+
|
|
563
567
|
// Process Docker images and inject image.json into the ZIP
|
|
564
568
|
let dockerVersion = json.version;
|
|
565
569
|
if (debug) {
|
package/bin/commands/test.js
CHANGED
|
@@ -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
|
-
|
|
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,41 @@
|
|
|
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` in package.ts — functions executed as celery tasks
|
|
13
|
+
// in the Node worker (see help/develop/how-to/packages/js-server-functions.md).
|
|
14
|
+
const queueTrueRegex = /^\s*\/\/\s*meta\.queue\s*:\s*true\s*$/m;
|
|
15
|
+
const QUEUE_DIR = 'queue';
|
|
16
|
+
const TEMPLATE_NODE_DOCKER = `FROM datagrok/celery_node_worker:bleeding-edge
|
|
17
|
+
EXPOSE 8000
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
/** Generates dockerfiles/queue/ for packages with \`meta.queue: true\` functions so the
|
|
21
|
+
* standard docker build/push/image.json flow covers the auto-created Node worker
|
|
22
|
+
* container (mirror of generateCeleryArtifacts for python/). The worker fetches the
|
|
23
|
+
* package bundle at runtime, so the image is just the stock worker base. */
|
|
24
|
+
function generateQueueArtifacts(packageDir) {
|
|
25
|
+
const candidates = ['package.ts', 'package.g.ts'].map(f => _path.default.join(packageDir, 'src', f)).filter(f => _fs.default.existsSync(f));
|
|
26
|
+
const hasQueueFuncs = candidates.some(f => queueTrueRegex.test(_fs.default.readFileSync(f, 'utf-8')));
|
|
27
|
+
if (!hasQueueFuncs) return false;
|
|
28
|
+
const dockerfilesDir = _path.default.join(packageDir, 'dockerfiles', QUEUE_DIR);
|
|
29
|
+
const dockerfilePath = _path.default.join(dockerfilesDir, 'Dockerfile');
|
|
30
|
+
_fs.default.mkdirSync(dockerfilesDir, {
|
|
31
|
+
recursive: true
|
|
32
|
+
});
|
|
33
|
+
if (!_fs.default.existsSync(dockerfilePath)) _fs.default.writeFileSync(dockerfilePath, TEMPLATE_NODE_DOCKER);
|
|
34
|
+
|
|
35
|
+
// Resource config lives in the reserved queue/ package folder
|
|
36
|
+
const containerJsonSrc = _path.default.join(packageDir, QUEUE_DIR, 'container.json');
|
|
37
|
+
const containerJsonDest = _path.default.join(dockerfilesDir, 'container.json');
|
|
38
|
+
if (_fs.default.existsSync(containerJsonSrc) && !_fs.default.existsSync(containerJsonDest)) _fs.default.copyFileSync(containerJsonSrc, containerJsonDest);
|
|
39
|
+
color.log(`Generated Node worker Docker artifacts in dockerfiles/${QUEUE_DIR}/`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -46,17 +46,44 @@ const defaultLaunchParameters = exports.defaultLaunchParameters = {
|
|
|
46
46
|
protocolTimeout: 0
|
|
47
47
|
};
|
|
48
48
|
async function getToken(url, key) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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 {
|
|
@@ -256,9 +283,14 @@ async function loadTestsList(packages, core = false, record = false) {
|
|
|
256
283
|
const suffix = process.env.BACKUP_SIZE && process.env.WORKER_ID && process.env.TOTAL_WORKERS ? `_${process.env.BACKUP_SIZE}_${process.env.WORKER_ID}_${process.env.TOTAL_WORKERS}` : '';
|
|
257
284
|
const logsDir = `./load-test-console-output${suffix}.log`;
|
|
258
285
|
const recordDir = `./load-test-record${suffix}.mp4`;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
try {
|
|
287
|
+
recorder = await page.screencast({
|
|
288
|
+
path: recordDir
|
|
289
|
+
});
|
|
290
|
+
} catch (e) {
|
|
291
|
+
recorder = null;
|
|
292
|
+
color.warn(`Screen recording disabled: ${e?.message || e}`);
|
|
293
|
+
}
|
|
262
294
|
await page.exposeFunction('addLogsToFile', addLogsToFile);
|
|
263
295
|
_fs.default.writeFileSync(logsDir, ``);
|
|
264
296
|
page.on('console', msg => {
|
|
@@ -636,10 +668,17 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
|
|
|
636
668
|
const logsDir = `./test-console-output-${currentBrowserNum}.log`;
|
|
637
669
|
const recordDir = `./test-record-${currentBrowserNum}.mp4`;
|
|
638
670
|
if (browserOptions.record && !existingBrowserSession) {
|
|
639
|
-
// Only set up recording on initial browser creation, not on retry
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
671
|
+
// Only set up recording on initial browser creation, not on retry.
|
|
672
|
+
// Recording is an optional diagnostic — if it can't start (e.g. ffmpeg is
|
|
673
|
+
// missing on the runner), warn and keep testing instead of failing the pass.
|
|
674
|
+
try {
|
|
675
|
+
recorder = await page.screencast({
|
|
676
|
+
path: recordDir
|
|
677
|
+
});
|
|
678
|
+
} catch (e) {
|
|
679
|
+
recorder = null;
|
|
680
|
+
color.warn(`Screen recording disabled: ${e?.message || e}`);
|
|
681
|
+
}
|
|
643
682
|
await page.exposeFunction('addLogsToFile', addLogsToFile);
|
|
644
683
|
_fs.default.writeFileSync(logsDir, ``);
|
|
645
684
|
page.on('console', msg => {
|
package/package.json
CHANGED
package/script-template/node.js
CHANGED