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.
- package/bin/commands/publish.js +102 -53
- package/package.json +1 -1
package/bin/commands/publish.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
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(
|
|
157
|
+
hash.update(content);
|
|
132
158
|
}
|
|
133
159
|
}
|
|
134
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
result = await fallbackImage(img, host, devKey, registry, version, contentHash);
|
|
276
|
+
return pushImage(img.imageName, registryTag, registry);
|
|
241
277
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
274
|
-
color.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
color.
|
|
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:
|
|
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.
|
|
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(
|
|
297
|
-
const canonicalImage = `datagrok/${
|
|
345
|
+
function pushImage(imageName, tag, registry) {
|
|
346
|
+
const canonicalImage = `datagrok/${imageName}:${tag}`;
|
|
298
347
|
if (registry) {
|
|
299
348
|
const remoteTag = `${registry}/${canonicalImage}`;
|
|
300
|
-
|
|
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