class Dependabot::GoModules::FileUpdater::GoModUpdater

Constants

ENVIRONMENT

Turn off the module proxy for now, as it's causing issues with private git dependencies

GO_MOD_VERSION
MODULE_PATH_MISMATCH_REGEXES
OUT_OF_DISK_REGEXES
REPO_RESOLVABILITY_ERROR_REGEXES
RESOLVABILITY_ERROR_REGEXES

Attributes

credentials[R]
dependencies[R]
directory[R]
repo_contents_path[R]

Public Class Methods

new(dependencies:, credentials:, repo_contents_path:, directory:, options:) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 56
def initialize(dependencies:, credentials:, repo_contents_path:,
               directory:, options:)
  @dependencies = dependencies
  @credentials = credentials
  @repo_contents_path = repo_contents_path
  @directory = directory
  @tidy = options.fetch(:tidy, false)
  @vendor = options.fetch(:vendor, false)
end

Public Instance Methods

updated_go_mod_content() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 66
def updated_go_mod_content
  updated_files[:go_mod]
end
updated_go_sum_content() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 70
def updated_go_sum_content
  updated_files[:go_sum]
end

Private Instance Methods

build_module_stubs(stub_paths) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 199
def build_module_stubs(stub_paths)
  # Create a fake empty module for each local module so that
  # `go get -d` works, even if some modules have been `replace`d
  # with a local module that we don't have access to.
  stub_paths.each do |stub_path|
    Dir.mkdir(stub_path) unless Dir.exist?(stub_path)
    FileUtils.touch(File.join(stub_path, "go.mod"))
    FileUtils.touch(File.join(stub_path, "main.go"))
  end
end
filter_error_message(message:, regex:) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 270
def filter_error_message(message:, regex:)
  lines = message.lines.select { |l| regex =~ l }
  return lines.join if lines.any?

  # In case the regex is multi-line, match the whole string
  message.match(regex).to_s
end
go_mod_path() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 278
def go_mod_path
  return "go.mod" if directory == "/"

  File.join(directory, "go.mod")
end
handle_subprocess_error(stderr) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 232
def handle_subprocess_error(stderr) # rubocop:disable Metrics/AbcSize
  stderr = stderr.gsub(Dir.getwd, "")

  # Package version doesn't match the module major version
  error_regex = RESOLVABILITY_ERROR_REGEXES.find { |r| stderr =~ r }
  if error_regex
    error_message = filter_error_message(message: stderr, regex: error_regex)
    raise Dependabot::DependencyFileNotResolvable, error_message
  end

  if (matches = stderr.match(/Authentication failed for '(?<url>.+)'/))
    raise Dependabot::PrivateSourceAuthenticationFailure, matches[:url]
  end

  repo_error_regex = REPO_RESOLVABILITY_ERROR_REGEXES.find { |r| stderr =~ r }
  if repo_error_regex
    error_message = filter_error_message(message: stderr, regex: repo_error_regex)
    ResolvabilityErrors.handle(error_message, credentials: credentials)
  end

  path_regex = MODULE_PATH_MISMATCH_REGEXES.find { |r| stderr =~ r }
  if path_regex
    match = path_regex.match(stderr)
    raise Dependabot::GoModulePathMismatch.
      new(go_mod_path, match[1], match[2])
  end

  out_of_disk_regex = OUT_OF_DISK_REGEXES.find { |r| stderr =~ r }
  if out_of_disk_regex
    error_message = filter_error_message(message: stderr, regex: out_of_disk_regex)
    raise Dependabot::OutOfDisk.new, error_message
  end

  # We don't know what happened so we raise a generic error
  msg = stderr.lines.last(10).join.strip
  raise Dependabot::DependabotError, msg
end
in_repo_path(&block) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 191
def in_repo_path(&block)
  SharedHelpers.in_a_temporary_repo_directory(directory, repo_contents_path) do
    SharedHelpers.with_git_configured(credentials: credentials) do
      block.call
    end
  end
end
parse_manifest() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 183
def parse_manifest
  command = "go mod edit -json"
  stdout, stderr, status = Open3.capture3(ENVIRONMENT, command)
  handle_subprocess_error(stderr) unless status.success?

  JSON.parse(stdout) || {}
end
replace_directive_substitutions(manifest) click to toggle source

Given a go.mod file, find all `replace` directives pointing to a path on the local filesystem, and return an array of pairs mapping the original path to a hash of the path.

This lets us substitute all parts of the go.mod that are dependent on the layout of the filesystem with a structure we can reproduce (i.e. no paths such as ../../../foo), run the Go tooling, then reverse the process afterwards.

# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 218
def replace_directive_substitutions(manifest)
  @replace_directive_substitutions ||=
    Dependabot::GoModules::ReplaceStubber.new(repo_contents_path).
    stub_paths(manifest, directory)
end
run_go_get(dependencies = []) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 159
def run_go_get(dependencies = [])
  tmp_go_file = "#{SecureRandom.hex}.go"

  package = Dir.glob("[^\._]*.go").any? do |path|
    !File.read(path).include?("// +build")
  end

  File.write(tmp_go_file, "package dummypkg\n") unless package

  # TODO: go 1.18 will make `-d` the default behavior, so remove the flag then
  command = +"go get -d"
  # `go get` accepts multiple packages, each separated by a space
  dependencies.each do |dep|
    version = "v" + dep.version.sub(/^v/i, "")
    command << " #{dep.name}@#{version}"
  end
  command = SharedHelpers.escape_command(command)

  _, stderr, status = Open3.capture3(ENVIRONMENT, command)
  handle_subprocess_error(stderr) unless status.success?
ensure
  File.delete(tmp_go_file) if File.exist?(tmp_go_file)
end
run_go_mod_tidy() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 139
def run_go_mod_tidy
  return unless tidy?

  command = "go mod tidy -e"

  # we explicitly don't raise an error for 'go mod tidy' and silently
  # continue here. `go mod tidy` shouldn't block updating versions
  # because there are some edge cases where it's OK to fail (such as
  # generated files not available yet to us).
  Open3.capture3(ENVIRONMENT, command)
end
run_go_vendor() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 151
def run_go_vendor
  return unless vendor?

  command = "go mod vendor"
  _, stderr, status = Open3.capture3(ENVIRONMENT, command)
  handle_subprocess_error(stderr) unless status.success?
end
substitute_all(substitutions) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 224
def substitute_all(substitutions)
  body = substitutions.reduce(File.read("go.mod")) do |text, (a, b)|
    text.sub(a, b)
  end

  write_go_mod(body)
end
tidy?() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 288
def tidy?
  !!@tidy
end
update_files() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 83
def update_files # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
  in_repo_path do
    # Map paths in local replace directives to path hashes
    original_go_mod = File.read("go.mod")
    original_manifest = parse_manifest
    original_go_sum = File.read("go.sum") if File.exist?("go.sum")

    substitutions = replace_directive_substitutions(original_manifest)
    build_module_stubs(substitutions.values)

    # Replace full paths with path hashes in the go.mod
    substitute_all(substitutions)

    # Bump the deps we want to upgrade using `go get lib@version`
    run_go_get(dependencies)

    # Run `go get`'s internal validation checks against _each_ module in `go.mod`
    # by running `go get` w/o specifying any library. It finds problems like when a
    # module declares itself using a different name than specified in our `go.mod` etc.
    run_go_get

    # If we stubbed modules, don't run `go mod {tidy,vendor}` as
    # dependencies are incomplete
    if substitutions.empty?
      # go mod tidy should run before go mod vendor to ensure any
      # dependencies removed by go mod tidy are also removed from vendors.
      run_go_mod_tidy
      run_go_vendor
    else
      substitute_all(substitutions.invert)
    end

    updated_go_sum = original_go_sum ? File.read("go.sum") : nil
    updated_go_mod = File.read("go.mod")

    # running "go get" may inject the current go version, remove it
    original_go_version = original_go_mod.match(GO_MOD_VERSION)&.to_a&.first
    updated_go_version = updated_go_mod.match(GO_MOD_VERSION)&.to_a&.first
    if original_go_version != updated_go_version
      go_mod_lines = updated_go_mod.lines
      go_mod_lines.each_with_index do |line, i|
        next unless line&.match?(GO_MOD_VERSION)

        # replace with the original version
        go_mod_lines[i] = original_go_version
        # avoid a stranded newline if there was no version originally
        go_mod_lines[i + 1] = nil if original_go_version.nil?
      end

      updated_go_mod = go_mod_lines.compact.join
    end

    { go_mod: updated_go_mod, go_sum: updated_go_sum }
  end
end
updated_files() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 79
def updated_files
  @updated_files ||= update_files
end
vendor?() click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 292
def vendor?
  !!@vendor
end
write_go_mod(body) click to toggle source
# File lib/dependabot/go_modules/file_updater/go_mod_updater.rb, line 284
def write_go_mod(body)
  File.write("go.mod", body)
end