ExAtlas.Fly.Deploy (ExAtlas v0.5.0)

Copy Markdown View Source

Discovers Fly.io apps (via fly.toml) and runs deploys against them.

deploy/2 runs fly deploy --remote-only via System.cmd/3 with a 15 min timeout. stream_deploy/3 uses Port.open to stream output line-by-line through ExAtlas.Fly.Dispatcher, with a 5 min activity timeout and a 30 min absolute cap.

Streamed output lands as {:ex_atlas_fly_deploy, ticket_id, line} on the topic "ex_atlas_fly_deploy:#{ticket_id}".

Error shape

Both deploy/2 and stream_deploy/3 return the same error shape on failure:

{:error, :invalid_deploy_dir}
  | {:error, {:fly_error, :not_found, String.t()}}
  | {:error, {:fly_error, :timeout,   String.t()}}
  | {:error, {:fly_error, non_neg_integer(), String.t()}}  # exit code + captured output

:not_found means the fly executable is not on PATH; :timeout means the 15 min (deploy/2) or 30 min (stream_deploy/3) cap was hit; a positive integer is the process exit code. The third element is always a human-readable string (captured output or a short explanation) suitable for logging — do not pattern match on it.

Summary

Functions

Run fly deploy --remote-only from fly_toml_dir (absolute path or relative to project_path). 15 min timeout.

Scan project_path for Fly apps (root fly.toml + :max_depth levels of subdirectories, default 1).

Parse the app = "..." line out of a fly.toml body.

Types

deploy_error()

@type deploy_error() ::
  :invalid_deploy_dir | {:fly_error, fly_error_reason(), String.t()}

fly_error_reason()

@type fly_error_reason() :: :not_found | :timeout | non_neg_integer()

Functions

deploy(project_path, fly_toml_dir)

@spec deploy(String.t(), String.t()) :: {:ok, String.t()} | {:error, deploy_error()}

Run fly deploy --remote-only from fly_toml_dir (absolute path or relative to project_path). 15 min timeout.

Returns {:ok, output} or {:error, reason} — see the module docs for the full error shape. In particular, a missing fly executable returns {:error, {:fly_error, :not_found, _}}, matching stream_deploy/3.

discover_apps(project_path, opts \\ [])

@spec discover_apps(
  String.t(),
  keyword()
) :: [{String.t(), String.t()}]

Scan project_path for Fly apps (root fly.toml + :max_depth levels of subdirectories, default 1).

Returns a sorted list of {app_name, directory} tuples.

Options

  • :max_depth — how many subdirectory levels below project_path to descend when searching for fly.toml files. Default 1. Set higher (e.g. 2 or 3) for monorepos that nest Fly apps under apps/*/ or services/*/ trees.

parse_app_name(content)

@spec parse_app_name(String.t()) :: {:ok, String.t()} | :error

Parse the app = "..." line out of a fly.toml body.

Requires the value to be either fully quoted (app = "my-app" or app = 'my-app') or an unquoted sequence of non-whitespace, non-quote characters (app = my-app). Values with internal whitespace inside quotes are intentionally rejected since Fly app names cannot contain whitespace.

stream_deploy(project_path, fly_toml_dir, ticket_id, opts \\ [])

@spec stream_deploy(String.t(), String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, deploy_error()}

Run fly deploy --remote-only and stream output via ExAtlas.Fly.Dispatcher.

For each non-empty line, broadcasts {:ex_atlas_fly_deploy, ticket_id, line} on "ex_atlas_fly_deploy:#{ticket_id}".

Two timers guard the deploy:

  • Activity timer (default 5 min) — resets on each chunk. Fires if the deploy stalls (e.g. builder hang).
  • Absolute timer (default 30 min) — never resets. Caps total deploy time.

Options

  • :activity_timeout_ms — override the per-chunk inactivity timeout.
  • :max_timeout_ms — override the absolute deploy timeout.