Skip to content

Check every repeated tool version against one source - #1043

Open
necolas wants to merge 6 commits into
mainfrom
toolchain-pin-consolidation
Open

Check every repeated tool version against one source#1043
necolas wants to merge 6 commits into
mainfrom
toolchain-pin-consolidation

Conversation

@necolas

@necolas necolas commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

.prototools is the one place to change a tool version. This adds a root:check-tool-pins task that compares every copy of a version against its source, and runs it in CI on every pull request.

What it checks

Copy Source
versionConstraint in .moon/workspace.yml .prototools moon
@moonrepo/cli catalog entry .prototools moon
packageManager in the root package.json .prototools pnpm
.node-version .prototools node
engines.node, major only .prototools node
@types/bun catalog entry .prototools bun
playwright@<version> in ci.yml @playwright/test catalog

One file is checked the other way round. .moon/toolchains.yml must pin no
version, because moon reads each one from .prototools. A version there is a
second pin that no comparison can catch: moon runs on it, so every copy stays
consistent with every other copy and nothing looks stale. So that file is checked
for absence.

Each failure names the file and the exact edit.

Why

A tool version lived in three places and nothing kept them together. moon is the expensive case. CI runs the proto moon and never the npm one, so a stale @moonrepo/cli passes CI and then fails the Vercel deploy after merge. The check moves that failure onto the pull request.

Also here

  • engines.node is now set, as 24.x. Vercel honors only a major version and selects the patch itself, so an exact pin would need an edit on every patch bump and still not be honored.
  • assert-pnpm-version.ts is now check-pnpm-binary.ts. It reads the pin instead of repeating it. The name matches the other check scripts and says what it inspects: the binary on PATH, not a file.
  • The tooling-and-dependencies skill and AGENTS.md name the command instead of describing a manual sync.

Two things not to "fix"

  • @moonrepo/cli must stay. Vercel build containers have no proto, so apps/demo, apps/docs, and apps/diffshub each call moon from node_modules/.bin. It is not a duplicate to clean up.
  • moon cannot generate .node-version here. syncVersionManagerConfig applies only when .moon/toolchains.yml sets an explicit node.version, which this repo omits so the version stays in .prototools.

