Class: Dependabot::Linguist::DependabotFileValidator

Inherits:
Object
  • Object
show all
Defined in:
lib/dependabot/linguist/dependabot_file_validator.rb

Overview

Reads an existing dependabot file and determines how it should be updated to meet the suggested entried to the updates list coming from repository’s directories_per_ecosystem_validated_by_dependabot

Defined Under Namespace

Modules: ConfigDriftStatus

Constant Summary collapse

YAML_FILE_PATH =
".github/dependabot.yaml"
YML_FILE_PATH =
".github/dependabot.yml"
CONFIG_FILE_PATH =
".github/.dependabot-linguist"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(repo_path, remove_undiscovered: false, update_existing: true, minimum_interval: "weekly", max_open_pull_requests_limit: 5, verbose: false) ⇒ DependabotFileValidator

Returns a new instance of DependabotFileValidator.



12
13
14
15
16
17
18
19
20
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 12

def initialize(repo_path, remove_undiscovered: false, update_existing: true, minimum_interval: "weekly", max_open_pull_requests_limit: 5, verbose: false)
  @repo = Rugged::Repository.new(repo_path)
  @remove_undiscovered = remove_undiscovered
  @update_existing = update_existing
  @minimum_interval = minimum_interval
  @max_open_pull_requests_limit = [max_open_pull_requests_limit, 0].max
  @verbose = verbose
  @load_ecosystem_directories ||= nil
end

Class Method Details

.checking_exists(checking, exists) ⇒ Object



98
99
100
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 98

def self.checking_exists(checking, exists)
  exists["package-ecosystem"] == checking[0] && exists["directory"] == checking[1]
end

.flatten_ecodirs_to_ecodir(ecosystem_directories_map) ⇒ Object



94
95
96
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 94

def self.flatten_ecodirs_to_ecodir(ecosystem_directories_map)
  ecosystem_directories_map.collect { |eco, dirs| dirs.collect { |dir| [eco, dir] } }.flatten(1)
end

Instance Method Details

#commit_new_configObject

The expected environment to run this final step in should have ‘git’ AND ‘gh’ available as commands to run, and calls out to a subshell to run them as set up by the environment that runs this, rather than requiring credentials being provided to this class.



214
215
216
217
218
219
220
221
222
223
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 214

def commit_new_config
  new_branch = @repo.create_branch("dependabot-linguist_auto-config-update")
  in_repo = "cd #{@repo.path.delete_suffix("/.git/")} &&"
  `#{"#{in_repo} git checkout #{new_branch.name}"}`
  write_new_config
  `#{"#{in_repo} git add #{dependabot_file_path}"}`
  `#{"#{in_repo} git commit -m \"Auto update #{dependabot_file_path} -- dependabot-linguist\""}`
  `#{"#{in_repo} git push --set-upstream #{@repo.remotes["origin"].name} #{new_branch.name}"}`
  `#{"#{in_repo} gh pr create --fill"}`
end

#config_driftObject



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 108

def config_drift
  confirm_config_version_is_valid
  @config_drift ||= {}.tap do |this|
    ecodir_list = self.class.flatten_ecodirs_to_ecodir(load_ecosystem_directories)
    this[ConfigDriftStatus::ALREADY_IN] = []
    this[ConfigDriftStatus::TO_BE_ADDED] = []
    this[ConfigDriftStatus::UNDISCOVERED] = []
    this.freeze
    ecodir_list.each do |checking_ecodir|
      next if ecodir_is_ignored(checking_ecodir[0], checking_ecodir[1])
      if !existing_config.empty? && !existing_config["updates"].nil?
        existed_ecodir = nil
        existing_config["updates"].each do |existing_ecodir|
          if self.class.checking_exists(checking_ecodir, existing_ecodir)
            puts "#{ConfigDriftStatus::ALREADY_IN}; {#{checking_ecodir[0]} @ #{checking_ecodir[1]}}" if @verbose
            this[ConfigDriftStatus::ALREADY_IN].append(checking_ecodir)
            existed_ecodir = existing_ecodir
            break # existing_ecodir
          end
        end
        # break to here
        next unless existed_ecodir.nil? # checking_ecodir
      end
      # If we didn't break -> next, then we've got a checking_ecodir
      # that we didn't find already present in the existing ecodirs.
      puts "#{ConfigDriftStatus::TO_BE_ADDED}; {#{checking_ecodir[0]} @ #{checking_ecodir[1]}}" if @verbose
      this[ConfigDriftStatus::TO_BE_ADDED].append(checking_ecodir)
    end
    if !existing_config.empty? && !existing_config["updates"].nil?
      existing_config["updates"].each do |existing_ecodir|
        existed_ecodir = nil
        ecodir_list.each do |checking_ecodir|
          break if ecodir_is_ignored(checking_ecodir[0], checking_ecodir[1])
          existed_ecodir = checking_ecodir if self.class.checking_exists(checking_ecodir, existing_ecodir)
          break unless existed_ecodir.nil?
        end
        if existed_ecodir.nil?
          puts "#{ConfigDriftStatus::UNDISCOVERED}; {#{existing_ecodir["package-ecosystem"]} @ #{existing_ecodir["directory"]}} that wasn't found by us!!" if @verbose
          this[ConfigDriftStatus::UNDISCOVERED].append([existing_ecodir["package-ecosystem"], existing_ecodir["directory"]])
        end
      end
    end
  end
end

#confirm_config_version_is_validObject



76
77
78
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 76

def confirm_config_version_is_valid
  raise StandardError("The existing config has a version other than 2") unless existing_config["version"] == 2
end

#dependabot_file_pathObject

rubocop:disable Layout/IndentationWidth, Layout/ElseAlignment, Layout/EndAlignment



30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 30

def dependabot_file_path
  @dependabot_file_path ||= if @repo.blob_at(@repo.head.target_id, YML_FILE_PATH)
    # the yml extension is preferred by GitHub, so even though this
    # returns the same as the `else`, check it before YAML.
    YML_FILE_PATH
  elsif @repo.blob_at(@repo.head.target_id, YAML_FILE_PATH)
    YAML_FILE_PATH
  else
    @existing_config = { "version" => 2, "updates" => [] }
    YML_FILE_PATH
  end
end

#ecodir_is_ignored(eco, dir) ⇒ Object

Is a yaml config file exists that looks like

ignore:

directory:
  /path/to/somewhere:
  - some_ecosystem
ecosystem:
  some_other_ecosystem:
  - /path/to/somewhere_else

then both (some_ecosystem, “/path/to/somewhere”) and (some_other_ecosystem, “/path/to/somewhere_else”) should be “ignored” by this system.



72
73
74
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 72

def ecodir_is_ignored(eco, dir)
  ((((meta_config["ignore"] || {})["directory"] || {})[dir] || []).any? eco) || ((((meta_config["ignore"] || {})["ecosystem"] || {})[eco] || []).any? dir)
end

#existing_configObject



43
44
45
46
47
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 43

def existing_config
  dependabot_file_path # to = {} if the file doesn't exist or isn't committed.
  # @existing_config ||= YAML.load_file(File.join(@repo.path, dependabot_file_path))
  @existing_config ||= YAML.safe_load(@repo.blob_at(@repo.head.target_id, dependabot_file_path).content)
end

#load_ecosystem_directories(incoming: @load_ecosystem_directories) ⇒ Object

Expects an input that is the output of ::Dependabot::Linguist::Repository.new(~)‘s directories_per_ecosystem_validated_by_dependabot, which should be a map => [“<folder_path>”, …], …



83
84
85
86
87
88
89
90
91
92
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 83

def load_ecosystem_directories(incoming: @load_ecosystem_directories)
  @load_ecosystem_directories ||= nil
  if @load_ecosystem_directories == incoming
    @load_ecosystem_directories
  else
    @config_drift = nil
    @new_config = nil
    @load_ecosystem_directories = incoming
  end
end

#meta_configObject



49
50
51
52
53
54
55
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 49

def meta_config
  @meta_config ||= if @repo.blob_at(@repo.head.target_id, CONFIG_FILE_PATH)
    YAML.safe_load(@repo.blob_at(@repo.head.target_id, CONFIG_FILE_PATH).content)
  else
    {}
  end
end

#new_configObject



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 162

def new_config
  confirm_config_version_is_valid
  @new_config ||= YAML.safe_load(existing_config.to_yaml).tap do |this|
    this["updates"] = [] if this["updates"].nil?
    # If "remove_undiscovered" is set, then set this to reject any
    # updates that are in the list of those undiscovered. Removing
    # is not safe from inside each, so reject instead.
    this["updates"] = this["updates"].reject { |u| config_drift[ConfigDriftStatus::UNDISCOVERED].any? [u["package-ecosystem"], u["directory"]] } if @remove_undiscovered
    # Next, go through and update any existing.
    if @update_existing
      this["updates"].each do |existing_update|
        if config_drift[ConfigDriftStatus::ALREADY_IN].any? [existing_update["package-ecosystem"], existing_update["directory"]]
          # Confirm that the already present entry is good enough
          if existing_update["schedule"].is_a? Hash
            new_interval = parsed_schedule_interval(existing_update["schedule"]["interval"])
            existing_update["schedule"]["interval"] = new_interval
            # if it's not weekly anymore remove day if it's specified.
            if existing_update["schedule"]["interval"] != "weekly"
              existing_update["schedule"].delete("day")
            end
          else
            existing_update["schedule"] = { "interval" => parsed_schedule_interval("monthly") }
          end
          # Confirm the open-pull-requests-limit
          if existing_update["open-pull-requests-limit"]
            existing_update["open-pull-requests-limit"] = [existing_update["open-pull-requests-limit"], @max_open_pull_requests_limit].min
          else
            existing_update["open-pull-requests-limit"] = @max_open_pull_requests_limit
          end
          existing_update.delete("open-pull-requests-limit") if existing_update["open-pull-requests-limit"] == 5
        end
      end
    end
    config_drift[ConfigDriftStatus::TO_BE_ADDED].each do |tba|
      new_update = { "package-ecosystem" => tba[0], "directory" => tba[1] }
      new_update["schedule"] = { "interval" => parsed_schedule_interval("monthly") }
      new_update["open-pull-requests-limit"] = @max_open_pull_requests_limit if @max_open_pull_requests_limit != 5
      this["updates"].append(new_update)
    end
  end
end

#parsed_schedule_interval(interval) ⇒ Object



153
154
155
156
157
158
159
160
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 153

def parsed_schedule_interval(interval)
  intervals = ["daily", "weekly", "monthly"].freeze
  if intervals.any? @minimum_interval
    intervals[[intervals.find_index(@minimum_interval) || (intervals.length-1), intervals.find_index(interval) || (intervals.length-1)].min]
  else
    interval
  end
end

#write_new_configObject



204
205
206
207
208
# File 'lib/dependabot/linguist/dependabot_file_validator.rb', line 204

def write_new_config
  full_file_path = "#{@repo.path.delete_suffix("/.git/")}/#{dependabot_file_path}"
  FileUtils.mkdir_p File.dirname(full_file_path)
  File.open(full_file_path, "w") { |file| file.write(new_config.to_yaml) } if new_config != existing_config
end