Class: Dpl::Ctx::Bash

Inherits:
Cl::Ctx
  • Object
show all
Includes:
FileUtils
Defined in:
lib/dpl/ctx/bash.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stdout = $stdout, stderr = $stderr) ⇒ Bash

Returns a new instance of Bash.



19
20
21
22
23
24
# 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

Instance Attribute Details

#foldsObject

Returns the value of attribute folds.



17
18
19
# File 'lib/dpl/ctx/bash.rb', line 17

def folds
  @folds
end

#last_errObject

Returns the value of attribute last_err.



17
18
19
# File 'lib/dpl/ctx/bash.rb', line 17

def last_err
  @last_err
end

#last_outObject

Returns the value of attribute last_out.



17
18
19
# File 'lib/dpl/ctx/bash.rb', line 17

def last_out
  @last_out
end

#stderrObject

Returns the value of attribute stderr.



17
18
19
# File 'lib/dpl/ctx/bash.rb', line 17

def stderr
  @stderr
end

#stdoutObject

Returns the value of attribute stdout.



17
18
19
# File 'lib/dpl/ctx/bash.rb', line 17

def stdout
  @stdout
end

Instance Method Details

#apt_get(package, cmd = package, opts = {}) ⇒ Object

Installs an APT package

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

Parameters:

  • package (String)

    the package name

  • cmd (String) (defaults to: package)

    an executable installed by the package, defaults to the package name



154
155
156
157
158
159
# 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_updateObject



161
162
163
# File 'lib/dpl/ctx/bash.rb', line 161

def apt_update
  shell 'sudo apt-get update', retry: true
end

#apts_get(packages) ⇒ Object



139
140
141
142
143
144
145
# 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_dirObject

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.



375
376
377
# File 'lib/dpl/ctx/bash.rb', line 375

def build_dir
  ENV['TRAVIS_BUILD_DIR'] || '.'
end

#build_numberObject

Returns the current build number

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



383
384
385
# 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) ⇒ Object

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

Parameters:

  • key (Symbol)

    the deprecated option key

  • msg (String or Symbol)

    the deprecation message. if given a Symbol this will be wrapped into the string “Please use #symbol”.



63
64
65
66
# 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) ⇒ Object

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



388
389
390
391
392
393
394
395
396
397
398
399
# 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) ⇒ Object

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.

Raises:



107
108
109
# File 'lib/dpl/ctx/bash.rb', line 107

def error(message)
  raise Error, message
end

#failed?Boolean

Whether or not the last executed shell command has failed.

Returns:

  • (Boolean)


326
327
328
# File 'lib/dpl/ctx/bash.rb', line 326

def failed?
  !success?
end

#file_size(path) ⇒ Object

Returns the size of the given file path



495
496
497
# File 'lib/dpl/ctx/bash.rb', line 495

def file_size(path)
  File.size(path)
end

#fold(msg) ⇒ Object

Folds any log output from the given block

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

Parameters:

  • msg (String)

    the message that will appear on the log fold



32
33
34
35
36
37
38
39
40
41
# 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) ⇒ Object

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.

Parameters:

  • 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 Also:



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# 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_emailObject

Returns the comitter email of the commit ‘git_sha`.



417
418
419
# File 'lib/dpl/ctx/bash.rb', line 417

def git_author_email
  `git log #{git_sha} -n 1 --pretty=%ae`.chomp
end

#git_author_nameObject

Returns the committer name of the commit ‘git_sha`.



412
413
414
# File 'lib/dpl/ctx/bash.rb', line 412

def git_author_name
  `git log #{git_sha} -n 1 --pretty=%an`.chomp
end

#git_branchObject

Returns the current branch name



402
403
404
# File 'lib/dpl/ctx/bash.rb', line 402

def git_branch
  ENV['TRAVIS_BRANCH'] || git_rev_parse('HEAD')
end

#git_commit_msgObject

