@faable/faable 1.28.0 → 1.30.0
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/dist/api/FaableApi.js +24 -0
- package/dist/commands/deploy/index.js +14 -0
- package/dist/commands/deploy/inspect/deployments.js +40 -0
- package/dist/commands/deploy/inspect/format.js +60 -0
- package/dist/commands/deploy/inspect/list.js +24 -0
- package/dist/commands/deploy/inspect/logs.js +102 -0
- package/dist/commands/deploy/inspect/open.js +37 -0
- package/dist/commands/deploy/inspect/redeploy.js +43 -0
- package/dist/commands/deploy/inspect/status.js +46 -0
- package/dist/commands/deploy/inspect/trigger.js +28 -0
- package/dist/commands/login/index.js +2 -2
- package/package.json +1 -1
package/dist/api/FaableApi.js
CHANGED
|
@@ -169,6 +169,30 @@ class FaableApi {
|
|
|
169
169
|
async getMe() {
|
|
170
170
|
return data(this.client.get(`/auth/me`));
|
|
171
171
|
}
|
|
172
|
+
// Runtime logs of the app (Loki-backed; last 24h, up to 200 lines, newest
|
|
173
|
+
// first). Optionally scoped to one deployment.
|
|
174
|
+
async getAppLogs(app_id, params = {}) {
|
|
175
|
+
return data(this.client.get(`/app/${app_id}/logs`, { params }));
|
|
176
|
+
}
|
|
177
|
+
// Deployments of an app, newest first (the API's list index sorts
|
|
178
|
+
// createdAt desc). Team pinned via header — same reason as domains.
|
|
179
|
+
async listDeployments(app_id, team) {
|
|
180
|
+
return firstPage(data(this.client.get(`/deployment`, {
|
|
181
|
+
params: { app_id },
|
|
182
|
+
headers: { "x-faable-team": team },
|
|
183
|
+
})));
|
|
184
|
+
}
|
|
185
|
+
// Build and deploy the current head of the deploy branch server-side —
|
|
186
|
+
// the same path a push webhook takes, same-commit dedupe included.
|
|
187
|
+
async deployNow(app_id) {
|
|
188
|
+
return data(this.client.post(`/app/${app_id}/deploy`));
|
|
189
|
+
}
|
|
190
|
+
// Rebuild a failed deployment from its recorded source (CAS manifest or
|
|
191
|
+
// git ref). The API enforces the guards: failed phase only, never older
|
|
192
|
+
// than what production serves.
|
|
193
|
+
async redeployDeployment(deployment_id, team) {
|
|
194
|
+
return data(this.client.post(`/deployment/${deployment_id}/redeploy`, undefined, { headers: { "x-faable-team": team } }));
|
|
195
|
+
}
|
|
172
196
|
// Domains are team-scoped rows; a CLI user token carries no default team,
|
|
173
197
|
// so every call pins the app's team via `x-faable-team` (same pattern as
|
|
174
198
|
// createSecretsBatch).
|
|
@@ -4,6 +4,13 @@ import { log } from '../../log.js';
|
|
|
4
4
|
import { link } from '../link/index.js';
|
|
5
5
|
import { domains } from './domains/index.js';
|
|
6
6
|
import { git_context } from './git_context.js';
|
|
7
|
+
import { deployments } from './inspect/deployments.js';
|
|
8
|
+
import { apps_list } from './inspect/list.js';
|
|
9
|
+
import { logs } from './inspect/logs.js';
|
|
10
|
+
import { open_app } from './inspect/open.js';
|
|
11
|
+
import { redeploy } from './inspect/redeploy.js';
|
|
12
|
+
import { status } from './inspect/status.js';
|
|
13
|
+
import { trigger } from './inspect/trigger.js';
|
|
7
14
|
import { propose_release } from './release_version.js';
|
|
8
15
|
import { deploy_remote } from './remote/index.js';
|
|
9
16
|
import { resolve_app_id } from './resolve_app_id.js';
|
|
@@ -19,6 +26,13 @@ const deploy = {
|
|
|
19
26
|
return yargs
|
|
20
27
|
.command(secrets)
|
|
21
28
|
.command(domains)
|
|
29
|
+
.command(logs)
|
|
30
|
+
.command(status)
|
|
31
|
+
.command(apps_list)
|
|
32
|
+
.command(deployments)
|
|
33
|
+
.command(open_app)
|
|
34
|
+
.command(trigger)
|
|
35
|
+
.command(redeploy)
|
|
22
36
|
.command(link)
|
|
23
37
|
.positional('app_id', {
|
|
24
38
|
type: 'string',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
import { deployment_row } from './format.js';
|
|
5
|
+
|
|
6
|
+
const deployments = {
|
|
7
|
+
command: 'deployments',
|
|
8
|
+
describe: 'List recent deployments of the app',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.option('app', {
|
|
11
|
+
alias: 'a',
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
14
|
+
})
|
|
15
|
+
.option('limit', {
|
|
16
|
+
alias: 'n',
|
|
17
|
+
type: 'number',
|
|
18
|
+
default: 10,
|
|
19
|
+
description: 'How many to show'
|
|
20
|
+
})
|
|
21
|
+
.showHelpOnFail(false),
|
|
22
|
+
handler: async (args) => {
|
|
23
|
+
const ctx = await requireApi();
|
|
24
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
25
|
+
const app = await ctx.api.getApp(app_id);
|
|
26
|
+
const rows = await ctx.api.listDeployments(app_id, app.team);
|
|
27
|
+
if (rows.length === 0) {
|
|
28
|
+
log.info(`📭 ${app.name} has no deployments yet.`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const shown = rows.slice(0, args.limit ?? 10);
|
|
32
|
+
log.info(`🚀 Last ${shown.length} deployment(s) of ${app.name}:`);
|
|
33
|
+
for (const d of shown) {
|
|
34
|
+
const live = d.id === app.status?.deployment ? ' ← live' : '';
|
|
35
|
+
log.info(` ${deployment_row(d)}${live}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { deployments };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Pure formatting helpers for the read commands (logs/status/list/
|
|
2
|
+
// deployments), kept out of the handlers for tests.
|
|
3
|
+
const PHASE_ICONS = {
|
|
4
|
+
READY: '🟢',
|
|
5
|
+
INITIALIZING: '🔵',
|
|
6
|
+
BUILDING: '🔵',
|
|
7
|
+
QUEUED: '⚪',
|
|
8
|
+
UNKNOWN: '⚪',
|
|
9
|
+
QUOTA_HOLD: '🟡',
|
|
10
|
+
SUPERSEDED: '⚪',
|
|
11
|
+
CANCELED: '⚪',
|
|
12
|
+
TERMINATING: '⚪',
|
|
13
|
+
ERROR: '🔴',
|
|
14
|
+
BUILD_ERROR: '🔴'
|
|
15
|
+
};
|
|
16
|
+
const phase_badge = (phase) => {
|
|
17
|
+
const p = phase || 'UNKNOWN';
|
|
18
|
+
return `${PHASE_ICONS[p] ?? '⚪'} ${p}`;
|
|
19
|
+
};
|
|
20
|
+
// "python 3.11.3 (django)" from the platform-detected metadata; null when
|
|
21
|
+
// nothing was ever detected (no build yet).
|
|
22
|
+
const detected_summary = (detected) => {
|
|
23
|
+
if (!detected)
|
|
24
|
+
return null;
|
|
25
|
+
const runtime = [detected.runtime.name, detected.runtime.version]
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(' ');
|
|
28
|
+
return detected.framework ? `${runtime} (${detected.framework})` : runtime;
|
|
29
|
+
};
|
|
30
|
+
// Loki serves [ns_timestamp, text, deployment_id] newest first; render
|
|
31
|
+
// oldest first (reading order) with an ISO second-precision prefix.
|
|
32
|
+
const format_log_lines = (lines) => [...lines]
|
|
33
|
+
.sort((a, b) => Number(a[0]) - Number(b[0]))
|
|
34
|
+
.map(([ts, text]) => {
|
|
35
|
+
const iso = new Date(Number(ts) / 1e6).toISOString().replace(/\.\d+Z$/, 'Z');
|
|
36
|
+
return `${iso} ${text.replace(/\n+$/, '')}`;
|
|
37
|
+
});
|
|
38
|
+
const short_commit = (sha) => sha ? sha.slice(0, 7) : '-';
|
|
39
|
+
const when = (iso) => {
|
|
40
|
+
if (!iso)
|
|
41
|
+
return '-';
|
|
42
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
43
|
+
const minutes = Math.floor(ms / 60_000);
|
|
44
|
+
if (minutes < 1)
|
|
45
|
+
return 'just now';
|
|
46
|
+
if (minutes < 60)
|
|
47
|
+
return `${minutes}m ago`;
|
|
48
|
+
const hours = Math.floor(minutes / 60);
|
|
49
|
+
if (hours < 48)
|
|
50
|
+
return `${hours}h ago`;
|
|
51
|
+
return `${Math.floor(hours / 24)}d ago`;
|
|
52
|
+
};
|
|
53
|
+
// One row per deployment for the `deployments` table.
|
|
54
|
+
const deployment_row = (d) => {
|
|
55
|
+
const release = d.release ? ` ${d.release}` : '';
|
|
56
|
+
const trigger = d.trigger === 'webhook' ? 'push' : 'cli';
|
|
57
|
+
return `${phase_badge(d.status?.phase)} ${d.id} ${short_commit(d.github_commit)}${release} (${trigger}, ${when(d.createdAt)})`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { deployment_row, detected_summary, format_log_lines, phase_badge, short_commit, when };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { phase_badge } from './format.js';
|
|
4
|
+
|
|
5
|
+
const apps_list = {
|
|
6
|
+
command: 'list',
|
|
7
|
+
describe: 'List your apps',
|
|
8
|
+
builder: yargs => yargs.showHelpOnFail(false),
|
|
9
|
+
handler: async () => {
|
|
10
|
+
const ctx = await requireApi();
|
|
11
|
+
const apps = await ctx.api.list();
|
|
12
|
+
if (apps.length === 0) {
|
|
13
|
+
log.info(`📭 No apps yet. Create one in the dashboard (https://dashboard.faable.com) and link your repo.`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
log.info(`📦 ${apps.length} app(s):`);
|
|
17
|
+
const width = Math.max(...apps.map(a => a.name.length));
|
|
18
|
+
for (const app of apps) {
|
|
19
|
+
log.info(` ${app.name.padEnd(width)} ${phase_badge(app.status?.phase)} ${app.id} https://${app.url}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { apps_list };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { follow_remote_build } from '../remote/follow.js';
|
|
4
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
5
|
+
import { format_log_lines } from './format.js';
|
|
6
|
+
|
|
7
|
+
// Phases where the build is still producing output; anything else is a frozen
|
|
8
|
+
// record and following would just wait on nothing.
|
|
9
|
+
const FOLLOWABLE_PHASES = new Set(['UNKNOWN', 'QUEUED', 'BUILDING']);
|
|
10
|
+
const logs = {
|
|
11
|
+
command: 'logs',
|
|
12
|
+
describe: 'Show runtime logs of the app (or build logs with --build)',
|
|
13
|
+
builder: yargs => yargs
|
|
14
|
+
.option('app', {
|
|
15
|
+
alias: 'a',
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
18
|
+
})
|
|
19
|
+
.option('build', {
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
default: false,
|
|
22
|
+
description: 'Show the build output of the latest deployment instead'
|
|
23
|
+
})
|
|
24
|
+
.option('deployment', {
|
|
25
|
+
alias: 'd',
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Scope to one deployment id'
|
|
28
|
+
})
|
|
29
|
+
.option('follow', {
|
|
30
|
+
alias: 'f',
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
default: false,
|
|
33
|
+
description: 'With --build: keep tailing the output while the build runs'
|
|
34
|
+
})
|
|
35
|
+
.example('$0 deploy logs', 'Runtime logs of the linked app (last 24h)')
|
|
36
|
+
.example('$0 deploy logs --build', 'Build output of the latest deployment')
|
|
37
|
+
.example('$0 deploy logs --build --follow', 'Tail the build that is running right now')
|
|
38
|
+
.showHelpOnFail(false),
|
|
39
|
+
handler: async (args) => {
|
|
40
|
+
if (args.follow && !args.build) {
|
|
41
|
+
// Runtime logs have no follow mode (the API serves a 24h window, not a
|
|
42
|
+
// stream) — only the build output can be tailed.
|
|
43
|
+
throw new Error('--follow only works with --build');
|
|
44
|
+
}
|
|
45
|
+
const ctx = await requireApi();
|
|
46
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
47
|
+
const app = await ctx.api.getApp(app_id);
|
|
48
|
+
if (args.build) {
|
|
49
|
+
// Build output lives on the deployment. Default to the newest one —
|
|
50
|
+
// exactly what you want after a red `faable deploy`.
|
|
51
|
+
let deployment_id = args.deployment;
|
|
52
|
+
if (!deployment_id) {
|
|
53
|
+
const deployments = await ctx.api.listDeployments(app_id, app.team);
|
|
54
|
+
deployment_id = deployments[0]?.id;
|
|
55
|
+
if (!deployment_id) {
|
|
56
|
+
log.info(`📭 ${app.name} has no deployments yet.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (args.follow) {
|
|
61
|
+
// Live tail (the builder re-uploads the log every ~10s): same loop the
|
|
62
|
+
// deploy command uses, so it ends at the image handoff and exits red
|
|
63
|
+
// on BUILD_ERROR. On an already-settled deployment fall through to the
|
|
64
|
+
// recorded snapshot instead of waiting on nothing.
|
|
65
|
+
const deployment = await ctx.api.getDeployment(deployment_id);
|
|
66
|
+
const phase = deployment?.status?.phase ?? '';
|
|
67
|
+
if (FOLLOWABLE_PHASES.has(phase)) {
|
|
68
|
+
log.info(`🏗️ Following the build of ${deployment_id}:`);
|
|
69
|
+
await follow_remote_build(ctx.api, deployment_id);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
log.info(`Build of ${deployment_id} already finished (${phase}) — showing the recorded output.`);
|
|
73
|
+
}
|
|
74
|
+
const build = await ctx.api.getDeploymentLogs(deployment_id);
|
|
75
|
+
if (!build.content) {
|
|
76
|
+
log.info(`📭 No build output recorded for ${deployment_id}.`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
log.info(`🏗️ Build output of ${deployment_id}:`);
|
|
80
|
+
process.stdout.write(build.content);
|
|
81
|
+
if (!build.content.endsWith('\n'))
|
|
82
|
+
process.stdout.write('\n');
|
|
83
|
+
if (build.truncated)
|
|
84
|
+
log.warn(`(output truncated)`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const lines = await ctx.api.getAppLogs(app_id, {
|
|
88
|
+
deployment_id: args.deployment
|
|
89
|
+
});
|
|
90
|
+
if (lines.length === 0) {
|
|
91
|
+
log.info(`📭 No runtime logs in the last 24h for ${app.name} (${app_id}).`);
|
|
92
|
+
log.info(`For build output, use: faable deploy logs --build`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
log.info(`📜 Runtime logs of ${app.name} (last 24h, newest last):`);
|
|
96
|
+
for (const line of format_log_lines(lines)) {
|
|
97
|
+
process.stdout.write(line + '\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export { logs };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import openBrowser from 'open';
|
|
2
|
+
import { requireApi } from '../../../api/context.js';
|
|
3
|
+
import { log } from '../../../log.js';
|
|
4
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
5
|
+
|
|
6
|
+
const open_app = {
|
|
7
|
+
command: 'open',
|
|
8
|
+
describe: 'Open the app in the browser',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.option('app', {
|
|
11
|
+
alias: 'a',
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
14
|
+
})
|
|
15
|
+
.option('dashboard', {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
default: false,
|
|
18
|
+
description: 'Open the Faable dashboard page of the app instead'
|
|
19
|
+
})
|
|
20
|
+
.example('$0 deploy open', 'Open the live app URL')
|
|
21
|
+
.example('$0 deploy open --dashboard', 'Open the app in the dashboard')
|
|
22
|
+
.showHelpOnFail(false),
|
|
23
|
+
handler: async (args) => {
|
|
24
|
+
const ctx = await requireApi();
|
|
25
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
26
|
+
const app = await ctx.api.getApp(app_id);
|
|
27
|
+
const url = args.dashboard
|
|
28
|
+
? `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`
|
|
29
|
+
: `https://${app.url}`;
|
|
30
|
+
log.info(`🌍 Opening ${url}`);
|
|
31
|
+
await openBrowser(url).catch(() => {
|
|
32
|
+
log.warn(`Could not open the browser automatically — visit: ${url}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { open_app };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
|
|
5
|
+
const FAILED_PHASES = new Set(['ERROR', 'BUILD_ERROR']);
|
|
6
|
+
const redeploy = {
|
|
7
|
+
command: 'redeploy [deployment]',
|
|
8
|
+
describe: 'Rebuild a failed deployment from its recorded source',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.positional('deployment', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Deployment id to rebuild (defaults to the latest failed one)'
|
|
13
|
+
})
|
|
14
|
+
.option('app', {
|
|
15
|
+
alias: 'a',
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
18
|
+
})
|
|
19
|
+
.example('$0 deploy redeploy', 'Retry the latest failed deployment')
|
|
20
|
+
.showHelpOnFail(false),
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
const ctx = await requireApi();
|
|
23
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
24
|
+
const app = await ctx.api.getApp(app_id);
|
|
25
|
+
let deployment_id = args.deployment;
|
|
26
|
+
if (!deployment_id) {
|
|
27
|
+
const rows = await ctx.api.listDeployments(app_id, app.team);
|
|
28
|
+
const failed = rows.find(d => FAILED_PHASES.has(d.status?.phase ?? ''));
|
|
29
|
+
if (!failed) {
|
|
30
|
+
log.info(`✅ No failed deployments to retry for ${app.name}. To rebuild the repo HEAD use: faable deploy trigger`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
deployment_id = failed.id;
|
|
34
|
+
}
|
|
35
|
+
// The API enforces the guards (failed phase only, never older than what
|
|
36
|
+
// production serves) and answers with an actionable refusal otherwise.
|
|
37
|
+
const clone = await ctx.api.redeployDeployment(deployment_id, app.team);
|
|
38
|
+
log.info(`🔁 Rebuilding ${deployment_id} as ${clone.id}.`);
|
|
39
|
+
log.info(`Track it with: faable deploy status · build output: faable deploy logs --build`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { redeploy };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
import { phase_badge, detected_summary, short_commit, when } from './format.js';
|
|
5
|
+
|
|
6
|
+
const status = {
|
|
7
|
+
command: 'status',
|
|
8
|
+
describe: 'Show what is live for the app',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.option('app', {
|
|
11
|
+
alias: 'a',
|
|
12
|
+
type: 'string',
|
|
13
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
14
|
+
})
|
|
15
|
+
.showHelpOnFail(false),
|
|
16
|
+
handler: async (args) => {
|
|
17
|
+
const ctx = await requireApi();
|
|
18
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
19
|
+
const app = await ctx.api.getApp(app_id);
|
|
20
|
+
const deployments = await ctx.api.listDeployments(app_id, app.team);
|
|
21
|
+
const latest = deployments[0];
|
|
22
|
+
log.info(`${phase_badge(app.status?.phase)} ${app.name} (${app.id})`);
|
|
23
|
+
log.info(` URL: https://${app.url}`);
|
|
24
|
+
const stack = detected_summary(app.detected);
|
|
25
|
+
if (stack)
|
|
26
|
+
log.info(` Stack: ${stack}`);
|
|
27
|
+
if (app.repository) {
|
|
28
|
+
log.info(` Repository: ${app.repository} (${app.github_branch || 'main'}${app.deploy_trigger === 'webhook' ? ', push-to-deploy' : ''})`);
|
|
29
|
+
}
|
|
30
|
+
if (app.status?.deployment) {
|
|
31
|
+
log.info(` Live: ${app.status.deployment}`);
|
|
32
|
+
}
|
|
33
|
+
if (latest && latest.id !== app.status?.deployment) {
|
|
34
|
+
log.info(` Latest: ${latest.id} — ${phase_badge(latest.status?.phase)} (${short_commit(latest.github_commit)}, ${when(latest.createdAt)})`);
|
|
35
|
+
if (latest.status?.reason) {
|
|
36
|
+
log.info(` Reason: ${latest.status.reason.split('\n')[0]}`);
|
|
37
|
+
log.info(` Full error: faable deploy logs --build`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (deployments.length === 0) {
|
|
41
|
+
log.info(` No deployments yet — push to deploy, or run: faable deploy`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { status };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { log } from '../../../log.js';
|
|
3
|
+
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
|
+
|
|
5
|
+
const trigger = {
|
|
6
|
+
command: 'trigger',
|
|
7
|
+
describe: 'Build and deploy the latest commit of the deploy branch, server-side',
|
|
8
|
+
builder: yargs => yargs
|
|
9
|
+
.option('app', {
|
|
10
|
+
alias: 'a',
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
13
|
+
})
|
|
14
|
+
.example('$0 deploy trigger', 'Deploy the repo HEAD without uploading anything from this machine')
|
|
15
|
+
.showHelpOnFail(false),
|
|
16
|
+
handler: async (args) => {
|
|
17
|
+
const ctx = await requireApi();
|
|
18
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
19
|
+
const app = await ctx.api.getApp(app_id);
|
|
20
|
+
// Takes the exact same path a git push would (same-commit dedupe
|
|
21
|
+
// included) — the API answers with an actionable refusal otherwise.
|
|
22
|
+
const result = await ctx.api.deployNow(app_id);
|
|
23
|
+
log.info(`🚀 Building ${result.commit.slice(0, 7)} (${result.branch}) of ${app.name} server-side.`);
|
|
24
|
+
log.info(`Track it with: faable deploy status · or in the dashboard: https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { trigger };
|
|
@@ -2,7 +2,7 @@ import { FaableApi } from '../../api/FaableApi.js';
|
|
|
2
2
|
import { getDeviceCode, getDeviceToken, getMe } from '../../api/auth.js';
|
|
3
3
|
import { isTokenLive } from '../../api/session.js';
|
|
4
4
|
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
5
|
-
import
|
|
5
|
+
import openBrowser from 'open';
|
|
6
6
|
import ora from 'ora';
|
|
7
7
|
import prompts from 'prompts';
|
|
8
8
|
import { log } from '../../log.js';
|
|
@@ -120,7 +120,7 @@ const login = {
|
|
|
120
120
|
process.stdout.write(renderUserCodeBlock(user_code));
|
|
121
121
|
log.info(`If your browser doesn't open automatically, visit: ${verification_uri}`);
|
|
122
122
|
try {
|
|
123
|
-
await
|
|
123
|
+
await openBrowser(verification_uri_complete);
|
|
124
124
|
}
|
|
125
125
|
catch {
|
|
126
126
|
log.warn("Could not open browser automatically.");
|