Bean & Bark: a teammate joins
Monday's post-mortem verdict: the pricing bug happened because pricing logic lives in two places — the API and Tunde's storefront each carry a copy. The fix is one shared BeanAndBark.Pricing library in the platform repo, released as v1.4.0. Which raises the question that has broken a thousand onboarding days: how does this repo build against code that lives in that repo — on your machine, on Tunde's, and on a CI runner that has neither?
- Declare a cross-repo dependency by what it is — git URL, release tag, project path — with
kanject add lib - Read what
kanject syncwrites: the lockfile and the generated MSBuild imports - Explain why a fresh clone builds with
sync+dotnet buildand nothing else — and why the tag matters - Keep CI honest with
sync --locked, and know what KANCLI196 is telling you
Declare what it is, not where it sits
The reflex fix is a <ProjectReference> to ../../platform/src/… — which builds on exactly one machine: the one with platform checked out at that relative path. A path on someone's disk is not a dependency declaration. A cross-repo dependency has three identifying facts — the repo, the ref, and the project inside it — plus the project in this service that uses it:
kanject add lib \ --repository git@github.com:beanandbark/platform.git \ --ref v1.4.0 \ --project src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj \ --consumer BeanAndBark.Orders.Api.csproj + added BeanAndBark.Pricing to manifest.json ✓ Synced. 1 consumer edge(s) wired via kanject.g.props. --consumer names a project in the service root — here the API project itself, beside kanject-cli/. add lib appended one entry to manifest.json and ran sync straight away (pass --no-sync to defer). Notice what it did not do: your consumer .csproj is never edited — the wiring arrives another way.
"dependencies": [ { "name": "BeanAndBark.Pricing", "repository": "git@github.com:beanandbark/platform.git", "ref": "v1.4.0", "projectPath": "src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj", "consumers": [ "BeanAndBark.Orders.Api.csproj" ] }] What sync actually does
Sync resolved v1.4.0 to a commit with git ls-remote, cloned the platform repo at that commit into the git-ignored .kanject/cache/, and wrote two things. manifest.lock.json records the repository, the ref you asked for, the commit it resolved to, the project, and the consumer:
{ "schemaVersion": 1, "dependencies": [ { "repository": "git@github.com:beanandbark/platform.git", "requestedRef": "v1.4.0", "resolvedSha": "3e7a9c1d52b84f06a1c9e2d7b4f8a06c5d13e9b2", "projectPath": "src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj", "consumerProjectFile": "BeanAndBark.Orders.Api.csproj" } ]} And kanject-cli/kanject.g.props + kanject.g.targets, generated MSBuild files that wire the cached project into your build, imported through a Directory.Build.props and Directory.Build.targets beside the API project. Commit all of those; delete .kanject/ any time and re-sync. Then review lockfile diffs: a changed resolvedSha is a dependency upgrade, and it deserves the same eyes as any other change in the PR.
Tunde's first build
# Tunde, day one: clone, resolve, build. No sibling checkouts, no wiki page.git clone git@github.com:beanandbark/orders-api.gitcd orders-api/src/BeanAndBark.Orders.Apikanject syncdotnet build One guess before he runs it: which commit of the pricing library does Tunde get? Every kanject sync re-resolves refs with git ls-remote — so the answer depends on what you pinned. You pinned a release tag, which doesn't move, so his sync resolves to the commit already in the lock:
── Sync generated MSBuild imports ───────────────────────────────────────────── ✓ Synced 1 unique dep(s) across 1 consumer edge(s). no dep changes since lastsync. Lock: kanject-cli/manifest.lock.json (sha256:c5005b7bb9e2…) Wired: kanject-cli/kanject.g.props + .g.targets "No dep changes" is the reproducibility guarantee: same commit as yours, without a wiki page or a sibling checkout. Had you declared --ref main, his first sync would have built whatever main was that morning and changed the lockfile under him. From here, he runs kanject sync again after any pull that changes manifest.lock.json.
CI: verify, never drift
# CI: verify — never silently upgradekanject sync --locked On a runner, --locked changes sync's job from resolve to verify: it resolves every ref as usual, then refuses to write a lockfile that differs from the committed one. If someone edited a ref and forgot to re-sync — or a branch ref moved — it fails fast with KANCLI196 instead of silently building something nobody reviewed:
✗ `kanject sync --locked` would change manifest.lock.json but is forbidden to.The committed lock no longer matches what the manifest resolves to. KANCLI196 → Run `kanject sync` (without `--locked`) on a dev machine, commit theupdated lock, and re-run CI. A surprise dependency upgrade in CI is exactly the class of Friday you've just lived through. The pipeline Kanject CLI scaffolds runs plain kanject sync before dotnet build; change that line to --locked to make it verify. (--offline goes further and skips the network, replaying the lock from a cache that must already be filled.)
- Declare cross-repo dependencies by identity — repo, release tag, project path, consumer — via
kanject add libintomanifest.json; consumer csprojs are never edited. syncresolves the ref to a commit withgit ls-remote, clones it into the disposable.kanject/cache, and writes the lockfile plus generated MSBuild imports (kanject.g.props/.g.targets).- Commit the manifest, the lockfile and the generated imports, and review lock diffs — a changed commit is a dependency upgrade. A fresh clone needs only
sync+dotnet build, and a pinned tag gives it your commit. sync --lockedin CI fails fast (KANCLI196) when what the manifest resolves to differs from the committed lock — including when a branch ref moved.
sync --locked. What happened?v1.5.0. Move the API onto it so the upgrade shows up in review.ref in manifest.json → dependencies[], then let sync re-pin the lock. Watch what changes in manifest.lock.json.Show solution
"ref": "v1.4.0" to "ref": "v1.5.0" and run kanject sync. It reports "1 dep changed", and the lockfile diff shows the new requestedRef and resolvedSha — commit them together with the regenerated kanject.g.props. Upgrading pricing is a one-line manifest change with a reviewable lock diff, never a surprise.kanject sync ref but the diff touches only manifest.json. Predict exactly how this PR dies in CI — which command, which code, and what the author should have done.Show solution
kanject sync --locked runs before the build, resolves the edited ref to a commit the committed lock doesn't have, and fails fast with KANCLI196 — before a single line compiles. The author should have run kanject sync locally and committed the updated manifest.lock.json in the same PR, making the upgrade visible in review.