Returns the message of the commit ‘git_sha`.



407
408
409
# File 'lib/dpl/ctx/bash.rb', line 407

def git_commit_msg
  `git log #{git_sha} -n 1 --pretty=%B`.chomp
end

#git_dirty?Boolean

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

Returns:

  • (Boolean)


422
423
424
# File 'lib/dpl/ctx/bash.rb', line 422

def git_dirty?
  !`git status --short`.chomp.empty?
end

#git_log(args) ⇒ Object

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



427
428
429
# File 'lib/dpl/ctx/bash.rb', line 427

def git_log(args)
  `git log #{args}`.chomp
end

#git_ls_filesObject

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.



435
436
437
# File 'lib/dpl/ctx/bash.rb', line 435

def git_ls_files
  `git ls-files -z`.split("\x0")
end

#git_ls_remote?(url, ref) ⇒ Boolean

Returns true if the given ref exists remotely

Returns:

  • (Boolean)


440
441
442
# 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_urlsObject

Returns known Git remote URLs



445
446
447
# 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) ⇒ Object

Returns the sha for the given Git ref



450
451
452
# File 'lib/dpl/ctx/bash.rb', line 450

def git_rev_parse(ref)
  `git rev-parse #{ref}`.strip
end

#git_shaObject

Returns the current commit sha



460
461
462
# File 'lib/dpl/ctx/bash.rb', line 460

def git_sha
  ENV['TRAVIS_COMMIT'] || `git rev-parse HEAD`.chomp
end

#git_tagObject

Returns the latest tag name, if any



455
456
457
# File 'lib/dpl/ctx/bash.rb', line 455

def git_tag
  `git describe --tags --exact-match 2>/dev/null`.chomp
end

#info(*msgs) ⇒ Object

Outputs an info level message to stdout.



69
70
71
# File 'lib/dpl/ctx/bash.rb', line 69

def info(*msgs)
  stdout.puts(*msgs)
end

#last_process_statusObject

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.



334
335
336
# File 'lib/dpl/ctx/bash.rb', line 334

def last_process_status
  $CHILD_STATUS.success?
end

#logger(level = :info) ⇒ Object

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.

Parameters:

  • level (Symbol) (defaults to: :info)

    the Ruby logger log level



120
121
122
123
124
# 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_nameObject

Returns the local machine’s hostname



465
466
467
# File 'lib/dpl/ctx/bash.rb', line 465

def machine_name
  `hostname`.strip
end

#move_files(paths) ⇒ Object



499
500
501
502
503
504
# 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) ⇒ Object



513
514
515
# File 'lib/dpl/ctx/bash.rb', line 513

def mv(src, dest)
  Kernel.system("sudo mv #{src} #{dest} 2> /dev/null")
end

#node_versionObject

Returns the current Node.js version



470
471
472
# File 'lib/dpl/ctx/bash.rb', line 470

def node_version
  `node -v`.sub(/^v/, '').chomp
end

#npm_install(package, cmd = package) ⇒ Object

Installs an NPM package

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

Parameters:

  • package (String)

    the package name

  • cmd (String) (defaults to: package)

    an executable installed by the package, defaults to the package name



202
203
204
# 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_versionObject

Returns the current NPM version



475
476
477
# File 'lib/dpl/ctx/bash.rb', line 475

def npm_version
  `npm --version`
end

#only(hash, *keys) ⇒ Object

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



542
543
544
# File 'lib/dpl/ctx/bash.rb', line 542

def only(hash, *keys)
  hash.select { |key, _| keys.include?(key) }.to_h
end

#open3(cmd, opts) ⇒ Object

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.

Parameters:

  • chdir (Hash)

    a customizable set of options



301
302
303
304
305
# 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) ⇒ Object

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.

Parameters:

  • package (String)

    Package name (required).

  • cmd (String) (defaults to: package)

    Executable command installed by that package (optional, defaults to the package name).

  • version (String) (defaults to: nil)

    Package version (optional).



