@glash/cli 0.2.22 → 0.2.24
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 +87 -46
- package/lib/version.mjs +3 -1
- package/package.json +1 -1
package/commands/deploy.mjs
CHANGED
|
@@ -129,22 +129,28 @@ export async function deploy({ flags }) {
|
|
|
129
129
|
const target = flags.prod || flags.production ? 'production' : (flags.target || 'production');
|
|
130
130
|
|
|
131
131
|
let dep;
|
|
132
|
+
let envSummary = { count: 0, skipped: Boolean(flags['no-env']) };
|
|
133
|
+
let currentPhase = 'doing';
|
|
134
|
+
const start = Date.now();
|
|
135
|
+
const indicator = createDeployIndicator();
|
|
136
|
+
indicator.start(currentPhase);
|
|
132
137
|
|
|
133
138
|
if (!flags['no-env']) {
|
|
139
|
+
indicator.setPhase('syncing');
|
|
134
140
|
try {
|
|
135
|
-
await syncEnvBeforeDeploy(projectId, target, flags);
|
|
141
|
+
envSummary = await syncEnvBeforeDeploy(projectId, target, flags);
|
|
136
142
|
} catch (e) {
|
|
143
|
+
indicator.stop();
|
|
137
144
|
return fail(`Env sync failed: ${e.message}`);
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
if (!remoteDeploy) {
|
|
142
|
-
|
|
149
|
+
indicator.setPhase('packaging');
|
|
143
150
|
const zipPath = join(tmpdir(), `glash-deploy-${Date.now()}.zip`);
|
|
144
151
|
try {
|
|
145
152
|
await zipDirectory(process.cwd(), zipPath);
|
|
146
|
-
|
|
147
|
-
info(color.dim(`→ source bundle ${(zipStat.size / 1024 / 1024).toFixed(1)} MB`));
|
|
153
|
+
await stat(zipPath);
|
|
148
154
|
const formData = new FormData();
|
|
149
155
|
formData.append('projectId', projectId);
|
|
150
156
|
formData.append('file', await openAsBlob(zipPath, { type: 'application/zip' }), 'project.zip');
|
|
@@ -153,39 +159,37 @@ export async function deploy({ flags }) {
|
|
|
153
159
|
formData.append('gitBranch', gitBranch || '');
|
|
154
160
|
formData.append('target', target);
|
|
155
161
|
|
|
156
|
-
|
|
162
|
+
indicator.setPhase('glashing');
|
|
157
163
|
dep = await apiPost('/deployments/upload', formData);
|
|
158
164
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
159
165
|
} catch (e) {
|
|
160
166
|
await rm(zipPath, { force: true }).catch(() => {});
|
|
167
|
+
indicator.stop();
|
|
161
168
|
return fail(`Upload failed: ${e.message}`);
|
|
162
169
|
}
|
|
163
170
|
} else {
|
|
164
|
-
|
|
171
|
+
indicator.setPhase('hatching');
|
|
165
172
|
try {
|
|
166
173
|
dep = await apiPost('/deployments', { projectId, commitHash, gitBranch, target });
|
|
167
174
|
} catch (e) {
|
|
175
|
+
indicator.stop();
|
|
168
176
|
return fail(e.message);
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
|
|
172
|
-
ok(`Deployment ${color.bold(dep.id)} queued`);
|
|
173
|
-
|
|
174
180
|
if (flags.detach || flags.D) {
|
|
181
|
+
indicator.stop();
|
|
182
|
+
info(`Queued: ${color.dim(dep.id)}`);
|
|
175
183
|
info(`Track it with: ${color.cyan(`glash status ${dep.id}`)}`);
|
|
176
184
|
return;
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
// Poll the pipeline until it terminates.
|
|
180
188
|
const seen = new Set();
|
|
181
|
-
let lastStatus = '';
|
|
182
|
-
const start = Date.now();
|
|
183
189
|
const timeoutMs = deployTimeoutMs(flags);
|
|
184
190
|
let pollDelayMs = 5000;
|
|
185
191
|
let lastProgressAt = Date.now();
|
|
186
|
-
|
|
187
|
-
const indicator = createDeployIndicator({ start, getPhase: () => currentPhase });
|
|
188
|
-
indicator.start();
|
|
192
|
+
indicator.setPhase('hatching');
|
|
189
193
|
while (Date.now() - start < timeoutMs) {
|
|
190
194
|
let pipe;
|
|
191
195
|
try {
|
|
@@ -198,7 +202,7 @@ export async function deploy({ flags }) {
|
|
|
198
202
|
continue;
|
|
199
203
|
}
|
|
200
204
|
if (Date.now() - lastProgressAt > 60_000) {
|
|
201
|
-
|
|
205
|
+
indicator.setPhase('waiting');
|
|
202
206
|
lastProgressAt = Date.now();
|
|
203
207
|
}
|
|
204
208
|
pollDelayMs = Math.min(Math.max(pollDelayMs * 1.5, 5000), 30000);
|
|
@@ -210,19 +214,22 @@ export async function deploy({ flags }) {
|
|
|
210
214
|
const key = `${ev.id ?? ev.createdAt}:${ev.step}:${ev.status}`;
|
|
211
215
|
if (seen.has(key)) continue;
|
|
212
216
|
seen.add(key);
|
|
213
|
-
indicator.clear();
|
|
214
|
-
const tag = stepBadge(ev.status);
|
|
215
|
-
info(` ${tag} ${color.dim(ev.step.padEnd(8))} ${ev.message ?? ''}`);
|
|
216
217
|
lastProgressAt = Date.now();
|
|
217
218
|
currentPhase = phaseFromEvent(ev);
|
|
219
|
+
indicator.setPhase(currentPhase);
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
|
|
221
|
-
if (status && status !== lastStatus) lastStatus = status;
|
|
222
223
|
if (['ready', 'live', 'success'].includes(status)) {
|
|
223
224
|
indicator.stop();
|
|
224
225
|
const url = pipe.hostings?.[0]?.domains?.[0]?.domain;
|
|
225
|
-
|
|
226
|
+
if (url) {
|
|
227
|
+
const problem = await detectLiveUrlProblem(`https://${url.replace(/^https?:\/\//, '')}`);
|
|
228
|
+
if (problem) {
|
|
229
|
+
return fail(`Deployment finished, but the hostname is still not serving your app: ${problem}. Run \`glash logs ${dep.id}\` for details.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
printDeploySuccess({ url, envSummary });
|
|
226
233
|
return;
|
|
227
234
|
}
|
|
228
235
|
if (status === 'placeholder') {
|
|
@@ -239,6 +246,7 @@ export async function deploy({ flags }) {
|
|
|
239
246
|
return fail(`Deployment failed (${status})${detail}. Run \`glash logs ${dep.id}\` for details.`);
|
|
240
247
|
}
|
|
241
248
|
currentPhase = phaseFromPipeline(pipe, currentPhase);
|
|
249
|
+
indicator.setPhase(currentPhase);
|
|
242
250
|
pollDelayMs = nextPollDelay(pipe);
|
|
243
251
|
await indicator.wait(pollDelayMs);
|
|
244
252
|
}
|
|
@@ -248,7 +256,7 @@ export async function deploy({ flags }) {
|
|
|
248
256
|
|
|
249
257
|
async function syncEnvBeforeDeploy(projectId, target, flags) {
|
|
250
258
|
const files = await resolveEnvFiles(target, flags);
|
|
251
|
-
if (!files.length) return;
|
|
259
|
+
if (!files.length) return { count: 0, files: [] };
|
|
252
260
|
|
|
253
261
|
const merged = new Map();
|
|
254
262
|
for (const file of files) {
|
|
@@ -259,9 +267,9 @@ async function syncEnvBeforeDeploy(projectId, target, flags) {
|
|
|
259
267
|
.filter(([key]) => !key.startsWith('GLASH_'))
|
|
260
268
|
.map(([key, value]) => ({ key, value, environment: target }));
|
|
261
269
|
|
|
262
|
-
if (!vars.length) return;
|
|
270
|
+
if (!vars.length) return { count: 0, files };
|
|
263
271
|
await apiPost(`/projects/${projectId}/env-vars/bulk`, { vars });
|
|
264
|
-
|
|
272
|
+
return { count: vars.length, files };
|
|
265
273
|
}
|
|
266
274
|
|
|
267
275
|
async function resolveEnvFiles(target, flags) {
|
|
@@ -306,13 +314,6 @@ function parseDotenv(text) {
|
|
|
306
314
|
return out;
|
|
307
315
|
}
|
|
308
316
|
|
|
309
|
-
function stepBadge(status) {
|
|
310
|
-
if (status === 'success' || status === 'issued' || status === 'active' || status === 'ready') return color.green('✓');
|
|
311
|
-
if (status === 'error' || status === 'failed') return color.red('✗');
|
|
312
|
-
if (status === 'warning') return color.yellow('!');
|
|
313
|
-
return color.cyan('•');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
317
|
function nextPollDelay(pipe) {
|
|
317
318
|
const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
|
|
318
319
|
const last = [...(pipe.events ?? [])].reverse().find(Boolean);
|
|
@@ -340,19 +341,19 @@ function formatDuration(ms) {
|
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
function phaseFromEvent(ev) {
|
|
343
|
-
if (ev.step === 'detect') return '
|
|
344
|
-
if (ev.step === 'host') return '
|
|
345
|
-
if (ev.step === 'package') return ev.status === 'queued' ? '
|
|
344
|
+
if (ev.step === 'detect') return 'doing';
|
|
345
|
+
if (ev.step === 'host') return 'hatching';
|
|
346
|
+
if (ev.step === 'package') return ev.status === 'queued' ? 'hatching' : 'packaging';
|
|
346
347
|
if (ev.step === 'build') return 'building';
|
|
347
|
-
if (ev.step === 'deploy') return ev.status === 'ready' ? '
|
|
348
|
+
if (ev.step === 'deploy') return ev.status === 'ready' ? 'checking' : 'deploying';
|
|
348
349
|
if (ev.step === 'route' || ev.step === 'edge' || ev.step === 'ssl') return 'routing';
|
|
349
|
-
if (ev.step === 'runtime') return 'checking
|
|
350
|
-
return ev.step || '
|
|
350
|
+
if (ev.step === 'runtime') return 'checking';
|
|
351
|
+
return ev.step || 'glashing';
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
function phaseFromPipeline(pipe, fallback) {
|
|
354
355
|
const status = pipe.deployStatus ?? pipe.buildStatus ?? '';
|
|
355
|
-
if (status === 'queued') return '
|
|
356
|
+
if (status === 'queued') return 'hatching';
|
|
356
357
|
if (status === 'building') return 'building';
|
|
357
358
|
if (status === 'rolling-out') return 'deploying';
|
|
358
359
|
const last = [...(pipe.events ?? [])].reverse().find(Boolean);
|
|
@@ -369,23 +370,60 @@ function deploymentFailureHint(message = '') {
|
|
|
369
370
|
return '';
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
function
|
|
373
|
+
async function detectLiveUrlProblem(url) {
|
|
374
|
+
try {
|
|
375
|
+
const res = await fetch(url, {
|
|
376
|
+
redirect: 'manual',
|
|
377
|
+
signal: AbortSignal.timeout(10_000),
|
|
378
|
+
headers: { 'User-Agent': `glash-cli-live-check` },
|
|
379
|
+
});
|
|
380
|
+
const text = await res.clone().text().catch(() => '');
|
|
381
|
+
if (/glashDB:\s*no application is deployed at this hostname/i.test(text)) {
|
|
382
|
+
return 'the edge reached the host, but no nginx route exists for this hostname';
|
|
383
|
+
}
|
|
384
|
+
if (/Glash\s+Site reserved|Site reserved/i.test(text)) {
|
|
385
|
+
return 'the hostname is still showing the glashDB parked page';
|
|
386
|
+
}
|
|
387
|
+
if (res.status >= 500) {
|
|
388
|
+
return `the live URL returned HTTP ${res.status}`;
|
|
389
|
+
}
|
|
390
|
+
return '';
|
|
391
|
+
} catch (e) {
|
|
392
|
+
return `the live URL check failed (${e.message})`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function printDeploySuccess({ url, envSummary }) {
|
|
397
|
+
const liveUrl = url ? `https://${url.replace(/^https?:\/\//, '')}` : '';
|
|
398
|
+
if (liveUrl) info(`Live: ${terminalLink(liveUrl)}`);
|
|
399
|
+
const envText = envSummary?.skipped ? 'skipped' : `${envSummary?.count ?? 0} moved to glash`;
|
|
400
|
+
info(`Env: ${envText}`);
|
|
401
|
+
ok('Success');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function terminalLink(url) {
|
|
405
|
+
const label = color.cyan(url);
|
|
406
|
+
if (!stdout.isTTY) return label;
|
|
407
|
+
return `\x1b]8;;${url}\x07${label}\x1b]8;;\x07`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function createDeployIndicator() {
|
|
373
411
|
const interactive = Boolean(stdout.isTTY);
|
|
374
412
|
let frame = 0;
|
|
375
413
|
let timer;
|
|
376
|
-
let
|
|
414
|
+
let phase = 'doing';
|
|
415
|
+
let printedPhase = '';
|
|
377
416
|
const frames = ['.', '..', '...'];
|
|
378
417
|
|
|
379
418
|
function message() {
|
|
380
|
-
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
381
419
|
const dots = frames[frame++ % frames.length].padEnd(3, ' ');
|
|
382
|
-
return
|
|
420
|
+
return color.dim(`${phase}${dots}`);
|
|
383
421
|
}
|
|
384
422
|
|
|
385
|
-
function render() {
|
|
423
|
+
function render({ force = false } = {}) {
|
|
386
424
|
if (!interactive) {
|
|
387
|
-
if (
|
|
388
|
-
|
|
425
|
+
if (force && phase !== printedPhase) {
|
|
426
|
+
printedPhase = phase;
|
|
389
427
|
info(message().trimEnd());
|
|
390
428
|
}
|
|
391
429
|
return;
|
|
@@ -394,12 +432,15 @@ function createDeployIndicator({ start, getPhase }) {
|
|
|
394
432
|
}
|
|
395
433
|
|
|
396
434
|
return {
|
|
397
|
-
start() {
|
|
435
|
+
start(initialPhase = 'doing') {
|
|
436
|
+
phase = initialPhase;
|
|
398
437
|
render();
|
|
399
438
|
timer = setInterval(render, 1000);
|
|
400
439
|
},
|
|
401
|
-
|
|
402
|
-
if (
|
|
440
|
+
setPhase(nextPhase) {
|
|
441
|
+
if (!nextPhase || nextPhase === phase) return;
|
|
442
|
+
phase = nextPhase;
|
|
443
|
+
render({ force: true });
|
|
403
444
|
},
|
|
404
445
|
stop() {
|
|
405
446
|
if (timer) clearInterval(timer);
|
package/lib/version.mjs
CHANGED
package/package.json
CHANGED