Skip to content

feat(pacquet): publish (initial)#12691

Draft
KSXGitHub wants to merge 77 commits into
mainfrom
claude/port-publish-rust-3y63vc
Draft

feat(pacquet): publish (initial)#12691
KSXGitHub wants to merge 77 commits into
mainfrom
claude/port-publish-rust-3y63vc

Conversation

@KSXGitHub

@KSXGitHub KSXGitHub commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports pnpm's publish command to pacquet — the Rust port's first releasing command. It implements:

  • Single-package & tarball publish — pack → build the npm publish document → PUT /:pkg, with private / unscoped-restricted validation and semver cleaning.
  • Recursive / --filter workspace publish — dependency-ordered, skipping private/unnamed and already-published packages (concurrent registry probes), with --report-summarypnpm-publish-summary.json and --json array output. A bare --filter= (without -r) enters recursive mode — the shape pnpm's .github/workflows/release.yml publishes the monorepo with (pn publish --filter=pnpm, then pn publish --filter=!pnpm --filter=!@pnpm/exe ...); the global -r short flag also works after the subcommand, and an empty selection is a clean exit-0 no-op that writes no summary.
  • OIDC trusted publishing — CI id-token exchange and keyless sigstore SLSA provenance (Fulcio + Rekor).
  • OTP / web-auth, git pre-publish checks (git-checks config + --no-git-checks), publish-lifecycle scripts, and the --access / --tag / --dry-run / --force / --ignore-scripts / --skip-manifest-obfuscation / --publish-branch flags.
  • --batch is accepted for surface parity but errors — not yet ported.

The "no new packages that should be published" notice is emitted on the generic pnpm log channel with the workspace dir as prefix, matching pnpm's logger.info({ message, prefix }).

Related to #11633 (Rust Roadmap — Stage 3 publish).

Testing

  • Manual verification against the real npmjs.com registry — the web-auth login flow was manually tested end-to-end by publishing a real package to the real npmjs.com registry; token-auth publish and OIDC keyless provenance were likewise exercised against npmjs.com. The classic 2FA OTP path (the npm-otp header) is covered only by the automated tests below, not a live run — it needs a 2FA-enabled npm account, which was not available.
  • pacquet publish integration tests — the real binary is driven against a mockito registry (the CI environment is cleared so the OIDC probe stays offline), covering the single-package document + tarball attachment, --dry-run uploading nothing, a publishConfig.registry override, --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 then PUT, the already-published skip, --force, --report-summary, and the --json array.
  • Publish HTTP / OTP request layerput_publish (success, 5xx-as-completed-response, 401 WWW-Authenticate / one-time pass challenge classification, staged stage-id extraction, request headers, transport failure), parse_otp_challenge, and publish_with_otp_handling are covered with mockito.
  • OTP orchestrationpublish_with_otp_handling is host-generic so pnpm's otp.test.ts scenarios (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.
  • OIDC & provenancecreate_publish_options, fetch_token_and_provenance_by_oidc, fetch_sigstore_token, and build_statement are covered with dependency-injected fakes. The sigstore signing step sits behind a SignProvenance capability, so generate_provenance and the --provenance publish flow (the signed bundle spliced into the document) are unit-tested with a fake signer.
  • Shared web-auth test fakes — the pacquet-network-web-auth-testing crate exports a web_auth_fake!() macro that expands the FakeHost (every web-auth capability over per-test-local thread_local! state) plus recording reporters inside each test body; the network-web-auth OTP tests and the publish tests both use it.
  • Each new test was mutation-tested (break the subject, confirm the test fails; restore, confirm it passes).

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 both pacquet publish and pnpm publish); the real sigstore signing call (live Fulcio / Rekor) is #12739.

Squash Commit Body

Add the `publish` command to pacquet — the Rust port's first `releasing`
command — bringing it to parity with `pnpm publish`.

It implements single-package and tarball publish (pack -> build the npm publish
document -> PUT /:pkg, with private / unscoped-restricted validation and semver
cleaning); recursive / --filter workspace publish (dependency-ordered, skipping
private / unnamed / already-published packages via concurrent registry probes,
with --report-summary and --json, and the filter-without--r recursive
promotion); OIDC trusted publishing with keyless sigstore SLSA provenance
(Fulcio + Rekor); OTP / web-auth; git pre-publish checks (git-checks config and
--no-git-checks); publish-lifecycle scripts; and the --access / --tag /
--dry-run / --force / --ignore-scripts / --skip-manifest-obfuscation /
--publish-branch flags. --batch is accepted for surface parity but errors, as it
is not yet ported.

The external-service side effects — OIDC HTTP, the clock, CI-provider
detection, subprocess spawns, and the sigstore signing step — are each behind a
self-less capability trait on a Host provider, threaded as a Sys generic, so the
flows are unit-testable offline with fakes. The web-auth OTP fakes live in a
dedicated pacquet-network-web-auth-testing crate and are expanded per test by a
web_auth_fake!() macro. The `pacquet publish` binary is additionally covered by
integration tests that drive it against a mockito registry.

Related to pnpm/pnpm#11633. A live end-to-end publish harness (a pnpr instance
implementing OIDC / OTP / web-auth / provenance) is tracked by pnpm/pnpm#12738,
and real sigstore signing by pnpm/pnpm#12739.