214
215
216
217
218
219
220
221
222
# 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) ⇒ Object



224
225
226
# File 'lib/dpl/ctx/bash.rb', line 224

def pip_version(version)
  version =~ /^\d+/ ? "==#{version}" : version
end

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.'


85
86
87
# File 'lib/dpl/ctx/bash.rb', line 85

def print(chars)
  stdout.print(chars)
end

#python_versionObject

Returns the current Node.js version



480
481
482
# File 'lib/dpl/ctx/bash.rb', line 480

def python_version
  `python --version 2>&1`.sub(/^Python /, '').chomp
end

#repo_nameObject

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.



351
352
353
# 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_slugObject

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.



363
364
365
# 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) ⇒ Object



282
283
284
285
286
287
288
# 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 = {}) ⇒ Boolean

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:

Parameters:

  • cmd (String)

    the shell command to execute

  • opts (Hash) (defaults to: {})

    options

Options Hash (opts):

  • :echo (Boolean)

    output the command to stdout before running it

  • :silence (Boolean)

    silence all log output by redirecting stdout and stderr to ‘/dev/null`

  • :capture (Boolean)

    use ‘Open3.capture3` to capture stdout and stderr

  • :python (String)

    wrap the command into Bash code that enforces the given Python version to be used

  • :retry (String)

    retries the command 2 more times if it fails

  • :info (String)

    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.

  • :assert (String)

    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.

Returns:

  • (Boolean)

    whether or not the command was successful (has exited with the exit code 0)



267
268
269
270
271
272
273
274
275
276
277
278
279
280
# 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) ⇒ Object



533
534
535
# File 'lib/dpl/ctx/bash.rb', line 533

def sleep(sec)
  Kernel.sleep(sec)
end

#ssh_keygen(name, file) ⇒ Object

Generates an SSH key

Parameters:

  • name (String)

    the key name

  • file (String)

    path to the key file



232
233
234
# 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?Boolean

Whether or not the last executed shell command was successful.

Returns:

  • (Boolean)


321
322
323
# File 'lib/dpl/ctx/bash.rb', line 321

def success?
  !!@last_status
end

#sudo?Boolean

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

Returns:

  • (Boolean)


339
340
341
# File 'lib/dpl/ctx/bash.rb', line 339

def sudo?
  Process::UID.eid.zero?
end

#system(cmd, opts = {}) ⇒ Object

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.

Parameters:

  • chdir (Hash)

    a customizable set of options



314
315
316
317
318
# 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?Boolean

Returns:

  • (Boolean)


546
547
548
# File 'lib/dpl/ctx/bash.rb', line 546

def test?
  false
end

#timeObject

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.



48
49
50
51
52
53
54
55
56
57
# 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_dirObject

Returns a unique temporary directory name



490
491
492
# File 'lib/dpl/ctx/bash.rb', line 490

def tmp_dir
  @tmp_dir ||= Dir.mktmpdir
end

#tty?Boolean

Returns:

  • (Boolean)


537
538
539
# File 'lib/dpl/ctx/bash.rb', line 537

def tty?
  $stdout.isatty
end

#unmove_files(paths) ⇒ Object



506
507
508
509
510
511
# 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) ⇒ Object



132
133
134
135
136
137
# 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) ⇒ Object



126
127
128
129
130
# 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) ⇒ Object

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.



95
96
97
98
# 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) ⇒ Object

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



485
486
487
# File 'lib/dpl/ctx/bash.rb', line 485

def which(cmd)
  !`which #{cmd}`.chomp.empty? if cmd
end

#write_file(path, content, chmod = nil) ⇒ Object

Writes the given content to the given file path



518
519
520
521
522
523
# 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) ⇒ Object

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



526
527
528
529
530
531
# File 'lib/dpl/ctx/bash.rb', line 526

def write_netrc(machine, , password)
  require 'netrc'
  netrc = Netrc.read
  netrc[machine] = [, password]
  netrc.save
end