RFC: workspace task cache - #22
Conversation
|
Warning Review limit reachedNext included review available in 51 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Implements the pnpm-as-the-CI-engine RFC's engine as a Rust-only PoC under the command name pipeline (the name ci is the clean-install command): a frozen install first, affected-since-merge-base selection as a pre-pass with full-graph fallthrough on workspace-root changes, the pipeline's task set scheduled over the dependency-closed task graph without bailing, a local task cache (pnpm/rfcs#22, local tier only) with log replay and guarded working-tree restoration, and a run report (events.ndjson + summary.json) under the pnpm cache directory. New settings: pipelines and pipelineBase in pnpm-workspace.yaml, and tasks entries gain outputs, inputs, env, and cache. Task cache keys are computed for the whole plan before anything runs, over a dependency-closed graph so a key never depends on how the run was narrowed. The working-tree restore records content hashes of what it produced and refuses to overwrite or delete anything it cannot account for. Deliberately single-stack on this PoC branch: no TypeScript counterpart, per the experiment's scope. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared-artifact protocol encoded package identity and source integrity at the top level of every candidate and signed payload. That shape could only represent dependency side effects, even though task caching is designed to reuse the same transport and trust model. Move those fields into a discriminated subject shared by the Rust and TypeScript implementations. Dependency artifacts retain their package and source integrity, while workspace-task artifacts identify a workspace-relative project and task. Validate each subject against its artifact kind, input-key domain, and allowed owner type, and include the subject in pnpr's entry identity and candidate matching. This intentionally changes the experimental wire format, so pnpr servers and clients must be upgraded together. Related to pnpm/rfcs#22.
Cache the output of a workspace project's own scripts — files, logs, and exit code — keyed on declared inputs, reusing the artifact protocol RFC 0007 shipped. Scope it as a cache rather than a task runner: no dependsOn, upstream tasks are the same-named script in each workspace dependency, which is what runRecursive's topological chunking already implies. Adding dependsOn later changes which upstream keys feed a task key and nothing else in the document. Two things pnpm knows that a tool sitting on top does not, and they are the argument for building this at all: the resolved dependency graph per project, so a task invalidates on the dependencies that project actually resolves rather than on any lockfile edit; and a file-granular content-addressed store, so identical outputs across tasks cost one copy. Specify the key, the tasks section in pnpm-workspace.yaml with outputs mandatory for cacheability, and the part with no precedent in 0007 — restoring into a directory the user owns: never hardlink from CAFS, delete what the previous run left, and refuse to overwrite a file nothing accounts for. Note that a cache hit must still replay logs and still sync injected dependencies. Record the protocol change this needs first: ArtifactCandidate and ArtifactPayload require package identity next to the opaque key, which 0007's own forward-compatibility constraint forbids. Both need a kind-discriminated subject. pnpr is unaffected, and the change is free only while the protocol is opt-in and alpha. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keying a task on the same-named upstream script is not merely limited, it is wrong. Project A's test consumes node_modules/B/dist, produced by B's build; the same-name rule keys it on B's test, which includes tests A does not consume and omits the build it does. Link targets enter the dependency-graph hash by identity alone, so nothing makes up the difference. When B has build but no test script, A's test key contains nothing about B at all, and a rebuild of B leaves A's test hitting cache against a dist it has never seen. Take a dependency on the workspace task orchestration RFC and let dependsOn name the producing task, with its pass-through rule keeping a project that lacks a script from severing the chain. Split the key's dependency component in two: the resolved package graph for external dependencies, the declared upstream task keys for internal ones. Record hashing the materialized workspace dependencies as the fallback for undeclared edges rather than the primary mechanism, and add the question of how to detect a task that reads an upstream output it never declared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Name the git-index fast path as the input-hashing question's expected answer (index blob hashes via ls-files -s, hashing only stat-dirty files), leaving the untracked-tree and stale-stat residue as what still wants measuring. And require the last-outputs restoration record to carry content hashes rather than a path set: a set cannot tell a produced file from a produced-then-edited one, and the proof of concept overwrote a user edit exactly that way before its record grew hashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subject change this RFC called a prerequisite has landed (pnpm/pnpm#14267), as have RFC 0007's two then-unfinished items and the orchestration RFC's scheduler, so the document was describing work as pending that is now the ground it stands on. It also carried three findings the pnpm-ci proof of concept established and one contradiction with it: - The graph a task's upstream keys resolve through must be dependency-closed. A selection that narrows the graph rather than the requested set truncates `^` edges at its boundary, which makes the key a function of the invocation: spurious misses when the selection widens, and a stale hit when a workspace dependency falls outside it entirely. - The runtime version must be fingerprinted from the workspace, not the invoking process, or a `--dir` run splits the cache under a context-aware toolchain. - The default input set is tracked *and* untracked-unignored files. A source file not yet added to the index is one somebody is editing, and the previous wording served a stale hit for it. The git-index fast path covers only the tracked half, which is the other half of the untracked-residue measurement. - `cache: false` and `--no-cache` settle the bypass half of the bypass question; repair stays open. Lockfile pinning does not carry over as it stands — a pin is recorded on a package snapshot and a task has no snapshot — so that becomes an open question rather than a lesser weight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3XFeFuiejqx4Fxp1WCd8e
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3XFeFuiejqx4Fxp1WCd8e
The orchestration RFC merged as 0008, so the cross-reference resolves and a status note can say plainly what exists: the scheduler and the artifact subject, plus a draft proof of concept whose findings this text already carries. Nothing else here is built. RFC 0007 has meanwhile recorded two things that reach this document: the lockfile pin's actual shape, which sharpens why a task has nowhere to record one, and the shipped single-channel reuse policy, under which a persisted artifact is a miss once the configured server changes. Tasks should keep that narrowing rather than relax it, but a whole task cache going cold on a server rename is a different order of cost from rebuilding one dependency. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3XFeFuiejqx4Fxp1WCd8e
6621edc to
92abdac
Compare
PR Summary by QodoRFC: Define content-addressed caching for workspace tasks
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
Cache the output of a workspace project's own scripts — files, logs, and exit code — keyed on declared inputs, reusing the artifact protocol that #20 shipped.
Depends on #23 for the task graph. That dependency is load-bearing, not cosmetic — see below. Both #23's scheduler and the protocol change this RFC asked for have since shipped, so what is left here is the key, the declarations, and the restore.
Why pnpm rather than a tool on top
Two things pnpm knows that Turborepo and Nx cannot:
calcDepStateInputKey, shipped with RFC: shared side-effects cache #20.Why it needs the task graph
An earlier revision of this RFC scoped itself as cache-only and keyed a task on the same-named upstream script. That is wrong, not merely limited.
Project A's
testconsumesnode_modules/B/dist, produced by B'sbuild. The same-name rule keys it on B'stest— including tests A does not consume, omitting the build it does. Nothing makes up the difference: link targets enter the dependency-graph hash by identity alone (graph-hasher/src/index.ts:481-486gives themfullPkgId: linkTargetNode,children: {}, no content).The sharp case: B has
buildbut notestscript. Then A'stestkey contains nothing whatsoever about B. Change B's source, rebuild, and A'stesthits cache against adistit has never seen.With #23's
dependsOn,test: ['build']names the producing task, and its pass-through rule keeps a project that lacks a script from severing the chain. The key becomes correct by construction rather than correct by accident.What it specifies
pre/post), declared input digests, declared env as a name-to-value map, the resolved package graph for external dependencies, and the declared upstream task keys for internal ones. Env values are hashed into the key and never recorded in provenance. The graph those upstream keys resolve through must be dependency-closed: a selection narrows which tasks are requested, never which projects the graph contains, or the key becomes a function of the invocation.outputs,inputs,env, andcachealongside RFC: workspace task orchestration #23'sdependsOnin the sametaskssection.outputsis mandatory for cacheability, and an absent key means not cacheable rather than produces nothing — RFC: shared side-effects cache #20's "absence of information is not permission" rule applied to a different field.outputs: []is the positive assertion that letslintbe cached for its logs alone.syncInjectedDepsAfterScriptsor consumers of an injected dependency see the previous build.organization, compatibility is usuallyuniversal, and eligibility is trivially satisfied because a workspace task is first-party.What has since shipped
The prerequisite this RFC called out — #20's rule that the protocol must not embed package identity in the shape of a request or response key, which
ArtifactCandidateandArtifactPayloadviolated by requiringpackageandsourceIntegritynext to the opaque key — landed in pnpm/pnpm#14267 while it was still free. Both types now carry a kind-discriminatedsubject, and aworkspace-tasksubject is nameable on the wire today.#20's two then-unfinished items also shipped, and they land here unevenly: persistent quarantine (pnpm/pnpm#14259) is inherited whole and matters more for tasks than for dependencies, while lockfile pinning (pnpm/pnpm#14243) does not carry over as it stands, because a pin is recorded on a package snapshot and a workspace task has none.
#23's scheduler is merged in both stacks. The one change it still needs for this RFC is the requested-versus-contained split described above.
Open for discussion
Whether this is pnpm's scope at all is the first question, and the RFC argues the "do nothing, let Turborepo do this" alternative as a real one — including that being a remote-cache backend for Turborepo is cheap, non-exclusive, and worth doing on its own merits. Also open: what an undeclared
dependsOnshould key on, whether pnpm can detect a task that reads an upstream output it never declared, non-relocatable output, input-hashing cost on misses, two tasks declaring the same output root, and where a task artifact's lockfile pin would live if it has one at all.🤖 Generated with Claude Code