Checklist

  • The change is implemented in both the TypeScript CLI and the Rust pacquet/ port — this PR is the pacquet-side port of the already-implemented TypeScript pnpm publish; no TypeScript behavior changes.
  • Added or updated tests.
  • Updated the documentation if needed — no update needed; pnpm publish is already documented and pacquet matches its behavior.

claude added 8 commits June 22, 2026 05:33
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.
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cc6d3396-9877-4779-9709-121d4fb3bd94

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/port-publish-rust-3y63vc

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      6.7±0.12ms   645.7 KB/sec    1.01      6.8±0.35ms   636.2 KB/sec

@codecov-commenter

codecov-commenter commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.40992% with 109 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.81%. Comparing base (9ef4c01) to head (a959693).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pacquet/crates/publish/src/provenance_gen.rs 84.76% 23 Missing ⚠️
pacquet/crates/publish/src/git_checks.rs 75.32% 19 Missing ⚠️
pacquet/crates/network-web-auth-testing/src/lib.rs 91.03% 13 Missing ⚠️
pacquet/crates/publish/src/capabilities.rs 78.00% 11 Missing ⚠️
pacquet/crates/publish/src/publish_options.rs 92.80% 9 Missing ⚠️
pacquet/crates/publish/src/oidc/provenance.rs 93.18% 6 Missing ⚠️
...crates/publish/src/extract_manifest_from_packed.rs 90.38% 5 Missing ⚠️
pacquet/crates/publish/src/oidc/id_token.rs 84.37% 5 Missing ⚠️
pacquet/crates/publish/src/oidc/auth_token.rs 91.83% 4 Missing ⚠️
pacquet/crates/publish/src/execute_token_helper.rs 90.00% 3 Missing ⚠️
... and 7 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

claude added 2 commits June 27, 2026 07:31
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 KSXGitHub left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Some suggestions and questions.

Comment thread pacquet/crates/publish/src/oidc/provenance/tests.rs Outdated
Comment thread pacquet/crates/publish/src/oidc/provenance/tests.rs Outdated
Comment thread pacquet/crates/publish/src/oidc/auth_token.rs Outdated
- 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`.
Comment thread pacquet/crates/publish/src/oidc/auth_token.rs Outdated
@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 27, 2026
Comment thread pacquet/crates/publish/src/oidc/provenance/tests.rs Outdated
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.
Comment thread pacquet/crates/publish/src/oidc/provenance.rs Outdated
claude added 3 commits June 27, 2026 07:56
- 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.
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: a9596931599e

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.805 ± 0.560 4.128 5.806 1.76 ± 0.31
pacquet@main 4.663 ± 0.439 3.868 5.284 1.71 ± 0.27
pnpr@HEAD 2.772 ± 0.369 2.147 3.472 1.01 ± 0.19
pnpr@main 2.734 ± 0.352 2.162 3.339 1.00
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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 676.4 ± 61.2 625.0 767.9 1.00
pacquet@main 820.2 ± 174.8 633.2 1214.7 1.21 ± 0.28
pnpr@HEAD 841.8 ± 138.1 685.2 1191.1 1.24 ± 0.23
pnpr@main 871.3 ± 120.0 659.9 1094.1 1.29 ± 0.21
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.397 ± 0.273 4.019 4.883 1.50 ± 0.20
pacquet@main 4.398 ± 0.180 4.139 4.677 1.50 ± 0.19
pnpr@HEAD 3.182 ± 0.423 2.611 4.086 1.09 ± 0.19
pnpr@main 2.930 ± 0.351 2.478 3.549 1.00
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.281 ± 0.054 1.236 1.386 1.60 ± 0.38
pacquet@main 1.452 ± 0.225 1.207 1.804 1.81 ± 0.51
pnpr@HEAD 0.907 ± 0.305 0.639 1.524 1.13 ± 0.47
pnpr@main 0.800 ± 0.189 0.661 1.275 1.00
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.840 ± 0.081 2.734 2.965 3.70 ± 0.49
pacquet@main 2.862 ± 0.188 2.728 3.379 3.73 ± 0.54
pnpr@HEAD 0.768 ± 0.099 0.667 0.938 1.00
pnpr@main 0.870 ± 0.241 0.649 1.437 1.13 ± 0.35
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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 11.347 ± 1.947 8.977 15.363 1.21 ± 0.39
pacquet@main 11.947 ± 1.441 10.271 14.617 1.28 ± 0.38
pnpr@HEAD 9.356 ± 2.534 5.666 14.334 1.00
pnpr@main 10.169 ± 2.871 7.557 16.320 1.09 ± 0.43
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
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12691
Testbedpacquet

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnprLatency
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
BenchmarkLatencyBenchmark 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%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12691
Testbedpnpr

⚠️ 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-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,264.14 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
699.65 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
701.31 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,159.15 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
4,953.14 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
709.06 ms
🐰 View full continuous benchmarking report in Bencher

claude added 3 commits June 27, 2026 10:25
- 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
claude added 5 commits June 29, 2026 23:42
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.
claude added 3 commits June 30, 2026 01:02
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).
Comment thread pacquet/crates/network-web-auth-testing/src/lib.rs Outdated
claude added 2 commits July 3, 2026 09:17
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.
claude added 16 commits July 3, 2026 09:46
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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pacquet reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants