class Dpl::Ctx::Bash

Attributes

folds[RW]
last_err[RW]
last_out[RW]
stderr[RW]
stdout[RW]

Public Class Methods

new(stdout = $stdout, stderr = $stderr) click to toggle source
Calls superclass method
# File lib/dpl/ctx/bash.rb, line 19
def initialize(stdout = $stdout, stderr = $stderr)
  @stdout = stdout
  @stderr = stderr
  @folds = 0
  super('dpl', abort: false)
end

Public Instance Methods

apt_get(package, cmd = package, opts = {}) click to toggle source

Installs an APT package

Installs the APT package with the given name, unless the command is already available (as determined by ‘which [cmd]`.

@param package [String] the package name @param cmd [String] an executable installed by the package, defaults to the package name

# File lib/dpl/ctx/bash.rb, line 154
def apt_get(package, cmd = package, opts = {})
  return if which(cmd)

  apt_update unless opts[:update].is_a?(FalseClass)
  shell "sudo apt-get -qq install #{package}", retry: true
end
apt_update() click to toggle source
# File lib/dpl/ctx/bash.rb, line 161
def apt_update
  shell 'sudo apt-get update', retry: true
end
apts_get(packages) click to toggle source
# File lib/dpl/ctx/bash.rb, line 139
def apts_get(packages)
  packages = packages.reject { |name, cmd = name| which(cmd || name) }
  return unless packages.any?

  apt_update
  packages.each { |package, cmd| apt_get(package, cmd || package, update: false) }
end
build_dir() click to toggle source

Returns the current build directory

Uses the environment variable ‘TRAVIS_REPO_SLUG` if present, and defaults to `.` otherwise.

Note that this might return an unexpected string outside of the context of Travis CI build environments if the method is called at a time when the current working directory has changed.

# File lib/dpl/ctx/bash.rb, line 375
def build_dir
  ENV['TRAVIS_BUILD_DIR'] || '.'
end
build_number() click to toggle source

Returns the current build number

Returns the value of the environment variable ‘TRAVIS_BUILD_NUMBER` if present.

# File lib/dpl/ctx/bash.rb, line 383
def build_number
  ENV['TRAVIS_BUILD_NUMBER'] || raise('TRAVIS_BUILD_NUMBER not set')
end
deprecate_opt(key, msg) click to toggle source

Outputs a deprecation warning for a given deprecated option key to stderr.

@param key [Symbol] the deprecated option key @param msg [String or Symbol] the deprecation message. if given a Symbol this will be wrapped into the string “Please use #{symbol}”.

# File lib/dpl/ctx/bash.rb, line 63
def deprecate_opt(key, msg)
  msg = "please use #{msg}" if msg.is_a?(Symbol)
  warn "Deprecated option #{key} used (#{msg})."
end
encoding(path) click to toggle source

Returns the encoding of the given file, as determined by ‘file`.

# File lib/dpl/ctx/bash.rb, line 388
def encoding(path)
  case `file '#{path}'`
  when /gzip compressed/
    'gzip'
  when /compress'd/
    'compress'
  when /text/
    'text'
  when /data/
    # shrugs?
  end
end
error(message) click to toggle source

Raises an exception, halting the deployment process.

The calling executable ‘bin/dpl` will catch the exception, and abort the ruby process with the given error message.

This method is intended to be used for all error conditions that require the deployment process to be aborted.

# File lib/dpl/ctx/bash.rb, line 107
def error(message)
  raise Error, message
end
failed?() click to toggle source

Whether or not the last executed shell command has failed.

# File lib/dpl/ctx/bash.rb, line 326
def failed?
  !success?
end
file_size(path) click to toggle source

Returns the size of the given file path

# File lib/dpl/ctx/bash.rb, line 495
def file_size(path)
  File.size(path)
end
fold(msg) { || ... } click to toggle source

Folds any log output from the given block

Starts a log fold with the given fold message, calls the block, and closes the fold.

@param msg [String] the message that will appear on the log fold

# File lib/dpl/ctx/bash.rb, line 32
def fold(msg)
  self.folds += 1
  print "travis_fold:start:dpl.#{folds}\r\e[K"
  time do
    info "\e[33m#{msg}\e[0m"
    yield
  end
ensure
  print "\ntravis_fold:end:dpl.#{folds}\r\e[K"
end
gems_require(gems) click to toggle source

Requires source files from Ruby gems, installing them on demand if required

Installs the Ruby gems with the given version, if not already installed, and requires the specified source files from that gem.

This happens using the bundler/inline API.

@param gems [Array<String, String, Hash>] Array of gem requirements: gem name, version, and options (‘require`: A single path or a list of paths to source files to require from this Ruby gem)

@see bundler.io/v2.0/guides/bundler_in_a_single_file_ruby_script.html

# File lib/dpl/ctx/bash.rb, line 175
def gems_require(gems)
  # A local Gemfile.lock might interfer with bundler/inline, even though
  # it should not. Switching to a temporary dir fixes this.
  Dir.chdir(tmp_dir) do
    require 'bundler/inline'
    info "Installing gem dependencies: #{gems.map { |name, version, _| "#{name} #{"(#{version})" if version}".strip }.join(', ')}"
    env = ENV.to_h
    # Bundler.reset!
    # Gem.loaded_specs.clear
    gemfile do
      source 'https://rubygems.org'
      gems.each do |g|
        gem(*g)
      end
    end
    # https://github.com/bundler/bundler/issues/7181
    ENV.replace(env)
  end
end
git_author_email() click to toggle source

Returns the comitter email of the commit ‘git_sha`.

# File lib/dpl/ctx/bash.rb, line 417
def git_author_email
  `git log #{git_sha} -n 1 --pretty=%ae`.chomp
end
git_author_name() click to toggle source

Returns the committer name of the commit ‘git_sha`.

# File lib/dpl/ctx/bash.rb, line 412
def git_author_name
  `git log #{git_sha} -n 1 --pretty=%an`.chomp
end
git_branch() click to toggle source

Returns the current branch name

# File lib/dpl/ctx/bash.rb, line 402
def git_branch
  ENV['TRAVIS_BRANCH'] || git_rev_parse('HEAD')
end
git_commit_msg() click to toggle source

Returns the message of the commit ‘git_sha`.

# File lib/dpl/ctx/bash.rb, line 407
def git_commit_msg
  `git log #{git_sha} -n 1 --pretty=%B`.chomp
end
git_dirty?() click to toggle source

Whether or not the git working directory is dirty or has new or deleted files

# File lib/dpl/ctx/bash.rb, line 422
def git_dirty?
  !`git status --short`.chomp.empty?
end
git_log(args) click to toggle source

Returns the output of ‘git log`, using the given args.

# File lib/dpl/ctx/bash.rb, line 427
def git_log(args)
  `git log #{args}`.chomp
end
git_ls_files() click to toggle source

Returns the Git log, separated by NULs

Returns the output of ‘git ls-files -z`, which separates log entries by NULs, rather than newline characters.

# File lib/dpl/ctx/bash.rb, line 435
def git_ls_files
  `git ls-files -z`.split("\x0")
end
git_ls_remote?(url, ref) click to toggle source

Returns true if the given ref exists remotely

# File lib/dpl/ctx/bash.rb, line 440
def git_ls_remote?(url, ref)
  Kernel.system("git ls-remote --exit-code #{url} #{ref} > /dev/null 2>&1")
end
git_remote_urls() click to toggle source

Returns known Git remote URLs

# File lib/dpl/ctx/bash.rb, line 445
def git_remote_urls
  `git remote -v`.scan(/\t[^\s]+\s/).map(&:strip).uniq
end
git_rev_parse(ref) click to toggle source

Returns the sha for the given Git ref

# File lib/dpl/ctx/bash.rb, line 450
def git_rev_parse(ref)
  `git rev-parse #{ref}`.strip
end
git_sha() click to toggle source

Returns the current commit sha

# File lib/dpl/ctx/bash.rb, line 460
def git_sha
  ENV['TRAVIS_COMMIT'] || `git rev-parse HEAD`.chomp
end
git_tag() click to toggle source

Returns the latest tag name, if any

# File lib/dpl/ctx/bash.rb, line 455
def git_tag
  `git describe --tags --exact-match 2>/dev/null`.chomp
end
info(*msgs) click to toggle source

Outputs an info level message to stdout.

# File lib/dpl/ctx/bash.rb, line 69
def info(*msgs)
  stdout.puts(*msgs)
end
last_process_status() click to toggle source

