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.
Run fly deploy --remote-only and stream output via ExAtlas.Fly.Dispatcher.
Types
@type deploy_error() :: :invalid_deploy_dir | {:fly_error, fly_error_reason(), String.t()}
@type fly_error_reason() :: :not_found | :timeout | non_neg_integer()
Functions
@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.
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 belowproject_pathto descend when searching forfly.tomlfiles. Default1. Set higher (e.g.2or3) for monorepos that nest Fly apps underapps/*/orservices/*/trees.
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.
@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.