@faable/faable 1.14.0 → 1.15.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
CHANGED
|
@@ -79,11 +79,19 @@ class FaableApi {
|
|
|
79
79
|
async createDeployment(params) {
|
|
80
80
|
return data(this.client.post(`/deployment`, params));
|
|
81
81
|
}
|
|
82
|
-
// Phase transitions the CLI owns (
|
|
83
|
-
// Runtime phases stay controller-territory.
|
|
82
|
+
// Phase transitions the CLI owns (BUILDING when the build starts,
|
|
83
|
+
// BUILD_ERROR on a failed build). Runtime phases stay controller-territory.
|
|
84
84
|
async updateDeploymentStatus(deployment_id, status) {
|
|
85
85
|
return data(this.client.post(`/status/${deployment_id}`, status));
|
|
86
86
|
}
|
|
87
|
+
// Complete a create-first deployment with the built image (write-once
|
|
88
|
+
// server-side). Setting the image is what makes the controller claim the
|
|
89
|
+
// deployment and materialize it.
|
|
90
|
+
async completeDeployment(deployment_id, image) {
|
|
91
|
+
return data(this.client.post(`/deployment/${deployment_id}`, {
|
|
92
|
+
image,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
87
95
|
// Attach the captured build/deploy output to a deployment. The base client
|
|
88
96
|
// timeout (10s) is too short for a multi-MB body on a slow uplink.
|
|
89
97
|
async uploadDeploymentLogs(deployment_id, body) {
|
|
@@ -8,7 +8,7 @@ import { check_environment } from './check_environment.js';
|
|
|
8
8
|
import { git_context } from './git_context.js';
|
|
9
9
|
import { resolve_app_id } from './resolve_app_id.js';
|
|
10
10
|
import { secrets } from './secrets/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { start_log_sync, mark_build_failure, upload_logs } from './upload_logs.js';
|
|
12
12
|
import { upload_tag } from './upload_tag.js';
|
|
13
13
|
|
|
14
14
|
const deploy = {
|
|
@@ -47,9 +47,19 @@ const deploy = {
|
|
|
47
47
|
const plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
|
|
48
48
|
const app_id = await resolve_app_id(args.app_id, ctx.appId, api, workdir);
|
|
49
49
|
const app = await api.getApp(app_id);
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
50
|
+
// Capture the commit/ref/actor so the deployment records which commit
|
|
51
|
+
// it came from and who pushed it (env in CI, git fallback locally).
|
|
52
|
+
const git = await git_context({ workdir });
|
|
53
|
+
// Create-first: register the deployment BEFORE building. Gate rejections
|
|
54
|
+
// (free-plan quota 429, disabled app 409) surface here, at second 0 —
|
|
55
|
+
// before any build minutes are spent. The row is born QUEUED; the CLI
|
|
56
|
+
// owns it until the built image lands (or the build fails). `type` rides
|
|
57
|
+
// on the create as before — the buildpack plan is already resolved.
|
|
58
|
+
const deployment = await api.createDeployment({
|
|
59
|
+
app_id: app.id,
|
|
60
|
+
type: plan.type,
|
|
61
|
+
...git
|
|
62
|
+
});
|
|
53
63
|
try {
|
|
54
64
|
// Check if we can build docker images
|
|
55
65
|
await check_environment();
|
|
@@ -64,26 +74,33 @@ const deploy = {
|
|
|
64
74
|
if (!buildpack) {
|
|
65
75
|
throw new Error(`No buildpack registered for plan=${plan.buildpack}`);
|
|
66
76
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
image:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
77
|
+
// The build starts now: declare it (QUEUED → BUILDING) and stream the
|
|
78
|
+
// captured output to the deployment while it runs (best-effort).
|
|
79
|
+
await api
|
|
80
|
+
.updateDeploymentStatus(deployment.id, { phase: 'BUILDING' })
|
|
81
|
+
.catch((error) => log.warn(`Could not mark deployment BUILDING: ${error.message}`));
|
|
82
|
+
const stop_log_sync = start_log_sync(api, deployment.id);
|
|
83
|
+
try {
|
|
84
|
+
await buildpack.build({ workdir, config, app, env_vars }, plan);
|
|
85
|
+
// Upload to Faable registry
|
|
86
|
+
const { upload_tagname } = await upload_tag({ app, api });
|
|
87
|
+
// Complete the deployment with the built image — this is the handoff:
|
|
88
|
+
// the controller claims it and materializes the workload.
|
|
89
|
+
await api.completeDeployment(deployment.id, upload_tagname);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
stop_log_sync();
|
|
93
|
+
}
|
|
81
94
|
}
|
|
82
95
|
catch (error) {
|
|
83
|
-
|
|
96
|
+
// The deployment already exists (created pre-build), so a failed build
|
|
97
|
+
// marks THAT row BUILD_ERROR with the captured logs — no extra row, no
|
|
98
|
+
// second quota hit. Gate rejections can't reach here anymore: the
|
|
99
|
+
// create happens before any building.
|
|
100
|
+
await mark_build_failure(api, { deployment_id: deployment.id, app });
|
|
84
101
|
throw error;
|
|
85
102
|
}
|
|
86
|
-
// Attach the build output to the deployment (best-effort).
|
|
103
|
+
// Attach the final build output to the deployment (best-effort).
|
|
87
104
|
await upload_logs(api, deployment.id);
|
|
88
105
|
const dashboard_url = `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`;
|
|
89
106
|
log.info(`Preparing to deploy in faable cloud · ${deployment.id}`);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { buildLog } from '../../lib/log_buffer.js';
|
|
2
2
|
import { log } from '../../log.js';
|
|
3
|
-
import { git_context } from './git_context.js';
|
|
4
3
|
|
|
5
4
|
// Best-effort by design: attaching logs (or recording a failed build) must
|
|
6
5
|
// never break or fail a deploy — an older API without these endpoints just
|
|
@@ -17,24 +16,52 @@ const upload_logs = async (api, deployment_id) => {
|
|
|
17
16
|
log.warn(`Could not upload build logs (non-fatal): ${error.message}`);
|
|
18
17
|
}
|
|
19
18
|
};
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
19
|
+
// Live log sync during the build: re-upload the whole buffer every interval
|
|
20
|
+
// while it grows. The API endpoint is an idempotent replace (one log row per
|
|
21
|
+
// deployment), so partial uploads are simply superseded — the dashboard sees
|
|
22
|
+
// the build progressing instead of only the final dump. Rate-limit friendly:
|
|
23
|
+
// 10s interval = 6 req/min against the endpoint's 30/min cap.
|
|
24
|
+
const start_log_sync = (api, deployment_id, intervalMs = 10_000) => {
|
|
25
|
+
let lastSize = 0;
|
|
26
|
+
let inFlight = false;
|
|
27
|
+
const timer = setInterval(async () => {
|
|
28
|
+
if (inFlight)
|
|
29
|
+
return;
|
|
30
|
+
const { content, truncated, size } = buildLog.contents();
|
|
31
|
+
if (!content || size === lastSize)
|
|
32
|
+
return;
|
|
33
|
+
inFlight = true;
|
|
34
|
+
try {
|
|
35
|
+
await api.uploadDeploymentLogs(deployment_id, { content, truncated });
|
|
36
|
+
lastSize = size;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// Quiet: the final upload_logs still runs at the end of the deploy.
|
|
40
|
+
log.debug(`Log sync failed (non-fatal): ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
inFlight = false;
|
|
44
|
+
}
|
|
45
|
+
}, intervalMs);
|
|
46
|
+
// Never keep the process alive just for log syncing.
|
|
47
|
+
timer.unref?.();
|
|
48
|
+
return () => clearInterval(timer);
|
|
49
|
+
};
|
|
50
|
+
// Mark the create-first deployment as a failed build (phase BUILD_ERROR) and
|
|
51
|
+
// attach the captured logs. The deployment row already exists — created
|
|
52
|
+
// before the build started — so unlike the old flow no extra row is created
|
|
53
|
+
// (and no quota is consumed) to record the failure.
|
|
54
|
+
const mark_build_failure = async (api, { deployment_id, app, }) => {
|
|
24
55
|
try {
|
|
25
|
-
const git = await git_context({ workdir }).catch(() => ({}));
|
|
26
|
-
// No `image` and no `type`: a failed build must not materialize anything
|
|
27
|
-
// nor rewrite the app's runtime_strategy.
|
|
28
|
-
const failed = await api.createDeployment({ app_id: app.id, ...git });
|
|
29
56
|
await api
|
|
30
|
-
.updateDeploymentStatus(
|
|
57
|
+
.updateDeploymentStatus(deployment_id, { phase: "BUILD_ERROR" })
|
|
31
58
|
.catch((error) => log.warn(`Could not mark deployment BUILD_ERROR: ${error.message}`));
|
|
32
|
-
await upload_logs(api,
|
|
33
|
-
log.error(`❌ Build failed — logs attached to deployment ${
|
|
59
|
+
await upload_logs(api, deployment_id);
|
|
60
|
+
log.error(`❌ Build failed — logs attached to deployment ${deployment_id} · https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`);
|
|
34
61
|
}
|
|
35
62
|
catch (error) {
|
|
36
63
|
log.warn(`Could not report the failed build (non-fatal): ${error.message}`);
|
|
37
64
|
}
|
|
38
65
|
};
|
|
39
66
|
|
|
40
|
-
export {
|
|
67
|
+
export { mark_build_failure, start_log_sync, upload_logs };
|