feat(pacquet): publish (initial)#12691
Conversation
Port the `@pnpm/network.web-auth` package to a new `pacquet-network-web-auth` crate, in preparation for the registry-auth commands (login / publish) that pacquet has not ported yet. The crate provides: - `poll_for_web_auth_token` — polls a registry "done" URL for an auth token, honoring `Retry-After` and an overall timeout (`ERR_PNPM_WEBAUTH_TIMEOUT`). - `with_otp_handling` — runs an operation and, on an EOTP challenge, drives either the web-auth flow (QR code + browser open + poll) or a classic OTP prompt, then retries once. Surfaces `ERR_PNPM_OTP_NON_INTERACTIVE` and `ERR_PNPM_OTP_SECOND_CHALLENGE`. - `prompt_browser_open`, `generate_qr_code`, and the `SyntheticOtpError` helpers. Rather than porting the TypeScript context-of-closures verbatim, every side effect is a self-less capability trait composed on a single `Sys` parameter (`Clock`, `Sleep`, `WebAuthFetch`, `OpenUrl`, `EnterKeyListener`, `PromptOtp`, plus the TTY probes), with the real OS behind `Host` and fn-bound unit-struct fakes in tests. User-facing messages flow through the `Reporter` seam on a new `pnpm:global` channel, matching pnpm's `globalInfo` / `globalWarn` and rendered by `pacquet-default-reporter`. New workspace dependencies: `qrcode` (no default features) and `open`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
CI's dylint job flagged the new crate against the `perfectionist` lint
library (which clippy does not run). Fix every finding:
- Reorder derive lists to the configured `prefix_then_alphabetical`
order (`Debug, Default, Clone, ...`).
- Rename the single-letter `R` reporter generic to `Reporter`, bound as
`self::Reporter`, matching the existing pacquet convention.
- Move the inline `mod tests { ... }` blocks for `capabilities`,
`generate_qr_code`, and `web_auth_timeout_error` into external
`tests.rs` files (the crate's `unit_test_file_layout` is `external_only`).
- Add trailing commas to multi-line macro invocations.
No behavior change. Verified clean with the same command CI runs:
`RUSTFLAGS='-D warnings' cargo dylint --all -- --all-targets --workspace`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
The production `EnterKeyListener` used `spawn_blocking` + a blocking
`stdin().read_line()`, which cannot be cancelled: if the user never
pressed Enter (e.g. authentication completed via a phone QR scan), the
reader thread lingered until process exit. Replace it with a dedicated
thread that polls stdin with a 100ms timeout via `crossterm::event::poll`
and re-checks a cancel flag each tick, so dropping the handle stops the
thread within one poll interval.
`crossterm` reads in the terminal's default (cooked) mode — no raw mode —
matching pnpm's plain `readline.createInterface({ input: process.stdin })`,
which reacts to a submitted line rather than individual keypresses. In
Node, stdin is event-loop-driven so `rl.close()` needs no thread to
cancel; the poll loop is the Rust equivalent.
Adds `crossterm` to `[workspace.dependencies]`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
A code review of the cancellable web-auth listener surfaced two items: - The polling reader thread could consume one keystroke in the window between the handle being dropped (cancel flag set) and the thread noticing it: `event::poll` reports input, then `event::read` swallows a key meant for whatever reads stdin next. Re-check the cancel flag after poll and before read so a dropped handle leaves the input buffered, matching pnpm's clean `readline` close. - Drop the redundant `Option<sender>`: the Enter arm returns right after sending, so the oneshot sender moves directly into `send()`. Also document that the handle is meant to be raced and never resolves on a stdin read error, so it must not be awaited on its own. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
Review follow-up: keep the polling/cancel rationale on `HostEnterHandle` (where `Drop` lives) and trim `listen`'s doc to its unique cooked-mode detail; drop a select-arm comment sentence that restated automatic `Drop` and the function's own doc comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
…nale Round-3 review follow-up: - The `HostEnterHandle` doc blamed `std`'s blocking `read_line` for needing a poll loop, but the listener uses crossterm's `event::read()` (`read_line` was the pre-crossterm implementation). Name the real primitive. - Drop the `listen` doc's reference to the private `HostEnterHandle` type (a public method's docs must not name a more-private item); point to "the returned handle" instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
Brings in 95 commits from main — pacquet command ports (pack, deploy, config, audit, global install, completion, dist-tag, list/ls, and more), pnpr fixes, the pnpm v12 rebrand, and version bumps. Only Cargo.lock conflicted; resolved by taking main's lockfile and re-adding this branch's network-web-auth subtree (crossterm, qrcode, open) via cargo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
Port the `pnpm publish` command to Rust as the new `pacquet-publish` crate and wire it into the CLI, faithfully reproducing pnpm's `pnpm11/releasing/commands/src/publish` (upstream commit `54c5c0e028`). The function-based dependency injection upstream uses (a `context` bag of `fetch` / `Date.now` / `ci-info` / `process.env` / `execa` closures) is ported to pacquet's trait-based seam: `self`-less capability traits (`OidcFetch`, `EnvVar`, `CiInfo`, `Clock`, `RunCommand`, `ConfirmPrompt`) composed as bounds on a single `Sys` type parameter, with the real OS behind `Host` and `fn`-bound unit-struct fakes in the tests. The OTP / web-authentication flow reuses the existing `pacquet-network-web-auth` seam. What landed: - OIDC trusted publishing: `oidc::get_id_token`, `oidc::fetch_auth_token`, `oidc::determine_provenance`, and the `fetch_token_and_provenance_by_oidc` precedence orchestration — each unit-tested through the capability fakes (the external-service happy paths a real fixture can't stage). - The `libnpmpublish`-equivalent publish document builder and PUT, with OTP / web-auth challenge handling, plus token, OTP-env (`PNPM_CONFIG_OTP`) and `tokenHelper` support. - Registry-config-key parsing, packed-tarball manifest extraction, the `npm publish --json` summary, the git working-tree / branch / remote checks, and the publish-lifecycle scripts (`prepublishOnly` / `prepublish` / `publish` / `postpublish`). - A `publish` subcommand for the single-package and pre-built-tarball paths. Scope not yet ported (errors clearly rather than diverging silently): - `--recursive` / `--batch` workspace publishing. - Signed provenance attestation generation (needs sigstore, which pacquet does not bundle); the OIDC-driven provenance *flag* is determined, but a `true` result is refused at publish time.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #12691 +/- ##
==========================================
+ Coverage 85.56% 85.81% +0.24%
==========================================
Files 413 433 +20
Lines 63986 65787 +1801
==========================================
+ Hits 54750 56453 +1703
- Misses 9236 9334 +98 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Address the dylint (perfectionist) and rustdoc errors reported by CI on the publish crate: - Reorder derive lists to `prefix_then_alphabetical` (move `derive_more::Display` ahead of the std markers). - Rename single-letter `R` reporter generics to `Reporter`, bounded `Reporter: self::Reporter`, matching the existing pacquet convention. - Add `reason = "..."` to the `#[allow(clippy::too_many_arguments)]` attributes. - Use a raw string for the git-checks hint and add trailing commas to the multi-line macro invocations perfectionist flagged. - Replace the broken intra-doc link in `capabilities.rs` with plain prose.
Running `cargo dylint` locally surfaced perfectionist findings the CI log had
not reached (it aborted on the earlier lib errors):
- `unit_test_file_layout`: move every inline `#[cfg(test)] mod tests { ... }`
block into an external `tests.rs` sibling, matching the workspace convention.
- `macro_trailing_comma`: use the nested multi-line `assert!(matches!(...))`
form (no trailing comma) and drop trailing commas from the now single-line
`assert_eq!` calls that collapsed after de-indentation.
- `single_letter_generic` / single-letter closure params: rename the remaining
one-letter closure bindings to descriptive names.
KSXGitHub
left a comment
There was a problem hiding this comment.
Some suggestions and questions.
- Import `URL_SAFE_NO_PAD` with `use` instead of fully qualifying it, in both `provenance.rs` and its test. - Invert the error-body parse in `auth_token.rs` into a `pipe_as_ref` method chain via `pipe-trait`. - Build the provenance test's id-token payload from a typed `#[derive(Serialize)]` struct rather than an ad-hoc `serde_json::Value`.
Invert the rest of the `serde_json::from_str::<Value>(&body)` calls in the OIDC auth-token and provenance modules into `pipe_as_ref` / `pipe` method chains, as requested in review.
- Add commit-pinned permalinks to the upstream TypeScript source on each ported module's doc comment (and the provenance test's `Payload` type), per the citation convention. - Drop the redundant "both optional" prose from the `Payload` doc. - Revert the over-engineered `pipe` on the visibility-error body parse in `provenance.rs`; a single `serde_json::from_str` reads clearer there.
Correctness: - Don't hard-fail a publish when provenance was only auto-detected by OIDC for a public CI repo. An explicit `--provenance` still errors (pacquet can't generate the attestation yet), but the auto-detected case now warns and publishes, matching what pnpm would do minus the attestation. - Propagate a malformed package-visibility response as a hard error instead of silently treating it as "not public", matching the unguarded `response.json()` in the TS source. - Collapse `.` / `..` segments when matching a tar entry against `package/package.json`, mirroring `path.normalize`. - Honor a manifest-level `tag` over the default dist-tag, mirroring libnpmpublish's `manifest.tag || defaultTag`. Cleanup: - Reuse the tarball digests already computed for the summary instead of hashing the tarball a second time while building the publish document. - Send the request body as `bytes::Bytes` so the megabytes-large body is not re-copied on each PUT (including the OTP retry). - Drop the unused `is_stage` parameter from the document builder.
- Honor `--ignore-scripts` (and the `ignore-scripts` config setting) when packing for publish, not just for the publish-lifecycle scripts. Previously `prepack` / `prepare` / `postpack` still ran when the user asked for no scripts, because the pack step read only `config.ignore_scripts`. - Broaden OTP challenge detection to match npm-registry-fetch: accept the `otp` token as a comma-separated entry of `WWW-Authenticate` (exact token, not a loose substring) and fall back to a `one-time pass` body, so 2FA works against registries that omit the header.
Integrated-Benchmark Report (Linux)Commit: Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.804621587000001,
"stddev": 0.5597006964221056,
"median": 4.6248597241,
"user": 3.0057165799999996,
"system": 2.6748423199999993,
"min": 4.1275696426,
"max": 5.8055641986,
"times": [
5.4190669226,
4.1275696426,
4.6376071726,
5.4101826476,
4.4409779216,
4.4565314816,
4.8722754406,
4.6121122756,
4.2643281666,
5.8055641986
]
},
{
"command": "pacquet@main",
"mean": 4.6626789447000005,
"stddev": 0.4393313962685409,
"median": 4.6323083996,
"user": 2.989417279999999,
"system": 2.6645495199999996,
"min": 3.8675778926,
"max": 5.2842694956,
"times": [
4.9341885096,
5.2747186626,
4.5583760136,
5.2842694956,
4.5520082536,
4.5455056036000006,
4.7190403056,
4.1848639246,
3.8675778926,
4.7062407856
]
},
{
"command": "pnpr@HEAD",
"mean": 2.7722999273,
"stddev": 0.36862517077081086,
"median": 2.7563367551,
"user": 2.10385098,
"system": 2.25974892,
"min": 2.1473227736,
"max": 3.4719357846000003,
"times": [
3.4719357846000003,
3.0468623516,
2.7278416036,
2.7090377156,
2.7848319066,
2.3896382656,
2.5461199896,
2.9294360746,
2.1473227736,
2.9699728076
]
},
{
"command": "pnpr@main",
"mean": 2.7344153086,
"stddev": 0.3523443602786209,
"median": 2.7958445821,
"user": 2.0836386799999995,
"system": 2.2573217199999998,
"min": 2.1620606686,
"max": 3.3393748596,
"times": [
2.8170542596,
2.7213161306,
3.0082595066,
2.1735877706,
2.8426700016,
2.7746349046,
2.1620606686,
3.3393748596,
2.8339492186,
2.6712457656
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6764496422799999,
"stddev": 0.061202197540738405,
"median": 0.64408988638,
"user": 0.31508127999999996,
"system": 1.04224254,
"min": 0.62498912588,
"max": 0.76787293088,
"times": [
0.6449081398800001,
0.64327163288,
0.6377773558800001,
0.62498912588,
0.66645437488,
0.76383971588,
0.76787293088,
0.75860076288,
0.62629122388,
0.63049115988
]
},
{
"command": "pacquet@main",
"mean": 0.8201728193800001,
"stddev": 0.1748359589255875,
"median": 0.80813466388,
"user": 0.29712437999999997,
"system": 1.05208834,
"min": 0.6331847538800001,
"max": 1.21468602088,
"times": [
0.83742552888,
0.6331847538800001,
0.81780639988,
0.83258240888,
0.67051768688,
0.7984629278800001,
0.66586825988,
0.73667571288,
0.99451849388,
1.21468602088
]
},
{
"command": "pnpr@HEAD",
"mean": 0.84183433568,
"stddev": 0.13812148430202123,
"median": 0.83999906738,
"user": 0.33190668,
"system": 1.07453504,
"min": 0.68515454388,
"max": 1.19108673488,
"times": [
0.76082755788,
0.83014467688,
0.72398952788,
0.8787819558800001,
0.85516103188,
0.68515454388,
0.85457999488,
0.78876387488,
0.84985345788,
1.19108673488
]
},
{
"command": "pnpr@main",
"mean": 0.87128507578,
"stddev": 0.12001922970597718,
"median": 0.85885081838,
"user": 0.31615878,
"system": 1.0571166399999998,
"min": 0.65994446888,
"max": 1.09411957988,
"times": [
0.8291096988800001,
0.9822310778800001,
0.76177033188,
0.84978248788,
0.65994446888,
0.8256301258800001,
0.89197641588,
1.09411957988,
0.86791914888,
0.95036742188
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.396797218920001,
"stddev": 0.2726771336450791,
"median": 4.39832674392,
"user": 3.00178862,
"system": 2.60080592,
"min": 4.01892165292,
"max": 4.88334335192,
"times": [
4.4295044599199995,
4.657860074919999,
4.36205086592,
4.54340591592,
4.51555665592,
4.07643409492,
4.88334335192,
4.01892165292,
4.11374608892,
4.36714902792
]
},
{
"command": "pacquet@main",
"mean": 4.39826829822,
"stddev": 0.18025016286143897,
"median": 4.36933921092,
"user": 3.02200302,
"system": 2.5838335199999998,
"min": 4.139460328919999,
"max": 4.67668254092,
"times": [
4.67668254092,
4.53041332492,
4.139460328919999,
4.5264794759199996,
4.589045232919999,
4.39563012792,
4.30832649292,
4.34304829392,
4.32143931292,
4.15215785092
]
},
{
"command": "pnpr@HEAD",
"mean": 3.18246827942,
"stddev": 0.4225643183920584,
"median": 3.19264168692,
"user": 2.00412622,
"system": 2.20610212,
"min": 2.61110875892,
"max": 4.08640667592,
"times": [
4.08640667592,
2.8044368489200004,
3.1273498289200004,
3.2477682269200003,
3.56122406792,
2.61110875892,
2.7742215069200005,
3.2147564979200003,
3.1705268759200003,
3.22688350592
]
},
{
"command": "pnpr@main",
"mean": 2.9301460882200003,
"stddev": 0.3506198809612118,
"median": 2.8529884834200003,
"user": 1.98138172,
"system": 2.1933608200000005,
"min": 2.47808510292,
"max": 3.5494531939200002,
"times": [
3.06041732692,
2.47808510292,
2.67386886792,
2.69875332892,
2.9549503189200004,
2.61911565092,
3.5494531939200002,
2.7510266479200003,
3.1199838039200003,
3.3958066399200004
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.28086113992,
"stddev": 0.05408171895812493,
"median": 1.2663446719199998,
"user": 1.0761307599999999,
"system": 1.3710482599999998,
"min": 1.23554643892,
"max": 1.38645362092,
"times": [
1.27269574592,
1.23554643892,
1.2599935979199999,
1.24601199992,
1.27759777492,
1.28299288392,
1.23802384192,
1.36953947592,
1.2397560189199999,
1.38645362092
]
},
{
"command": "pacquet@main",
"mean": 1.4522136574200002,
"stddev": 0.22476047461383358,
"median": 1.35176492342,
"user": 1.0482941599999998,
"system": 1.34751906,
"min": 1.2066706169199999,
"max": 1.8035222389199999,
"times": [
1.29181294992,
1.8035222389199999,
1.7453346729199999,
1.66491759192,
1.2066706169199999,
1.58799324292,
1.37035948792,
1.26970836992,
1.24864704392,
1.33317035892
]
},
{
"command": "pnpr@HEAD",
"mean": 0.90713037052,
"stddev": 0.30517412369850355,
"median": 0.7990199474199999,
"user": 0.27973126,
"system": 1.0153101599999999,
"min": 0.63919534892,
"max": 1.52423628792,
"times": [
0.89030089092,
0.73439648192,
1.32575013692,
1.52423628792,
0.70030281692,
1.05560934492,
0.86364341292,
0.66575054792,
0.63919534892,
0.67211843592
]
},
{
"command": "pnpr@main",
"mean": 0.80039004082,
"stddev": 0.1894620732744819,
"median": 0.74941993092,
"user": 0.27111265999999995,
"system": 1.0137061600000001,
"min": 0.66103361492,
"max": 1.27484642992,
"times": [
0.67765654192,
0.66103361492,
0.76421551492,
0.77604656992,
0.67227340392,
0.73462434692,
0.97150542792,
1.27484642992,
0.77596541592,
0.69573314192
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.8397469004,
"stddev": 0.08135158106083266,
"median": 2.8278531991,
"user": 1.4395521799999997,
"system": 1.60650994,
"min": 2.7340785886,
"max": 2.9648159186000003,
"times": [
2.7340785886,
2.8145792586000002,
2.7785428776,
2.7889726676,
2.8411271396,
2.8609981816000003,
2.7501591766,
2.9435106626,
2.9206845326,
2.9648159186000003
]
},
{
"command": "pacquet@main",
"mean": 2.8623487971,
"stddev": 0.18785313353022928,
"median": 2.8081899906,
"user": 1.43574748,
"system": 1.57511134,
"min": 2.7277508776,
"max": 3.3790729666000003,
"times": [
2.8112381316,
2.7277508776,
2.7582023006000003,
2.7922475686,
2.7772727026,
2.8941302646000002,
2.8667368526000003,
2.8116944566,
3.3790729666000003,
2.8051418496
]
},
{
"command": "pnpr@HEAD",
"mean": 0.7676283156,
"stddev": 0.09947916310123735,
"median": 0.7266118561000001,
"user": 0.28032587999999997,
"system": 1.01693494,
"min": 0.6668210706000001,
"max": 0.9381537476,
"times": [
0.9023878686000001,
0.6668210706000001,
0.9381537476,
0.6814617156,
0.8374292316,
0.7091841366,
0.7138228466000001,
0.6692292056,
0.7394008656000001,
0.8183924676000001
]
},
{
"command": "pnpr@main",
"mean": 0.8698736767999999,
"stddev": 0.24054091726542623,
"median": 0.8487370916000001,
"user": 0.26900048,
"system": 1.00670154,
"min": 0.6490713766,
"max": 1.4372617716000002,
"times": [
0.9084299976000001,
0.8534824706,
0.6490713766,
0.8517485196000001,
0.7269493266,
0.8457256636,
0.6521746496,
0.6882175576,
1.4372617716000002,
1.0856754346000002
]
}
]
}Scenario: Isolated linker: fresh restore, cold cache + cold store + cold pnpr
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 11.34680915168,
"stddev": 1.947143784514273,
"median": 11.27668925388,
"user": 3.0956121599999995,
"system": 2.76520882,
"min": 8.97684871088,
"max": 15.36289191488,
"times": [
12.49465214288,
15.36289191488,
11.41227055788,
10.69098313688,
11.14110794988,
8.97684871088,
9.33798278988,
9.43627166588,
13.03241651388,
11.58266613388
]
},
{
"command": "pacquet@main",
"mean": 11.94666501428,
"stddev": 1.440648295474188,
"median": 12.03867518138,
"user": 3.14183196,
"system": 2.8216691199999997,
"min": 10.27100806588,
"max": 14.61740045088,
"times": [
11.98744499088,
12.55516208288,
12.08990537188,
10.87180551988,
10.29514663888,
14.61740045088,
10.75481517788,
13.63365859388,
10.27100806588,
12.39030324988
]
},
{
"command": "pnpr@HEAD",
"mean": 9.35601698908,
"stddev": 2.534314192711736,
"median": 9.46970940488,
"user": 2.18367486,
"system": 2.37961852,
"min": 5.66570687988,
"max": 14.33350117588,
"times": [
8.90287747388,
14.33350117588,
7.58444393988,
5.66570687988,
6.86633291588,
11.42116484488,
10.03654133588,
10.36971697088,
10.61436603388,
7.76551831988
]
},
{
"command": "pnpr@main",
"mean": 10.168509621979998,
"stddev": 2.8712523343970826,
"median": 9.26133106988,
"user": 2.1648140600000003,
"system": 2.38299322,
"min": 7.55694321488,
"max": 16.31961564588,
"times": [
10.10394169188,
7.56510740788,
12.18449926688,
8.53486388688,
7.55694321488,
8.42478989188,
7.98111824188,
13.02641871888,
16.31961564588,
9.98779825288
]
}
]
} |
|
| Branch | pr/12691 |
| Testbed | pacquet |
🚨 1 Alert
| Benchmark | Measure Units | View | Benchmark Result (Result Δ%) | Upper Boundary (Limit %) |
|---|---|---|---|---|
| isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr | Latency seconds (s) | 📈 plot 🚷 threshold 🚨 alert (🔔) | 11.35 s(+60.89%)Baseline: 7.05 s | 8.46 s (134.08%) |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,396.80 ms(-1.76%)Baseline: 4,475.62 ms | 5,370.74 ms (81.87%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,839.75 ms(-7.16%)Baseline: 3,058.75 ms | 3,670.50 ms (77.37%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,280.86 ms(-6.09%)Baseline: 1,363.99 ms | 1,636.79 ms (78.25%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,804.62 ms(+10.00%)Baseline: 4,367.73 ms | 5,241.28 ms (91.67%) |
| isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr | 📈 view plot 🚷 view threshold 🚨 view alert (🔔) | 11,346.81 ms(+60.89%)Baseline: 7,052.43 ms | 8,462.92 ms (134.08%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 676.45 ms(+7.48%)Baseline: 629.39 ms | 755.27 ms (89.56%) |
|
| Branch | pr/12691 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,264.14 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 699.65 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 701.31 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,159.15 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr | 📈 view plot | 4,953.14 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 709.06 ms |
- tokenHelper now errors on a non-zero exit instead of proceeding with an empty token, mirroring execa.sync's throw-on-failure. - Serialize the --json summary lazily: publish helpers return a typed PublishSummary and the handler only stringifies when --json is set. - Add an actionable hint to the batch-requires-recursive error. - Default the publish directory through &Path (map_or) so a non-UTF-8 project path no longer silently degrades to ".".
Self-review round 4 findings: - clean_version now drops build metadata to match `semver.clean`, which returns SemVer.version (major.minor.patch[-prerelease], never +build). node_semver's Display appends +build, so a manifest version like `1.2.3+build` was being registered verbatim instead of as `1.2.3`. - Hoist the explicit `--provenance` hard error ahead of the OIDC token exchange so a request that cannot be honored fails before performing authenticated network round-trips. - Document that execute_token_helper is a faithful port that is not yet wired into the publish auth path: pacquet reuses the shared, pre-resolved AuthHeaders map, which has no per-registry tokenHelper slot. Wiring it is a config/network-layer change; a tokenHelper-only registry is currently unauthenticated on publish.
Brings in 3 commits from main: the cli_args.rs split (#12690, CLI-only), plus the bytes and tower-http dependency-version bumps. Only Cargo.lock conflicted; resolved by taking main's lockfile and re-adding this branch's network-web-auth subtree (crossterm, qrcode, open) via cargo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWejcxTU8n144a1KK4nRj8
CodeCov flagged put_publish, publish_with_otp_handling and parse_otp_challenge as entirely uncovered. The OTP *orchestration* behaviours from pnpm's otp.test.ts are already ported in the network-web-auth crate (where pacquet's with_otp_handling lives); the gap was the publish-side HTTP layer those tests reach through a mocked publish function. Cover it directly with mockito: - put_publish: success, 5xx-as-completed-response, 401 WWW-Authenticate otp challenge, one-time-pass body -> web-auth challenge (authUrl / doneUrl), staged stage-id extraction (and its absence when unstaged), the request headers, and a refused connection -> Transport. - parse_otp_challenge: authUrl/doneUrl extraction, the plain-OTP body with no URLs, and each URL read independently. - publish_with_otp_handling: returns the response when no OTP is required. Adds mockito as a dev-dependency and derives Debug on PublishResponse so the Result asserts can render it.
pnpm's 2FA/OTP/web-auth tests build their mock context through one shared test-helper package, reused by publish's otp.test.ts and login's login.test.ts. Mirror that with pacquet-network-web-auth-testing: a FakeHost implementing every web-auth capability (Clock, Sleep, WebAuthFetch, PromptOtp, EnterKeyListener, the TTY probes, OpenUrl) over thread-local script state, plus the recording/strict reporters, the response builders, and a reusable FakeOtpError. Extracted verbatim from network-web-auth's inline with_otp_handling test fakes so the network-web-auth, publish, and future pacquet login tests share one fake. No production code is touched.
Replace with_otp_handling's inline fakes with the shared pacquet-network-web-auth-testing crate. Because a crate's own unit tests cannot depend on a helper crate that depends back on it (the helper sees the non-test build, so the two builds' types do not unify), move these tests from the in-lib `mod tests` to tests/with_otp_handling.rs, where the integration-test crate and the helper both link the same build. The scenarios are unchanged; they now drive FakeHost / FakeOtpError from the shared crate instead of file-local copies.
Add a Sys type parameter (the web-auth host: Clock + Sleep + WebAuthFetch + the TTY probes + EnterKeyListener + OpenUrl + PromptOtp) instead of hardcoding the real WebAuthHost, so a test can drive the OTP orchestration with a fake host while the PUT still goes through a mocked registry. Production passes the real host at the single call site; the web-auth Clock is aliased to avoid colliding with the OIDC capability Clock already imported here.
Drive publish_with_otp_handling through the full OTP flow with the shared FakeHost on the web-auth side and mockito on the PUT side, distinguishing the first attempt from the retry by the npm-otp header each carries. Covers pnpm otp.test.ts's classic prompt-and-retry, second-challenge, non-interactive, web-auth poll-then-retry, and timeout cases — verifying the put_publish 401 classification feeds the with_otp_handling orchestration and that the challenge OTP / web token reaches the retry request, an integration a mocked operation cannot reach.
CodeCov flagged build_statement, fetch_sigstore_token, fetch_token_and_provenance_by_oidc and create_publish_options as uncovered. They each take the Sys capability seam, so drive them with fake EnvVar/CiInfo/Clock/OidcFetch providers (the established publish DI test pattern): - build_statement: GitHub -> SLSA v1, GitLab -> SLSA v0.1, neither -> UnsupportedProvider. - fetch_sigstore_token: GitHub request-token fetch, GitLab SIGSTORE_ID_TOKEN (present/missing), unsupported provider. - fetch_token_and_provenance_by_oidc: the id-token -> auth-exchange -> visibility chain (fetch dispatched on the request URL) for the happy path, the non-CI no-op, the provenance override short-circuit, the skippable auth-exchange failure, and keeping the token when provenance can't be determined. - create_publish_options: OIDC disabled vs enabled (auth-token override + provenance merge) and the unsupported-protocol error. Each test was mutation-verified.
The production Host impls were the lowest-covered file. Cover the two that carry real branching and are portably testable: - OidcFetch::fetch against a mockito server: GET (accept/authorization headers + per-request timeout), POST (zero-length body), a non-2xx classified as a completed response, and a refused connection mapped to a transport error. - RunCommand::run as a real subprocess (unix-gated, matching the crate's /bin/sh convention): stdout/success capture, non-zero exit, the cwd argument, and a missing program surfacing as an io::Error. The remaining Host impls are left to their consumers' fake-Sys tests: EnvVar/CiInfo/Clock are one-line passes through to std whose only test seam is the process-global env/clock the Sys seam exists to avoid, and ConfirmPrompt reads an interactive TTY.
Fix CI Format + Dylint on the coverage commits: - rustfmt: collapse a one-line mockito mock builder in the Host fetch tests. - dylint bare_identifier_reference: link the GhEnv/GlEnv doc references as intra-doc links. - dylint macro_trailing_comma: add the trailing comma to the multi-line assert_eq! invocations in the OIDC orchestrator tests. Verified with the CI commands: cargo fmt --check and cargo dylint --all -- --all-targets --workspace (the --all-targets flag the earlier scoped run omitted, which is why these slipped through).
Resolve the dispatch_query.rs import conflict (keep both the new prefix command and this branch's publish command, alphabetically ordered) and adapt publish's recursive handler to the recursive-selection API change from #12688: select_recursive_projects now returns a RecursiveSelection, and ordering goes through sort_filtered_projects (selected + full/prod graphs) instead of sort_projects.
The Dense1x2 renderer's default draws dark QR modules as block glyphs and light modules as blank cells. On the usual light-on-dark terminal that inverts the code — light modules appear lit, dark modules blank, and the light quiet zone vanishes into the background — so it scans awkwardly and looks nothing like pnpm's output. pnpm renders through qrcode-terminal's small mode, which draws the light modules (and the quiet zone) as the block glyphs and leaves dark modules blank, showing dark modules inside a light frame. Swap the renderer's dark and light colors to match that polarity exactly.
Two pure handler helpers CodeCov flagged as uncovered: - should_ignore_scripts: the flag-or-config OR. - publish_options: the flag/config -> PublishPackedPkgOptions mapping (tag defaulting to "latest", access parse, provenance, dry-run, otp). Constructed directly from a default PublishArgs + Config, no registry. Mutation verification to follow with the rest of the new publish tests.
Addresses review #12691 (r3518711611): the shared crate held module-level thread_local scenario state reconfigured by pub set_* setters and read by a shared FakeHost — shared mutable state that contradicts the DI convention (CODE_STYLE_GUIDE 'Dependency injection for tests', principle 3: keep a stateful fake's state in a static inside each #[test] body so tests never share or race). Replace it with a web_auth_fake!() macro that expands, inside a test body, to fn-local thread_local statics plus a local FakeHost (all eight web-auth capabilities), the recording/strict reporters, and the reset / set_* / infos / warns config fns. Nothing mutable lives at module scope. The stateless helpers (InputResponse, SleepBehavior, FetchScript, FakeOtpError, ok_202, ok_token, web_auth_body) stay pub. The network-web-auth OTP integration tests and the publish OTP orchestration tests invoke the macro at the top of each test; scenarios are unchanged. Other module-level shared-mutable test statics found by a codebase scan are tracked in #12779.
Close the smaller publish_packed_pkg holes CodeCov flagged: - web_auth_fetch_options: the OidcHttpOptions -> WebAuthFetchOptions retry/timeout mapping. - OtpError for PublishHttpError: an Otp variant surfaces a challenge, a Transport variant does not (the previously-uncovered None arm). - the From<CreatePublishOptionsError> and From<WithOtpError> conversions into PublishPackedPkgError.
…references Apply the #12737 paradigm to this branch's files: pacquet and the TypeScript pnpm CLI are co-developed parallel implementations at near-complete parity, neither downstream of the other, so comments describe pacquet's own behavior rather than pointing at the TS source. Removed the `/blob/` permalinks to the pnpm-org TypeScript repos and the 'Ports / Mirrors / matching the TS X' framing (cited TS symbols and files like otp.test.ts, recursivePublish, generateProvenance, createReadline- Interface, execa, libnpmpublish, sigstore-js), rewriting the affected prose. Kept issue/PR references, pnpm-product mentions, shared-contract identifiers (ERR_PNPM_* codes, the pnpm:global reporter channel, npm registry terms), third-party attributions, and Rust intra-doc links. Comment-only, except two incidental lint fixes to the new tests surfaced while verifying (a Default field-reassign and a use-collapse).
Two publish-flow helpers CodeCov flagged, both registry-free: - pack_for_publish: pack a tempdir package (scripts ignored) and assert a .tgz lands in the destination and the returned manifest is the packed one. - run_publish_scripts: a declared prepublishOnly runs through sh -c in the package dir (unix-gated), and an all-absent script set is a no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Drive the real pacquet publish binary against a mockito registry, porting the plain token-auth scenarios from pnpm's test/publish/publish.ts that don't depend on OIDC / provenance / OTP: the package document + tarball attachment, --dry-run uploading nothing, publishConfig.registry override, --tag, scoped %2f-escaped path with --access, publishing a prebuilt tarball, a missing-tarball error, --json summary, and the publish lifecycle scripts (run and --ignore-scripts). CI env is cleared per spawn so the OIDC probe stays offline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
…istry Cover run_recursive's actual publish loop with a mockito registry, porting the plain token-auth scenarios from pnpm's recursivePublish.ts: each eligible package is probed (404) then PUT, --force skips the probe and republishes, --report-summary records both packages, and --json prints the per-package array. Refreshes the file header now that the publish loop is exercised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
… paths Add a single-package test for `pacquet publish <dir>` (publishing a package in a subdirectory rather than the cwd) and a recursive test where one package's version already exists on the registry (probe returns it) so it is skipped while the absent package is published. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Fixes the Format CI failure from the previous commit and drops the resulting single-line trailing comma dylint flagged.
A 5xx PUT response is a completed request, so publish surfaces FailedToPublishError rather than a transport error. Closes the last offline-reachable branch of publish_packed_pkg's publish tail (the remaining gaps are provenance signing and the staged path).
The only non-deterministic step in generate_provenance was the sigstore Fulcio/Rekor signing call, which made the whole function untestable offline (and left it uncovered). Extract that step behind a SignProvenance capability on Sys — the DI seam AGENTS.md prescribes for publish's external-service paths — with the real sigstore exchange in the Host impl. generate_provenance is now deterministic given a fake signer. Adds a direct unit test (statement + token + signer -> attachment), a signer-failure path, and a --provenance flow test that drives the real generate_provenance through publish_packed_pkg and asserts the signed .sigstore bundle is spliced into the PUT document's _attachments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
… pnpm The QR was drawn with the qrcode crate's Dense1x2 renderer (a four-module quiet zone) at its default error-correction level, so it came out larger and with a much thicker margin than pnpm's qrcode-terminal output. Replicate qrcode-terminal small mode instead: encode a single byte segment at EC level L (matching the version, and so the size, pnpm renders) and frame it with a one-module light border. Dark-on-light polarity is unchanged. The module pattern can still differ from pnpm because the qrcode crate and qrcode-terminal pick different mask patterns (qrcode-terminal's penalty uses the short 1011101 rule-3 core), but the size, margin, and polarity now match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KtBQzmLLDU3RcGzzCMopPB
Summary
Ports pnpm's
publishcommand to pacquet — the Rust port's firstreleasingcommand. It implements:PUT /:pkg, with private / unscoped-restricted validation and semver cleaning.--filterworkspace publish — dependency-ordered, skipping private/unnamed and already-published packages (concurrent registry probes), with--report-summary→pnpm-publish-summary.jsonand--jsonarray output. A bare--filter=(without-r) enters recursive mode — the shape pnpm's.github/workflows/release.ymlpublishes the monorepo with (pn publish --filter=pnpm, thenpn publish --filter=!pnpm --filter=!@pnpm/exe ...); the global-rshort flag also works after the subcommand, and an empty selection is a clean exit-0 no-op that writes no summary.git-checksconfig +--no-git-checks), publish-lifecycle scripts, and the--access/--tag/--dry-run/--force/--ignore-scripts/--skip-manifest-obfuscation/--publish-branchflags.--batchis accepted for surface parity but errors — not yet ported.The "no new packages that should be published" notice is emitted on the generic
pnpmlog channel with the workspace dir as prefix, matching pnpm'slogger.info({ message, prefix }).Related to #11633 (Rust Roadmap — Stage 3
publish).Testing
npm-otpheader) is covered only by the automated tests below, not a live run — it needs a 2FA-enabled npm account, which was not available.pacquet publishintegration tests — the real binary is driven against amockitoregistry (the CI environment is cleared so the OIDC probe stays offline), covering the single-package document + tarball attachment,--dry-runuploading nothing, apublishConfig.registryoverride,--tag, the scoped%2f-escaped path with--access, publishing a prebuilt tarball and a directory argument, the missing-tarball and registry-5xx failures,--json, and the publish lifecycle scripts (run and--ignore-scripts). The recursive tests cover the publish loop: each eligible package probed thenPUT, the already-published skip,--force,--report-summary, and the--jsonarray.put_publish(success, 5xx-as-completed-response, 401WWW-Authenticate/one-time passchallenge classification, staged stage-id extraction, request headers, transport failure),parse_otp_challenge, andpublish_with_otp_handlingare covered withmockito.publish_with_otp_handlingis host-generic so pnpm'sotp.test.tsscenarios (classic prompt-and-retry, second-challenge, non-interactive, web-auth poll-then-retry, timeout) run with the PUT mocked and the web-auth side scripted by a fake host.create_publish_options,fetch_token_and_provenance_by_oidc,fetch_sigstore_token, andbuild_statementare covered with dependency-injected fakes. The sigstore signing step sits behind aSignProvenancecapability, sogenerate_provenanceand the--provenancepublish flow (the signed bundle spliced into the document) are unit-tested with a fake signer.pacquet-network-web-auth-testingcrate exports aweb_auth_fake!()macro that expands theFakeHost(every web-auth capability over per-test-localthread_local!state) plus recording reporters inside each test body; thenetwork-web-authOTP tests and the publish tests both use it.Known coverage gap: a live end-to-end publish against a registry that implements OIDC / OTP / web-auth / provenance needs a
pnpr-based harness — #12738 (which also tracks integration tests for bothpacquet publishandpnpm publish); the real sigstore signing call (live Fulcio / Rekor) is #12739.Squash Commit Body
Checklist
pacquet/port — this PR is the pacquet-side port of the already-implemented TypeScriptpnpm publish; no TypeScript behavior changes.pnpm publishis already documented and pacquet matches its behavior.