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. 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, ref, 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 - Keep CI honest with
sync --locked, and know what KANCLI196 is telling you
The trap you're avoiding
The reflex solution is a <ProjectReference Include="..\..\platform\src\BeanAndBark.Pricing\…" /> — and it works perfectly on the one machine that happens to have platform checked out at exactly that relative path. Tunde's laptop doesn't. The CI runner definitely doesn't: its clean checkout fails restore with an "unable to find project" error, and the build is red before lunch. A path on someone's disk is not a dependency declaration.
Declare what it is, not where it sits
A cross-repo dependency has three identifying facts: the repo, the ref, and the project inside it — plus the consumer in this repo that uses it:
kanject add lib \ --repository git@github.com:beanandbark/platform.git \ --ref main \ --project src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj \ --consumer src/BeanAndBark.Orders/BeanAndBark.Orders.csproj That appends one entry to manifest.json and immediately runs sync (pass --no-sync to defer). Notice what it does 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": "main", "projectPath": "src/BeanAndBark.Pricing/BeanAndBark.Pricing.csproj", "consumers": ["src/BeanAndBark.Orders/BeanAndBark.Orders.csproj"] }] What sync actually does
Sync generated MSBuild imports ──────────────────────────────── ✓ Synced 1 unique dep(s) across 1 consumer edge(s). 1 dep changed. Lock: kanject-cli/manifest.lock.json (sha256:4be1a9c02f…) Wired: kanject-cli/kanject.g.props + .g.targets Three lines, three artifacts. Sync resolved main to a commit SHA (git ls-remote), cloned the platform repo at that exact SHA into the git-ignored .kanject/cache/, and wrote two things: manifest.lock.json — the ref pinned to its resolved commit, with content hashes — and kanject.g.props + kanject.g.targets, generated MSBuild imports that wire the dependency into your build. Your csproj stays untouched; delete .kanject/ any time and re-sync.
Commit the lockfile and review its diffs: a changed SHA in manifest.lock.json 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.git && cd orders-apikanject syncdotnet build One guess before he runs it: which commit of the pricing library does Tunde get — whatever main points at this morning, or the one you built against on Friday? Yours. Sync replays the lock: same SHA, same content hashes, byte-identical build, even though main has moved since. And after this first sync, the git hooks kanject installed keep him current automatically — every git pull that brings a manifest change re-syncs without a keystroke (run kanject msbuild status any time to inspect the wiring).
CI: verify, never drift
# CI: verify — never silently upgradekanject sync --locked On a runner, --locked changes sync's job from resolve to verify: if the manifest and lockfile disagree — someone edited a ref and forgot to re-sync — it fails fast with KANCLI196 instead of silently building something nobody reviewed. A surprise dependency upgrade in CI is exactly the class of Friday you've just lived through. (The AWS pipeline from the CLI docs runs sync before dotnet build for precisely this reason; --offline goes further and forbids the network entirely, trusting lock + cache.)
- Declare cross-repo dependencies by identity — repo, ref, project path, consumers — via
kanject add libintomanifest.json; consumer csprojs are never edited. syncresolves the ref to a SHA, clones into the disposable.kanject/cache, and writes the lockfile plus generated MSBuild imports (kanject.g.props/.g.targets).- Commit
manifest.lock.jsonand review its diffs — a changed SHA is a dependency upgrade. A fresh clone needs onlysync+dotnet build. sync --lockedin CI fails fast (KANCLI196) on manifest/lock drift; pin prod-critical deps to a tag or SHA, not a branch.
sync --locked. What happened?pricing-v2.1.0. Move the API off the moving main ref and onto the tag, so future upgrades are always explicit.ref in manifest.json → dependencies[], then let sync re-pin the lock. Watch what changes in manifest.lock.json.Show solution
"ref": "main" to "ref": "pricing-v2.1.0" and run kanject sync. The lockfile diff shows the newly resolved SHA — commit both files together. From here, 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, compares manifest against lockfile, finds the edited ref unpinned, 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.