Back to publishing & release Automate semantic versioning Manage release channels Harden the supply chain

Dry-Running a Publish Before Release

A publish is irreversible in practice: a version number, once used, can never be reused, and a broken release reaches consumers the moment it lands. A dry run lets you see exactly what would be published — the files, the version, the registry, the dist-tag, the access level and the lifecycle scripts that run — without uploading anything. Combined with a few assertions, it turns release mistakes into failed CI jobs instead of deprecated versions. This guide covers npm publish --dry-run and its equivalents in pnpm, Yarn and release tools, what to check in the output, and how to automate those checks.

What a dry run shows

npm publish --dry-run
> @acme/sdk@2.4.0 prepack
> npm run build

npm notice
npm notice 📦  @acme/sdk@2.4.0
npm notice Tarball Contents
npm notice 1.1kB LICENSE
npm notice 3.2kB README.md
npm notice 21.4kB dist/index.cjs
npm notice 20.9kB dist/index.js
npm notice 8.1kB dist/index.d.cts
npm notice 8.1kB dist/index.d.ts
npm notice 1.6kB package.json
npm notice Tarball Details
npm notice name: @acme/sdk
npm notice version: 2.4.0
npm notice filename: acme-sdk-2.4.0.tgz
npm notice package size: 17.9 kB
npm notice unpacked size: 64.4 kB
npm notice total files: 7
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access (dry-run)

Every line answers a release question: did the build run (lifecycle output), are the right files included, is the version the one you meant, how big is the package, which registry, which dist-tag, public or restricted. The overall release flow is covered in npm Registry Publishing Workflows.

What npm publish --dry-run executes and skips The dry run runs prepublishOnly, prepack, prepare and postpack, packs the tarball in memory, resolves registry and tag, and stops before uploading. lifecycle scripts prepublishOnly, prepack, prepare pack tarball files, sizes, integrity resolve target registry, dist-tag, access stop no upload, no version consumed
Everything except the upload runs, so the output reflects the real release — including your build scripts.

Because lifecycle scripts run, a dry run also catches build failures that would otherwise abort a real publish midway through a multi-package release.

The release checklist a dry run answers

Question Where to look
Is the version correct and unpublished? version: line; compare with npm view <name> versions
Are the build outputs included? Tarball Contents lists every exports, main, types and bin target
Did anything unwanted slip in? no src/, tests, fixtures, .env, coverage
Is the size reasonable? unpacked size compared with the previous release
Right registry? Publishing to <url>
Right dist-tag? with tag latest — prereleases must not say latest
Right access? public access for public scoped packages

The file-level checks are covered in depth in Smoke-Testing a Tarball with npm pack.

Dry runs in other tools

# pnpm (also rewrites workspace: and catalog: ranges as a real publish would)
pnpm publish --dry-run --no-git-checks

# pnpm, every public package in a workspace
pnpm -r publish --dry-run --no-git-checks

# Yarn Berry: inspect the packed contents
yarn pack --dry-run

# Changesets: show what would be versioned and published
pnpm changeset status --verbose
pnpm changeset version --snapshot preview   # optional, on a throwaway branch

# semantic-release
npx semantic-release --dry-run --no-ci

# Lerna
npx lerna publish --no-push --no-git-tag-version --dist-tag next --yes --registry http://localhost:4873

The Lerna line is not a dry run but a common alternative: publish to a local Verdaccio registry to rehearse the whole flow. Rehearsing against a private registry is covered in Setting Up Verdaccio as a Private Proxy Registry.

Rehearsal options before a release Compares npm publish --dry-run, pnpm pack inspection, release-tool dry runs and publishing to a local Verdaccio on what they verify. contents version / tag installability npm publish --dry-run yes yes no pnpm publish --dry-run yes (ranges rewritten) yes no changeset status / semantic-release --dry-run no planned versions no publish to local Verdaccio yes yes install from it
Dry runs verify contents and targets; a local registry rehearsal also verifies install and dependency resolution.

Automating the checks

Human review of dry-run output does not scale to monorepos or frequent releases. The JSON output of npm pack --dry-run --json makes the checks scriptable, and npm publish --dry-run output can be parsed for the registry and tag:

#!/usr/bin/env bash
set -euo pipefail
name=$(node -p "require('./package.json').name")
version=$(node -p "require('./package.json').version")

# 1. Version must not already exist
if npm view "$name@$version" version >/dev/null 2>&1; then
  echo "✗ $name@$version is already published"; exit 1
fi

# 2. Prereleases must not publish to latest
out=$(npm publish --dry-run 2>&1)
if [[ "$version" == *-* ]] && grep -q "with tag latest" <<<"$out"; then
  echo "✗ prerelease $version would be tagged latest; pass --tag next"; exit 1
