[release] harden release script: yarn, public access, resilient publish, pin Node#1712
Conversation
The release script ran `npm install`, which generated a stray `package-lock.json` (and required `.npmrc` legacy-peer-deps) even though the repo uses yarn — CI installs with `yarn install --frozen-lockfile`. `git add .` then committed those npm artifacts. - Install with `yarn install` instead of `npm install`. - Stage only the workspace manifests + `yarn.lock` instead of `git add .`, so stray/unrelated files can never be committed by a release again. Verified: running this on main for 0.19.0 produces package manifests identical to the release/0.19.0 branch, with no package-lock.json/.npmrc.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Updates the monorepo release script to use Yarn (the repo’s configured package manager) and to stage only expected release artifacts, preventing stray files like package-lock.json from being accidentally committed in release commits.
Changes:
- Switch dependency installation in the release script from
npm installtoyarn install. - Replace
git add .with explicit staging of workspacepackage.jsonfiles plusyarn.lock.
| const manifestPaths = workspaces.map(({ packageJsonPath }) => | ||
| path.relative(repoRoot, packageJsonPath), | ||
| ); | ||
| execSync(`git add ${manifestPaths.map((p) => `"${p}"`).join(' ')} yarn.lock`, { | ||
| stdio: 'inherit', | ||
| }); |
workflow: benchmarks/perfComparison of performance test results, measured in operations per second. Larger is better.
|
workflow: benchmarks/sizeComparison of minified (terser) and compressed (brotli) size results, measured in bytes. Smaller is better.
|
Follow-on fixes to the release script surfaced while cutting 0.19.0: - Publish with `npm publish --access public`. New scoped packages (@stylexjs/*) default to restricted access and fail with E402 on first publish; all stylex packages are public. No-op for already-public ones. - Wrap each `npm publish` in try/catch so one failure (e.g. a package the user lacks publish rights for, like the unscoped `create-stylex-app`) logs and is skipped instead of aborting the whole release halfway. - Add `.nvmrc` pinned to Node 22 (matches CI `setup-node` 22.x). Node 26 breaks the `gen-types`/yargs build toolchain.
Address review + CodeQL feedback on the release script: - Track publish failures and set `process.exitCode = 1` at the end, so a release that skipped packages isn't reported as successful. Uses exitCode (not exit) so every package is still attempted first. - Replace all shell-interpolated `execSync` calls with `execFileSync` arg arrays (no shell). Fixes CodeQL "shell command built from environment values" — publish now runs via `cwd: directory` instead of `cd <dir> &&`, and npm/git args are passed as arrays. Only the static `yarn install` remains an execSync. - Declare `glob` in root devDependencies; the script requires it but it was only resolving via hoisting (caused "Cannot find module 'glob'").
Hardens the release script (
tools/npm/release.js) and pins the Node version. All fixes surfaced while cutting0.19.0.Fixes
Install with yarn, not npm. The script ran
npm install, which regenerates apackage-lock.json(and needs.npmrc legacy-peer-deps) even though the repo uses yarn (yarn install --frozen-lockfilein CI).git add .then committed those npm artifacts — that's how the ~35k-linepackage-lock.json+.npmrcended up in the 0.19.0 release commit.Scope the staging. Stage only the workspace manifests +
yarn.lockinstead ofgit add ., so a release can never commit stray/unrelated files again.Publish with
--access public. New scoped packages (@stylexjs/*) default to restricted access and fail on first publish with402 Payment Required. All stylex packages are public; no-op for already-public ones. (Hit this on@stylexjs/atoms, the new package in 0.19.0.)Resilient publish loop. Each publish is wrapped in try/catch, so a single failure — e.g. a package the publisher lacks rights for, like the unscoped
create-stylex-app(403) — logs and is skipped instead of aborting the release halfway. Failures are collected and the script setsprocess.exitCode = 1at the end, so a release that skipped packages isn't reported as success (usesexitCode, notexit, so every package is still attempted first).No shell-built commands (CodeQL). All variable-interpolating
execSynccalls are replaced withexecFileSyncarg arrays (no shell): publish runs viacwd: directoryinstead ofcd <dir> && ..., and npm/git args are passed as arrays. Resolves the "shell command built from environment values" alert. Only the staticyarn installremainsexecSync.Declare
glob. The scriptrequiresglobbut it was only resolving via hoisting (Cannot find module 'glob'on a clean checkout). Added to root devDependencies..nvmrcpinned to Node 22 (matches CIsetup-node: 22.x). Node 26 breaks thegen-types/yargsbuild toolchain (require is not defined in ES module scope), cascading into every dependent package's build.Verification
mainfor0.19.0: resulting manifests are byte-for-byte identical to therelease/0.19.0branch, with nopackage-lock.json/.npmrcproduced.node --checkpasses; fixes 3 & 4 validated live during the 0.19.0 publish.