Skip to content

WIP PoC: migrate Cloudserver to Yarn 4 (Berry) - #6293

Draft
delthas wants to merge 6 commits into
development/9.5from
wip-yarn-berry
Draft

delthas wants to merge 6 commits into
development/9.5from
wip-yarn-berry

Conversation

@delthas

@delthas delthas commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Draft / WIP — not for merge. Second PoC for ZENKO-4051 (open since 2022), after Arsenal#2706. Cloudserver was chosen because it exercises what a library could not: a Docker image build, a production install, and 7 Git dependencies.

Measured: Berry is ~2x faster for developers

Same machine, same cold/warm state, both serialised (Yarn 1 already used --network-concurrency 1 everywhere):

cold warm
Yarn 1 365 s 57 s
Berry 171 s 26 s

The warm number is the day-to-day one — every branch switch or dependency change. There is further upside: Berry at default concurrency was 107 s cold, so once the dependency repos are themselves on Berry, cold approaches 3.4x.

Security posture improves

Berry disables build scripts by default. The 12 packages that genuinely need compiling are allowlisted in dependenciesMeta; under Yarn 1 every package may run install scripts.

yarn install silently wrote npmMinimalAgeGate: 0 and approvedGitRepositories: ["**"] into .yarnrc.yml during migration — both reverted. Re-read that file after migrating, before committing.

What Docker forced us to learn

  1. yarn install --production exits 1 under Berry — not deprecated-but-working, broken. Replacement is yarn workspaces focus --production (no plugin needed).
  2. workspaces focus cannot enforce lockfile immutability. Verified: with a stale lockfile and enableImmutableInstalls: true it still exits 0 and installs the drift. A preceding yarn install --immutable --mode=skip-build restores what --frozen-lockfile gave us, without building anything.
  3. yarn global add was removed; nyc now comes from npm.
  4. networkConcurrency: 1 is load-bearing. Dropping Yarn 1's flag as obsolete was wrong. Git dependencies that still carry Yarn 1 lockfiles get bootstrapped by Yarn Classic, and concurrent Classic installs corrupt each other on its shared cache (YN0058, ENOENT while unpacking). This only reproduces in Docker. Berry rejects the CLI flag but honours the setting. It can be dropped once those repos move to Berry.

Checksums

Of 17 Git-sourced lockfile entries, exactly one is unstable across a cold reinstall: fcntl. It is node-gyp-built, so Yarn repacks it and the archive is not byte-reproducible. Deps that merely run tsc in prepare (arsenal, scubaclient, utapi) are perfectly stable.

Its checksum is therefore omitted. This is not durable — Yarn re-adds one on every install, so any developer who commits after installing reintroduces a machine-specific hash. The real fix is to publish scality/node-fcntl to npm as fcntl@0.3.0: the repo is public and Apache-2.0, the bare name is unclaimed, and package.json already declares name: "fcntl", so consumers change one line with no code change and no registry auth. Only Arsenal and Backbeat consume it.

State

Verified locally: install, --immutable, Docker image build (--target builder), eslint (0 errors), prettier. The test suite has not been run locally — CI is the check for that.

DockerfileMem untouched: FROM node:6-slim, unreferenced anywhere, last modified 2022, and Berry needs Node 18+.

Same shape as the Arsenal PoC: Yarn pinned via packageManager (with its
integrity hash) rather than a committed release binary.

- .yarnrc.yml: nodeLinker node-modules, Scality-scoped
  approvedGitRepositories, and networkConcurrency: 1 (Git dependencies
  with Yarn 1 lockfiles are bootstrapped by Yarn Classic, which races on
  its shared cache when run in parallel -- see YN0058 in Docker builds).
  npmMinimalAgeGate is left at Yarn's 1-day default.
- Build scripts stay disabled globally; the 12 packages that need
  compiling are allowlisted via dependenciesMeta. Yarn 1 by contrast
  lets every package run install scripts.
- fcntl carries no checksum: it is a Git dependency with a node-gyp
  build, so Yarn repacks it and the archive is not reproducible. Of the
  17 Git-sourced entries it is the only unstable one -- publishing
  scality/node-fcntl to npm would remove the need for this.
