From 12542afd5ac5a3311a3d7a48dedc80f210c817c5 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Mon, 5 Oct 2026 23:18:50 -0700 Subject: [PATCH] Name the subcommand for hooks and output logs run through Main `kamal app boot` records Main's `app` invocation before App's `boot`, and the subcommand was only taken from the first invocation, so every real subcommand run from the command line came out as nil: hooks got no KAMAL_SUBCOMMAND and output logs were named `app` rather than `app_boot`. Tests calling the subcommand class directly record App's invocation first, which is why they didn't notice. When the first invocation is Main running a subcommand, look up that subcommand class's own first invocation instead. --- lib/kamal/cli/base.rb | 7 ++++++- test/cli/app_test.rb | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/kamal/cli/base.rb b/lib/kamal/cli/base.rb index 9d99f759e..bc4c636e0 100644 --- a/lib/kamal/cli/base.rb +++ b/lib/kamal/cli/base.rb @@ -280,7 +280,12 @@ def command def subcommand @kamal_subcommand ||= begin invocation_class, invocation_commands = *first_invocation - invocation_commands[0] if invocation_class != Kamal::Cli::Main + if invocation_class != Kamal::Cli::Main + invocation_commands[0] + elsif subcommand_class = Kamal::Cli::Main.subcommand_classes[invocation_commands[0]] + # `kamal app boot` records Main's `app` first, then App's `boot` + instance_variable_get("@_invocations").fetch(subcommand_class, []).first + end end end diff --git a/test/cli/app_test.rb b/test/cli/app_test.rb index 754b5ae8d..8f643157e 100644 --- a/test/cli/app_test.rb +++ b/test/cli/app_test.rb @@ -10,6 +10,29 @@ class CliAppTest < CliTestCase end end + test "boot through main names the subcommand to hooks and the output loggers" do + stub_running + Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) + + hook_env = {} + SSHKit::Backend::Abstract.any_instance.stubs(:execute).with do |*args| + hook_env[args.first] = ENV.slice("KAMAL_COMMAND", "KAMAL_SUBCOMMAND") if args.first.to_s.start_with?(".kamal/hooks/") + true + end + + modifications = [] + subscriber = ActiveSupport::Notifications.subscribe("modify.kamal") do |*, payload| + modifications << payload.slice(:command, :subcommand) + end + + stdouted { Kamal::Cli::Main.start([ "app", "boot", "-c", "test/fixtures/deploy_with_accessories.yml", "--hosts", "1.1.1.1" ]) } + + assert_equal({ "KAMAL_COMMAND" => "app", "KAMAL_SUBCOMMAND" => "boot" }, hook_env[".kamal/hooks/pre-app-boot"]) + assert_equal [ { command: "app", subcommand: "boot" } ], modifications + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + test "boot will rename if same version is already running" do Object.any_instance.stubs(:sleep) run_command("details") # Preheat Kamal const