diff --git a/lib/kamal/cli/build.rb b/lib/kamal/cli/build.rb index f22922f9..80cc6637 100644 --- a/lib/kamal/cli/build.rb +++ b/lib/kamal/cli/build.rb @@ -109,6 +109,28 @@ def details end end + desc "dev", "Build using the working directory, tag it as dirty, and push to local image store." + option :output, type: :string, default: "docker", banner: "export_type", desc: "Exported type for the build result, and may be any exported type supported by 'buildx --output'." + def dev + cli = self + + ensure_docker_installed + + uncommitted_changes = Kamal::Git.uncommitted_changes + if uncommitted_changes.present? + say "WARNING: building with uncommitted changes:\n #{uncommitted_changes}", :yellow + end + + with_env(KAMAL.config.builder.secrets) do + run_locally do + build = KAMAL.builder.push(cli.options[:output], tag_as_dirty: true) + KAMAL.with_verbosity(:debug) do + execute(*build) + end + end + end + end + private def connect_to_remote_host(remote_host) remote_uri = URI.parse(remote_host) diff --git a/lib/kamal/commands/builder.rb b/lib/kamal/commands/builder.rb index 687eb35e..ddc7be7c 100644 --- a/lib/kamal/commands/builder.rb +++ b/lib/kamal/commands/builder.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/string/filters" class Kamal::Commands::Builder < Kamal::Commands::Base - delegate :create, :remove, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target + delegate :create, :remove, :dev, :push, :clean, :pull, :info, :inspect_builder, :validate_image, :first_mirror, to: :target delegate :local?, :remote?, :cloud?, to: "config.builder" include Clone diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index 213cb6fe..0bd613cb 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -13,11 +13,12 @@ def clean docker :image, :rm, "--force", config.absolute_image end - def push(export_action = "registry") + def push(export_action = "registry", tag_as_dirty: false) docker :buildx, :build, "--output=type=#{export_action}", *platform_options(arches), *([ "--builder", builder_name ] unless docker_driver?), + *build_tag_options(tag_as_dirty: tag_as_dirty), *build_options, build_context end @@ -37,7 +38,7 @@ def inspect_builder end def build_options - [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ] + [ *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_target, *build_ssh, *builder_provenance, *builder_sbom ] end def build_context @@ -58,8 +59,14 @@ def first_mirror end private - def build_tags - [ "-t", config.absolute_image, "-t", config.latest_image ] + def build_tag_names(tag_as_dirty: false) + tag_names = [ config.absolute_image, config.latest_image ] + tag_names.map! { |t| "#{t}-dirty" } if tag_as_dirty + tag_names + end + + def build_tag_options(tag_as_dirty: false) + build_tag_names(tag_as_dirty: tag_as_dirty).flat_map { |name| [ "-t", name ] } end def build_cache diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index e25ed0cc..f93f26ec 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -298,6 +298,30 @@ class CliBuildTest < CliTestCase end end + test "dev" do + with_build_directory do |build_directory| + Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) + + run_command("dev", "--verbose").tap do |output| + assert_no_match(/Cloning repo into build directory/, output) + assert_match(/docker --version && docker buildx version/, output) + assert_match(/docker buildx build --output=type=docker --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile \. as .*@localhost/, output) + end + end + end + + test "dev --output=local" do + with_build_directory do |build_directory| + Kamal::Commands::Hook.any_instance.stubs(:hook_exists?).returns(true) + + run_command("dev", "--output=local", "--verbose").tap do |output| + assert_no_match(/Cloning repo into build directory/, output) + assert_match(/docker --version && docker buildx version/, output) + assert_match(/docker buildx build --output=type=local --platform linux\/amd64 --builder kamal-local-docker-container -t dhh\/app:999-dirty -t dhh\/app:latest-dirty --label service="app" --file Dockerfile \. as .*@localhost/, output) + end + end + end + private def run_command(*command, fixture: :with_accessories) stdouted { Kamal::Cli::Build.start([ *command, "-c", "test/fixtures/deploy_#{fixture}.yml" ]) } diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 33c26afa..8ac8382e 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -72,7 +72,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile", + "--label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile", builder.target.build_options.join(" ") end @@ -81,7 +81,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase FileUtils.touch("Dockerfile") builder = new_builder_command(builder: { "secrets" => [ "token_a", "token_b" ] }) assert_equal \ - "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile", + "--label service=\"app\" --secret id=\"token_a\" --secret id=\"token_b\" --file Dockerfile", builder.target.build_options.join(" ") end end @@ -90,7 +90,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase Pathname.any_instance.expects(:exist?).returns(true).once builder = new_builder_command(builder: { "dockerfile" => "Dockerfile.xyz" }) assert_equal \ - "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile.xyz", + "--label service=\"app\" --file Dockerfile.xyz", builder.target.build_options.join(" ") end @@ -105,7 +105,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "build target" do builder = new_builder_command(builder: { "target" => "prod" }) assert_equal \ - "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --target prod", + "--label service=\"app\" --file Dockerfile --target prod", builder.target.build_options.join(" ") end @@ -137,7 +137,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "ssh" => "default=$SSH_AUTH_SOCK" }) assert_equal \ - "-t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK", + "--label service=\"app\" --file Dockerfile --ssh default=$SSH_AUTH_SOCK", builder.target.build_options.join(" ") end