- corepack enable must precede actions/setup-node: its cache: yarn probe
  shells out to Yarn to find the cache folder, and Yarn 1 refuses once
  packageManager is set (actions/setup-node#1027).
- --frozen-lockfile -> --immutable, and drop --network-concurrency
  (rejected on the CLI; now a .yarnrc.yml setting).
- Berry removed the global --silent flag, so 'yarn run --silent X'
  becomes 'yarn X'; Berry is quiet by default.
- yarn install --production exits 1 under Berry; the replacement is
  yarn workspaces focus --production, which needs no plugin.
- workspaces focus cannot enforce lockfile immutability -- verified: with
  a deliberately stale lockfile and enableImmutableInstalls it still
  exits 0 and installs the drift. A preceding
  'yarn install --immutable --mode=skip-build' restores that guarantee
  cheaply, since it resolves without building.
- yarn global add was removed in Berry; nyc now comes from npm, matching
  the node-gyp/typescript installs already in this stage.
- .yarnrc.yml has to be COPYed in for any of the above to apply.

DockerfileMem is left alone: it is FROM node:6-slim, unreferenced
anywhere, and last touched in 2022 -- Berry needs Node 18+.
Comment thread .github/workflows/tests.yaml Outdated
@@ -99,7 +101,7 @@ jobs:
run: yarn global add typescript@4.9.5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yarn global add was removed in Yarn 4 — this will error out. The Dockerfile correctly switched nyc to npm install -g; same treatment needed here. Same issue at line 175.

Suggested change
run: yarn global add typescript@4.9.5
run: npm install -g typescript@4.9.5

Comment thread .github/workflows/tests.yaml Outdated
@@ -169,7 +175,7 @@ jobs:
run: yarn global add typescript@4.9.5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same yarn global add issue as line 101 — needs npm install -g.

Suggested change
run: yarn global add typescript@4.9.5
run: npm install -g typescript@4.9.5

Comment thread package.json
"prettier": "prettier",
"prettier:diff": "bash scripts/prettier-diff.sh"
},
"packageManager": "yarn@4.18.0+sha512.fcb8716fe7cd0eece141ffc18b92193a9df9204c1ba83189c288835223fc0bbe64af473bab0d5e9927a7daeb5caf2bb07eb2787cc9338ca040ea125f2a1f2f7e",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yarn Berry doesn't auto-run pre/post lifecycle scripts for custom scripts. The postcover script (line 167: nyc report --report-dir ./coverage/test --reporter=lcov) won't execute after yarn run cover, silently breaking the coverage pipeline in CI. Either fold the report command into cover or call postcover explicitly in the CI workflow after cover.

Comment thread Dockerfile
# `workspaces focus` cannot enforce lockfile immutability (it silently
# re-resolves), so validate the lockfile first with a cheap build-less
# install. Together these preserve what --frozen-lockfile used to give us.
RUN corepack enable \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

corepack enable runs only in the builder stage. The production stage (CMD [ "yarn", "start" ] at line 64) inherits from node:${NODE_VERSION} directly — yarn there falls back to the globally pre-installed Yarn 1, not Yarn 4. This works today but is fragile (Node images may drop bundled Yarn 1 in favor of corepack). Consider adding RUN corepack enable to the production stage, or switching the CMD to node index.js directly.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.54%. Comparing base (ca73322) to head (4a20cc2).
⚠️ Report is 2 commits behind head on development/9.5.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@               Coverage Diff                @@
##           development/9.5    #6293   +/-   ##
================================================
  Coverage            86.54%   86.54%           
================================================
  Files                  213      213           
  Lines                14615    14615           
================================================
  Hits                 12649    12649           
  Misses                1966     1966           
Flag Coverage Δ
checksums-disabled-tests 35.38% <ø> (ø)
file-ft-tests 70.03% <ø> (-0.05%) ⬇️
file-ft-tests-null-compat 70.55% <ø> (ø)
kmip-ft-tests 28.14% <ø> (ø)
mongo-v0-ft-tests 71.19% <ø> (ø)
mongo-v1-ft-tests 71.13% <ø> (+0.01%) ⬆️
multiple-backend 36.13% <ø> (ø)
s3c-ft-tests-v0 65.04% <ø> (ø)
s3c-ft-tests-v1 65.02% <ø> (ø)
sur-tests 36.66% <ø> (-0.87%) ⬇️
sur-tests-inflights 39.46% <ø> (ø)
unit 74.29% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