Returns the last child process’ exit status

Internal, and not to be used by implementors. $? is a read-only variable, so we use a method that we can stub during tests.

# File lib/dpl/ctx/bash.rb, line 334
def last_process_status
  $CHILD_STATUS.success?
end
logger(level = :info) click to toggle source

Returns a logger

Returns a logger instance, with the given log level set. This can be used to pass to clients that accept a Ruby logger, such as Faraday, for debugging purposes.

Use with care.

@param level [Symbol] the Ruby logger log level

# File lib/dpl/ctx/bash.rb, line 120
def logger(level = :info)
  logger = Logger.new(stderr)
  logger.level = Logger.const_get(level.to_s.upcase)
  logger
end
machine_name() click to toggle source

Returns the local machine’s hostname

# File lib/dpl/ctx/bash.rb, line 465
def machine_name
  `hostname`.strip
end
move_files(paths) click to toggle source
# File lib/dpl/ctx/bash.rb, line 499
def move_files(paths)
  paths.each do |path|
    target = "#{tmp_dir}/#{File.basename(path)}"
    mv(path, target) if File.exist?(path)
  end
end
mv(src, dest) click to toggle source
# File lib/dpl/ctx/bash.rb, line 513
def mv(src, dest)
  Kernel.system("sudo mv #{src} #{dest} 2> /dev/null")
end
node_version() click to toggle source

Returns the current Node.js version

# File lib/dpl/ctx/bash.rb, line 470
def node_version
  `node -v`.sub(/^v/, '').chomp
end
npm_install(package, cmd = package) click to toggle source

Installs an NPM package

