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.
- package/.devcontainer/entrypoint.sh +0 -0
- package/bin/commands/help.js +2 -1
- package/bin/commands/publish.js +179 -4
- package/bin/commands/test.js +5 -7
- package/bin/utils/test-utils.js +31 -13
- package/bin/utils/utils.js +1 -1
- package/config-template.yaml +2 -0
- package/package.json +1 -1
|
File without changes
|
package/bin/commands/help.js
CHANGED
|
@@ -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
|
|
package/bin/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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;
|
package/bin/commands/test.js
CHANGED
|
@@ -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('
|
|
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
|
-
|
|
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,
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
369
|
-
console.log('Skipped
|
|
370
|
-
console.log('Failed
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/bin/utils/utils.js
CHANGED
|
@@ -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) {
|
package/config-template.yaml
CHANGED
|
@@ -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