Skip to content

Add npm staged publishing workflow - #341

Merged
arcanis merged 2 commits into
mainfrom
mael/do-we-implement-yarn-npm-publi
Sep 21, 2026
Merged

arcanis merged 2 commits into
mainfrom
mael/do-we-implement-yarn-npm-publi

Conversation

@arcanis

@arcanis arcanis commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

yarn npm publish --staged now uploads packages for later approval and returns their stage IDs, with support for dry runs and JSON output.

Ports Berry’s staging workflow, including npm stage list, approve, and reject, paginated listing, UUID validation, and OTP handling for approval and rejection. All four commands share the active workspace’s publish registry selection, honoring publishConfig.registry, scoped settings, matching package rules, and the corresponding credentials.

yarn npm stage approve <id> <id> … approves multiple versions on that registry in order. All IDs are validated before sending requests; a failed approval stops the command, while earlier successful approvals remain published.

Includes Berry’s acceptance tests and additional coverage for scoped packages, registry and credential precedence in nested workspaces, pagination, multiple approvals, invalid input, and partial failures.

Validation:

  • cargo build -r and the publish/staging integration suites: 42 passed; one CI-only provenance test skipped.
  • yarn tsc --build passed.
  • ESLint passed for the changed TypeScript files.

Note

Medium Risk
Touches npm publish and authenticated registry HTTP paths; mistaken approve publishes real versions, though behavior mirrors existing publish auth and OTP flows.

Overview
Adds npm staged publishing: yarn npm publish --staged uploads to the registry staging API (POST /-/stage/package…) instead of publishing immediately, returns a stage ID (and updated JSON fields staged, published, stageId), and does not send OTP during staging.

Introduces yarn npm stage list, approve, and reject to list paginated staged versions, approve one or more UUIDs in order (with OTP on approve/reject), or delete a staged upload. Stage commands share get_publish_registry and workspace publish-registry/auth resolution with staged publish.

http_npm gains get_publish_registry, delete (with OTP retry), and publish uses the shared registry helper. InvalidNpmStageId validates UUID-shaped stage IDs before any network calls on approve.

Acceptance harness mocks stage registry routes; a large stage.test.ts suite covers dry-run/JSON, OTP behavior, multi-approve partial failure, registry precedence, and pagination.

Reviewed by Cursor Bugbot for commit f1a5a30. Bugbot is set up for automated code reviews on this repo. Configure here.

@netlify

netlify Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for yarn-v6 ready!

Name Link
🔨 Latest commit f1a5a30
🔍 Latest deploy log https://app.netlify.com/projects/yarn-v6/deploys/6ab105bd3872a1000888c208
😎 Deploy Preview https://deploy-preview-341--yarn-v6.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stage commands ignore workspace registry
    • Modified registry_auth to accept package ident and use get_registry_for_ident, ensuring stage commands respect publishConfig.registry, scoped settings, and packageRules like publish does.

Create PR

Or push these changes by commenting:

@cursor push 0bb4d46687
Preview (0bb4d46687)
diff --git a/packages/zpm/src/commands/npm/stage/approve.rs b/packages/zpm/src/commands/npm/stage/approve.rs
--- a/packages/zpm/src/commands/npm/stage/approve.rs
+++ b/packages/zpm/src/commands/npm/stage/approve.rs
@@ -36,8 +36,11 @@
             = StreamReport::new(StreamReportConfig::from_config(&project.config));
 
         with_report_result(report, async {
+            let active_workspace
+                = project.active_workspace()?;
+
             let (registry, authorization)
-                = registry_auth(&project).await?;
+                = registry_auth(&project, active_workspace.manifest.name.as_ref()).await?;
 
             let pretty_stage_id
                 = DataType::Code.colorize(&self.stage_id.0);

diff --git a/packages/zpm/src/commands/npm/stage/list.rs b/packages/zpm/src/commands/npm/stage/list.rs
--- a/packages/zpm/src/commands/npm/stage/list.rs
+++ b/packages/zpm/src/commands/npm/stage/list.rs
@@ -51,8 +51,11 @@
         let project
             = Project::new(None).await?;
 
+        let active_workspace
+            = project.active_workspace()?;
+
         let (registry, authorization)
-            = registry_auth(&project).await?;
+            = registry_auth(&project, active_workspace.manifest.name.as_ref()).await?;
 
         let mut items = Vec::new();
         let mut page = 0;

diff --git a/packages/zpm/src/commands/npm/stage/mod.rs b/packages/zpm/src/commands/npm/stage/mod.rs
--- a/packages/zpm/src/commands/npm/stage/mod.rs
+++ b/packages/zpm/src/commands/npm/stage/mod.rs
@@ -5,6 +5,7 @@
     http_npm::{self, AuthorizationMode, GetAuthorizationOptions},
     project::Project,
 };
+use zpm_primitives::Ident;
 
 pub mod approve;
 pub mod list;
@@ -32,16 +33,16 @@
     }
 }
 
