chore: migrate from npm to pnpm with supply chain hardening#579
chore: migrate from npm to pnpm with supply chain hardening#579Kabidoye-17 wants to merge 12 commits into
Conversation
Team: Integration Capabilities
pnpm's global install has no fallback when a registry manifest lacks dist-tags, and Intercom's Socket Firewall proxy serves the fern-api manifest without them, so `pnpm add -g fern-api` (unpinned) fails with ERR_PNPM_NO_MATCHING_VERSION. Pinning bypasses `latest` resolution. Upstream: pnpm/pnpm#5564, pnpm/pnpm#9944 (both open).
Re-merge main (16 commits) and regenerate pnpm-lock.yaml from mains package-lock.json. package.json + spec YAMLs auto-merged. The regen used a one-time resolution bypass of the pnpm-workspace.yaml trustPolicy: no-downgrade, which flags undici@5.29.0 (transitive via @actions/core) as a provenance downgrade. undici@5.29.0 is the version main already pins on the 5.x line; no-downgrade reads the provenance-less 5.x backport as a downgrade from the 6.x line (same false-positive class developer-docs documents for semver@6.3.1 / undici-types@6.20.0). trustPolicy is left fully intact; --frozen-lockfile does not re-apply it, so CI installs the pinned lock cleanly. Verified on pnpm 10.23.0: pnpm install --frozen-lockfile clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnpm's preinstall lifecycle fires AFTER pnpm relayouts node_modules
(creates .pnpm/ and moves any npm-installed dirs to .ignored_<name>/),
so the guard's `!fs.existsSync('node_modules/.pnpm')` check is always
false by the time it runs. The branch is dead code under `pnpm install`.
The wrong-package-manager path above already catches the common
failure mode (a contributor running `npm install`), so nothing else
is needed. Verified by instrumenting the script and running each
install path.
On a fresh macOS shell, `pnpm add -g <pkg>` errors with `ERR_PNPM_NO_GLOBAL_BIN_DIR` because PNPM_HOME isn't wired into PATH yet. This trips up anyone regenerating an SDK from a clean clone — including Fern-support engineers. Document the one-time `pnpm setup` step in both the internal Development runbook (CLAUDE.md) and the external CONTRIBUTING guide.
The CI/CD table describes intended behavior, but `.github/workflows/` doesn't exist on main — every workflow moved to `workflows-disabled/` as part of PR #536 (org-wide GitHub Actions enablement migration, 2026-06-03), and FERN_TOKEN / FERN_NPM_TOKEN were removed after the 2026-05-01 Fern token incident. Restoration is a separate operation gated on both the org migration and secret re-provisioning. Adds a leading note above the table so readers know the schedule column is aspirational until re-enablement.
The `pnpm-install` telemetry event fired on every successful install forever, not just during the migration window — it would keep sending per-engineer / per-repo data to Honeycomb long after the org-wide pnpm rollout completed, polluting the dataset with steady-state noise. Rollout progress can be inferred from the ABSENCE of `wrong-package-manager` events over time, so no substitute signal is needed. The failure-path telemetry (`wrong-package-manager`) remains in place — that's where the interesting signal lives.
Drops scripts/check-package-manager.js and its package.json preinstall hook. The guard emitted install-time telemetry to an external endpoint, which is out of place in a public repo and unnecessary for the pnpm migration itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fresh machines without PNPM_HOME configured hit ERR_PNPM_NO_GLOBAL_BIN_DIR on pnpm add -g. Add the one-time pnpm setup step in front of the fern-api install so external contributors following CONTRIBUTING.md don't trip on it.
There was a problem hiding this comment.
❌ PR Review Summary: NEEDS HUMAN REVIEW
| Summary | |
|---|---|
| ✅ | 🎯 Problem — The PR title and description are excellent, clearly articulating the motivation (org-wide supply chain hardening audit), referencing the broader initiative (FE-3214), and documenting key implementation decisions and follow-up considerations. |
| ✅ | 📝 Alignment — All substantive claims in the description are confirmed by the diff, including the npm-to-pnpm migration, supply-chain settings in pnpm-workspace.yaml, es5-ext silencing, SHA-pinned pnpm/action-setup in disabled workflows, fern-api pinned to 5.65.3, and explicitly scoped-out GHA hardening gaps. |
| ℹ️ | 🧠 Correctness — Disabled for this repository. |
| ❌ | 🦺 Safety — The PR touches multiple categories requiring mandatory human review: seven CI/CD workflow files under .github/, dependency manifests and lockfiles (package.json modified, package-lock.json deleted, pnpm-lock.yaml and pnpm-workspace.yaml added), and the sensitive CLAUDE.md configuration file. |
| ❌ | 🚦 Auto-approval — Touches AI config files (CLAUDE.md); Exceeds LOC limit (10800 meaningful LOC > 150, raw 10800) |
Per-criterion details
Problem
Excellent PR description that clearly explains the why (org-wide supply chain hardening audit), provides context about the broader initiative (FE-3214), and documents important follow-up considerations. While the How section is detailed, it's justified for a security-related infrastructure change where implementation decisions matter.
Alignment
The PR description claims a migration from npm to pnpm with supply chain hardening. Checking each claim against the diff:
-
npm to pnpm migration: ✅ The diff shows
package-lock.jsondeleted,pnpm-lock.yamlcreated,package.jsonupdated with"packageManager": "pnpm@10.23.0", all workflow files changed fromnpm install -g fern-apitopnpm add -g fern-api@5.65.3, and documentation (CLAUDE.md, CONTRIBUTING.md) updated with pnpm commands. -
Supply-chain settings (
minimumReleaseAge: 10080,trustPolicy: no-downgrade) inpnpm-workspace.yaml: ✅ Confirmed exactly in the newpnpm-workspace.yamlfile. -
es5-extsilenced viapnpm.ignoredBuiltDependencies: ✅ Confirmed in thepackage.jsonchanges. -
Disabled workflows updated with SHA-pinned
pnpm/action-setup: ✅ All 7 workflow files under.github/workflows-disabled/usepnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.1.0. -
Pre-existing GHA hardening gaps left out of scope: ✅ The diff shows
actions/checkout@v4and@v3remain unpinned, consistent with the claim. -
fern-apipinned to5.65.3: ✅ All workflows usepnpm add -g fern-api@5.65.3. -
pnpm importused: The diff showspackage-lock.jsondeleted andpnpm-lock.yamlcreated as new file, consistent with this claim (we can't verify the exact command from the diff alone, but the outcome matches).
No significant changes in the diff are omitted from the description. The CONTRIBUTING.md additions (pnpm setup instructions) are a minor doc update that's implicitly part of the migration. All substantive changes are accurately described.
Safety
This PR is a tooling migration from npm to pnpm that touches multiple categories of files explicitly listed as requiring human review in the safety criteria. The changes include:
-
CI/CD workflow files (
.github/workflows-disabled/): Seven workflow files are modified. Even though they're currently in a "disabled" directory, they are under.github/and represent CI/CD configuration. -
Dependency manifests and lockfiles:
package.jsonis modified (addingpackageManagerfield andpnpm.ignoredBuiltDependencies),package-lock.jsonis deleted entirely, and newpnpm-lock.yamlandpnpm-workspace.yamlfiles are added. These are explicitly called out as unsafe in the criteria. -
CLAUDE.md: The project instructions file is modified (changing
npm installtopnpm installreferences). The criteria note that "CLAUDE.md and.claude/config are treated as sensitive."
None of the changes touch the safe categories (Unstable/Preview spec, generated Postman collections, or general documentation like README). While CONTRIBUTING.md changes alone might qualify as safe documentation, the PR as a whole is clearly infrastructure/tooling work that requires human review.
<violated_criteria>
CI/CD and repo config — changes to files under .github/ (seven workflow files in .github/workflows-disabled/ are modified)
Dependency manifests / lockfiles — package.json modified, package-lock.json deleted, new pnpm-lock.yaml and pnpm-workspace.yaml added
CLAUDE.md modification — sensitive project configuration file edited
</violated_criteria>
Rate this comment 👍 / 👎 to help us improve 🙏 · Disagree with assessment? Establish ground truth here
levindixon
left a comment
There was a problem hiding this comment.
Reviewed, tested, and verified this locally. All the repo's workflows are disabled so CI can't exercise any of the pnpm path - I ran the migration's claims end-to-end on this branch instead. Everything load-bearing checks out.
No blockers - one should-fix nit, one optional nit, two FYIs below.
What looks good:
- The
pnpm importfidelity claim is exactly true: I diffed resolved versions across both lockfiles - 487 packages on each side, zero only-in-one, zero version drift. pnpm install --frozen-lockfileis clean on 10.23.0: no unknown-setting warnings (bothpnpm-workspace.yamlkeys are valid on this version), no build-script prompts, andes5-ext@0.10.64really is in the tree, so the silencing entry is doing something.- Every non-builtin
require()inscripts/is a declared dependency, and regenerating the Postman collections for all 9 stable versions worked on the pnpm tree - the stricternode_moduleslayout doesn't break anything here. - All 7 workflows run checkout before
pnpm/action-setup(required, since with noversioninput the action readspackageManagerfrom the checked-outpackage.json), and the action exportsPNPM_HOMEat the pinned ref, sopnpm add -g fern-api@5.65.3gets a global bin dir on runners. Thepnpm setupnote in CONTRIBUTING is correctly scoped to local machines only.
Nit (should fix): the pin comments say # v4.1.0 but b906affcc is v4.3.0. v4.1.0 is a7487c7, and the floating v4 tag currently sits on b906affcc, which is probably how the SHA got resolved. The pin itself is correct and safe - it's just the label that's wrong, and both humans and version-bump tooling (Renovate-style pin updates) read that comment to know what's pinned. 10 occurrences across the 7 workflow files, s/v4.1.0/v4.3.0/ and done.
Nit (optional): the ts job in preview_sdks.yml still has the pre-existing unpinned pnpm/action-setup@v4 at its Compile step. Pre-existing hardening gaps are declared out of scope, but this one is the same action the PR pins everywhere else, and v4 resolves to the same b906affcc commit today, so pinning it is behavior-neutral.
FYI: three lockfile entries will trip trustPolicy: no-downgrade the next time they get re-resolved: semver@5.7.2, semver@6.3.1, undici@5.29.0. I replayed pnpm's check against all 524 pinned versions and each of these is a provenance downgrade vs an earlier release of the same package. Frozen installs don't run the check (verified - that's why CI stays green), so this only bites the first pnpm add / pnpm update that touches one of them. Escape hatch when it does: trustPolicyExclude (supported since 10.22.0), or bump the parent deps.
FYI for whoever re-enables fern_check.yml: fern check currently fails with 3 pre-existing errors in the Preview fern defs (duplicate CreateConversationAttributeOptionRequest / PublishArticleDraftRequest declarations, undefined UpdateArticleRequestBody). Reproduces on main, unrelated to this PR.
Verification detail: what I ran and what it showed
- Lockfile fidelity: parsed both lockfiles and diffed resolved versions. 487 packages each, zero only-in-one, zero version differences.
pnpm install --frozen-lockfileon pnpm 10.23.0 (matchingpackageManager): clean in ~3s, 524 packages. No unknown-setting warnings (minimumReleaseAgelanded in 10.16.0,trustPolicyin 10.21.0).- Phantom-dep audit: every non-builtin
require()acrossscripts/(@actions/core,@stoplight/json-ref-resolver,api,axios,deepmerge,dotenv,jsonpath-plus,lodash,uuid,yaml) is declared inpackage.json. - End-to-end:
node scripts/postman/generate-postman-in-repo.jsregenerates all 9 stable versions on the pnpm tree. Two pre-existing observations, both reproduce on main: the checked-inpostman/outputs are stale (last generated 2026-04-16, the spec has since gained fields likeaudience_ids), and the Preview (0) version crashes incollection.js's formdata path - that script hasn't changed since 2025-04 and runs identical dep versions, so it's not this PR. - Workflows: all 7 files run
actions/checkoutbeforepnpm/action-setup.generate_postman_collection.ymlhas the right cache order (action-setup->setup-nodewithcache: pnpm-> frozen install). The action atb906affccexportsPNPM_HOMEand prepends it toPATH. - Pinned fern install:
npx -y fern-api@5.65.3 checkresolves and runs through the registry proxy, consistent with the dist-tags analysis in the description (exact-version resolution doesn't needdist-tags.latest). - Bonus latent fix: the tracked
.npmrcreferences${GITHUB_TOKEN}. npm hard-fails on an unresolvable env template and runners don't export that var by default, so the oldnpm install -g fern-apisteps had a latent failure mode. pnpm 10 tolerates the unset var - tested with it explicitly unset. - Residue sweep: zero references to npm commands or
package-lock.jsonleft in docs, workflows, or scripts.
The upstream issue links (pnpm/pnpm#5564, pnpm/pnpm#9944) and the explicit re-enable caveat in the description are exactly the context a future maintainer needs.





Why?
This repo was surfaced in a follow-up audit as part of the org-wide effort to bring active npm repos onto a supply-chain-hardened footing (per-package script gating, install cooldown, pnpm's stricter dependency graph). Migrating to pnpm 10 aligns Intercom-OpenAPI with the target config already applied across the sibling repos in FE-3214.
How?
pnpm importwas used to convertpackage-lock.jsontopnpm-lock.yamlverbatim, preserving every resolved version so no transitive dependency drift shipped alongside the tooling swap. Supply-chain settings (minimumReleaseAge: 10080,trustPolicy: no-downgrade) live inpnpm-workspace.yaml; thees5-extcosmetic postinstall is silenced viapnpm.ignoredBuiltDependencies. Registry-source lockdown is already handled at the infra layer by Socket Firewall, so no pnpm-side config is needed here. Disabled workflows under.github/workflows-disabled/were updated so they continue to work if re-enabled — the newpnpm/action-setuprefs are SHA-pinned. Pre-existing GHA hardening gaps (unpinnedactions/checkout, missing top-levelpermissions:blocks) were left out of scope; whoever re-enables any workflow should run the security-review skill end-to-end first.Follow-up
fern-apiis pinned to5.65.3in workflows.pnpm add -g fern-api(unpinned) fails through Intercom's registry proxy because the proxy serves thefern-apimanifest without adist-tagsblock, and pnpm has no fallback resolver for a missinglatesttag. Upstream issues, both open:When those are fixed (or the proxy is fixed to include dist-tags), the pin can be dropped.
Generated with Claude Code