Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for npm 7 lockfiles #3011

Merged
merged 52 commits into from
Feb 8, 2021
Merged

Add support for npm 7 lockfiles #3011

merged 52 commits into from
Feb 8, 2021

Conversation

feelepxyz
Copy link
Contributor

@feelepxyz feelepxyz commented Jan 19, 2021

Add support for updating npm 7 lockfiles alongside npm 6 based on the lockfileVersion set in the package-lock.json file. If this is set to 2 we used npm 7 otherwise fallback to npm 6.

The native helpers to update dependencies shell out to npm install pkg-name for direct/top-level and npm update pkg-name for indirect/sub-dependencies.

We made an initial attempt to use arborist programatically to do the update but abandoned this approach as we ran into a bug where arborist depends on pacote which re-runs what it thinks is the main npm cli command on failures to retry, which in our setup lead to an infinite loop of retries. The bug was reproduced here: https://github.com/feelepxyz/npm7-hang-investigation and discussed with the npm team who advised us to no use arborist programatically in this way (yet).

Review checklist/notable changes

  • Rebase on top of Extract npm 7 project fixtures and update specs #3086
  • Review relevant files: https://github.com/dependabot/dependabot-core/pull/3011/files?file-filters%5B%5D=.js&file-filters%5B%5D=.rb&file-filters%5B%5D=dotfile
  • Changes to call out
    • npm_and_yarn/lib/dependabot/npm_and_yarn/dependency_files_filterer.rb
      • Changes required to support npm 7 workspaces
    • npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb
      • Handle new error output
        • npm 7 now exits with a full error log so we can't do starts_with? on anything meaningful
      • handle updating the root lockfile when using workspaces in top_level_dependency_update_not_required?
      • Handle locked git dependencies, when updating pkg-a we go through and find any top-level git dependencies and lock them to the current resolved sha in package.json and then re-set the lockfile after update
      • Handle npm 7 lockfile packages."".*dependencies entries in restore_locked_package_dependencies which should mirror what's in your package.json, we need to reset this after update otherwise we'll have both locked git deps and updated dependencies new requirements in there even though we didn't update the package.json
        • If your package.json says eslint: ^1.0.0 but we want to update to 1.1.1 without updating the package.json we run npm install eslint@1.1.0 which updates the packages."".*dependencies entry to 1.1.0 but we want it to match the package.json so reset it to ^1.0.0 after update
    • git_dependencies_to_lock git dependencies from syntax in lockfiles has changed to include the dependency name and also always seem to default to ssh:// protocol when not explicitly fetching from https://github.. for example
      • It seems like a new default as it changes when converting a npm6 lockfile to npm 7 with npm i --package-lock-only

Release plan

  • Review ⏫
  • Dry run existing npm 6 updates
  • Dry run npm 7 projects from around github, how to search for "\"lockfileVersion\": 2" with quotes?

@feelepxyz feelepxyz requested a review from a team as a code owner January 19, 2021 14:25
@feelepxyz feelepxyz marked this pull request as draft January 19, 2021 14:26
feelepxyz added a commit that referenced this pull request Jan 19, 2021
This adds a couple launch scripts to debug npm and yarn tests when
running then using jest.

The launch scripts complained about a missing config file so added this
and changed the default env from jsdom to node as this is what we use.

Changed `eslint-plugin-prettier` to `eslint-config-prettier` as this
seems like the recommended extension now when using prettier with
eslint: https://prettier.io/docs/en/integrating-with-linters.html

This extracts these changes from the npm 7 branch:
#3011
@jurre jurre force-pushed the feelepxyz/use-npm7 branch 2 times, most recently from 01ed498 to 144e747 Compare January 26, 2021 12:17
@feelepxyz feelepxyz mentioned this pull request Feb 3, 2021
@feelepxyz feelepxyz changed the title WIP: Add support for npm 7 lockfiles Add support for npm 7 lockfiles Feb 5, 2021
@feelepxyz feelepxyz marked this pull request as ready for review February 5, 2021 11:25
feelepxyz added a commit that referenced this pull request Feb 5, 2021
Extract all project based spec fixtures from npm7 pr to reduce diff:
#3011