fi

# 3. Required entry files must be in the tarball
npm pack --dry-run --json | node -e '
  const [info] = JSON.parse(require("fs").readFileSync(0, "utf8"));
  const files = new Set(info.files.map(f => f.path));
  const pkg = require("./package.json");
  const want = [pkg.main, pkg.types].filter(Boolean).map(p => p.replace(/^\.\//, ""));
  const missing = want.filter(f => !files.has(f));
  if (missing.length) { console.error("✗ missing", missing); process.exit(1); }
  console.log("✓ entry files present;", info.unpackedSize, "bytes unpacked");
'
echo "✓ dry run checks passed for $name@$version"

Run it in the release job before the real publish, for each package being released. Keep the script in the repository rather than inline in the workflow file, so it can be run locally before tagging a release and so its checks evolve with the packages.

Rehearsing a monorepo release

Monorepo releases publish several packages in one run, which multiplies the ways a release can go wrong: one package fails its build halfway through, internal ranges are not rewritten, or a package that should stay private is included. Rehearse the whole set, not just one package.

Rehearsing a multi-package release Compute the planned versions, dry-run publish each package, assert contents and tags, optionally publish to a local registry and install, then run the real release. planned versions changeset status / semantic-release dry run which packages, which bumps dry-run each package pnpm -r publish --dry-run assert contents + tags scripted checks per package local registry rehearsal publish to Verdaccio, install optional, for big releases real release same commit, same tarballs
Rehearse every package in the release together, because failures often come from interactions between them.

Two monorepo-specific checks deserve a place in the scripted assertions. First, confirm that no packed manifest contains workspace: or catalog: ranges — the leak described in Fixing 'Unsupported URL Type workspace:' After Publishing. Second, confirm that every internal dependency named in a packed manifest is either already on the registry at a satisfying version or also part of this release; otherwise consumers will fail to install. A few lines of script over the dry-run tarballs cover both.

What a dry run cannot catch

A dry run is a rehearsal of the publish step, not of the release as consumers experience it. It does not prove the package installs and imports correctly — that is the job of a tarball smoke test. It does not check authentication beyond what configuration reveals, so a revoked token or an OIDC mismatch appears only on the real publish. It does not verify registry-side policies such as required 2FA, provenance requirements or organisation publishing restrictions. And it cannot tell you whether the version you are about to publish follows semantic versioning — only the API report and your changelog can. Treat the dry run as one gate in a sequence: build, validate the package, dry-run, authenticate, publish, then verify on the registry with npm view and a fresh install.

Worked example: a prerelease that almost became latest

A maintainer prepares 3.0.0-rc.1 and runs the release workflow. The dry-run step's check fails: prerelease 3.0.0-rc.1 would be tagged latest; pass --tag next. The workflow's publish command lacked --tag, and recent npm versions refuse such a publish outright, but older npm versions in some CI images would have made every npm install @acme/sdk pull the release candidate. The workflow is fixed to derive the tag from the version, and the release goes out as next. Recovering from a wrong tag after the fact is covered in Fixing a Wrong latest Dist-Tag.

Prevention and CI/CD guardrails

  • Run a dry run in every release job before the real publish.
  • Fail on already-published versions, wrong tags and missing entry files with scripted checks.
  • Compare size with the previous release and flag large jumps.
  • Publish the same tarball you dry-ran, built once in the same job.

Frequently Asked Questions

Does --dry-run contact the registry? It resolves configuration and may fetch metadata, but it does not upload or reserve the version. Authentication problems can still surface only on the real publish, so pair it with npm whoami --registry <url>.

Do lifecycle scripts run during a dry run? Yes. prepublishOnly, prepack, prepare and postpack all run, which is why the dry run reflects the real build.

Is npm pack --dry-run the same thing? It shows the same file list but not the registry, tag or access decisions, and does not run prepublishOnly. Use both: pack for file assertions, publish dry-run for target checks.

Should dry runs run on every pull request or only on release? Running them on pull requests that change package.json, build configuration or entry points catches problems before they reach the release branch. They are quick, so many teams run the pack-level assertions on every pull request and the full publish dry run only in the release job.

Why does my dry run pass but the real publish fail? Usually authentication, registry policy or a version collision caused by another release that happened in between. Check npm whoami, the package's publishing settings, and whether the version appeared on the registry after the dry run.

Can a dry run modify my repository? Lifecycle scripts run, so builds write to dist/ and any script that edits files will do so. The publish itself changes nothing on the registry, and npm does not create git tags or commits during a dry run.

Related

npm Registry Publishing Workflows