Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: include nix-store realization output in seed deployment log

The download phase (nix-store --realize) output was being discarded on
success and not included in the failure report. Now the realization
output is threaded through realize_seed -> upgrade pipeline and prepended
to the deployment log sent with SeedDeploymentResult.

sow-78

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+72 -25
+30 -18
apps/garden/lib/garden/deployer.ex
··· 91 91 realize_seed_fun.(seed_deploy) 92 92 end) 93 93 |> async_stream_fun.(fn 94 - {:ok, {:ok, %SeedDeployment{seed: seed} = seed_deploy}} -> 94 + {:ok, {:ok, %SeedDeployment{seed: seed} = seed_deploy, download_output}} -> 95 95 profile = get_deployment_profile_fun.(seed_deploy.subscription_sid) 96 96 mode = Garden.Seed.activation_mode(profile) 97 97 98 - preamble = [ 99 - decision_line("realized #{seed.name} (#{seed.seed_type})"), 100 - decision_line("activating #{seed.name} (#{seed.seed_type}) with mode: #{mode}") 101 - ] 98 + preamble = 99 + download_output ++ 100 + [ 101 + decision_line("realized #{seed.name} (#{seed.seed_type})"), 102 + decision_line("activating #{seed.name} (#{seed.seed_type}) with mode: #{mode}") 103 + ] 102 104 103 105 Logger.info( 104 106 msg: "Activating seed", ··· 157 159 158 160 result 159 161 160 - {:ok, {:error, :failed_to_realize, %SeedDeployment{seed: seed} = _seed_deploy}} -> 161 - report_seed_result_fun.(deployment, seed, :failure, [ 162 - decision_line("realization failed for #{seed.name} (#{seed.seed_type})") 163 - ]) 162 + {:ok, 163 + {:error, :failed_to_realize, %SeedDeployment{seed: seed} = _seed_deploy, download_output}} -> 164 + report_seed_result_fun.( 165 + deployment, 166 + seed, 167 + :failure, 168 + download_output ++ 169 + [ 170 + decision_line("realization failed for #{seed.name} (#{seed.seed_type})") 171 + ] 172 + ) 164 173 165 174 {:error, :failed_to_realize, seed} 166 175 ··· 179 188 into: [], 180 189 lines: 1024 181 190 ) do 182 - {_output, 0} -> 191 + {output, 0} -> 183 192 Logger.info( 184 193 msg: "Successfully realized seed", 185 194 name: seed.name, ··· 188 197 artifact: seed.artifact 189 198 ) 190 199 191 - {:ok, seed_deploy} 200 + {:ok, seed_deploy, filter_realize_output(output)} 192 201 193 202 {output, exit_code} -> 194 - output = 195 - Enum.filter(output, fn line -> 196 - line not in [ 197 - "warning: you did not specify '--add-root'; the result might be removed by the garbage collector" 198 - ] 199 - end) 203 + output = filter_realize_output(output) 200 204 201 205 Logger.error( 202 206 msg: "Failed to realize seed", ··· 208 212 output: output 209 213 ) 210 214 211 - {:error, :failed_to_realize, seed_deploy} 215 + {:error, :failed_to_realize, seed_deploy, output} 212 216 end 213 217 end 214 218 ··· 458 462 def decision_line(message) do 459 463 timestamp = DateTime.utc_now() |> DateTime.to_iso8601() 460 464 "#{timestamp} [garden] #{message}" 465 + end 466 + 467 + defp filter_realize_output(output) do 468 + Enum.filter(output, fn line -> 469 + line not in [ 470 + "warning: you did not specify '--add-root'; the result might be removed by the garbage collector" 471 + ] 472 + end) 461 473 end 462 474 463 475 defp strip_ansi(text) do
+42 -7
apps/garden/test/garden/deployer_test.exs
··· 287 287 async_stream_fun: fn enumerable, func -> 288 288 Enum.map(enumerable, fn item -> {:ok, func.(item)} end) 289 289 end, 290 - realize_seed_fun: fn seed_deploy -> {:ok, seed_deploy} end, 290 + realize_seed_fun: fn seed_deploy -> {:ok, seed_deploy, []} end, 291 291 get_deployment_profile_fun: fn _ -> %DeploymentProfile{} end, 292 292 activate_seed_fun: fn _seed, _profile -> {:error, :activator_unavailable} end, 293 293 report_seed_status_fun: fn _, _, _ -> :ok end, ··· 319 319 async_stream_fun: fn enumerable, func -> 320 320 Enum.map(enumerable, fn item -> {:ok, func.(item)} end) 321 321 end, 322 - realize_seed_fun: fn seed_deploy -> {:ok, seed_deploy} end, 322 + realize_seed_fun: fn seed_deploy -> {:ok, seed_deploy, []} end, 323 323 get_deployment_profile_fun: fn _ -> %DeploymentProfile{} end, 324 324 activate_seed_fun: fn _seed, _profile -> {:ok, ["activation complete"]} end, 325 325 report_seed_status_fun: fn _, _, _ -> :ok end, ··· 349 349 async_stream_fun: fn enumerable, func -> 350 350 Enum.map(enumerable, fn item -> {:ok, func.(item)} end) 351 351 end, 352 - realize_seed_fun: fn seed_deploy -> {:ok, seed_deploy} end, 352 + realize_seed_fun: fn seed_deploy -> {:ok, seed_deploy, []} end, 353 353 get_deployment_profile_fun: fn _ -> %DeploymentProfile{} end, 354 354 activate_seed_fun: fn _seed, _profile -> {:error, :cmd_not_found} end, 355 355 report_seed_status_fun: fn _, _, _ -> :ok end, ··· 399 399 400 400 logged_lines = 401 401 capture_seed_result_lines(deployment, 402 - realize_seed_fun: fn seed_deploy -> {:error, :failed_to_realize, seed_deploy} end 402 + realize_seed_fun: fn seed_deploy -> {:error, :failed_to_realize, seed_deploy, []} end 403 403 ) 404 404 405 405 assert Enum.any?(logged_lines, &(&1 =~ "[garden]" and &1 =~ "realization failed")) ··· 438 438 async_stream_fun: fn enumerable, func -> 439 439 Enum.map(enumerable, fn item -> {:ok, func.(item)} end) 440 440 end, 441 - realize_seed_fun: fn sd -> {:ok, sd} end, 441 + realize_seed_fun: fn sd -> {:ok, sd, []} end, 442 442 get_deployment_profile_fun: fn _ -> 443 443 %DeploymentProfile{reboot_policy: "always"} 444 444 end, ··· 480 480 async_stream_fun: fn enumerable, func -> 481 481 Enum.map(enumerable, fn item -> {:ok, func.(item)} end) 482 482 end, 483 - realize_seed_fun: fn sd -> {:ok, sd} end, 483 + realize_seed_fun: fn sd -> {:ok, sd, []} end, 484 484 get_deployment_profile_fun: fn _ -> %DeploymentProfile{} end, 485 485 activate_seed_fun: fn _seed, _profile -> {:error, 1, ["failed"]} end, 486 486 report_seed_result_fun: fn _deployment, _seed, result, output_lines -> ··· 498 498 assert Enum.any?(reboot_lines, &(&1 =~ "[garden]" and &1 =~ "reboot skipped")) 499 499 end 500 500 501 + test "includes download output in log before decision lines" do 502 + deployment = %Deployment{ 503 + sid: "dep_dl_log", 504 + seed_deployments: [seed_deploy_with_identity("seed_dl1")] 505 + } 506 + 507 + download_lines = ["copying path '/nix/store/abc123'", "copying path '/nix/store/def456'"] 508 + 509 + logged_lines = 510 + capture_seed_result_lines(deployment, 511 + realize_seed_fun: fn sd -> {:ok, sd, download_lines} end 512 + ) 513 + 514 + assert Enum.at(logged_lines, 0) == "copying path '/nix/store/abc123'" 515 + assert Enum.at(logged_lines, 1) == "copying path '/nix/store/def456'" 516 + assert Enum.any?(logged_lines, &(&1 =~ "[garden]" and &1 =~ "realized")) 517 + end 518 + 519 + test "includes download output in failure log" do 520 + deployment = %Deployment{ 521 + sid: "dep_dl_fail", 522 + seed_deployments: [seed_deploy_with_identity("seed_dlf1")] 523 + } 524 + 525 + download_lines = ["error: path '/nix/store/missing' is not valid"] 526 + 527 + logged_lines = 528 + capture_seed_result_lines(deployment, 529 + realize_seed_fun: fn sd -> {:error, :failed_to_realize, sd, download_lines} end 530 + ) 531 + 532 + assert Enum.at(logged_lines, 0) == "error: path '/nix/store/missing' is not valid" 533 + assert Enum.any?(logged_lines, &(&1 =~ "[garden]" and &1 =~ "realization failed")) 534 + end 535 + 501 536 test "includes default activation mode when none configured" do 502 537 deployment = %Deployment{ 503 538 sid: "dep_mode_default", ··· 521 556 async_stream_fun: fn enumerable, func -> 522 557 Enum.map(enumerable, fn item -> {:ok, func.(item)} end) 523 558 end, 524 - realize_seed_fun: Keyword.get(opts, :realize_seed_fun, fn sd -> {:ok, sd} end), 559 + realize_seed_fun: Keyword.get(opts, :realize_seed_fun, fn sd -> {:ok, sd, []} end), 525 560 get_deployment_profile_fun: 526 561 Keyword.get(opts, :get_deployment_profile_fun, fn _ -> %DeploymentProfile{} end), 527 562 activate_seed_fun: