dependabot-hex 0.88.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/build +19 -0
- data/helpers/deps/jason/.fetch +0 -0
- data/helpers/deps/jason/.hex +2 -0
- data/helpers/deps/jason/CHANGELOG.md +60 -0
- data/helpers/deps/jason/LICENSE +13 -0
- data/helpers/deps/jason/README.md +179 -0
- data/helpers/deps/jason/hex_metadata.config +20 -0
- data/helpers/deps/jason/lib/codegen.ex +158 -0
- data/helpers/deps/jason/lib/decoder.ex +657 -0
- data/helpers/deps/jason/lib/encode.ex +630 -0
- data/helpers/deps/jason/lib/encoder.ex +216 -0
- data/helpers/deps/jason/lib/formatter.ex +253 -0
- data/helpers/deps/jason/lib/fragment.ex +11 -0
- data/helpers/deps/jason/lib/helpers.ex +90 -0
- data/helpers/deps/jason/lib/jason.ex +228 -0
- data/helpers/deps/jason/mix.exs +92 -0
- data/helpers/lib/check_update.exs +92 -0
- data/helpers/lib/do_update.exs +39 -0
- data/helpers/lib/parse_deps.exs +103 -0
- data/helpers/lib/run.exs +76 -0
- data/helpers/mix.exs +21 -0
- data/helpers/mix.lock +3 -0
- data/lib/dependabot/hex.rb +11 -0
- data/lib/dependabot/hex/file_fetcher.rb +79 -0
- data/lib/dependabot/hex/file_parser.rb +125 -0
- data/lib/dependabot/hex/file_updater.rb +71 -0
- data/lib/dependabot/hex/file_updater/lockfile_updater.rb +142 -0
- data/lib/dependabot/hex/file_updater/mixfile_git_pin_updater.rb +51 -0
- data/lib/dependabot/hex/file_updater/mixfile_requirement_updater.rb +72 -0
- data/lib/dependabot/hex/file_updater/mixfile_sanitizer.rb +26 -0
- data/lib/dependabot/hex/file_updater/mixfile_updater.rb +94 -0
- data/lib/dependabot/hex/metadata_finder.rb +70 -0
- data/lib/dependabot/hex/native_helpers.rb +20 -0
- data/lib/dependabot/hex/requirement.rb +53 -0
- data/lib/dependabot/hex/update_checker.rb +275 -0
- data/lib/dependabot/hex/update_checker/file_preparer.rb +191 -0
- data/lib/dependabot/hex/update_checker/requirements_updater.rb +173 -0
- data/lib/dependabot/hex/update_checker/version_resolver.rb +170 -0
- data/lib/dependabot/hex/version.rb +67 -0
- metadata +208 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
[dependency_name | credentials] = System.argv()
|
2
|
+
|
3
|
+
grouped_creds = Enum.reduce credentials, [], fn cred, acc ->
|
4
|
+
if List.last(acc) == nil || List.last(acc)[:token] do
|
5
|
+
List.insert_at(acc, -1, %{ organization: cred })
|
6
|
+
else
|
7
|
+
{ item, acc } = List.pop_at(acc, -1)
|
8
|
+
item = Map.put(item, :token, cred)
|
9
|
+
List.insert_at(acc, -1, item)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
Enum.each grouped_creds, fn cred ->
|
14
|
+
hexpm = Hex.Repo.get_repo("hexpm")
|
15
|
+
repo = %{
|
16
|
+
url: hexpm.url <> "/repos/#{cred.organization}",
|
17
|
+
public_key: nil,
|
18
|
+
auth_key: cred.token
|
19
|
+
}
|
20
|
+
|
21
|
+
Hex.Config.read()
|
22
|
+
|> Hex.Config.read_repos()
|
23
|
+
|> Map.put("hexpm:#{cred.organization}", repo)
|
24
|
+
|> Hex.Config.update_repos()
|
25
|
+
end
|
26
|
+
|
27
|
+
# dependency atom
|
28
|
+
dependency = String.to_atom(dependency_name)
|
29
|
+
|
30
|
+
# Fetch dependencies that needs updating
|
31
|
+
{dependency_lock, rest_lock} = Map.split(Mix.Dep.Lock.read(), [dependency])
|
32
|
+
Mix.Dep.Fetcher.by_name([dependency_name], dependency_lock, rest_lock, [])
|
33
|
+
|
34
|
+
lockfile_content =
|
35
|
+
"mix.lock"
|
36
|
+
|> File.read()
|
37
|
+
|> :erlang.term_to_binary()
|
38
|
+
|
39
|
+
IO.write(:stdio, lockfile_content)
|
@@ -0,0 +1,103 @@
|
|
1
|
+
defmodule Parser do
|
2
|
+
def run do
|
3
|
+
Mix.Dep.load_on_environment([])
|
4
|
+
|> Enum.flat_map(&parse_dep/1)
|
5
|
+
|> Enum.map(&build_dependency(&1.opts[:lock], &1))
|
6
|
+
end
|
7
|
+
|
8
|
+
defp build_dependency(nil, dep) do
|
9
|
+
%{
|
10
|
+
name: dep.app,
|
11
|
+
from: Path.relative_to_cwd(dep.from),
|
12
|
+
groups: [],
|
13
|
+
requirement: normalise_requirement(dep.requirement),
|
14
|
+
top_level: dep.top_level || umbrella_top_level_dep?(dep)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
defp build_dependency(lock, dep) do
|
19
|
+
{version, checksum, source} = parse_lock(lock)
|
20
|
+
groups = parse_groups(dep.opts[:only])
|
21
|
+
|
22
|
+
%{
|
23
|
+
name: dep.app,
|
24
|
+
from: Path.relative_to_cwd(dep.from),
|
25
|
+
version: version,
|
26
|
+
groups: groups,
|
27
|
+
checksum: checksum,
|
28
|
+
requirement: normalise_requirement(dep.requirement),
|
29
|
+
source: source,
|
30
|
+
top_level: dep.top_level || umbrella_top_level_dep?(dep)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
defp parse_groups(nil), do: []
|
35
|
+
defp parse_groups(only) when is_list(only), do: only
|
36
|
+
defp parse_groups(only), do: [only]
|
37
|
+
|
38
|
+
# path dependency
|
39
|
+
defp parse_dep(%{scm: Mix.SCM.Path, opts: opts} = dep) do
|
40
|
+
cond do
|
41
|
+
# umbrella dependency - ignore
|
42
|
+
opts[:in_umbrella] ->
|
43
|
+
[]
|
44
|
+
|
45
|
+
# umbrella application
|
46
|
+
opts[:from_umbrella] ->
|
47
|
+
Enum.reject(dep.deps, fn dep -> dep.opts[:in_umbrella] end)
|
48
|
+
|
49
|
+
true ->
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# hex, git dependency
|
55
|
+
defp parse_dep(%{scm: scm} = dep) when scm in [Hex.SCM, Mix.SCM.Git], do: [dep]
|
56
|
+
|
57
|
+
# unsupported
|
58
|
+
defp parse_dep(_dep), do: []
|
59
|
+
|
60
|
+
defp umbrella_top_level_dep?(dep) do
|
61
|
+
if Mix.Project.umbrella?() do
|
62
|
+
apps_paths = Path.expand(Mix.Project.config()[:apps_path], File.cwd!())
|
63
|
+
String.contains?(Path.dirname(Path.dirname(dep.from)), apps_paths)
|
64
|
+
else
|
65
|
+
false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
defp parse_lock({:git, repo_url, checksum, opts}),
|
70
|
+
do: {nil, checksum, git_source(repo_url, opts)}
|
71
|
+
|
72
|
+
defp parse_lock({:hex, _app, version, checksum, _managers, _dependencies, _source}),
|
73
|
+
do: {version, checksum, nil}
|
74
|
+
|
75
|
+
defp parse_lock({:hex, _app, version, checksum, _managers, _dependencies}),
|
76
|
+
do: {version, checksum, nil}
|
77
|
+
|
78
|
+
defp normalise_requirement(req) do
|
79
|
+
req
|
80
|
+
|> maybe_regex_to_str()
|
81
|
+
|> empty_str_to_nil()
|
82
|
+
end
|
83
|
+
|
84
|
+
defp maybe_regex_to_str(s), do: if Regex.regex?(s), do: Regex.source(s), else: s
|
85
|
+
defp empty_str_to_nil(""), do: nil
|
86
|
+
defp empty_str_to_nil(s), do: s
|
87
|
+
|
88
|
+
def git_source(repo_url, opts) do
|
89
|
+
ref = opts[:ref] || opts[:tag]
|
90
|
+
ref = if is_list(ref), do: to_string(ref), else: ref
|
91
|
+
|
92
|
+
%{
|
93
|
+
type: "git",
|
94
|
+
url: repo_url,
|
95
|
+
branch: opts[:branch] || "master",
|
96
|
+
ref: ref
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
dependencies = :erlang.term_to_binary({:ok, Parser.run()})
|
102
|
+
|
103
|
+
IO.write(:stdio, dependencies)
|
data/helpers/lib/run.exs
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
defmodule DependencyHelper do
|
2
|
+
def main() do
|
3
|
+
IO.read(:stdio, :all)
|
4
|
+
|> Jason.decode!()
|
5
|
+
|> run()
|
6
|
+
|> case do
|
7
|
+
{output, 0} ->
|
8
|
+
if output =~ "No authenticated organization found" do
|
9
|
+
{:error, output}
|
10
|
+
else
|
11
|
+
{:ok, :erlang.binary_to_term(output)}
|
12
|
+
end
|
13
|
+
|
14
|
+
{error, 1} -> {:error, error}
|
15
|
+
end
|
16
|
+
|> handle_result()
|
17
|
+
end
|
18
|
+
|
19
|
+
defp handle_result({:ok, {:ok, result}}) do
|
20
|
+
encode_and_write(%{"result" => result})
|
21
|
+
end
|
22
|
+
|
23
|
+
defp handle_result({:ok, {:error, reason}}) do
|
24
|
+
encode_and_write(%{"error" => reason})
|
25
|
+
System.halt(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
defp handle_result({:error, reason}) do
|
29
|
+
encode_and_write(%{"error" => reason})
|
30
|
+
System.halt(1)
|
31
|
+
end
|
32
|
+
|
33
|
+
defp encode_and_write(content) do
|
34
|
+
content
|
35
|
+
|> Jason.encode!()
|
36
|
+
|> IO.write()
|
37
|
+
end
|
38
|
+
|
39
|
+
defp run(%{"function" => "parse", "args" => [dir]}) do
|
40
|
+
run_script("parse_deps.exs", dir)
|
41
|
+
end
|
42
|
+
|
43
|
+
defp run(%{"function" => "get_latest_resolvable_version", "args" => [dir, dependency_name, credentials]}) do
|
44
|
+
run_script("check_update.exs", dir, [dependency_name] ++ credentials)
|
45
|
+
end
|
46
|
+
|
47
|
+
defp run(%{"function" => "get_updated_lockfile", "args" => [dir, dependency_name, credentials]}) do
|
48
|
+
run_script("do_update.exs", dir, [dependency_name] ++ credentials)
|
49
|
+
end
|
50
|
+
|
51
|
+
defp run_script(script, dir, args \\ []) do
|
52
|
+
args = [
|
53
|
+
"run",
|
54
|
+
"--no-deps-check",
|
55
|
+
"--no-start",
|
56
|
+
"--no-compile",
|
57
|
+
"--no-elixir-version-check",
|
58
|
+
script
|
59
|
+
] ++ args
|
60
|
+
|
61
|
+
System.cmd(
|
62
|
+
"mix",
|
63
|
+
args,
|
64
|
+
[
|
65
|
+
cd: dir,
|
66
|
+
env: %{
|
67
|
+
"MIX_EXS" => nil,
|
68
|
+
"MIX_LOCK" => nil,
|
69
|
+
"MIX_DEPS" => nil
|
70
|
+
}
|
71
|
+
]
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
DependencyHelper.main()
|
data/helpers/mix.exs
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
defmodule DependabotCore.Mixfile do
|
2
|
+
use Mix.Project
|
3
|
+
|
4
|
+
def project do
|
5
|
+
[app: :dependabot_core,
|
6
|
+
version: "0.1.0",
|
7
|
+
elixir: "~> 1.5",
|
8
|
+
start_permanent: Mix.env == :prod,
|
9
|
+
lockfile: System.get_env("MIX_LOCK") || "mix.lock",
|
10
|
+
deps_path: System.get_env("MIX_DEPS") || "deps",
|
11
|
+
deps: deps()]
|
12
|
+
end
|
13
|
+
|
14
|
+
def application do
|
15
|
+
[extra_applications: [:logger]]
|
16
|
+
end
|
17
|
+
|
18
|
+
defp deps() do
|
19
|
+
[{:jason, "~> 1.0-rc"}]
|
20
|
+
end
|
21
|
+
end
|
data/helpers/mix.lock
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# These all need to be required so the various classes can be registered in a
|
4
|
+
# lookup table of package manager names to concrete classes.
|
5
|
+
require "dependabot/hex/file_fetcher"
|
6
|
+
require "dependabot/hex/file_parser"
|
7
|
+
require "dependabot/hex/update_checker"
|
8
|
+
require "dependabot/hex/file_updater"
|
9
|
+
require "dependabot/hex/metadata_finder"
|
10
|
+
require "dependabot/hex/requirement"
|
11
|
+
require "dependabot/hex/version"
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/file_fetchers"
|
4
|
+
require "dependabot/file_fetchers/base"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Hex
|
8
|
+
class FileFetcher < Dependabot::FileFetchers::Base
|
9
|
+
APPS_PATH_REGEX = /apps_path:\s*"(?<path>.*?)"/m.freeze
|
10
|
+
STRING_ARG = %{(?:["'](.*?)["'])}
|
11
|
+
EVAL_FILE = /Code\.eval_file\(#{STRING_ARG}(?:\s*,\s*#{STRING_ARG})?\)/.
|
12
|
+
freeze
|
13
|
+
|
14
|
+
def self.required_files_in?(filenames)
|
15
|
+
filenames.include?("mix.exs")
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.required_files_message
|
19
|
+
"Repo must contain a mix.exs."
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def fetch_files
|
25
|
+
fetched_files = []
|
26
|
+
fetched_files << mixfile
|
27
|
+
fetched_files << lockfile if lockfile
|
28
|
+
fetched_files += subapp_mixfiles
|
29
|
+
fetched_files += evaled_files
|
30
|
+
fetched_files
|
31
|
+
end
|
32
|
+
|
33
|
+
def mixfile
|
34
|
+
@mixfile ||= fetch_file_from_host("mix.exs")
|
35
|
+
end
|
36
|
+
|
37
|
+
def lockfile
|
38
|
+
return @lockfile if @lockfile_lookup_attempted
|
39
|
+
|
40
|
+
@lockfile_lookup_attempted = true
|
41
|
+
@lockfile ||= fetch_file_from_host("mix.lock")
|
42
|
+
rescue Dependabot::DependencyFileNotFound
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def subapp_mixfiles
|
47
|
+
apps_path = mixfile.content.match(APPS_PATH_REGEX)&.
|
48
|
+
named_captures&.fetch("path")
|
49
|
+
return [] unless apps_path
|
50
|
+
|
51
|
+
app_directories = repo_contents(dir: apps_path).
|
52
|
+
select { |f| f.type == "dir" }
|
53
|
+
|
54
|
+
app_directories.map do |dir|
|
55
|
+
fetch_file_from_host("#{dir.path}/mix.exs")
|
56
|
+
rescue Dependabot::DependencyFileNotFound
|
57
|
+
# If the folder doesn't have a mix.exs it *might* be because it's
|
58
|
+
# not an app. Ignore the fact we couldn't fetch one and proceed with
|
59
|
+
# updating (it will blow up later if there are problems)
|
60
|
+
nil
|
61
|
+
end.compact
|
62
|
+
rescue Octokit::NotFound, Gitlab::Error::NotFound
|
63
|
+
# If the path specified in apps_path doesn't exist then it's not being
|
64
|
+
# used. We can just return an empty array of subapp files.
|
65
|
+
[]
|
66
|
+
end
|
67
|
+
|
68
|
+
def evaled_files
|
69
|
+
mixfile.content.scan(EVAL_FILE).map do |eval_file_args|
|
70
|
+
path = Pathname.new(File.join(*eval_file_args.reverse)).
|
71
|
+
cleanpath.to_path
|
72
|
+
fetch_file_from_host(path).tap { |f| f.support_file = true }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Dependabot::FileFetchers.register("hex", Dependabot::Hex::FileFetcher)
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/dependency"
|
4
|
+
require "dependabot/file_parsers"
|
5
|
+
require "dependabot/file_parsers/base"
|
6
|
+
require "dependabot/hex/native_helpers"
|
7
|
+
require "dependabot/shared_helpers"
|
8
|
+
require "dependabot/errors"
|
9
|
+
|
10
|
+
# For docs, see https://hexdocs.pm/mix/Mix.Tasks.Deps.html
|
11
|
+
module Dependabot
|
12
|
+
module Hex
|
13
|
+
class FileParser < Dependabot::FileParsers::Base
|
14
|
+
require "dependabot/file_parsers/base/dependency_set"
|
15
|
+
|
16
|
+
def parse
|
17
|
+
dependency_set = DependencySet.new
|
18
|
+
|
19
|
+
dependency_details.each do |dep|
|
20
|
+
git_dependency = dep["source"]&.fetch("type") == "git"
|
21
|
+
|
22
|
+
dependency_set <<
|
23
|
+
Dependency.new(
|
24
|
+
name: dep["name"],
|
25
|
+
version: git_dependency ? dep["checksum"] : dep["version"],
|
26
|
+
requirements: [{
|
27
|
+
requirement: dep["requirement"],
|
28
|
+
groups: dep["groups"],
|
29
|
+
source: dep["source"] && symbolize_keys(dep["source"]),
|
30
|
+
file: dep["from"]
|
31
|
+
}],
|
32
|
+
package_manager: "hex"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
dependency_set.dependencies.sort_by(&:name)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def dependency_details
|
42
|
+
SharedHelpers.in_a_temporary_directory do
|
43
|
+
write_sanitized_mixfiles
|
44
|
+
write_supporting_files
|
45
|
+
File.write("mix.lock", lockfile.content) if lockfile
|
46
|
+
FileUtils.cp(elixir_helper_parse_deps_path, "parse_deps.exs")
|
47
|
+
|
48
|
+
SharedHelpers.run_helper_subprocess(
|
49
|
+
env: mix_env,
|
50
|
+
command: "mix run #{elixir_helper_path}",
|
51
|
+
function: "parse",
|
52
|
+
args: [Dir.pwd],
|
53
|
+
popen_opts: { err: %i(child out) }
|
54
|
+
)
|
55
|
+
end
|
56
|
+
rescue Dependabot::SharedHelpers::HelperSubprocessFailed => error
|
57
|
+
result_json =
|
58
|
+
error.message.lines.
|
59
|
+
drop_while { |l| !l.start_with?('{"result":') }.
|
60
|
+
join
|
61
|
+
|
62
|
+
raise DependencyFileNotEvaluatable, error.message if result_json.empty?
|
63
|
+
|
64
|
+
JSON.parse(result_json).fetch("result")
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_sanitized_mixfiles
|
68
|
+
mixfiles.each do |file|
|
69
|
+
path = file.name
|
70
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
71
|
+
File.write(path, sanitize_mixfile(file.content))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def write_supporting_files
|
76
|
+
dependency_files.select(&:support_file).each do |file|
|
77
|
+
path = file.name
|
78
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
79
|
+
File.write(path, file.content)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def sanitize_mixfile(content)
|
84
|
+
content.
|
85
|
+
gsub(/File\.read!\(.*?\)/, '"0.0.1"').
|
86
|
+
gsub(/File\.read\(.*?\)/, '{:ok, "0.0.1"}')
|
87
|
+
end
|
88
|
+
|
89
|
+
def mix_env
|
90
|
+
{
|
91
|
+
"MIX_EXS" => File.join(NativeHelpers.hex_helpers_dir, "mix.exs"),
|
92
|
+
"MIX_LOCK" => File.join(NativeHelpers.hex_helpers_dir, "mix.lock"),
|
93
|
+
"MIX_DEPS" => File.join(NativeHelpers.hex_helpers_dir, "deps"),
|
94
|
+
"MIX_QUIET" => "1"
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
def elixir_helper_path
|
99
|
+
File.join(NativeHelpers.hex_helpers_dir, "lib/run.exs")
|
100
|
+
end
|
101
|
+
|
102
|
+
def elixir_helper_parse_deps_path
|
103
|
+
File.join(NativeHelpers.hex_helpers_dir, "lib/parse_deps.exs")
|
104
|
+
end
|
105
|
+
|
106
|
+
def check_required_files
|
107
|
+
raise "No mixfile!" if mixfiles.none?
|
108
|
+
end
|
109
|
+
|
110
|
+
def symbolize_keys(hash)
|
111
|
+
Hash[hash.keys.map { |k| [k.to_sym, hash[k]] }]
|
112
|
+
end
|
113
|
+
|
114
|
+
def mixfiles
|
115
|
+
dependency_files.select { |f| f.name.end_with?("mix.exs") }
|
116
|
+
end
|
117
|
+
|
118
|
+
def lockfile
|
119
|
+
@lockfile ||= get_original_file("mix.lock")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
Dependabot::FileParsers.register("hex", Dependabot::Hex::FileParser)
|