Skip to content

Migrate to Yarn 4 (Berry) - #2706

Draft
delthas wants to merge 10 commits into
development/8.6from
wip-yarn-berry
Draft

delthas wants to merge 10 commits into
development/8.6from
wip-yarn-berry

Conversation

@delthas

@delthas delthas commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Moves Arsenal from Yarn 1 to Yarn 4.18.0 with the node-modules linker, including the two nested install roots (tests/workflows, lib/executables/pensieveCreds).

Yarn is pinned through the packageManager field with its integrity hash, rather than by committing a release binary; CI enables Corepack, and contributors need corepack enable once.

Install-time code execution drops from every dependency to four: Berry disables build scripts by default, and the packages that genuinely need one are allowlisted in dependenciesMeta.

Berry does not run the root prepare script on install, so build/ is no longer a side effect of yarn install. A prepack script keeps consumers that install Arsenal from Git getting a built package, and CI builds explicitly.

Why node-modules and not PnP

PnP is Berry's default: no node_modules, dependencies stay zipped, and a package may only require what it declares. That last property is the interesting one, because it turns every dependency that resolves by luck of hoisting into a hard failure.

It does work here. Flipping nodeLinker to pnp and fixing four things gets a fully green yarn build, all 4433 tests, and yarn lint:

  • packageExtensions for @aws-sdk/lib-storage, whose published .d.ts imports @smithy/types while declaring it only as a devDependency — a recurring shape in that SDK (Missing @aws-sdk/smithy-client dependency in @aws-sdk/lib-storage aws/aws-sdk-js-v3#3626, #5035, #5362).
  • packageExtensions for ts-jest, which requires jest-util unconditionally but declares it as an optional peer.
  • The two changes already in this PR: appending to NODE_OPTIONS instead of overwriting it (PnP passes --require .pnp.cjs through that variable), and declaring @eslint/js / @eslint/eslintrc.

This PR still lands on node-modules, because that keeps today's resolution semantics and makes the change purely a package-manager swap — reviewable and revertible on its own. PnP is a sensible follow-up, and the remaining work is two packageExtensions entries.

Worth knowing before that follow-up: the same experiment on Cloudserver does not come out clean. @scality/cloudserverclient@1.0.12 declares 4 dependencies while its shipped code requires far more; 28 AWS/Smithy packages had to be added by hand before I stopped, and it had not converged. That one needs fixing at the source, not papering over downstream.

Notes for reviewers

  • approvedGitRepositories is required, not hardening. Yarn's default is an empty allowlist that blocks every Git fetch, so a package with Git dependencies cannot install without it. It is scoped to the Scality org rather than the ** Yarn writes during migration.

  • networkConcurrency: 1 replaces Yarn 1's --network-concurrency, which Berry rejects on the CLI but still honours as a setting. Git dependencies that still carry Yarn 1 lockfiles are bootstrapped by Yarn Classic, and concurrent Classic installs race on its shared cache. Droppable once those dependencies are on Berry.

  • npmMinimalAgeGate stays at Yarn's 1-day default, so a dependency update touching a release published in the last 24 hours will fail. @scality/* is preapproved, so our own releases are usable immediately.

  • Consumers are unaffected. Yarn 1 never reads a Git dependency's lockfile — it resolves Arsenal's tree into its own — and Berry consumers do the same.

Comment thread .github/workflows/lint.yaml
Comment thread package.json Outdated
Comment thread .github/workflows/tests.yaml Outdated
Comment thread lib/executables/pensieveCreds/package.json Outdated
@delthas delthas changed the title PoC: migrate Arsenal to Yarn 4 (Berry) Migrate to Yarn 4 (Berry) Sep 18, 2026
@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 74.77%. Comparing base (1ed7c25) to head (0437a71).

Files with missing lines Patch % Lines
lib/storage/data/file/utils.js 50.00% 1 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           development/8.6    #2706      +/-   ##
===================================================
+ Coverage            74.76%   74.77%   +0.01%     
===================================================
  Files                  227      227              
  Lines                18650    18650              
  Branches              3864     3894      +30     
===================================================
+ Hits                 13943    13945       +2     
+ Misses                4702     4700       -2     
  Partials                 5        5              

☔ 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.

@delthas
delthas changed the base branch from development/8.5 to development/8.6 September 22, 2026 13:57
@delthas
delthas force-pushed the wip-yarn-berry branch 3 times, most recently from d7056cc to 7d5797a Compare September 22, 2026 14:39
node-fcntl is now published to npm (scality/node-fcntl#23), so there is
no reason to keep pulling it from GitHub. A registry tarball is
immutable and carries an integrity hash, where the codeload tarball
carried none; nothing else changes, as it is the same code under a
scoped name.

Only the import path differs, in lib/storage/data/file/utils.js. Yarn 1
resolves it identically -- the addon builds and loads as before.
Yarn 1 is unmaintained and its installs have become unreliable; this
moves Arsenal to Yarn 4.18.0.

Yarn is pinned through the packageManager field, with its integrity
hash, rather than by committing a release binary: CI enables Corepack
explicitly, and anyone without it gets a clear error naming the version
to use rather than a silently different Yarn.

nodeLinker is node-modules rather than the default PnP, which cannot
load this package's native addons from inside a zip. It is also what
scality/bench-vault chose.

approvedGitRepositories is required, not optional hardening: Yarn
defaults to an empty allowlist that blocks every Git fetch, so a package
with Git dependencies cannot install without it. Scoped to the Scality
org rather than the "**" Yarn writes during migration.

npmPreapprovedPackages exempts @scality/* from the one-day quarantine
Yarn applies to newly published versions. That guard is aimed at
compromised third-party publishes; our own packages come from our CI, so
waiting a day to consume them buys nothing. Everything else keeps it.

networkConcurrency: 1 replaces Yarn 1's --network-concurrency flag, which
Berry rejects on the CLI but still honours as a setting. Git dependencies
that still carry Yarn 1 lockfiles are bootstrapped with Yarn Classic, and
concurrent Classic installs race on its shared cache. It can be dropped
once those dependencies are themselves on Berry.

Note that Yarn rewrites .yarnrc.yml during the migration step itself,
dropping comments and re-adding permissive security defaults; the file
here is the reviewed version, and it stays put on subsequent installs.
@delthas
delthas force-pushed the wip-yarn-berry branch 2 times, most recently from 4310775 to 031aac6 Compare September 22, 2026 15:13
eslint.config.mjs imports @eslint/js and @eslint/eslintrc, neither of
which is declared. Both resolve today only because ESLint pulls them in
and the node-modules linker hoists them to the top of the tree, so any
stricter resolver breaks linting outright.

They are pinned to the versions already resolved, so this is a no-op at
install time.
Berry disables install/build scripts by default, where Yarn 1 let all
~1100 packages run them. Four genuinely need to compile: @scality/fcntl
and ioctl are node-gyp addons, leveldown ships native code, and
mongodb-memory-server downloads a server binary.

The key must be the real package name. Keying it on an alias makes Berry
silently skip the build (YN0004) -- the install still succeeds and the
addon is simply never compiled.

This is the clearest security improvement in the migration: install-time
code execution goes from every dependency to four named ones.
- name: Install NodeJS
uses: actions/setup-node@v6
with:
node-version: '24'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cache: yarn and cache-dependency-path were removed from this job but kept in every other job. Without them, the test-workflows job re-downloads all dependencies on every CI run.

Suggested change
node-version: '24'
node-version: '24'
cache: yarn
cache-dependency-path: tests/workflows/yarn.lock

Berry runs scripts through its own portable shell, which has no `export`
builtin: both scripts failed immediately with "command not found:
export". An inline environment prefix is equivalent and also valid in
bash, so this works under either package manager.

Append to NODE_OPTIONS rather than replacing it, so that a value set by
the caller survives; Yarn passes `--require .pnp.cjs` through this
variable under the PnP linker, and overwriting it leaves every script
unable to resolve its own dependencies. The `:-` default is required
because that same shell treats an unset variable as an error rather
than as empty, and CI does not set NODE_OPTIONS.
Setting packageManager flips Yarn's Git-dependency bootstrap from the
Yarn Classic branch to the Berry branch. Classic ran `yarn install`,
which executes prepare; Berry runs `yarn pack --install-if-needed`,
which executes prepack and not prepare.

Without prepack, that bootstrap produces a 3-entry, 6969-byte tarball
containing only LICENSE, README and package.json -- no build/ at all --
so every Berry consumer would install an empty Arsenal. Verified on a
pristine `git archive HEAD` checkout; with prepack the same command
produces 701 entries including the full build/ tree.

Cloudserver already pins Yarn 4, so it is such a consumer today.

# Conflicts:
#	package.json

# Conflicts:
#	package.json
- corepack enable has to run before actions/setup-node. Its `cache: yarn`
  probe shells out to Yarn to locate the cache folder, and `yarn cache
  dir` exits 1 once packageManager pins Yarn 4 (actions/setup-node#1027).
- --frozen-lockfile becomes --immutable, and --network-concurrency is
  dropped: Berry rejects it on the CLI and it now lives in .yarnrc.yml.
- Berry removed the global --silent flag, so `yarn --silent X` becomes
  `yarn X`; Berry is quiet by default.
- The tests/workflows job keeps Yarn 1 and therefore drops `cache: yarn`,
  since setup-node probes from the repo root. It also uses
  working-directory rather than `yarn --cwd`: Corepack resolves
  packageManager from the process CWD, so --cwd would pick up the root's
  Yarn 4 and ignore the nested pin.
- Yarn 1 built the package as a side effect of install, via the root
  prepare script; Berry does not run prepare on install. Some tests spawn
  a process that requires build/, so it is now built explicitly.
- release.spec.ts mocks workflow steps by name, so the new Corepack step
  needs an entry or the suite really executes it inside the act image.
This is a separate install root with its own lockfile, so it becomes its
own Berry project rather than a workspace: keeping its dependency tree
isolated from the root avoids any hoisting interaction, and Berry
inherits the root .yarnrc.yml anyway, so only the linker is restated
here.

@kie/act-js and unrs-resolver need to compile, so they are allowlisted
via dependenciesMeta; build scripts are otherwise off by default.

CI enables Corepack for this job and uses --immutable in place of
--frozen-lockfile. Verified locally on Node 24 against a live container
socket: 7/7 suites pass, including the release workflow tests that
really execute the new Corepack step through act.
This directory had a package.json but no lockfile, so Berry treated it
as part of the root project and refused to install ("doesn't seem to be
part of the project declared in ..."). It now carries its own lockfile
and is a separate Berry project, which also means CI stops re-resolving
its dependency ranges on every run.

Since the versions were being pinned for the first time, the 2018-era
ones are refreshed rather than frozen in place:

  node-forge  ^0.7.1 -> ^1.3.1   (matches the root)
  async       ~2.6.1 -> ~2.6.4   (matches the root)
  mocha        5.2.0 -> ^12.0.2  (and moved to devDependencies)

The code only uses pki.privateKeyFromPem, util.decode64, md.sha256 and
async.waterfall, all unchanged across those majors. All 3 tests pass,
including the decryption path that exercises node-forge.
@delthas
delthas force-pushed the wip-yarn-berry branch 2 times, most recently from 0593c98 to 0437a71 Compare September 22, 2026 15:50
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.

1 participant