@agent8/deploy 1.3.0 → 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 +142 -55
- package/package.json +5 -3
- package/.env +0 -0
package/bin/agent8.js
CHANGED
|
@@ -148,21 +148,36 @@ 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
|
+
});
|
|
161
|
+
|
|
162
|
+
const main = async () => {
|
|
163
|
+
// Check for server.js (priority: ./server.js, fallback: ./server/dist/server.js)
|
|
164
|
+
let serverFilePath = path.resolve(process.cwd(), "server.js");
|
|
165
|
+
if (!fs.existsSync(serverFilePath)) {
|
|
166
|
+
// Fallback to ./server/dist/server.js if ./server.js doesn't exist
|
|
167
|
+
const fallbackPath = path.resolve(process.cwd(), "server/dist/server.js");
|
|
168
|
+
if (fs.existsSync(fallbackPath)) {
|
|
169
|
+
serverFilePath = fallbackPath;
|
|
170
|
+
console.log("Found server file: ./server/dist/server.js");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
156
173
|
|
|
157
|
-
// Check for server.js
|
|
158
|
-
const serverFilePath = path.resolve(process.cwd(), "server.js");
|
|
159
174
|
if (fs.existsSync(serverFilePath)) {
|
|
160
175
|
// Validate server.js syntax before deployment
|
|
161
176
|
try {
|
|
162
177
|
const serverContent = fs.readFileSync(serverFilePath, "utf8");
|
|
163
178
|
eval(`(function() { ${serverContent}; })`)();
|
|
164
179
|
|
|
165
|
-
const hash = calculateFileHash(serverFilePath);
|
|
180
|
+
const hash = await calculateFileHash(serverFilePath);
|
|
166
181
|
newHashes["server.js"] = hash;
|
|
167
182
|
|
|
168
183
|
if (prodMode || hash !== deployedHashes["server.js"]) {
|
|
@@ -170,6 +185,7 @@ if (fs.existsSync(serverFilePath)) {
|
|
|
170
185
|
filePath: serverFilePath,
|
|
171
186
|
uploadPath: "",
|
|
172
187
|
fileName: "server.js",
|
|
188
|
+
deployKey: "server.js",
|
|
173
189
|
});
|
|
174
190
|
}
|
|
175
191
|
} catch (error) {
|
|
@@ -182,7 +198,10 @@ if (!previewMode) {
|
|
|
182
198
|
// Check for dist directory
|
|
183
199
|
const distDirPath = path.resolve(process.cwd(), "dist");
|
|
184
200
|
if (fs.existsSync(distDirPath) && fs.statSync(distDirPath).isDirectory()) {
|
|
185
|
-
|
|
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 = "") => {
|
|
186
205
|
const files = fs.readdirSync(dirPath);
|
|
187
206
|
|
|
188
207
|
files.forEach((file) => {
|
|
@@ -194,23 +213,28 @@ if (!previewMode) {
|
|
|
194
213
|
.replace(/\\/g, "/");
|
|
195
214
|
|
|
196
215
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
197
|
-
|
|
216
|
+
collectDistFiles(fullPath, relativePath);
|
|
198
217
|
} else {
|
|
199
|
-
|
|
200
|
-
newHashes[deployKey] = hash;
|
|
201
|
-
|
|
202
|
-
if (prodMode || hash !== deployedHashes[deployKey]) {
|
|
203
|
-
filesToUpload.push({
|
|
204
|
-
filePath: fullPath,
|
|
205
|
-
uploadPath: uploadPath === "." ? "" : uploadPath,
|
|
206
|
-
fileName: path.basename(file),
|
|
207
|
-
});
|
|
208
|
-
}
|
|
218
|
+
distEntries.push({ fullPath, uploadPath, deployKey });
|
|
209
219
|
}
|
|
210
220
|
});
|
|
211
221
|
};
|
|
212
222
|
|
|
213
|
-
|
|
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
|
+
}
|
|
214
238
|
}
|
|
215
239
|
}
|
|
216
240
|
|
|
@@ -220,32 +244,51 @@ if (filesToUpload.length === 0) {
|
|
|
220
244
|
"No file changes detected. Setting up CrossRamp configuration..."
|
|
221
245
|
);
|
|
222
246
|
|
|
223
|
-
setupCrossRamp(targetVerse, crossrampConfig)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
});
|
|
247
|
+
const success = await setupCrossRamp(targetVerse, crossrampConfig);
|
|
248
|
+
if (success) {
|
|
249
|
+
console.log("CrossRamp setup completed.");
|
|
250
|
+
}
|
|
251
|
+
process.exit(success ? 0 : 1);
|
|
229
252
|
} else {
|
|
230
253
|
console.log("No changes detected. Nothing to deploy.");
|
|
231
254
|
process.exit(0);
|
|
232
255
|
}
|
|
233
|
-
return;
|
|
234
256
|
}
|
|
235
257
|
|
|
236
258
|
console.log(`Found ${filesToUpload.length} files to upload...`);
|
|
237
259
|
|
|
238
|
-
// Upload files
|
|
239
|
-
|
|
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) => {
|
|
240
271
|
const { filePath, uploadPath, fileName } = fileInfo;
|
|
241
272
|
const form = new FormData();
|
|
242
|
-
|
|
243
|
-
|
|
273
|
+
form.append("file", fs.createReadStream(filePath), {
|
|
274
|
+
filename: fileName,
|
|
275
|
+
knownLength: fs.statSync(filePath).size,
|
|
276
|
+
});
|
|
244
277
|
form.append("path", uploadPath || "/");
|
|
245
278
|
|
|
246
279
|
return axios
|
|
247
280
|
.post(`${endpoint}/verses/${targetVerse}/files`, form, {
|
|
248
|
-
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,
|
|
249
292
|
})
|
|
250
293
|
.then(() => {
|
|
251
294
|
console.log(`Uploaded: ${uploadPath}/${fileName}`);
|
|
@@ -258,30 +301,74 @@ const uploadPromises = filesToUpload.map((fileInfo) => {
|
|
|
258
301
|
);
|
|
259
302
|
return false;
|
|
260
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
|
+
}
|
|
261
345
|
});
|
|
262
346
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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"
|
|
268
354
|
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
269
357
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const success = await setupCrossRamp(targetVerse, crossrampConfig);
|
|
274
|
-
if (!success) {
|
|
275
|
-
console.warn(
|
|
276
|
-
"Warning: CrossRamp setup failed, but files were deployed"
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
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");
|
|
280
361
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
.
|
|
286
|
-
|
|
287
|
-
|
|
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
|