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.



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

def initialize(stdout = $stdout, stderr = $stderr)
  @stdout, @stderr = stdout, stderr
  @folds = 0
  super('dpl', abort: false)
end

Instance Attribute Details

#foldsObject

Returns the value of attribute folds.



14
15
16
# File 'lib/dpl/ctx/bash.rb', line 14

def folds
  @folds
end

#last_errObject

Returns the value of attribute last_err.



14
15
16
# File 'lib/dpl/ctx/bash.rb', line 14

def last_err
  @last_err
end

#last_outObject

Returns the value of attribute last_out.



14
15
16
# File 'lib/dpl/ctx/bash.rb', line 14

def last_out
  @last_out
end

#stderrObject

Returns the value of attribute stderr.



14
15
16
# File 'lib/dpl/ctx/bash.rb', line 14

def stderr
  @stderr
end

#stdoutObject

Returns the value of attribute stdout.



14
15
16
# File 'lib/dpl/ctx/bash.rb', line 14

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



149
150
151
152
153
# File 'lib/dpl/ctx/bash.rb', line 149

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



155
156
157
# File 'lib/dpl/ctx/bash.rb', line 155

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

#apts_get(packages) ⇒ Object



135
136
137
138
139
140
# File 'lib/dpl/ctx/bash.rb', line 135

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.



367
368
369
# File 'lib/dpl/ctx/bash.rb', line 367

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.



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

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



59
60
61
62
# File 'lib/dpl/ctx/bash.rb', line 59

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



380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/dpl/ctx/bash.rb', line 380

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:



103
104
105
# File 'lib/dpl/ctx/bash.rb', line 103

def error(message)
  raise Error, message
end

#failed?Boolean

Whether or not the last executed shell command has failed.

Returns:

  • (Boolean)


318
319
320
# File 'lib/dpl/ctx/bash.rb', line 318

def failed?
  !success?
end

#file_size(path) ⇒ Object

Returns the size of the given file path



487
488
489
# File 'lib/dpl/ctx/bash.rb', line 487

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

#fold(msg, &block) ⇒ 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



28
29
30
31
32
33
34
35
36
37
# File 'lib/dpl/ctx/bash.rb', line 28

def fold(msg, &block)
  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:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/dpl/ctx/bash.rb', line 169

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 { |g| gem *g }
    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`.



409
410
411
# File 'lib/dpl/ctx/bash.rb', line 409

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



404
405
406
# File 'lib/dpl/ctx/bash.rb', line 404

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

#git_branchObject

Returns the current branch name



394
395
396
# File 'lib/dpl/ctx/bash.rb', line 394

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

#git_commit_msgObject

Returns the message of the commit ‘git_sha`.



399
400
401
# File 'lib/dpl/ctx/bash.rb', line 399

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)


414
415
416
# File 'lib/dpl/ctx/bash.rb', line 414

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

#git_log(args) ⇒ Object

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



419
420
421
# File 'lib/dpl/ctx/bash.rb', line 419

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.



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

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)


432
433
434
# File 'lib/dpl/ctx/bash.rb', line 432

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



437
438
439
# File 'lib/dpl/ctx/bash.rb', line 437

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



442
443
444
# File 'lib/dpl/ctx/bash.rb', line 442

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

#git_shaObject

Returns the current commit sha



452
453
454
# File 'lib/dpl/ctx/bash.rb', line 452

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

#git_tagObject

Returns the latest tag name, if any



447
448
449
# File 'lib/dpl/ctx/bash.rb', line 447

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

#info(*msgs) ⇒ Object

Outputs an info level message to stdout.



65
66
67
# File 'lib/dpl/ctx/bash.rb', line 65

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.



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

def last_process_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



116
117
118
119
120
# File 'lib/dpl/ctx/bash.rb', line 116

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



457
458
459
# File 'lib/dpl/ctx/bash.rb', line 457

def machine_name
  `hostname`.strip
end

#move_files(paths) ⇒ Object



491
492
493
494
495
496
# File 'lib/dpl/ctx/bash.rb', line 491