The file updater specs changes are copied but excluding the npm 7
specifc specs.
@@ -222,7 +232,7 @@ def handle_npm_updater_error(error, lockfile)
# This happens if a new version has been published but npm is having
# consistency issues and the version isn't fully available on all
# queries
if error_message.start_with?("No matching vers") &&
if error_message.include?("No matching vers") &&
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we match on the full string here? No matching version

def post_process_npm_lockfile(original_content, updated_content)
updated_content =
replace_project_metadata(updated_content, original_content)
def post_process_npm_lockfile(original_content, updated_content, lockfile_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in pair-review: this might be a good candidate to extract out into its own class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed, there's a bunch of pre/post-processing that's intertwined and would probably be a lot easier to grok if it was in one place.


context "when a git src dependency doesn't have a valid package.json" do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was only removed from npm 7 as it takes absolutely ages and actually manages to do the update, the spec exists here for npm 6:

context "when a git src dependency doesn't have a valid package.json" do


context "with a corrupted npm lockfile (version missing)" do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer failing in npm 7, npm 6 spec is here

context "with a corrupted npm lockfile (version missing)" do


# NOTE: This no longer raises in npm 7
context "when there is a private git dep we don't have access to" do
Copy link
Contributor Author

@feelepxyz feelepxyz Feb 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer fails for npm 7, tests have moved here:

# NOTE: This no longer raises in npm 7
context "when there is a private git dep we don't have access to" do
let(:files) { project_dependency_files("npm6/github_dependency_private") }
let(:dependency_name) { "strict-uri-encode" }
let(:version) { "1.1.0" }
let(:requirements) { [] }
it "raises a helpful error" do
expect { updated_npm_lock_content }.
to raise_error(Dependabot::GitDependenciesNotReachable) do |error|
expect(error.dependency_urls).
to eq(
[
"https://github.com/hmarr/dependabot-test-private-npm-package.git/"
]
)
end
end
end
end
describe "npm 7 specific" do
# NOTE: This used to raise in npm 6
context "when there is a private git dep we don't have access to" do
let(:files) { project_dependency_files("npm7/github_dependency_private") }
let(:dependency_name) { "strict-uri-encode" }
let(:version) { "1.1.0" }
let(:requirements) { [] }
it "updates the dependency and leaves the private git dep alone" do
parsed_lockfile = JSON.parse(updated_npm_lock_content)
expect(parsed_lockfile.fetch("dependencies")["strict-uri-encode"]["version"]).
to eq("1.1.0")
expect(parsed_lockfile.fetch("dependencies")["bus-replacement-service"]["version"]).
to include("19c4dba3bfce7574e28f1df2138d47ab4cc665b3")
end
end
end

"--ignore-scripts",
"--package-lock-only"
].join(" ")
SharedHelpers.run_shell_command(command)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -198,7 +309,7 @@ def handle_npm_updater_error(error, lockfile)
# workspace project)
sub_dep_local_path_error = "does not contain a package.json file"
if error_message.match?(INVALID_PACKAGE) ||
error_message.start_with?("Invalid package name") ||
error_message.include?("Invalid package name") ||
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: npm 7 now exits with a full error log so we can't do starts_with? on anything meaningful

end
end

def run_npm_7_subdependency_updater(lockfile_name:)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned while pairing: might want to extract a separate Npm7LockfileUpdater in a follow-up PR.

# need to copy this from the manifest to the lockfile after the update
# has finished.
def restore_locked_package_dependencies(lockfile_name, lockfile_content)
npm_version = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We check for the npm_version 6 times in this file, I know we talked about extracting an npm 7 updater, but maybe we can start with:

def npm7?
  return @npm7 if defined?(@npm7)

  @npm7 = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content)
end

And using that everywhere? Might be a micro-optimization but I think it slightly clarifies the code and should be a pretty cheap change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this would be good but I think we need to refactor the lockfile updater a bit to get there as we currently pass around the lockfile name everywhere from here:

Initialising the class with a single lockfile to be updated would make the class a lot simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted it into a method but still passing around the content

Copy link
Member

@jurre jurre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep iterating on the code, but functionally I feel pretty confident about this

@feelepxyz
Copy link
Contributor Author

Investigating python failures on main, seems unrelated to any recent changes

@feelepxyz feelepxyz merged commit 19fef63 into main Feb 8, 2021
@feelepxyz feelepxyz deleted the feelepxyz/use-npm7 branch February 8, 2021 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants