@faable/faable 1.24.0 โ†’ 1.25.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.
@@ -1,5 +1,13 @@
1
1
  import { create_base_client } from './base_client.js';
2
2
 
3
+ // Socket-level failures where no response ever arrived: the connection died
4
+ // before the server produced anything, so a single retry is safe for any
5
+ // method (a reset mid-flight means the request was not processed).
6
+ const RESET_CODES = new Set(["ECONNRESET", "EPIPE"]);
7
+ const RETRY_DELAY_MS = 300;
8
+ const is_connection_reset = (e) => e.isAxiosError &&
9
+ !e.response &&
10
+ (RESET_CODES.has(e.code ?? "") || e.message.includes("socket hang up"));
3
11
  const firstPage = async (res) => {
4
12
  const items = (await res).results;
5
13
  return items;
@@ -27,6 +35,20 @@ class FaableApi {
27
35
  // Do something with request error
28
36
  return Promise.reject(error);
29
37
  });
38
+ // Registered before the error-wrapping interceptor so it sees the raw
39
+ // axios error. Retries once per request; the retried call re-enters the
40
+ // full chain (auth headers included).
41
+ const client = this.client;
42
+ this.client.interceptors.response.use(undefined, async (error) => {
43
+ const e = error;
44
+ const config = e.config;
45
+ if (config && !config._retried && is_connection_reset(e)) {
46
+ config._retried = true;
47
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
48
+ return client.request(config);
49
+ }
50
+ throw error;
51
+ });
30
52
  this.client.interceptors.response.use((response) => response, (error) => {
31
53
  const e = error;
32
54
  if (e.isAxiosError) {
@@ -22,14 +22,14 @@ const exchangeGithubOidcToken = async (gh_token, target_app_id) => {
22
22
  // actionable next step.
23
23
  if (status === 404) {
24
24
  throw new Error('No app linked to this repository. Run "faable link" locally to link it, ' +
25
- "or link it from the dashboard (https://dashboard.faable.com).");
25
+ "or link it from the dashboard (https://dashboard.faable.com).", { cause: err });
26
26
  }
27
27
  // Monorepo: several apps are linked to the same repository.
28
28
  if (status === 400) {
29
29
  throw new Error(serverMessage ||
30
- "This repository has multiple linked apps. Specify which one with `faable deploy <app_id>`.");
30
+ "This repository has multiple linked apps. Specify which one with `faable deploy <app_id>`.", { cause: err });
31
31
  }
32
- throw new Error(`Faable OIDC token exchange failed (${status ?? "network error"})${serverMessage ? `: ${serverMessage}` : ""}`);
32
+ throw new Error(`Faable OIDC token exchange failed (${status ?? "network error"})${serverMessage ? `: ${serverMessage}` : ""}`, { cause: err });
33
33
  }
34
34
  throw err;
35
35
  }
@@ -3,6 +3,7 @@ import { Configuration } from '../../lib/Configuration.js';
3
3
  import { log } from '../../log.js';
4
4
  import { link } from '../link/index.js';
5
5
  import { git_context } from './git_context.js';
6
+ import { propose_release } from './release_version.js';
6
7
  import { deploy_remote } from './remote/index.js';
7
8
  import { resolve_app_id } from './resolve_app_id.js';
8
9
  import { secrets } from './secrets/index.js';
@@ -25,6 +26,10 @@ const deploy = {
25
26
  alias: 'w',
26
27
  type: 'string',
27
28
  description: 'Working directory'
29
+ })
30
+ .option('release', {
31
+ type: 'string',
32
+ description: 'Release version to record on the deployment (injected as FAABLE_RELEASE). Defaults to FAABLE_RELEASE env or the latest git tag'
28
33
  })
29
34
  .showHelpOnFail(false);
30
35
  },
