@faable/faable 1.37.0 → 1.38.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 +6 -0
- package/dist/commands/deploy/index.js +2 -0
- package/dist/commands/deploy/inspect/deployments.js +1 -0
- package/dist/commands/deploy/inspect/detail.js +122 -0
- package/dist/commands/deploy/inspect/inspect.js +67 -0
- package/dist/commands/deploy/inspect/logs.js +9 -3
- package/package.json +1 -1
package/dist/api/FaableApi.js
CHANGED
|
@@ -151,6 +151,12 @@ class FaableApi {
|
|
|
151
151
|
async getDeployment(deployment_id) {
|
|
152
152
|
return data(this.client.get(`/deployment/${deployment_id}`));
|
|
153
153
|
}
|
|
154
|
+
// Build payload of a deployment (runnable descriptor: runtime, profile,
|
|
155
|
+
// size, checksum). Authorized by the caller's access to the parent
|
|
156
|
+
// deployment, so no team header is needed — same posture as getDeployment.
|
|
157
|
+
async getArtifact(artifact_id) {
|
|
158
|
+
return data(this.client.get(`/artifact/${artifact_id}`));
|
|
159
|
+
}
|
|
154
160
|
// Attach the captured build/deploy output to a deployment. The base client
|
|
155
161
|
// timeout (10s) is too short for a multi-MB body on a slow uplink.
|
|
156
162
|
async uploadDeploymentLogs(deployment_id, body) {
|
|
@@ -5,6 +5,7 @@ import { link } from '../link/index.js';
|
|
|
5
5
|
import { domains } from './domains/index.js';
|
|
6
6
|
import { git_context } from './git_context.js';
|
|
7
7
|
import { deployments } from './inspect/deployments.js';
|
|
8
|
+
import { inspect } from './inspect/inspect.js';
|
|
8
9
|
import { apps_list } from './inspect/list.js';
|
|
9
10
|
import { logs } from './inspect/logs.js';
|
|
10
11
|
import { open_app } from './inspect/open.js';
|
|
@@ -32,6 +33,7 @@ const deploy = {
|
|
|
32
33
|
.command(status)
|
|
33
34
|
.command(apps_list)
|
|
34
35
|
.command(deployments)
|
|
36
|
+
.command(inspect)
|
|
35
37
|
.command(open_app)
|
|
36
38
|
.command(trigger)
|
|
37
39
|
.command(redeploy)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { phase_badge, when, short_commit, detected_summary } from './format.js';
|
|
2
|
+
|
|
3
|
+
// Pure rendering of `faable deploy inspect` — everything the platform
|
|
4
|
+
// recorded about ONE deployment, in reading order. Kept out of the handler
|
|
5
|
+
// so the layout is testable without an API.
|
|
6
|
+
const format_bytes = (bytes) => {
|
|
7
|
+
if (!bytes || bytes < 0)
|
|
8
|
+
return '-';
|
|
9
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
10
|
+
let value = bytes;
|
|
11
|
+
let unit = 0;
|
|
12
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
13
|
+
value /= 1024;
|
|
14
|
+
unit++;
|
|
15
|
+
}
|
|
16
|
+
return `${unit === 0 ? value : value.toFixed(1)} ${units[unit]}`;
|
|
17
|
+
};
|
|
18
|
+
// "refs/heads/main" → "main"; a bare branch name passes through.
|
|
19
|
+
const short_ref = (ref) => ref ? ref.replace(/^refs\/(heads|tags)\//, '') : null;
|
|
20
|
+
// "next-standalone · node 22 · 41.2 MB" from the deploy-v3 runnable
|
|
21
|
+
// descriptor; null while the build has not sealed one yet.
|
|
22
|
+
const artifact_summary = (artifact) => {
|
|
23
|
+
const a = artifact?.artifact;
|
|
24
|
+
if (!a)
|
|
25
|
+
return null;
|
|
26
|
+
const runtime = [a.runtime.name, a.runtime.version].filter(Boolean).join(' ');
|
|
27
|
+
return [a.profile, runtime, format_bytes(a.size)].filter(Boolean).join(' · ');
|
|
28
|
+
};
|
|
29
|
+
// First line of a commit message (the subject), quoted.
|
|
30
|
+
const commit_subject = (message) => {
|
|
31
|
+
const subject = message?.split('\n')[0].trim();
|
|
32
|
+
return subject ? `"${subject}"` : null;
|
|
33
|
+
};
|
|
34
|
+
const label = (name, value) => ` ${`${name}:`.padEnd(12)}${value}`;
|
|
35
|
+
// Command suggestions that match the phase: what you would actually run next,
|
|
36
|
+
// rendered as labeled lines so they stay in the same column as the record.
|
|
37
|
+
// A BUILD_ERROR never ran, so it has no runtime logs to offer; a retired
|
|
38
|
+
// deployment does (for 24h — the runtime window), plus its frozen build output.
|
|
39
|
+
const next_steps = (d, is_live) => {
|
|
40
|
+
const phase = d.status?.phase ?? '';
|
|
41
|
+
const runtime = label('Logs', `faable deploy logs -d ${d.id}`);
|
|
42
|
+
const build = label('Build', `faable deploy logs --build -d ${d.id}`);
|
|
43
|
+
const retry = label('Retry', `faable deploy redeploy ${d.id}`);
|
|
44
|
+
if (phase === 'BUILD_ERROR')
|
|
45
|
+
return [build, retry];
|
|
46
|
+
if (phase === 'ERROR')
|
|
47
|
+
return [runtime, build, retry];
|
|
48
|
+
if (phase === 'BUILDING' || phase === 'QUEUED' || phase === 'UNKNOWN') {
|
|
49
|
+
return [label('Follow', `faable deploy logs --build -d ${d.id} --follow`)];
|
|
50
|
+
}
|
|
51
|
+
if (is_live)
|
|
52
|
+
return [label('Logs', 'faable deploy logs')];
|
|
53
|
+
return [runtime, build];
|
|
54
|
+
};
|
|
55
|
+
const deployment_detail = (args) => {
|
|
56
|
+
const { app, deployment: d, artifact } = args;
|
|
57
|
+
const is_live = d.id === app.status?.deployment;
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push(`${phase_badge(d.status?.phase)} ${d.id}`);
|
|
60
|
+
lines.push(label('App', `${app.name} (${app.id})`));
|
|
61
|
+
if (d.createdAt) {
|
|
62
|
+
lines.push(label('Created', `${when(d.createdAt)} (${d.createdAt})`));
|
|
63
|
+
}
|
|
64
|
+
lines.push(label('Trigger', d.trigger === 'webhook' ? 'push (webhook)' : 'cli'));
|
|
65
|
+
lines.push(label('Serving', is_live ? `yes — https://${app.url}` : 'no (not live)'));
|
|
66
|
+
if (d.github_commit) {
|
|
67
|
+
const attribution = [
|
|
68
|
+
short_ref(d.github_ref),
|
|
69
|
+
d.github_actor && `by ${d.github_actor}`
|
|
70
|
+
]
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(' ');
|
|
73
|
+
const commit = [
|
|
74
|
+
short_commit(d.github_commit),
|
|
75
|
+
commit_subject(d.github_commit_message),
|
|
76
|
+
attribution && `(${attribution})`
|
|
77
|
+
]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join(' ');
|
|
80
|
+
lines.push(label('Commit', commit));
|
|
81
|
+
}
|
|
82
|
+
if (d.release)
|
|
83
|
+
lines.push(label('Release', d.release));
|
|
84
|
+
// Stack the builder detected for THIS build (falls back to the app's, which
|
|
85
|
+
// is the last successful detection).
|
|
86
|
+
const stack = detected_summary(d.detected ?? app.detected);
|
|
87
|
+
if (stack)
|
|
88
|
+
lines.push(label('Stack', stack));
|
|
89
|
+
const runnable = artifact_summary(artifact);
|
|
90
|
+
if (runnable) {
|
|
91
|
+
const ready = d.artifact_ready_at
|
|
92
|
+
? ` (sealed ${when(d.artifact_ready_at)})`
|
|
93
|
+
: '';
|
|
94
|
+
lines.push(label('Artifact', `${runnable}${ready}`));
|
|
95
|
+
}
|
|
96
|
+
if (artifact?.artifact?.start_command) {
|
|
97
|
+
lines.push(label('Start', artifact.artifact.start_command));
|
|
98
|
+
}
|
|
99
|
+
if (artifact?.purged_at) {
|
|
100
|
+
lines.push(label('Purged', `${when(artifact.purged_at)} — source no longer stored`));
|
|
101
|
+
}
|
|
102
|
+
if (d.image)
|
|
103
|
+
lines.push(label('Image', d.image));
|
|
104
|
+
if (d.status?.runtime_image) {
|
|
105
|
+
lines.push(label('Runtime', d.status.runtime_image));
|
|
106
|
+
}
|
|
107
|
+
if (d.redeploy_of)
|
|
108
|
+
lines.push(label('Rebuild of', d.redeploy_of));
|
|
109
|
+
if (d.quota_released_at) {
|
|
110
|
+
lines.push(label('Quota', `hold released ${when(d.quota_released_at)}`));
|
|
111
|
+
}
|
|
112
|
+
if (d.status?.reason) {
|
|
113
|
+
lines.push(' Reason:');
|
|
114
|
+
for (const line of d.status.reason.split('\n')) {
|
|
115
|
+
lines.push(` ${line}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
lines.push(...next_steps(d, is_live));
|
|
119
|
+
return lines;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { artifact_summary, deployment_detail, format_bytes, short_ref };
|
|
@@ -0,0 +1,67 @@
|
|
|
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_detail } from './detail.js';
|
|
5
|
+
|
|
6
|
+
const inspect = {
|
|
7
|
+
command: 'inspect [deployment]',
|
|
8
|
+
describe: 'Show everything recorded about one deployment',
|
|
9
|
+
builder: yargs => yargs
|
|
10
|
+
.positional('deployment', {
|
|
11
|
+
type: 'string',
|
|
12
|
+
description: 'Deployment id (defaults to the latest one of the app)'
|
|
13
|
+
})
|
|
14
|
+
.option('app', {
|
|
15
|
+
alias: 'a',
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'App Identifier (defaults to the linked app)'
|
|
18
|
+
})
|
|
19
|
+
.option('json', {
|
|
20
|
+
type: 'boolean',
|
|
21
|
+
default: false,
|
|
22
|
+
description: 'Output raw JSON (for scripting)'
|
|
23
|
+
})
|
|
24
|
+
.example('$0 deploy inspect deployment_a1b2c3', 'Everything about that deployment: phase, commit, artifact, failure reason')
|
|
25
|
+
.example('$0 deploy inspect', 'Same, for the latest deployment')
|
|
26
|
+
.showHelpOnFail(false),
|
|
27
|
+
handler: async (args) => {
|
|
28
|
+
const ctx = await requireApi();
|
|
29
|
+
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
30
|
+
const app = await ctx.api.getApp(app_id);
|
|
31
|
+
let deployment_id = args.deployment;
|
|
32
|
+
if (!deployment_id) {
|
|
33
|
+
const rows = await ctx.api.listDeployments(app_id, app.team);
|
|
34
|
+
deployment_id = rows[0]?.id;
|
|
35
|
+
if (!deployment_id) {
|
|
36
|
+
log.info(`📭 ${app.name} has no deployments yet.`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const deployment = await ctx.api.getDeployment(deployment_id);
|
|
41
|
+
// A deployment id is globally unique but the app comes from the link/--app
|
|
42
|
+
// flag: refuse to render one against the wrong app instead of printing a
|
|
43
|
+
// mismatched "Serving"/"App" header.
|
|
44
|
+
if (deployment.app_id && deployment.app_id !== app_id) {
|
|
45
|
+
throw new Error(`${deployment_id} belongs to another app (${deployment.app_id}), not ${app.name} (${app_id}). Pass --app ${deployment.app_id}.`);
|
|
46
|
+
}
|
|
47
|
+
// The runnable descriptor (runtime, profile, size) lives on the artifact
|
|
48
|
+
// row. Absent on image deploys, and a purged/expired row must not break
|
|
49
|
+
// the read — the rest of the record is still worth showing.
|
|
50
|
+
let artifact = null;
|
|
51
|
+
if (deployment.artifact_id) {
|
|
52
|
+
artifact = await ctx.api.getArtifact(deployment.artifact_id).catch(() => {
|
|
53
|
+
log.debug(`Could not read artifact ${deployment.artifact_id}`);
|
|
54
|
+
return null;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (args.json) {
|
|
58
|
+
process.stdout.write(JSON.stringify({ ...deployment, artifact }, null, 2) + '\n');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const line of deployment_detail({ app, deployment, artifact })) {
|
|
62
|
+
log.info(line);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export { inspect };
|
|
@@ -33,6 +33,7 @@ const logs = {
|
|
|
33
33
|
description: 'With --build: keep tailing the output while the build runs'
|
|
34
34
|
})
|
|
35
35
|
.example('$0 deploy logs', 'Runtime logs of the linked app (last 24h)')
|
|
36
|
+
.example('$0 deploy logs -d deployment_a1b2c3', 'Runtime logs of ONE deployment (last 24h)')
|
|
36
37
|
.example('$0 deploy logs --build', 'Build output of the latest deployment')
|
|
37
38
|
.example('$0 deploy logs --build --follow', 'Tail the build that is running right now')
|
|
38
39
|
.showHelpOnFail(false),
|
|
@@ -87,12 +88,17 @@ const logs = {
|
|
|
87
88
|
const lines = await ctx.api.getAppLogs(app_id, {
|
|
88
89
|
deployment_id: args.deployment
|
|
89
90
|
});
|
|
91
|
+
// Say WHICH scope came back empty: with -d the 24h window is usually the
|
|
92
|
+
// reason (a retired deployment stopped writing when it stopped serving).
|
|
93
|
+
const scope = args.deployment
|
|
94
|
+
? `${args.deployment} (${app.name})`
|
|
95
|
+
: `${app.name} (${app_id})`;
|
|
90
96
|
if (lines.length === 0) {
|
|
91
|
-
log.info(`📭 No runtime logs in the last 24h for ${
|
|
92
|
-
log.info(`For build output, use: faable deploy logs --build`);
|
|
97
|
+
log.info(`📭 No runtime logs in the last 24h for ${scope}.`);
|
|
98
|
+
log.info(`For build output, use: faable deploy logs --build${args.deployment ? ` -d ${args.deployment}` : ''}`);
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
95
|
-
log.info(`📜 Runtime logs of ${app.name} (last 24h, newest last):`);
|
|
101
|
+
log.info(`📜 Runtime logs of ${app.name}${args.deployment ? ` · ${args.deployment}` : ''} (last 24h, newest last):`);
|
|
96
102
|
for (const line of format_log_lines(lines)) {
|
|
97
103
|
process.stdout.write(line + '\n');
|
|
98
104
|
}
|