- yarn global add was removed in Berry, and CI still used it for
  typescript in two jobs (tests.yaml:101 and :175). This is why every
  test job failed within ~20s. Now npm install -g, matching the
  Dockerfile.

- Berry does not run pre/post lifecycle scripts for custom scripts.
  Verified: with pre/main/post defined, Yarn 1 runs all three and Berry
  runs only the main one. postcover writes the lcov report, so coverage
  was silently not being produced; CI now calls it explicitly.

- The production image would not have started. It does COPY . (so it has
  package.json with packageManager: yarn@4.18.0) and runs
  CMD ["yarn","start"], but Corepack was only enabled in the builder
  stage, so the image's bundled Yarn 1 refused to run at all. Verified in
  a node:22.14.0-bookworm-slim container both ways: it fails without
  corepack enable and prints the script output with it, including
  against production-pruned node_modules.
.github/actions/setup-ci is used by 11 of the 14 test jobs and was never
migrated -- I had only grepped .github/workflows. It had no corepack
enable, used 'yarn global add', and passed --ignore-engines,
--frozen-lockfile and --network-concurrency, all of which Berry rejects.
This is why every functional-test job failed.

tests/functional/{report,raw-node,healthchecks} have a package.json but
are not workspaces, so Berry hard-errors on 'cd X && yarn test' ('the
nearest package directory doesn't seem to be part of the project').
That broke ft_management, ft_node, ft_node_routes, ft_gcp and
ft_healthchecks. They now invoke mocha directly, like the 20 sibling
ft_* scripts already do.

The production image enabled Corepack but did not install Yarn, so every
container start fetched the CLI over the network -- fatal for air-gapped
deployments. Yarn is now baked in, with COREPACK_HOME shared and
world-readable so images/federation (which runs as 'scality') can use
it. Verified offline as a non-root user.

Also: drop aws-sdk, es5-ext, level and protobufjs from the build
allowlist (their install scripts only print notices), disable telemetry,
and add the missing !.yarn/releases gitignore exception.
fetch-depth: 0
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing second corepack enable after setup-node. The setup-ci/action.yaml correctly runs corepack enable twice — before and after setup-node — with a comment: "setup-node installs its own Node, so re-enable Corepack against it." Without it, yarn resolves to the runner's pre-installed Yarn 1, which doesn't understand --immutable.

Same issue in the async-migration-report (line 138), unit-tests (line 160), and cleanup-gcp-buckets (line 1183) jobs.

fetch-depth: 0
- name: Enable Corepack
run: corepack enable
- uses: actions/setup-node@v6

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same issue as in tests.yaml: missing second corepack enable after setup-node. The setup-ci/action.yaml runs it twice because setup-node installs its own Node, invalidating the earlier shims. Without the second call, yarn falls back to the runner's global Yarn 1.

Comment thread package.json
"ft_checksums_disabled": "cd tests/functional/checksumsDisabled && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 120000 *.js --exit",
"ft_mixed_bucket_format_version": "cd tests/functional/metadata && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json MixedVersionFormat.js --exit",
"ft_management": "cd tests/functional/report && yarn test",
"ft_management": "cd tests/functional/report && mocha -t 40000 *.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing --exit flag. Every other functional test script uses --exit to prevent CI hangs from dangling handles.

Suggested change
"ft_management": "cd tests/functional/report && mocha -t 40000 *.js",
"ft_management": "cd tests/functional/report && mocha -t 40000 *.js --exit",

…ists

Telemetry was disabled on review advice, but that was not asked for and
Berry's default is fine here.

approvedGitRepositories is not optional hardening as the comment implied:
Yarn's default is an empty allowlist, which blocks every Git fetch.
Verified by removing it -- the install fails with YN0080 on fcntl. The
real choice is only between Yarn's migration-written "**" and a scoped
list. All 11 Git resolutions here use the .git clone form, which is the
path that enforces the allowlist, so scoping it to Scality is meaningful
rather than decorative.
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