@faable/faable 4.21.0 → 4.21.1

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.
@@ -272,13 +272,21 @@ class FaableApi {
272
272
  })));
273
273
  }
274
274
  // Replace the app's whole secret set (the endpoint deletes and recreates).
275
- // This is the only mutation path that triggers an immediate restart of the
276
- // app; the per-secret upsert/delete endpoints are not used by the CLI.
275
+ // The per-secret upsert/delete endpoints are not used by the CLI.
277
276
  // The endpoint stamps the created secrets with the team from the request
278
277
  // context, which a CLI user token does not carry — pass the app's team
279
278
  // (from getApp) so it travels as the `x-faable-project` header.
279
+ //
280
+ // `with_redeploy=true` (api §403): a running app restarts to apply the new
281
+ // values, but an app with nothing running whose last deployment failed at
282
+ // runtime has nothing to restart — the api relaunches that deployment and
283
+ // says so in `redeploy`. An api that predates it ignores the parameter and
284
+ // answers the bare array: read that as "no redeploy to report".
280
285
  async createSecretsBatch(context_id, team, secrets) {
281
- return data(this.client.post(`/secret/createbatch`, { context_id, secrets }, { headers: projectHeader(team) }));
286
+ const body = await data(this.client.post(`/secret/createbatch`, { context_id, secrets }, { headers: projectHeader(team), params: { with_redeploy: true } }));
287
+ if (Array.isArray(body))
288
+ return { secrets: body, redeploy: null };
289
+ return { secrets: body?.secrets ?? [], redeploy: body?.redeploy ?? null };
282
290
  }
283
291
  async updateApp(app_id, params) {
284
292
  return data(this.client.post(`/app/${app_id}`, params));
@@ -0,0 +1,67 @@
1
+ // What happens to the app after a secrets save, in the words the user reads.
2
+ //
3
+ // A running app restarts to apply the new values. An app with NOTHING running
4
+ // whose last deployment failed at runtime (a crashloop over a missing
5
+ // variable, the usual reason to come and set one) has nothing to restart: the
6
+ // api relaunches that deployment itself and reports it (api §403). Promising
7
+ // a restart there was wrong — nothing was coming, or a whole new deployment
8
+ // was.
9
+ //
10
+ // Every command carries `-a <app id>`: these lines get copied, and outside
11
+ // the app's checkout a bare command resolves no app.
12
+ const after_save_lines = (args) => {
13
+ const { app, redeploy, changed } = args;
14
+ const what = args.what ?? 'the new variables';
15
+ const a = `-a ${app.id}`;
16
+ if (!redeploy) {
17
+ return [
18
+ {
19
+ level: 'info',
20
+ text: changed
21
+ ? `ℹ️ The app is restarting to apply the changes.`
22
+ : `ℹ️ Already up to date — the app was not restarted.`
23
+ }
24
+ ];
25
+ }
26
+ const failed = redeploy.failed_deployment_id;
27
+ if (redeploy.status === 'started') {
28
+ const id = redeploy.deployment_id;
29
+ return [
30
+ {
31
+ level: 'info',
32
+ text: `🔁 Redeploying ${app.name} with ${what} (deployment ${id}): its last deployment ${failed} had failed and nothing was running.`
33
+ },
34
+ {
35
+ level: 'info',
36
+ text: `Follow it with: faable deploy logs --build -d ${id} ${a} --follow · status: faable deploy status ${a}`
37
+ }
38
+ ];
39
+ }
40
+ if (redeploy.status === 'needs_rebuild') {
41
+ return [
42
+ {
43
+ level: 'warn',
44
+ text: `⚠️ Nothing is running: the last deployment ${failed} failed to build, so ${what} are saved but not deployed.`
45
+ },
46
+ {
47
+ level: 'warn',
48
+ text: `Rebuild it with: faable deploy redeploy ${failed} ${a} · why it failed: faable deploy logs --build -d ${failed} ${a}`
49
+ }
50
+ ];
51
+ }
52
+ return [
53
+ {
54
+ level: 'warn',
55
+ text: `⚠️ Nothing is running and Faable could not relaunch the failed deployment ${failed}, so ${what} are saved but not deployed.`
56
+ },
57
+ {
58
+ level: 'warn',
59
+ text: `Relaunch it by hand (it tells you why if it is refused): faable deploy redeploy ${failed} ${a}`
60
+ }
61
+ ];
62
+ };
63
+ // The `restarting` of the --json output: only a running app restarts. When
64
+ // the api reports a redeploy (started or not), nothing restarts.
65
+ const is_restarting = (redeploy, changed) => changed && !redeploy;
66
+
67
+ export { after_save_lines, is_restarting };
@@ -2,6 +2,7 @@ import { requireApi } from '../../../api/context.js';
2
2
  import { print_json, json_option } from '../../../lib/listing.js';
3
3
  import { log } from '../../../log.js';
4
4
  import { resolve_app_id } from '../resolve_app_id.js';
5
+ import { is_restarting, after_save_lines } from './after_save.js';
5
6
  import { remove_app_secret } from './merge.js';
6
7
  import { confirm } from '../../../lib/interactive.js';
7
8
 
@@ -44,12 +45,24 @@ const secrets_rm = {
44
45
  log.info('Cancelled.');
45
46
  return;
46
47
  }
47
- await ctx.api.createSecretsBatch(app.id, app.team, remaining);
48
+ const { redeploy } = await ctx.api.createSecretsBatch(app.id, app.team, remaining);
48
49
  if (args.json) {
49
- return print_json({ deleted: true, name: args.name, restarting: true });
50
+ return print_json({
51
+ deleted: true,
52
+ name: args.name,
53
+ restarting: is_restarting(redeploy, true),
54
+ redeploy
55
+ });
50
56
  }
51
57
  log.info(`🗑️ Removed secret ${args.name} from ${app_id}.`);
52
- log.info(`ℹ️ The app is restarting to apply the changes.`);
58
+ const lines = after_save_lines({
59
+ app,
60
+ redeploy,
61
+ changed: true,
62
+ what: 'the updated variables'
63
+ });
64
+ for (const { level, text } of lines)
65
+ log[level](text);
53
66
  }
