Skip to content

Implement staged publish - #6459

Open
mifi wants to merge 3 commits into
mainfrom
staged-publish
Open

Implement staged publish#6459
mifi wants to merge 3 commits into
mainfrom
staged-publish

Conversation

@mifi

@mifi mifi commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

TLDR: We should probably wait for changesets to natively support staged publish (it doesn't currently): changesets/changesets#2025 that way we won't have to maintain all this custom logic and scripts. If we want staged publish before that, then this PR could be the workaround. Dry run was tested by AI but I haven't really reviewed, as i'm not very familiar with how changeset releases work.

What changed

[scripts/stage-publish.mjs](scripts/stage-publish.mjs) (new) — replaces changeset publish as yarn release. It finds every non-private workspace package whose version isn't on npm, packs each with Yarn (so workspace:^ resolves, as your changesets patch does today), then uploads with npm stage publish <tarball>. It prints New tag: <pkg>@<version> and creates the annotated git tag, which is exactly what changesets/action@a45c4d5 (v1.9.0) greps for to push tags and open GitHub releases.

[scripts/approve-staged-release.mjs](scripts/approve-staged-release.mjs) (new) — yarn release:approve. Lists what CI staged (filtered to this monorepo's packages), confirms, then approves each one. One OTP is reused until npm refuses it, then it asks for a new one — as you asked. Supports --dry-run and a package-name filter.

[.github/workflows/release.yml](.github/workflows/release.yml) — split into versionpublishdocker, per [Andarist's recommendation](changesets/changesets#2025 (comment)). Top-level permissions: {}, with each job declaring its own; only publish gets id-token: write. Plus a npm install --global npm@latest step, since npm stage needs npm ≥ 11.15.0 and Node LTS ships older.

[RELEASE.md](RELEASE.md) (new) — the full process.

On the split workflow

I've taken the structural half — the permission scoping and independent re-runnability are real wins. I deliberately left out the environment: npm manual approval that changesets pairs it with: that would give you two human gates per release, and npm staging is the stronger of the two for your threat model (proof-of-presence at the registry, not at the runner — it still holds if the OIDC token or a build dependency is compromised). RELEASE.md documents the one line to add if you want both.

What you need to do on npmjs.com

Your trust relationship must allow staging — npm trust has --allow-stage-publish alongside --allow-publish. npm's own guidance is to enable only stage-publish, so CI can't publish directly at all. Nothing in this repo can set that.

Verified vs. not

I confirmed against npm 11.19 locally that npm stage publish <yarn-tarball> reads the Yarn-packed tarball correctly and reaches the registry — it failed only with the expected "cannot publish over the previously published versions: 5.1.1". Package selection and dist-tag derivation are tested (a simulated 99.0.0-beta.2 correctly resolved to tag beta). The stage list/approve handling is built from the npm CLI source (stage ids are UUIDs; items carry id/packageName/version/tag/actor/createdAt) but I could not exercise it against a live staged package.

Two caveats worth knowing: a package that has never been published can't be staged — the script detects this, warns, and publishes that one directly. And per your choice, tags/releases/CDN/Docker all happen at stage time, so a rejected stage leaves those behind to clean up by hand.

Sources: [npm-stage docs](https://docs.npmjs.com/cli/v11/commands/npm-stage/), [staged publishing](https://docs.npmjs.com/staged-publishing/), [changesets#2025](changesets/changesets#2025), [changesets publish.yml](https://github.com/changesets/changesets/blob/372523f4c2ee4ffeb8330d444d47ffb6d0af5126/.github/workflows/publish.yml)

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cb80e8f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@qxprakash qxprakash left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few comments , I will add more as I go through it more

images: transloadit/companion
tags: |
type=edge
type=semver,pattern={{version}},value=${{ needs.release.outputs.companionWasReleased }}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stale reference here, job was renamed to publish so it should be needs.publish here ?

Comment thread RELEASE.md
Comment on lines +78 to +82
- The npm trust relationship (OIDC) used by CI must allow `npm stage publish`.
On npmjs.com this is the `--allow-stage-publish` permission; you can leave
`--allow-publish` disabled so CI is unable to publish directly at all.
- Approving is a 2FA action and can never be done by a token or in CI. It has to
be a maintainer on their own machine.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI suggested this : RELEASE.md recommends leaving --allow-publish disabled on the npm trust relationship "so CI is unable to publish directly at all" But the script's fallback for a brand-new package is a direct yarn npm publish ?

} else {
result = await exec(
'yarn',
['npm', 'publish', '--access', 'public', '--tag', tag],
{ cwd: location },
)
}

@remcohaszing remcohaszing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to point out that the biggest security benefit doesn’t come from any changes we make to the code. The real security benefit comes from enforcing staged publishing in the package settings on npmjs.

Comment thread RELEASE.md
Comment on lines +49 to +72
## 4. Approve the release

From a checkout of `main`, logged in to npm (`npm login`) with 2FA enabled:

```bash
yarn release:approve
```

It lists everything CI staged for this repo, asks for confirmation, and then
approves each package. One 2FA code is reused for as long as npm accepts it; you
are only asked for a new one when a code is refused.

Useful variants:

```bash
yarn release:approve --dry-run
```

```bash
yarn release:approve @uppy/core
```

To throw a staged version away instead of publishing it, find its id with
`npm stage list` and run `npm stage reject <stage-id>`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try this in the npmjs UI? Maybe we should see if we like that before we try semi-automating the process with a script.

Comment on lines +24 to +25
- name: Checkout Repo
uses: actions/checkout@v6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a personal opinion: IMO adding name to a step reduces clarity.

When I see “actions/checkout” or “corepack yarn install --immutable” somewhere in the GitHub Actions UI, I know exactly what it does. When I see “Checkout Repo” or “Install dependencies”, I need to dive deeper to find out.

This is definitely not a blocker. This is consistent with what we do in other workflows.

Comment on lines +78 to +81
# `npm stage` (staged publishing) needs npm 11.15.0 or newer, which is
# newer than what Node LTS bundles.
- run: npm install --global npm@latest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step is redundant, based on https://github.com/transloadit/uppy/actions/runs/32023136464/job/95366811336#step:5:1.

Image
Suggested change
# `npm stage` (staged publishing) needs npm 11.15.0 or newer, which is
# newer than what Node LTS bundles.
- run: npm install --global npm@latest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants