Files
upstream-rocketchat/docs/npm-publishing.md
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

145 lines
6.9 KiB
Markdown

# npm publishing
The public `@rocket.chat/*` packages under `packages/` are published to npm by
`.github/workflows/release.yml`, which delegates to the local `packages/release-action`.
Everything else in the workspace is `private: true` and never published.
## How a publish happens
`release.yml` is the **only** workflow that publishes. It carries three triggers and maps each one
to a `release-action` mode:
| Trigger | `action` | Publishes? | dist-tag |
| ----------------------------- | --------------- | ---------- | --------------- |
| `push` to `master` | `publish-final` | yes | `latest` |
| `schedule` (monthly, 20th) | `next` | yes | `rc` |
| `workflow_dispatch` → `cut` | `cut` | yes | `latest` / `rc` |
| `workflow_dispatch` → `next` | `next` | yes | `rc` |
| `workflow_dispatch` → `patch` | `patch` | no | — |
`publishRelease.ts` and `bumpNextVersion.ts` both end with `yarn changeset publish --no-git-tag`.
Changesets then spawns `npm publish <packageDir> --access public --tag <tag>` from the repo root, so
**the npm CLI on `PATH` is what authenticates** — not yarn.
## Trusted publishing (OIDC)
Publishing authenticates via [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/)
rather than a long-lived token. GitHub Actions mints a short-lived, job-scoped credential at publish
time, and npm attaches a provenance attestation to every published tarball.
Two hard requirements, both handled in `release.yml`:
- `permissions: id-token: write` on the publishing job — without it the runner never exposes
`ACTIONS_ID_TOKEN_REQUEST_URL` and the OIDC exchange cannot happen.
- **npm >= 11.5.1** (the `Setup npm` step installs it). `engines.node` is 22.x, which bundles npm 10.
### How `NPM_TOKEN` interacts with the exchange
`npm publish` runs the OIDC exchange *before* it reads credentials, and on success it overrides the
`~/.npmrc` auth line with the freshly minted token. A present `NPM_TOKEN` therefore does **not**
prevent trusted publishing — the exchange still wins whenever it succeeds.
The token matters only when the exchange fails. npm's `oidc()` is written to never throw: every
failure path (missing `id-token` permission, package not registered, exchange rejected) returns
quietly, and `publish` then proceeds with the npmrc token — no provenance, no error, exit code 0.
That makes the token a **migration aid and a hazard at the same time**:
- While packages are being registered one at a time, it keeps releases from hard-failing on the
ones that are not registered yet.
- Once everything is registered, it hides regressions — a broken registration keeps publishing
under token auth instead of failing.
So `release.yml` keeps `NPM_TOKEN` only until every package publishes with provenance, then drops
both references (the `setup-node` input and the `Release` step's `env`).
### Migrating packages incrementally
npm allows one trusted publisher per package, so packages move over one at a time:
1. Register the package (see below). Watch for packages previously released from another repo —
`@rocket.chat/emitter`'s last provenance came from `RocketChat/fuselage`'s `cd.yml`, and that
registration has to be re-pointed here before this workflow can publish it.
2. Let a release run. The job log is not evidence — a token fallback looks identical to success.
3. Confirm with `npm view @rocket.chat/<name> --json dist.attestations`; non-null means the package
is publishing through OIDC.
4. When all published packages are attested, remove `NPM_TOKEN` from `release.yml`.
### Why there is only one release workflow
npm allows **one trusted publisher per package**, and it validates the _entry-point_ workflow
filename — the `workflow_ref` OIDC claim. `workflow_call` reusable workflows inherit the caller's
`workflow_ref`, so splitting the publish step into a shared workflow does not help: every caller
would need its own registration. Hence the single `release.yml` with three triggers.
**Renaming or moving `release.yml` breaks publishing** until every package's trusted publisher is
updated on npmjs.com.
### Registering a newly published package
npm cannot configure a trusted publisher for a package that does not exist yet, so a brand-new
package needs one manual first publish before it can be registered.
Register from a machine with npm >= 11.15.0 and account-level 2FA (granular tokens with the
bypass-2FA option are rejected):
```sh
npm trust github @rocket.chat/<name> \
--file release.yml \
--repo RocketChat/Rocket.Chat \
--allow-publish
npm trust list @rocket.chat/<name>
```
Or on `npmjs.com/package/@rocket.chat/<name>/access`:
| Field | Value |
| ----------------- | -------------------------------------- |
| Provider | GitHub Actions |
| Organization | `RocketChat` |
| Repository | `Rocket.Chat` |
| Workflow filename | `release.yml` (filename only, no path) |
| Environment | _(blank)_ |
| Allowed actions | `npm publish` |
No GitHub Environment is configured: the monthly RC cron and the push-to-master final release both
run unattended, and an environment with required reviewers would stall them.
### Provenance requires a correct `repository` field
npm generates provenance automatically for public packages published from a public repo via OIDC,
and **rejects the publish (422) when `repository.url` does not match the repository the workflow ran
in**. Every published package therefore needs:
```json
"repository": {
"type": "git",
"url": "git+https://github.com/RocketChat/Rocket.Chat.git",
"directory": "packages/<name>"
}
```
The org/repo casing (`RocketChat/Rocket.Chat`) has to match too.
Verify after a release:
```sh
npm view @rocket.chat/<name> --json dist.attestations
```
A non-null result is proof the publish went through OIDC — token-based publishes from this repo
never carried provenance.
## Troubleshooting
- **`E404` / `ENEEDAUTH` on `/-/npm/v1/oidc/token/exchange/package/…`** — almost always a
trusted-publisher mismatch: wrong repository, or the workflow filename entered with a path
(`.github/workflows/release.yml`) instead of bare `release.yml`.
- **`E401` / `ENEEDAUTH` with no token configured** — the exchange failed and there was nothing to
fall back to. Run with `--loglevel verbose`: `oidc()` logs its reason (`Skipped because incorrect
permissions`, `Failed token exchange request…`) instead of throwing.
- **422 on publish** — provenance mismatch; check the package's `repository` field.
- **Silent fallback to token auth** — if a valid token is present npm may authenticate with it
instead. Confirm via `dist.attestations` rather than the job log.