datagrok-tools 6.1.3 → 6.1.5

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.
@@ -52,7 +52,7 @@ function discoverDockerfiles(packageName, version, debug) {
52
52
  if (!_fs.default.existsSync(dockerfilePath)) continue;
53
53
  const cleanName = utils.removeScope(packageName).toLowerCase();
54
54
  const imageName = `${cleanName}-${entry.name.toLowerCase()}`;
55
- const imageTag = debug ? version : `${version}.X`;
55
+ const imageTag = version;
56
56
  results.push({
57
57
  imageName,
58
58
  imageTag,
@@ -68,17 +68,26 @@ function dockerCommand(args) {
68
68
  stdio: ['pipe', 'pipe', 'pipe']
69
69
  }).trim();
70
70
  }
71
- function localImageExists(fullName) {
71
+ function localImageExists(fullName, checkPlatform = true) {
72
72
  try {
73
73
  const output = dockerCommand(`images --format "{{.Repository}}:{{.Tag}}"`);
74
- return output.split('\n').some(line => line.trim() === fullName);
74
+ if (!output.split('\n').some(line => line.trim() === fullName)) return false;
75
+ if (checkPlatform) {
76
+ // Verify the image is linux/amd64 — ARM images won't run on Datagrok servers
77
+ const inspect = dockerCommand(`inspect --format "{{.Os}}/{{.Architecture}}" ${fullName}`);
78
+ if (inspect.trim() !== 'linux/amd64') {
79
+ color.warn(`Local image ${fullName} is ${inspect.trim()}, not linux/amd64 — treating as not found`);
80
+ return false;
81
+ }
82
+ }
83
+ return true;
75
84
  } catch {
76
85
  return false;
77
86
  }
78
87
  }
79
88
  function dockerLogin(registry, devKey) {
80
89
  try {
81
- dockerCommand(`login ${registry} -u ${devKey} -p ${devKey}`);
90
+ dockerCommand(`login ${registry} -u any -p ${devKey}`);
82
91
  return true;
83
92
  } catch (e) {
84
93
  color.warn(`Docker login to ${registry} failed: ${e.message || e}`);
@@ -106,10 +115,22 @@ function dockerPush(image) {
106
115
  return false;
107
116
  }
108
117
  }
109
- function dockerBuild(imageName, dockerfilePath, context) {
118
+ function isLocalhostRegistry(registry) {
119
+ return registry.startsWith('localhost') || registry.startsWith('127.0.0.1');
120
+ }
121
+ function dockerRemove(imageName) {
122
+ try {
123
+ dockerCommand(`rmi ${imageName}`);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ function dockerBuild(imageName, dockerfilePath, context, usePlatform = true) {
110
130
  try {
111
- color.log(`Building Docker image ${imageName}...`);
112
- execSync(`docker build -t ${imageName} -f ${dockerfilePath} ${context}`, {
131
+ const platformFlag = usePlatform ? '--platform linux/amd64 ' : '';
132
+ color.log(`Building Docker image ${imageName}${usePlatform ? ' (platform: linux/amd64)' : ''}...`);
133
+ execSync(`docker build ${platformFlag}-t ${imageName} -f ${dockerfilePath} ${context}`, {
113
134
  encoding: 'utf-8',
114
135
  stdio: 'inherit'
115
136
  });
@@ -125,13 +146,20 @@ function calculateFolderHash(dirPath) {
125
146
  entries.sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
126
147
  for (const entry of entries) {
127
148
  if (entry.isDir) {
149
+ color.log(` Hash entry: dir:${entry.relPath}:`);
128
150
  hash.update(`dir:${entry.relPath}:`);
129
151
  } else {
152
+ // Normalize CRLF to LF to match server-side storage
153
+ const raw = _fs.default.readFileSync(entry.fullPath);
154
+ const content = raw.includes(0x0d) && !raw.includes(0x00) ? Buffer.from(raw.toString('utf8').replace(/\r\n/g, '\n'), 'utf8') : raw;
155
+ color.log(` Hash entry: file:${entry.relPath}: (${content.length} bytes)`);
130
156
  hash.update(`file:${entry.relPath}:`);
131
- hash.update(_fs.default.readFileSync(entry.fullPath));
157
+ hash.update(content);
132
158
  }
133
159
  }
134
- return hash.digest('hex');
160
+ const result = hash.digest('hex');
161
+ color.log(` Folder hash: ${result}`);
162
+ return result;
135
163
  }
136
164
  function listRecursive(basePath, rel) {
137
165
  const results = [];
@@ -226,62 +254,83 @@ async function processDockerImages(packageName, version, registry, devKey, host,
226
254
  if (dockerImages.length === 0) return;
227
255
  color.log(`Found ${dockerImages.length} Dockerfile(s)`);
228
256
  if (registry) dockerLogin(registry, devKey);
257
+ const needsPlatform = !!registry && !isLocalhostRegistry(registry);
229
258
  for (const img of dockerImages) {
230
259
  color.info(`Processing docker image ${img.fullLocalName}...`);
231
260
  let result;
232
261
  const dockerfileDir = _path.default.join('dockerfiles', img.dirName);
233
262
  const dockerfilePath = _path.default.join(dockerfileDir, 'Dockerfile');
234
263
  const contentHash = calculateFolderHash(_path.default.join(curDir, dockerfileDir));
235
- if (rebuildDocker) {
236
- if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
237
- result = pushImage(img, registry);
264
+
265
+ // In release mode, append short content hash to tag for cache-busting
266
+ const shortHash = contentHash.substring(0, 8);
267
+ const registryTag = debug ? img.imageTag : `${img.imageTag}.${shortHash}`;
268
+ const remoteFullName = `${img.imageName}:${registryTag}`;
269
+ const buildAndPush = () => {
270
+ if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir, needsPlatform)) {
271
+ if (registry) {
272
+ const remoteTag = `${registry}/datagrok/${remoteFullName}`;
273
+ dockerTag(img.fullLocalName, remoteTag);
274
+ }
238
275
  color.success(`Built and tagged ${img.fullLocalName}`);
239
- } else {
240
- result = await fallbackImage(img, host, devKey, registry, version, contentHash);
276
+ return pushImage(img.imageName, registryTag, registry);
241
277
  }
242
- } else if (localImageExists(img.fullLocalName)) {
243
- color.success(`Found local image ${img.fullLocalName}`);
244
- result = pushImage(img, registry);
278
+ return null;
279
+ };
280
+ if (rebuildDocker) {
281
+ // Delete old images before rebuilding
282
+ dockerRemove(img.fullLocalName);
283
+ if (registry) dockerRemove(`${registry}/datagrok/${remoteFullName}`);
284
+ result = buildAndPush() ?? (await fallbackImage(img, host, devKey, registry, version, contentHash));
245
285
  } else {
246
- color.warn(`Local image not found. Expected: ${img.fullLocalName}`);
247
- color.log(` Build it with: docker build -t ${img.fullLocalName} -f ${dockerfilePath} ${dockerfileDir}`);
248
- const fallback = await fallbackImage(img, host, devKey, registry, version, contentHash);
249
- if (fallback.serverError) {
250
- color.error(`Cannot resolve fallback: ${fallback.serverError}`);
251
- result = {
252
- image: null,
253
- fallback: true,
254
- requestedVersion: img.imageTag
255
- };
256
- } else if (fallback.image && fallback.hashMatch === true) {
257
- result = fallback;
258
- color.success(`Falling back to ${fallback.image} (dockerfile unchanged)`);
259
- } else if (fallback.image && fallback.hashMatch === false && !skipDockerRebuild) {
260
- color.warn(`Dockerfile folder has changed. Rebuilding image...`);
261
- if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
262
- result = pushImage(img, registry);
263
- color.success(`Built and tagged ${img.fullLocalName}`);
264
- } else {
265
- result = {
266
- image: fallback.image,
267
- fallback: true,
268
- requestedVersion: img.imageTag
269
- };
270
- color.warn(`Build failed. Falling back to ${fallback.image} (hash mismatch)`);
286
+ // Look for registry-qualified image first, then unqualified
287
+ let foundLocalName = null;
288
+ if (registry) {
289
+ const registryQualified = `${registry}/datagrok/${remoteFullName}`;
290
+ if (localImageExists(registryQualified, needsPlatform)) foundLocalName = registryQualified;
291
+ }
292
+ if (!foundLocalName && localImageExists(img.fullLocalName, needsPlatform)) foundLocalName = img.fullLocalName;
293
+ if (foundLocalName) {
294
+ color.success(`Found local image ${foundLocalName}`);
295
+ if (registry) {
296
+ const remoteTag = `${registry}/datagrok/${remoteFullName}`;
297
+ if (foundLocalName !== remoteTag) dockerTag(foundLocalName, remoteTag);
271
298
  }
299
+ result = pushImage(img.imageName, registryTag, registry);
272
300
  } else {
273
- // No fallback and no local image — must build
274
- color.warn(`No fallback available. Building ${img.fullLocalName}...`);
275
- if (dockerBuild(img.fullLocalName, dockerfilePath, dockerfileDir)) {
276
- result = pushImage(img, registry);
277
- color.success(`Built and tagged ${img.fullLocalName}`);
278
- } else {
301
+ color.warn(`Local image not found. Expected: ${img.fullLocalName}`);
302
+ color.log(` Build it with: docker build -t ${img.fullLocalName} -f ${dockerfilePath} ${dockerfileDir}`);
303
+ const fallback = await fallbackImage(img, host, devKey, registry, version, contentHash);
304
+ if (fallback.serverError) {
305
+ color.error(`Cannot resolve fallback: ${fallback.serverError}`);
279
306
  result = {
280
307
  image: null,
281
308
  fallback: true,
282
- requestedVersion: img.imageTag
309
+ requestedVersion: registryTag
310
+ };
311
+ } else if (fallback.image && fallback.hashMatch === true) {
312
+ result = fallback;
313
+ color.success(`Falling back to ${fallback.image} (dockerfile unchanged)`);
314
+ } else if (fallback.image && fallback.hashMatch === false && !skipDockerRebuild) {
315
+ color.warn(`Dockerfile folder has changed. Rebuilding image...`);
316
+ result = buildAndPush() ?? {
317
+ image: fallback.image,
318
+ fallback: true,
319
+ requestedVersion: registryTag
283
320
  };
284
- color.error(`Failed to build ${img.fullLocalName}. No container will be available.`);
321
+ if (!result || result.fallback) color.warn(`Build failed. Falling back to ${fallback.image} (hash mismatch)`);
322
+ } else {
323
+ // No fallback and no local image — must build
324
+ color.warn(`No fallback available. Building ${img.fullLocalName}...`);
325
+ const built = buildAndPush();
326
+ if (built) result = built;else {
327
+ result = {
328
+ image: null,
329
+ fallback: true,
330
+ requestedVersion: registryTag
331
+ };
332
+ color.error(`Failed to build ${img.fullLocalName}. No container will be available.`);
333
+ }
285
334
  }
286
335
  }
287
336
  }
@@ -293,11 +342,11 @@ async function processDockerImages(packageName, version, registry, devKey, host,
293
342
  color.log(`Added ${imageJsonPath}`);
294
343
  }
295
344
  }
296
- function pushImage(img, registry) {
297
- const canonicalImage = `datagrok/${img.fullLocalName}`;
345
+ function pushImage(imageName, tag, registry) {
346
+ const canonicalImage = `datagrok/${imageName}:${tag}`;
298
347
  if (registry) {
299
348
  const remoteTag = `${registry}/${canonicalImage}`;
300
- dockerTag(img.fullLocalName, remoteTag);
349
+ // Image should already be tagged from build or retag step
301
350
  if (dockerPush(remoteTag)) return {
302
351
  image: canonicalImage,
303
352
  fallback: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datagrok-tools",
3
- "version": "6.1.3",
3
+ "version": "6.1.5",
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": {