Skip to content

fix: preserve user-defined npm_config_* env vars in lifecycle scripts#12400

Merged
zkochan merged 3 commits into
pnpm:mainfrom
felipecrs:preserve-user-defined-npm-config-vars
Jun 14, 2026
Merged

fix: preserve user-defined npm_config_* env vars in lifecycle scripts#12400
zkochan merged 3 commits into
pnpm:mainfrom
felipecrs:preserve-user-defined-npm-config-vars

Conversation

@felipecrs

@felipecrs felipecrs commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

The actual fix is pnpm/npm-lifecycle#59. This PR only bumps @pnpm/npm-lifecycle to the version that contains the fix (WIP) and a adds a test to ensure this issue doesn't get reintroduced yet again.

Closes #12399

Summary by CodeRabbit

Bug Fixes

  • Fixed lifecycle script environment handling so user-defined npm_config_* values are preserved during execution, preventing accidental removal of settings like npm_config_platform_arch. Sensitive authentication-related npm_config_*/pnpm_config_* credentials continue to be filtered out as before.

Tests

  • Updated and expanded lifecycle and environment inheritance test coverage to verify preserved user config values and correctly stripped auth config values across platforms and scenarios.

Copilot AI review requested due to automatic review settings June 14, 2026 05:02
@felipecrs felipecrs requested a review from zkochan as a code owner June 14, 2026 05:02
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Action required

1. Mutable dependency reference 🐞 Bug ☼ Reliability
Description
The workspace catalog pins @pnpm/npm-lifecycle to a GitHub PR head ref (refs/pull/59/head), which is
mutable; future installs/lockfile regenerations can silently resolve to different code than what was
reviewed. This is especially risky because @pnpm/exec.lifecycle depends on @pnpm/npm-lifecycle via
"catalog:", so the catalog directly controls production code used by lifecycle execution.
Code

pnpm-workspace.yaml[94]

+  '@pnpm/npm-lifecycle': https://github.com/pnpm/npm-lifecycle#refs/pull/59/head
Evidence
The catalog entry is a mutable PR head, and @pnpm/exec.lifecycle sources @pnpm/npm-lifecycle from
the catalog, so resolution drift can change runtime behavior. The lockfile also records the PR-head
specifier, reinforcing that future updates may re-resolve from the moving ref.

