@glash/cli 0.2.26 → 0.2.28
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/commands/deploy.mjs +174 -35
- package/lib/zip.mjs +1 -1
- package/package.json +1 -1
package/commands/deploy.mjs
CHANGED
|
@@ -138,14 +138,31 @@ export async function deploy({ flags }) {
|
|
|
138
138
|
let currentPhase = 'doing';
|
|
139
139
|
const start = Date.now();
|
|
140
140
|
const indicator = createDeployIndicator();
|
|
141
|
+
const report = {
|
|
142
|
+
status: 'running',
|
|
143
|
+
projectId,
|
|
144
|
+
deploymentId: '',
|
|
145
|
+
target,
|
|
146
|
+
source: remoteDeploy ? 'remote git deploy' : 'local upload',
|
|
147
|
+
packagesUploaded: 0,
|
|
148
|
+
filesIncluded: 0,
|
|
149
|
+
deploymentSizeBytes: 0,
|
|
150
|
+
envSummary,
|
|
151
|
+
liveUrl: '',
|
|
152
|
+
error: '',
|
|
153
|
+
start,
|
|
154
|
+
};
|
|
141
155
|
indicator.start(currentPhase);
|
|
142
156
|
|
|
143
157
|
if (!flags['no-env']) {
|
|
144
158
|
indicator.setPhase('syncing');
|
|
145
159
|
try {
|
|
146
160
|
envSummary = await syncEnvBeforeDeploy(projectId, target, flags);
|
|
161
|
+
report.envSummary = envSummary;
|
|
147
162
|
} catch (e) {
|
|
148
|
-
indicator.
|
|
163
|
+
indicator.fail('failed');
|
|
164
|
+
report.envSummary = envSummary;
|
|
165
|
+
printDeployReport({ ...report, status: 'failed', error: `Env sync failed: ${e.message}` });
|
|
149
166
|
return fail(`Env sync failed: ${e.message}`);
|
|
150
167
|
}
|
|
151
168
|
}
|
|
@@ -154,8 +171,10 @@ export async function deploy({ flags }) {
|
|
|
154
171
|
indicator.setPhase('packaging');
|
|
155
172
|
const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
|
|
156
173
|
try {
|
|
157
|
-
await zipDirectory(process.cwd(), zipPath);
|
|
158
|
-
await stat(zipPath);
|
|
174
|
+
const bundle = await zipDirectory(process.cwd(), zipPath);
|
|
175
|
+
const zipStat = await stat(zipPath);
|
|
176
|
+
report.filesIncluded = bundle.fileCount ?? 0;
|
|
177
|
+
report.deploymentSizeBytes = zipStat.size || bundle.bytes || 0;
|
|
159
178
|
const formData = new FormData();
|
|
160
179
|
formData.append('projectId', projectId);
|
|
161
180
|
formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
|
|
@@ -166,24 +185,30 @@ export async function deploy({ flags }) {
|
|
|
166
185
|
|
|
167
186
|
indicator.setPhase('glashing');
|
|
168
187
|
dep = await apiPost('/deployments/upload', formData);
|
|
188
|
+
report.deploymentId = dep?.id ?? '';
|
|
189
|
+
report.packagesUploaded = 1;
|
|
169
190
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
170
191
|
} catch (e) {
|
|
171
192
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
172
|
-
indicator.
|
|
193
|
+
indicator.fail('failed');
|
|
194
|
+
printDeployReport({ ...report, status: 'failed', error: `Upload failed: ${e.message}` });
|
|
173
195
|
return fail(`Upload failed: ${e.message}`);
|
|
174
196
|
}
|
|
175
197
|
} else {
|
|
176
198
|
indicator.setPhase('hatching');
|
|
177
199
|
try {
|
|
178
200
|
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
|
|
201
|
+
report.deploymentId = dep?.id ?? '';
|
|
179
202
|
} catch (e) {
|
|
180
|
-
indicator.
|
|
203
|
+
indicator.fail('failed');
|
|
204
|
+
printDeployReport({ ...report, status: 'failed', error: e.message });
|
|
181
205
|
return fail(e.message);
|
|
182
206
|
}
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
if (flags.detach || flags.D) {
|
|
186
|
-
indicator.
|
|
210
|
+
indicator.fail('queued');
|
|
211
|
+
printDeployReport({ ...report, status: 'queued' });
|
|
187
212
|
info(`Queued: ${color.dim(dep.id)}`);
|
|
188
213
|
info(`Track it with: ${color.cyan(`glash status ${dep.id}`)}`);
|
|
189
214
|
return;
|
|
@@ -203,7 +228,7 @@ export async function deploy({ flags }) {
|
|
|
203
228
|
if (e.status === 429) {
|
|
204
229
|
const wait = Math.min(Math.max(e.retryAfterMs ?? pollDelayMs * 2, 10000), 60000);
|
|
205
230
|
pollDelayMs = wait;
|
|
206
|
-
await
|
|
231
|
+
await indicator.wait(wait);
|
|
207
232
|
continue;
|
|
208
233
|
}
|
|
209
234
|
if (Date.now() - lastProgressAt > 60_000) {
|
|
@@ -211,7 +236,7 @@ export async function deploy({ flags }) {
|
|
|
211
236
|
lastProgressAt = Date.now();
|
|
212
237
|
}
|
|
213
238
|
pollDelayMs = Math.min(Math.max(pollDelayMs * 1.5, 5000), 30000);
|
|
214
|
-
await
|
|
239
|
+
await indicator.wait(pollDelayMs);
|
|
215
240
|
continue;
|
|
216
241
|
}
|
|
217
242
|
|
|
@@ -229,28 +254,34 @@ export async function deploy({ flags }) {
|
|
|
229
254
|
const url = pipe.hostings?.[0]?.domains?.[0]?.domain;
|
|
230
255
|
if (url) {
|
|
231
256
|
indicator.setPhase('checking');
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
report.liveUrl = normalizeLiveUrl(url);
|
|
258
|
+
const problem = await waitForLiveUrlProblem(report.liveUrl);
|
|
234
259
|
if (problem) {
|
|
260
|
+
indicator.fail('failed');
|
|
261
|
+
printDeployReport({ ...report, status: 'failed', error: `Live URL check failed: ${problem}` });
|
|
235
262
|
return fail(`Deployment finished, but the hostname is still not serving your app: ${problem}. Run \`glash logs ${dep.id}\` for details.`);
|
|
236
263
|
}
|
|
237
264
|
} else {
|
|
238
|
-
|
|
265
|
+
report.liveUrl = '';
|
|
239
266
|
}
|
|
240
|
-
|
|
267
|
+
indicator.finish('complete');
|
|
268
|
+
printDeployReport({ ...report, status: 'successful' });
|
|
269
|
+
ok('Deployment live');
|
|
241
270
|
return;
|
|
242
271
|
}
|
|
243
272
|
if (status === 'placeholder') {
|
|
244
|
-
indicator.stop();
|
|
245
273
|
const lastPlaceholder = [...(pipe.events ?? [])].reverse().find((ev) => ev.status === 'placeholder' || ev.step === 'deploy');
|
|
246
274
|
const detail = lastPlaceholder?.message ? `: ${lastPlaceholder.message}` : '';
|
|
275
|
+
indicator.fail('failed');
|
|
276
|
+
printDeployReport({ ...report, status: 'failed', error: `Deployment reached the glashDB parked page, not your app${detail}` });
|
|
247
277
|
return fail(`Deployment reached the glashDB parked page, not your app${detail}. Run \`glash logs ${dep.id}\` for details.`);
|
|
248
278
|
}
|
|
249
279
|
if (['error', 'failed'].includes(status)) {
|
|
250
|
-
indicator.stop();
|
|
251
280
|
const lastError = [...(pipe.events ?? [])].reverse().find((ev) => ev.status === 'error' || ev.status === 'failed');
|
|
252
281
|
const rawDetail = lastError?.message ? `: ${lastError.message}` : '';
|
|
253
282
|
const detail = `${rawDetail}${deploymentFailureHint(lastError?.message)}`;
|
|
283
|
+
indicator.fail('failed');
|
|
284
|
+
printDeployReport({ ...report, status: 'failed', error: `Deployment failed (${status})${rawDetail}` });
|
|
254
285
|
return fail(`Deployment failed (${status})${detail}. Run \`glash logs ${dep.id}\` for details.`);
|
|
255
286
|
}
|
|
256
287
|
currentPhase = phaseFromPipeline(pipe, currentPhase);
|
|
@@ -258,7 +289,8 @@ export async function deploy({ flags }) {
|
|
|
258
289
|
pollDelayMs = nextPollDelay(pipe);
|
|
259
290
|
await indicator.wait(pollDelayMs);
|
|
260
291
|
}
|
|
261
|
-
indicator.
|
|
292
|
+
indicator.fail('timed out');
|
|
293
|
+
printDeployReport({ ...report, status: 'timed out', error: `Waited ${formatDuration(timeoutMs)} for deployment to finish` });
|
|
262
294
|
warn(`Timed out after ${formatDuration(timeoutMs)} waiting for deployment. Check \`glash status ${dep.id}\`.`);
|
|
263
295
|
}
|
|
264
296
|
|
|
@@ -416,12 +448,74 @@ function isTransientLiveUrlProblem(problem) {
|
|
|
416
448
|
return /no nginx route|parked page|HTTP 5\d\d|live URL check failed/i.test(problem);
|
|
417
449
|
}
|
|
418
450
|
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
451
|
+
function normalizeLiveUrl(url) {
|
|
452
|
+
return url ? `https://${String(url).replace(/^https?:\/\//, '')}` : '';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function printDeployReport(report) {
|
|
456
|
+
const elapsedMs = Date.now() - report.start;
|
|
457
|
+
const liveUrl = report.liveUrl ? terminalLink(report.liveUrl) : '-';
|
|
458
|
+
const rows = [
|
|
459
|
+
['Status', formatReportStatus(report.status)],
|
|
460
|
+
['Deployment ID', report.deploymentId || '-'],
|
|
461
|
+
['Target', report.target || 'production'],
|
|
462
|
+
['Source', report.source || '-'],
|
|
463
|
+
['Packages uploaded', formatPackagesUploaded(report)],
|
|
464
|
+
['Deployment size', formatDeploymentSize(report)],
|
|
465
|
+
['Total time spent', formatMinutes(elapsedMs)],
|
|
466
|
+
['Live URL', liveUrl],
|
|
467
|
+
['Environment', formatEnvSummary(report.envSummary)],
|
|
468
|
+
];
|
|
469
|
+
if (report.error) rows.push(['Error', report.error]);
|
|
470
|
+
|
|
471
|
+
const width = rows.reduce((max, [label]) => Math.max(max, label.length), 0);
|
|
472
|
+
info('');
|
|
473
|
+
info(color.bold('Deployment report'));
|
|
474
|
+
for (const [label, value] of rows) {
|
|
475
|
+
info(` ${color.dim(label.padEnd(width))} ${value}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function formatReportStatus(status) {
|
|
480
|
+
if (status === 'successful') return color.green('successful');
|
|
481
|
+
if (status === 'queued') return color.yellow('queued');
|
|
482
|
+
if (status === 'timed out') return color.yellow('timed out');
|
|
483
|
+
if (status === 'failed') return color.red('failed');
|
|
484
|
+
return status || '-';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function formatPackagesUploaded(report) {
|
|
488
|
+
const count = report.packagesUploaded ?? 0;
|
|
489
|
+
const label = count === 1 ? 'source bundle' : 'source bundles';
|
|
490
|
+
const files = report.filesIncluded
|
|
491
|
+
? ` (${report.filesIncluded.toLocaleString()} files included)`
|
|
492
|
+
: (report.source === 'remote git deploy' ? ' (remote source)' : '');
|
|
493
|
+
return `${count.toLocaleString()} ${label}${files}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function formatDeploymentSize(report) {
|
|
497
|
+
if (report.deploymentSizeBytes) return formatBytes(report.deploymentSizeBytes);
|
|
498
|
+
return report.source === 'remote git deploy' ? 'not measured for remote deploy' : '-';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function formatEnvSummary(envSummary) {
|
|
502
|
+
if (envSummary?.skipped) return 'skipped';
|
|
503
|
+
const count = envSummary?.count ?? 0;
|
|
504
|
+
return `${count.toLocaleString()} variable${count === 1 ? '' : 's'} synced`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function formatMinutes(ms) {
|
|
508
|
+
const minutes = ms / 60_000;
|
|
509
|
+
const rounded = minutes < 10 ? minutes.toFixed(2) : minutes.toFixed(1);
|
|
510
|
+
return `${rounded} min (${formatDuration(ms)})`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function formatBytes(bytes) {
|
|
514
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return '-';
|
|
515
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
516
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
517
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
518
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
425
519
|
}
|
|
426
520
|
|
|
427
521
|
function terminalLink(url) {
|
|
@@ -432,42 +526,71 @@ function terminalLink(url) {
|
|
|
432
526
|
|
|
433
527
|
function createDeployIndicator() {
|
|
434
528
|
const interactive = Boolean(stdout.isTTY);
|
|
435
|
-
let frame = 0;
|
|
436
529
|
let timer;
|
|
437
530
|
let phase = 'doing';
|
|
438
|
-
let
|
|
439
|
-
|
|
531
|
+
let progress = 0;
|
|
532
|
+
let lastPrinted = '';
|
|
440
533
|
|
|
441
534
|
function message() {
|
|
442
|
-
const
|
|
443
|
-
|
|
535
|
+
const width = 24;
|
|
536
|
+
const filled = Math.round((progress / 100) * width);
|
|
537
|
+
const bar = `${'#'.repeat(filled)}${'-'.repeat(width - filled)}`;
|
|
538
|
+
const percent = `${String(Math.round(progress)).padStart(3, ' ')}%`;
|
|
539
|
+
return `${color.dim(`[${bar}]`)} ${percent} ${phase}`;
|
|
444
540
|
}
|
|
445
541
|
|
|
446
542
|
function render({ force = false } = {}) {
|
|
543
|
+
const next = message();
|
|
447
544
|
if (!interactive) {
|
|
448
|
-
if (force &&
|
|
449
|
-
|
|
450
|
-
info(
|
|
545
|
+
if (force && next !== lastPrinted) {
|
|
546
|
+
lastPrinted = next;
|
|
547
|
+
info(next);
|
|
451
548
|
}
|
|
452
549
|
return;
|
|
453
550
|
}
|
|
454
|
-
stdout.write(`\r\x1b[2K${
|
|
551
|
+
stdout.write(`\r\x1b[2K${next}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function stopTimer() {
|
|
555
|
+
if (timer) clearInterval(timer);
|
|
556
|
+
timer = undefined;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function setProgress(nextProgress, nextPhase = phase) {
|
|
560
|
+
const bounded = Math.max(0, Math.min(100, Number(nextProgress) || 0));
|
|
561
|
+
const advanced = Math.max(progress, bounded);
|
|
562
|
+
const changed = nextPhase !== phase || advanced !== progress;
|
|
563
|
+
phase = nextPhase;
|
|
564
|
+
progress = advanced;
|
|
565
|
+
if (changed) render({ force: true });
|
|
455
566
|
}
|
|
456
567
|
|
|
457
568
|
return {
|
|
458
569
|
start(initialPhase = 'doing') {
|
|
459
570
|
phase = initialPhase;
|
|
460
|
-
|
|
571
|
+
progress = 0;
|
|
572
|
+
render({ force: true });
|
|
461
573
|
timer = setInterval(render, 1000);
|
|
462
574
|
},
|
|
463
575
|
setPhase(nextPhase) {
|
|
464
|
-
if (!nextPhase
|
|
465
|
-
|
|
576
|
+
if (!nextPhase) return;
|
|
577
|
+
setProgress(progressFromPhase(nextPhase), nextPhase);
|
|
578
|
+
},
|
|
579
|
+
finish(finalPhase = 'complete') {
|
|
580
|
+
stopTimer();
|
|
581
|
+
phase = finalPhase;
|
|
582
|
+
progress = 100;
|
|
583
|
+
render({ force: true });
|
|
584
|
+
if (interactive) stdout.write('\n');
|
|
585
|
+
},
|
|
586
|
+
fail(finalPhase = 'failed') {
|
|
587
|
+
stopTimer();
|
|
588
|
+
phase = finalPhase;
|
|
466
589
|
render({ force: true });
|
|
590
|
+
if (interactive) stdout.write('\n');
|
|
467
591
|
},
|
|
468
592
|
stop() {
|
|
469
|
-
|
|
470
|
-
timer = undefined;
|
|
593
|
+
stopTimer();
|
|
471
594
|
if (interactive) stdout.write('\r\x1b[2K');
|
|
472
595
|
},
|
|
473
596
|
wait(ms) {
|
|
@@ -476,4 +599,20 @@ function createDeployIndicator() {
|
|
|
476
599
|
};
|
|
477
600
|
}
|
|
478
601
|
|
|
602
|
+
function progressFromPhase(phase) {
|
|
603
|
+
const progressByPhase = {
|
|
604
|
+
doing: 2,
|
|
605
|
+
syncing: 8,
|
|
606
|
+
packaging: 22,
|
|
607
|
+
glashing: 35,
|
|
608
|
+
hatching: 45,
|
|
609
|
+
waiting: 48,
|
|
610
|
+
building: 65,
|
|
611
|
+
deploying: 82,
|
|
612
|
+
routing: 90,
|
|
613
|
+
checking: 96,
|
|
614
|
+
};
|
|
615
|
+
return progressByPhase[phase] ?? 50;
|
|
616
|
+
}
|
|
617
|
+
|
|
479
618
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
package/lib/zip.mjs
CHANGED
|
@@ -76,7 +76,7 @@ export async function zipDirectory(sourceDir, outPath, { ignore = [] } = {}) {
|
|
|
76
76
|
const output = createWriteStream(outPath);
|
|
77
77
|
let bytes = 0;
|
|
78
78
|
|
|
79
|
-
output.on('close', () => resolve(bytes));
|
|
79
|
+
output.on('close', () => resolve({ bytes, fileCount: files.length, files }));
|
|
80
80
|
output.on('error', reject);
|
|
81
81
|
zip.outputStream.on('data', (chunk) => { bytes += chunk.length; });
|
|
82
82
|
zip.outputStream.on('error', reject);
|
package/package.json
CHANGED