diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d9ea9..5980da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.1.0.alpha.4 - 2026-08-28 + +- Expose the Rails build task as `boring:build`. +- Run the Rails task through the same pretty, streaming CLI experience as + `boringbuilder build`. +- Keep BoringCache Artifact receipt JSON out of streamed build logs. + ## 0.1.0.alpha.3 - 2026-08-27 - Remove Bundler cache metadata before compiling assets so cold and restored Rails builds stay identical. diff --git a/README.md b/README.md index b1de243..7ca2324 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ boringbuilder build --runtime docker --platform linux/arm64 Rails applications can also use the included task: ```sh -bin/rails boringbuilder:build +bin/rails boring:build ``` ## Caching diff --git a/lib/boringbuilder/exporters/boring_cache.rb b/lib/boringbuilder/exporters/boring_cache.rb index 4e6d10a..fce0399 100644 --- a/lib/boringbuilder/exporters/boring_cache.rb +++ b/lib/boringbuilder/exporters/boring_cache.rb @@ -5,6 +5,8 @@ module BoringBuilder module Exporters class BoringCache + RECEIPT_PATH = "/tmp/boringbuilder-artifact.json" + attr_reader :project, :client, :cache def initialize(project, client, environment: ENV) @@ -31,7 +33,8 @@ def export(asset) publisher = client.container.from(cache.cli_image) publisher = cache.prepare_artifact_publisher(publisher) path, publisher = mount_asset(publisher, asset) - output = publisher.with_exec(publish_command(path, asset.kind)).stdout + publisher = publisher.with_exec(quiet_publish_command(path, asset.kind)) + output = publisher.file(RECEIPT_PATH).contents artifact = JSON.parse(output).fetch("artifact") validate_artifact!(artifact) Receipt.new(artifact_id: artifact.fetch("id"), artifact_name: artifact.fetch("name")) @@ -73,6 +76,10 @@ def publish_command(path, kind) command end + def quiet_publish_command(path, kind) + ["sh", "-c", "exec \"$@\" > #{RECEIPT_PATH}", "boringbuilder-artifact", *publish_command(path, kind)] + end + def validate_artifact!(artifact) return if artifact.fetch("id", "").start_with?("art_") && artifact.fetch("status", nil) == "ready" diff --git a/lib/boringbuilder/railtie.rb b/lib/boringbuilder/railtie.rb index 6449937..1db4f30 100644 --- a/lib/boringbuilder/railtie.rb +++ b/lib/boringbuilder/railtie.rb @@ -3,7 +3,7 @@ module BoringBuilder class Railtie < Rails::Railtie rake_tasks do - load File.expand_path("tasks/boringbuilder.rake", __dir__) + load File.expand_path("tasks/boring.rake", __dir__) end end end diff --git a/lib/boringbuilder/tasks/boring.rake b/lib/boringbuilder/tasks/boring.rake new file mode 100644 index 0000000..000cb86 --- /dev/null +++ b/lib/boringbuilder/tasks/boring.rake @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +namespace :boring do + desc "Build the Rails application with Dagger" + task build: :environment do + command = ["build"] + { + "--runtime" => "BORINGBUILDER_RUNTIME", + "--platform" => "BORINGBUILDER_PLATFORM", + "--format" => "BORINGBUILDER_FORMAT", + "--output" => "BORINGBUILDER_OUTPUT", + "--push" => "BORINGBUILDER_PUSH", + "--load" => "BORINGBUILDER_LOAD", + "--progress" => "BORINGBUILDER_PROGRESS", + "--exporter" => "BORINGBUILDER_EXPORTER", + "--artifact-name" => "BORINGBUILDER_ARTIFACT_NAME" + }.each do |option, environment_name| + value = ENV.fetch(environment_name, nil) + command.push(option, value) if value && !value.empty? + end + + { + "--path" => "BORINGBUILDER_PATHS", + "--file" => "BORINGBUILDER_FILES", + "--host-path" => "BORINGBUILDER_HOST_PATHS" + }.each do |option, environment_name| + values = ENV.fetch(environment_name, "").split(",").reject(&:empty?) + values.each { |value| command.push(option, value) } + end + + command << Rails.root.to_s + status = BoringBuilder::CLI.start(command) + raise BoringBuilder::BuildError, "BoringBuilder exited unsuccessfully" unless status.zero? + end + + desc "Check the configured container runtime" + task :doctor do + requested = ENV.fetch("BORINGBUILDER_RUNTIME", "auto") + runtime = BoringBuilder::Runtime.new(requested).resolve + puts "Dagger runtime ready: #{runtime}" + end +end diff --git a/lib/boringbuilder/tasks/boringbuilder.rake b/lib/boringbuilder/tasks/boringbuilder.rake deleted file mode 100644 index 37b6522..0000000 --- a/lib/boringbuilder/tasks/boringbuilder.rake +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -namespace :boringbuilder do - desc "Build the Rails application with Dagger" - task build: :environment do - options = {} - { - runtime: "BORINGBUILDER_RUNTIME", - platform: "BORINGBUILDER_PLATFORM", - format: "BORINGBUILDER_FORMAT", - output: "BORINGBUILDER_OUTPUT", - publish: "BORINGBUILDER_PUSH", - load: "BORINGBUILDER_LOAD" - }.each do |option, environment_name| - value = ENV.fetch(environment_name, nil) - options[option] = value if value && !value.empty? - end - - { - paths: "BORINGBUILDER_PATHS", - files: "BORINGBUILDER_FILES", - host_paths: "BORINGBUILDER_HOST_PATHS" - }.each do |option, environment_name| - values = ENV.fetch(environment_name, "").split(",").reject(&:empty?) - options[option] = values unless values.empty? - end - - result = BoringBuilder.build(root: Rails.root, **options) - puts "Built with Dagger on #{result.runtime}" - puts "Exported #{result.format}: #{result.path}" if result.exported? - puts "Image: #{result.reference}" if result.published? - end - - desc "Check the configured container runtime" - task :doctor do - requested = ENV.fetch("BORINGBUILDER_RUNTIME", "auto") - runtime = BoringBuilder::Runtime.new(requested).resolve - puts "Dagger runtime ready: #{runtime}" - end -end diff --git a/lib/boringbuilder/version.rb b/lib/boringbuilder/version.rb index 81c8929..d21d21b 100644 --- a/lib/boringbuilder/version.rb +++ b/lib/boringbuilder/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module BoringBuilder - VERSION = "0.1.0.alpha.3" + VERSION = "0.1.0.alpha.4" end diff --git a/test/boringbuilder/exporters_test.rb b/test/boringbuilder/exporters_test.rb index 287db64..a5ad68f 100644 --- a/test/boringbuilder/exporters_test.rb +++ b/test/boringbuilder/exporters_test.rb @@ -78,11 +78,14 @@ def test_boringcache_exporter_publishes_and_returns_a_ready_receipt assert_includes client.calls, [:set_secret, %w[BORINGCACHE_SAVE_TOKEN save-token], {}] assert(client.calls.any? do |method, arguments, _options| method == :with_exec && arguments.first == [ + "sh", "-c", "exec \"$@\" > /tmp/boringbuilder-artifact.json", "boringbuilder-artifact", "boringcache", "artifact", "push", "/artifact/app.tar.zst", "--name", "my-app-linux-arm64-tar-zst", "--include-hidden", "--json", "--compression", "none" ] end) + assert_includes client.calls, [:file, ["/tmp/boringbuilder-artifact.json"], {}] + assert_includes client.calls, [:contents, [], {}] end private diff --git a/test/boringbuilder/rake_task_test.rb b/test/boringbuilder/rake_task_test.rb new file mode 100644 index 0000000..2388480 --- /dev/null +++ b/test/boringbuilder/rake_task_test.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require "test_helper" +require "rake" + +class RakeTaskTest < Minitest::Test + def setup + super + @original_rake_application = Rake.application + Rake.application = Rake::Application.new + Rake::Task.define_task(:environment) + load File.expand_path("../../lib/boringbuilder/tasks/boring.rake", __dir__) + end + + def teardown + Rake.application = @original_rake_application + super + end + + def test_exposes_the_rails_shaped_build_task + assert Rake::Task.task_defined?("boring:build") + refute Rake::Task.task_defined?("boringbuilder:build") + end + + def test_builds_the_rails_application + calls = [] + start = lambda do |arguments| + calls << arguments + 0 + end + + with_environment("BORINGBUILDER_RUNTIME" => "docker", "BORINGBUILDER_OUTPUT" => "dist/app.tar.zst") do + with_rails_root do + with_cli_start(start) do + Rake::Task["boring:build"].invoke + end + end + end + + assert_equal [ + ["build", "--runtime", "docker", "--output", "dist/app.tar.zst", Pathname.pwd.to_s] + ], calls + end + + private + + def with_cli_start(replacement) + singleton_class = BoringBuilder::CLI.singleton_class + original = BoringBuilder::CLI.method(:start) + singleton_class.send(:remove_method, :start) + singleton_class.define_method(:start, replacement) + yield + ensure + singleton_class.send(:remove_method, :start) + singleton_class.define_method(:start, original) + end + + def with_environment(values) + previous = values.to_h { |name, _value| [name, ENV.fetch(name, nil)] } + values.each { |name, value| ENV[name] = value } + yield + ensure + previous.each { |name, value| value.nil? ? ENV.delete(name) : ENV[name] = value } + end + + def with_rails_root + return yield if Object.const_defined?(:Rails, false) + + rails = Module.new + rails.define_singleton_method(:root) { Pathname.pwd } + Object.const_set(:Rails, rails) + yield + ensure + Object.send(:remove_const, :Rails) if rails + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index af3de47..9963b74 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -56,7 +56,7 @@ def method_missing(name, *arguments, **options) case name when :publish then "registry.example/app@sha256:123" when :export_image, :export then arguments.first - when :stdout then @stdout || self + when :stdout, :contents then @stdout || self when :stderr then @stderr || self else self end