Bean & Bark: release the Pricing SDK

View .md

Friday's doubled totals came from pricing logic copied into two applications. The post-mortem fixed the source tree; now the shared BeanAndBark.Pricing library has to become something the Orders API, storefront, and future Roastery Desk can all install. The dangerous part is not dotnet pack. It is proving the package is restorable, versioned honestly, and unchanged between Tunde's test and the approved release.

You'll learn
  • Turn an SDK repository into an explicit public/private package surface
  • Use the evaluated dependency graph to stop a private implementation leaking through a public contract
  • Catch an already-claimed version and let bump plan write the new ones
  • Build a clean consumer from the RC feed and publish the approved stable bytes with a journal

Declare what is allowed to leave

The repository contains two packable projects: the contract partners may consume, and roastery-only discount rules. packages init --split gives that boundary a checked-in policy instead of leaving it in someone's memory.

bash
# The platform repo, where chapter 4's BeanAndBark.Pricing livescd platform​# Keep the release tool version reviewable in this repodotnet new tool-manifestdotnet tool install Kanject.Cli​# This SDK has a public contract and private roastery rulesdotnet kanject packages init --splitdotnet kanject packages plan --writedotnet kanject packages verify
json
"policy": {  "allowPublic": [    "BeanAndBark.Pricing"  ],  "denyPublic": [    "BeanAndBark.Pricing.RoasteryRules"  ]}

The first gate goes red—and that is good

verify evaluates the same MSBuild graph pack sees. It finds that the public Pricing project still references RoasteryRules: a public consumer would need a package the policy forbids from leaving. The build is green, but the product boundary is not.

The fix is not to add the private package to allowPublic. Move the shared calculation contract into the public project and keep the seasonal margin tables behind the private implementation. Rerun plan --write, review the one-line public-list diff, then rerun verify. The gate is now green for the reason you intended.

Ask whether the version is still yours to use

The Pricing project still says 1.2.4, the version from before the duplicate logic was removed. The feed already owns that identity. versions --packed-dir --strict compares the packed payload and normalized dependency contract, then names the project that needs a bump.

bash
dotnet pack BeanAndBark.Platform.slnx -c Release -o packed​dotnet kanject packages versions \  --packed-dir packed \  --strict

The fix corrects the totals without changing the contract, so this is a patch. Run these with credentials that can read the feeds:

bash
# You choose the part; the CLI works out which packages need itdotnet kanject packages bump plan --all patch​# Write exactly the versions the plan proved into the project filesdotnet kanject packages bump apply package-bump-plan.json

The plan gives Pricing 1.2.5. It bumps RoasteryRules too: the refactor changed it, and it now references Pricing, so its packed dependency moves to 1.2.5 as well. Typing 1.2.5 into Pricing by hand would have missed that, and RoasteryRules would have shipped new contents under a version the feed already owns. The release would stop with KANCLI495.

Commit the edited project files, re-pack into a clean packed directory, and rerun the audit. It passes. Kanject CLI still doesn't decide semantic-version magnitude; it makes an accidental overwrite impossible and puts the decision in the pull request.

Give Tunde a candidate, not a promise

The release build compiles and packs once, and the stable packages travel forward in the protected pipeline artifact. The evidence step checks every destination as if the release were armed and writes the release plan and preflight verdicts; it pushes nothing. The candidates are then derived from those stable packages: candidate materialize adds the suffix to each version and points in-set dependencies at their rc siblings, leaving every other byte alone. The suffix comes from the candidate feed's own history, so no human guesses rc.7, and two runners can't both claim it silently.

These steps run in the build job from Publish .NET packages, with AWS CodeArtifact feeds: REPOSITORY names the release feed, CANDIDATE_REPOSITORY the -rc feed, and RELEASE_V3 the release feed's NuGet URL. next-rc and smoke feed take no profile, so the REPOSITORY= prefix points each at the -rc feed for that one command — without it, the smoke would never touch Tunde's candidate.