54
67
  };
55
68
 
@@ -5,6 +5,7 @@ import { print_json, json_option } from '../../../lib/listing.js';
5
5
  import { read_stdin } from '../../../lib/stdin.js';
6
6
  import { log } from '../../../log.js';
7
7
  import { resolve_app_id } from '../resolve_app_id.js';
8
+ import { is_restarting, after_save_lines } from './after_save.js';
8
9
  import { managed_warning } from './managed_names.js';
9
10
  import { merge_app_secrets } from './merge.js';
10
11
  import { parse_env } from './parse_env.js';
@@ -113,12 +114,12 @@ const secrets_set = {
113
114
  const app = await ctx.api.getApp(app_id);
114
115
  const existing = await ctx.api.getAppSecrets(app_id, app.team);
115
116
  const merged = merge_app_secrets(existing, parsed);
116
- await ctx.api.createSecretsBatch(app.id, app.team, merged);
117
+ const { redeploy } = await ctx.api.createSecretsBatch(app.id, app.team, merged);
117
118
  const current = new Map(existing
118
119
  .filter(s => s.related_model === 'app')
119
120
  .map(s => [s.name, s.value]));
120
121
  const updated = parsed.filter(({ name }) => current.has(name)).length;
121
- // Only claim the restart when something actually changed. Setting a name
122
+ // Only claim a restart when something actually changed. Setting a name
122
123
  // to the value it already has is a no-op end to end — the API compares
123
124
  // before writing and never emits the event, so the pod keeps running —
124
125
  // and this line used to promise a restart that was not coming.
@@ -134,7 +135,10 @@ const secrets_set = {
134
135
  unchanged: parsed
135
136
  .filter(p => current.get(p.name) === p.value)
136
137
  .map(p => p.name),
137
- restarting: changed
138
+ restarting: is_restarting(redeploy, changed),
139
+ // api §403: what the api did for an app with nothing running whose
140
+ // last deployment failed — null when there was nothing to relaunch.
141
+ redeploy
138
142
  });
139
143
  }
140
144
  if (parsed.length <= MAX_DETAILED) {
@@ -148,9 +152,13 @@ const secrets_set = {
148
152
  log.info(`🔑 ${parsed.length - updated} added, ${updated} updated`);
149
153
  }
150
154
  log.info(`✅ ${parsed.length} secret(s) saved to ${app_id}.`);
151
- log.info(changed
152
- ? `ℹ️ The app is restarting to apply the changes.`
153
- : `ℹ️ Already up to date — the app was not restarted.`);
155
+ for (const { level, text } of after_save_lines({
156
+ app,
157
+ redeploy,
158
+ changed
159
+ })) {
160
+ log[level](text);
161
+ }
154
162
  }
155
163
  };
156
164
 
package/dist/mcp/tools.js CHANGED
@@ -760,7 +760,7 @@ const TOOLS = [
760
760
  {
761
761
  name: 'set_secrets',
762
762
  title: 'Set environment variables',
763
- description: 'Add or update environment variables of an app. The app restarts to apply them when a value changed. Returns the names added, updated and unchanged — never the values.',
763
+ description: 'Add or update environment variables of an app. When the app is running, it restarts to apply them (only if a value changed); if its last deployment failed at runtime and nothing is running, Faable redeploys it with the new variables. Returns the names added, updated and unchanged — never the values — and `redeploy`: null when there was nothing to relaunch; otherwise `status` is `started` (follow `deployment_id` with get_deployment), `needs_rebuild` (the last deployment failed to build: retry `failed_deployment_id` with redeploy) or `not_started` (the platform refused the relaunch: retry it with redeploy to see why).',
764
764
  input: {
765
765
  app,
766
766
  project,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "4.21.0",
3
+ "version": "4.21.1",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",