diff --git a/lib/livebook_cli/deploy.ex b/lib/livebook_cli/deploy.ex index d99107f2c..78a2e4f5e 100644 --- a/lib/livebook_cli/deploy.ex +++ b/lib/livebook_cli/deploy.ex @@ -132,34 +132,45 @@ defmodule LivebookCLI.Deploy do log_info("Deploying notebooks:") - for path <- config.paths do - log_info(" * Preparing to deploy notebook #{Path.basename(path)}") - files_dir = Livebook.FileSystem.File.local(path) + deploy_results = + for path <- config.paths do + log_info(" * Preparing to deploy notebook #{Path.basename(path)}") + files_dir = Livebook.FileSystem.File.local(path) - with {:ok, content} <- File.read(path), - {:ok, app_deployment} <- prepare_app_deployment(path, content, files_dir) do - case Livebook.Teams.deploy_app_from_cli(team, app_deployment, config.deployment_group) do - {:ok, url} -> - log_info([:green, " * #{app_deployment.title} deployed successfully. (#{url})"]) + with {:ok, content} <- File.read(path), + {:ok, app_deployment} <- prepare_app_deployment(path, content, files_dir) do + case Livebook.Teams.deploy_app_from_cli(team, app_deployment, config.deployment_group) do + {:ok, url} -> + log_info([:green, " * #{app_deployment.title} deployed successfully. (#{url})"]) + :ok - {:error, errors} -> - log_error(" * #{app_deployment.title} failed to deploy.") - errors = normalize_errors(errors) + {:error, errors} -> + log_error(" * #{app_deployment.title} failed to deploy.") - raise LivebookCLI.Error, """ - #{format_errors(errors, " * ")} + error_message = + errors + |> normalize_errors + |> format_errors(" * ") - #{Teams.Requests.error_message()}\ - """ + log_error(error_message) - {:transport_error, reason} -> - log_error(" * #{app_deployment.title} failed to deploy.") - raise LivebookCLI.Error, reason + :error + + {:transport_error, reason} -> + log_error( + " * #{app_deployment.title} failed to deploy. Transport error: #{reason}" + ) + + :error + end end end - end - :ok + if Enum.any?(deploy_results, fn result -> result != :ok end) do + raise LivebookCLI.Error, "Some app deployments failed." + else + :ok + end end defp prepare_app_deployment(path, content, files_dir) do @@ -168,13 +179,19 @@ defmodule LivebookCLI.Deploy do {:ok, app_deployment} {:warning, warnings} -> - raise LivebookCLI.Error, """ - Deployment for notebook #{Path.basename(path)} failed because the notebook has some warnings: - #{format_list(warnings, " * ")} + error_message = """ + * Deployment for notebook #{Path.basename(path)} failed because the notebook has some warnings: + #{format_list(warnings, " * ")} """ + log_error(error_message) + + :error + {:error, reason} -> - raise LivebookCLI.Error, "Failed to handle I/O operations: #{reason}" + log_error(" * Failed to handle I/O operations: #{reason}") + + :error end end diff --git a/test/livebook_teams/cli/deploy_test.exs b/test/livebook_teams/cli/deploy_test.exs index b61cb4d47..b9da2d6b2 100644 --- a/test/livebook_teams/cli/deploy_test.exs +++ b/test/livebook_teams/cli/deploy_test.exs @@ -199,16 +199,19 @@ defmodule LivebookCLI.Integration.DeployTest do ``` """) - assert_raise LivebookCLI.Error, ~r/Deployment Group does not exist/s, fn -> + output = ExUnit.CaptureIO.capture_io(fn -> - deploy( - key, - team.teams_key, - Utils.random_short_id(), - app_path - ) + assert_raise LivebookCLI.Error, "Some app deployments failed.", fn -> + deploy( + key, + team.teams_key, + Utils.random_short_id(), + app_path + ) + end end) - end + + assert output =~ ~r/Deployment Group does not exist/ refute_receive {:app_deployment_started, %{ @@ -247,14 +250,88 @@ defmodule LivebookCLI.Integration.DeployTest do ) end end + + test "handles partial failure when deploying multiple notebooks", + %{team: team, node: node, org: org, tmp_dir: tmp_dir} do + {deploy_key, _} = TeamsRPC.create_deploy_key(node, org: org) + deployment_group = TeamsRPC.create_deployment_group(node, org: org, url: @url) + hub_id = team.id + + # Notebook without app settings (deploymeny should fail) + invalid_title = "Invalid App" + invalid_slug = "invalid-#{Utils.random_short_id()}" + invalid_app_path = Path.join(tmp_dir, "#{invalid_slug}.livemd") + + File.write!(invalid_app_path, """ + + + # #{invalid_title} + + ```elixir + 1 + 1 + ``` + """) + + # Second notebook should succeed + valid_title = "Valid App" + valid_slug = "valid-#{Utils.random_short_id()}" + valid_app_path = Path.join(tmp_dir, "#{valid_slug}.livemd") + + stamp_notebook(valid_app_path, """ + + + # #{valid_title} + + ```elixir + 1 + 1 + ``` + """) + + output = + ExUnit.CaptureIO.capture_io(fn -> + assert_raise(LivebookCLI.Error, "Some app deployments failed.", fn -> + deploy( + deploy_key, + team.teams_key, + deployment_group.name, + [invalid_app_path, valid_app_path] + ) + end) + end) + + # The valid notebook should have been deployed successfully + assert output =~ + "#{valid_title} deployed successfully. (#{@url}/apps/#{valid_slug})" + + deployment_group_id = to_string(deployment_group.id) + + assert_receive {:app_deployment_started, + %{ + title: ^valid_title, + slug: ^valid_slug, + deployment_group_id: ^deployment_group_id, + hub_id: ^hub_id, + deployed_by: "CLI" + }} + + # And deployment of the invalide notebook shows an error message + invalid_app_filename = Path.basename(invalid_app_path) + + assert output =~ + "Deployment for notebook #{invalid_app_filename} failed" + end end defp deploy(deploy_key, teams_key, deployment_group_name, path) do paths = - case Path.wildcard(path) do - [] -> [path] - [path] -> [path] - paths -> paths + if is_list(path) do + path + else + case Path.wildcard(path) do + [] -> [path] + [path] -> [path] + paths -> paths + end end LivebookCLI.Deploy.call(