Per-package codegen with a root prepare task: 9 of 23 task hashes change between two runs of the identical commit while every reported hash ingredient is identical #13624
Replies: 3 comments
Follow-up: a stronger reproduction — identical git tree, different task hashSince posting, I ran a cleaner control that removes the last bit of ambiguity in the original report. Previously I could only show that every ingredient Turborepo reports in the run summary was identical. Now I can show that the git tree object is identical and the task hash still changes. Setup. Three consecutive Vercel preview deployments of the same branch, same lockfile, same
Result for the 10
So runs A and C built the same tree object, with the same dependency-task hashes (verified in the logs — every upstream task reports the identical hash), and the app task still computed a third distinct hash and missed the cache. The task hashed unstably is the only one in the graph with an inferred framework. From It does not reproduce locally. Applying the exact same source edit as run B locally leaves all 11 hashes untouched, including the app task, and a second local run restores 10/10 from cache. The instability only appears between Vercel deployments. Relevant task definition (inputs are declared explicitly, and the only {
"dependsOn": ["^codegen:buf", "//#codegen:prepare:buf"],
"env": ["!VITE_*", "!NEXT_PUBLIC_*"],
"inputs": [
"./buf.gen.ts",
"$TURBO_ROOT$/packages/rpc-codegen/bin/**",
"$TURBO_ROOT$/packages/rpc-codegen/plugins/**",
"$TURBO_ROOT$/packages/rpc-codegen/src/shared-packages.ts",
"$TURBO_ROOT$/packages/rpc-codegen/src/prune-rewrite.ts",
"$TURBO_ROOT$/packages/rpc-codegen/git-repo.ts",
{ "mode": "jit", "globs": ["./.codegen-schemas-inputs-hash"] }
],
"outputs": ["./src/gen/service/**", "./gen/service/**"]
}If framework inference contributes anything to the hash beyond the |
Follow-up: a structurally identical task type in the same runs does not moveEarlier in this thread I reported 9 of 23 SetupTwo Vercel builds of the same branch, turbo 2.10.6:
The two commits differ by exactly one added comment line in a Both runs resolved identical values for the two external inputs codegen depends on (logged by our own prepare tasks):
Run B was a redeploy, which forces execution, so every task logged The two task types are structurally identical"codegen:buf": {
"dependsOn": ["^codegen:buf", "//#codegen:prepare:buf"],
"env": ["!VITE_*", "!NEXT_PUBLIC_*"],
"inputs": [ /* … static globs … */
{ "mode": "jit", "globs": ["./.codegen-schemas-inputs-hash"] } ]
},
"codegen:gql": {
"dependsOn": ["^codegen:gql", "//#codegen:prepare:gql"],
"env": ["!VITE_*", "!NEXT_PUBLIC_*"],
"inputs": [ /* … static globs … */
{ "mode": "jit", "globs": ["$TURBO_ROOT$/graphql-schema.json"] } ]
}Both Result
The 9 movers: This is the same set of 9 packages in every pair of runs we have compared. Why the control matters
Whatever changes those 9 hashes is specific to those tasks, and there is now a stable sibling in the same run to diff against. Previously established in this thread
Happy to run further experiments on this graph if there is a specific field or debug flag worth dumping. |
Narrower repro: the entry package's own task never reproduces its hash, while all of its dependencies doFollow-up to the two comments above. I have a much smaller reproduction now — a graph of 17 tasks in which 15 are byte-stable across builds and the only 2 that move both belong to the entry package. SetupVercel builds each app by running turbo from the app directory, so the entry package is inferred from cwd: turbo 2.10.6. The entry package ( Both codegen task types declare explicit "codegen:buf": {
"dependsOn": ["^codegen:buf", "//#codegen:prepare:buf"],
"env": ["!VITE_*", "!NEXT_PUBLIC_*"],
"inputs": [ "./buf.gen.ts", /* … root globs … */
{ "mode": "jit", "globs": ["./.codegen-schemas-inputs-hash"] } ]
},
"codegen:gql": {
"dependsOn": ["^codegen:gql", "//#codegen:prepare:gql"],
"env": ["!VITE_*", "!NEXT_PUBLIC_*"],
"inputs": [ "graphql/*.graphql", "src/**/*.graphql", /* … root globs … */
{ "mode": "jit", "globs": ["$TURBO_ROOT$/graphql-schema.json"] } ]
}Three consecutive builds of the same branch
That component file matches no glob in either task's
ResultIn every build, all 15 library-package tasks reproduced identical hashes and were restored from cache. The entry package's own two tasks produced a new hash in every build and never repeated one: B3's tree is byte-identical to a tree that had already been built, so no input difference can account for its hash. A second project, What this rules out
The remaining asymmetryThe only structural difference we can find between the movers and the stable tasks is that the movers belong to the entry package — the package turbo infers from cwd, and the only package in scope that is a framework. We opt out of framework inference on these tasks with If any per-deployment value can still enter the hash through framework inference for the entry package, that would produce exactly this pattern: framework package moves every build, non-framework dependencies stay stable. I have not been able to confirm that from the run summary, since the reported ingredients are identical between runs whose hashes differ. Happy to dump anything specific for these two tasks if there is a field or debug flag that would settle it. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
We run per-package codegen (
codegen:buf,codegen:gql) in a large monorepo, scoped so thatbuilding one app only generates for that app and the packages it actually uses. That shape works,
with one exception: on Vercel, a subset of
codegen:buftask hashes changes between tworuns of the identical commit, while every hash ingredient the run summary reports for those
tasks is byte-identical.
Two questions, one about the layout and one about the hashes:
repository, per package"?
resolvedTaskDefinition,environmentVariables,hashOfExternalDependencies,dependencies,framework,directory,command) is identical, what else feeds the hash, and how do weinspect it?
All names below are anonymized. Structure, counts, task config and hashes are real.
Environment
2.10.610.24.0, Node24.13.0codegen:bufenvMode: strictWhat we are trying to do
Generated RPC clients derive from a separate schema repository (protobuf), pinned by commit.
Each package that consumes protos owns a
buf.gen.tsnaming the proto packages it generates from.Building
apps/ashould runcodegen:bufforapps/aplus the packages in its dependencyclosure, and nothing else.
One root task fetches the schema repo once per run and writes a per-package input pin
(
.codegen-schemas-inputs-hash) next to eachbuf.gen.ts. That pin is a content hash of only theprotos that package generates from, plus their transitive proto
importclosure. Each package'scodegen:bufthen declares its own pin as a JIT input, so a schema commit only invalidates thepackages whose protos actually changed:
{ "tasks": { "//#codegen:prepare:buf": { "cache": false, "inputs": ["scripts/prepare-codegen-inputs.mjs"], "passThroughEnv": ["CI", "SCHEMA_REPO_HASH", "SCHEMA_GIT_URL", "GIT_SSH_COMMAND", "HOME"] }, "codegen:buf": { "dependsOn": ["^codegen:buf", "//#codegen:prepare:buf"], "env": ["!VITE_*", "!NEXT_PUBLIC_*"], "passThroughEnv": ["SCHEMA_GIT_URL", "GIT_SSH_COMMAND"], "inputs": [ "./buf.gen.ts", "$TURBO_ROOT$/packages/codegen-tool/bin/**", "$TURBO_ROOT$/packages/codegen-tool/plugins/**", "$TURBO_ROOT$/packages/codegen-tool/src/shared-packages.ts", "$TURBO_ROOT$/packages/codegen-tool/src/prune-rewrite.ts", { "mode": "jit", "globs": ["./.codegen-schemas-inputs-hash"] } ], "outputs": ["./src/gen/service/**", "./gen/service/**"] }, "codegen": { "dependsOn": ["codegen:gql", "codegen:buf", "codegen:translations", "^codegen"], "cache": false } } }codegen:gqlis the same shape with its own root prepare task. Builds runturbo run build --filter=<app>, andbuilddepends oncodegen.The pin is deliberately fail-safe: if a proto reference cannot be resolved, or the schema fetch
fails, the package falls back to the whole-repo schema SHA — over-invalidates, never stale.
If there is a more idiomatic way to express "external input, scoped per package", we would rather
adopt it than keep this.
The hash observation
Deployment B is a redeploy of deployment A: same commit, same lockfile, same schema pin, no
input file differs. In each build, after the primary
codegen:bufgraph finished, we ran a nestedturbo run codegen:buf --filter=<app> --summarizeand logged each task's hash, its per-input filehashes, and the global cache inputs.
Of 23 tasks in that graph, 14 hash identically across A and B and 9 change:
packages/p1#codegen:buf0f1c828f98793f4f(HIT)0c25d63aaa8e96ef(MISS)packages/p2#codegen:buf8c1aa79318b64687(HIT)812a118e917198a0(MISS)packages/p3#codegen:bufe861d9a04eb49502(HIT)4731177494c1c619(MISS)packages/p4#codegen:bufe57190a7ca97d29a(HIT)ea821254525b1e85(MISS)packages/p5#codegen:bufe071a679d31d3147(HIT)984acfa873b845c5(MISS)packages/p6#codegen:bufd23ed1bdbf8c39d0(HIT)cbeef30e7dbefe34(MISS)packages/p7#codegen:buf3d520244a9fd4b9f(HIT)62884172f1232d0c(MISS)packages/p8#codegen:buf4bfad31c74d982b9(HIT)0c2f47fd68d7990d(MISS)apps/a#codegen:bufecc5431972f521d4(HIT)c2b94805fe00f92f(MISS)Only three of those nine are roots:
p1,p2andp3have no dependency whose hash also moved.The other six are downstream, which is expected once a dependency hash moves.
For the roots, comparing the full summary entry field by field between A and B, the only fields
that differ are
hashandcache. Identical: every declared input path and its hash (11 files),resolvedTaskDefinition,environmentVariables(configured: [],inferred: [],passthrough: []),hashOfExternalDependencies,dependencies,dependents,command,directory,framework,outputs,envMode,cliArguments.globalCacheInputsis identical too, except for the hashed values of three pass-through variables(
VERCEL_DEPLOYMENT_ID,VERCEL_URL,VERCEL_OIDC_TOKEN) — which by definition should not entera cache key, and cannot be the cause anyway, since a global change would move all 23 tasks rather
than 9.
Within a single build, two consecutive nested
--summarizeruns hash all 23 tasks identically, sothis is variance between runs, not state mutating as a run proceeds. Locally, across two
different schema pins and across three different
--filterroots, all 23 hashes are stable.What we ruled out
cache: falseprepare task.We snapshot every pin file before it and abort the comparison if any content changed; none did.
hashOfExternalDependenciesis identical per task, and identicalglobally.
dependencieslist contains many<NONEXISTENT>nodes from the^codegen:bufedge (15–27 per root; 76 for the app task). Welooked for a transit node shared by all three roots and absent from every stable task's
dependency set, which would localize the movement: there are 9 shared transit nodes and all 9
also appear under at least one task whose hash did not move. Also, the task with the second
largest transit-node count in the graph (42) is stable. So neither "has transit nodes" nor "has
many transit nodes" predicts the movement.
Questions
entries that are byte-identical apart from
hashshould be impossible. If it is not, whichingredients are omitted?
<NONEXISTENT>dependency node? Those nodes appear independenciesbut get no entry of their own, and a parent hash includes its dependency hashes,so today they are the one ingredient we cannot see or diff. Is there a flag or log level that
prints them?
cache: falseroot task contribute anything to a dependent's hash, and if so, isthat contribution visible anywhere?
--summarizereport the resolved file set and hashes formode: "jit"inputs asevaluated at execution time?
--dry=jsonnecessarily defers them, which leaves exactly thisclass of problem un-diffable without the nested-run workaround above.
pattern for an external schema dependency, or is there a first-class mechanism we are missing?
Happy to extend the public repro at
https://github.com/bitttttten/turborepo-vercel-broad-dependency-cache-repro to whatever shape is
most useful — it already covers the
^task/<NONEXISTENT>graph and JIT inputs, and previouslydid not reproduce a related invalidation, which is why this is a Help discussion and not an
issue. A previous discussion (#13590) covered the
^taskexpansion question and is closed; this isthe follow-up with the identical-commit redeploy evidence.
All reactions