pnpm-workspace.yaml[93-95]
pnpm-lock.yaml[354-358]
exec/lifecycle/package.json[36-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm-workspace.yaml` points `@pnpm/npm-lifecycle` at `https://github.com/pnpm/npm-lifecycle#refs/pull/59/head`, which can change over time. Even though the current `pnpm-lock.yaml` resolves to a specific commit tarball, any future resolution (e.g. lockfile refresh/update) may pull different code without an explicit version change.
### Issue Context
`@pnpm/exec.lifecycle` consumes `@pnpm/npm-lifecycle` via `catalog:`, so this affects lifecycle script execution behavior.
### Fix Focus Areas
- pnpm-workspace.yaml[93-95]
- pnpm-lock.yaml[354-358]
### Suggested fix
- Replace the PR head ref with an immutable identifier:
- Prefer a published version (once available), OR
- Pin to a commit SHA/ref (e.g. `https://github.com/pnpm/npm-lifecycle#<commitSha>`), then regenerate the lockfile so specifier+resolution are consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Flaky npm_config env test 🐞 Bug ☼ Reliability ⭐ New
Description
inspect-npm-config-env/postinstall.js prints every npm_config_* variable from the parent
process, but the Jest test only sets npm_config_platform_arch and asserts the output is exactly
one line. If the developer/CI environment already has other user-defined npm_config_* variables
(e.g. npm_config_registry), this test can fail even though the implementation is correct.
Code

exec/lifecycle/test/index.ts[R65-87]

+test('runLifecycleHook() does not set npm_config env vars but preserves user-defined ones', async () => {
+  const pkgRoot = f.find('inspect-npm-config-env')
  await using server = await createTestIpcServer(path.join(pkgRoot, 'test.sock'))
  const { default: pkg } = await import(path.join(pkgRoot, 'package.json'))
-  await runLifecycleHook('postinstall', pkg, {
-    depPath: '/inspect-frozen-lockfile/1.0.0',
-    pkgRoot,
-    rootModulesDir,
-    unsafePerm: true,
-  })
-
-  expect(server.getLines()).toStrictEqual(['unset'])
+  const prevPlatformArch = process.env.npm_config_platform_arch
+  process.env.npm_config_platform_arch = 'x64'
+  try {
+    await runLifecycleHook('postinstall', pkg, {
+      depPath: '/inspect-npm-config-env/1.0.0',
+      pkgRoot,
+      rootModulesDir,
+      unsafePerm: true,
+    })
+  } finally {
+    if (prevPlatformArch === undefined) {
+      delete process.env.npm_config_platform_arch
+    } else {
+      process.env.npm_config_platform_arch = prevPlatformArch
+    }
+  }
+
+  expect(server.getLines()).toStrictEqual(['npm_config_platform_arch=x64'])
})
Evidence
The fixture prints all npm_config_* variables (except npm_config_node_gyp), while the test only
sets/restores npm_config_platform_arch and then asserts the output equals a single-line array; any
pre-existing npm_config_* vars will add extra lines and break the strict equality assertion.

exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js[1-5]
exec/lifecycle/test/index.ts[65-87]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test `runLifecycleHook() does not set npm_config env vars but preserves user-defined ones` expects only `npm_config_platform_arch=x64` to be observed in the child, but the fixture emits *all* `npm_config_*` vars present in `process.env`. This makes the test non-deterministic when the parent environment already contains other user-defined `npm_config_*` variables.

## Issue Context
The fixture enumerates `process.env` and writes every `npm_config_*` key (excluding `npm_config_node_gyp`). The test currently only snapshots/restores `npm_config_platform_arch`, and does not sanitize other existing `npm_config_*` keys.

## Fix Focus Areas
- exec/lifecycle/test/index.ts[65-87]
- exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js[1-5]

## Suggested fix
In `exec/lifecycle/test/index.ts`, snapshot *all* existing `npm_config_*` variables (at least those the fixture would print), delete them before running `runLifecycleHook`, then set `npm_config_platform_arch='x64'`. In the `finally` block, restore the full snapshot (including removal of any vars that were originally absent).

(Alternative) Narrow the fixture to only print `npm_config_platform_arch` so the assertion cannot be affected by unrelated environment variables.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Windows env key collisions 🐞 Bug ≡ Correctness
Description
On Windows, filter_parent_env() no longer strips most non-auth npm_* variables, but
build_env() later inserts several npm_* lifecycle stamps (e.g. npm_lifecycle_event,
npm_execpath, npm_lifecycle_script). If the parent environment contains a differently-cased
variant (e.g. NPM_LIFECYCLE_EVENT), the child may receive duplicates with an unspecified winner
and observe incorrect lifecycle values.
Code

pacquet/crates/executor/src/make_env.rs[R163-179]

fn is_stamping_key(key: &str, is_windows: bool) -> bool {
+    if strip_env_prefix(key, "npm_package_", is_windows).is_some() {
+        return true;
+    }
+    if let Some(rest) = strip_env_prefix(key, "npm_config_", is_windows)
+        .or_else(|| strip_env_prefix(key, "pnpm_config_", is_windows))
+        && (rest.starts_with(['_', '/', '@']) || rest.contains(":_"))
+    {
+        return true;
+    }
   if is_windows {
-        // Byte-level prefix check: `key[..4]` would panic if byte 4
-        // lands inside a multi-byte UTF-8 codepoint (e.g. a key
-        // containing `𐀀` whose first byte sits at index 0). Since
-        // the prefix we want is pure ASCII, comparing bytes
-        // sidesteps the UTF-8 boundary question entirely.
-        if key.as_bytes().get(..4).is_some_and(|b| b.eq_ignore_ascii_case(b"npm_")) {
-            return true;
-        }
       return ["NODE", "TMPDIR", "INIT_CWD", "PNPM_SCRIPT_SRC_DIR"]
           .iter()
           .any(|name| key.eq_ignore_ascii_case(name));
   }
-    if key.starts_with("npm_") {
-        return true;
-    }
   matches!(key, "NODE" | "TMPDIR" | "INIT_CWD" | "PNPM_SCRIPT_SRC_DIR")
}
Evidence
build_env() unconditionally inserts lifecycle-related npm_* keys into the env map, while
is_stamping_key() on Windows does not filter those families; on Windows, case-insensitive env-key
collapsing is explicitly called out as a hazard elsewhere in the same crate (for PATH),
demonstrating the same risk applies here.

pacquet/crates/executor/src/make_env.rs[52-129]
pacquet/crates/executor/src/make_env.rs[131-179]
pacquet/crates/executor/src/lifecycle.rs[327-335]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_parent_env()` uses `is_stamping_key()` to remove parent env entries that would conflict with values later stamped by `build_env()`. After this PR, `is_stamping_key()` no longer strips lifecycle-related `npm_*` keys (only `npm_package_*`, auth-ish `(npm|pnpm)_config_*`, and a small fixed set of per-call stamps).
On Windows, env keys are case-insensitive at spawn time. If the parent env contains `NPM_LIFECYCLE_EVENT` (or other stamped keys) and `build_env()` inserts `npm_lifecycle_event`, the resulting `HashMap` can contain both keys (Rust key comparison is case-sensitive), and `Command::envs` will collapse them with an unspecified winner.
## Issue Context
`pacquet/crates/executor/src/lifecycle.rs` already has a special-case fix for this exact Windows hazard for `PATH`/`Path`, but the same risk exists for stamped lifecycle keys like `npm_lifecycle_event`, `npm_lifecycle_script`, `npm_execpath`, etc.
## Fix Focus Areas
- pacquet/crates/executor/src/make_env.rs[52-129]
- pacquet/crates/executor/src/make_env.rs[131-201]
- pacquet/crates/executor/src/lifecycle.rs[327-350]
## Suggested fix approach
1. Extend `is_stamping_key()` (Windows branch) to also strip any key that equals (case-insensitively) *all keys that `build_env()` may insert*, such as:
 - `npm_lifecycle_event`
 - `npm_lifecycle_script`
 - `npm_node_execpath`
 - `npm_execpath`
 - `npm_package_json`
 - `npm_config_node_gyp`
 - `npm_config_user_agent`
 - (keep existing `NODE`, `TMPDIR`, `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`)
 This avoids creating case-only duplicates when stamping.
2. (Alternative) Add a second retain pass in `run_lifecycle_hook()` similar to the existing PATH handling, removing case-insensitively any preexisting keys from the same stamped set before calling `.envs(&child_env)`.
Either approach should ensure that, on Windows, there is at most one logical value per stamped env var name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Env var not restored 🐞 Bug ☼ Reliability
Description
The new test sets process.env.npm_config_platform_arch and always deletes it in finally, which will
clobber any pre-existing value from the developer/CI environment. Because Jest runs multiple tests
in the same process for this test file, this can cause order-dependent or environment-dependent test
failures.
Code

exec/lifecycle/test/index.ts[R83-93]

+  process.env.npm_config_platform_arch = 'x64'
+  try {
+    await runLifecycleHook('postinstall', pkg, {
+      depPath: '/inspect-user-npm-config/1.0.0',
+      pkgRoot,
+      rootModulesDir,
+      unsafePerm: true,
+    })
+  } finally {
+    delete process.env.npm_config_platform_arch
+  }
Evidence
The added test explicitly mutates and deletes a global env var; Jest is used to run this test file,
so that mutation persists within the process unless properly restored.

exec/lifecycle/test/index.ts[79-96]
exec/lifecycle/package.json[28-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test mutates `process.env.npm_config_platform_arch` and then unconditionally deletes it. If it was already set, the original value is lost for subsequent tests in the same Jest process.
### Issue Context
This test suite is executed by Jest (single process per test file by default). Global env mutations should be restored to avoid cross-test interference.
### Fix Focus Areas
- exec/lifecycle/test/index.ts[79-96]
### Suggested fix
Wrap the mutation with save/restore semantics:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit b65f393

Results up to commit 5b885fc


🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Mutable dependency reference 🐞 Bug ☼ Reliability
Description
The workspace catalog pins @pnpm/npm-lifecycle to a GitHub PR head ref (refs/pull/59/head), which is
mutable; future installs/lockfile regenerations can silently resolve to different code than what was
reviewed. This is especially risky because @pnpm/exec.lifecycle depends on @pnpm/npm-lifecycle via
"catalog:", so the catalog directly controls production code used by lifecycle execution.
Code

pnpm-workspace.yaml[94]

+  '@pnpm/npm-lifecycle': https://github.com/pnpm/npm-lifecycle#refs/pull/59/head
Evidence
The catalog entry is a mutable PR head, and @pnpm/exec.lifecycle sources @pnpm/npm-lifecycle from
the catalog, so resolution drift can change runtime behavior. The lockfile also records the PR-head
specifier, reinforcing that future updates may re-resolve from the moving ref.

pnpm-workspace.yaml[93-95]
pnpm-lock.yaml[354-358]
exec/lifecycle/package.json[36-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpm-workspace.yaml` points `@pnpm/npm-lifecycle` at `https://github.com/pnpm/npm-lifecycle#refs/pull/59/head`, which can change over time. Even though the current `pnpm-lock.yaml` resolves to a specific commit tarball, any future resolution (e.g. lockfile refresh/update) may pull different code without an explicit version change.
### Issue Context
`@pnpm/exec.lifecycle` consumes `@pnpm/npm-lifecycle` via `catalog:`, so this affects lifecycle script execution behavior.
### Fix Focus Areas
- pnpm-workspace.yaml[93-95]
- pnpm-lock.yaml[354-358]
### Suggested fix
- Replace the PR head ref with an immutable identifier:
- Prefer a published version (once available), OR
- Pin to a commit SHA/ref (e.g. `https://github.com/pnpm/npm-lifecycle#<commitSha>`), then regenerate the lockfile so specifier+resolution are consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Windows env key collisions 🐞 Bug ≡ Correctness ⭐ New
Description
On Windows, filter_parent_env() no longer strips most non-auth npm_* variables, but
build_env() later inserts several npm_* lifecycle stamps (e.g. npm_lifecycle_event,
npm_execpath, npm_lifecycle_script). If the parent environment contains a differently-cased
variant (e.g. NPM_LIFECYCLE_EVENT), the child may receive duplicates with an unspecified winner
and observe incorrect lifecycle values.
Code

pacquet/crates/executor/src/make_env.rs[R163-179]

fn is_stamping_key(key: &str, is_windows: bool) -> bool {
+    if strip_env_prefix(key, "npm_package_", is_windows).is_some() {
+        return true;
+    }
+    if let Some(rest) = strip_env_prefix(key, "npm_config_", is_windows)
+        .or_else(|| strip_env_prefix(key, "pnpm_config_", is_windows))
+        && (rest.starts_with(['_', '/', '@']) || rest.contains(":_"))
+    {
+        return true;
+    }
    if is_windows {
-        // Byte-level prefix check: `key[..4]` would panic if byte 4
-        // lands inside a multi-byte UTF-8 codepoint (e.g. a key
-        // containing `𐀀` whose first byte sits at index 0). Since
-        // the prefix we want is pure ASCII, comparing bytes
-        // sidesteps the UTF-8 boundary question entirely.
-        if key.as_bytes().get(..4).is_some_and(|b| b.eq_ignore_ascii_case(b"npm_")) {
-            return true;
-        }
        return ["NODE", "TMPDIR", "INIT_CWD", "PNPM_SCRIPT_SRC_DIR"]
            .iter()
            .any(|name| key.eq_ignore_ascii_case(name));
    }
-    if key.starts_with("npm_") {
-        return true;
-    }
    matches!(key, "NODE" | "TMPDIR" | "INIT_CWD" | "PNPM_SCRIPT_SRC_DIR")
}
Evidence
build_env() unconditionally inserts lifecycle-related npm_* keys into the env map, while
is_stamping_key() on Windows does not filter those families; on Windows, case-insensitive env-key
collapsing is explicitly called out as a hazard elsewhere in the same crate (for PATH),
demonstrating the same risk applies here.

pacquet/crates/executor/src/make_env.rs[52-129]
pacquet/crates/executor/src/make_env.rs[131-179]
pacquet/crates/executor/src/lifecycle.rs[327-335]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`filter_parent_env()` uses `is_stamping_key()` to remove parent env entries that would conflict with values later stamped by `build_env()`. After this PR, `is_stamping_key()` no longer strips lifecycle-related `npm_*` keys (only `npm_package_*`, auth-ish `(npm|pnpm)_config_*`, and a small fixed set of per-call stamps).

On Windows, env keys are case-insensitive at spawn time. If the parent env contains `NPM_LIFECYCLE_EVENT` (or other stamped keys) and `build_env()` inserts `npm_lifecycle_event`, the resulting `HashMap` can contain both keys (Rust key comparison is case-sensitive), and `Command::envs` will collapse them with an unspecified winner.

## Issue Context
`pacquet/crates/executor/src/lifecycle.rs` already has a special-case fix for this exact Windows hazard for `PATH`/`Path`, but the same risk exists for stamped lifecycle keys like `npm_lifecycle_event`, `npm_lifecycle_script`, `npm_execpath`, etc.

## Fix Focus Areas
- pacquet/crates/executor/src/make_env.rs[52-129]
- pacquet/crates/executor/src/make_env.rs[131-201]
- pacquet/crates/executor/src/lifecycle.rs[327-350]

## Suggested fix approach
1. Extend `is_stamping_key()` (Windows branch) to also strip any key that equals (case-insensitively) *all keys that `build_env()` may insert*, such as:
  - `npm_lifecycle_event`
  - `npm_lifecycle_script`
  - `npm_node_execpath`
  - `npm_execpath`
  - `npm_package_json`
  - `npm_config_node_gyp`
  - `npm_config_user_agent`
  - (keep existing `NODE`, `TMPDIR`, `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`)

  This avoids creating case-only duplicates when stamping.

2. (Alternative) Add a second retain pass in `run_lifecycle_hook()` similar to the existing PATH handling, removing case-insensitively any preexisting keys from the same stamped set before calling `.envs(&child_env)`.

Either approach should ensure that, on Windows, there is at most one logical value per stamped env var name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Env var not restored 🐞 Bug ☼ Reliability
Description
The new test sets process.env.npm_config_platform_arch and always deletes it in finally, which will
clobber any pre-existing value from the developer/CI environment. Because Jest runs multiple tests
in the same process for this test file, this can cause order-dependent or environment-dependent test
failures.
Code

exec/lifecycle/test/index.ts[R83-93]

+  process.env.npm_config_platform_arch = 'x64'
+  try {
+    await runLifecycleHook('postinstall', pkg, {
+      depPath: '/inspect-user-npm-config/1.0.0',
+      pkgRoot,
+      rootModulesDir,
+      unsafePerm: true,
+    })
+  } finally {
+    delete process.env.npm_config_platform_arch
+  }
Evidence
The added test explicitly mutates and deletes a global env var; Jest is used to run this test file,
so that mutation persists within the process unless properly restored.

exec/lifecycle/test/index.ts[79-96]
exec/lifecycle/package.json[28-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test mutates `process.env.npm_config_platform_arch` and then unconditionally deletes it. If it was already set, the original value is lost for subsequent tests in the same Jest process.
### Issue Context
This test suite is executed by Jest (single process per test file by default). Global env mutations should be restored to avoid cross-test interference.
### Fix Focus Areas
- exec/lifecycle/test/index.ts[79-96]
### Suggested fix
Wrap the mutation with save/restore semantics:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 71e46de


🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Action required
1. Mutable dependency reference 🐞 Bug ☼ Reliability
Description
The workspace catalog pins @pnpm/npm-lifecycle to a GitHub PR head ref (refs/pull/59/head), which is
mutable; future installs/lockfile regenerations can silently resolve to different code than what was
reviewed. This is especially risky because @pnpm/exec.lifecycle depends on @pnpm/npm-lifecycle via
"catalog:", so the catalog directly controls production code used by lifecycle execution.
Code

pnpm-workspace.yaml[94]

+  '@pnpm/npm-lifecycle': https://github.com/pnpm/npm-lifecycle#refs/pull/59/head
Evidence
The catalog entry is a mutable PR head, and @pnpm/exec.lifecycle sources @pnpm/npm-lifecycle from
the catalog, so resolution drift can change runtime behavior. The lockfile also records the PR-head
specifier, reinforcing that future updates may re-resolve from the moving ref.

pnpm-workspace.yaml[93-95]
pnpm-lock.yaml[354-358]
exec/lifecycle/package.json[36-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`pnpm-workspace.yaml` points `@pnpm/npm-lifecycle` at `https://github.com/pnpm/npm-lifecycle#refs/pull/59/head`, which can change over time. Even though the current `pnpm-lock.yaml` resolves to a specific commit tarball, any future resolution (e.g. lockfile refresh/update) may pull different code without an explicit version change.

### Issue Context
`@pnpm/exec.lifecycle` consumes `@pnpm/npm-lifecycle` via `catalog:`, so this affects lifecycle script execution behavior.

### Fix Focus Areas
- pnpm-workspace.yaml[93-95]
- pnpm-lock.yaml[354-358]

### Suggested fix
- Replace the PR head ref with an immutable identifier:
 - Prefer a published version (once available), OR
 - Pin to a commit SHA/ref (e.g. `https://github.com/pnpm/npm-lifecycle#<commitSha>`), then regenerate the lockfile so specifier+resolution are consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Env var not restored 🐞 Bug ☼ Reliability
Description
The new test sets process.env.npm_config_platform_arch and always deletes it in finally, which will
clobber any pre-existing value from the developer/CI environment. Because Jest runs multiple tests
in the same process for this test file, this can cause order-dependent or environment-dependent test
failures.
Code

exec/lifecycle/test/index.ts[R83-93]

+  process.env.npm_config_platform_arch = 'x64'
+  try {
+    await runLifecycleHook('postinstall', pkg, {
+      depPath: '/inspect-user-npm-config/1.0.0',
+      pkgRoot,
+      rootModulesDir,
+      unsafePerm: true,
+    })
+  } finally {
+    delete process.env.npm_config_platform_arch
+  }
Evidence
The added test explicitly mutates and deletes a global env var; Jest is used to run this test file,
so that mutation persists within the process unless properly restored.

exec/lifecycle/test/index.ts[79-96]
exec/lifecycle/package.json[28-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The test mutates `process.env.npm_config_platform_arch` and then unconditionally deletes it. If it was already set, the original value is lost for subsequent tests in the same Jest process.

### Issue Context
This test suite is executed by Jest (single process per test file by default). Global env mutations should be restored to avoid cross-test interference.

### Fix Focus Areas
- exec/lifecycle/test/index.ts[79-96]

### Suggested fix
Wrap the mutation with save/restore semantics:
```ts
const prev = process.env.npm_config_platform_arch
process.env.npm_config_platform_arch = 'x64'
try {
 ...
} finally {
 if (prev === undefined) delete process.env.npm_config_platform_arch
 else process.env.npm_config_platform_arch = prev
}
```
This keeps the test isolated regardless of the runner’s environment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Qodo Logo

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b02ae74c-fd75-40a1-941a-b43e67ca42f5

📥 Commits

Reviewing files that changed from the base of the PR and between 5b885fc and b65f393.

📒 Files selected for processing (1)
  • pacquet/crates/executor/src/lifecycle/tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • pacquet/crates/executor/src/lifecycle/tests.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Clippy
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Dylint
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Doc
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint

📝 Walkthrough

Walkthrough

The parent-environment filtering in the Rust executor (make_env.rs) is reworked so that only npm_package_* stamps, auth-related (npm|pnpm)_config_* credential keys, and per-call re-derived keys are removed; user-defined npm_config_* values are now preserved. The JS lifecycle test fixture is renamed and updated to report surviving npm_config_* keys. Tests in both languages are updated to assert the new behavior. The @pnpm/npm-lifecycle catalog entry is widened and a changeset entry is added.

Changes

Preserve user npm_config_* env vars in lifecycle scripts

Layer / File(s) Summary
Rust make_env filtering logic
pacquet/crates/executor/src/make_env.rs
is_stamping_key now removes only npm_package_* stamps, auth-related (npm|pnpm)_config_* credential keys, and per-call re-derived keys (NODE, TMPDIR, INIT_CWD, PNPM_SCRIPT_SRC_DIR). A new strip_env_prefix helper handles case-insensitive Windows matching. filter_parent_env and doc comments are updated to match the refined rules.
Rust make_env unit tests
pacquet/crates/executor/src/make_env/tests.rs
Tests verify npm_config_platform_arch is preserved, a comprehensive set of auth-related config keys is stripped, HOME/PNPM_HOME pass through, and npm_package_* keys are regenerated from the manifest. POSIX case-sensitivity and Windows case-insensitivity tests are expanded with new edge cases.
Rust lifecycle e2e test
pacquet/crates/executor/src/lifecycle/tests.rs
The unix e2e test is renamed and updated to set both npm_config_platform_arch (expected preserved) and npm_config__authtoken (expected stripped), capture both in the shell dump via EnvGuard, and assert user=x64 and auth="".
JS fixture and test
exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json, exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js, exec/lifecycle/test/index.ts
The inspect-frozen-lockfile fixture is renamed to inspect-npm-config-env with a postinstall script that prints all npm_config_* vars. The Jest test sets npm_config_platform_arch=x64, runs runLifecycleHook, and asserts the value is received via IPC.
Dependency update and changeset
pnpm-workspace.yaml, .changeset/preserve-user-npm-config-vars.md
The @pnpm/npm-lifecycle catalog entry is widened from 1100.0.0-1 to ^1100.0.0. A changeset entry documents the preservation of user-defined npm_config_* variables in lifecycle scripts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • pnpm/pnpm#12338: Both PRs center on npm_config_* environment-variable handling—this PR changes the lifecycle make_env filtering to preserve user-defined npm_config_* values instead of stripping them, which is directly relevant to new URL-scoped auth support carried via npm_config_//… env vars.

Suggested reviewers

  • zkochan

Poem

🐇 Hop hop, the config vars stay!
No more stripping them away,
npm_config_platform_arch lives on,
While auth tokens — poof — are gone.
The rabbit twitched its nose with glee,
"User settings should roam free!" 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: preserving user-defined npm_config_* environment variables in lifecycle scripts, which is the core objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates lifecycle script execution to preserve user-defined npm_config_* environment variables, addressing a regression where npm_-prefixed vars were stripped.

Changes:

  • Switch @pnpm/npm-lifecycle to a GitHub PR ref that contains the env filtering fix.
  • Add a new lifecycle fixture and test to assert preservation of user-defined npm_config_* vars.
  • Add a changeset announcing the patch behavior change for @pnpm/exec.lifecycle and pnpm.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Points @pnpm/npm-lifecycle to a PR ref that presumably contains the env-var preservation fix.
exec/lifecycle/test/index.ts Adds test coverage ensuring user-defined npm_config_* env vars are preserved.
exec/lifecycle/test/fixtures/inspect-user-npm-config/postinstall.js Fixture script that prints the observed npm_config_platform_arch value.
exec/lifecycle/test/fixtures/inspect-user-npm-config/package.json Adds a fixture package with a postinstall that reports env to the IPC server.
.changeset/preserve-user-npm-config-vars.md Documents the behavioral change and bumps relevant packages (patch).
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread exec/lifecycle/test/index.ts
Comment thread pnpm-workspace.yaml Outdated
Comment thread exec/lifecycle/test/fixtures/inspect-user-npm-config/postinstall.js Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Preserve user-defined npm_config_* env vars in lifecycle scripts
🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Preserve user-defined npm_config_* variables when spawning lifecycle scripts.
• Continue stripping (npm|pnpm)_config_* auth-related keys to avoid credential leaks.
• Add/port TS and Rust tests to prevent regressions; bump @pnpm/npm-lifecycle.
Diagram
graph TD
  A[("Parent process env")] --> B["pacquet make_env filter"] --> C["Lifecycle hook spawn"] --> D["Child lifecycle script"] --> E["Assert visible/stripped env"]
  F["TS runLifecycleHook test"] --> C
  G["Rust executor e2e + unit tests"] --> B
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Allowlist specific npm_config_* keys to preserve
  • ➕ Minimizes risk of preserving unexpected config that influences scripts
  • ➕ Simpler mental model than pattern-based auth detection
  • ➖ High maintenance burden; misses legitimate user config keys
  • ➖ Behavior diverges from npm/pnpm expectations and upstream @pnpm/npm-lifecycle
2. Centralize env filtering logic in a shared crate/module
  • ➕ Reduces drift between @pnpm/npm-lifecycle behavior and pacquet ports
  • ➕ Single place to update security-sensitive auth stripping rules
  • ➖ Requires new shared artifact/API boundary between JS and Rust components
  • ➖ More upfront refactor than a targeted fix

Recommendation: The PR’s approach (bump to the fixed @pnpm/npm-lifecycle and port the upstream parent-env filter into pacquet) is the most compatible and least disruptive. The key review focus should be the auth-key detection rules (prefix _, /, @, or containing :_) to ensure credentials remain stripped while not regressing user-defined npm_config_* propagation.

Grey Divider

File Changes

Bug fix (2)
tests.rs Update executor e2e test to preserve user config and strip auth +15/-9

Update executor e2e test to preserve user config and strip auth

• Renames and rewrites the Unix end-to-end lifecycle spawn test to validate two behaviors: a user-defined 'npm_config_*' var is preserved, and an auth-like 'npm_config_*' var is stripped (empty in child). Uses RAII guards to avoid leaking env between tests.

pacquet/crates/executor/src/lifecycle/tests.rs


make_env.rs Refine parent env filtering to preserve user npm_config_* +61/-31

Refine parent env filtering to preserve user npm_config_*

• Changes env filtering to stop stripping all 'npm_*' keys and instead strip only 'npm_package_*' (regenerated) plus auth-like '(npm|pnpm)_config_*' keys. Adds a robust prefix matcher supporting Windows case-insensitivity without UTF-8 boundary panics.

pacquet/crates/executor/src/make_env.rs


Tests (4)
package.json Rename fixture package for npm_config_* env inspection +1/-1

Rename fixture package for npm_config_* env inspection

• Updates the fixture package name from the old frozen-lockfile inspector to a new npm-config env inspector. Keeps the postinstall script wiring to the IPC-based test harness.

exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json


postinstall.js Add fixture script to print npm_config_* env values +5/-0

Add fixture script to print npm_config_* env values

• Adds a postinstall script that enumerates 'npm_config_*' env vars and prints them (excluding 'npm_config_node_gyp'). This allows tests to assert which config keys reach the child lifecycle process.

exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js


index.ts Ensure runLifecycleHook preserves user-defined npm_config_* +20/-10

Ensure runLifecycleHook preserves user-defined npm_config_*

• Replaces the prior assertion that no npm_config env vars are present with a stronger invariant: pnpm must not inject config vars, but must preserve user-defined ones. The test now seeds 'npm_config_platform_arch', runs the hook, asserts it is observed, and restores the original env value.

exec/lifecycle/test/index.ts


tests.rs Expand make_env tests for preserved user config and stripped auth +79/-22

Expand make_env tests for preserved user config and stripped auth

• Updates the ported upstream test expectations: user 'npm_config_platform_arch' must pass through, multiple auth-like keys must be stripped, and leaked 'npm_package_*' must be regenerated. Extends POSIX/Windows stamping-key tests to cover the new rules.

pacquet/crates/executor/src/make_env/tests.rs


Documentation (1)
preserve-user-npm-config-vars.md Add changeset describing npm_config_* preservation fix +6/-0

Add changeset describing npm_config_* preservation fix

• Introduces a changeset marking patch releases for affected packages. Documents that user-defined 'npm_config_*' vars are preserved during lifecycle scripts and references the linked issue.

.changeset/preserve-user-npm-config-vars.md


Other (2)
pnpm-lock.yaml Regenerate lockfile for released @pnpm/npm-lifecycle 1100.0.0 +6/-6

Regenerate lockfile for released @pnpm/npm-lifecycle 1100.0.0

• Pins the catalog dependency from a pre-release '1100.0.0-1' to the released '^1100.0.0' and updates the resolved package entries accordingly. Ensures the repo consumes an immutable registry tarball.

pnpm-lock.yaml


pnpm-workspace.yaml Bump workspace catalog @pnpm/npm-lifecycle to ^1100.0.0 +1/-1

Bump workspace catalog @pnpm/npm-lifecycle to ^1100.0.0

• Updates the workspace catalog to depend on the released '@pnpm/npm-lifecycle' version range. Aligns the workspace with the lockfile update.

pnpm-workspace.yaml


Grey Divider

Qodo Logo

Comment thread pnpm-workspace.yaml Outdated
@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.515 ± 0.165 3.353 3.802 1.79 ± 0.17
pacquet@main 3.523 ± 0.171 3.353 3.767 1.80 ± 0.18
pnpr@HEAD 1.958 ± 0.167 1.746 2.178 1.00
pnpr@main 2.013 ± 0.108 1.842 2.143 1.03 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.5146504515400006,
      "stddev": 0.16488256492057274,
      "median": 3.43824416774,
      "user": 3.8224788000000003,
      "system": 2.0611685,
      "min": 3.35294744574,
      "max": 3.80192083774,
      "times": [
        3.71413332274,
        3.35294744574,
        3.66498553674,
        3.37135073774,
        3.4607970847400003,
        3.80192083774,
        3.59503763874,
        3.41569125074,
        3.39502851674,
        3.37461214374
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.52338909744,
      "stddev": 0.17114545773617412,
      "median": 3.44126206924,
      "user": 3.8219708000000003,
      "system": 2.0709218000000003,
      "min": 3.35349795274,
      "max": 3.76749740774,
      "times": [
        3.36351684674,
        3.40348364374,
        3.37817847674,
        3.35349795274,
        3.76749740774,
        3.41409410274,
        3.75512947174,
        3.46843003574,
        3.61332167274,
        3.71674136374
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.95817075474,
      "stddev": 0.16705735420735077,
      "median": 1.95860930774,
      "user": 2.8751168,
      "system": 1.7690194000000001,
      "min": 1.74561520274,
      "max": 2.17802403174,
      "times": [
        1.7504606627400001,
        2.13538570274,
        2.17802403174,
        1.87575839374,
        2.04190508774,
        1.83292988474,
        2.04146022174,
        1.84067382174,
        2.13949453774,
        1.74561520274
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.0132962024400003,
      "stddev": 0.10764879283226089,
      "median": 2.0317080587399996,
      "user": 2.827972,
      "system": 1.7937020000000001,
      "min": 1.8421040877400001,
      "max": 2.14255639474,
      "times": [
        1.96813026674,
        2.01454627974,
        2.04886983774,
        1.93954978374,
        2.14255639474,
        2.12872284374,
        2.09418689274,
        2.09345314774,
        1.8421040877400001,
        1.86084248974
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 445.4 ± 12.1 430.2 463.1 1.01 ± 0.03
pacquet@main 440.0 ± 9.3 432.2 460.7 1.00
pnpr@HEAD 577.7 ± 137.8 468.4 821.4 1.31 ± 0.31
pnpr@main 478.4 ± 16.2 461.8 513.4 1.09 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.44536769538000004,
      "stddev": 0.012078518454689966,
      "median": 0.44295699237999997,
      "user": 0.36121633999999997,
      "system": 0.76003444,
      "min": 0.43023078788,
      "max": 0.46314507888,
      "times": [
        0.44585895288,
        0.43070292388000003,
        0.45140533788000003,
        0.43847567088,
        0.46314507888,
        0.45358158488,
        0.43730614788,
        0.46291543688000003,
        0.43023078788,
        0.44005503188
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.4400341124800001,
      "stddev": 0.009297694584645085,
      "median": 0.43562207838,
      "user": 0.35752493999999996,
      "system": 0.7590570399999998,
      "min": 0.43219617188,
      "max": 0.46072948088,
      "times": [
        0.43523303788,
        0.45166010088,
        0.43305977088000003,
        0.43801140588,
        0.44337112888,
        0.43516195088,
        0.46072948088,
        0.43490695788,
        0.43601111888,
        0.43219617188
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.57770548288,
      "stddev": 0.13780992483084217,
      "median": 0.49255546788000004,
      "user": 0.3737044399999999,
      "system": 0.7870872399999999,
      "min": 0.46839422688,
      "max": 0.82137510788,
      "times": [
        0.46957628188,
        0.46839422688,
        0.47097401888,
        0.49137080888,
        0.47153658388,
        0.62825561688,
        0.82137510788,
        0.75580439388,
        0.7060276628800001,
        0.49374012688
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.47835870638000005,
      "stddev": 0.016237752469651326,
      "median": 0.47619870588,
      "user": 0.36463983999999994,
      "system": 0.78316614,
      "min": 0.46178303788,
      "max": 0.51338550388,
      "times": [
        0.48963473788,
        0.51338550388,
        0.46511808388000003,
        0.48522828888,
        0.46178303788,
        0.46774729088,
        0.46859560988,
        0.46299595788000003,
        0.48380180188,
        0.48529675088
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.929 ± 0.037 3.879 3.993 2.09 ± 0.07
pacquet@main 3.944 ± 0.047 3.882 4.013 2.10 ± 0.08
pnpr@HEAD 1.976 ± 0.107 1.817 2.157 1.05 ± 0.07
pnpr@main 1.879 ± 0.064 1.790 1.984 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.92921579888,
      "stddev": 0.036512083042880325,
      "median": 3.92151285588,
      "user": 3.9564478999999992,
      "system": 2.0576589399999996,
      "min": 3.8792408958799998,
      "max": 3.99264298988,
      "times": [
        3.91339063788,
        3.9171099268800003,
        3.8872183378800003,
        3.99264298988,
        3.96851596688,
        3.92591578488,
        3.8792408958799998,
        3.90325424888,
        3.95027879588,
        3.95459040388
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.9438758882799996,
      "stddev": 0.04693621480537176,
      "median": 3.93990311788,
      "user": 3.9621223,
      "system": 2.0583756400000004,
      "min": 3.88205475888,
      "max": 4.0131665418799995,
      "times": [
        3.94766331388,
        3.89972486988,
        3.9978587878800003,
        3.95365001588,
        3.91716918488,
        3.88205475888,
        4.0131665418799995,
        3.93214292188,
        3.99938981688,
        3.89593867088
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.9758315077800002,
      "stddev": 0.10696211262168633,
      "median": 1.99075332488,
      "user": 2.684778,
      "system": 1.7493133399999998,
      "min": 1.81691144588,
      "max": 2.15700814088,
      "times": [
        2.15700814088,
        1.85737390488,
        1.8714292218800002,
        2.04204666188,
        2.04536971588,
        1.9638040378800001,
        2.01770261188,
        2.05746290988,
        1.81691144588,
        1.92920642688
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.87907213528,
      "stddev": 0.06378471708266278,
      "median": 1.90243369988,
      "user": 2.7291799999999995,
      "system": 1.76203564,
      "min": 1.78998939288,
      "max": 1.98370274488,
      "times": [
        1.9057636598799998,
        1.84530739588,
        1.98370274488,
        1.80680084288,
        1.78998939288,
        1.90339614688,
        1.93882740388,
        1.90147125288,
        1.90814226988,
        1.80732024288
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.106 ± 0.016 1.086 1.129 2.36 ± 0.05
pacquet@main 1.120 ± 0.044 1.084 1.236 2.39 ± 0.10
pnpr@HEAD 0.469 ± 0.007 0.461 0.483 1.00
pnpr@main 0.478 ± 0.006 0.468 0.489 1.02 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.10555246226,
      "stddev": 0.015503653320392134,
      "median": 1.1005775996599998,
      "user": 1.2840084999999999,
      "system": 0.9887237800000002,
      "min": 1.08608912716,
      "max": 1.1294373951599999,
      "times": [
        1.10074595116,
        1.1294373951599999,
        1.10000951716,
        1.1004092481599999,
        1.0998234091599999,
        1.10337719316,
        1.1294092361599999,
        1.08608912716,
        1.11915959316,
        1.08706395216
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.11982142176,
      "stddev": 0.043596806123377,
      "median": 1.1095106936599999,
      "user": 1.2759361,
      "system": 1.00922388,
      "min": 1.08426954016,
      "max": 1.23607349916,
      "times": [
        1.09128971916,
        1.1223057001599999,
        1.08426954016,
        1.09668054616,
        1.11598321516,
        1.0966584851599999,
        1.10303817216,
        1.23607349916,
        1.12031480216,
        1.13160053816
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.46926945166000006,
      "stddev": 0.006592125784060579,
      "median": 0.46849961766000003,
      "user": 0.31558689999999995,
      "system": 0.74021518,
      "min": 0.46064922816000003,
      "max": 0.48263876116000004,
      "times": [
        0.46213345516000004,
        0.46064922816000003,
        0.46727173816000006,
        0.46453369416000007,
        0.46672194116000004,
        0.47491650316000006,
        0.48263876116000004,
        0.47022649216000006,
        0.46972749716000006,
        0.47387520616000006
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.47765111476,
      "stddev": 0.005667873690264281,
      "median": 0.4768660491600001,
      "user": 0.32273089999999993,
      "system": 0.7442259799999998,
      "min": 0.46768488216000004,
      "max": 0.48944362416000003,
      "times": [
        0.47883302216000007,
        0.47508674216,
        0.47462111716000005,
        0.47625817416000005,
        0.48234090616,
        0.46768488216000004,
        0.47956062916000003,
        0.48944362416000003,
        0.47520812616,
        0.47747392416000006
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.789 ± 0.042 2.747 2.883 5.95 ± 0.10
pacquet@main 2.814 ± 0.024 2.780 2.844 6.00 ± 0.07
pnpr@HEAD 0.482 ± 0.011 0.465 0.497 1.03 ± 0.03
pnpr@main 0.469 ± 0.004 0.462 0.474 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.7891072544,
      "stddev": 0.04203004351770844,
      "median": 2.7821681446,
      "user": 1.7690864199999996,
      "system": 1.2016063,
      "min": 2.7468767751,
      "max": 2.8833377061,
      "times": [
        2.7973202191,
        2.7600682981,
        2.7468767751,
        2.7578587211,
        2.7524254741000003,
        2.7675143191,
        2.7968219701,
        2.8243111721000003,
        2.8045378891,
        2.8833377061
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.813549419,
      "stddev": 0.023616895780627725,
      "median": 2.8169694706,
      "user": 1.7637086199999998,
      "system": 1.212416,
      "min": 2.7798307261,
      "max": 2.8444770001000004,
      "times": [
        2.8033994831,
        2.8233762791,
        2.8239828051,
        2.7939460311,
        2.8418387481000003,
        2.7811590411,
        2.8444770001000004,
        2.7798307261,
        2.8329214141000003,
        2.8105626621
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.48199071260000004,
      "stddev": 0.010894500403428183,
      "median": 0.48224600410000007,
      "user": 0.31441902000000005,
      "system": 0.7403345,
      "min": 0.4648001051,
      "max": 0.4970492441,
      "times": [
        0.47583464710000006,
        0.4923160401,
        0.48096601910000003,
        0.47646006310000005,
        0.49449430210000006,
        0.48352598910000005,
        0.4970492441,
        0.4648001051,
        0.4863344371,
        0.46812627910000004
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.46899170460000006,
      "stddev": 0.004257307512507492,
      "median": 0.47080495110000004,
      "user": 0.31155531999999997,
      "system": 0.7369406000000001,
      "min": 0.4618823891,
      "max": 0.47421026510000003,
      "times": [
        0.46518751510000006,
        0.46509412510000003,
        0.47421026510000003,
        0.46517972710000005,
        0.4618823891,
        0.47212115010000005,
        0.47342078610000005,
        0.47115142810000005,
        0.4704584741,
        0.4712111861
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12400
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
3,929.22 ms
(-1.66%)Baseline: 3,995.74 ms
4,794.89 ms
(81.95%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,789.11 ms
(+0.24%)Baseline: 2,782.55 ms
3,339.05 ms
(83.53%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,105.55 ms
(-7.77%)Baseline: 1,198.73 ms
1,438.47 ms
(76.86%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
3,514.65 ms
(-13.18%)Baseline: 4,048.39 ms
4,858.07 ms
(72.35%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
445.37 ms
(-30.63%)Baseline: 641.98 ms
770.37 ms
(57.81%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12400
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
1,975.83 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
481.99 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
469.27 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
1,958.17 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
577.71 ms
🐰 View full continuous benchmarking report in Bencher

@felipecrs felipecrs force-pushed the preserve-user-defined-npm-config-vars branch from 4576c79 to 0e45b11 Compare June 14, 2026 05:57
@zkochan zkochan force-pushed the preserve-user-defined-npm-config-vars branch from 0e45b11 to 03ba3e0 Compare June 14, 2026 11:26
…to pacquet

Pin the catalog to the released `@pnpm/npm-lifecycle` ^1100.0.0 instead of a
mutable PR-head ref, regenerating the lockfile to the immutable registry
tarball.

Port the upstream env filter to pacquet's make_env so user-defined
npm_config_* vars (e.g. npm_config_platform_arch) survive lifecycle scripts
while (npm|pnpm)_config_* auth keys are still stripped, matching
`@pnpm/npm-lifecycle` 9e2ac78148.

Harden the new TS test to save/restore npm_config_platform_arch.
@zkochan zkochan marked this pull request as ready for review June 14, 2026 11:42
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5b885fc

@codecov-commenter

codecov-commenter commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.14%. Comparing base (681b593) to head (b65f393).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #12400   +/-   ##
=======================================
  Coverage   88.14%   88.14%           
=======================================
  Files         300      300           
  Lines       39716    39730   +14     
=======================================
+ Hits        35006    35020   +14     
  Misses       4710     4710           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pacquet/crates/executor/src/lifecycle/tests.rs (1)

348-357: ⚠️ Potential issue | 🟡 Minor

Restore prior env values instead of always removing on drop.

EnvGuard currently deletes keys unconditionally in Drop. If either variable was already set before this test, the original process env state is lost for subsequent tests. Capture std::env::var_os before setting and restore that value on drop (remove only when it was originally absent).

Suggested patch
-    struct EnvGuard(&'static str);
+    struct EnvGuard {
+        key: &'static str,
+        prev: Option<std::ffi::OsString>,
+    }
+    impl EnvGuard {
+        fn set(key: &'static str, value: &str) -> Self {
+            let prev = std::env::var_os(key);
+            // SAFETY: guarded test-local mutation; cleanup in Drop.
+            unsafe { std::env::set_var(key, value) }
+            Self { key, prev }
+        }
+    }
     impl Drop for EnvGuard {
         fn drop(&mut self) {
-            // SAFETY: nextest runs each test in its own thread, so the
-            // only risk is sibling tests calling `env::vars()`
-            // concurrently — this `Drop` still runs on panic.
-            unsafe { std::env::remove_var(self.0) }
+            match &self.prev {
+                Some(v) => unsafe { std::env::set_var(self.key, v) },
+                None => unsafe { std::env::remove_var(self.key) },
+            }
         }
     }
-    let _user_guard = EnvGuard("npm_config_platform_arch");
-    let _auth_guard = EnvGuard("npm_config__authtoken");
-    // SAFETY: nextest runs each test in its own thread, so the only
-    // risk is sibling tests calling `env::vars()` concurrently — the
-    // guards' `Drop` removes the vars even on panic.
-    unsafe {
-        std::env::set_var("npm_config_platform_arch", "x64");
-        std::env::set_var("npm_config__authtoken", "should-not-leak");
-    }
+    let _user_guard = EnvGuard::set("npm_config_platform_arch", "x64");
+    let _auth_guard = EnvGuard::set("npm_config__authtoken", "should-not-leak");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/executor/src/lifecycle/tests.rs` around lines 348 - 357, The
EnvGuard struct currently only stores the environment variable name and
unconditionally removes it in the Drop implementation, which loses the original
value if that variable was already set before the test. Modify the EnvGuard
struct to capture and store the original environment variable value using
std::env::var_os before any modifications are made. Then update the Drop
implementation's drop method to restore the previous value (using
std::env::set_var if it existed, or remove it only if it was originally absent)
instead of unconditionally calling std::env::remove_var. This ensures that the
original process environment state is preserved for subsequent tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@pacquet/crates/executor/src/lifecycle/tests.rs`:
- Around line 348-357: The EnvGuard struct currently only stores the environment
variable name and unconditionally removes it in the Drop implementation, which
loses the original value if that variable was already set before the test.
Modify the EnvGuard struct to capture and store the original environment
variable value using std::env::var_os before any modifications are made. Then
update the Drop implementation's drop method to restore the previous value
(using std::env::set_var if it existed, or remove it only if it was originally
absent) instead of unconditionally calling std::env::remove_var. This ensures
that the original process environment state is preserved for subsequent tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fe18acd8-d79f-4cb4-a4bb-b47c00b839f3

📥 Commits

Reviewing files that changed from the base of the PR and between 71e46de and 5b885fc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • .changeset/preserve-user-npm-config-vars.md
  • exec/lifecycle/test/fixtures/inspect-frozen-lockfile/postinstall.js
  • exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json
  • exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js
  • exec/lifecycle/test/index.ts
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
  • pacquet/crates/executor/src/make_env/tests.rs
  • pnpm-workspace.yaml
💤 Files with no reviewable changes (1)
  • exec/lifecycle/test/fixtures/inspect-frozen-lockfile/postinstall.js
✅ Files skipped from review due to trivial changes (3)
  • exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js
  • .changeset/preserve-user-npm-config-vars.md
  • exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • exec/lifecycle/test/index.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Dylint
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Match how the same feature is implemented in the TypeScript pnpm CLI in pnpm/, pkg-manager/, resolving/, lockfile/, store/, fetching/, config/, hooks/, and other TypeScript workspaces — behavior, flags, defaults, error codes, file formats, and directory layout must match pnpm exactly
When porting a function that fires pnpm: events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way it parses pnpm's. Follow the Reporter / log events convention for channel mapping, threading R: Reporter, emit-site placement, and recording-fake tests.
Prefer real fixtures using tempfile::TempDir, the mocked registry, or integration tests over dependency-injection seams for testing. Only use the DI seam (Host capability trait, Sys bounds, etc.) for branches real fixtures cannot cover: filesystem error kinds (PermissionDenied, ENOSPC), deterministic time, shared process-global state (env::set_var, set_current_dir, umask), or external-service happy paths (pnpm login 2FA, pnpm publish OIDC/provenance). Follow the eight principles and naming conventions (Sys, Host, Fs*, Clock, EnvVar) documented in CODE_STYLE_GUIDE.md.
Declare a newtype wrapper when porting code using branded string types from TypeScript pnpm. Do not collapse the brand into plain String or &str. Give the type its own struct so misuse is a type error.
If upstream always validates before construction of a branded string type, validate too. The Rust wrapper must construct only via TryFrom and/or FromStr. Do not provide an infallible public constructor that takes an arbitrary string.
If upstream never validates a branded string type, just brand for type-safety. Expose an infallible From and From<&str> when convenient. The type-safety win is the whole point, and no validator is needed.
If upstream occasionally constructs a branded string type without validatio...

Files:

  • pacquet/crates/executor/src/make_env/tests.rs
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
🧠 Learnings (5)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/executor/src/make_env/tests.rs
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/executor/src/make_env/tests.rs
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/executor/src/make_env/tests.rs
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

  • pacquet/crates/executor/src/make_env/tests.rs
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
📚 Learning: 2026-06-12T20:41:57.558Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12364
File: pacquet/crates/package-manager/src/install/tests.rs:5717-5717
Timestamp: 2026-06-12T20:41:57.558Z
Learning: In the pnpm/pnpm Rust workspace (toolchain Rust 1.95.0), keep using `std::time::Duration::from_mins(...)` and `std::time::Duration::from_hours(...)` as-is. Do not flag these as invalid or suggest replacing them with `Duration::from_secs(...)`, because Clippy’s `clippy::duration_suboptimal_units` lint is denied by `-D warnings` in CI, and changing to seconds may trigger CI failures.

Applied to files:

  • pacquet/crates/executor/src/make_env/tests.rs
  • pacquet/crates/executor/src/lifecycle/tests.rs
  • pacquet/crates/executor/src/make_env.rs
🔇 Additional comments (2)
pacquet/crates/executor/src/make_env.rs (1)

11-13: LGTM!

Also applies to: 41-44, 57-64, 131-134, 147-159, 164-172, 181-201

pacquet/crates/executor/src/make_env/tests.rs (1)

33-48: LGTM!

Also applies to: 51-59, 82-102, 282-317, 340-367

The guard removed the seeded var unconditionally on drop, which would
discard any value the process env already had. Capture the original via
var_os and restore it (or remove only when originally absent).
Copilot AI review requested due to automatic review settings June 14, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 14, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b65f393

@zkochan zkochan merged commit 23716ed into pnpm:main Jun 14, 2026
24 checks passed
@welcome

welcome Bot commented Jun 14, 2026

Copy link
Copy Markdown

Congrats on merging your first pull request! 🎉🎉🎉

@felipecrs felipecrs deleted the preserve-user-defined-npm-config-vars branch June 14, 2026 14:58
@felipecrs

felipecrs commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks a lot for completing this PR as well, @zkochan!

It's very nice to learn that pnpm is being rewritten in Rust.

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.

pnpm v11 unsets npm_config_ environment variables

4 participants