A CLI tool and library for publishing packages from npm workspaces to registries (npmjs.org, GitHub Packages, etc.). It determines which workspace packages haven't been published yet by checking each package's version against the registry, and publishes only what's needed — making it ideal for CI/CD pipelines alongside release-please.
When npm >= 10.0.0 is available, it shells out to npm publish directly (supporting OIDC, provenance, etc. out of the box).
Otherwise it falls back to libnpmpublish / libnpmpack.
Table of Contents
- Node.js >= 22.0.0
- npm 7+ (workspace support required)
npm install monoship --save-devnpx monoship \
--token <token> \
--registry <registry> \
--root <root> \
--rootPackage| Option | Type | Default | Description |
|---|---|---|---|
--token <token> |
string |
NODE_AUTH_TOKEN env var |
Token for the registry. Optional when using OIDC trusted publishing. |
--registry <registry> |
string |
https://registry.npmjs.org/ |
Registry URL to publish to. |
--root <root> |
string |
process.cwd() |
Directory where the root package.json is located. |
--rootPackage |
boolean |
true |
Also consider the root package for publishing (skipped if private: true or missing name/version). |
The tool supports three authentication methods, resolved in the following order:
--tokenCLI flag — Explicit npm access token, used as-is.- OIDC Trusted Publishing — Tokenless publishing via GitHub Actions OIDC (auto-detected when no
--tokenflag is given). Falls back toNODE_AUTH_TOKENif OIDC fails. NODE_AUTH_TOKENenvironment variable — Default fallback.
When running in GitHub Actions with trusted publishers configured, the tool automatically detects the OIDC environment and exchanges short-lived, per-package tokens with the npm registry — no long-lived NPM_TOKEN secret required.
Requirements:
- npm trusted publisher configured for each package on npmjs.com
- GitHub Actions workflow with
id-token: writepermission - No
--tokenflag set (OIDC is bypassed when an explicit token is provided)
How it works:
- Detects
ACTIONS_ID_TOKEN_REQUEST_URLandACTIONS_ID_TOKEN_REQUEST_TOKENenvironment variables - Requests an OIDC identity token from GitHub with audience
npm:<registry-host> - Exchanges the identity token with the npm registry for a short-lived, package-scoped publish token
- Uses that token for publishing (each package gets its own scoped token)
If OIDC token exchange fails for a package, it falls back to NODE_AUTH_TOKEN automatically via the chain provider.
monoship is also available as a GitHub Action:
- uses: tada5hi/monoship@v2
with:
token: ${{ secrets.NPM_TOKEN }}Or with OIDC trusted publishing (no token needed):
- uses: tada5hi/monoship@v2| Input | Required | Default | Description |
|---|---|---|---|
token |
No | — | npm auth token. Optional when using OIDC trusted publishing. |
registry |
No | https://registry.npmjs.org/ |
Registry URL to publish to. |
root-package |
No | true |
Also consider the root package for publishing. |
dry-run |
No | false |
Show what would be published without actually publishing. |
import { publish } from 'monoship';
const packages = await publish({
cwd: '/path/to/monorepo',
registry: 'https://registry.npmjs.org/',
token: 'npm_...',
rootPackage: true,
dryRun: false,
});The publish() function returns an array of Package objects for each successfully published package.
| Option | Type | Default | Description |
|---|---|---|---|
cwd |
string |
process.cwd() |
Root directory of the monorepo. |
registry |
string |
https://registry.npmjs.org/ |
Registry URL. |
token |
string |
— | Auth token (wrapped in MemoryTokenProvider internally). |
rootPackage |
boolean |
true |
Include the root package as a publish candidate. |
dryRun |
boolean |
false |
Resolve dependencies and check versions without actually publishing. |
fileSystem |
IFileSystem |
NodeFileSystem |
File system adapter. |
registryClient |
IRegistryClient |
HapicRegistryClient |
Registry metadata adapter. |
publisher |
IPackagePublisher |
Auto-detected | Publisher adapter (npm CLI or libnpmpublish). |
tokenProvider |
ITokenProvider |
EnvTokenProvider |
Token resolution adapter (overrides token). |
logger |
ILogger |
— | Logger adapter. |
The library uses a hexagonal architecture — all external I/O is behind port interfaces, making it fully testable and extensible:
import {
publish,
MemoryFileSystem,
MemoryRegistryClient,
MemoryPublisher,
MemoryTokenProvider,
NoopLogger,
} from 'monoship';
const packages = await publish({
cwd: '/project',
fileSystem: new MemoryFileSystem({ /* virtual files */ }),
registryClient: new MemoryRegistryClient({ /* virtual packuments */ }),
publisher: new MemoryPublisher(),
tokenProvider: new MemoryTokenProvider('test-token'),
logger: new NoopLogger(),
});Available port interfaces and their adapters:
| Port | Real Adapters | Test Adapter |
|---|---|---|
IFileSystem |
NodeFileSystem |
MemoryFileSystem |
IRegistryClient |
HapicRegistryClient |
MemoryRegistryClient |
IPackagePublisher |
NpmCliPublisher, NpmPublisher |
MemoryPublisher |
ITokenProvider |
MemoryTokenProvider, EnvTokenProvider, OidcTokenProvider, ChainTokenProvider |
MemoryTokenProvider |
ILogger |
ConsolaLogger |
NoopLogger |
Use with release-please — it bumps versions and creates release PRs, then monoship handles the actual publishing:
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
if: steps.release.outputs.releases_created == 'true'
uses: actions/checkout@v4
- name: Publish
if: steps.release.outputs.releases_created == 'true'
uses: tada5hi/monoship@v2
with:
token: ${{ secrets.NPM_TOKEN }}No npm token secrets needed — configure trusted publishers on npmjs.com for each package instead:
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
id-token: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
if: steps.release.outputs.releases_created == 'true'
uses: actions/checkout@v4
- name: Publish
if: steps.release.outputs.releases_created == 'true'
uses: tada5hi/monoship@v2Made with 💚
Published under MIT.