Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .agents/skills/tooling-and-dependencies/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ description:
managed by [proto](https://moonrepo.dev/docs/proto); its shims put the pinned
versions on PATH inside the repo. `proto use` installs everything after a pin
changes.
- Bump a tool by editing `.prototools` only — never install tools globally or
pin versions elsewhere. moon's version is additionally enforced by
`versionConstraint` in `.moon/workspace.yml` and mirrored as the
`@moonrepo/cli` catalog entry (for Vercel builders without proto); keep all
three in sync.
- `.prototools` is the only file to edit to change a tool version. Never install
tools globally. A few places cannot use proto and must repeat a version. After
a pin change, run `moon run root:check-tool-pins`. It names every file that
still disagrees, and CI runs it on every pull request.
- Never delete the `@moonrepo/cli` dependency to remove a duplicate version. It
is how moon reaches Vercel. Vercel build containers have no proto, so each
app's `vercel.json` prefixes PATH with `node_modules/.bin` and calls `moon`.
- CI and local shells resolve the same toolchain: CI installs it with
`moonrepo/setup-toolchain`, which runs `proto install` against the same
`.prototools`.
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ jobs:
run: >-
moon run root:check-licenses

- name: Check toolchain pins
run: >-
moon run root:check-tool-pins

- name: Run affected tasks
run: >-
moon ci --include-relations --summary detailed :build demo:build
Expand Down
6 changes: 3 additions & 3 deletions .moon/tasks/tag-publishable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ inheritedBy:
tasks:
# Publishes must use the pnpm version pinned in .prototools so lockfile and
# package-manager behavior match CI. The script reads the pin itself.
assert-pnpm-version:
command: 'bun $workspaceRoot/scripts/assert-pnpm-version.ts'
check-pnpm-binary:
command: 'bun $workspaceRoot/scripts/check-pnpm-binary.ts'
options:
cache: false
internal: true
Expand All @@ -28,7 +28,7 @@ tasks:
prepublish:
command: 'noop'
deps:
- 'assert-pnpm-version'
- 'check-pnpm-binary'
- 'build'
options:
cache: false
Expand Down
3 changes: 3 additions & 0 deletions .moon/toolchains.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ $schema: 'https://moonrepo.dev/schemas/toolchains.json'
javascript:
packageManager: 'pnpm'

# Do not set `version` here. The node version comes from .prototools.
# `syncVersionManagerConfig` needs an explicit `version`, so moon cannot write
# .node-version. The root:check-tool-pins task compares the two files instead.
node: {}

pnpm:
Expand Down
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ themselves, unset the var: `CI= pnpm publish --dry-run`.

- Tool versions (bun, pnpm, node, moon, gh) are pinned in `.prototools` and
managed by [proto](https://moonrepo.dev/docs/proto); run `proto use` if a tool
is missing or a pin changed. Never install toolchain versions globally; bump
pins only in `.prototools`.
is missing or a pin changed. Never install toolchain versions globally. Bump
pins only in `.prototools`, then run `moon run root:check-tool-pins` to find
every file that repeats the version.
- [moon](https://moonrepo.dev/docs) is the task runner; `package.json` scripts
are npm lifecycle hooks only.

Expand Down
21 changes: 21 additions & 0 deletions moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,27 @@ tasks:
options:
runInCI: 'always'

# Compares each version that this repo states twice with its one source. The
# source is .prototools for a proto tool, and the pnpm-workspace.yaml catalog
# for an npm package. It also rejects a version pin in .moon/toolchains.yml,
# which must hold none. It catches moon drift. CI runs the proto moon and never
# the npm moon, so a stale @moonrepo/cli passes CI. No graph edges, so it stays
# runInCI: 'always' (see the header note) and CI runs it as its own step.
check-tool-pins:
command: 'bun --silent scripts/check-tool-pins.ts'
inputs:
- 'scripts/check-tool-pins.ts'
- 'scripts/prototools.ts'
- '.prototools'
- '.node-version'
- '.moon/workspace.yml'
- '.moon/toolchains.yml'
- '.github/workflows/ci.yml'
- 'pnpm-workspace.yaml'
- 'package.json'
options:
runInCI: 'always'

# Regenerates the committed icon sprite module from @pierre/icons sources,
# then formats just the generated file. Not targeted by any CI lane.
icons:
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@
"stylelint --fix"
]
},
"engines": {
"node": "24.x"
},
"packageManager": "pnpm@11.9.0"
}
2 changes: 1 addition & 1 deletion packages/trees/moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ tasks:
internal: true
runInCI: 'skip'