Installs the NPM package with the given name, unless the command is already available (as determined by ‘which [cmd]`.

@param package [String] the package name @param cmd [String] an executable installed by the package, defaults to the package name

# File lib/dpl/ctx/bash.rb, line 202
def npm_install(package, cmd = package)
  shell "npm install -g #{package}", retry: true unless which(cmd)
end
npm_version() click to toggle source

Returns the current NPM version

# File lib/dpl/ctx/bash.rb, line 475
def npm_version
  `npm --version`
end
only(hash, *keys) click to toggle source

Returns a copy of the given hash, reduced to the given keys

# File lib/dpl/ctx/bash.rb, line 542
def only(hash, *keys)
  hash.select { |key, _| keys.include?(key) }.to_h
end
open3(cmd, opts) click to toggle source

Runs a shell command and captures stdout, stderr, and the exit status

Runs the given command using ‘Open3.capture3`, which will capture the stdout and stderr streams, as well as the exit status. I.e. this will not stream log output in real time, but capture the output, and allow implementors to display it later (using the `%{out}` and `%{err}` interpolation variables.

Use sparingly.

@option chdir [String] directory temporarily to change to before running the command

# File lib/dpl/ctx/bash.rb, line 301
def open3(cmd, opts)
  opts = [opts[:chdir] ? only(opts, :chdir) : nil].compact
  out, err, status = Open3.capture3(cmd, *opts)
  [out, err, status.success?]
end
pip_install(package, cmd = package, version = nil) click to toggle source

Installs a Python package

Installs the Python package with the given name. A previously installed package is uninstalled before that, but only if ‘version` was given.

@param package [String] Package name (required). @param cmd [String] Executable command installed by that package (optional, defaults to the package name). @param version [String] Package version (optional).

# File lib/dpl/ctx/bash.rb, line 214
def pip_install(package, cmd = package, version = nil)
  ENV['VIRTUAL_ENV'] = File.expand_path('~/dpl_venv')
  ENV['PATH'] = File.expand_path("~/dpl_venv/bin:#{ENV['PATH']}")
  shell 'virtualenv ~/dpl_venv', echo: true
  shell 'pip install urllib3[secure]'
  cmd = "pip install #{package}"
  cmd << pip_version(version) if version
  shell cmd, retry: true
end
pip_version(version) click to toggle source
# File lib/dpl/ctx/bash.rb, line 224
def pip_version(version)
  version =~ /^\d+/ ? "==#{version}" : version
end
print(chars) click to toggle source

Prints an info level message to stdout.

This method does not append a newline character to the given message, which usually is not the desired behaviour. The method is intended to be used if an initial, partial message is supposed to be printed, which will be completed later (using the method ‘info`).

For example:

print 'Starting a long running task ...'
run_long_running_task
info 'done.'
python_version() click to toggle source

Returns the current Node.js version

# File lib/dpl/ctx/bash.rb, line 480
def python_version
  `python --version 2>&1`.sub(/^Python /, '').chomp
end
repo_name() click to toggle source

Returns current repository name

Uses the environment variable ‘TRAVIS_REPO_SLUG` if present, or the current directory’s base name.

Note that this might return an unexpected string outside of the context of Travis CI build environments if the method is called at a time when the current working directory has changed.

# File lib/dpl/ctx/bash.rb, line 351
def repo_name
  ENV['TRAVIS_REPO_SLUG'] ? ENV['TRAVIS_REPO_SLUG'].split('/').last : File.basename(Dir.pwd)
end
repo_slug() click to toggle source

Returns current repository slug

Uses the environment variable ‘TRAVIS_REPO_SLUG` if present, or the last two segmens of the current working directory’s path.

Note that this might return an unexpected string outside of the context of Travis CI build environments if the method is called at a time when the current working directory has changed.

# File lib/dpl/ctx/bash.rb, line 363
def repo_slug
  ENV['TRAVIS_REPO_SLUG'] || Dir.pwd.split('/')[-2, 2].join('/')
end
retrying(max, tries = 0, status = false) { || ... } click to toggle source
# File lib/dpl/ctx/bash.rb, line 282
def retrying(max, tries = 0, status = false)
  loop do
    tries += 1
    out, err, status = yield
    return [out, err, status] if status || tries > max
  end
end
shell(cmd, opts = {}) click to toggle source

Runs a single shell command

This the is the central point of executing any shell commands. It allows two strategies for running commands in subprocesses:

  • Using [Kernel#system](ruby-doc.org/core-2.6.3/Kernel.html#method-i-system) which is the default strategy, and should be used when possible. The stdout and stderr streams will not be captured, but streamed directly to the parent process (so any output on these streams appears in the build log as soon as possible).

  • Using [Open3.capture3](ruby-doc.org/stdlib-2.6.3/libdoc/open3/rdoc/Open3.html#method-c-capture3) which captures both stdout and stderr, and does not automatically output it to the build log. Implementors can choose to display it after the shell command has completed, using the ‘%{out}` and `%{err}` interpolation variables. Use sparingly.

The method accepts the following options:

@param cmd [String] the shell command to execute @param opts [Hash] options

@option opts [Boolean] :echo output the command to stdout before running it @option opts [Boolean] :silence silence all log output by redirecting stdout and stderr to ‘/dev/null` @option opts [Boolean] :capture use `Open3.capture3` to capture stdout and stderr @option opts [String] :python wrap the command into Bash code that enforces the given Python version to be used @option opts [String] :retry retries the command 2 more times if it fails @option opts [String] :info message to output to stdout if the command has exited with the exit code 0 (supports the interpolation variable `${out}` for stdout in case it was captured. @option opts [String] :assert error message to be raised if the command has exited with a non-zero exit code (supports the interpolation variable `${out}` for stdout in case it was captured.

@return [Boolean] whether or not the command was successful (has exited with the exit code 0)

# File lib/dpl/ctx/bash.rb, line 267
def shell(cmd, opts = {})
  cmd = Cmd.new(nil, cmd, opts) if cmd.is_a?(String)
  info cmd.msg if cmd.msg?
  info cmd.echo if cmd.echo?

  @last_out, @last_err, @last_status = retrying(cmd.retry ? 2 : 0) do
    send(cmd.capture? ? :open3 : :system, cmd.cmd, cmd.opts)
  end

  info format(cmd.success, out: last_out) if success? && cmd.success?
  error format(cmd.error, err: last_err) if failed? && cmd.assert?

  success? && cmd.capture? ? last_out.chomp : @last_status
end
sleep(sec) click to toggle source
# File lib/dpl/ctx/bash.rb, line 533
def sleep(sec)
  Kernel.sleep(sec)
end
ssh_keygen(name, file) click to toggle source

Generates an SSH key

@param name [String] the key name @param file [String] path to the key file

# File lib/dpl/ctx/bash.rb, line 232
def ssh_keygen(name, file)
  shell %(ssh-keygen -t rsa -N "" -C #{name} -f #{file})
end
success?() click to toggle source

Whether or not the last executed shell command was successful.

# File lib/dpl/ctx/bash.rb, line 321
def success?
  !!@last_status
end
sudo?() click to toggle source

Whether or not the current Ruby process runs with superuser priviledges.

# File lib/dpl/ctx/bash.rb, line 339
def sudo?
  Process::UID.eid.zero?
end
system(cmd, opts = {}) click to toggle source

Runs a shell command, streaming any stdout or stderr output, and returning the exit status

This is the default method for executing shell commands. The stdout and stderr will not be captured, but streamed directly to the parent process.

@option chdir [String] directory temporarily to change to before running the command

# File lib/dpl/ctx/bash.rb, line 314
def system(cmd, opts = {})
  opts = [opts[:chdir] ? only(opts, :chdir) : nil].compact
  Kernel.system(cmd, *opts)
  ['', '', last_process_status]
end
test?() click to toggle source
# File lib/dpl/ctx/bash.rb, line 546
def test?
  false
end
time() { || ... } click to toggle source

Times the given block

Starts a travis time log tag, calls the block, and closes the tag, including timing information. This makes a timing badge appear on the surrounding log fold.

# File lib/dpl/ctx/bash.rb, line 48
def time
  id = SecureRandom.hex[0, 8]
  start = Time.now.to_i * (10**9)
  print "travis_time:start:#{id}\r\e[K"
  yield
ensure
  finish = Time.now.to_i * (10**9)
  duration = finish - start
  print "\ntravis_time:end:#{id}:start=#{start},finish=#{finish},duration=#{duration}\r\e[K"
end
tmp_dir() click to toggle source

Returns a unique temporary directory name

# File lib/dpl/ctx/bash.rb, line 490
def tmp_dir
  @tmp_dir ||= Dir.mktmpdir
end
tty?() click to toggle source
# File lib/dpl/ctx/bash.rb, line 537
def tty?
  $stdout.isatty
end
unmove_files(paths) click to toggle source
# File lib/dpl/ctx/bash.rb, line 506
def unmove_files(paths)
  paths.each do |path|
    source = "#{tmp_dir}/#{File.basename(path)}"
    mv(source, path) if File.exist?(source)
  end
end
validate_runtime(args) click to toggle source
# File lib/dpl/ctx/bash.rb, line 132
def validate_runtime(args)
  name, required = *args
  info "Validating required runtime version: #{name} (#{required.join(', ')})"
  version = name == :node_js ? node_version : python_version
  required.all? { |required| Version.new(version).satisfies?(required) }
end
validate_runtimes(runtimes) click to toggle source
# File lib/dpl/ctx/bash.rb, line 126
def validate_runtimes(runtimes)
  failed = runtimes.reject(&method(:validate_runtime))
  failed = failed.map { |name, versions| "#{name} (#{versions.join(', ')})" }
  error "Failed validating runtimes: #{failed.join(', ')}" if failed.any?
end
warn(*msgs) click to toggle source

Outputs an warning message to stderr

This method is intended to be used for warning messages that are supposed to show up in the build log, but do not qualify as errors that would abort the deployment process. The warning will be highlighted as yellow text. Use sparingly.

# File lib/dpl/ctx/bash.rb, line 95
def warn(*msgs)
  msgs = msgs.join("\n").lines
  msgs.each { |msg| stderr.puts("\e[33;1m#{msg}\e[0m") }
end
which(cmd) click to toggle source

Returns true or false depending if the given command can be found

# File lib/dpl/ctx/bash.rb, line 485
def which(cmd)
  !`which #{cmd}`.chomp.empty? if cmd
end
write_file(path, content, chmod = nil) click to toggle source

Writes the given content to the given file path

# File lib/dpl/ctx/bash.rb, line 518
def write_file(path, content, chmod = nil)
  path = File.expand_path(path)
  FileUtils.mkdir_p(File.dirname(path))
  File.open(path, 'w+') { |f| f.write(content) }
  FileUtils.chmod(chmod, path) if chmod
end
write_netrc(machine, login, password) click to toggle source

Writes the given machine, login, and password to ~/.netrc

# File lib/dpl/ctx/bash.rb, line 526
def write_netrc(machine, login, password)
  require 'netrc'
  netrc = Netrc.read
  netrc[machine] = [login, password]
  netrc.save
end