datagrok-tools 6.0.8 → 6.1.0

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.
File without changes
@@ -156,13 +156,14 @@ Uploads a package
156
156
  Checks for errors before publishing — the package won't be published if there are any.
157
157
 
158
158
  Options:
159
- [--all] [--refresh] [--link] [--build] [--release] [--skip-check] [-v | --verbose]
159
+ [--all] [--refresh] [--link] [--build] [--release] [--rebuild-docker] [--skip-check] [-v | --verbose]
160
160
 
161
161
  --all Publish all available packages (run in packages directory)
162
162
  --refresh Publish all available already loaded packages (run in packages directory)
163
163
  --link Link the package to local packages
164
164
  --build Builds the package
165
165
  --release Publish package as release version
166
+ --rebuild-docker Force rebuild Docker images locally before pushing to registry
166
167
  --skip-check Skip check stage
167
168
  --verbose Print detailed output
168
169
 
@@ -23,7 +23,8 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
23
23
  // @ts-ignore
24
24
 
25
25
  const {
26
- exec
26
+ exec,
27
+ execSync
27
28
  } = require('child_process');
28
29
  const grokDir = _path.default.join(_os.default.homedir(), '.grok');
29
30
  const confPath = _path.default.join(grokDir, 'config.yaml');
@@ -35,7 +36,171 @@ let curDir = process.cwd();
35
36
  const packDir = _path.default.join(curDir, 'package.json');
36
37
  const packageFiles = ['src/package.ts', 'src/detectors.ts', 'src/package.js', 'src/detectors.js', 'src/package-test.ts', 'src/package-test.js', 'package.js', 'detectors.js'];
37
38
  let config;
