Skip to content
This repository has been archived by the owner on Oct 9, 2023. It is now read-only.

Update recommendations for lockfiles #25

Merged
merged 28 commits into from Jul 13, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
166 changes: 129 additions & 37 deletions review/npm.md
Expand Up @@ -14,14 +14,17 @@ alternative.

- [npm Best Practices Guide](#npm-best-practices-guide)
* [TOC](#toc)
* [CI configuration](#ci-configuration)
* [Dependency management](#dependency-management)
+ [Intake](#intake)
+ [Declaration](#declaration)
+ [Project types](#project-types)
+ [Reproducible installation](#reproducible-installation)
- [Vendoring dependencies](#vendoring-dependencies)
- [Use a Lockfile](#use-a-lockfile)
* [Package-lock.json](#package-lockjson)
* [Shrinkwrap.json](#shrinkwrapjson)
* [npm-shrinkwrap.json](#shrinkwrapjson)
- [Lockfiles and commands](#lockfiles-and-commands)
+ [Maintenance](#maintenance)
* [Release](#release)
+ [Account](#account)
Expand All @@ -31,6 +34,18 @@ alternative.
+ [Scopes](#scopes)
+ [Private registry configurations](#private-registry-configurations)

## CI configuration

Follow the [principle of least privilege](https://www.cisa.gov/uscert/bsi/articles/knowledge/principles/least-privilege)
for your CI configuration.

If you run CI via GitHub Actions, a non-privileged environment is a workflow **without** access to
GitHub secrets and with non-write permissions defined, such as `permissions: read-all`, `permissions:`,
`contents: none`, `contents: read`. For more information about permissions, refer to the [official documentation](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token).

You may install the [OpenSSF Scorecard Action](https://github.com/ossf/scorecard-action)
to flag non-read permissions on your project.

## Dependency management

### Intake
Expand Down Expand Up @@ -94,6 +109,25 @@ commitish are allowed.
**Note**: The manifest file ***does not*** list transitive dependencies; it
lists only direct project dependencies.

### Project types

In the rest of this document, we will refer to three types of projects:

- **Libraries**: These are projects published on the npm registry and consumed
by other projects in the form of API calls. (Their manifest file
typically contains a `main`, `exports`, `browser`, `module`, and/or `types` entry).

- **Standalone CLIs**: These are projects published on the npm registry
and consumed by end-users in the form of locally installed programs that
are **always** run stand alone via `npx` or via a global install.
An example would be [clipboard-cli](https://github.com/sindresorhus/clipboard-cli).
(Their manifest file contains a `bin` entry).

- **Application projects**: These are projects that teams collaborate on in
development and deploy, such as web sites and/or web applications.
An example would be a React web app for a company's user-facing SaaS.
laurentsimon marked this conversation as resolved.
Show resolved Hide resolved
(Their manifest file typically contains `"private": true`).

### Reproducible installation

A reproducible installation is one that guarantees the exact same copy the
Expand Down Expand Up @@ -121,6 +155,11 @@ benefits, including:
proxy packages from public registries. Note: Versions are immutable in
principle, but immutability is enforced by the registry.

- Improve the accuracy of automated tools such as GitHub's security alerts.

- Let maintainers test updates before accepting them in the default branch,
e.g., via renovatebot's [stabilityDays](https://docs.renovatebot.com/configuration-options/#stabilitydays).

There are two ways to reliably achieve a reproducible installation: vendoring
dependencies and pinning by hash.

Expand Down Expand Up @@ -172,7 +211,7 @@ cryptographic hash of their content:
}
```

The package-lock.json file is a ***snapshot of an installation*** that allows
The `package-lock.json` file is a ***snapshot of an installation*** that allows
later reproduction of the same installation. As such, the lock file is
generated or updated via the various commands that install packages, e.g., `npm
install`. If some dependencies are missing or not pinned by hash (e.g.,
Expand All @@ -183,80 +222,133 @@ The lock file ***cannot*** be uploaded to a registry, which means that
consumers who locally install the package via `npm install` may [see different
dependency
versions](https://dev.to/saurabhdaware/but-what-the-hell-is-package-lock-json-b04)
than the repo owners used during testing. Using package-lock.json is akin to
than the repo owners used during testing. Using `package-lock.json` is akin to
dynamic linking for low-level programming languages: the loader will resolve
dependencies at load time using libraries available on the system. Using this
lock file leaves the task of deciding dependencies' versions to use to the
package consumer.

##### Shrinkwrap.json
##### npm-shrinkwrap.json

[Shrinkwrap.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json#package-lockjson-vs-npm-shrinkwrapjson)
[npm-shrinkwrap.json](https://docs.npmjs.com/cli/v7/configuring-npm/package-lock-json#package-lockjson-vs-npm-shrinkwrapjson)
is another lock file supported by npm. The main difference is that this lock
file ***may*** be uploaded to a registry along with the package. This ensures that
consumers of the dependency will obtain the same dependencies' hashes as the
repo owners intended. With shrinkwrap.json, the package producer takes
repo owners intended. With `npm-shrinkwrap.json`, the package producer takes
responsibility for updating the dependencies on behalf of their consumers. It's
akin to static linking for low-level programming languages: everything is
declared at packaging time.

To generate the shrinkwrap.json, an existing package-lock.json must be present
To generate the `npm-shrinkwrap.json`, an existing `package-lock.json` must be present
and the command [`npm
shrinkwrap`](https://docs.npmjs.com/cli/v8/commands/npm-shrinkwrap) must be
run.

#### Lockfiles and commands

Certain `npm` commmands treat the lockfiles as read-only, while others do not.

The following commands treat the lock file as read-only:

1. [`npm ci`](https://docs.npmjs.com/cli/v8/commands/npm-ci), used to
install a project and its dependencies,

1. [`npm install-ci-test`](https://docs.npmjs.com/cli/v8/commands/npm-install-ci-test),
used to install a project and run tests.

The following commands ***do not*** treat the lock file as read-only, may fetch / install
unpinned dependencies and update the lockfiles:

1. `npm install`, `npm i`, `npm install -g`

1. `npm update`

1. `npm install-test`

1. `npm pkg set` and `npm pkg delete`

1. `npm exec`, `npx`

1. `npm set-script`

**Recommendations:**

1. Developers should declare and commit a manifest file for ***all*** their
projects. Use the official [Creating a package.json
file](https://docs.npmjs.com/creating-a-package-json-file) documentation to
create the manifest file.
create the manifest file.

1. To add a dependency to a manifest file, ***locally*** run [`npm
install --save <dep-name>`](https://docs.npmjs.com/cli/v8/commands/npm-install).
install --save <dep-name>`](https://docs.npmjs.com/cli/v8/commands/npm-install)
and commit the updated manifest to the repository.

1. If you need to run a standalone CLI package from the registry, ensure the package is a part of
the dependencies defined in your project via the `package.json` file, prior to
being installed at build-time in your CI or otherwise automated environment.

1. Developers should declare and commit a lockfile for ***all*** their
projects. The reasoning is that this lockfile will provide the benefits of
[Reproducible installation](#reproducible-installation)
by default for privileged environments (project developers' machines,
CI, production or other environments with access to sensitive data,
such as secrets, PII, write/push permission to the repository, etc).

When running tests locally, developers should use commands that treat a lockfile
as read-only (see [Lockfiles and commands](#lockfiles-and-commands)), unless they
are intentionally adding / removing a dependency.

1. In automated environments (e.g., in CI or production) or when building
artifacts for end-users (e.g., container images, etc):
Below we explain the type of lockfile acceptable by project type.

1. Always generate a package-lock.json ***locally*** and commit it to the
repository.
1. If a project is a library:

1. Never run commands that may update the lock files or fetch unpinned
dependencies:
1. An `npm-shrinkwrap.json` ***should not*** be published.
The reasoning is that version resolution should be left to the package consumer.
Allow all versions from the minimum to the latest you support, e.g.,
`^m.n.o` to pin to a major range; `~m.n.o` to pin to a minor range. Avoid versions
with critical vulnerabilities as much as possible. Visit the [semver
calculator](https://semver.npmjs.com/) to help you define the ranges.

1. `npm install`, `npm i`, `npm install -g`
1. The lockfile `package-lock.json` ***should*** be ignored for tests running in CI
(e.g. via `npm install --no-package-lock`). The reasoning is that CI tests should
Copy link
Member

Choose a reason for hiding this comment

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

to be fair tho, this is also true for developers, despite the (relatively small) risk to their local machines.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't entirely disagree. I think developers will do in practice regardless of what we say (e.g. when troubleshooting/repdoducing a problem). I think the intention is to raise awareness and encourage users to run these non-locked tests in CI/less-privileged env when possible.

Let me know if you think we should change it or if this is acceptable as-is.

exercise a wide range of dependency versions in order to discover / fix problems
before the library users do, so tests need to pull the latest versions of packages.

1. `npm update`
1. Locally, developers should only run npm commands that treat the lockfile as
read-only (see [Lockfiles and commands](#lockfiles-and-commands)), except
when intentionally adding /removing a dependency.

1. `npm install-test`
1. Follow the principle of least privilege in your [CI configuration](#ci-configuration).
This is particularly important since the lockfile is ignored.

1. `npm pkg set` and `npm pkg delete`
1. If a project is a standalone CLI:

1. `npm exec`, `npx`
1. Developers may publish an `npm-shrinkwrap.json`.
Remember that, by declaring an `npm-shrinkwrap.json`, you take responsibility
for rapidly and consistently updating all the dependencies. Your users will not be able
to update or deduplicate them. If you expect your CLI to be used by other projects and defined
in their `package.json` or lockfile, do **not** use `npm-shrinkwrap.json` because it will
hinder dependency resolution for your consumers: follow the recommendations as if your project
was a library.

1. `npm set-script`
1. In CI, only run npm commands that treat the lockfile as
read-only (see [Lockfiles and commands](#lockfiles-and-commands)).

1. Only run commands that treat the lock file as read-only:
1. Locally, developers should only run npm commands that treat the lockfile as
read-only (see [Lockfiles and commands](#lockfiles-and-commands)), except
when intentionally adding /removing a dependency.

1. To install a project and its dependencies, use [`npm
ci`](https://docs.npmjs.com/cli/v8/commands/npm-ci).
1. Follow the principle of least privilege in your [CI configuration](#ci-configuration).

1. To run tests, run [`npm
install-ci-test`](https://docs.npmjs.com/cli/v8/commands/npm-install-ci-test).
1. If a project is an application:

1. If you need to run a CLI package from the registry, ensure the package is a part of
the dependencies defined in your project via the `package.json` file, prior to being installed at build-time in your CI or otherwise automated environment.
1. Developers should declare and commit a lockfile to their repository.

1. If a project is a CLI application (`main` entry in the manifest file),
developers may publish a shrinkwrap.json.
1. In CI, only run npm commands that treat the lockfile as
read-only (see [Lockfiles and commands](#lockfiles-and-commands)).

1. If a project is a library (`lib` entry in the manifest file), a
shrinkwrap.json should ***not*** be published. The reasoning is that version
resolution should be left to the package consumer. Allow all versions from
the minimum to the latest you support, e.g., `^m.n.o` to pin to a major
range; `~m.n.o` to pin to a minor range. Avoid versions with critical
vulnerabilities as much as possible. Visit the [semver
calculator](https://semver.npmjs.com/) to help you define the ranges.
1. Locally, developers should only run npm commands that treat the lockfile as
read-only (see [Lockfiles and commands](#lockfiles-and-commands)), except
when intentionally adding /removing a dependency.

### Maintenance

Expand Down