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.
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.
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.
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 describes the full publish process.
- Smoke-Testing a Tarball with npm pack installs the tarball to prove it works.
- Choosing Between prepare, prepack and prepublishOnly explains the scripts a dry run executes.
- Release Channels and Dist-Tags covers the tag decisions a dry run reveals.