@@ -47,6 +52,13 @@ const deploy = {
47
52
  // Capture the commit/ref/actor so the deployment records which commit
48
53
  // it came from and who pushed it (env in CI, git fallback locally).
49
54
  const git = await git_context({ workdir });
55
+ // Propose the release version (--release > FAABLE_RELEASE > git tag).
56
+ // Optional: when absent the platform injects no FAABLE_RELEASE and the
57
+ // app falls back to its own version source.
58
+ const proposed = await propose_release({ workdir, explicit: args.release });
59
+ if (proposed) {
60
+ log.info(`๐Ÿท๏ธ Release: ${proposed.release} (from ${proposed.source})`);
61
+ }
50
62
  // Remote build only (arch/deploy/remote-artifact-default-cutover.md): the
51
63
  // CLI no longer builds โ€” it uploads the source and the platform builds
52
64
  // server-side (framework detection, buildpacks, artifact/image output all
@@ -54,7 +66,13 @@ const deploy = {
54
66
  // (build_mode=local opt-out, or the global kill-switch off) or a build
55
67
  // error throws and exits red.
56
68
  log.info(`๐Ÿš€ Deploying "${app.name}" (${app.id})`);
57
- const deployment = await deploy_remote({ api, app, git, workdir });
69
+ const deployment = await deploy_remote({
70
+ api,
71
+ app,
72
+ git,
73
+ release: proposed?.release,
74
+ workdir
75
+ });
58
76
  const dashboard_url = `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`;
59
77
  log.info(`Preparing to deploy in faable cloud ยท ${deployment.id}`);
60
78
  log.info(`๐Ÿ“Š View it in the dashboard -> ${dashboard_url}`);
@@ -0,0 +1,53 @@
1
+ import { exec } from 'child_process';
2
+
3
+ // Quiet git runner, same contract as git_context's: trimmed stdout or
4
+ // undefined on any failure. A deploy must never fail because a release
5
+ // version couldn't be proposed.
6
+ const gitRunner = (workdir) => command => new Promise(resolve => {
7
+ exec(command, { cwd: workdir }, (err, stdout) => {
8
+ if (err)
9
+ return resolve(undefined);
10
+ const out = stdout?.toString().trim();
11
+ resolve(out || undefined);
12
+ });
13
+ });
14
+ // Loose version shape: "starts like semver". Filters out non-version tags
15
+ // (e.g. "nightly", "deploy-2026-07-01") when falling back to git describe;
16
+ // explicit values (--release / FAABLE_RELEASE) are NEVER validated โ€” the
17
+ // platform treats release as free text.
18
+ const looksLikeVersion = (v) => /^\d+\.\d+\.\d+/.test(v);
19
+ const stripV = (tag) => tag.replace(/^v/, "");
20
+ /**
21
+ * Propose the release version for a deploy (Sentry propose-version pattern):
22
+ * explicit `--release` > `FAABLE_RELEASE` env > latest reachable git tag
23
+ * (`v1.2.3` or `1.2.3`, e.g. the tag semantic-release created) > undefined.
24
+ * When undefined the field is omitted from the deployment and the platform
25
+ * injects no FAABLE_RELEASE โ€” the app falls back to its own version source.
26
+ * No SHA fallback: the commit already travels as `github_commit`.
27
+ */
28
+ const propose_release = async (opts) => {
29
+ const env = opts?.env ?? process.env;
30
+ const run = opts?.run ?? gitRunner(opts?.workdir);
31
+ if (opts?.explicit)
32
+ return { release: opts.explicit, source: "--release" };
33
+ if (env.FAABLE_RELEASE)
34
+ return { release: env.FAABLE_RELEASE, source: "FAABLE_RELEASE env" };
35
+ // Latest tag reachable from HEAD. Try version-shaped tags first so a
36
+ // repo that also tags non-releases still resolves; retry unfiltered for
37
+ // repos whose release tags carry no `v` prefix.
38
+ for (const cmd of [
39
+ `git describe --tags --abbrev=0 --match "v[0-9]*"`,
40
+ `git describe --tags --abbrev=0`,
41
+ ]) {
42
+ const tag = await run(cmd);
43
+ if (!tag)
44
+ continue;
45
+ const version = stripV(tag);
46
+ if (looksLikeVersion(version)) {
47
+ return { release: version, source: `git tag ${tag}` };
48
+ }
49
+ }
50
+ return undefined;
51
+ };
52
+
53
+ export { propose_release };
@@ -15,7 +15,7 @@ import { upload_missing_blobs } from './upload.js';
15
15
  * BUILD_ERROR โ€” throws so the command exits red.
16
16
  */
17
17
  const deploy_remote = async (props) => {
18
- const { api, app, git, workdir } = props;
18
+ const { api, app, git, release, workdir } = props;
19
19
  log.info(`โ˜๏ธ Remote build`);
20
20
  const manifest = await collect_manifest(workdir);
21
21
  log.info(`๐Ÿ—‚๏ธ ${manifest.length} files in the source manifest`);
@@ -25,6 +25,7 @@ const deploy_remote = async (props) => {
25
25
  const deployment = await api.createDeployment({
26
26
  app_id: app.id,
27
27
  source: { manifest },
28
+ ...(release ? { release } : {}),
28
29
  ...git,
29
30
  });
30
31
  log.info(`Preparing to build in faable cloud ยท ${deployment.id}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "@actions/core": "^3.0.0",
30
- "axios": "^1.13.2",
30
+ "axios": "^1.18.1",
31
31
  "fs-extra": "^11.3.2",
32
32
  "handlebars": "^4.7.8",
33
33
  "open": "^11.0.0",