Bean & Bark: a teammate joins

View .md

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?

You'll learn
  • Declare a cross-repo dependency by what it is — git URL, release tag, project path — with kanject add lib
  • Read what kanject sync writes: the lockfile and the generated MSBuild imports
  • Explain why a fresh clone builds with sync + dotnet build and 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:

bash
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
text
  +  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.

json
"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:

json
{  "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

bash
# 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:

text
── 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

bash
# 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:

text
✗  `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.)

Recap
  • Declare cross-repo dependencies by identity — repo, release tag, project path, consumer — via kanject add lib into manifest.json; consumer csprojs are never edited.
  • sync resolves the ref to a commit with git 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 --locked in CI fails fast (KANCLI196) when what the manifest resolves to differs from the committed lock — including when a branch ref moved.
Before Tunde's first PR — check yourself2
You pull a teammate's change that moves Pricing to a new tag in the manifest and lockfile. What do you run before building?
CI fails with KANCLI196 on sync --locked. What happened?
Try it yourself 2
1 Take the next release
The platform team tags v1.5.0. Move the API onto it so the upgrade shows up in review.
Edit the ref in manifest.json → dependencies[], then let sync re-pin the lock. Watch what changes in manifest.lock.json.
Show solution
Change "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.
bash
kanject sync
2 Explain the red runner
A teammate's PR edits a dependency's 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.
What does locked-mode sync compare, and when does it run relative to the build?
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.
Was this page helpful?