Skip to content

chore(deps): pnpm 11.21.0, and why the pin stops short of 12 - #310

Merged
oddharsh merged 1 commit into
mainfrom
chore/pnpm-11-21
Aug 10, 2026
Merged

oddharsh merged 1 commit into
mainfrom
chore/pnpm-11-21

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

Two things: the free bump, and the note explaining why the expensive one is parked.

The bump

packageManager only. No lockfile byte changes and no build byte changes.

check result
pnpm install --frozen-lockfile Already up to date, no-op
contract suite 206 pass, 0 fail
pnpm run typecheck exit 0
build vs baseline byte-identical, 1975 files, 0 differing

Gotcha 30: why not 12

Measured against 12.0.0-rc.3 on the branch behind #309, which stays a draft.

Everything here works under pnpm 12, including a byte-identical build across all 1975 files. What doesn't work is pnpm 10.11.1 — the version gotcha 29 records the Workers Builds image running — reading the lockfile pnpm 12 writes:

ERR_PNPM_BROKEN_LOCKFILE  The lockfile at ".../pnpm-lock.yaml" is broken:
expected a single document in the stream, but found more

It's a --- YAML document separator, and lockfileVersion stays '9.0', so one upstream line could retire the whole objection.

Production would still build, for exactly one reason: pnpm reads packageManager before it opens the lockfile, downloads the pinned binary, and that binary parses its own format. So adopting 12 makes every production install depend on a single registry fetch with no fallback. Run the build image with manage-package-manager-versions off, or lose that fetch, and it's holding a lockfile it cannot parse. Two majors behind is the milder failure.

The rule worth keeping past this instance: check that the build image can read what your package manager writes, not just that your own install works. A lockfile is an interchange format between two machines on different versions, and "it installs here" tests one of them.

The contrast between the two changes is the point: a patch bump inside a major is free here, and the major is not.

🤖 Generated with Claude Code

The bump is free: `packageManager` only, no lockfile byte changes, no build
byte changes, `--frozen-lockfile` is a no-op, 206/206, typecheck 0.

Gotcha 30 records why 12 is not the answer yet, measured against 12.0.0-rc.3
on the branch behind #309. Everything here works under it, including a
byte-identical build across all 1975 files. What does not work is pnpm
10.11.1 — the version gotcha 29 records the Workers Builds image running —
reading the lockfile pnpm 12 writes:

  ERR_PNPM_BROKEN_LOCKFILE  the lockfile is broken: expected a single
  document in the stream, but found more

It is a `---` YAML document separator, and `lockfileVersion` stays '9.0', so
one upstream line could retire the whole objection. Production would still
build only because pnpm reads `packageManager` before it opens the lockfile
and downloads the pinned binary, which parses its own format. That makes every
production install depend on a single registry fetch with no fallback, and
being two majors behind is the milder failure.

The rule worth keeping past this instance: check that the BUILD IMAGE can read
what your package manager WRITES, not just that your own install works. A
lockfile is an interchange format between two machines on different versions,
and "it installs here" tests one of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
aadhar-sh 1b66b49 Aug 10 2026, 07:11 PM

@oddharsh
oddharsh merged commit 02ae953 into main Aug 10, 2026
6 of 7 checks passed
@oddharsh
oddharsh deleted the chore/pnpm-11-21 branch August 10, 2026 19:16
oddharsh added a commit that referenced this pull request Aug 12, 2026
…stops rotting (#348)

The dictionary sets are the one build input no build step may produce. A returning
visitor holds whatever `/a/` asset and page production served them last, offers its
hash, and earns a delta only if those exact bytes are committed here. build.mjs
cannot derive that from source, so the sets are committed, and committing them is a
step someone has to remember.

Nobody remembered since #178 on 2026-07-30, 161 commits back.

## What was broken

Every dictionary-carrying shell asset production serves was missing from
`www/a-dict`: nav.1c6af07b, luna.1523f4f6, hoist.d9081560, tooltip.5dad2389,
lens.88fba28b. Main built its six shell deltas against #178's candidates, which no
living browser holds, so a returning visitor took plain brotli on the render-blocking
path. After the roll, against the bytes browsers actually hold:

| | stale | rolled |
|---|--:|--:|
| `luna` delta | 7,781 B (no match, plain br) | **155 B** |
| `nav` delta | 5,973 B (no match, plain br) | **1,113 B** |

Per-page deltas went 121,288 B over 92 to 74,337 B over 94, because the snapshots
now match live pages rather than pages from twelve days ago.

## Why dcz:check said PASS the whole time

Its shell probe picks an a-dict candidate that is NOT the live hash, offers it, and
asserts dcz comes back. build.mjs builds a delta for every a-dict entry, so that is
true by construction; the probe reads the live hash only to exclude it. It is the
same self-agreeing shape as the Markdown-twin test in gotcha 24 and the page tier's
old snapshot probe in gotcha 20.

`live shell is covered by a-dict` asks the question a returning visitor asks: are the
bytes THEY hold covered? Bases with no a-dict history are skipped, since a new asset
cannot have a dictionary until it has been served once. Byte-compared, not
filename-compared, because 8 hex of a sha256 is not the digest the browser keys on.
Advisory on purpose: like infra:check's edge tier it reads production, so making it
required would deadlock the release that would clear it.

## Why it can now run unattended

`shell:roll` adopted from `.build/www/a`, which forced it to run from the deployed
commit. `--live` (`pnpm run dict:roll`) reads production instead, the same way the
page half has since gotcha 20. Adopting from a build can only capture THIS commit's
shell, so a release that goes by without a roll is unrecoverable, and on an unramped
tree it adopts bytes nobody holds while evicting one still in use (KEEP is 3).
Whatever is live is protected from the prune. It refuses outright if production reads
empty, since an empty read hands the prune an empty `current` set, which is exactly
when it is free to evict what browsers are holding.

`.github/workflows/dictionary-roll.yml` runs it nightly and opens a PR when anything
moved. It holds no Cloudflare credential: `--live` is plain HTTPS and the PR goes out
on GITHUB_TOKEN. It cannot merge, because `main` takes zero bypass actors and that
property is the whole release model.

## CLAUDE.md

Two paragraphs described a `.github/workflows/ramp.yml` running the ramp from a
scoped environment secret. Verified 2026-08-12: no such workflow in any commit, the
environments are `copilot` and `production` rather than the canary/full pair, and the
only repo secret is the six-read-scope CLOUDFLARE_API_TOKEN. What landed on
2026-08-06 was the release-guard rewrite alone. Both blocks are relabelled as a PLAN
rather than deleted, since the reasoning about where a secret lives is still right.

Also corrected: "not urgent, a dictionary 11 days stale still gave 87-93%" is true of
a STALE dictionary and false of an ABSENT one. The page tier degrades to the family
corpus at ~26%; the shell tier has no fallback.

## Drive-by

Four workflows bootstrapped pnpm@11.20.0 while package.json has pinned 11.21.0 since
#310 two days ago, each under a comment calling package.json the source of truth.
Behaviour-neutral (self-switching is on by default, so 11.21.0 already ran every
time), but the gap stops being harmless the moment it crosses a major: pnpm 12 ships
a native CLI that an older pnpm cannot exec, which is gotcha 30. A contract test now
pins every workflow's bootstrap to package.json, and counts what it checked so a
regex that stops matching cannot pass silently.

Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant