Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 314 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ semver = "1.0.18"
serde = { version = "1.0.188", features = ["derive"] }
serde_json = { version = "1.0.107", features = ["preserve_order"] }
serde_yaml = "0.9.25"
toml = { version = "1.1.4", default-features = false, features = ["parse", "serde"] }
ureq = { version = "3.4.0", default-features = false, features = ["rustls", "gzip"] }

[dev-dependencies]
debugless-unwrap = "0.0.4"
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sherif is an opinionated, zero-config linter for TypeScript & JavaScript monorep
- ✨ **PNPM, Bun, NPM, Yarn...**: sherif works with all package managers
- 🔎 **Zero-config**: it just works and prevents regressions
- ⚡ **Fast**: doesn't need `node_modules` installed, written in 🦀 Rust
- 📴 **Offline**: every rule works without network access, except the opt-in [`unnecessary-release-age-exclude`](#rules)

## Installation

Expand Down Expand Up @@ -191,6 +192,43 @@ The root `package.json` should be private to prevent accidentaly publishing it t

Private packages shouldn't have `@types/*` in `dependencies`, since they don't need it at runtime. Move them to `devDependencies`.

#### `unnecessary-release-age-exclude` ⚠️

Packages excluded from the minimum release age (the cooldown that protects against compromised releases) bypass it forever, even though the bypass is usually added to unblock a single urgent upgrade. This rule reports exclusions that don't unblock anything anymore:

- exclusions pinning versions that are past the minimum release age, e.g. `typescript@5.4.5` - those versions are already published, so they will never be gated again;
- exclusions without a version whose package hasn't published anything for at least ten times the minimum release age. A package that publishes often would be gated again as soon as it releases, so only dormant packages are reported.

The following configurations are read from the monorepo root:

| Package manager | File | Minimum release age | Exclusion list |
| --------------- | --------------------- | ---------------------------------- | --------------------------- |
| pnpm | `pnpm-workspace.yaml` | `minimumReleaseAge` (minutes) | `minimumReleaseAgeExclude` |
| Yarn | `.yarnrc.yml` | `npmMinimalAgeGate` (minutes) | `npmPreapprovedPackages` |
| Bun | `bunfig.toml` | `minimumReleaseAge` (seconds) | `minimumReleaseAgeExcludes` |

npm is not supported, as it doesn't have an exclusion list. pnpm 10.x, which configures the minimum release age in `.npmrc` instead of `pnpm-workspace.yaml`, is not supported either.

> [!IMPORTANT]
> **This is the only rule that requires network access, and the only one that is disabled by default.** Enable it with `--check-release-age`:
>
> ```bash
> sherif --check-release-age
> ```
>
> Deciding whether an exclusion is still useful requires knowing when versions were published, and that date is nowhere on disk: lockfiles (`pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, `bun.lock`) only contain resolved versions and integrity hashes, and package managers strip publish dates from their metadata cache. The rule therefore fetches the registry document of the excluded packages (this is also what package managers do to enforce the minimum release age in the first place).

Requests are only sent to the public npm registry (`https://registry.npmjs.org`), and only for the packages of the exclusion lists. Nothing is sent if:

- `--check-release-age` isn't passed;
- no configuration above defines a minimum release age with a non-empty exclusion list;
- the rule is ignored via `-r unnecessary-release-age-exclude`;
- a custom registry is configured in the root `.npmrc`, via `registry=` (no request at all) or `@scope:registry=` (no request for the packages of that scope).

The rule is best-effort: if the registry is unreachable - offline machine, sandboxed CI, network error - it reports nothing instead of failing, and doesn't change the exit code.

Exclusions using a pattern (e.g. `@myorg/*`) or a version range (e.g. `nx@^21.0.0`) are skipped, as they can match versions that aren't published yet.

#### `unordered-dependencies` ❌

Dependencies should be ordered alphabetically to prevent complex diffs when installing a new dependency via a package manager.
Expand All @@ -206,6 +244,7 @@ When using many CLI arguments, it might be easier to move to the configuration f
"select": "highest", // "highest" | "lowest"
"noInstall": false,
"failOnWarnings": false,
"checkReleaseAge": false,
"ignoreDependency": [], // string[]
"ignorePackage": [], // string[]
"ignoreRule": [] // string[]
Expand Down
3 changes: 3 additions & 0 deletions fixtures/release-age-bun-yarn/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
npmMinimalAgeGate: 1440
npmPreapprovedPackages:
- '@myorg/tools'
3 changes: 3 additions & 0 deletions fixtures/release-age-bun-yarn/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[install]
minimumReleaseAge = 86400
minimumReleaseAgeExcludes = ["typescript"]
4 changes: 4 additions & 0 deletions fixtures/release-age-bun-yarn/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "release-age-bun-yarn",
"workspaces": ["packages/*"]
}
1 change: 1 addition & 0 deletions fixtures/release-age-custom-registry/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
registry=https://registry.mycompany.com/
3 changes: 3 additions & 0 deletions fixtures/release-age-custom-registry/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "release-age-custom-registry"
}
2 changes: 2 additions & 0 deletions fixtures/release-age-custom-scope/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@myorg:registry=https://npm.pkg.github.com/
registry=https://registry.npmjs.org/
3 changes: 3 additions & 0 deletions fixtures/release-age-custom-scope/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "release-age-custom-scope"
}
3 changes: 3 additions & 0 deletions fixtures/release-age-no-exclude/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "release-age-no-exclude"
}
3 changes: 3 additions & 0 deletions fixtures/release-age-no-exclude/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
packages:
- 'packages/*'
minimumReleaseAge: 1440
3 changes: 3 additions & 0 deletions fixtures/release-age-pnpm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "release-age-pnpm"
}
6 changes: 6 additions & 0 deletions fixtures/release-age-pnpm/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
packages:
- 'packages/*'
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- 'nx@21.6.5'
- '@myorg/*'
5 changes: 5 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ pub struct Args {
#[arg(long)]
pub fail_on_warnings: bool,

/// Enable the `unnecessary-release-age-exclude` rule, which queries the npm registry.
#[arg(long)]
pub check_release_age: bool,

/// Ignore the `multiple-dependency-versions` rule for the given dependency name and/or version.
#[arg(long, short)]
pub ignore_dependency: Vec<String>,
Expand All @@ -64,6 +68,7 @@ impl From<Args> for Config {
select: args.select,
no_install: args.no_install,
fail_on_warnings: args.fail_on_warnings,
check_release_age: args.check_release_age,
ignore_dependency: args.ignore_dependency,
ignore_package: args.ignore_package,
ignore_rule: args.ignore_rule,
Expand Down
Loading