Skip to content

chore: migrate from npm to pnpm with supply chain hardening#579

Merged
zilleeizad-inter merged 13 commits into
mainfrom
pnpm-migration
Jul 24, 2026
Merged

chore: migrate from npm to pnpm with supply chain hardening#579
zilleeizad-inter merged 13 commits into
mainfrom
pnpm-migration

Conversation

@Kabidoye-17

@Kabidoye-17 Kabidoye-17 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

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 import was used to convert package-lock.json to pnpm-lock.yaml verbatim, preserving every resolved version so no transitive dependency drift shipped alongside the tooling swap. Supply-chain settings (minimumReleaseAge: 10080, trustPolicy: no-downgrade) live in pnpm-workspace.yaml; the es5-ext cosmetic postinstall is silenced via pnpm.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 new pnpm/action-setup refs are SHA-pinned. Pre-existing GHA hardening gaps (unpinned actions/checkout, missing top-level permissions: blocks) were left out of scope; whoever re-enables any workflow should run the security-review skill end-to-end first.

Follow-up

fern-api is pinned to 5.65.3 in workflows. pnpm add -g fern-api (unpinned) fails through Intercom's registry proxy because the proxy serves the fern-api manifest without a dist-tags block, and pnpm has no fallback resolver for a missing latest tag. 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

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).
@Kabidoye-17

Kabidoye-17 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Local Verification — no regressions

Same fern flow run on both sides. The same 3 pre-existing spec errors and 295 warnings appear on main and on this branch — no new errors introduced by the migration.

Master (npm install)

rm -rf node_modules && npm install — 550 packages in 6s; 6 deprecation warnings; 12 vulnerabilities (4 moderate, 8 high) reported by npm audit.

master: npm install output

npm install -g fern-apifern checkfern generate --preview --group ts-sdk — 3 errors, 295 warnings.

master: fern check + generate --preview

This branch (pnpm install)

rm -rf node_modules && pnpm install — 522 packages in 3.3s; preinstall guard runs and exits 0. The ${GITHUB_TOKEN} .npmrc warning is pre-existing (harmless — no @intercom/* runtime deps consume it).

branch: pnpm install output

fern check — 3 errors, 295 warnings (identical to master).

branch: fern check

fern generate --preview --group ts-sdk — 3 errors, 295 warnings (identical to master).

branch: fern generate --preview --group ts-sdk

Errors — identical on both sides

File Error
preview/conversationsAttributes.yml CreateConversationAttributeOptionRequest already declared in preview/__package__.yml
preview/articles.yml PublishArticleDraftRequest already declared in preview/__package__.yml
preview/articles.yml UpdateArticleRequestBody is not defined

Pre-existing spec issues, unrelated to the migration.

~ Automated via Claude

@Kabidoye-17
Kabidoye-17 marked this pull request as ready for review July 8, 2026 14:12
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>
shrek-intercom[bot]

This comment was marked as outdated.

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.
shrek-intercom[bot]

This comment was marked as outdated.

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.
shrek-intercom[bot]

This comment was marked as outdated.

@Kabidoye-17 Kabidoye-17 changed the title [FE-3214] chore: migrate from npm to pnpm with supply chain hardening chore: migrate from npm to pnpm with supply chain hardening Jul 23, 2026
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>
shrek-intercom[bot]

This comment was marked as outdated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shrek-intercom[bot]

This comment was marked as outdated.

Kabidoye-17 and others added 2 commits July 23, 2026 16:48
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shrek-intercom[bot]

This comment was marked as outdated.

shrek-intercom[bot]

This comment was marked as outdated.

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.
shrek-intercom[bot]

This comment was marked as outdated.

@zilleeizad-inter
zilleeizad-inter enabled auto-merge (squash) July 23, 2026 16:31

@levindixon levindixon 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.

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 import fidelity 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-lockfile is clean on 10.23.0: no unknown-setting warnings (both pnpm-workspace.yaml keys are valid on this version), no build-script prompts, and es5-ext@0.10.64 really is in the tree, so the silencing entry is doing something.
  • Every non-builtin require() in scripts/ is a declared dependency, and regenerating the Postman collections for all 9 stable versions worked on the pnpm tree - the stricter node_modules layout doesn't break anything here.
  • All 7 workflows run checkout before pnpm/action-setup (required, since with no version input the action reads packageManager from the checked-out package.json), and the action exports PNPM_HOME at the pinned ref, so pnpm add -g fern-api@5.65.3 gets a global bin dir on runners. The pnpm setup note 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-lockfile on pnpm 10.23.0 (matching packageManager): clean in ~3s, 524 packages. No unknown-setting warnings (minimumReleaseAge landed in 10.16.0, trustPolicy in 10.21.0).
  • Phantom-dep audit: every non-builtin require() across scripts/ (@actions/core, @stoplight/json-ref-resolver, api, axios, deepmerge, dotenv, jsonpath-plus, lodash, uuid, yaml) is declared in package.json.
  • End-to-end: node scripts/postman/generate-postman-in-repo.js regenerates all 9 stable versions on the pnpm tree. Two pre-existing observations, both reproduce on main: the checked-in postman/ outputs are stale (last generated 2026-04-16, the spec has since gained fields like audience_ids), and the Preview (0) version crashes in collection.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/checkout before pnpm/action-setup. generate_postman_collection.yml has the right cache order (action-setup -> setup-node with cache: pnpm -> frozen install). The action at b906affcc exports PNPM_HOME and prepends it to PATH.
  • Pinned fern install: npx -y fern-api@5.65.3 check resolves and runs through the registry proxy, consistent with the dist-tags analysis in the description (exact-version resolution doesn't need dist-tags.latest).
  • Bonus latent fix: the tracked .npmrc references ${GITHUB_TOKEN}. npm hard-fails on an unresolvable env template and runners don't export that var by default, so the old npm install -g fern-api steps 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.json left 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.

~ Automated via Claude

The pinned SHA b906affcc is v4.3.0, not v4.1.0 (v4.1.0 is a7487c7).
Update the trailing comments on the 9 pinned occurrences so both humans
and pin-update tooling read the correct label.

Also pin the previously-unpinned pnpm/action-setup@v4 in the ts Compile
step of preview_sdks.yml to the same SHA the rest of the file uses;
today the v4 tag resolves to the same commit, so this is behavior-
neutral and closes the last unpinned pnpm/action-setup reference.

@shrek-intercom shrek-intercom 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.

❌ PR Review Summary: NEEDS HUMAN REVIEW

Summary
🎯 Problem — Well-structured PR with clear motivation, specific ticket reference (FE-3214), detailed approach, and documented limitations — scored 85/100 for excellent clarity appropriate to a security/infrastructure change.
📝 Alignment — Every substantive claim in the description — npm-to-pnpm migration, lockfile conversion, supply-chain settings, es5-ext suppression, SHA-pinned action refs, fern-api version pin, and doc updates — is confirmed by the diff with no omissions.
ℹ️ 🧠 Correctness — Disabled for this repository.
🦺 Safety — The PR touches three categories requiring mandatory human review: files under .github/ (seven disabled workflow files), 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 (10802 meaningful LOC > 150, raw 10802)
Per-criterion details

Problem

Excellent PR with clear motivation (org-wide supply chain hardening audit, alignment with sibling repos), specific context (FE-3214), and even documents known limitations and follow-up work. The 'How' section is detailed but justified for a security/infrastructure change where reviewers need to understand the approach. Could be slightly more concise but the detail is appropriate for the scope.

Alignment

The PR description claims a migration from npm to pnpm with supply chain hardening. Let me verify each claim against the diff:

  1. npm to pnpm migration: ✅ All workflow files change from npm install -g fern-api to pnpm add -g fern-api@5.65.3, package-lock.json is deleted, pnpm-lock.yaml is created, package.json adds "packageManager": "pnpm@10.23.0", and generate_postman_collection.yml changes npm install to pnpm install --frozen-lockfile.

  2. pnpm import used to convert lockfile: Consistent with the diff (package-lock.json deleted, pnpm-lock.yaml created). Cannot verify method but results match.

  3. Supply-chain settings (minimumReleaseAge: 10080, trustPolicy: no-downgrade) in pnpm-workspace.yaml: ✅ Exactly matches the new file content at lines 240-241.

  4. es5-ext silenced via pnpm.ignoredBuiltDependencies: ✅ Present in package.json changes (lines 228-230).

  5. Disabled workflows updated with SHA-pinned pnpm/action-setup refs: ✅ All workflow files use pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0.

  6. Pre-existing GHA hardening gaps left out of scope: ✅ Consistent - no changes to actions/checkout pinning or permissions: blocks.

  7. fern-api pinned to 5.65.3: ✅ All workflows use fern-api@5.65.3.

  8. CLAUDE.md and CONTRIBUTING.md updates: ✅ Both show npm→pnpm command changes.

All substantive claims in the description are verified by the diff, and no significant changes in the diff are omitted from the description. The description is thorough and accurately represents the actual code changes.

Safety

This PR migrates the repository from npm to pnpm with supply chain hardening. While the intent is reasonable and the changes are well-organized, the PR touches multiple categories explicitly listed as requiring human review:

  1. CI/CD workflows — Seven files under .github/workflows-disabled/ are modified. Even though they are currently disabled, they are still under .github/ and the safety criteria explicitly covers "anything under .github/ (workflows, CODEOWNERS)."

  2. Dependency manifests / lockfilespackage.json is modified (adds packageManager field and pnpm.ignoredBuiltDependencies), package-lock.json is deleted, and new files pnpm-lock.yaml and pnpm-workspace.yaml are added. The criteria explicitly flags changes to "dependency manifests / lockfiles (package.json, package-lock.json)."

  3. CLAUDE.md — The project instructions note that "CLAUDE.md and .claude/ config are treated as sensitive." This PR modifies CLAUDE.md to update install commands from npm to pnpm.

None of the changes fall into the "safe to auto-approve" categories (which are limited to additive Unstable/Preview spec changes, generated Postman collections, and non-sensitive documentation).

<violated_criteria>
CI/CD and repo config — changes to files under .github/ (workflows-disabled/fern_check.yml, generate_postman_collection.yml, java-sdk.yml, php-sdk.yml, preview_sdks.yml, python-sdk.yml, ts-sdk.yml)
CI/CD and repo config — changes to dependency manifests/lockfiles (package.json modified, package-lock.json deleted, pnpm-lock.yaml added, pnpm-workspace.yaml added)
CLAUDE.md is treated as sensitive configuration and was modified
</violated_criteria>

Rate this comment 👍 / 👎 to help us improve 🙏 · Disagree with assessment? Establish ground truth here

@zilleeizad-inter
zilleeizad-inter merged commit d30a140 into main Jul 24, 2026
4 checks passed
@zilleeizad-inter
zilleeizad-inter deleted the pnpm-migration branch July 24, 2026 11:15
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.

4 participants