Commit Graph

290 Commits

Author SHA1 Message Date
Zoltan Kochan
258b11274c fix(ci): point pnpm CLI bencher extraction at pnpm11/pnpm (#12544)
The TypeScript pnpm CLI was relocated from `pnpm/` to `pnpm11/pnpm/` in
pnpm/pnpm#12537, but the "Extract pnpm CLI e2e test duration" step still
passed `--package-dir pnpm`. That path no longer exists, so the
exec-summary lookup found no entry and the step exited 1, failing the
full TS CI test job on main.

Point `--package-dir` at the package's new location, `pnpm11/pnpm`.
2026-06-20 20:34:12 +02:00
btea
f6dde6246f ci: skip ecosystem-e2e on forks (#12541) 2026-06-20 20:18:47 +02:00
Zoltan Kochan
fc2f33912e refactor: move the TypeScript pnpm CLI into a pnpm11/ directory (#12537)
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet
port. To make that split legible, all TypeScript source, test, and build
directories move under a new top-level pnpm11/ directory. The name states
the version boundary rather than implying a behavioral fork, since the two
stacks are meant to behave identically.

Scope is source-only: the shared workspace root stays at the repo root.
pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs,
.meta-updater, __patches__, .changeset, .husky, and the lint/spell configs
remain in place, so one pnpm workspace and one Cargo workspace still span
all three products. pnpr/client and pacquet/tasks/registry-mock stay as
cross-product workspace members.

Rewiring the move required:
- pnpm-workspace.yaml globs prefixed with pnpm11/
- root package.json script paths, eslint.config.mjs, tsconfig.lint.json,
  .gitignore, and CODEOWNERS updated
- .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json,
  pnpm11/__utils__, pnpm11/__typings__, and the main package directory)
- regenerated every moved package's repository/homepage URL via meta-updater
- pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts
  climb one more level to reach the repo root

.meta-updater stays at the repo root because @pnpm/meta-updater resolves
its config at <cwd>/.meta-updater/main.mjs.

TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths
change, via a dorny/paths-filter changes job plus a TS CI / Success
aggregate gate; branch protection should require only that gate.
2026-06-20 14:36:25 +02:00
Zoltan Kochan
33745b892b fix(ci): grant pull-requests: write so the review label step can label PRs (#12501) 2026-06-18 16:11:04 +02:00
Zoltan Kochan
8d17c7b8cd chore: update lockfile, Node.js, pnpm, and pacquet versions (#12441)
* chore: update lockfile, Node.js, pnpm, and pacquet versions

* fix: node range

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-18 14:35:34 +02:00
Zoltan Kochan
6ee484e3c5 fix(ci): run PR review automation for fork PRs via workflow_run (#12493)
Approvals on PRs from forks never got the `reviewed: coderabbit` label or a
Discord announcement. A pull_request_review run triggered by a forked PR is
granted a read-only token and no secrets, so the label step failed with HTTP
403 and the Discord step had no webhook.

Split the automation in two: the pull_request_review workflow now only records
the approval as an artifact (no token, no secrets), and a new workflow_run
companion runs from the default branch in base-repo context — where it has the
write-scoped token and secrets — to add the label and post to Discord.

The privileged half never checks out or executes PR content: it reads inert
data files, validates the PR number is an integer, maps the reviewer to a fixed
label, requests only actions:read + issues:write, surfaces a failed (vs absent)
artifact download instead of passing as a silent no-op, and scopes the Discord
webhook secret to the announce step.

Also documents that agents must open PRs using .github/pull_request_template.md,
since gh pr create does not apply it automatically.
2026-06-18 14:25:52 +02:00
Zoltan Kochan
7a313077fe fix: restore CodeRabbit and Qodo PR auto-approval (#12474)
* chore: stop configuring CodeRabbit pre-merge checks

Defining any pre-merge check turned on the whole pre-merge-checks
subsystem, which under Request Changes Workflow replaces the
auto-approve flow: CodeRabbit emits a pre-merge-checks status block
instead of approving once review threads are resolved, leaving PRs at
REVIEW_REQUIRED even with all threads cleared and all checks green.

Remove the title check (added in pnpm/pnpm#12460) to restore
auto-approval and document why the block is intentionally absent.
Conventional Commits on the squash title stay covered by the commit-msg
hook and the squash-title convention.

* fix: declare Qodo auto-approval under [config]

enable_auto_approval and auto_approve_for_no_suggestions were declared
under [pr_reviewer], where Qodo never reads them, so auto-approval never
ran. Per the Qodo docs both keys belong under [config]. Move them and
drop the now-empty [pr_reviewer] section.

* chore: stop applying labels from Qodo

Product labels are applied by CodeRabbit (auto_apply_labels +
labeling_instructions in .coderabbit.yaml). Having Qodo also publish
labels via enable_custom_labels/publish_labels/custom_labels meant two
tools managing the same labels. Drop the Qodo label config and leave
labeling to CodeRabbit.

* ci: tag Qodo-approved PRs and fix the label step

Add an on-qodo-approval job that applies the 'reviewed: qodo' label when
qodo-free-for-open-source-projects[bot] approves, mirroring the existing
CodeRabbit job.

The label step used 'gh pr edit --add-label', which is broken (it errors
on the deprecated Projects-v1 GraphQL field), so adding the label failed
on approval. Switch all three jobs to the issues REST API
(POST /repos/{owner}/{repo}/issues/{number}/labels) instead.
2026-06-17 14:15:46 +02:00
Zoltan Kochan
c1f9cdfe95 ci: remove TS CI / Success aggregate gate job (#12466) 2026-06-17 10:03:40 +02:00
Zoltan Kochan
c7950e7fdb fix(ci): grant issues: write so the approval label step can run (#12464)
The pr-review-automation workflow merged in pnpm/pnpm#12462 fails at the
label step with 'Resource not accessible by integration
(addLabelsToLabelable)'. gh pr edit --add-label uses the GraphQL
addLabelsToLabelable mutation, which requires issues: write even when
labeling a PR; pull-requests: write alone is insufficient. Grant it per
job, matching pacquet-integrated-benchmark-comment.yml.
2026-06-17 08:07:08 +02:00
Zoltan Kochan
c38f68aecc ci: PR review automation (CodeRabbit approval + maintainer automerge) (#12462)
* ci: label and notify on CodeRabbit approval

Add a workflow that fires on pull_request_review and, when CodeRabbit
submits an approving review, applies the informational "reviewed:
coderabbit" label and (if a DISCORD_WEBHOOK secret is configured) posts
a notice to Discord.

PR fields reach the steps through the environment rather than script
interpolation, so an attacker-controlled PR title cannot inject shell
commands. The Discord step is skipped when the webhook secret is unset.

* ci: flag maintainer-approved PRs for automerge

Rename the approval workflow to cover both review automations and add a
second job: when zkochan submits an approving review, apply the existing
"state: automerge" label.

Like the CodeRabbit job, the PR number is passed through the environment
rather than interpolated into the run script.

* ci: harden PR review automation per review feedback

- Scope permissions to each job (top-level permissions: {}) instead of
  granting pull-requests: write workflow-wide (zizmor).
- Set allowed_mentions to {parse: []} on the Discord payload so a PR
  title containing `@everyone`/`@here` cannot ping the server.
- Add connect/overall timeouts and retries to the Discord curl call.
2026-06-17 01:33:50 +02:00
Zoltan Kochan
7c97d9e680 ci: require a single aggregate gate job per workflow (#12453)
Branch protection required the individual Rust CI and TS CI jobs as
status checks. That is brittle: most Rust jobs skip on PRs that touch no
Rust files, and a matrix job skipped at the job level never expands its
'${{ matrix.* }}' name — GitHub reports one check under the literal
unexpanded name, so the required per-OS contexts never appear and any
non-Rust PR blocks forever waiting for status that is never reported.

Add one static-named aggregate gate per workflow ('Rust CI / Success'
and 'TS CI / Success') that runs with 'if: always()' and fails only when
a dependency actually failed or was cancelled; skipped dependencies
count as a pass. Branch protection can then require just these two
contexts, decoupling them from the matrix shapes.

TS CI runs on both push and pull_request, and on same-repo PRs the
pull_request run skips every job for deduplication. A naive gate would
report a green 'TS CI / Success' on that skipped run and let a PR merge
before the real push run finished. The gate's name is therefore
'TS CI / Success' only on runs that do real work, and a different name
on the skipped duplicate run — using 'compile-and-lint''s 'if:' verbatim,
which the name MUST stay in sync with. Rust CI needs no such guard: its
push trigger is restricted to main, so a PR produces a single run.

Requires a branch-protection update: replace the per-job Rust CI / TS CI
contexts with 'Rust CI / Success' and 'TS CI / Success'.
2026-06-16 19:10:53 +02:00
dependabot[bot]
8cec791e38 chore(deps): bump the github-actions group across 1 directory with 4 updates (#12356)
Bumps the github-actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [github/codeql-action](https://github.com/github/codeql-action), [taiki-e/install-action](https://github.com/taiki-e/install-action) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](de0fac2e45...df4cb1c069)

Updates `github/codeql-action` from 4.36.0 to 4.36.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](7211b7c807...8aad20d150)

Updates `taiki-e/install-action` from 2.79.14 to 2.81.8
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](873c7452ca...0631aa6515)

Updates `crate-ci/typos` from 1.47.0 to 1.47.2
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](f8a58b6b53...37bb98842b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.47.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: taiki-e/install-action
  dependency-version: 2.81.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 15:17:49 +02:00
Zoltan Kochan
48359ad213 fix(ecosystem-e2e): absolute /bin/sh for build, build binaries once in CI (#12440)
* fix(ecosystem-e2e): run the build script via absolute /bin/sh

The build stage sets the child PATH to prepend the project's
node_modules/.bin, and Rust resolves a bare program name against that child
PATH. Spawning `sh` therefore let a dependency-installed `.bin/sh` run in
place of the system shell — running unintended code and masking build
pass/fail. Use the absolute `/bin/sh` so the orchestrating shell is always
the system one; the script it runs still finds framework bins through the
prepended PATH.

* ci(ecosystem-e2e): build pnpm and pacquet once, share via artifacts

The stack matrix rebuilt the pacquet release binary and the pnpm bundle in
every one of its seven jobs. Add a single build job that compiles pacquet,
the harness, and the pnpm bundle once and uploads them as an artifact; the
per-stack jobs download it and run, so the multi-minute Rust and bundle
builds happen once per run instead of seven times.
2026-06-16 08:56:19 +02:00
Zoltan Kochan
70f1343d9a feat(ecosystem-e2e): ecosystem test harness across pnpm/pacquet and node_modules layouts (#12439)
* feat(ecosystem-e2e): add harness installing real JS stacks across pnpm/pacquet and layouts

Adds `pacquet/tasks/ecosystem-e2e`, a Yarn-PnP-style ecosystem test that
installs, builds, and serves real framework scaffolds (Next.js, Vite,
Angular, Astro, SvelteKit, Nuxt, React Router 7) across the cross product
of {pnpm, pacquet} x {isolated, global virtual store}.

The binary axis catches pnpm/pacquet parity gaps; the layout axis catches
breakage introduced by the global virtual store. Each cell scaffolds the
project once, installs with the binary under test, runs the build, then
boots the production server and probes it over HTTP — so a green cell means
the produced node_modules works at runtime, not just at bundle time.

Runs on a daily cron (one job per stack), not per-PR: the installs are slow
and track upstream framework releases, so a red cell is investigated rather
than treated as a merge blocker.

* style(ecosystem-e2e): use ASCII ellipsis in comments for dylint

* fix(ecosystem-e2e): address PR review findings

- workflow: wrap `pnpm/bin/pnpm.cjs` for the `--pnpm` shim; the built bundle
  is `dist/pnpm.mjs`, so the previous `dist/pnpm.cjs` path never existed and
  the shim would have failed to launch the repo-built pnpm.
- keep: `--keep` now reuses an already-scaffolded template instead of
  re-running the generator into a non-empty directory (which fails).
- serve: retry HTTP 4xx/5xx until the deadline rather than failing on the
  first one — dev servers can briefly answer error statuses while warming up.
- serve: the probe reads only the status line, not the whole response body.
- stacks: pin the create-astro, sv, nuxi, and create-react-router generators
  to a major version instead of tracking the latest tag, matching the stated
  reproducibility intent for the scheduled run.

* fix(ecosystem-e2e): scrub subprocess env, bound CI job, fix README fence

- security: scaffold/install/build/serve run third-party lifecycle code, so
  they now launch via `sandboxed_command`, which clears the inherited
  environment down to a small allowlist (PATH, HOME, proxy/cert/locale vars,
  plus PORT/HOST for servers). An unattended dependency build script can no
  longer read ambient CI secrets such as the workflow token.
- workflow: add a 60-minute job timeout so a hung build or serve subprocess
  can't pin a runner indefinitely.
- docs: give the README's grid code block a language identifier (MD040).

* fix(ecosystem-e2e): spawn serve argv directly and reset cell log per run

- serve: run the tokenized serve command directly off the project's
  node_modules/.bin instead of joining tokens into a `sh -c` string, so
  argument boundaries survive and a token that needs quoting can't break the
  launch. The spawned PID is the server itself, so teardown still works.
- log: truncate cell.log at the start of each run so reruns (notably under
  --keep) don't interleave with stale output.
2026-06-16 08:15:28 +02:00
Zoltan Kochan
41b74e792e fix(ci): refresh update-lockfile install state (#12420) 2026-06-15 12:25:06 +02:00
Zoltan Kochan
a8c4704ac0 ci: structure CI check names (#12417) 2026-06-15 11:57:14 +02:00
Zoltan Kochan
bc13e49f96 ci: keep Dylint required check reportable (#12415) 2026-06-15 09:03:21 +02:00
Zoltan Kochan
05e82c945e ci: upload partial CI performance results (#12411) 2026-06-15 01:09:45 +02:00
Zoltan Kochan
18cba025ed ci: resolve merged PRs for Bencher uploads (#12407) 2026-06-14 23:20:15 +02:00
Zoltan Kochan
5f63458644 fix: bound descendant-process lookup on error exit to avoid a Windows hang (#12403)
## Problem

On Windows, **any failed `pnpm` command hangs 20–46 seconds before exiting.** The error handler (`pnpm/src/errorHandler.ts`) enumerates descendant processes via `pidtree` to terminate them on every error exit. On Windows `pidtree` shells out to `wmic` and, where wmic has been removed, a PowerShell `Get-CimInstance Win32_Process` fallback — a process listing that takes tens of seconds on busy CI runners.

This also broke Windows CI: the `verifyDepsBeforeRun/*` e2e suites are full of intentional-failure assertions (e.g. `pnpm start` with `--config.verify-deps-before-run=error` when deps aren't installed). Each failure paid the ~23 s error-handler tax, so the suite blew past the 70-minute cap. `pnpm install` and success paths never hit the error handler, which is why only failures were slow.

Diagnosed by sampling `process.getActiveResourcesInfo()` during the hang: it showed a lingering `ProcessWrap` (a spawned child), and hooking `child_process.spawn` named it (`wmic` → `powershell … Get-CimInstance Win32_Process`, exiting after ~23–46 s).

## Fix

Race the descendant-process lookup against a 2 s timeout. If it doesn't return in time, skip the kill and exit — `exit()` calls `process.exit`, which abandons the still-running (harmless, read-only) process query instead of blocking on it. The fast path (Unix, fast Windows) is unchanged.

Confirmed on Windows CI: the failing `start` invocations dropped from **~23 s to ~2.7 s**, and `multiProjectWorkspace.ts` went from **716 s to 124 s**.

## Also included

The CI pnpr-binary cache is split into `restore` + an explicit `save` step that runs right after the build, so a failing test step no longer discards the ~20-minute Rust build (the combined `actions/cache` only saved in a post-job step that gets skipped on failure).
2026-06-14 18:45:49 +02:00
Zoltan Kochan
9ddc86b635 ci: track test suite durations in bencher (#12404)
## Summary

Adds CI duration tracking for the `pnpm-ci-performance` Bencher project.

Tracked Rust testbeds and benchmarks:

- `pacquet.ubuntu`, `pacquet.windows`, `pacquet.macos` -> `tests.all`
- `pnpr.ubuntu`, `pnpr.windows`, `pnpr.macos` -> `tests.all`

Tracked pnpm testbeds and benchmarks for full test runs:

- `pnpm.ubuntu.node22`, `pnpm.ubuntu.node24`, `pnpm.ubuntu.node26` -> `tests.all`, `tests.cli`
- `pnpm.windows.node22`, `pnpm.windows.node24`, `pnpm.windows.node26` -> `tests.all`, `tests.cli`

The test workflows produce Bencher-compatible JSON artifacts without receiving `BENCHER_API_TOKEN`. A separate `workflow_run` workflow downloads those artifacts only for same-repository runs, validates their metadata, and uploads from trusted workflow code using the existing `BENCHER_API_TOKEN` secret. The pnpm CLI e2e duration is extracted from `pnpm run --report-summary` output during the same full-test execution, so the CLI e2e suite is not run a second time.
2026-06-14 18:44:17 +02:00
Zoltan Kochan
94c13cc068 ci: run clippy as a single-OS job and add it to the pre-push hook (#12389)
* fix(pnpr): pass batch_publish test request bodies by reference

The put_json/put_json_with_token test helpers took the JSON body by
value but only borrowed it for serde_json::to_vec, tripping clippy's
needless_pass_by_value under --all-targets. Take &Value instead, which
also drops an unnecessary body.clone() at one call site.

* ci: run clippy as a single-OS job and add it to the pre-push hook

Clippy was a step inside the three-OS Lint-and-Test matrix, so it ran
once per OS even though it lints the same platform-independent source
each time. Move it to its own ubuntu-only job, mirroring the existing
single-OS doc, format, and dylint jobs (platform-gated cfg blocks are
still type-checked per-OS by the test build).

It was also missing from pacquet/scripts/pre-push-rust.sh, so a clippy
lint that only fires under --all-targets — like the one that just
reached main — slipped past local pushes and surfaced only in CI. Add
the same --all-targets workspace clippy gate to the hook.
2026-06-13 20:23:16 +02:00
Zoltan Kochan
84bb4b1a04 perf: close the warm-resolve, symlink-churn, and download-concurrency gaps (#12329)
## Motivation

The [vlt.sh benchmarks](https://benchmarks.vlt.sh/) (2026-06-11 run, pacquet 0.11.3) show pacquet several times slower than the fastest package managers in the warm-metadata fresh-resolve cells (`cache`: 3.9–8.1x), the cold-cache frozen-install cells (`lockfile`: up to 10x on vue), and `clean`. Profiling the babylon and vue fixtures locally (macOS time profiles of the warm fresh resolve and the install tail) surfaced three independent causes, fixed here.

## Changes

### 1. Deprecation probing without manifest hydration (pacquet)

With `minimumReleaseAge` active (the default), every range pick runs `filter_pkg_metadata_by_publish_date`, and any dist-tag pointing outside the maturity cutoff (`next`, `beta`, `canary`, a too-fresh `latest`) repopulates by scanning all candidates and reading each candidate's `deprecated` flag. Each read hydrated the full version manifest — a complete `serde_json` parse including the flattened catch-all map. On babylon's warm fresh resolve this was the single largest CPU consumer (~10 thread-seconds, all on the resolve task's critical path).

`PackageVersions::is_deprecated` now answers from the raw fragment (substring pre-check, then a single-field deserialize with the same normalization as `PackageVersion::deprecated`), the tag-repopulation loop parses candidate versions once per filter call (mirroring the `parsedSemverCache` in pnpm's `filterPkgMetadataByPublishDate`), and the deprecated-pick fallback uses the probe instead of hydrating every version.

**babylon warm fresh resolve: `resolve_workspace` 7.5s → 2.6s.**

### 2. Relative-symlink up-to-date check (pacquet)

`force_symlink_dir` joined an existing link's relative contents onto the link parent and compared the result *verbatim* against the wanted target. Virtual-store links contain `..` segments (`../../<pkg>/node_modules/<name>`), so the joined path never compared equal and every up-to-date symlink was unlinked and recreated. Node's `path.relative` — which upstream `symlink-dir`'s `isExistingSymlinkUpToDate` builds on — resolves its arguments, so pnpm treats those links as current. Both sides now pass through `lexical_normalize`. The babylon install tail was dominated by exactly this unlink+symlink churn.

**babylon warm install: 6.8s → 4.7s; warm frozen install: 4.2s → 2.3s.**

### 3. Default network concurrency floor 16 → 64 (pnpm + pacquet)

The default was `min(64, max(workers * 3, 16))`. Downloads are I/O-bound, not CPU-bound: on a 4-vCPU CI runner the formula yields 16 concurrent requests, so a low-latency registry drains 600–1300-tarball installs 16 at a time while staying unsaturated — a large share of the cold-cell (`lockfile`/`clean`) gap on the benchmark runners. The default is now `min(96, max(workers * 3, 64))`; the `networkConcurrency` setting still overrides it. Applied to `@pnpm/installing.package-requester`, the lockfile-resolution verifier fan-out that mirrors its floor, and the same two spots in pacquet. Changeset included (minor). **This is a user-visible defaults change on both stacks — flagging it explicitly for review.**

## Local results (M-series macOS, vlt fixtures, isolated store/cache)

| cell | before | after |
|---|---|---|
| vue `cache` | 1159 ms | **479 ms** |
| vue `cache+lockfile` | 621 ms | **392 ms** |
| vue no-op install | 48 ms | **41 ms** |
| babylon `cache` | ~8.8 s | **4.75 s** |
| babylon `cache+lockfile` | ~4.2 s | **2.27 s** |

vue's warm cells are now ahead of every competitor measured locally; babylon's `cache` cell closed from ~2.5x behind the leader to ~1.35x (the remainder is the per-file store-integrity verify and per-file linking that the pnpm store contract requires).

## Validation

- `cargo nextest`: registry, resolving-npm-resolver, resolving-deps-resolver, lockfile-verification, network, fs, tarball, package-manager, cli — 1300+ tests, all green; new unit tests cover the deprecation probe (string/bool/empty/corrupt shapes, nested-key false positives) and cross-parent relative-symlink reuse (fails without the fix).
- Lockfile stability: `--lockfile-only` output is byte-identical before/after on vue; on babylon the resolved **package-version sets are identical across 6 runs (3 per binary)**. The babylon lockfile does flap between runs in the peer-suffix shape of `webpack-dev-server@5.2.2` (`(bufferutil@4.1.0)(utf-8-validate@5.0.10)` appearing/disappearing) — this is **pre-existing nondeterminism** reproducible with the unmodified binary against itself, in the optional-peer area; worth a separate issue.
- Pre-push checks (fmt, taplo, `cargo doc -D warnings`, dylint) pass; eslint (root config) and `tsgo --build` pass for the two touched TS packages.
2026-06-11 19:39:45 +02:00
Zoltan Kochan
5aed1200ea feat: add musl binaries for pacquet and pnpr (#12316)
Summary:
- add Linux musl binary package selection to the pacquet and pnpr npm shims
- generate linux-x64-musl and linux-arm64-musl native npm packages with libc metadata
- build musl Rust release targets for both pacquet and pnpr
- update package docs and cspell entries for the touched workflow files
2026-06-10 18:10:36 +02:00
Zoltan Kochan
3d50680eda fix(security): verify Node.js runtime SHASUMS OpenPGP signature (#12295)
Follow-up to #12292 (which verifies the **package-manager** binary). This closes the same class of gap for the **Node.js runtime**.

When a repository requests a Node.js runtime — `devEngines.runtime: node@X` (with `onFail: download`, the default) or `useNodeVersion` — pnpm downloads and then executes a Node binary (it's used to run lifecycle / `run` / `exec` scripts). The download **mirror is repository-configurable** via `node-mirror:<channel>` (`nodeDownloadMirrors`) in project `.npmrc`, and the integrity comes from `SHASUMS256.txt` fetched **from that same mirror**.

That's a circular check: a malicious mirror serves a tampered `node` tarball **and** a matching `SHASUMS256.txt`, the sha256 check passes, and pnpm runs the binary. Drive-by on a normal command in a cloned repo.

## Fix

pnpm now fetches `SHASUMS256.txt.sig` and verifies its **detached OpenPGP signature** against the **Node.js release team's public keys, embedded in the pnpm CLI**, before trusting the hashes. A mirror that serves a tampered binary cannot also produce a valid signature, so verification fails. Any faithful mirror (one that proxies the real signed SHASUMS) keeps working.

- `@pnpm/crypto.shasums-file`: new `fetchVerifiedNodeShasums` / `fetchVerifiedNodeShasumsFile` verify the signature via `openpgp` against the embedded keys.
- The keys live in a generated file (`src/nodeReleaseKeys.ts`, 28 keys) mirrored from the canonical `nodejs/release-keys` list. `crypto/shasums-file/scripts/update-node-release-keys.mjs` keeps them current (`pnpm check:node-release-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate so a new release signer can't silently break verification.
- `@pnpm/engine.runtime.node-resolver` verifies the **configurable-mirror** SHASUMS. The hardcoded `unofficial-builds.nodejs.org` musl mirror is **not** repo-configurable and is signed by a different key, so it stays trusted over TLS.

## Scope

- **Pre-release channels (rc, nightly, …) are not verified** — Node only signs the `release` channel (no `SHASUMS256.txt.sig` exists for them, even on nodejs.org), so they remain unverifiable. Verification is gated on the `release` channel.
- **Bun / Deno are unaffected** — their download/SHASUMS URLs are hardcoded to canonical GitHub (`github.com/oven-sh/bun`, `api.github.com/repos/denoland/deno`), not mirror-configurable, so a repo can't redirect them.
- **Pacquet parity:** `pacquet/crates/engine-runtime-node-resolver` has the same mirror-configurable SHASUMS logic and needs the equivalent Rust port — tracked as a follow-up (per the repo's parity rule, opening the TS side first).
2026-06-10 00:33:31 +02:00
Zoltan Kochan
5f2bb9f5ba fix(security): verify npm registry signature before spawning a package-manager binary (#12292)
pnpm can be made to download and execute a native binary through two **repository-controlled** inputs, neither of which was authenticated before this change:

1. **pacquet install engine** — declaring `pacquet` (or `@pnpm/pacquet`) in `configDependencies` (in `pnpm-workspace.yaml`) opts in to pnpm's Rust install engine, and pnpm spawns the platform binary `@pacquet/<platform>-<arch>` during `pnpm install`.
2. **package-manager version switch** — the `packageManager` / `devEngines.packageManager` field makes pnpm download and run a specific pnpm version. This is **on by default** (`onFail` defaults to `download`) and also covers `pnpm self-update` and `pnpm with`.

In both cases the repository also controls the lockfile integrity and the registry the bytes are fetched from (via `.npmrc`), so matching the lockfile integrity proves nothing — it matches the hash the attacker wrote. A cloned, untrusted repository could therefore execute an arbitrary native binary just by running a normal pnpm command.

## Fix (corepack-style registry-signature verification)

pnpm now verifies the **npm registry signature** of the bytes it is about to spawn, **over the installed integrity**, against npm's public signing keys that **ship embedded in the pnpm CLI** (exactly as corepack does). If the bytes on disk were substituted or tampered with, npm's real signature does not validate over them.

- New reusable `verifyInstalledPackageSignatures()` in `@pnpm/deps.security.signatures` verifies `name@version:integrity` against `dist.signatures` using the embedded keys.
- Because the keys are **embedded** (not fetched), a registry the user did not vouch for cannot supply its own keypair to forge a signature. The signed packument is fetched from the **configured** registry, so an **npm mirror works transparently** — it proxies the same signed packument, with no configuration. There is intentionally **no runtime override or off-switch** for the keys.
- **pacquet** (`installing/commands`): verifies the `pacquet` shim and the host platform binary. It **fails the command** if the signature does not verify or cannot be checked (e.g. registry unreachable); the only graceful fallback to pnpm's own engine is when pacquet has no binary for the current platform.
- **pnpm engine** (`engine/pm/commands`): verifies `pnpm`, `@pnpm/exe`, and the host platform binary, **only on a store cache miss** (an actual download), so it adds no network round trip to every command. It **fails closed** — any verification failure, including an unreachable registry, refuses the version switch rather than running an unverified binary.

## Keeping the embedded keys fresh

The embedded keys live in a generated file. `deps/security/signatures/scripts/update-npm-signing-keys.mjs` keeps them in sync with npm's keys endpoint (`pnpm check:npm-signing-keys` / `--update`), and the **create-release-pr** workflow runs the check as a gate, so a key rotation cannot silently break verification — a stale key set blocks the release until refreshed.

## Pacquet parity

pacquet gained `configDependencies` support on `main` (#12285), but it has **no install-engine-spawn sink** — pacquet *is* the engine, and it does not select/spawn an alternate engine from `configDependencies` (its only config-dependency code-execution path is `updateConfig` plugin pnpmfiles, which it shares with pnpm and which this advisory does not cover). So CAND-PNPM-097 has no pacquet-side analog; no pacquet code change is needed.
2026-06-09 23:37:20 +02:00
Zoltan Kochan
b1a6f4d4b5 fix(deps-resolver): align pacquet peer parent contexts (#12273)
- align pacquet peer parent context handling with pnpm for same-package child providers and peer diamonds
- keep optional cached peer resolutions bubbling to later parents without an explicit provider, matching the jest-config and @types/node case
- preserve pnpm's duplicated peer-suffix segments for aliased providers that resolve to the same package
- include aliased child providers when their real package name is peer-relevant, not only when the install alias is peer-relevant
- limit importer-seeded peer parent refs to alias/real names that can affect peer resolution, reducing clone overhead
- add focused resolver tests for the issue 12272 lockfile mismatch and related diamond/alias-provider behavior
- port the six related pnpm CLI alias-peer install cases to Pacquet CLI tests
- pin the alias-peer CLI test peer suffix length so exact lockfile suffix assertions are independent of global config
- update the Pacquet coverage upload to codecov-action v6.0.2 so it uses Codecov's current signing-key configuration

Fixes pnpm/pnpm#12272.
2026-06-09 07:16:58 +02:00
Zoltan Kochan
0dc770aa7b perf(pacquet): share virtual-store slot linking pass (#12251)
Refs: pnpm/pnpm#12250

- share warm/cold virtual-store slot linking through one parallel helper
- emit structured pacquet install phase metrics for virtual-store partition sizes and link-slot elapsed time
- generate integrated-benchmark diagnostics artifacts and use them for the fresh pnpr cold-batch and pnpr-vs-direct guardrails
- split client registry latency/bandwidth from pnpr server registry latency, while rewriting server-origin tarball URLs back to the measured client registry path
- keep the benchmark PR comment focused on scenario tables and collapsed raw JSON; diagnostics stay available as artifacts instead of inline report noise
2026-06-07 23:27:15 +02:00
Zoltan Kochan
c199198e94 perf(pnpr): stream /v1/resolve, and fix the integrated benchmark to actually exercise pnpr (#12237)
Closes #12234.

This PR has two parts. The headline turned out to be the **benchmark**, not the feature.

## Part 1 — `/v1/resolve` streaming (the #12234 feature)

`POST /v1/resolve` now streams **NDJSON** instead of buffering the whole lockfile: one `package` frame per resolved tarball as the server's tree walk yields it, then a terminal `done` (full lockfile + stats), `error`, or `violations` frame. The client fetches each tarball as its frame arrives, overlapping the server's resolution — matching the native `PrefetchingResolver` shape.

- Server: new `ResolutionObserver`/`ObservingResolver` in `package-manager`, threaded through `Install`; `handle_resolve` runs the resolve in a detached task whose observer pushes frames into the response channel; `application/x-ndjson` is excluded from the gzip layer so frames flush incrementally.
- Client: `resolve_streaming(opts, on_package)`; `install_via_pnpr` drives a new `TarballPrefetcher` that warms the shared mem cache as frames arrive.
- Breaking change within protocol v1 (no version bump — experimental pnpr allows it).

**Honest caveat:** streaming only helps when the *server's* resolve is slow (cold/distant server). Against a warm server it's inert — see the results below (`pnpr@HEAD` ≈ `pnpr@main` everywhere). Whether to keep this commit or defer it is an open question; the benchmark fixes below stand on their own.

## Part 2 — make the integrated benchmark actually measure pnpr

While validating Part 1, the benchmark turned out not to be exercising pnpr **at all**, plus it was serving the registry far faster than reality. Fixes:

- **`pnpr@<rev>` targets never routed through pnpr.** `.pnpr-env` exported a bare `PNPR_SERVER`, but pacquet reads config env vars only under the `PNPM_CONFIG_*` prefix, so `config.pnpr_server` was always `None` and every pnpr row was a silent duplicate of its direct row. Fixed the env var name; added a post-run guard that fails the benchmark if a pnpr target's `pnpr-storage` is empty (proof it never served a resolve).
- **Emulate a real registry link for every client.** The latency proxy modeled RTT but not throughput, and fronted only direct targets. Generalized it to a `LinkProfile` (one-way delay + per-direction bandwidth cap), added `--registry-bandwidth-mbps`, and routed *all* registry traffic through it (direct installs, the pnpr server's resolve, the pnpr client's fetches) so the registry-mock is uniformly as remote as real npm. CI runs it at 50 ms + 200 Mbit/s (≈ the measured public-npm peak).
- **Make "cold cache" cold for resolution.** Forced `cacheDir` bench-local and wipe it in cold-cache scenarios, so a direct install actually pays the packument-fetch waterfall (previously the global metadata mirror survived every wipe).
- **New scenario** `fresh-install.cold-cache.hot-store` that isolates resolution (cold metadata, hot store → no download to mask it).

## Results (Linux CI, after the fixes)

| Scenario | pacquet@HEAD | pnpr@HEAD | pnpr speedup |
|---|---:|---:|---:|
| fresh-install · cold cache · **hot store** | 5.06 s | 0.69 s | **7.4×** |
| fresh-install · cold cache · cold store | 5.36 s | 2.03 s | **2.65×** |
| fresh-install · hot cache · hot store | 1.46 s | 0.69 s | **2.1×** |
| fresh-restore (frozen) · cold cache · cold store | 10.08 s | 5.13 s | **2.0×** |
| fresh-restore (frozen) · hot cache · hot store | 0.71 s | 0.80 s | 0.89× (slower) |

pnpr offloads the client's resolution (and, on the frozen path, lockfile verification) to its warm server: 2–7× faster wherever the client would otherwise pay that cost. The lone regression is the fully-warm frozen install, where there's nothing to offload and pnpr's one round trip is pure overhead. `pnpr@HEAD` vs `pnpr@main` is flat throughout — i.e. the streaming commit (Part 1) adds ~nothing against a warm server, while the base pnpr win (offload to a warm server) is large.
2026-06-06 15:25:08 +02:00
dependabot[bot]
04473e027c chore(deps): bump the github-actions group across 1 directory with 10 updates (#12220)
Bumps the github-actions group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.35.5` | `4.36.0` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.0.0` | `4.1.0` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.0.0` | `4.1.0` |
| [docker/login-action](https://github.com/docker/login-action) | `4.1.0` | `4.2.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.1.0` | `7.2.0` |
| [taiki-e/install-action](https://github.com/taiki-e/install-action) | `2.78.1` | `2.79.14` |
| [crate-ci/typos](https://github.com/crate-ci/typos) | `1.46.1` | `1.47.0` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `6.0.0` | `6.0.1` |
| [cbrgm/mastodon-github-action](https://github.com/cbrgm/mastodon-github-action) | `2.2.0` | `2.2.1` |
| [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.5.5` | `0.5.6` |



Updates `github/codeql-action` from 4.35.5 to 4.36.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](9e0d7b8d25...7211b7c807)

Updates `docker/setup-qemu-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](ce360397dd...06116385d9)

Updates `docker/setup-buildx-action` from 4.0.0 to 4.1.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](4d04d5d948...d7f5e7f509)

Updates `docker/login-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](4907a6ddec...650006c6eb)

Updates `docker/build-push-action` from 7.1.0 to 7.2.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](bcafcacb16...f9f3042f7e)

Updates `taiki-e/install-action` from 2.78.1 to 2.79.14
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](184183c240...873c7452ca)

Updates `crate-ci/typos` from 1.46.1 to 1.47.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](5374cbf686...f8a58b6b53)

Updates `codecov/codecov-action` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](57e3a136b7...e79a6962e0)

Updates `cbrgm/mastodon-github-action` from 2.2.0 to 2.2.1
- [Release notes](https://github.com/cbrgm/mastodon-github-action/releases)
- [Commits](776364a15d...244bbe72e6)

Updates `zizmorcore/zizmor-action` from 0.5.5 to 0.5.6
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](a16621b09c...5f14fd08f7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: 7.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: taiki-e/install-action
  dependency-version: 2.79.14
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: cbrgm/mastodon-github-action
  dependency-version: 2.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-05 20:56:11 +02:00
Zoltan Kochan
69354b288f chore(release): key changeset-released ledger by target branch (#12201)
Releases now land via a PR from an ad-hoc branch rather than a commit
pushed straight to the target, so `bump.ts` keyed the ledger off the
ephemeral PR branch and scattered each release into its own file instead
of accumulating in `main.txt`.

Recover the target from a `release-pr/<target>` branch name, add a
manually-dispatched workflow that creates such a PR, and drop the
accumulated per-PR ledger files (verified inert: no overlap with pending
changesets, no live branch reintroduces a released changeset on merge).
2026-06-04 23:32:47 +02:00
Zoltan Kochan
69cfcb7417 perf(ci): cache benchmark binaries per commit instead of rebuilding (#12173)
The integrated-benchmark "Precompile benchmark revisions" step took ~14
minutes every run. Two causes:

1. The "Cache Rust builds" step cached the multi-GB
   `bench-work-env/*/pacquet/target` dirs under a 1-minute restore
   timeout. A restore that large never finished in 60s, so (with
   `continue-on-error`) the cache silently missed and every run built all
   four targets cold.
2. `pacquet@HEAD` and `pnpr@HEAD` resolve to the same commit but built in
   separate clones, compiling the `pacquet` binary twice (same for main).

Cache the compiled binaries per *resolved commit* instead:

- Orchestrator: a `--reuse-prebuilt-binaries` flag skips the clone +
  `cargo build` for a target whose output binary is already present (i.e.
  restored from cache). Targets are built pnpr-first; since a `pnpr@<rev>`
  build also produces the `pacquet` client binary, a same-revision
  `pacquet@<rev>` reuses it by copy rather than recompiling the commit.
- Workflow: resolve the HEAD/main SHAs, then cache the two `pnpr@<rev>`
  binaries keyed on the commit (they cover all four targets via the
  dedup-copy). `main` is a near-certain hit on PRs (stable SHA) and a
  same-HEAD re-run hits HEAD too, so only a fresh HEAD compiles. Drop the
  giant `bench-work-env/*/pacquet/target` cache (the small binary caches
  restore in seconds, with no eviction risk) and keep a cargo-deps +
  orchestrator-target cache with a realistic 3-minute timeout.

A fresh-HEAD run now compiles one workspace once (~half the old work);
re-runs and main reuse cached binaries and skip compilation entirely.
2026-06-03 17:26:49 +02:00
Zoltan Kochan
65c9bef283 ci(pnpr): inject network latency into the install-accelerator benchmark (#12166)
## What

The pnpr install accelerator is a **remote** server, but the integrated benchmark ran it on **loopback** (RTT ≈ 0), which hides the round-trip cost that dominates a real install — and that pnpr exists to reduce. This injects network latency so the benchmark measures pnpr as the remote service it is in production.

## How

A dependency-free, synchronous latency-injecting TCP proxy (`latency_proxy`) plus two knobs on `integrated-benchmark`:

- **`--pnpr-latency-ms`** — fronts each `pnpr@<rev>` server, so the client↔server link pays the given round trip (half each direction).
- **`--registry-latency-ms`** — fronts the registry for the direct (`pacquet`/`pnpm`/`--with-pnpm`) targets, so a direct install crosses the same network.

`pnpr@<rev>` targets keep a **direct (fast) registry link** — that models a warm, colocated server, so pnpr's advantage shows up as **fewer round trips, not a faster backend**:

```
direct target:  client → [latency proxy] → registry
pnpr target:     client → [latency proxy] → pnpr server → (direct) → registry
```

The workflow sets both equal (`50ms`) so the in-run pnpr-vs-direct ratio is fair and the `pnpr` Bencher testbed (pnpr@HEAD vs pnpr@main) becomes **sensitive to protocol round-trip-count changes** — which is what makes the upcoming protocol work (collapsing the 3-round-trip handshake/install/files flow) measurable on main. See #12165 for that plan.

## Notes

- **Latency only, no bandwidth cap.** The public registry is CDN-backed and CI runners are fast, so install time is latency/round-trip bound, not throughput bound — a bandwidth cap would be overly pessimistic. A high-ceiling, opt-in bandwidth knob can follow if a slow-link scenario is ever wanted.
- Both flags **default to `0`** (current behavior unchanged); the registry proxy is also skipped in `--registry=npm` mode (already remote).
- The proxy is unit-tested (a round trip through it reflects the injected latency). `cargo check`/`clippy`/`fmt`/`dylint` clean.
- One caveat the proxy does **not** model: TLS-handshake round trips and HTTP/2 multiplexing of a real CDN — it reproduces propagation delay, the dominant and relevant factor here, not a byte-exact replica of registry.npmjs.org.
2026-06-03 15:54:36 +02:00
Zoltan Kochan
930c9d7718 ci(pnpr): benchmark the install accelerator (new Bencher pnpr testbed) (#12154)
* ci(pnpr): add pnpr@<rev> target + Bencher testbed for the install accelerator

Measures the pnpr-accelerated install path end to end. A new `pnpr@<rev>`
target in the integrated-benchmark orchestrator builds both the `pacquet`
client and the `pnpr` server from the revision's monorepo clone, boots a
per-target pnpr server with an isolated `--storage`, and points the client
at it via `PNPR_SERVER`.

Reusing the existing multi-target hyperfine model gives both comparisons:

- `pnpr@HEAD pacquet@HEAD` -> pnpr-vs-direct ratio in one run (same client,
  with and without the accelerator).
- `pnpr@HEAD pnpr@main` -> regression delta tracked in a new Bencher `pnpr`
  testbed.

Two CI workflows mirror the fork-safe two-stage pacquet pattern, triggered
on pnpr/**, pacquet/crates/pnpr-client/**, and pacquet/crates/config/**
(the pnprServer plumbing), running the hot-cache/hot-store restore and
fresh-install scenarios that model a warm long-running server.

* ci(pnpr): fold the install-accelerator bench into the pacquet workflow

The pnpr server is built from the pacquet resolver/store/tarball crates,
so any pacquet change can move the pnpr-accelerated numbers as much as the
direct ones. That means the two benchmarks share a trigger surface and
should co-run — so rather than a separate pnpr workflow posting a second
comment on every pacquet PR, measure both in one run.

The pacquet integrated-benchmark workflow now also runs `pnpr@<rev>`
targets in the two hot-cache/hot-store scenarios (a warm long-running
server is pnpr's realistic shape), emits one combined report/comment, and
uploads to two Bencher testbeds: `pacquet` (direct, all scenarios) and
`pnpr` (accelerated, hot scenarios). The trigger gains `pnpr/**`.

Deletes the standalone pnpr-integrated-benchmark{,-comment}.yml added
earlier in this branch.

* ci(pnpr): also benchmark pnpr with a cold client store

Run the pnpr targets in the cold-cache/cold-store scenarios too, not just
the hot ones. Those scenarios already wipe the client store between
iterations while the per-target pnpr server store stays warm, so this
measures pnpr's cold-client-vs-warm-server shape — the realistic CI case
(empty local store hitting a warm shared server) — alongside the existing
hot-client numbers.

Both tools now run all four scenarios, so the report tables and both
Bencher testbeds (pacquet, pnpr) cover cold and hot. Collapses the two
target-list env vars into one and bumps the cold-step timeouts for the
extra commands. Table rendering is unchanged.

* ci(pnpr): address PR review feedback

- work_env: wrap the spawned pnpr child in its PnprServer guard before the
  readiness wait and .pnpr-env write, so an early panic kills the process
  on unwind instead of leaking an orphaned server (Copilot).
- cli_args: document pnpr@<rev> in the `targets` --help text (CodeRabbit).
- workflows: guard each bencher upload on its file existing, so a missing
  optional results file logs a notice instead of failing the step (Copilot).
2026-06-03 12:01:17 +02:00
Zoltan Kochan
2b788d53fd refactor: replace the experimental pnpm-agent server with pnpr (#12151)
The experimental TypeScript `pnpm-agent` install-accelerator server is
superseded by the `pnpr` server, which implements the same protocol.
Remove `agent/server` and route the agent e2e test through pnpr.

The pnpm TypeScript client (`@pnpm/agent.client`) is kept and made
compatible with pnpr. The wire protocol carries the on-disk lockfile
format, while pnpm keeps an in-memory `LockfileObject` in process:

- Incoming: the agent's response lockfile is converted to the in-memory
  shape via `convertToLockfileObject`.
- Outgoing: the existing lockfile is read in its on-disk shape with the
  new `readWantedLockfileFile` and forwarded as-is — no in-memory
  round-trip.

pnpr now resolves multi-project workspaces by reconstructing the
workspace on disk (root manifest + `pnpm-workspace.yaml` + member
manifests) and letting pacquet's install path discover every importer.
Member dirs are written as quoted YAML scalars; importer dirs are
validated against path traversal (rejecting absolute, `..`, backslash,
and slashes-only inputs) and de-duplicated; synthetic manifest names
map injectively from dirs.

The CI test job builds the `pnpr` server from source (cached on the
Rust sources) so the agent e2e tests run against the current server.
The published `@pnpm/pnpr` is dropped as a test dependency: running the
suite already requires building `pnpr-prepare` from source (no npm
fallback), so the toolchain to build `pnpr` is always present, and the
published binary can predate the server protocol the tests exercise.
2026-06-03 01:11:24 +02:00
Zoltan Kochan
ae6251ca7d chore: extend update-lockfile workflow to bump Node.js, pnpm, and pacquet (#12135)
* chore: also bump Node.js, pnpm, and pacquet in update-lockfile workflow

* chore: address PR review feedback on update-lockfile workflow

- Base the update branch on an explicitly fetched origin/main
- Don't persist the write token during install; push with explicit URL
- Detect open PRs via gh --json instead of grepping table output
- Add a concurrency guard to serialize dispatch + scheduled runs
2026-06-02 14:55:04 +02:00
Zoltan Kochan
172d5e56e6 chore: update pnpm-lock.yaml (#12014)
* chore: update pnpm-lock.yaml

* chore: sync node.js runtime version in scripts with devEngines via meta-updater

* chore: sync node.js version in CI workflows with devEngines via meta-updater
2026-06-02 12:06:18 +02:00
Alessio Attilio
5c669d7387 feat(pacquet): add pnpmfile hooks support (#12044)
Implements Tier 4 pnpmfile hooks for pacquet (#11633, point 4.1): pacquet now discovers and runs a project `.pnpmfile` during dependency management, matching pnpm.

## What it does

- **Discovery** — looks for `.pnpmfile.mjs` then `.pnpmfile.cjs` (dotted names only, `.mjs` preferred), matching pnpm's `requireHooks`. Only actual files are accepted (`is_file()`).
- **`readPackage`** — wired into resolution. Mirrors pnpm's `requirePnpmfile` contract: the four dependency fields are defaulted to `{}` before the hook runs, and the returned manifest is validated (must be a non-null object whose dependency fields, when present, are objects rather than arrays). A throwing/syntactically-invalid pnpmfile, a missing `require`, or a hook that returns nothing aborts the install (`PNPMFILE_FAIL`) instead of being silently ignored.
- **`afterAllResolved`** — wired into the lockfile write. The resolved lockfile is passed to the hook and its return value is what gets written to `pnpm-lock.yaml`. The round-trip goes through `serde_json::Value` (the workspace already enables `preserve_order`) so hook-added keys the typed `Lockfile` cannot represent survive to disk; the round-trip only runs when a hook is present, so unmodified installs write byte-identical lockfiles. A throwing hook aborts the install.
- **`preResolution`** — wired. Receives the resolution context (wanted/current lockfile, `existsCurrentLockfile`, `existsNonEmptyWantedLockfile`, lockfile dir, store dir, registries) over stdin.
- **`filterLog`** — implemented in the bridge but not yet routed through the reporter (pacquet's reporter is a stateless synchronous emitter); deferred, see follow-ups.

## How hooks run

Hooks are served by a long-lived Node.js worker, spawned lazily once per pnpmfile. Requests and responses are newline-delimited JSON over the worker's stdin/stdout, multiplexed by a monotonic request id so the concurrent `readPackage` calls the resolver makes (it resolves dependencies in parallel) share one process. This removes the per-package `node` startup cost on the resolution hot path and avoids interpolating payloads into a `node -e` argument (no `E2BIG` risk for large lockfiles). Each `context.log(...)` a hook emits is forwarded back to the call's log callback. `preResolution` keeps a one-shot `node` invocation since it runs once per install and needs an `info`/`warn` logger.

## Tests

- Unit (hooks crate): readPackage validation (returns nothing / non-object / array dependency fields), manifest-field normalization, syntax-error and missing-module failures, worker request-id multiplexing under concurrency, and `context.log` forwarding.
- Integration (package-manager): a `readPackage` hook pins a transitive dependency version; a hook that returns nothing aborts the install; a pnpmfile syntax error aborts the install; an `afterAllResolved` hook's mutation is written to `pnpm-lock.yaml`; a throwing `afterAllResolved` aborts the install.

## Scope

The remaining pnpmfile-hook surface pnpm has but pacquet does not yet implement — wiring `filterLog` and the `pnpm:hook` log channel into the reporter, the `--pnpmfile` / `--global-pnpmfile` / `--ignore-pnpmfile` flags, pnpmfile checksum invalidation, `updateConfig`, and finders/resolvers/fetchers — is tracked in #12118.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-06-02 01:08:54 +02:00
Zoltan Kochan
c5d9d3a8f3 refactor(pnpr): rename pnpm-registry to pnpr (#12069)
* refactor(pnpr): rename pnpm-registry to pnpr

Rename the registry server across the board to match the npm wrapper
package name, which was already `@pnpm/pnpr`.

- crate `pnpm-registry` -> `pnpr`, `pnpm-registry-fixtures` -> `pnpr-fixtures`
- binaries `pnpm-registry` -> `pnpr`, `pnpm-registry-prepare` -> `pnpr-prepare`
- module paths and log targets `pnpm_registry::*` -> `pnpr::*`
- binary-locating env vars `PNPM_REGISTRY_BIN` -> `PNPR_BIN`,
  `PNPM_REGISTRY_PREPARE_BIN` -> `PNPR_PREPARE_BIN`
- top-level directory `registry/` -> `pnpr/` (crates, npm wrapper, fixtures)

The registry-mock storage concept is intentionally left as-is:
`PNPM_REGISTRY_MOCK_PORT`/`PNPM_REGISTRY_MOCK_STORAGE`/`PNPM_REGISTRY_STORAGE`,
the `~/.cache/pnpm-registry/storage` path + benchmark cache keys, and the
external `pnpm-registry-mock` npm package referenced in test fixtures.

* style(pnpr): rustfmt import grouping after rename

* ci(pnpr): point typos at pnpr instead of removed registry dir

* chore(pnpr): update pre-push path filter from registry to pnpr
2026-05-29 20:02:10 +02:00
Zoltan Kochan
4024f13741 chore: only run expensive Rust pre-push checks when pacquet or registry change (#12050) 2026-05-29 17:58:13 +02:00
Marvin Hagemeister
49e6074644 test: replace @pnpm/registry-mock with an in-repo in-process registry (#11927)
Replace the external `@pnpm/registry-mock` (Verdaccio) test dependency with an in-repo, in-process registry that serves package fixtures to **both** the pacquet Rust tests and the pnpm CLI (Jest) tests. No separately managed registry process is needed.

### How it works

- **Fixtures** live at `registry/.fixtures/packages/<name>/<version>/…`, moved verbatim from [`pnpm/registry-mock`](https://github.com/pnpm/registry-mock) (keyed by each `package.json`'s `name`+`version`).
- **`pnpm-registry-fixtures`** builds verdaccio-shaped storage from those fixtures; the in-tree **`pnpm-registry`** crate serves it.
  - Files whose names differ only by case (`@pnpm.e2e/with-same-file-in-different-cases`) and `bundleDependencies` trees are composed **in memory** by the builder, since neither can be committed to the working tree.
- **pacquet**: `pacquet-testing-utils`' `TestRegistry` starts the server lazily (once per process) in proxy mode, serving `@pnpm.e2e` fixtures locally and falling through to the npm uplink for real packages (`is-positive`, `is-negative`, …) — matching how registry-mock behaved.
- **pnpm CLI**: the `with-registry` Jest `globalSetup` builds storage from the fixtures via the new `pnpm-registry-prepare` binary (built from source in the Test CI job) and serves it with `pnpm-registry`. `REGISTRY_MOCK_PORT` / `REGISTRY_MOCK_CREDENTIALS` / `getIntegrity` now come from `@pnpm/testing.registry-mock`.

### Result

`@pnpm/registry-mock` is removed from every manifest, the catalog, and `packageExtensions`; `cargo test` / `cargo nextest run` / `just test` and the pnpm CLI Jest suites all run registry-backed tests without launching Verdaccio.
2026-05-29 14:35:45 +02:00
Zoltan Kochan
98d4c60f61 fix(ci): inject release version into PACQUET_VERSION constant (#12058)
The pacquet release workflow patched the clap `version` attribute in
cli_args.rs, expecting a string literal. Since #12047 that attribute
references the `pacquet_config::PACQUET_VERSION` constant, so the perl
substitution matched nothing and the verifying grep failed, aborting the
whole release.

Patch the `PACQUET_VERSION` constant in defaults.rs instead. That single
constant feeds both `pacquet --version` and the default User-Agent, so
both report the published version.

Also tag the default User-Agent as `pnpm/pacquet-<version>` so registries
can tell pacquet's traffic apart from the TypeScript pnpm CLI, while
keeping the `pnpm` token for UA-keyed allow / rate-limit rules.
2026-05-29 12:06:13 +02:00
Zoltan Kochan
89c2b52728 ci(pacquet): pin cargo-dylint to 6.0.0 to unblock CI (#11969)
trailofbits/dylint 6.0.1 (published 2026-05-26 17:51 UTC) ships
prebuilt cargo-binstall artifacts that bake in a path from the
dylint repo's own CI workspace:

    error: failed to get `dylint_driver` as a dependency of package
      `dylint_driver-nightly-2026-04-16-x86_64-unknown-linux-gnu`
    Caused by:
      failed to read `/home/runner/work/dylint/dylint/driver/Cargo.toml`
    Caused by:
      No such file or directory (os error 2)

Downstream runners don't have that workspace, so the driver bootstrap
fails before any lint runs and the Dylint job goes red on every PR.
6.0.0 (the version main was passing with 90 minutes earlier) is
unaffected. Pin both binaries until upstream cuts 6.0.2.

---
Written by an agent (Claude Code, claude-opus-4-7).
2026-05-26 20:59:03 +02:00
Zoltan Kochan
f107b1e13a ci(test): install @pnpm/pnpr from npm instead of building locally (#11964)
The e2e/integration test harness spawns `pnpm-registry` as a faster
verdaccio replacement. CI used to install Rust and build the crate
from source on every test job — adding several minutes per platform.

`@pnpm/pnpr` now publishes the prebuilt binary to npm, and `pnpm install`
already pulls in the matching `@pnpm/pnpr.<platform>-<arch>` package
via optionalDependencies. The Jest globalSetup resolves that binary
through `@pnpm/pnpr/bin/pnpr`'s own module path (the wrapper carries
the platform packages as siblings in its `node_modules`, not on the
parent chain of this file).

- Add `@pnpm/pnpr` to `pnpm-workspace.yaml` catalog and depend on it
  from `@pnpm/jest-config`.
- Replace `resolvePnpmRegistryBin`'s `$CARGO_TARGET_DIR` lookup with
  `require.resolve` through the npm-installed wrapper. The
  `PNPM_REGISTRY_BIN` env var is still honored as an escape hatch for
  contributors who want to point at a locally-built Rust binary.
- Remove the "Install Rust toolchain" + "Build pnpm-registry" steps
  from `.github/workflows/test.yml`.

---
Written by an agent (Claude Code, claude-opus-4-7).
2026-05-26 19:33:11 +02:00
Zoltan Kochan
c42f69ffaf ci(pnpr): add manual release workflow that publishes @pnpm/pnpr to npm (#11963)
* ci(pnpm-registry): add manual release workflow that publishes to npm

Mirror pacquet's release pipeline for the pnpm-registry crate:

- New `Release pnpm-registry` GitHub workflow (manual `workflow_dispatch`
  with a version input) builds the `pnpm-registry` binary for six
  `<os>-<cpu>` matrix legs via `cross`, attests provenance, and uploads
  artifacts.
- The publish job patches `registry/npm/pnpm-registry/package.json`'s
  `version` field with the input, downloads the artifacts, runs
  `generate-packages.mjs` to produce per-platform packages, then
  publishes everything via `pnpm publish --provenance` using OIDC.
- The wrapper package is `pnpm-registry`; the per-platform binary
  packages are scoped `@pnpm/registry.<os>-<cpu>` and resolved by a
  Node shim under `bin/pnpm-registry`.

The CLI's `--version` output comes from `CARGO_PKG_VERSION` via clap's
derive `version` attribute, so the build leg patches the crate's
`Cargo.toml` `version` field rather than a hardcoded clap string.

---
Written by an agent (Claude Code, claude-opus-4-7).

* ci(pnpr): rename package to @pnpm/pnpr with @pnpm/pnpr.<os>-<cpu> binaries

Shorter, scoped name that reads naturally on npm: `@pnpm/pnpr` for the
wrapper, `@pnpm/pnpr.<os>-<cpu>` for the six per-platform binary
packages (mirroring `@pacquet/<os>-<cpu>` under a different scope).

- Move `registry/npm/pnpm-registry/` to `registry/npm/pnpr/` and rename
  the JS shim and `bin` entry to `pnpr`.
- Update `generate-packages.mjs` so `BIN_NAME = pnpr` (used for both
  the binary file inside each platform package and the autogenerated
  package dir) and the scope/prefix produce `@pnpm/pnpr.<os>-<cpu>`.
- Rename the workflow file to `pnpr-release-to-npm.yml` and rename
  the built `pnpm-registry` binary to `pnpr-<code-target>` at archive
  time, so `generate-packages.mjs` finds it at `REPO_ROOT/pnpr-<plat>-<arch>`.
- The Rust crate name stays `pnpm-registry` (still what `cross build -p`
  targets); only the npm-publishing layer is renamed.

---
Written by an agent (Claude Code, claude-opus-4-7).

* fix(pnpr): propagate signal exits through the Node shim

`spawnSync` returns `result.status === null` when the child terminates
via a signal. Assigning that to `process.exitCode` makes the parent
exit 0 and masks the signal — bad for a long-running server where
the operator is most likely to kill it with SIGINT/SIGTERM. Re-raise
the signal so the parent terminates the same way, falling back to a
non-zero exit code if for some reason we can't.

---
Written by an agent (Claude Code, claude-opus-4-7).
2026-05-26 17:54:58 +02:00
Zoltan Kochan
d8a79a9c30 feat(registry): add auth/dist-tag/publish endpoints + wire TS tests onto pnpm-registry (#11914)
Lands the pieces of the npm registry protocol that pnpm-registry was missing, and switches the TypeScript test harness off verdaccio onto pnpm-registry. `@pnpm/registry-mock` (the npm package) is untouched.

### Server-side additions (`registry/crates/pnpm-registry`)

- `PUT /-/user/org.couchdb.user:<name>` — adduser / login, returns a Bearer token. In-memory user + token stores.
- `PUT /:pkg` — publish (scoped + unscoped). Base64-decodes `_attachments`, merges into the existing packument, writes manifest + tarball atomically. 100 MiB body limit.
- `GET /-/package/:pkg/dist-tags` + `PUT/DELETE /-/package/:pkg/dist-tags/:tag` — rewrites the on-disk packument so tag changes survive a restart.
- `Authorization: Bearer` and `Authorization: Basic` both identify the caller.
- Per-package access policy (wax glob patterns). Defaults mirror `@pnpm/registry-mock`'s `config.yaml`: `@private/*` and `@pnpm.e2e/needs-auth` require auth; everything else is anonymous read, authenticated write. Enforced on every packument / version-manifest / tarball GET and every write endpoint.

### TypeScript-test migration

- `__utils__/jest-config/with-registry/globalSetup.js` keeps `prepare()` from `@pnpm/registry-mock` (still needed for the tempy storage path written into the runtime-config yaml — `getIntegrity` reads it from there) but spawns `pnpm-registry` instead of verdaccio. `addUser`, `addDistTag`, `getIntegrity`, `REGISTRY_MOCK_*` from registry-mock work as-is — they're plain npm-wire-protocol HTTP calls.
- Binary lookup follows pacquet's pattern: `PNPM_REGISTRY_BIN` env override, then `target/release/pnpm-registry`, then `target/debug/pnpm-registry`.
- CI test job (`.github/workflows/test.yml`) installs the Rust toolchain via the existing `./.github/actions/rustup` composite action and builds `pnpm-registry --release` before tests run. Per-platform — Linux and Windows in the matrix each build their own.
2026-05-25 09:40:09 +02:00
Zoltan Kochan
b9de85dcb6 ci(pacquet): drop pnpm comparison and self-compare on main from integrated-benchmark (#11913)
The pnpm-CLI baseline was useful while pacquet was slower than pnpm; now
that pacquet is the perf target itself, comparing against pnpm on every
run is noise. Drop --with-pnpm from every scenario step.

When the workflow runs on main, HEAD and main point at the same commit,
so a HEAD-vs-main comparison is wasted work. Resolve the target list at
job level: pacquet@HEAD on main, pacquet@HEAD pacquet@main everywhere
else (PRs, workflow_dispatch from non-main). The Bencher upload already
filters to pacquet@HEAD, so the single-target result still lands on the
main baseline as before.
2026-05-24 22:12:29 +02:00
Zoltan Kochan
add6c794f1 feat(registry): implement pnpm-registry server and adopt it in pacquet's test mock (#11898)
Creates a working pnpm-compatible npm registry server (verdaccio analogue, in Rust) — and replaces `@pnpm/registry-mock`'s Node + Verdaccio launcher in pacquet's test setup with the new binary, against `@pnpm/registry-mock`'s shipped storage.

### What `pnpm-registry` does
- **HTTP server** (axum + tower-http) with the three endpoints pnpm/npm clients need:
  - `GET /<pkg>` — packument (`/{name}` and `/{scope}/{name}`)
  - `GET /<pkg>/<version-or-tag>` — single-version manifest, resolves `dist-tags` and rewrites `dist.tarball` to point at this server
  - `GET /<pkg>/-/<tarball>` — tarball, streamed
- **Two modes:**
  - **Proxy** — fetches missing packuments/tarballs from a configurable upstream (defaults to `https://registry.npmjs.org`), caches to disk
  - **Static** (`--static`) — serves the storage directory verbatim, 404s on cache miss
- **Verdaccio-shaped on-disk storage** (`<root>/<pkg>/package.json` + flat tarballs) — drop-in compatible with the storage `@pnpm/registry-mock` publishes
- **Tarball streaming** — cache hits stream off disk; cache misses tee upstream chunks into a temp file via an mpsc channel and forward them to the client at the same time, atomically renaming on success and abandoning on upstream error or client disconnect
- **Tuned HTTP client** — wraps `pacquet_network::ThrottledClient::new_for_installs()`, inheriting pnpm's tuned defaults (`User-Agent: pnpm`, HTTP/1.1, hickory DNS, connection-pool tuning, concurrency semaphore)
- **Gateway-style status mapping** — `is_timeout()` → 504, `is_connect()` → 503, everything else (incl. upstream 5xx) → 502. No proxy-side retry (the pnpm client already has `fetch-retries`; stacking retries would only multiply latency on real failures).

### What changed in pacquet
- `pacquet/tasks/registry-mock` now spawns `pnpm-registry` against `node_modules/@pnpm/registry-mock/registry/storage-cache` (proxy mode with `npmjs.org` upstream and a 1-year packument TTL — matching `@pnpm/registry-mock`'s `'**': proxy: npmjs` verdaccio config). No more Node, no more Verdaccio, no more `launch.mjs`, no more process-tree walk to kill child verdaccios.
- `@pnpm/registry-mock` stays as a devDep — only for the storage data it ships, not the launcher.

### Tests
- **36 pnpm-registry tests** (12 unit + 7 against `@pnpm/registry-mock` storage in static mode + 17 mockito-based proxy/cache/streaming): packument rewrite, version-manifest resolution, tarball streaming (large body, cache finalize, mid-stream upstream error, client disconnect mid-stream, concurrent fetches → one cache file), gateway status mapping (504/503/502), stale-cache fallback on upstream failure, TTL refresh, invalid-package-name 400, scoped vs unscoped routing.
- **Full pacquet test suite** (2043 tests) runs green against `pnpm-registry`-backed mock.

### CI
- `pacquet-ci.yml` and `pacquet-codecov.yml` path filters now include `registry/**` (so registry-only PRs trigger the workspace CI); typos checker covers `registry` too. The workflow name stays "Pacquet CI" but a header comment explains the intentional cross-stack scope.
- `just registry-mock launch` pre-builds with `cargo nextest run --no-run` (workspace-wide) so its fingerprint matches what `just test` will later need — without this, Windows MSVC fails with `os error 5` trying to re-link the running `pnpm-registry.exe`.

### Crates.io name reservations (from the original scaffold commit)
- [`pnpm-registry`](https://crates.io/crates/pnpm-registry) — published from this repo
- [`pnpm-registry-cli`](https://crates.io/crates/pnpm-registry-cli) / [`pnpm-registry-server`](https://crates.io/crates/pnpm-registry-server) — placeholder stubs, name reservation only
2026-05-24 21:18:09 +02:00
Zoltan Kochan
389dae8382 ci: fix zizmor ref-version-mismatch on action-gh-release (#11888)
The dependabot bump to v3.0.0 updated the pinned commit hash but left
the trailing version comment as v2.5.0.
2026-05-23 22:48:09 +02:00
dependabot[bot]
a5b1ac783f chore(deps): bump the github-actions group with 4 updates (#11854)
Bumps the github-actions group with 4 updates: [github/codeql-action](https://github.com/github/codeql-action), [taiki-e/install-action](https://github.com/taiki-e/install-action), [garnet-org/action](https://github.com/garnet-org/action) and [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action).


Updates `github/codeql-action` from 4.35.4 to 4.35.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](68bde559de...9e0d7b8d25)

Updates `taiki-e/install-action` from 2.78.0 to 2.78.1
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](e1c4cd4211...184183c240)

Updates `garnet-org/action` from 2.0.1 to 2.0.2
- [Release notes](https://github.com/garnet-org/action/releases)
- [Commits](9e819143e6...2b7fc9d79b)

Updates `zizmorcore/zizmor-action` from 0.5.3 to 0.5.5
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](b1d7e1fb5d...a16621b09c)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: taiki-e/install-action
  dependency-version: 2.78.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: garnet-org/action
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-23 19:43:23 +02:00