@faable/faable 4.18.2 → 4.18.4

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.
@@ -1,5 +1,6 @@
1
1
  import openBrowser from 'open';
2
2
  import { requireApi } from '../../../api/context.js';
3
+ import { dashboardAppUrl } from '../../../lib/dashboard_url.js';
3
4
  import { log } from '../../../log.js';
4
5
  import { resolve_app_id } from '../resolve_app_id.js';
5
6
 
@@ -25,7 +26,7 @@ const open_app = {
25
26
  const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
26
27
  const app = await ctx.api.getApp(app_id);
27
28
  const url = args.dashboard
28
- ? `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`
29
+ ? dashboardAppUrl(String(app.team), app.id)
29
30
  : `https://${app.url}`;
30
31
  log.info(`🌍 Opening ${url}`);
31
32
  await openBrowser(url).catch(() => {
@@ -1,4 +1,5 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
+ import { dashboardAppUrl } from '../../../lib/dashboard_url.js';
2
3
  import { print_json, json_option } from '../../../lib/listing.js';
3
4
  import { log } from '../../../log.js';
4
5
  import { resolve_app_id } from '../resolve_app_id.js';
@@ -55,7 +56,7 @@ const trigger = {
55
56
  if (result.deployment_id) {
56
57
  log.info(`Follow the build: faable deploy logs --build -d ${result.deployment_id} -a ${app.id} --follow`);
57
58
  }
58
- log.info(`Track it with: faable deploy status -a ${app.id} ¡ or in the dashboard: https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`);
59
+ log.info(`Track it with: faable deploy status -a ${app.id} ¡ or in the dashboard: ${dashboardAppUrl(String(app.team), app.id)}`);
59
60
  }
60
61
  };
61
62
 
@@ -1,15 +1,16 @@
1
1
  import { requireApi } from '../../api/context.js';
2
+ import { requireProject } from '../../api/project.js';
2
3
  import { Configuration } from '../../lib/Configuration.js';
4
+ import { dashboardAppUrl } from '../../lib/dashboard_url.js';
3
5
  import { CliError } from '../../lib/errors.js';
4
6
  import { is_non_interactive, confirm } from '../../lib/interactive.js';
5
7
  import { print_json } from '../../lib/listing.js';
6
8
  import { log } from '../../log.js';
7
- import { requireProject } from '../../api/project.js';
9
+ import { assert_name_free } from './apps/name_free.js';
8
10
  import { git_context } from './git_context.js';
9
11
  import { show_deploy_help } from './help.js';
10
12
  import { reachable_timeout_ms, wait_reachable } from './reachable.js';
11
13
  import { propose_release } from './release_version.js';
12
- import { assert_name_free } from './apps/name_free.js';
13
14
  import { deploy_remote } from './remote/index.js';
14
15
  import { collect_manifest } from './remote/manifest.js';
15
16
  import { find_app_id } from './resolve_app_id.js';
@@ -142,7 +143,10 @@ const launch = {
142
143
  // Propose the release version (--release > FAABLE_RELEASE > git tag).
143
144
  // Optional: when absent the platform injects no FAABLE_RELEASE and the
144
145
  // app falls back to its own version source.
145
- const proposed = await propose_release({ workdir, explicit: args.release });
146
+ const proposed = await propose_release({
147
+ workdir,
148
+ explicit: args.release
149
+ });
146
150
  if (proposed) {
147
151
  log.info(`đŸˇī¸ Release: ${proposed.release} (from ${proposed.source})`);
148
152
  }
@@ -165,7 +169,7 @@ const launch = {
165
169
  deployment_started = true;
166
170
  }
167
171
  });
168
- const dashboard_url = `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`;
172
+ const dashboard_url = dashboardAppUrl(String(app.team), app.id);
169
173
  log.info(`Preparing to deploy in faable cloud ¡ ${deployment.id}`);
170
174
  log.info(`📊 View it in the dashboard -> ${dashboard_url}`);
171
175
  // Wait (up to 5 minutes) for the deployment to be promoted (live)
@@ -253,7 +257,10 @@ const launch = {
253
257
  phase: failure.phase,
254
258
  reason: failure.reason,
255
259
  ...(fault
256
- ? { fault_owner: fault.fault_owner, error_code: fault.error_code }
260
+ ? {
261
+ fault_owner: fault.fault_owner,
262
+ error_code: fault.error_code
263
+ }
257
264
  : {})
258
265
  }
259
266
  : {}),
@@ -5,14 +5,34 @@ const POLL_INTERVAL_MS = 4000;
5
5
  // 30min, abandoned janitor 60min) resolve the deployment first in practice.
6
6
  const FOLLOW_TIMEOUT_MS = 45 * 60 * 1000;
7
7
  const wait = (ms) => new Promise((r) => setTimeout(r, ms));
8
+ /** The clone the platform created to retry this failed build, if any. */
9
+ const retry_of = (deployment) => {
10
+ const retry = deployment?.runtime_fallback_retry;
11
+ if (!retry?.deployment_id)
12
+ return null;
13
+ // The api stores the clone as bare hex (a `.lean()` ObjectId).
14
+ const id = retry.deployment_id.startsWith("deployment_")
15
+ ? retry.deployment_id
16
+ : `deployment_${retry.deployment_id}`;
17
+ return { id, to: retry.to };
18
+ };
8
19
  /**
9
20
  * Follow a remote build until the image handoff: poll the phase and tail the
10
21
  * build logs the builder uploads every ~10s (replace semantics → print the
11
22
  * suffix beyond what we already showed). Returns on INITIALIZING/READY
12
23
  * (image landed — the regular promotion poll takes over); throws on
13
24
  * BUILD_ERROR/ERROR with the server-provided reason.
25
+ *
26
+ * One exception to "BUILD_ERROR throws": when the platform already retried
27
+ * the build on the previous Python (`runtime_fallback_retry`, api §12), the
28
+ * failure is not the outcome — the clone's is. Follow the clone instead and
29
+ * return ITS id, so the promotion poll and `--json` speak about the deployment
30
+ * that will go live. Until 2026-10-18 the CLI exited red for a deploy that
31
+ * was about to succeed (backlog §379).
14
32
  */
15
- const follow_remote_build = async (api, deployment_id,
33
+ const follow_remote_build = async (api,
34
+ // `let`: a platform retry switches the follow to its clone.
35
+ initial_deployment_id,
16
36
  // Where the build log goes. stdout for a person; stderr when stdout carries
17
37
  // a JSON document (--json, or a program driving the CLI): one build line in
18
38
  // front of it and the caller can't parse the outcome — the MCP reported a
@@ -20,6 +40,8 @@ const follow_remote_build = async (api, deployment_id,
20
40
  out = process.stdout) => {
21
41
  let printed = 0;
22
42
  let last_phase = "";
43
+ let followed_retry = false;
44
+ let deployment_id = initial_deployment_id;
23
45
  const started = Date.now();
24
46
  const tail_logs = async () => {
25
47
  try {
@@ -49,6 +71,15 @@ out = process.stdout) => {
49
71
  }
50
72
  await tail_logs();
51
73
  if (phase === "BUILD_ERROR" || phase === "ERROR") {
74
+ const retry = retry_of(deployment);
75
+ if (retry && !followed_retry) {
76
+ followed_retry = true;
77
+ log.info(`â†Šī¸ The default Python failed with your dependencies — the platform is retrying on Python ${retry.to} ¡ ${retry.id}`);
78
+ deployment_id = retry.id;
79
+ printed = 0;
80
+ last_phase = "";
81
+ continue;
82
+ }
52
83
  const reason = deployment?.status?.reason;
53
84
  throw new Error(`Remote build failed${reason ? `: ${reason}` : ""}`.trim());
54
85
  }
@@ -56,7 +87,7 @@ out = process.stdout) => {
56
87
  // already live (READY) — the promotion poll takes over from here.
57
88
  if (phase === "INITIALIZING" || phase === "READY") {
58
89
  await tail_logs();
59
- return;
90
+ return { id: deployment_id };
60
91
  }
61
92
  await wait(POLL_INTERVAL_MS);
62
93
  }
@@ -34,7 +34,12 @@ const deploy_remote = async (props) => {
34
34
  });
35
35
  props.on_deployment?.(deployment.id);
36
36
  log.info(`Preparing to build in faable cloud ¡ ${deployment.id}`);
37
- await follow_remote_build(api, deployment.id, props.log_stream);
37
+ const landed = await follow_remote_build(api, deployment.id, props.log_stream);
38
+ // The platform retried the build on the previous Python (§12): what goes
39
+ // live is the clone, so the promotion poll must wait for IT.
40
+ if (landed.id !== deployment.id) {
41
+ return await api.getDeployment(landed.id);
42
+ }
38
43
  return deployment;
39
44
  };
40
45
 
@@ -0,0 +1,11 @@
1
+ // Dashboard links to an app, in the canonical `project_` form.
2
+ //
3
+ // The api returns `app.team` as `team_<hex>`. The dashboard accepts it, but a
4
+ // session that lands on a `team_` URL keeps navigating with it and pays a 400
5
+ // on `/project-favorites/recents` each time — so links leave as `project_`
6
+ // (same rule as the api's emails, arch backlog §387).
7
+ const DASHBOARD = 'https://dashboard.faable.com';
8
+ const toProjectId = (team) => team.replace(/^team_/, 'project_');
9
+ const dashboardAppUrl = (team, app_id) => `${DASHBOARD}/deploy/${toProjectId(team)}/app/${app_id}`;
10
+
11
+ export { dashboardAppUrl, toProjectId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "4.18.2",
3
+ "version": "4.18.4",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",