diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb index ef9876444c..f0d8a69c10 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb @@ -56,7 +56,7 @@ def clone_repo_contents def ecosystem_versions package_managers = {} - package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock + package_managers["npm"] = npm_version if package_lock package_managers["yarn"] = yarn_version if yarn_version package_managers["pnpm"] = pnpm_version if pnpm_version package_managers["shrinkwrap"] = 1 if shrinkwrap @@ -165,10 +165,14 @@ def inferred_npmrc # rubocop:disable Metrics/PerceivedComplexity @inferred_npmrc = nil end + def npm_version + Helpers.npm_version_numeric(package_lock.content) + end + def yarn_version return @yarn_version if defined?(@yarn_version) - @yarn_version = package_manager.locked_version("yarn") || guess_yarn_version + @yarn_version = package_manager.requested_version("yarn") || guess_yarn_version end def guess_yarn_version @@ -180,13 +184,19 @@ def guess_yarn_version def pnpm_version return @pnpm_version if defined?(@pnpm_version) - @pnpm_version = package_manager.locked_version("pnpm") || guess_pnpm_version + version = package_manager.requested_version("pnpm") || guess_pnpm_version + + if version && Version.new(version.to_s) < Version.new("7") + raise ToolVersionNotSupported.new("PNPM", version.to_s, "7.*, 8.*") + end + + @pnpm_version = version end def guess_pnpm_version return unless pnpm_lock - Helpers.pnpm_major_version + Helpers.pnpm_version_numeric(pnpm_lock) end def package_manager diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb index 4ba9e09dae..7601501dee 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb @@ -25,6 +25,16 @@ def self.yarn_version_numeric(yarn_lock) end end + # Mapping from lockfile versions to PNPM versions is at + # https://github.com/pnpm/spec/tree/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile, but simplify it for now. + def self.pnpm_version_numeric(pnpm_lock) + if pnpm_lockfile_version(pnpm_lock).to_f >= 5.4 + 8 + else + 6 + end + end + def self.fetch_yarnrc_yml_value(key, default_value) if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml")) yarnrc.fetch(key, default_value) @@ -44,20 +54,11 @@ def self.yarn_major_version @yarn_major_version ||= fetch_yarn_major_version end - def self.pnpm_major_version - @pnpm_major_version ||= fetch_pnpm_major_version - end - def self.fetch_yarn_major_version output = SharedHelpers.run_shell_command("yarn --version") Version.new(output).major end - def self.fetch_pnpm_major_version - output = SharedHelpers.run_shell_command("pnpm --version") - Version.new(output).major - end - def self.yarn_zero_install? File.exist?(".pnp.cjs") end @@ -122,6 +123,10 @@ def self.run_yarn_command(command, fingerprint: nil) SharedHelpers.run_shell_command(command, fingerprint: fingerprint) end + def self.pnpm_lockfile_version(pnpm_lock) + pnpm_lock.content.match(/^lockfileVersion: '?(?[\d.]+)/)[:version] + end + def self.dependencies_with_all_versions_metadata(dependency_set) dependency_set.dependencies.map do |dependency| dependency.metadata[:all_versions] = dependency_set.all_versions_for_name(dependency.name) diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb index 2d3299a1b4..2a1af8dca0 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/package_manager.rb @@ -8,11 +8,11 @@ def initialize(package_json) @package_json = package_json end - def locked_version(name) - locked = @package_json.fetch("packageManager", nil) - return unless locked + def requested_version(name) + version = @package_json.fetch("packageManager", nil) + return unless version - version_match = locked.match(/#{name}@(?\d+.\d+.\d+)/) + version_match = version.match(/#{name}@(?\d+.\d+.\d+)/) version_match&.named_captures&.fetch("version", nil) end end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_fetcher_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_fetcher_spec.rb index aecdd7a019..9eb46a965d 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_fetcher_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_fetcher_spec.rb @@ -314,6 +314,89 @@ end end + context "with a pnpm-lock.yaml but no package-lock.json file" do + before do + stub_request(:get, url + "?ref=sha") + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "contents_js_pnpm.json"), + headers: json_header + ) + stub_request(:get, File.join(url, "package-lock.json?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return(status: 404) + end + + context "and older than 5.4 lockfile format" do + before do + stub_request(:get, File.join(url, "pnpm-lock.yaml?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "pnpm_lock_5.3_content.json"), + headers: json_header + ) + end + + it "fetches the package.json and pnpm-lock.yaml" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(package.json pnpm-lock.yaml)) + end + + it "raises tool version not supported error" do + expect { file_fetcher_instance.ecosystem_versions } + .to raise_error(Dependabot::ToolVersionNotSupported) + end + end + + context "and 5.4 as lockfile format" do + before do + stub_request(:get, File.join(url, "pnpm-lock.yaml?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "pnpm_lock_5.4_content.json"), + headers: json_header + ) + end + + it "fetches the package.json and pnpm-lock.yaml" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(package.json pnpm-lock.yaml)) + end + + it "parses the version as 8" do + expect(file_fetcher_instance.ecosystem_versions).to eq( + { package_managers: { "pnpm" => 8 } } + ) + end + end + + context "and 6.0 as lockfile format" do + before do + stub_request(:get, File.join(url, "pnpm-lock.yaml?ref=sha")) + .with(headers: { "Authorization" => "token token" }) + .to_return( + status: 200, + body: fixture("github", "pnpm_lock_6.0_content.json"), + headers: json_header + ) + end + + it "fetches the package.json and pnpm-lock.yaml" do + expect(file_fetcher_instance.files.map(&:name)) + .to match_array(%w(package.json pnpm-lock.yaml)) + end + + it "parses the version as 8" do + expect(file_fetcher_instance.ecosystem_versions).to eq( + { package_managers: { "pnpm" => 8 } } + ) + end + end + end + context "with an npm-shrinkwrap.json but no package-lock.json file" do before do stub_request(:get, url + "?ref=sha") diff --git a/npm_and_yarn/spec/fixtures/github/contents_js_pnpm.json b/npm_and_yarn/spec/fixtures/github/contents_js_pnpm.json new file mode 100644 index 0000000000..a54409ef9e --- /dev/null +++ b/npm_and_yarn/spec/fixtures/github/contents_js_pnpm.json @@ -0,0 +1,34 @@ +[ + { + "name": "package.json", + "path": "package.json", + "sha": "58166807d223462b6b44dd016e0b31edb390d3f4", + "size": 329, + "url": "https://api.github.com/repos/org/repo/contents/package.json?ref=main", + "html_url": "https://github.com/org/repo/blob/main/package.json", + "git_url": "https://api.github.com/repos/org/repo/git/blobs/58166807d223462b6b44dd016e0b31edb390d3f4", + "download_url": "https://raw.githubusercontent.com/org/repo/main/package.json?token=ABMwe0apDiKCctWHnEHnszRBAebVHjQnks5WJWD9wA%3D%3D", + "type": "file", + "_links": { + "self": "https://api.github.com/repos/org/repo/contents/package.json?ref=main", + "git": "https://api.github.com/repos/org/repo/git/blobs/58166807d223462b6b44dd016e0b31edb390d3f4", + "html": "https://github.com/org/repo/blob/main/package.json" + } + }, + { + "name": "pnpm-lock.yaml", + "path": "pnpm-lock.yaml", + "sha": "8aabbc0c1ba844e67a02bee52d62be0451dc9e9f", + "size": 368, + "url": "https://api.github.com/repos/org/repo/contents/pnpm-lock.yaml?ref=main", + "html_url": "https://github.com/org/repo/blob/main/pnpm-lock.yaml", + "git_url": "https://api.github.com/repos/org/repo/git/blobs/8aabbc0c1ba844e67a02bee52d62be0451dc9e9f", + "download_url": "https://raw.githubusercontent.com/org/repo/main/pnpm-lock.yaml?token=ABMwe0apDiKCctWHnEHnszRBAebVHjQnks5WJWD9wA%3D%3D", + "type": "file", + "_links": { + "self": "https://api.github.com/repos/org/repo/contents/pnpm-lock.yaml?ref=main", + "git": "https://api.github.com/repos/org/repo/git/blobs/8aabbc0c1ba844e67a02bee52d62be0451dc9e9f", + "html": "https://github.com/org/repo/blob/main/pnpm-lock.yaml" + } + } +] diff --git a/npm_and_yarn/spec/fixtures/github/pnpm_lock_5.3_content.json b/npm_and_yarn/spec/fixtures/github/pnpm_lock_5.3_content.json new file mode 100644 index 0000000000..f01f2b5410 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/github/pnpm_lock_5.3_content.json @@ -0,0 +1,18 @@ +{ + "name": "pnpm-lock.yaml", + "path": "pnpm-lock.yaml", + "sha": "6056dc6051aa30361e468044bdaf42ab15fddcf6", + "size": 299, + "url": "https://api.github.com/repos/repo/org/contents/pnpm-lock.yaml?ref=main", + "html_url": "https://github.com/repo/org/blob/main/pnpm-lock.yaml", + "git_url": "https://api.github.com/repos/repo/org/git/blobs/6056dc6051aa30361e468044bdaf42ab15fddcf6", + "download_url": "https://raw.githubusercontent.com/repo/org/main/pnpm-lock.yaml", + "type": "file", + "content": "bG9ja2ZpbGVWZXJzaW9uOiA1LjMKCnNwZWNpZmllcnM6CiAgZXNidWlsZDog\nMC4xMi45CgpkZXZEZXBlbmRlbmNpZXM6CiAgZXNidWlsZDogMC4xMi45Cgpw\nYWNrYWdlczoKCiAgL2VzYnVpbGQvMC4xMi45OgogICAgcmVzb2x1dGlvbjog\ne2ludGVncml0eTogc2hhNTEyLU1XUmhBYk1PSjlSSnlnQ3J0Nzc4cnovcU5Z\nZ0E0WlZqNmFYbk5QeEZqczdQbUlwYjBmdUI5R21nNXVXcnI2bisrWEt3d20v\nUm1TejZSUjVKTDJPY3N3PT19CiAgICBoYXNCaW46IHRydWUKICAgIHJlcXVp\ncmVzQnVpbGQ6IHRydWUKICAgIGRldjogdHJ1ZQo=\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/repo/org/contents/pnpm-lock.yaml?ref=main", + "git": "https://api.github.com/repos/repo/org/git/blobs/6056dc6051aa30361e468044bdaf42ab15fddcf6", + "html": "https://github.com/repo/org/blob/main/pnpm-lock.yaml" + } +} diff --git a/npm_and_yarn/spec/fixtures/github/pnpm_lock_5.4_content.json b/npm_and_yarn/spec/fixtures/github/pnpm_lock_5.4_content.json new file mode 100644 index 0000000000..fb237cfa9e --- /dev/null +++ b/npm_and_yarn/spec/fixtures/github/pnpm_lock_5.4_content.json @@ -0,0 +1,18 @@ +{ + "name": "pnpm-lock.yaml", + "path": "pnpm-lock.yaml", + "sha": "8aabbc0c1ba844e67a02bee52d62be0451dc9e9f", + "size": 368, + "url": "https://api.github.com/repos/org/repo/contents/pnpm-lock.yaml?ref=main", + "html_url": "https://github.com/org/repo/blob/main/pnpm-lock.yaml", + "git_url": "https://api.github.com/repos/org/repo/git/blobs/8aabbc0c1ba844e67a02bee52d62be0451dc9e9f", + "download_url": "https://raw.githubusercontent.com/org/repo/main/pnpm-lock.yaml", + "type": "file", + "content": "bG9ja2ZpbGVWZXJzaW9uOiA1LjQKCnNwZWNpZmllcnM6CiAgJ0BicmFpbnRy\nZWUvc2FuaXRpemUtdXJsJzogJzUuMCcKCmRlcGVuZGVuY2llczoKICAnQGJy\nYWludHJlZS9zYW5pdGl6ZS11cmwnOiA1LjAuMgoKcGFja2FnZXM6CgogIC9A\nYnJhaW50cmVlL3Nhbml0aXplLXVybC81LjAuMjoKICAgIHJlc29sdXRpb246\nIHtpbnRlZ3JpdHk6IHNoYTUxMi1OQkVKbEhXcmhRdWNMaFpHSHRTeE0ybG9T\nYU5VTWFqQzdLT1lKTHlmY2RXLzZnb1ZvZmYySG9ZSTNiejhZQ0ROMHdLR2J4\ndFVMMGd4MmR2SHB2bldsdz09fQogICAgZGVwcmVjYXRlZDogUG90ZW50aWFs\nIFhTUyB2dWxuZXJhYmlsaXR5IHBhdGNoZWQgaW4gdjYuMC4wLgogICAgZGV2\nOiBmYWxzZQo=\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/org/repo/contents/pnpm-lock.yaml?ref=main", + "git": "https://api.github.com/repos/org/repo/git/blobs/8aabbc0c1ba844e67a02bee52d62be0451dc9e9f", + "html": "https://github.com/org/repo/blob/main/pnpm-lock.yaml" + } +} diff --git a/npm_and_yarn/spec/fixtures/github/pnpm_lock_6.0_content.json b/npm_and_yarn/spec/fixtures/github/pnpm_lock_6.0_content.json new file mode 100644 index 0000000000..8fc6fd0869 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/github/pnpm_lock_6.0_content.json @@ -0,0 +1,18 @@ +{ + "name": "pnpm-lock.yaml", + "path": "pnpm-lock.yaml", + "sha": "480ff6af70fbd95d24ca75130d88d5f130411e32", + "size": 426, + "url": "https://api.github.com/repos/org/repo/contents/pnpm-lock.yaml?ref=main", + "html_url": "https://github.com/org/repo/blob/main/pnpm-lock.yaml", + "git_url": "https://api.github.com/repos/org/repo/git/blobs/480ff6af70fbd95d24ca75130d88d5f130411e32", + "download_url": "https://raw.githubusercontent.com/org/repo/main/pnpm-lock.yaml", + "type": "file", + "content": "bG9ja2ZpbGVWZXJzaW9uOiAnNi4wJwoKc2V0dGluZ3M6CiAgYXV0b0luc3Rh\nbGxQZWVyczogdHJ1ZQogIGV4Y2x1ZGVMaW5rc0Zyb21Mb2NrZmlsZTogZmFs\nc2UKCmRlcGVuZGVuY2llczoKICAnQGJyYWludHJlZS9zYW5pdGl6ZS11cmwn\nOgogICAgc3BlY2lmaWVyOiAnNS4wJwogICAgdmVyc2lvbjogNS4wLjIKCnBh\nY2thZ2VzOgoKICAvQGJyYWludHJlZS9zYW5pdGl6ZS11cmxANS4wLjI6CiAg\nICByZXNvbHV0aW9uOiB7aW50ZWdyaXR5OiBzaGE1MTItTkJFSmxIV3JoUXVj\nTGhaR0h0U3hNMmxvU2FOVU1hakM3S09ZSkx5ZmNkVy82Z29Wb2ZmMkhvWUkz\nYno4WUNETjB3S0dieHRVTDBneDJkdkhwdm5XbHc9PX0KICAgIGRlcHJlY2F0\nZWQ6IFBvdGVudGlhbCBYU1MgdnVsbmVyYWJpbGl0eSBwYXRjaGVkIGluIHY2\nLjAuMC4KICAgIGRldjogZmFsc2UK\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/org/repo/contents/pnpm-lock.yaml?ref=main", + "git": "https://api.github.com/repos/org/repo/git/blobs/480ff6af70fbd95d24ca75130d88d5f130411e32", + "html": "https://github.com/org/repo/blob/main/pnpm-lock.yaml" + } +}