bash
# Evidence first: every destination checked as if armed, nothing pusheddotnet kanject packages publish \  --profile release-evidence \  --packed-dir packed​# The suffix comes from the -rc feed's own historyRC_SUFFIX="$(REPOSITORY="$CANDIDATE_REPOSITORY" dotnet kanject packages next-rc)"​# Same assemblies, -rc identities: derived from the stable packages, not rebuiltdotnet kanject packages candidate materialize \  --stable-dir packed \  --candidate-dir packed-rc \  --suffix "$RC_SUFFIX"​dotnet kanject packages publish \  --profile candidate \  --packed-dir packed-rc \  --ci​# Restore and build clean consumers from the -rc feed itselfREPOSITORY="$CANDIDATE_REPOSITORY" dotnet kanject packages smoke feed \  --plan release-plan-rc.json \  --dep-source "$RELEASE_V3" \  --build

The smoke command creates clean consumers and restores from the actual RC feed. A package that only worked because your global NuGet config knew a private source now fails here, before the approval button exists. Now Tunde takes the same candidate into the storefront:

bash
# Tunde, in the storefront repo: sign in to the -rc feed once,# then give the Storefront API the candidateaws codeartifact login --tool dotnet \  --domain beanandbark --repository beanandbark-packages-rcdotnet add src/BeanAndBark.Storefront.Api package BeanAndBark.Pricing \  --version 1.2.5-rc.7dotnet test

login registers the -rc feed as a NuGet source with a short-lived token, so the storefront restores 1.2.5-rc.7 the way CI just did, and its tests run against the exact assemblies the release will ship. Tunde confirms the wholesale totals against Thursday's orders, and the candidate is the thing that gets approved — not a project reference or a zip in chat.

Approve what was tested

Approval releases the trusted publisher, not another build. The publish job installs the CLI at a pinned version instead of running the one the repository's tool manifest names, so a commit can't change the code that holds the release credentials. It refuses a packed set that differs from the expected package list kept in the protected environment, refuses a version already published with different contents, publishes dependencies first, and writes a journal entry for every push.

bash
# Trusted publish stage, after approval: a pinned CLI,# never the one the repository's tool manifest namesdotnet tool install -g Kanject.Cli --version "$PUBLISHER_CLI_VERSION"​PUBLISH_PUBLIC=true kanject packages publish \  --profile approved-release \  --packed-dir packed \  --ci

The release plan, the preflight verdicts and the journal are the receipts Nadia asked for: what should happen, that every destination was safe before anything changed, and what actually happened — enough to resume a partial release without repeating finished pushes.

Recap
  • packages init --split, plan --write, and verify turn a public/private intention into an evaluated, blocking release boundary.
  • versions --strict names a claimed version whose contents changed; bump plan and bump apply write the new versions, including the packages that depend on it.
  • Candidates are derived from the stable packages with a suffix from the candidate feed, and clean consumers restore and build them from that feed before the release becomes approvable.
  • A pinned trusted publisher ships the approved stable identities and leaves release-plan, preflight, and journal evidence.
Before Tunde updates the storefront3
The public Pricing package references a private first-party package. dotnet pack succeeds. What should the release do?
Why is stable publication not another build after approval?
Pricing changed, and the feed already owns 1.2.4. What should write 1.2.5?
Try it yourself 2
1 Create a closure failure on purpose
In a sample two-package repo, mark one package public and its dependency private. Run packages verify, then explain why allowing the dependency publicly may be the wrong fix.
Ask whether consumers need the implementation or only an abstraction that currently lives in the wrong project.
Show solution
The durable fix is often to move the consumer-facing contract into the public package and keep implementation/configuration private. allowPublic is appropriate only when the dependency itself is intentionally part of the supported public surface.
2 Read the three receipts
Open a release plan, preflight verdict document, and publication journal. Write one sentence describing the question each artifact answers.
Show solution
Plan: what is intended? Preflight: is every destination safe before mutation? Journal: what actually completed, failed, or can resume?
Was this page helpful?