Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion lib/boringbuilder/exporters/boring_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"))
Expand Down Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion lib/boringbuilder/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
42 changes: 42 additions & 0 deletions lib/boringbuilder/tasks/boring.rake
Original file line number Diff line number Diff line change
@@ -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
40 changes: 0 additions & 40 deletions lib/boringbuilder/tasks/boringbuilder.rake

This file was deleted.

2 changes: 1 addition & 1 deletion lib/boringbuilder/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module BoringBuilder
VERSION = "0.1.0.alpha.3"
VERSION = "0.1.0.alpha.4"
end
3 changes: 3 additions & 0 deletions test/boringbuilder/exporters_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions test/boringbuilder/rake_task_test.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down