38
- async function processPackage(debug, rebuild, host, devKey, packageName, dropDb, suffix, hostAlias) {
39
+ const BLEEDING_EDGE_TAG = 'bleeding-edge';
40
+ function discoverDockerfiles(packageName, version) {
41
+ const dockerfilesDir = _path.default.join(curDir, 'dockerfiles');
42
+ if (!_fs.default.existsSync(dockerfilesDir)) return [];
43
+ const results = [];
44
+ const entries = _fs.default.readdirSync(dockerfilesDir, {
45
+ withFileTypes: true
46
+ });
47
+ for (const entry of entries) {
48
+ if (!entry.isDirectory()) continue;
49
+ const dockerfilePath = _path.default.join(dockerfilesDir, entry.name, 'Dockerfile');
50
+ if (!_fs.default.existsSync(dockerfilePath)) continue;
51
+ const cleanName = utils.removeScope(packageName).toLowerCase();
52
+ const imageName = `${cleanName}-${entry.name.toLowerCase()}`;
53
+ results.push({
54
+ imageName,
55
+ imageTag: `${version}.X`,
56
+ dirName: entry.name,
57
+ fullLocalName: `${imageName}:${version}.X`
58
+ });
59
+ }
60
+ return results;
61
+ }
62
+ function dockerCommand(args) {
63
+ return execSync(`docker ${args}`, {
64
+ encoding: 'utf-8',
65
+ stdio: ['pipe', 'pipe', 'pipe']
66
+ }).trim();
67
+ }
68
+ function localImageExists(fullName) {
69
+ try {
70
+ const output = dockerCommand(`images --format "{{.Repository}}:{{.Tag}}"`);
71
+ return output.split('\n').some(line => line.trim() === fullName);
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+ function dockerLogin(registry, devKey) {
77
+ try {
78
+ dockerCommand(`login ${registry} -u ${devKey} -p ${devKey}`);
79
+ return true;
80
+ } catch (e) {
81
+ color.warn(`Docker login to ${registry} failed: ${e.message || e}`);
82
+ return false;
83
+ }
84
+ }
85
+ function dockerTag(source, target) {
86
+ try {
87
+ dockerCommand(`tag ${source} ${target}`);
88
+ return true;
89
+ } catch (e) {
90
+ color.error(`Failed to tag ${source} as ${target}: ${e.message || e}`);
91
+ return false;
92
+ }
93
+ }
94
+ function dockerPush(image) {
95
+ try {
96
+ dockerCommand(`push ${image}`);
97
+ return true;
98
+ } catch (e) {
99
+ color.error(`Failed to push ${image}: ${e.message || e}`);
100
+ return false;
101
+ }
102
+ }
103
+ function dockerBuild(imageName, dockerfilePath, context) {
104
+ try {
105
+ color.log(`Building Docker image ${imageName}...`);
106
+ execSync(`docker build -t ${imageName} -f ${dockerfilePath} ${context}`, {
107
+ encoding: 'utf-8',
108
+ stdio: ['pipe', 'pipe', 'pipe']
109
+ });
110
+ return true;
111
+ } catch (e) {
112
+ color.error(`Failed to build ${imageName}: ${e.message || e}`);
113
+ return false;
114
+ }
115
+ }
116
+ async function resolveLatestCompatible(host, devKey, dockerName) {
117
+ try {
118
+ // Login with dev key to get a session token
119
+ const loginResp = await (0, _nodeFetch.default)(`${host}/users/login/dev/${devKey}`, {
120
+ method: 'POST'
121
+ });
122
+ if (loginResp.status !== 200) return null;
123
+ const loginData = await loginResp.json();
124
+ const token = loginData.token;
125
+ const url = `${host}/docker/images/${encodeURIComponent(dockerName)}/latest-compatible`;
126
+ const resp = await (0, _nodeFetch.default)(url, {
127
+ headers: {
128
+ 'Authorization': token
129
+ }
130
+ });
131
+ if (resp.status === 200) return await resp.json();
132
+ return null;
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+ async function processDockerImages(packageName, version, registry, devKey, host, rebuildDocker, zip, localTimestamps) {
138
+ const dockerImages = discoverDockerfiles(packageName, version);
139
+ if (dockerImages.length === 0) return;
140
+ color.log(`Found ${dockerImages.length} Dockerfile(s)`);
141
+ if (registry) dockerLogin(registry, devKey);
142
+ for (const img of dockerImages) {
143
+ let result;
144
+ if (rebuildDocker) {
145
+ const dockerfileDir = _path.default.join('dockerfiles', img.dirName);
146
+ const dockerfilePath = _path.default.join(dockerfileDir, 'Dockerfile');
147
+ if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
148
+ result = pushImage(img, registry, version);
149
+ color.success(`Built and tagged ${img.fullLocalName}`);
150
+ } else {
151
+ result = await fallbackImage(img, host, devKey, registry);
152
+ }
153
+ } else if (localImageExists(img.fullLocalName)) {
154
+ result = pushImage(img, registry, version);
155
+ color.success(`Found local image ${img.fullLocalName}`);
156
+ } else {
157
+ result = await fallbackImage(img, host, devKey, registry);
158
+ color.warn(`Local image not found. Expected: ${img.fullLocalName}` + (result.image ? `. Falling back to ${result.image}` : ''));
159
+ color.log(` Build it with: docker build -t ${img.fullLocalName} -f dockerfiles/${img.dirName}/Dockerfile dockerfiles/${img.dirName}`);
160
+ }
161
+ const imageJsonPath = _path.default.join('dockerfiles', img.dirName, 'image.json');
162
+ zip.append(JSON.stringify(result, null, 2), {
163
+ name: imageJsonPath
164
+ });
165
+ localTimestamps[imageJsonPath.replace(/\\/g, '/')] = new Date().toUTCString();
166
+ color.log(`Added ${imageJsonPath}`);
167
+ }
168
+ }
169
+ function pushImage(img, registry, version) {
170
+ if (registry) {
171
+ const remoteTag = `${registry}/${img.imageName}:${version}`;
172
+ dockerTag(img.fullLocalName, remoteTag);
173
+ if (dockerPush(remoteTag)) return {
174
+ image: remoteTag,
175
+ fallback: false
176
+ };
177
+ color.warn(`Push failed, image tagged locally only: ${remoteTag}`);
178
+ return {
179
+ image: remoteTag,
180
+ fallback: false
181
+ };
182
+ }
183
+ color.warn('No registry configured. Image tagged locally only.');
184
+ return {
185
+ image: img.fullLocalName,
186
+ fallback: false
187
+ };
188
+ }
189
+ async function fallbackImage(img, host, devKey, registry) {
190
+ const latest = await resolveLatestCompatible(host, devKey, img.imageName);
191
+ if (latest) return {
192
+ image: latest.image,
193
+ fallback: true,
194
+ requestedVersion: img.imageTag
195
+ };
196
+ color.warn('No previous version available. Container will not be available until an image is built.');
197
+ return {
198
+ image: null,
199
+ fallback: true,
200
+ requestedVersion: img.imageTag
201
+ };
202
+ }
203
+ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb, suffix, hostAlias, registry, rebuildDocker) {
39
204
  // Get the server timestamps
40
205
  let timestamps = {};
41
206
  let url = `${host}/packages/dev/${devKey}/${packageName}`;
@@ -147,6 +312,10 @@ async function processPackage(debug, rebuild, host, devKey, packageName, dropDb,
147
312
  errs.forEach(e => color.error(e));
148
313
  return 1;
149
314
  }
315
+
316
+ // Process Docker images and inject image.json into the ZIP
317
+ const packageVersion = json.version;
318
+ await processDockerImages(packageName, packageVersion, registry, devKey, host, rebuildDocker ?? false, zip, localTimestamps);
150
319
  zip.append(JSON.stringify(localTimestamps), {
151
320
  name: 'timestamps.json'
152
321
  });
@@ -245,16 +414,22 @@ async function publishPackage(args) {
245
414
  if (nArgs === 2) host = args['_'][1];
246
415
  let key = '';
247
416
  let url = '';
417
+ let registry;
248
418
 
249
419
  // The host can be passed either as a URL or an alias
250
420
  try {
251
421
  url = new URL(host).href;
252
422
  if (url.endsWith('/')) url = url.slice(0, -1);
253
- if (url in urls) key = config['servers'][urls[url]]['key'];
423
+ if (url in urls) {
424
+ const alias = urls[url];
425
+ key = config['servers'][alias]['key'];
426
+ registry = config['servers'][alias]['registry'];
427
+ }
254
428
  } catch (error) {
255
429
  if (!(host in config.servers)) return color.error(`Unknown server alias. Please add it to ${confPath}`);
256
430
  url = config['servers'][host]['url'];
257
431
  key = config['servers'][host]['key'];
432
+ registry = config['servers'][host]['registry'];
258
433
  }
259
434
 
260
435
  // Update the developer key
@@ -285,7 +460,7 @@ async function publishPackage(args) {
285
460
  if (!args.suffix && stdout) args.suffix = stdout.toString().substring(0, 8);
286
461
  });
287
462
  await utils.delay(100);
288
- code = await processPackage(!args.release, Boolean(args.rebuild), url, key, packageName, args.dropDb ?? false, args.suffix, host);
463
+ code = await processPackage(!args.release, Boolean(args.rebuild), url, key, packageName, args.dropDb ?? false, args.suffix, host, registry, args['rebuild-docker']);
289
464
  } catch (error) {
290
465
  console.error(error);
291
466
  code = 1;
@@ -125,9 +125,7 @@ async function test(args) {
125
125
  try {
126
126
  await testUtils.loadPackages(packagesDir, packageName, args.host, args['skip-publish'], args['skip-build'], args.link);
127
127
  } catch (e) {
128
- console.error('\n');
129
- // @ts-ignore
130
- console.error(e.message);
128
+ console.error(e.message || 'Package build/publish failed with no output. Run with --verbose for details.');
131
129
  process.exit(1);
132
130
  }
133
131
  }
@@ -214,7 +212,7 @@ async function runTesting(args) {
214
212
  // Store browser session for potential reuse
215
213
  if (r.browserSession) browserSession = r.browserSession;
216
214
  if (r.error) {
217
- console.log(`\nexecution error:`);
215
+ color.error(`\nTest execution failed:`);
218
216
  console.log(r.error);
219
217
  // Close browser on error
220
218
  if (browserSession?.browser) await browserSession.browser.close();
@@ -365,9 +363,9 @@ function buildTestArgs(args) {
365
363
  return parts;
366
364
  }
367
365
  function parseTestOutput(stdout) {
368
- const passedMatch = stdout.match(/Passed amount:\s*(\d+)/);
369
- const failedMatch = stdout.match(/Failed amount:\s*(\d+)/);
370
- const skippedMatch = stdout.match(/Skipped amount:\s*(\d+)/);
366
+ const passedMatch = stdout.match(/Passed (?:amount|tests):\s*(\d+)/);
367
+ const failedMatch = stdout.match(/Failed (?:amount|tests):\s*(\d+)/);
368
+ const skippedMatch = stdout.match(/Skipped (?:amount|tests):\s*(\d+)/);
371
369
  if (!passedMatch && !failedMatch) return null;
372
370
  return {
373
371
  passed: passedMatch ? parseInt(passedMatch[1]) : 0,
@@ -217,10 +217,15 @@ const recorderConfig = exports.recorderConfig = {
217
217
  async function loadPackage(packageDir, dirName, hostString, skipPublish, skipBuild, linkPackage, release) {
218
218
  if (skipPublish != true) {
219
219
  process.stdout.write(`Building and publishing ${dirName}...`);
220
- await utils.runScript(`npm install`, packageDir);
221
- if (linkPackage) await utils.runScript(`grok link`, packageDir);
222
- if (skipBuild != true) await utils.runScript(`npm run build`, packageDir);
223
- await utils.runScript(`grok publish ${hostString}${release ? ' --release' : ''}`, packageDir);
220
+ try {
221
+ await utils.runScript(`npm install`, packageDir);
222
+ if (linkPackage) await utils.runScript(`grok link`, packageDir);
223
+ if (skipBuild != true) await utils.runScript(`npm run build`, packageDir);
224
+ await utils.runScript(`grok publish ${hostString}${release ? ' --release' : ''}`, packageDir);
225
+ } catch (e) {
226
+ process.stdout.write(' FAILED\n');
227
+ throw e;
228
+ }
224
229
  process.stdout.write(` success!\n`);
225
230
  }
226
231
  }
@@ -365,9 +370,9 @@ function printBrowsersResult(browserResult, verbose = false) {
365
370
  }
366
371
  }
367
372
  }
368
- console.log('Passed amount: ' + browserResult?.passedAmount);
369
- console.log('Skipped amount: ' + browserResult?.skippedAmount);
370
- console.log('Failed amount: ' + browserResult?.failedAmount);
373
+ console.log('Passed tests: ' + (browserResult?.passedAmount ?? 0));
374
+ console.log('Skipped tests: ' + (browserResult?.skippedAmount ?? 0));
375
+ console.log('Failed tests: ' + (browserResult?.failedAmount ?? 0));
371
376
  }
372
377
  if (browserResult.failed) {
373
378
  if (browserResult.verboseFailed === 'Package not found') color.fail('Tests not found');else color.fail('Tests failed.');
@@ -413,7 +418,9 @@ async function runTests(testParams) {
413
418
  if (testParams.params?.skipToTest) testCallParams.skipToTest = testParams.params.skipToTest;
414
419
  if (testParams.params?.returnOnFail) testCallParams.returnOnFail = testParams.params.returnOnFail;
415
420
  }
416
- const df = await window.grok.functions.call(testParams.package + ':test', Object.keys(testCallParams).length > 0 ? testCallParams : undefined);
421
+ const testFuncName = testParams.package + ':test';
422
+ const df = await window.grok.functions.call(testFuncName, Object.keys(testCallParams).length > 0 ? testCallParams : undefined);
423
+ if (df == null) throw new Error(`${testFuncName} returned null instead of a DataFrame. Check that the test function exists and returns a valid result.`);
417
424
  if (!df.getCol('flaking')) {
418
425
  const flakingCol = window.DG.Column.fromType(window.DG.COLUMN_TYPE.BOOL, 'flaking', df.rowCount);
419
426
  df.columns.add(flakingCol);
@@ -460,11 +467,23 @@ async function runTests(testParams) {
460
467
  // df: resultDF?.toJson()
461
468
  };
462
469
  } catch (e) {
463
- console.log(`DEBUG: runTests: IN CATCH: ERROR: ${e}`);
470
+ let stack = '';
471
+ try {
472
+ stack = await window.DG.Logger.translateStackTrace(e.stack);
473
+ } catch (_) {
474
+ stack = e.stack ?? '';
475
+ }
464
476
  return {
465
477
  failed: true,
466
478
  retrySupported: false,
467
- error: `${e}, ${await window.DG.Logger.translateStackTrace(e.stack)}`
479
+ verbosePassed: verbosePassed,
480
+ verboseSkipped: verboseSkipped,
481
+ verboseFailed: verboseFailed,
482
+ passedAmount: countPassed,
483
+ skippedAmount: countSkipped,
484
+ failedAmount: countFailed,
485
+ csv: '',
486
+ error: `${e}` + (stack ? `, ${stack}` : '')
468
487
  };
469
488
  }
470
489
 
@@ -813,12 +832,11 @@ async function runBrowser(testExecutionData, browserOptions, browsersId, testInv
813
832
  verbosePassed: '',
814
833
  verboseSkipped: '',
815
834
  verboseFailed: '',
816
- error: JSON.stringify(e),
835
+ error: (e?.message ?? '') + (e?.stack ? '\n' + e.stack : '') || JSON.stringify(e),
817
836
  passedAmount: 0,
818
837
  skippedAmount: 0,
819
838
  failedAmount: 1,
820
- csv: '',
821
- df: undefined
839
+ csv: ''
822
840
  });
823
841
  });
824
842
 
@@ -277,7 +277,7 @@ async function runScript(script, path, verbose = false) {
277
277
  if (stdout && verbose) console.log(`Output: ${stdout}`);
278
278
  } catch (error) {
279
279
  const output = [error.stdout, error.stderr].filter(Boolean).join('\n');
280
- throw new Error(output);
280
+ throw new Error(output || `Command failed: ${script} (exit code ${error.code ?? 'unknown'})`);
281
281
  }
282
282
  }
283
283
  function setHost(host, configFile, quiet = false) {
@@ -2,9 +2,11 @@ servers:
2
2
  dev:
3
3
  url: 'https://dev.datagrok.ai/api'
4
4
  key: ''
5
+ registry: 'registry.dev.datagrok.ai'
5
6
  public:
6
7
  url: 'https://public.datagrok.ai/api'
7
8
  key: ''
9
+ registry: 'registry.public.datagrok.ai'
8
10
  local:
9
11
  url: 'http://127.0.0.1:8080/api'
10
12
  key: ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.0.8",
3
+ "version": "6.1.0",
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": {