@agent8/deploy 1.3.1 → 1.4.1
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/agent8.js +131 -53
- package/package.json +5 -3
- package/.env +0 -0
package/bin/agent8.js
CHANGED
|
@@ -148,12 +148,18 @@ if (fs.existsSync(crossrampFilePath)) {
|
|
|
148
148
|
const filesToUpload = [];
|
|
149
149
|
const newHashes = {};
|
|
150
150
|
|
|
151
|
-
// Calculate file hash
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
// Calculate file hash by streaming — a large asset must not be buffered
|
|
152
|
+
// whole just to be hashed (RSS high-water was dominated by this).
|
|
153
|
+
const calculateFileHash = (filePath) =>
|
|
154
|
+
new Promise((resolve, reject) => {
|
|
155
|
+
const hash = crypto.createHash("md5");
|
|
156
|
+
fs.createReadStream(filePath)
|
|
157
|
+
.on("data", (chunk) => hash.update(chunk))
|
|
158
|
+
.on("end", () => resolve(hash.digest("hex")))
|
|
159
|
+
.on("error", reject);
|
|
160
|
+
});
|
|
156
161
|
|
|
162
|
+
const main = async () => {
|
|
157
163
|
// Check for server.js (priority: ./server.js, fallback: ./server/dist/server.js)
|
|
158
164
|
let serverFilePath = path.resolve(process.cwd(), "server.js");
|
|
159
165
|
if (!fs.existsSync(serverFilePath)) {
|
|
@@ -171,7 +177,7 @@ if (fs.existsSync(serverFilePath)) {
|
|
|
171
177
|
const serverContent = fs.readFileSync(serverFilePath, "utf8");
|
|
172
178
|
eval(`(function() { ${serverContent}; })`)();
|
|
173
179
|
|
|
174
|
-
const hash = calculateFileHash(serverFilePath);
|
|
180
|
+
const hash = await calculateFileHash(serverFilePath);
|
|
175
181
|
newHashes["server.js"] = hash;
|
|
176
182
|
|
|
177
183
|
if (prodMode || hash !== deployedHashes["server.js"]) {
|
|
@@ -179,6 +185,7 @@ if (fs.existsSync(serverFilePath)) {
|
|
|
179
185
|
filePath: serverFilePath,
|
|
180
186
|
uploadPath: "",
|
|
181
187
|
fileName: "server.js",
|
|
188
|
+
deployKey: "server.js",
|
|
182
189
|
});
|
|
183
190
|
}
|
|
184
191
|
} catch (error) {
|
|
@@ -191,7 +198,10 @@ if (!previewMode) {
|
|
|
191
198
|
// Check for dist directory
|
|
192
199
|
const distDirPath = path.resolve(process.cwd(), "dist");
|
|
193
200
|
if (fs.existsSync(distDirPath) && fs.statSync(distDirPath).isDirectory()) {
|
|
194
|
-
|
|
201
|
+
// Collect paths synchronously, then hash sequentially via streams so at
|
|
202
|
+
// most one file's read window is in memory at a time.
|
|
203
|
+
const distEntries = [];
|
|
204
|
+
const collectDistFiles = (dirPath, basePath = "") => {
|
|
195
205
|
const files = fs.readdirSync(dirPath);
|
|
196
206
|
|
|
197
207
|
files.forEach((file) => {
|
|
@@ -203,23 +213,28 @@ if (!previewMode) {
|
|
|
203
213
|
.replace(/\\/g, "/");
|
|
204
214
|
|
|
205
215
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
206
|
-
|
|
216
|
+
collectDistFiles(fullPath, relativePath);
|
|
207
217
|
} else {
|
|
208
|
-
|
|
209
|
-
newHashes[deployKey] = hash;
|
|
210
|
-
|
|
211
|
-
if (prodMode || hash !== deployedHashes[deployKey]) {
|
|
212
|
-
filesToUpload.push({
|
|
213
|
-
filePath: fullPath,
|
|
214
|
-
uploadPath: uploadPath === "." ? "" : uploadPath,
|
|
215
|
-
fileName: path.basename(file),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
+
distEntries.push({ fullPath, uploadPath, deployKey });
|
|
218
219
|
}
|
|
219
220
|
});
|
|
220
221
|
};
|
|
221
222
|
|
|
222
|
-
|
|
223
|
+
collectDistFiles(distDirPath);
|
|
224
|
+
|
|
225
|
+
for (const entry of distEntries) {
|
|
226
|
+
const hash = await calculateFileHash(entry.fullPath);
|
|
227
|
+
newHashes[entry.deployKey] = hash;
|
|
228
|
+
|
|
229
|
+
if (prodMode || hash !== deployedHashes[entry.deployKey]) {
|
|
230
|
+
filesToUpload.push({
|
|
231
|
+
filePath: entry.fullPath,
|
|
232
|
+
uploadPath: entry.uploadPath === "." ? "" : entry.uploadPath,
|
|
233
|
+
fileName: path.basename(entry.fullPath),
|
|
234
|
+
deployKey: entry.deployKey,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
223
238
|
}
|
|
224
239
|
}
|
|
225
240
|
|
|
@@ -229,32 +244,51 @@ if (filesToUpload.length === 0) {
|
|
|
229
244
|
"No file changes detected. Setting up CrossRamp configuration..."
|
|
230
245
|
);
|
|
231
246
|
|
|
232
|
-
setupCrossRamp(targetVerse, crossrampConfig)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
});
|
|
247
|
+
const success = await setupCrossRamp(targetVerse, crossrampConfig);
|
|
248
|
+
if (success) {
|
|
249
|
+
console.log("CrossRamp setup completed.");
|
|
250
|
+
}
|
|
251
|
+
process.exit(success ? 0 : 1);
|
|
238
252
|
} else {
|
|
239
253
|
console.log("No changes detected. Nothing to deploy.");
|
|
240
254
|
process.exit(0);
|
|
241
255
|
}
|
|
242
|
-
return;
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
console.log(`Found ${filesToUpload.length} files to upload...`);
|
|
246
259
|
|
|
247
|
-
// Upload files
|
|
248
|
-
|
|
260
|
+
// Upload files with bounded concurrency, streaming each file from disk.
|
|
261
|
+
// The previous implementation buffered every file with readFileSync and
|
|
262
|
+
// started all uploads at once, so peak memory was roughly the whole dist
|
|
263
|
+
// size times 2-3 (raw buffer + multipart framing + axios serialization) —
|
|
264
|
+
// measured 2GB+ for asset-heavy games, inside 4GB containers. Streaming
|
|
265
|
+
// with knownLength keeps the request identical on the wire (same multipart
|
|
266
|
+
// framing, exact Content-Length) while holding only a few in-flight
|
|
267
|
+
// buffers in memory.
|
|
268
|
+
const UPLOAD_CONCURRENCY = 5;
|
|
269
|
+
|
|
270
|
+
const uploadFile = (fileInfo) => {
|
|
249
271
|
const { filePath, uploadPath, fileName } = fileInfo;
|
|
250
272
|
const form = new FormData();
|
|
251
|
-
|
|
252
|
-
|
|
273
|
+
form.append("file", fs.createReadStream(filePath), {
|
|
274
|
+
filename: fileName,
|
|
275
|
+
knownLength: fs.statSync(filePath).size,
|
|
276
|
+
});
|
|
253
277
|
form.append("path", uploadPath || "/");
|
|
254
278
|
|
|
255
279
|
return axios
|
|
256
280
|
.post(`${endpoint}/verses/${targetVerse}/files`, form, {
|
|
257
|
-
headers:
|
|
281
|
+
headers: {
|
|
282
|
+
...getAuthHeaders(form.getHeaders()),
|
|
283
|
+
"Content-Length": form.getLengthSync(),
|
|
284
|
+
},
|
|
285
|
+
// maxRedirects: 0 forces the plain http adapter. With redirects
|
|
286
|
+
// enabled, follow-redirects buffers the ENTIRE request body in memory
|
|
287
|
+
// (to replay it after a redirect), which silently defeats streaming.
|
|
288
|
+
// The upload endpoint never redirects POSTs, so nothing is lost.
|
|
289
|
+
maxRedirects: 0,
|
|
290
|
+
maxBodyLength: Infinity,
|
|
291
|
+
maxContentLength: Infinity,
|
|
258
292
|
})
|
|
259
293
|
.then(() => {
|
|
260
294
|
console.log(`Uploaded: ${uploadPath}/${fileName}`);
|
|
@@ -267,30 +301,74 @@ const uploadPromises = filesToUpload.map((fileInfo) => {
|
|
|
267
301
|
);
|
|
268
302
|
return false;
|
|
269
303
|
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Minimal worker pool: at most `concurrency` uploads in flight, results in
|
|
307
|
+
// input order, never rejects (uploadFile already maps errors to false).
|
|
308
|
+
const uploadAll = async (files, concurrency) => {
|
|
309
|
+
const results = new Array(files.length);
|
|
310
|
+
let next = 0;
|
|
311
|
+
const workers = Array.from(
|
|
312
|
+
{ length: Math.min(concurrency, files.length) },
|
|
313
|
+
async () => {
|
|
314
|
+
while (next < files.length) {
|
|
315
|
+
const i = next++;
|
|
316
|
+
results[i] = await uploadFile(files[i]);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
await Promise.all(workers);
|
|
321
|
+
return results;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const results = await uploadAll(filesToUpload, UPLOAD_CONCURRENCY);
|
|
325
|
+
const successCount = results.filter(Boolean).length;
|
|
326
|
+
console.log(
|
|
327
|
+
`Deployment complete: ${successCount}/${filesToUpload.length} files uploaded successfully.`
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// A failed upload must not be recorded as deployed — otherwise the next
|
|
331
|
+
// incremental run sees a matching hash, skips the file, and the verse stays
|
|
332
|
+
// broken until a --prod redeploy. Revert each failed file's hash to its last
|
|
333
|
+
// deployed value (the server still holds that content), or drop it entirely
|
|
334
|
+
// if it was never deployed, so the next run retries it.
|
|
335
|
+
const failedKeys = [];
|
|
336
|
+
results.forEach((ok, i) => {
|
|
337
|
+
if (ok) return;
|
|
338
|
+
const { deployKey } = filesToUpload[i];
|
|
339
|
+
failedKeys.push(deployKey);
|
|
340
|
+
if (deployKey in deployedHashes) {
|
|
341
|
+
newHashes[deployKey] = deployedHashes[deployKey];
|
|
342
|
+
} else {
|
|
343
|
+
delete newHashes[deployKey];
|
|
344
|
+
}
|
|
270
345
|
});
|
|
271
346
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
347
|
+
// CrossRamp setup if config exists and all files uploaded successfully
|
|
348
|
+
if (crossrampConfig && successCount === filesToUpload.length) {
|
|
349
|
+
console.log("Setting up CrossRamp configuration...");
|
|
350
|
+
const success = await setupCrossRamp(targetVerse, crossrampConfig);
|
|
351
|
+
if (!success) {
|
|
352
|
+
console.warn(
|
|
353
|
+
"Warning: CrossRamp setup failed, but files were deployed"
|
|
277
354
|
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
278
357
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const success = await setupCrossRamp(targetVerse, crossrampConfig);
|
|
283
|
-
if (!success) {
|
|
284
|
-
console.warn(
|
|
285
|
-
"Warning: CrossRamp setup failed, but files were deployed"
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
358
|
+
// Save new hashes to .deployed file
|
|
359
|
+
fs.writeFileSync(deployedFilePath, JSON.stringify(newHashes, null, 2));
|
|
360
|
+
console.log("Updated .deployed file with new file hashes");
|
|
289
361
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
.
|
|
295
|
-
|
|
296
|
-
|
|
362
|
+
if (failedKeys.length > 0) {
|
|
363
|
+
console.error(
|
|
364
|
+
`Deployment incomplete: ${failedKeys.length} file(s) failed to upload and will be retried on the next run:`
|
|
365
|
+
);
|
|
366
|
+
failedKeys.forEach((key) => console.error(` - ${key}`));
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
main().catch((error) => {
|
|
372
|
+
console.error("Deployment failed:", error.message);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent8/deploy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "A CLI tool for running agent8 commands",
|
|
5
|
-
"main": "index.js",
|
|
6
5
|
"bin": {
|
|
7
6
|
"agent8": "./bin/agent8.js"
|
|
8
7
|
},
|
|
@@ -13,5 +12,8 @@
|
|
|
13
12
|
"axios": "^1.7.9",
|
|
14
13
|
"dotenv": "^16.4.7",
|
|
15
14
|
"form-data": "^4.0.1"
|
|
16
|
-
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin"
|
|
18
|
+
]
|
|
17
19
|
}
|
package/.env
DELETED
|
File without changes
|