-async fn registry_auth(project: &Project) -> Result<(String, Option<String>), Error> {
+async fn registry_auth(project: &Project, ident: Option<&Ident>) -> Result<(String, Option<String>), Error> {
     let registry
-        = http_npm::get_registry(&project.config, None, true)?.to_string();
+        = http_npm::get_registry_for_ident(&project.config, ident, true)?.to_string();
 
     let authorization
         = http_npm::get_authorization(&GetAuthorizationOptions {
             configuration: &project.config,
             http_client: &project.http_client,
             registry: &registry,
-            ident: None,
+            ident: ident,
             auth_mode: AuthorizationMode::AlwaysAuthenticate,
             allow_oidc: false,
         }).await?;

diff --git a/packages/zpm/src/commands/npm/stage/reject.rs b/packages/zpm/src/commands/npm/stage/reject.rs
--- a/packages/zpm/src/commands/npm/stage/reject.rs
+++ b/packages/zpm/src/commands/npm/stage/reject.rs
@@ -36,8 +36,11 @@
             = StreamReport::new(StreamReportConfig::from_config(&project.config));
 
         with_report_result(report, async {
+            let active_workspace
+                = project.active_workspace()?;
+
             let (registry, authorization)
-                = registry_auth(&project).await?;
+                = registry_auth(&project, active_workspace.manifest.name.as_ref()).await?;
 
             let pretty_stage_id
                 = DataType::Code.colorize(&self.stage_id.0);

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit e60a960. Configure here.

Comment thread packages/zpm/src/commands/npm/stage/mod.rs
@github-actions

Copy link
Copy Markdown

⏱️ Benchmark Results

gatsby install-full-cold

Metric Base Head Difference
Mean 4.350s 4.338s -0.28% ✅
Median 4.307s 4.340s +0.76% ⚠️
Min 4.247s 4.226s
Max 4.890s 4.423s
Std Dev 0.128s 0.051s
📊 Raw benchmark data (gatsby install-full-cold)

Base times: 4.890s, 4.609s, 4.282s, 4.354s, 4.273s, 4.297s, 4.428s, 4.274s, 4.358s, 4.442s, 4.273s, 4.263s, 4.367s, 4.368s, 4.359s, 4.392s, 4.320s, 4.285s, 4.253s, 4.302s, 4.289s, 4.266s, 4.259s, 4.301s, 4.272s, 4.313s, 4.409s, 4.247s, 4.347s, 4.421s

Head times: 4.375s, 4.342s, 4.340s, 4.364s, 4.290s, 4.320s, 4.406s, 4.410s, 4.226s, 4.304s, 4.340s, 4.312s, 4.339s, 4.237s, 4.325s, 4.266s, 4.371s, 4.274s, 4.300s, 4.372s, 4.329s, 4.368s, 4.410s, 4.391s, 4.346s, 4.300s, 4.312s, 4.423s, 4.407s, 4.356s


gatsby install-cache-only

Metric Base Head Difference
Mean 1.304s 1.312s +0.66% ⚠️
Median 1.298s 1.312s +1.03% ⚠️
Min 1.264s 1.282s
Max 1.481s 1.348s
Std Dev 0.037s 0.014s
📊 Raw benchmark data (gatsby install-cache-only)

Base times: 1.309s, 1.295s, 1.298s, 1.301s, 1.264s, 1.290s, 1.276s, 1.286s, 1.288s, 1.284s, 1.282s, 1.313s, 1.297s, 1.306s, 1.302s, 1.289s, 1.299s, 1.304s, 1.481s, 1.296s, 1.295s, 1.293s, 1.268s, 1.304s, 1.305s, 1.309s, 1.309s, 1.316s, 1.334s, 1.327s

Head times: 1.320s, 1.325s, 1.308s, 1.305s, 1.315s, 1.317s, 1.314s, 1.334s, 1.316s, 1.317s, 1.310s, 1.348s, 1.307s, 1.312s, 1.327s, 1.338s, 1.289s, 1.320s, 1.310s, 1.310s, 1.319s, 1.282s, 1.311s, 1.314s, 1.306s, 1.292s, 1.307s, 1.310s, 1.300s, 1.293s


gatsby install-cache-and-lock (warm, with lockfile)

Metric Base Head Difference
Mean 0.362s 0.360s -0.66% ✅
Median 0.362s 0.359s -0.97% ✅
Min 0.355s 0.353s
Max 0.371s 0.403s
Std Dev 0.004s 0.009s
📊 Raw benchmark data (gatsby install-cache-and-lock (warm, with lockfile))

Base times: 0.371s, 0.365s, 0.361s, 0.363s, 0.356s, 0.368s, 0.358s, 0.360s, 0.362s, 0.361s, 0.355s, 0.362s, 0.366s, 0.366s, 0.361s, 0.364s, 0.360s, 0.359s, 0.359s, 0.359s, 0.359s, 0.367s, 0.365s, 0.360s, 0.367s, 0.363s, 0.366s, 0.367s, 0.361s, 0.363s

Head times: 0.358s, 0.360s, 0.356s, 0.359s, 0.363s, 0.356s, 0.360s, 0.354s, 0.357s, 0.354s, 0.360s, 0.359s, 0.355s, 0.355s, 0.362s, 0.355s, 0.357s, 0.360s, 0.361s, 0.357s, 0.356s, 0.359s, 0.353s, 0.363s, 0.357s, 0.364s, 0.359s, 0.363s, 0.367s, 0.403s

@arcanis
arcanis merged commit 17b2ae9 into main Sep 21, 2026
25 checks passed
@arcanis
arcanis deleted the mael/do-we-implement-yarn-npm-publi branch September 21, 2026 19:19

This branch was successfully deployed

1 active deployment
test-reports — f1a5a305 Deployed Sep 21, 2026 by arcanis via Reporting test results #1351
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.

1 participant