diff --git a/lib/kamal/commands/base.rb b/lib/kamal/commands/base.rb index 7e09fc1c..27948434 100644 --- a/lib/kamal/commands/base.rb +++ b/lib/kamal/commands/base.rb @@ -78,6 +78,10 @@ module Kamal::Commands args.compact.unshift :docker end + def git(*args) + args.compact.unshift :git + end + def tags(**details) Kamal::Tags.from_config(config, **details) end diff --git a/lib/kamal/commands/builder/base.rb b/lib/kamal/commands/builder/base.rb index e288db18..95b079e1 100644 --- a/lib/kamal/commands/builder/base.rb +++ b/lib/kamal/commands/builder/base.rb @@ -3,7 +3,7 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base class BuilderError < StandardError; end delegate :argumentize, to: Kamal::Utils - delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, to: :builder_config + delegate :args, :secrets, :dockerfile, :local_arch, :local_host, :remote_arch, :remote_host, :cache_from, :cache_to, :ssh, :git_archive?, to: :builder_config def clean docker :image, :rm, "--force", config.absolute_image @@ -13,6 +13,16 @@ class Kamal::Commands::Builder::Base < Kamal::Commands::Base docker :pull, config.absolute_image end + def push + if git_archive? + pipe \ + git(:archive, "--format=tar", :HEAD), + build_and_push + else + build_and_push + end + end + def build_options [ *build_tags, *build_cache, *build_labels, *build_args, *build_secrets, *build_dockerfile, *build_ssh ] end diff --git a/lib/kamal/commands/builder/multiarch.rb b/lib/kamal/commands/builder/multiarch.rb index 2f9e4d19..b80200cd 100644 --- a/lib/kamal/commands/builder/multiarch.rb +++ b/lib/kamal/commands/builder/multiarch.rb @@ -7,15 +7,6 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base docker :buildx, :rm, builder_name end - def push - docker :buildx, :build, - "--push", - "--platform", platform_names, - "--builder", builder_name, - *build_options, - build_context - end - def info combine \ docker(:context, :ls), @@ -34,4 +25,13 @@ class Kamal::Commands::Builder::Multiarch < Kamal::Commands::Builder::Base "linux/amd64,linux/arm64" end end + + def build_and_push + docker :buildx, :build, + "--push", + "--platform", platform_names, + "--builder", builder_name, + *build_options, + build_context + end end diff --git a/lib/kamal/commands/builder/native.rb b/lib/kamal/commands/builder/native.rb index d67d3519..cc0f03b1 100644 --- a/lib/kamal/commands/builder/native.rb +++ b/lib/kamal/commands/builder/native.rb @@ -7,14 +7,15 @@ class Kamal::Commands::Builder::Native < Kamal::Commands::Builder::Base # No-op on native without cache end - def push - combine \ - docker(:build, *build_options, build_context), - docker(:push, config.absolute_image), - docker(:push, config.latest_image) - end - def info # No-op on native end + + private + def build_and_push + combine \ + docker(:build, *build_options, build_context), + docker(:push, config.absolute_image), + docker(:push, config.latest_image) + end end diff --git a/lib/kamal/commands/builder/native/cached.rb b/lib/kamal/commands/builder/native/cached.rb index f72d1192..b3a3d635 100644 --- a/lib/kamal/commands/builder/native/cached.rb +++ b/lib/kamal/commands/builder/native/cached.rb @@ -7,10 +7,11 @@ class Kamal::Commands::Builder::Native::Cached < Kamal::Commands::Builder::Nativ docker :buildx, :rm, builder_name end - def push - docker :buildx, :build, - "--push", - *build_options, - build_context - end + private + def build_and_push + docker :buildx, :build, + "--push", + *build_options, + build_context + end end diff --git a/lib/kamal/commands/builder/native/remote.rb b/lib/kamal/commands/builder/native/remote.rb index 67d68e9f..d3053d40 100644 --- a/lib/kamal/commands/builder/native/remote.rb +++ b/lib/kamal/commands/builder/native/remote.rb @@ -11,15 +11,6 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ remove_buildx end - def push - docker :buildx, :build, - "--push", - "--platform", platform, - "--builder", builder_name, - *build_options, - build_context - end - def info chain \ docker(:context, :ls), @@ -56,4 +47,13 @@ class Kamal::Commands::Builder::Native::Remote < Kamal::Commands::Builder::Nativ def remove_buildx docker :buildx, :rm, builder_name end + + def build_and_push + docker :buildx, :build, + "--push", + "--platform", platform, + "--builder", builder_name, + *build_options, + build_context + end end diff --git a/lib/kamal/configuration.rb b/lib/kamal/configuration.rb index bd5189d9..483ca770 100644 --- a/lib/kamal/configuration.rb +++ b/lib/kamal/configuration.rb @@ -324,7 +324,10 @@ class Kamal::Configuration def git_version @git_version ||= if Kamal::Git.used? - [ Kamal::Git.revision, Kamal::Git.uncommitted_changes.present? ? "_uncommitted_#{SecureRandom.hex(8)}" : "" ].join + if Kamal::Git.uncommitted_changes.present? && !builder.git_archive? + uncommitted_suffix = "_uncommitted_#{SecureRandom.hex(8)}" + end + [ Kamal::Git.revision, uncommitted_suffix ].compact.join else raise "Can't use commit hash as version, no git repository found in #{Dir.pwd}" end diff --git a/lib/kamal/configuration/builder.rb b/lib/kamal/configuration/builder.rb index 33d10fbf..ea4e71d9 100644 --- a/lib/kamal/configuration/builder.rb +++ b/lib/kamal/configuration/builder.rb @@ -40,7 +40,7 @@ class Kamal::Configuration::Builder end def context - @options["context"] || "." + @options["context"] || (git_archive? ? "-" : ".") end def local_arch @@ -85,6 +85,10 @@ class Kamal::Configuration::Builder @options["ssh"] end + def git_archive? + Kamal::Git.used? && @options["context"].nil? + end + private def valid? if @options["cache"] && @options["cache"]["type"] diff --git a/test/cli/build_test.rb b/test/cli/build_test.rb index 1cb99fa4..186c1e9e 100644 --- a/test/cli/build_test.rb +++ b/test/cli/build_test.rb @@ -15,7 +15,7 @@ class CliBuildTest < CliTestCase run_command("push").tap do |output| assert_hook_ran "pre-build", output, **hook_variables assert_match /docker --version && docker buildx version/, output - assert_match /docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile \. as .*@localhost/, output + assert_match /git archive -tar HEAD | docker buildx build --push --platform linux\/amd64,linux\/arm64 --builder kamal-app-multiarch -t dhh\/app:999 -t dhh\/app:latest --label service="app" --file Dockerfile - as .*@localhost/, output end end @@ -25,7 +25,10 @@ class CliBuildTest < CliTestCase .with(:docker, "--version", "&&", :docker, :buildx, "version") SSHKit::Backend::Abstract.any_instance.stubs(:execute) - .with { |*args| args[0..1] == [ :docker, :buildx ] } + .with(:docker, :buildx, :create, "--use", "--name", "kamal-app-multiarch") + + SSHKit::Backend::Abstract.any_instance.stubs(:execute) + .with { |*args| p args[0..6]; args[0..6] == [ :git, :archive, "--format=tar", :HEAD, "|", :docker, :buildx ] } .raises(SSHKit::Command::Failed.new("no builder")) .then .returns(true) diff --git a/test/commands/builder_test.rb b/test/commands/builder_test.rb index 7b5a28e9..b03b0c83 100644 --- a/test/commands/builder_test.rb +++ b/test/commands/builder_test.rb @@ -9,7 +9,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "cache" => { "type" => "gha" } }) assert_equal "multiarch", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -", builder.push.join(" ") end @@ -17,7 +17,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "multiarch" => false }) assert_equal "native", builder.name assert_equal \ - "docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", + "git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest", builder.push.join(" ") end @@ -25,7 +25,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "multiarch" => false, "cache" => { "type" => "gha" } }) assert_equal "native/cached", builder.name assert_equal \ - "docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "git archive --format=tar HEAD | docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -", builder.push.join(" ") end @@ -33,7 +33,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "local" => {}, "remote" => {}, "cache" => { "type" => "gha" } }) assert_equal "multiarch/remote", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -", builder.push.join(" ") end @@ -41,7 +41,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "local" => { "arch" => "amd64" } }) assert_equal "multiarch", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile .", + "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile -", builder.push.join(" ") end @@ -49,7 +49,7 @@ class CommandsBuilderTest < ActiveSupport::TestCase builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "cache" => { "type" => "gha" } }) assert_equal "native/remote", builder.name assert_equal \ - "docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile .", + "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile -", builder.push.join(" ") end @@ -93,21 +93,21 @@ class CommandsBuilderTest < ActiveSupport::TestCase test "native push with build args" do builder = new_builder_command(builder: { "multiarch" => false, "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", + "git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest", builder.push.join(" ") end test "multiarch push with build args" do builder = new_builder_command(builder: { "args" => { "a" => 1, "b" => 2 } }) assert_equal \ - "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile .", + "git archive --format=tar HEAD | docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --build-arg a=\"1\" --build-arg b=\"2\" --file Dockerfile -", builder.push.join(" ") end test "native push with build secrets" do builder = new_builder_command(builder: { "multiarch" => false, "secrets" => [ "a", "b" ] }) assert_equal \ - "docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile . && docker push dhh/app:123 && docker push dhh/app:latest", + "git archive --format=tar HEAD | docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --secret id=\"a\" --secret id=\"b\" --file Dockerfile - && docker push dhh/app:123 && docker push dhh/app:latest", builder.push.join(" ") end @@ -123,6 +123,34 @@ class CommandsBuilderTest < ActiveSupport::TestCase assert_equal "docker inspect -f '{{ .Config.Labels.service }}' dhh/app:123 | grep -x app || (echo \"Image dhh/app:123 is missing the 'service' label\" && exit 1)", new_builder_command.validate_image.join(" ") end + test "multiarch context build" do + builder = new_builder_command(builder: { "context" => "./foo" }) + assert_equal \ + "docker buildx build --push --platform linux/amd64,linux/arm64 --builder kamal-app-multiarch -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + builder.push.join(" ") + end + + test "native context build" do + builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo" }) + assert_equal \ + "docker build -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo && docker push dhh/app:123 && docker push dhh/app:latest", + builder.push.join(" ") + end + + test "cached context build" do + builder = new_builder_command(builder: { "multiarch" => false, "context" => "./foo", "cache" => { "type" => "gha" } }) + assert_equal \ + "docker buildx build --push -t dhh/app:123 -t dhh/app:latest --cache-to type=gha --cache-from type=gha --label service=\"app\" --file Dockerfile ./foo", + builder.push.join(" ") + end + + test "remote context build" do + builder = new_builder_command(builder: { "remote" => { "arch" => "amd64" }, "context" => "./foo" }) + assert_equal \ + "docker buildx build --push --platform linux/amd64 --builder kamal-app-native-remote -t dhh/app:123 -t dhh/app:latest --label service=\"app\" --file Dockerfile ./foo", + builder.push.join(" ") + end + private def new_builder_command(additional_config = {}) Kamal::Commands::Builder.new(Kamal::Configuration.new(@config.merge(additional_config), version: "123")) diff --git a/test/configuration/builder_test.rb b/test/configuration/builder_test.rb index a519be67..9ac02ad6 100644 --- a/test/configuration/builder_test.rb +++ b/test/configuration/builder_test.rb @@ -140,7 +140,7 @@ class ConfigurationBuilderTest < ActiveSupport::TestCase end test "context" do - assert_equal ".", @config.builder.context + assert_equal "-", @config.builder.context end test "setting context" do diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 9f50e9df..6f89d669 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -103,7 +103,17 @@ class ConfigurationTest < ActiveSupport::TestCase Kamal::Git.expects(:revision).returns("git-version") Kamal::Git.expects(:uncommitted_changes).returns("M file\n") - assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, @config.version + assert_equal "git-version", @config.version + end + + test "version from uncommitted context" do + ENV.delete("VERSION") + + config = Kamal::Configuration.new(@deploy.tap { |c| c[:builder] = { "context" => "." } }) + + Kamal::Git.expects(:revision).returns("git-version") + Kamal::Git.expects(:uncommitted_changes).returns("M file\n") + assert_match /^git-version_uncommitted_[0-9a-f]{16}$/, config.version end test "version from env" do