Skip to content
Open
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
15 changes: 15 additions & 0 deletions lib/tomo/plugin/direct.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

require_relative "direct/helpers"
require_relative "direct/tasks"

module Tomo::Plugin
module Direct
extend Tomo::PluginDSL

helpers Tomo::Plugin::Direct::Helpers
tasks Tomo::Plugin::Direct::Tasks
defaults direct_source_path: nil,
direct_exclusions: nil
end
end
33 changes: 33 additions & 0 deletions lib/tomo/plugin/direct/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Direct Plugin

Deploy directly from your local filesystem to the target server via tarball streaming, bypassing the need for a git repository.

## Usage

```ruby
# .tomo/config.rb
plugin "direct"

host "[email protected]"

set application: "myapp"
set deploy_to: "/var/www/%{application}"
set direct_exclusions: %w[
.env*
log
tmp
node_modules
]

setup do
...
run "direct:create_release" # instead of git:*
...
end

deploy do
...
run "direct:create_release" # instead of git:create_release
...
end
```
25 changes: 25 additions & 0 deletions lib/tomo/plugin/direct/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "shellwords"

module Tomo::Plugin::Direct
module Helpers
class << self
attr_accessor :system_proc
end
self.system_proc = ->(cmd) { Kernel.system(cmd, exception: true) }

def upload_archive(source_path:, destination_path:, exclusions:)
tar_excludes = exclusions.map { |e| "--exclude=#{e.shellescape}" }.join(" ")
local_tar = "COPYFILE_DISABLE=1 tar --no-xattrs -c -C #{source_path.shellescape} #{tar_excludes} ."
remote_tar = "tar -x -C #{destination_path.to_s.shellescape}"
ssh_args = ssh_args_for_pipe.shelljoin

full_command = "#{local_tar} | #{ssh_args} #{remote_tar}"
Tomo.logger.info("Streaming archive to #{destination_path}")
Tomo.logger.debug(full_command)

Helpers.system_proc.call(full_command) unless Tomo.dry_run?
end
end
end
49 changes: 49 additions & 0 deletions lib/tomo/plugin/direct/tasks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module Tomo::Plugin::Direct
class Tasks < Tomo::TaskLibrary
DEFAULT_EXCLUSIONS = %w[
.git
.gitignore
.tomo
node_modules
tmp
log
.bundle
vendor/bundle
*.log
.env
.env.*
.DS_Store
].freeze

def create_release
remote.mkdir_p(paths.release)
store_release_info
remote.upload_archive(
source_path: resolve_source_path,
destination_path: paths.release,
exclusions: all_exclusions
)
end

private

def resolve_source_path
configured_path = settings[:direct_source_path]
return configured_path if configured_path

paths.tomo_config_file&.dirname&.dirname&.to_s || Dir.pwd
end

def all_exclusions
settings[:direct_exclusions] || DEFAULT_EXCLUSIONS
end

def store_release_info
remote.release[:deploy_date] = Time.now.to_s
remote.release[:deploy_user] = settings.fetch(:local_user)
remote.release[:source_path] = resolve_source_path
end
end
end
2 changes: 1 addition & 1 deletion lib/tomo/remote.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Remote

extend Forwardable

def_delegators :ssh, :close, :host
def_delegators :ssh, :close, :host, :ssh_args_for_pipe
def_delegators :shell_builder, :chdir, :env, :prepend, :umask

attr_reader :release
Expand Down
4 changes: 4 additions & 0 deletions lib/tomo/ssh/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def close
FileUtils.rm_f(control_path)
end

def ssh_args_for_pipe
options.build_args_for_pipe(host, control_path)
end

private

attr_reader :options, :exec_proc, :child_proc
Expand Down
12 changes: 12 additions & 0 deletions lib/tomo/ssh/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ def build_args(host, script, control_path, verbose) # rubocop:disable Metrics/Ab
[executable, args, script.to_s].flatten
end

def build_args_for_pipe(host, control_path)
args = [["-o", "LogLevel=ERROR"]]
args << "-A" if forward_agent
args << connect_timeout_option
args << strict_host_key_checking_option
args.push(*control_opts(control_path, false)) if reuse_connections
args.push(*extra_opts) if extra_opts
args << host.to_ssh_args

[executable, args].flatten
end

private

attr_writer :executable
Expand Down
60 changes: 60 additions & 0 deletions test/tomo/plugin/direct/tasks_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require "test_helper"
require "tomo/plugin/direct"

class Tomo::Plugin::Direct::TasksTest < Minitest::Test
def setup
@executed_commands = []
Tomo::Plugin::Direct::Helpers.system_proc = ->(cmd) { @executed_commands << cmd }
end

def teardown
Tomo::Plugin::Direct::Helpers.system_proc = ->(cmd) { Kernel.system(cmd, exception: true) }
end

def test_create_release_creates_release_directory
tester = configure
tester.run_task("direct:create_release")
assert_includes(tester.executed_scripts, "mkdir -p /app")
end

def test_create_release_stores_release_info
tester = configure
tester.run_task("direct:create_release")
assert_includes(tester.stdout, "Streaming archive")
end

def test_create_release_uses_configured_source_path
tester = configure(direct_source_path: "/my/project")
tester.run_task("direct:create_release")
assert(@executed_commands.any? { |cmd| cmd.include?("-C /my/project") })
end

def test_create_release_includes_default_exclusions
tester = configure
tester.run_task("direct:create_release")
cmd = @executed_commands.first
assert_includes(cmd, "--exclude=.git")
assert_includes(cmd, "--exclude=node_modules")
assert_includes(cmd, "--exclude=.DS_Store")
end

def test_create_release_includes_custom_exclusions
tester = configure(direct_exclusions: %w[spec/ coverage/])
tester.run_task("direct:create_release")
cmd = @executed_commands.first
assert_includes(cmd, "--exclude=spec/")
assert_includes(cmd, "--exclude=coverage/")
end

private

def configure(settings={})
defaults = {
release_path: "/app"
}
settings = defaults.merge(settings)
Tomo::Testing::MockPluginTester.new("direct", settings:)
end
end