def move_files(paths)
  paths.each do |path|
    target = "#{tmp_dir}/#{File.basename(path)}"
    mv(path, target) if File.exists?(path)
  end
end

#mv(src, dest) ⇒ Object



505
506
507
# File 'lib/dpl/ctx/bash.rb', line 505

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

#node_versionObject

Returns the current Node.js version



462
463
464
# File 'lib/dpl/ctx/bash.rb', line 462

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



194
195
196
# File 'lib/dpl/ctx/bash.rb', line 194

def npm_install(package, cmd = package)
  shell "npm install -g #{package}", retry: true unless which(cmd)
end

#npm_versionObject

Returns the current NPM version



467
468
469
# File 'lib/dpl/ctx/bash.rb', line 467

def npm_version
  `npm --version`
end

#only(hash, *keys) ⇒ Object

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



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

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



293
294
295
296
297
# File 'lib/dpl/ctx/bash.rb', line 293

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



206
207
208
209
210
211
212
213
214
# File 'lib/dpl/ctx/bash.rb', line 206

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 --no-site-packages ~/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



216
217
218
# File 'lib/dpl/ctx/bash.rb', line 216

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


81
82
83
# File 'lib/dpl/ctx/bash.rb', line 81

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

#python_versionObject

Returns the current Node.js version



472
473
474
# File 'lib/dpl/ctx/bash.rb', line 472

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.



343
344
345
# File 'lib/dpl/ctx/bash.rb', line 343

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.



355
356
357
# File 'lib/dpl/ctx/bash.rb', line 355

def repo_slug
  ENV['TRAVIS_REPO_SLUG'] || Dir.pwd.split('/')[-2, 2].join('/')
end

#retrying(max, tries = 0, status = false) ⇒ Object



274
275
276
277
278
279
280
# File 'lib/dpl/ctx/bash.rb', line 274

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)



259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/dpl/ctx/bash.rb', line 259

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 cmd.success % { out: last_out } if success? && cmd.success?
  error cmd.error % { err: last_err } if failed? && cmd.assert?

  success? && cmd.capture? ? last_out.chomp : @last_status
end

#sleep(sec) ⇒ Object



525
526
527
# File 'lib/dpl/ctx/bash.rb', line 525

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



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

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)


313
314
315
# File 'lib/dpl/ctx/bash.rb', line 313

def success?
  !!@last_status
end

#sudo?Boolean

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

Returns:

  • (Boolean)


331
332
333
# File 'lib/dpl/ctx/bash.rb', line 331

def sudo?
  Process::UID.eid == 0
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



306
307
308
309
310
# File 'lib/dpl/ctx/bash.rb', line 306

def system(cmd, opts = {})
  opts = [opts[:chdir] ? only(opts, :chdir) : nil].compact
  Kernel.system(cmd, *opts)
  ['', '', last_process_status]
end

#test?Boolean

Returns:

  • (Boolean)


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

def test?
  false
end

#time(&block) ⇒ Object

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.



44
45
46
47
48
49
50
51
52
53
# File 'lib/dpl/ctx/bash.rb', line 44

def time(&block)
  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



482
483
484
# File 'lib/dpl/ctx/bash.rb', line 482

def tmp_dir
  @tmp_dir ||= Dir.mktmpdir
end

#tty?Boolean

Returns:

  • (Boolean)


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

def tty?
  $stdout.isatty
end

#unmove_files(paths) ⇒ Object



498
499
500
501
502
503
# File 'lib/dpl/ctx/bash.rb', line 498

def unmove_files(paths)
  paths.each do |path|
    source = "#{tmp_dir}/#{File.basename(path)}"
    mv(source, path) if File.exists?(source)
  end
end

#validate_runtime(args) ⇒ Object



128
129
130
131
132
133
# File 'lib/dpl/ctx/bash.rb', line 128

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



122
123
124
125
126
# File 'lib/dpl/ctx/bash.rb', line 122

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.



91
92
93
94
# File 'lib/dpl/ctx/bash.rb', line 91

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



477
478
479
# File 'lib/dpl/ctx/bash.rb', line 477

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



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

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



518
519
520
521
522
523
# File 'lib/dpl/ctx/bash.rb', line 518

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