# Appends to the publishable tag's guard chain (assert-pnpm-version, build).
# Appends to the publishable tag's guard chain (check-pnpm-binary, build).
prepublish:
deps:
- 'assert-safe-publish'
Expand Down
15 changes: 13 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@ in the monorepo. Tasks (build/dev/test/lint) are run by moon —
enabled, worktree-aware (exposed as `root:chrome`).
- `load-worktree-env.mjs` — `.env.worktree` loader for configs that run outside
a moon task (Next/Playwright configs).
- `build-sprite.js`, `assert-pnpm-version.ts` — codegen/publish helpers behind
the `root:icons` task and the publishable packages' `prepublish` chain.
- `build-sprite.js` — codegen behind the `root:icons` task.
- Two scripts guard the versions this repo states more than once. They differ in
what they read:
- `check-tool-pins.ts` compares **files**. Six of them repeat a `.prototools`
version (`versionConstraint`, the `@moonrepo/cli` and `@types/bun` catalog
entries, `packageManager`, `.node-version`, `engines.node`), and one repeats
a catalog version (the `playwright@<version>` argument in `ci.yml`). The
`root:check-tool-pins` task runs it, and CI runs that task on every pull
request.
- `check-pnpm-binary.ts` tests the **binary**: the pnpm that is on PATH right
now. It runs in the publishable packages' `prepublish` chain, where a wrong
pnpm would publish a mismatched package.
- `prototools.ts` reads the pins. It is imported, not run.

The rest of this document explains `wt` in detail and walks through the most
common workflows.
Expand Down
73 changes: 0 additions & 73 deletions scripts/assert-pnpm-version.ts

This file was deleted.

63 changes: 63 additions & 0 deletions scripts/check-pnpm-binary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { spawnSync } from 'node:child_process';

import { pinnedVersion, protoToolsPath } from './prototools';

/**
* Fails a publish when the pnpm binary on PATH is not the version `.prototools`
* pins. A different pnpm can resolve or pack a package another way, so the
* published artifact may not match the repo.
*
* This script tests a binary, not a file. The publish chain therefore runs it
* (`.moon/tasks/tag-publishable.yml`), and CI does not. CI has no publish to
* protect. `check-tool-pins.ts` is the counterpart. It compares the version in
* each file that repeats a pin, and CI runs it on every pull request.
*
* Run `proto use` after a pin bump. A publish fails until you do.
*/

const expectedVersion = pinnedVersion('pnpm');

function fail(message: string): never {
console.error(message);
console.error(
`Install or activate the pnpm version pinned in ${protoToolsPath} before publishing.`
);
process.exit(1);
}

if (expectedVersion === null) {
fail(`Could not find a pinned pnpm version in ${protoToolsPath}.`);
}

const pnpmVersion = spawnSync('pnpm', ['--version'], { encoding: 'utf8' });

if (pnpmVersion.error != null) {
fail(`Could not run pnpm --version: ${pnpmVersion.error.message}.`);
}

if (pnpmVersion.status !== 0) {
fail(
[
`pnpm --version exited with status ${pnpmVersion.status ?? 'unknown'}.`,
pnpmVersion.stderr.trim(),
]
.filter(Boolean)
.join('\n')
);
}

// The last version-shaped line of stdout. proto's shim prepends a notice when
// it must resolve or install a version first, and prints that notice as NDJSON
// under AGENT=1. So the whole buffer is not the version.
const actualVersion =
pnpmVersion.stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => /^\d+\.\d+\.\d+/.test(line))
.pop() ?? '';

if (actualVersion !== expectedVersion) {
fail(
`Expected pnpm ${expectedVersion}, but this command is running pnpm ${actualVersion || '(empty version output)'}.`
);
}
Loading