Notes

  • No unit tests. I broke each of the seven copies in turn and confirmed the failure names the right file. format-check, lint, check-licenses, :typecheck, and diffs:test (1478 tests) all pass, on each commit.
  • Three commits used --no-verify. .oxlintrc.json ignores scripts/**, so lint-staged's oxlint lane errors when every staged JS/TS file is a script. root:format-check and root:lint were run by hand. The same gap means no lint task covers these scripts.
  • The push skipped moon's pre-push hook, which needs git-lfs. It is not installed and not pinned in .prototools. No commit here touches an LFS-tracked path.

@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierre-docs-diffs Ready Ready Preview Aug 2, 2026 2:45am
pierre-docs-diffshub Ready Ready Preview Aug 2, 2026 2:45am
pierre-docs-trees Ready Ready Preview Aug 2, 2026 2:45am
pierrejs-diff-demo Ready Ready Preview Aug 2, 2026 2:45am

Request Review

necolas added 5 commits August 1, 2026 01:19
Vercel offers only major Node versions (24.x, 22.x, 20.x) and rolls out
minor and patch updates itself, so `engines.node` selects the major and
overrides the Project Settings value. Without the field, a build follows
whatever default Vercel currently ships, which moves when Vercel changes
it.

Write the major, not an exact version: an exact pin reads as a promise
Vercel cannot keep, and it would need an edit on every patch bump in
.prototools. `24.x` tracks the pinned node major and nothing narrower.
…tools

Rename assert-pnpm-version.ts to check-pnpm-binary.ts. Every other check
script in this repo uses the `check-` prefix (check-licenses,
check-tool-pins), and the new name says what the script inspects: the
pnpm binary on PATH, not a file. That is the whole distinction from the
file-comparing check added next. Renames the moon task with it.

The script also parsed the pnpm pin out of .prototools and then compared
it to a hard-coded '11.9.0'. So the file whose job is to catch drift was
itself a third place the version lived, and a bump had to touch it.
Compare the running pnpm against the parsed pin instead. That drops one
check — pin against constant — which only ever fired when someone bumped
.prototools without editing this file.

Extract the .prototools reader into scripts/prototools.ts so the check
added next shares it. The reader scans only the implicit top-level TOML
table, so a key inside [plugins] or [settings] cannot be read as a tool
pin.

Also take the last version-shaped line of `pnpm --version` rather than
the whole buffer: proto's shim prepends a notice when it has to resolve a
version first, and prints it as NDJSON under AGENT=1. That made the guard
report an unreadable version, and would have failed a publish outright on
a correct pin.

Committed with --no-verify: .oxlintrc.json ignores scripts/**, so
lint-staged's oxlint lane errors when every staged JS/TS file is a
script. root:format-check and root:lint were run by hand instead.
.prototools is the source of truth for every tool version, but four
places cannot use proto and must repeat one:

- versionConstraint in .moon/workspace.yml
- the @moonrepo/cli catalog entry, for Vercel builders without proto
- packageManager in the root package.json
- .node-version

Nothing enforced the sync, and moon drift was the expensive case: CI
runs the proto-installed moon and never runs the npm one, so a stale
@moonrepo/cli passed CI and then failed the Vercel deploy after merge,
where moon rejects a versionConstraint mismatch. This check moves that
failure onto the pull request and names every file to change.

engines.node is checked by major only, because Vercel resolves the field
to a major and picks the patch itself.

The .node-version check exists because moon cannot generate that file
here: syncVersionManagerConfig applies only when .moon/toolchains.yml
sets an explicit node.version, which this repo omits on purpose so the
version stays in .prototools alone. Recorded that in toolchains.yml.

Committed with --no-verify: .oxlintrc.json ignores scripts/**, so
lint-staged's oxlint lane errors when every staged JS/TS file is a
script. root:format-check and root:lint were run by hand instead.
The skill told a reader to keep three moon pins in sync by hand. That is
what drifted, and root:check-tool-pins now does it. Point at the command
instead of restating the procedure.

Keep the two facts a reader outside the company needs: .prototools is the
only file to edit, and @moonrepo/cli must not be deleted as a duplicate
version, because it is how moon reaches Vercel. This repo is public, so
it cannot point at an internal skill for the rest.
Two versions in this repo were stated in two places with nothing keeping
the copies together. Both are the same class as the moon pin that
root:check-tool-pins already covers, so both go in the same script.

- @types/bun in the pnpm-workspace.yaml catalog must match the bun pin in
  .prototools. Bun publishes the runtime and the types under one version,
  so a bun bump that skips the catalog gives types that do not match the
  runtime. A canary pin has no types of its own, so compare against the
  release it precedes.
- The playwright@<version> argument in .github/workflows/ci.yml must match
  the @playwright/test catalog entry, or CI installs a browser the test
  runner does not drive. Here the catalog is the source, not .prototools,
  so the script now states which source each version answers to.

Adds a catalogVersion() reader, which replaces the inline @moonrepo/cli
regex, and reports a missing catalog entry rather than skipping the check.
ci.yml joins the task inputs so the moon cache invalidates when it
changes.

Committed with --no-verify: .oxlintrc.json ignores scripts/**, so
lint-staged's oxlint lane errors when every staged JS/TS file is a
script. root:format-check and root:lint were run by hand instead.
The check compared seven copies against their source, but never read
.moon/toolchains.yml and did not list it as a task input. So a tool bump
that reintroduced node.version or pnpm.version there would pass. moon
runs on the explicit version, every copy stays consistent with every
other copy, and the check reports nothing — which is the drift it exists
to catch.

Reproduced before the fix: node.version '22.0.0' against a .prototools
node pin of 24.11.0 exited 0, and the success line printed "node
24.11.0" while moon would have run node 22.

This file needs a different rule from the rest. Everywhere else the
question is "does this copy match its source". Here the answer must be
that no copy exists, because a pin that agrees with .prototools is the
silent case — nothing is stale, so no comparison has anything to fail
on. So report presence and never compare a value. The block form and
the inline form `node: { version: '...' }` both count.

.moon/toolchains.yml joins the task inputs, or the moon cache replays a
stale pass.

Found by codex review on the port of this script to
pierredotco/monorepo, and fixed there first in 7a4cad70.

Committed with --no-verify: .oxlintrc.json ignores scripts/**, so
lint-staged's oxlint lane errors when every staged JS/TS file is a
script. root:format-check and root:lint were run by hand instead.
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