<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tatted Dev</title>
    <description>The latest articles on DEV Community by Tatted Dev (@tatted_dev).</description>
    <link>https://dev.to/tatted_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3518366%2Faa49110a-cf6b-4606-a2d8-56a044b78834.png</url>
      <title>DEV Community: Tatted Dev</title>
      <link>https://dev.to/tatted_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tatted_dev"/>
    <language>en</language>
    <item>
      <title>Building HoneyDrunk.Lore: My LLM Wiki and Daily News Blast</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:05:16 +0000</pubDate>
      <link>https://dev.to/tatted_dev/building-honeydrunklore-my-llm-wiki-and-daily-news-blast-2pfl</link>
      <guid>https://dev.to/tatted_dev/building-honeydrunklore-my-llm-wiki-and-daily-news-blast-2pfl</guid>
      <description>&lt;p&gt;I know I am late to the party talking about LLM wikis after Andrej Karpathy first put the pattern in public, but here is how I implemented it in my own flow.&lt;/p&gt;

&lt;p&gt;The pattern clicked for me because it names a problem I had been living inside for a while. I read a lot. Model announcements, agent infrastructure posts, .NET and Azure updates, architecture writeups, indie software notes, game-dev tooling, security research, random high-signal threads that appear before the official writeup exists. All of it can matter to HoneyDrunk eventually, but almost none of it matters at the exact moment I read it.&lt;/p&gt;

&lt;p&gt;That is the place where normal bookmarks fall apart. Bookmarks preserve the link and lose the understanding. Chat history preserves the conversation and loses the durable structure. RAG gives me retrievable chunks, then makes me rebuild the synthesis every time I ask a question.&lt;/p&gt;

&lt;p&gt;The LLM wiki idea works at a different layer. Raw sources go in. An LLM reads them, extracts the useful claims, updates topic pages, links concepts together, tracks contradictions, and keeps a maintained markdown wiki between me and the source pile. The wiki becomes a compiled artifact. The model rediscovers context once, writes it down, and keeps it warm for the next query.&lt;/p&gt;

&lt;p&gt;Karpathy's &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLM Wiki gist&lt;/a&gt;, created on April 4, 2026, and the &lt;a href="https://x.com/karpathy/status/2039805659525644595" rel="noopener noreferrer"&gt;X post that pointed people at it&lt;/a&gt; gave the pattern a crisp public shape. My implementation became HoneyDrunk.Lore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of Lore
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk.Lore is my compiled research knowledge surface for the studio. It is a flat-file wiki: markdown on disk, source-backed, Obsidian-friendly, versioned in git, and maintained by agents under an explicit schema.&lt;/p&gt;

&lt;p&gt;The pipeline is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw sources
  -&amp;gt; compiled wiki
  -&amp;gt; AGENTS/schema
  -&amp;gt; query/output
  -&amp;gt; compile/lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;raw/&lt;/code&gt; layer is the evidence locker. Articles, notes, clipped pages, selected social posts, and research outputs land there as immutable source material. Once something is in &lt;code&gt;raw/&lt;/code&gt;, the rule is simple: add, do not rewrite history.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;wiki/&lt;/code&gt; layer is the compiled surface. It is where source material turns into concept pages, entity pages, topic indexes, confidence notes, and links. This is where claims get reinforced, weakened, superseded, or connected to other things I already know. The work that makes it more than a pile of summaries happens here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;AGENTS.md&lt;/code&gt; file is the schema and operating manual. It tells the agent what the directories mean, what operations exist, how to treat citations, when to record gaps, how to handle contradictions, and what counts as a durable claim. This matters more than it sounds. Without the schema, an LLM wiki is just a helpful assistant making markdown. With the schema, it becomes a maintained knowledge system with rules.&lt;/p&gt;

&lt;p&gt;Queries write back into &lt;code&gt;output/&lt;/code&gt;. If I ask, "what does today's agent security signal imply for HoneyHub?" I want the answer filed as a dated artifact with citations, confidence, and gaps. Later compile passes can crystallize durable parts of that answer back into the wiki. The question becomes another source of learning instead of disappearing into a chat transcript.&lt;/p&gt;

&lt;p&gt;Compile and lint are the maintenance loop. Compile ingests unprocessed sources, reconciles claims, updates indexes, and promotes durable query outputs. Lint looks for orphans, stale claims, contradictions, weak sourcing, and missing links. The payoff is the rhythm. The wiki has a standing process for staying healthy, so it keeps working long after the first burst of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Daily Blast
&lt;/h2&gt;

&lt;p&gt;The most recent work on Lore made the system feel less like a personal archive and more like an operator surface.&lt;/p&gt;

&lt;p&gt;Every day, Lore can produce a news blast for Discord. It reviews the latest saved source window, pulls out the useful signal, and turns it into a compact daily briefing. The blast stays small on purpose: a decision-support summary for the parts of the world that matter to HoneyDrunk right now.&lt;/p&gt;

&lt;p&gt;The shape is deliberately strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top 10 web stories.&lt;/li&gt;
&lt;li&gt;Top 10 X/Twitter posts.&lt;/li&gt;
&lt;li&gt;Actual source URLs and tweet URLs.&lt;/li&gt;
&lt;li&gt;Two or three sentence summaries.&lt;/li&gt;
&lt;li&gt;A HoneyDrunk angle for why each item matters.&lt;/li&gt;
&lt;li&gt;Discord messages split before they hit platform limits.&lt;/li&gt;
&lt;li&gt;No private internal tool names in the public-facing blast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line matters. The public blast should say what happened, where it came from, and why it matters. It should not leak the names of whatever private worker, script, lane, or local process happened to collect the source. Public signal should be clean signal.&lt;/p&gt;

&lt;p&gt;The X/Twitter lane is selective on purpose. I do not want Lore broadly crawling social media. That turns into noise immediately, and it makes the wiki depend on the most chaotic surface in the stack. But sometimes the useful signal really does appear there first: a primary-source launch post, a developer thread, an early warning, a discussion around a new agent pattern before the canonical blog post exists.&lt;/p&gt;

&lt;p&gt;So Lore treats X/Twitter as an early-signal lane that has to earn its way into the durable record. Selected posts can be captured with the actual tweet URL, marked as early signal, and compiled like any other raw source. When a durable source appears later, an official blog post, docs page, changelog, model card, transcript, or technical writeup, that source should join or supersede the social capture. The tweet stays useful as first-report context, and it has to wait for corroboration before it carries any authority.&lt;/p&gt;

&lt;p&gt;That distinction is the difference between "I saw a thing" and "the wiki knows a thing."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Wanted This
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk is not a single-product company in my head. It is a many-decade personal workshop: a computing platform, a studio, a lab, and a place to keep building strange useful things with AI agents over a long horizon.&lt;/p&gt;

&lt;p&gt;That changes what research means.&lt;/p&gt;

&lt;p&gt;I collect sources because they eventually affect decisions. Agent security patterns can shape how I build the agent tooling. Delivery and idempotency writeups can affect the messaging pipeline. Unity, asset pipeline, and game tooling posts can feed the game-dev work. Pricing, product, and solo-dev notes can change how I package the next experiment. A random model-routing benchmark might not matter today, then become exactly the missing context three months from now.&lt;/p&gt;

&lt;p&gt;Lore gives that material somewhere to compound.&lt;/p&gt;

&lt;p&gt;It is important to say what Lore is not. It is not HoneyDrunk governance. Governance lives in the architecture docs, ADRs, invariants, and repo contracts. Lore can inform a decision, but it does not make the decision official.&lt;/p&gt;

&lt;p&gt;It is also not agent memory. Agent memory is runtime state, preferences, session history, and working context. Lore is source-backed decision support. If Lore says something, it should be able to point at the sources, report confidence, and name the gaps. I do not want uncited wiki prose becoming doctrine just because an agent wrote it once.&lt;/p&gt;

&lt;p&gt;That boundary is the whole point. Lore is allowed to be useful because it is not allowed to be magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Feels Different
&lt;/h2&gt;

&lt;p&gt;The big unlock is that the wiki is a built artifact. It gets compiled, maintained, and carried forward the way code is.&lt;/p&gt;

&lt;p&gt;Search asks, "can I find the thing again?" Lore asks, "has the thing been digested into the shape of what I already know?" Those are different questions. One retrieves. The other compounds.&lt;/p&gt;

&lt;p&gt;The daily Discord blast made that visible. A source goes from a raw article or tweet into a reviewed daily signal. The signal keeps the original URL attached. The summary is short enough to read on a phone. The HoneyDrunk angle makes the relevance explicit. Later, if the item is durable, compile can fold it into the wiki. If it is weak, lint can let it decay. If a better source appears, the older claim can be superseded instead of silently overwritten.&lt;/p&gt;

&lt;p&gt;That is the kind of system I want around a long-running workshop. Something that runs on its own rhythm, keeps what I have already learned, and stays warm between sessions.&lt;/p&gt;

&lt;p&gt;A maintained wiki. A daily signal review. Source URLs attached. Confidence kept visible. Gaps named instead of hidden.&lt;/p&gt;

&lt;p&gt;It is still rough in places, because every useful personal system starts rough. But it already changes the feel of reading. Sources do not just pass through my attention anymore. They have a place to land, a way to be compiled, and a chance to become part of the workshop's long memory without pretending to be governance or truth.&lt;/p&gt;

&lt;p&gt;That is the version of the LLM wiki pattern I needed: a source-backed research surface for a studio I plan to keep building in for decades.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>honeydrunk</category>
      <category>llm</category>
    </item>
    <item>
      <title>Two Terminals, One Screen: Building an Agent Cockpit That Tells the Truth About Cost</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 14 Jun 2026 15:06:56 +0000</pubDate>
      <link>https://dev.to/tatted_dev/two-terminals-one-screen-building-an-agent-cockpit-that-tells-the-truth-about-cost-m56</link>
      <guid>https://dev.to/tatted_dev/two-terminals-one-screen-building-an-agent-cockpit-that-tells-the-truth-about-cost-m56</guid>
      <description>&lt;p&gt;The setup that pushed me to build this was embarrassingly low-tech. Two terminal windows, side by side. One running the Claude Code CLI on a refactor. One running Codex on a different repo. Two live coding agents, two different repos, two different ways of telling me what just happened.&lt;/p&gt;

&lt;p&gt;Each window has its own dialect for reporting a turn. Claude prints a tidy result line with tokens and a dollar figure. Codex streams whole messages and reports tokens but no money. So at any given moment I had a pair of agents running and no single place that could answer the one question I actually cared about: how much have I spent today, and on what.&lt;/p&gt;

&lt;p&gt;I kept Alt-Tabbing between windows trying to add that up in my head. That is a stupid way to run a studio. So I built a cockpit.&lt;/p&gt;

&lt;p&gt;This post is about that cockpit, which I call HoneyHub, and specifically about the part that was much harder than the windowing: making a single cost view that pulls from two tools which measure cost in completely different units, without lying to me about either of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What HoneyHub Is
&lt;/h2&gt;

&lt;p&gt;HoneyHub is a local control panel for AI coding agents. It runs on my own machine, serves a small web app I open in a browser (phone or desktop), and drives the official command-line tools I already pay for: the Claude Code CLI and the Codex CLI. Nothing gets proxied through a server I rent. Nothing holds my subscription credentials. HoneyHub shells out to the same binaries I was running by hand, reads their output, and shows it on one screen.&lt;/p&gt;

&lt;p&gt;The core is a Rust crate (&lt;code&gt;bridge&lt;/code&gt;) that owns the child processes, and a TypeScript web app that renders the sessions. Between them is a contract: every backend, no matter how differently its CLI behaves, gets adapted into the same stream of events. A message event. A status event. A usage event. The web app never has to know which tool produced a given line.&lt;/p&gt;

&lt;p&gt;That contract is the whole trick, and the usage event is where it earns its keep.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Two Tools, Two Definitions of "Cost"
&lt;/h2&gt;

&lt;p&gt;Before I wrote a line of the cost view, I ran each CLI and read exactly what it hands back when a turn finishes. The two could not report cost more differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; is the generous one. When a turn completes, it emits a &lt;code&gt;result&lt;/code&gt; line carrying exact input and output token counts and a real dollar figure (&lt;code&gt;total_cost_usd&lt;/code&gt;) computed on its side. I don't have to guess anything. I take the number it gives me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex&lt;/strong&gt; reports exact token counts but no money at all. It tells me a turn used 1,000 input tokens and 1,000 output tokens, and then it stops. If I want a dollar figure, I have to multiply those tokens by a rate I supply myself. The tokens are ground truth. The dollars are something I compute, and only if I've configured a rate.&lt;/p&gt;

&lt;p&gt;So even with just these two, I had two genuinely different kinds of number staring at me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A measured dollar figure that a vendor computed (Claude).&lt;/li&gt;
&lt;li&gt;A dollar figure I derive from exact tokens and a rate I supply (Codex).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not the same thing, and pretending they are is how a dashboard starts lying to you. The lazy move is to mash both into one "total spend" number and call it a day. I refused to, because the moment a derived estimate and a measured figure get added together you can no longer tell which dollars are real. The whole reason I wanted a cockpit was to &lt;em&gt;trust&lt;/em&gt; the readout. A blurred total is worse than no total.&lt;/p&gt;

&lt;p&gt;So the design rule became: the system has to know, for every cost figure it shows, how that figure came to exist. And it has to refuse to blend a measurement with a guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Fidelity Is a First-Class Field
&lt;/h2&gt;

&lt;p&gt;Inside the bridge, every usage report carries a &lt;code&gt;fidelity&lt;/code&gt; tag. There are exactly three values, ranked from most trustworthy to least, and the order matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/session.rs&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq,&lt;/span&gt; &lt;span class="nd"&gt;PartialOrd,&lt;/span&gt; &lt;span class="nd"&gt;Ord,&lt;/span&gt; &lt;span class="nd"&gt;Hash,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"snake_case"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;UsageFidelity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Exact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Derived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Estimated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Exact&lt;/code&gt; means the CLI handed me the dollars directly. &lt;code&gt;Derived&lt;/code&gt; means I computed dollars from exact tokens and a rate table. &lt;code&gt;Estimated&lt;/code&gt; means the numbers are a proxy and should never be read as money. Today's two backends use only the first two tiers: Claude reports &lt;code&gt;Exact&lt;/code&gt;, Codex reports &lt;code&gt;Derived&lt;/code&gt;. &lt;code&gt;Estimated&lt;/code&gt; is built in and waiting for the kind of backend that can only guess at what it spent, a tool whose CLI hands back neither a dollar figure nor a real token count. I have no such backend wired in right now, but the tier exists so that when one shows up, its guesses can never quietly pass as measurements. Every adapter is required to tag its usage signal honestly, and each one does it differently because each tool is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude: take the number, don't touch it
&lt;/h3&gt;

&lt;p&gt;The Claude adapter reads the &lt;code&gt;result&lt;/code&gt; line and copies the dollar figure straight through. No arithmetic. The only cleverness here is folding cache-read and cache-creation tokens into the total so an "exact" signal never under-reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/adapters/claude_local.rs&lt;/span&gt;
&lt;span class="n"&gt;UsageSignal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;AgentBackend&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ClaudeLocal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Claude Code reports exact tokens + USD; taken directly, no computation.&lt;/span&gt;
    &lt;span class="c1"&gt;// USD is never derived from a rate table for this backend.&lt;/span&gt;
    &lt;span class="n"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;UsageFidelity&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Exact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;as_f64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;UsageConfidence&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Codex: exact tokens, dollars only if I asked for them
&lt;/h3&gt;

&lt;p&gt;The Codex adapter has the exact tokens but has to derive the dollars. The rate lookup is &lt;em&gt;injected&lt;/em&gt; into the adapter rather than hardcoded, so the bridge crate never bakes a vendor's prices into itself. And here is the part I care about most: if no rate is configured, the tokens stay exact and the dollar figure is simply absent. It is never fabricated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/adapters/codex_local.rs&lt;/span&gt;
&lt;span class="c1"&gt;// Tokens are exact; USD is derived from the injected rate table.&lt;/span&gt;
&lt;span class="c1"&gt;// With no rate configured the cost is absent — never fabricated.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;total_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default rate lookup wired in when nothing is configured returns &lt;code&gt;None&lt;/code&gt; for everything, on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/adapters/codex_local.rs&lt;/span&gt;
&lt;span class="cd"&gt;/// The default lookup: no rate table wired, so USD is always absent (tokens exact).&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;no_rate_lookup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UsdRateLookup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_output&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So out of the box, Codex shows you exact tokens and an honest blank where the dollars would be. You opt into derived dollars by handing it a rate. That ordering is the whole point. When the system has no grounds for a number, it says "I don't know" and leaves the space empty. It never fills that space with a guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Centerpiece: The Two-Way Usage Table
&lt;/h2&gt;

&lt;p&gt;Here is the whole problem on one page. Same question, two tools, and what each one can actually tell you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Dollars&lt;/th&gt;
&lt;th&gt;Real billing unit&lt;/th&gt;
&lt;th&gt;Fidelity tag&lt;/th&gt;
&lt;th&gt;Shown as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;exact&lt;/td&gt;
&lt;td&gt;exact (&lt;code&gt;total_cost_usd&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;dollars&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$0.0123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;exact&lt;/td&gt;
&lt;td&gt;derived from a rate, or absent&lt;/td&gt;
&lt;td&gt;tokens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;derived&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;≈$0.0030&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "shown as" column is not cosmetic. The display layer formats every figure with a prefix that encodes its fidelity, so an estimate can never visually pass as a measurement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: packages/ui/src/usageFormat.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usdPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsageFidelity&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;estimated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;derived&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;≈$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain &lt;code&gt;$&lt;/code&gt; is a number a vendor measured. A &lt;code&gt;≈$&lt;/code&gt; is a number I computed from a rate. A &lt;code&gt;~$&lt;/code&gt; is a soft guess. You can tell at a glance which is which without reading a legend. And when a rollup has no dollar figure to show at all, the cell renders an em dash rather than a fabricated zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: packages/ui/src/routes/spend/spendModel.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rollupCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsageRollup&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;formatUsd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Headline Number Sums Only What It Can Measure
&lt;/h2&gt;

&lt;p&gt;The cockpit shows one big number at the top: today's spend. The temptation, the entire reason this was hard, is to make that number include everything. It doesn't. The grounded total sums only the dollar figures that are real, which is to say &lt;code&gt;exact&lt;/code&gt; and &lt;code&gt;derived&lt;/code&gt;. Anything tagged &lt;code&gt;estimated&lt;/code&gt; is excluded by construction, so the day a guessing backend gets wired in it still cannot touch the headline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: packages/ui/src/routes/spend/spendModel.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rollups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;derived&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anyGroundedUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groundedTotalUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anyGroundedUsd&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When nothing grounded has been recorded yet, the headline is &lt;code&gt;undefined&lt;/code&gt;, not &lt;code&gt;$0.00&lt;/code&gt;. That distinction is deliberate. There is a real difference between "you have spent zero dollars" and "I have no measured spend to report," and a tool that collapses the second into the first is lying to you about a number you might make decisions on. So the view says "no measured spend yet" rather than inventing a confident zero.&lt;/p&gt;

&lt;p&gt;The Rust side enforces the same rule, with a comment that is basically the thesis of the whole feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/session.rs&lt;/span&gt;
&lt;span class="cd"&gt;/// Sum of USD across **exact + derived** rollups only — a real dollar figure.&lt;/span&gt;
&lt;span class="cd"&gt;/// Estimated rollups are excluded so a guess can never inflate the headline&lt;/span&gt;
&lt;span class="cd"&gt;/// spend. `None` when no grounded signal reported USD.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementations, one in Rust on the host and one in TypeScript for the offline mock, and they agree on the rollup shape down to the ordering. That redundancy is on purpose too: the formatting honesty lives in tested code on both sides of the wire, so neither can quietly drift into showing an estimate as a fact.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The engineering lesson here is smaller than it looks and more useful than I expected: &lt;strong&gt;fidelity is data, not presentation.&lt;/strong&gt; My first instinct was to compute one blended dollar total and then sprinkle a "(estimated)" label on the UI somewhere. That fails the moment anything reads the number, because by then the measurement and the guess are already added together and you can't pull them back apart. Tagging every single usage signal at the source, the instant it leaves the adapter, is what makes the honest rollup even possible downstream. The guess and the measurement never touch.&lt;/p&gt;

&lt;p&gt;The product lesson is the one I keep coming back to. I built this so I would trust the readout, and a tool you can't trust is worse than the two terminal windows I started with, because at least the windows weren't pretending. A dashboard that confidently sums a measurement and a wild guess into one number trains you to distrust it, and then you go back to adding it up in your head anyway. The blank where Codex's dollars would be, and the "no measured spend yet" instead of a fake zero, are the features. They are the reason I look at the number and believe it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The cost view is one panel. The cockpit also surfaces live sessions, per-session diagnostics, and cross-session coaching, and the same fidelity discipline runs through all of it: a capability either exists for a backend or it doesn't, and the UI declares which honestly rather than faking a uniform experience. Codex can't do a live mid-turn reply, so the cockpit doesn't pretend it can; it routes the follow-up through a fresh resumed run instead. That honesty-by-construction posture is the through-line of the whole thing.&lt;/p&gt;

&lt;p&gt;What's left is the part no amount of code can do for me: running it for a real day of work and seeing whether the number at the top actually changes how I spend. That's the test that matters.&lt;/p&gt;

&lt;p&gt;If a tool is going to tell you what something cost, it owes you the truth about how sure it is. A blank is more honest than a guess wearing a dollar sign.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tooling</category>
      <category>honeydrunk</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop Receiving Webhooks, Start Polling: Rebuilding the Grid Review Runner</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:44:28 +0000</pubDate>
      <link>https://dev.to/tatted_dev/stop-receiving-webhooks-start-polling-rebuilding-the-grid-review-runner-3ofp</link>
      <guid>https://dev.to/tatted_dev/stop-receiving-webhooks-start-polling-rebuilding-the-grid-review-runner-3ofp</guid>
      <description>&lt;p&gt;Every pull request I open gets an automated code review from an AI agent before I merge it. It reads the diff, checks it against my project's conventions, and posts its findings as a comment, like a second set of eyes that never sleeps. I run a whole studio's worth of repos solo, so that reviewer is load-bearing infrastructure. I call it the Grid Review Runner.&lt;/p&gt;

&lt;p&gt;One weekend it went quiet, and I didn't notice for two days.&lt;/p&gt;

&lt;p&gt;A pull request sat in one of my repos with no review on it. I was on the road. When I finally opened my laptop, I had no idea whether the reviewer had looked at the PR and decided it was fine, or whether it had never been told the PR existed in the first place. There was no log to check. There was no signal either way. The review just... wasn't there.&lt;/p&gt;

&lt;p&gt;That ambiguity is the whole story. The way the system worked back then, GitHub was supposed to &lt;em&gt;push&lt;/em&gt; a notification to my reviewer the instant a PR opened (a webhook, in the usual web plumbing sense). If that push went missing, the reviewer never woke up, and nothing told me it had been skipped. Did the reviewer decline, or did the notification never fire? I couldn't tell. The runner worked beautifully when I was sitting at my desk and fell apart the moment I wasn't. So I rebuilt the transport from the ground up. This post is about that rebuild, and the one architectural decision underneath it: stop waiting to be told about work, and go looking for it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Shape: Inbound Webhook Over a Tunnel
&lt;/h2&gt;

&lt;p&gt;The Grid Review Runner is the piece of the HoneyDrunk Grid that runs an AI code review on pull requests. The review logic was never the problem. The rubric, the context-loading contract, the advisory posture all worked. The problem was the rail it rode in on.&lt;/p&gt;

&lt;p&gt;The original design (ADR-0044) looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub PR event
  → GitHub Action fires an HMAC-signed webhook
    → GitHub pushes it over a Cloudflare Tunnel
      → a local webhook bridge on my home server
        → which invokes OpenClaw/Codex to run the review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every arrow there is a thing that has to be up at the exact moment a PR event happens. OpenClaw was the execution runtime. The Cloudflare Tunnel was its inbound rail. The home server (ADR-0081) was the always-on box that hosted both. On paper it's clean: GitHub talks to a narrow, signed, authenticated endpoint, and the endpoint does the work.&lt;/p&gt;

&lt;p&gt;In practice it was three single points of failure stacked on top of each other, and all three were coupled to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Failed
&lt;/h2&gt;

&lt;p&gt;Three failure modes compounded over the weeks after it landed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhook delivery flaked.&lt;/strong&gt; GitHub redelivery is best-effort. On long PR sessions the bridge missed &lt;code&gt;synchronize&lt;/code&gt; events, and I had no inexpensive way to tell whether a missing review meant "the runner declined" or "the event never arrived."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunnel uptime was coupled to me.&lt;/strong&gt; The Cloudflare Tunnel ran on the home server, and it was the single inbound path. When I traveled, when the box rebooted, when the tunnel daemon hiccuped, the rail was down. No rail, no review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw process stability wasn't set-and-forget.&lt;/strong&gt; Crashes, session-credential expiry, dashboard-coupled state. "Did the review run?" was a non-trivial question to answer, every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The symptoms were all different. They had one root cause: an always-on inbound HTTP receiver on operator infrastructure does not fit an operator whose laptop travels.&lt;/p&gt;

&lt;p&gt;That shape works for a team with an on-call rotation absorbing the misses. It is mis-sized for one person and a single low-power box at home. I could have kept patching it (better redelivery handling, a hardier tunnel daemon, hardening OpenClaw against crashes), but every one of those is repair work on a house that doesn't fit the lot. The failure modes are intrinsic to the inbound-webhook shape on a solo setup. No tactical fix removes the category.&lt;/p&gt;

&lt;p&gt;So I stopped trying to fix the rail and changed the direction it ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pivot: Push Becomes Pull
&lt;/h2&gt;

&lt;p&gt;Here is the load-bearing decision, and everything else is downstream of it: invert the transport. Instead of GitHub pushing a trigger into my infrastructure, my worker reaches out and pulls the trigger inward. The queue it pulls from is GitHub itself, so there's no server of mine that has to stay alive to hold it.&lt;/p&gt;

&lt;p&gt;The new critical path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub PR event
  → cheap GitHub Action (no LLM work)
    → enqueue: add `needs-agent-review` label + structured queue comment
      → local worker polls GitHub on a 60s tick
        → claims one PR, runs the review locally under Codex CLI + Claude Code CLI
          → posts the verdict, swaps the label to its final state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No inbound webhook. No tunnel for review traffic. No OpenClaw on the path at all.&lt;/p&gt;

&lt;p&gt;The GitHub Action got dumber on purpose. It does exactly two API calls and exits: it normalizes the PR's managed labels, adds &lt;code&gt;needs-agent-review&lt;/code&gt;, and upserts a single structured queue comment carrying the repo, PR number, head SHA, author class, and the resolved review config. It invokes no LLM. Its entire cost is the GitHub Actions minute floor: a label write, a comment write, done. No tokens are spent in the cloud, by design.&lt;/p&gt;

&lt;p&gt;The label is the queue index. The comment is the audit trail and the metadata carrier. That's the trick that makes the whole thing durable: the queue lives in GitHub. When my worker is offline (traveling, home server rebooting, machine powered off) PRs just accumulate in the &lt;code&gt;needs-agent-review&lt;/code&gt; state and wait. Nothing is lost, because there's no local queue to lose. When the machine comes back, the next tick reads GitHub's state and resumes. A powered-off worker just pauses the poll, and the events are still sitting in GitHub when it wakes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worker (and Why the Scheduler Is Boring on Purpose)
&lt;/h2&gt;

&lt;p&gt;The worker is a PowerShell script invoked by Windows Task Scheduler on the same home-server hardware that used to host OpenClaw. The mini-PC survived the rebuild. Only OpenClaw, the webhook bridge, and the tunnel hostname came off it.&lt;/p&gt;

&lt;p&gt;One thing is easy to misread here, so I'll say it plainly: the headline is push → pull, and Task Scheduler is just the boring scheduler adapter sitting on top of that decision. The scheduler could be cron, systemd, or a cloud VM tomorrow without changing a single job spec. PowerShell + Task Scheduler is simply the lowest-friction option on a Windows box that's already on: no compile step, no runtime to deploy, sub-second per-tick boot. I deliberately wanted boring plumbing here.&lt;/p&gt;

&lt;p&gt;The Task Scheduler entry starts at logon and startup, repeats on a 60-second interval, restarts on failure, and refuses to overlap runs. The claim protocol uses GitHub's own primitives as the atomic operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List&lt;/strong&gt; every PR carrying &lt;code&gt;needs-agent-review&lt;/code&gt; (one search call per tick, cheap at solo-dev volume).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claim&lt;/strong&gt; the oldest one by swapping the label &lt;code&gt;needs-agent-review&lt;/code&gt; → &lt;code&gt;agent-review-in-progress&lt;/code&gt; and recording &lt;code&gt;claimed_by&lt;/code&gt; and &lt;code&gt;head_sha&lt;/code&gt; in the queue comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;/strong&gt; the canonical &lt;code&gt;.claude/agents/review.md&lt;/code&gt; agent locally under subscription-auth CLIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post&lt;/strong&gt; the verdict and swap to &lt;code&gt;agent-reviewed&lt;/code&gt; (clean) or &lt;code&gt;changes-requested-by-agent&lt;/code&gt; (findings).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the worker crashes mid-review, a &lt;strong&gt;stale-claim sweep&lt;/strong&gt; runs at the top of each tick and releases any &lt;code&gt;agent-review-in-progress&lt;/code&gt; claim older than ~15 minutes back to the queue. If a new commit lands while a review is in flight, the head SHA in the comment no longer matches the claim, the now-stale verdict is discarded, and the next tick re-reviews against the new head. The marginal cost of that wasted run is operator-machine CPU only. The review runs under my existing subscription CLIs, so the LLM cost is $0.&lt;/p&gt;

&lt;p&gt;The job spec itself is just data. Here's the relevant slice of the review job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# filepath: infrastructure/workers/grid-agent-runner/config/jobs/grid-review.psd1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;JobId&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grid-review"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;TriggerKind&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"label-queue"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;Schedule&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"interval"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IntervalSeconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AtStartup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AtLogon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;PromptPath&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".claude/agents/review.md"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;AgentCommands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Executable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;PromptStdin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Executable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PromptStdin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RiskClasses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Optional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;WriteMode&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comment-only"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker holds no secret-bearing inbound port. Its trust boundary collapsed to GitHub auth plus the local filesystem. There's no public ingress for review traffic to attack anymore, because there's no public ingress at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The most useful thing this rebuild taught me: the discipline was right the whole time. The shape it rode on was the part that was broken.&lt;/p&gt;

&lt;p&gt;Everything that made the reviewer good came through untouched: the context-loading contract, the review rubric, the advisory posture (a missing review never blocks a merge). All of that is a property of the agent prompt, and the prompt didn't change. The transport only ever carried diffs. So I got to rip out an entire failure-prone rail and the actual review behavior survived intact. When you can cleanly separate "what hurts" from "what works," a scary-sounding rebuild turns out to be a narrow one.&lt;/p&gt;

&lt;p&gt;The second thing: inverting the transport let me delete infrastructure instead of adding it. OpenClaw, the webhook bridge, and the tunnel hostname all came off the home server (ADR-0088 finished that teardown). The HMAC webhook-signing secret got retired instead of perpetually rotated. The box itself stayed. The hardware was always fine. What was dead was the OpenClaw-centric way of organizing everything on top of it.&lt;/p&gt;

&lt;p&gt;And a bonus I didn't fully plan for: once the runner stopped being a one-off review script and became a job framework with a scheduler adapter, every other scheduled agent job I'd been parking on OpenClaw came home to it too. &lt;code&gt;hive-sync&lt;/code&gt;, the Lore sourcing and ingest passes, all of it. One substrate, one scheduler story, one recovery story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The worker runs two model families on every high-risk review: Codex CLI under one subscription, Claude Code CLI under another, synthesized into a single verdict. Why the Grid runs that many review layers (this runner plus CodeRabbit, Copilot, the Actions gates, and SonarQube) is a whole post on its own, and it's the next one I'm writing. This post was only ever about the transport.&lt;/p&gt;

&lt;p&gt;The lesson I'm keeping: when an always-on inbound receiver keeps failing for a solo operator, a more reliable receiver is rarely the fix. Stop receiving. Reach out and pull instead, and let the queue live somewhere that's already always on.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>devops</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Treating Azure Infrastructure Like a Pull Request: Bicep and what-if</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:42:51 +0000</pubDate>
      <link>https://dev.to/tatted_dev/treating-azure-infrastructure-like-a-pull-request-bicep-and-what-if-1nme</link>
      <guid>https://dev.to/tatted_dev/treating-azure-infrastructure-like-a-pull-request-bicep-and-what-if-1nme</guid>
      <description>&lt;p&gt;For a long time, the complete and authoritative definition of my production infrastructure was a scratch file of &lt;code&gt;az&lt;/code&gt; commands and the order I remembered running them in. I'd click through the Azure Portal, paste in a few commands, and the service would come up: a container app, a Key Vault, some App Configuration, a Service Bus namespace, the role assignments that wire them together. It worked.&lt;/p&gt;

&lt;p&gt;It worked because I remembered the sequence. There was an order to it, a set of steps that mattered, and the only place that order lived was in my head. That holds up fine when there's one environment and you set it up once. It falls apart the moment you need a second one. I run a lot of these services solo, so "I'll just remember it" had quietly become the load-bearing plan across every repo I own.&lt;/p&gt;

&lt;p&gt;This post is about the moment I needed a second environment, watched my from-memory process fall apart on contact, and decided to stop provisioning from memory entirely. The fix has a thesis I'll keep coming back to: infrastructure should be a diff you review before it lands, the same way code is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: I Provisioned From Memory
&lt;/h2&gt;

&lt;p&gt;Here's what "provisioning from memory" actually looked like in practice.&lt;/p&gt;

&lt;p&gt;My dev environment apps started life as hello-world placeholders. I'd create a container app in the Portal, get something trivial running, and then evolve it into the real service over time. Every time I did that, the real service needed things the placeholder didn't, and I added them by hand, one at a time, as I noticed they were missing.&lt;/p&gt;

&lt;p&gt;The list of things I noticed missing, the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app's managed identity had no &lt;strong&gt;AcrPull&lt;/strong&gt; role, so it couldn't pull its own container image from the registry.&lt;/li&gt;
&lt;li&gt;It had no &lt;strong&gt;Key Vault&lt;/strong&gt; or &lt;strong&gt;App Configuration&lt;/strong&gt; read access, so it couldn't load any of its config or secrets.&lt;/li&gt;
&lt;li&gt;It had never actually &lt;strong&gt;joined the shared Container Apps environment&lt;/strong&gt;, so it wasn't wired into the logging and networking I assumed it was.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;App Configuration store was never seeded&lt;/strong&gt; with the keys the app expected to find.&lt;/li&gt;
&lt;li&gt;Ingress was configured for &lt;strong&gt;port 80&lt;/strong&gt; with health probes pointed there, while the app actually listens on &lt;strong&gt;8080&lt;/strong&gt;. So it was "running" and failing every health check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those was fixed live, by hand, while the thing was already deployed and broken. And the role assignments in particular fought me: the obvious &lt;code&gt;az role assignment create&lt;/code&gt; path kept throwing a &lt;code&gt;MissingSubscription&lt;/code&gt; error in my setup, so I ended up making the RBAC grants through raw &lt;code&gt;az rest&lt;/code&gt; calls against the management API instead. Each fix was a small thing. The sum of them was a service that only worked because I'd personally walked it through a recovery I never wrote down.&lt;/p&gt;

&lt;p&gt;The real failure was bigger than any single missing role. The complete, correct sequence for standing up one of my services lived only in my own hands, a thing I had done and could repeat but could never open and read. There was no artifact. There was no review. If someone asked me "what does it take to bring this service up in a fresh region for disaster recovery," the honest answer was "give me a day and let me remember."&lt;/p&gt;

&lt;p&gt;So when a second environment showed up on the roadmap, I didn't want to redo the from-memory walk. I wanted the walk to be a file.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Bicep Is (and What It Buys a Solo Dev)
&lt;/h2&gt;

&lt;p&gt;The tool I committed to is Bicep. If you haven't used it: Bicep is Microsoft's own infrastructure-as-code language for Azure. You describe the resources you want in a terse, typed DSL, and it compiles down to ARM, the JSON template format Azure deploys natively. There's no separate state file to manage the way Terraform has one; a deployment reconciles directly against whatever already exists in Azure. For an Azure-only shop run by one person, that's one fewer artifact to secure and back up, which I appreciated.&lt;/p&gt;

&lt;p&gt;I put all of it in one repo. Reusable modules are organized by concern (compute, data, secrets, messaging, observability) and a composition template stitches them together per environment. The modules are referenced by plain local file paths. I looked at publishing them to a Bicep registry and decided against it; for a single operator, a registry was overhead with no payoff, so modules just sit next to the templates that consume them and resolve off the filesystem at build time.&lt;/p&gt;

&lt;p&gt;The shared foundation for an environment composes the pieces that no single service owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// filepath: platform/main.bicep
module containerAppEnvironment '../modules/compute/containerAppEnvironment.bicep' = {
  name: 'platform-cae'
  params: {
    env: env
    location: location
    tags: tags
    logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id
  }
}

module appConfigurationStore '../modules/secrets/appConfigurationStore.bicep' = {
  name: 'platform-appcs'
  params: {
    env: env
    location: location
    tags: tags
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is what's now impossible to forget. Remember the port-80-versus-8080 mess? Here's the relevant slice of the container app module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// filepath: modules/compute/containerApp.bicep
@description('Ingress target port the container listens on.')
param targetPort int = 8080

resource containerApp 'Microsoft.App/containerApps@2025-07-01' = {
  name: 'ca-hd-${service}-${env}'
  identity: { type: 'SystemAssigned' }
  properties: {
    managedEnvironmentId: containerAppEnvironmentId
    configuration: {
      ingress: {
        external: externalIngress
        targetPort: targetPort
        transport: 'auto'
      }
    }
    // ...
  }
}

@description('Principal ID of the system-assigned managed identity. Grant AcrPull / Key Vault / App Configuration RBAC to this.')
output principalId string = containerApp.identity.principalId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The port default is 8080, written down once, applied everywhere. The module won't deploy a container app without joining the managed environment, because &lt;code&gt;managedEnvironmentId&lt;/code&gt; is a required parameter. The managed identity's principal ID comes out as an output specifically so the role assignments (the AcrPull, the Key Vault read, the App Configuration read) get wired to it in the same deploy instead of being remembered later. Every individual thing that bit me by hand is now a property of a file that gets compiled and checked.&lt;/p&gt;

&lt;p&gt;That's the difference between memory and code: the file can't quietly skip a step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infra as a Diff You Review Before It Lands
&lt;/h2&gt;

&lt;p&gt;Having the resources in Bicep is half of it. The half I actually care about is what happens when I change them.&lt;/p&gt;

&lt;p&gt;Azure has a command called &lt;code&gt;what-if&lt;/code&gt;. It's a dry run: you point it at a template and it tells you exactly what a real deployment &lt;em&gt;would&lt;/em&gt; do (what it would create, what it would modify, what it would delete) without touching anything. It prints a diff of your infrastructure.&lt;/p&gt;

&lt;p&gt;My deploy pipeline is built around that command. Before it applies anything, it runs the same three preflight steps every time: compile the Bicep, lint it, then run &lt;code&gt;what-if&lt;/code&gt;. Only after that does it consider applying. And whether it applies at all is a switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filepath: .github/workflows/job-deploy-bicep.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;What-if preflight&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;az deployment group what-if \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group "$RG" \&lt;/span&gt;
      &lt;span class="s"&gt;--parameters "$PARAMS"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply deployment&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.what-if-only != &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt; }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;az deployment group create \&lt;/span&gt;
      &lt;span class="s"&gt;--name "hd-infra-${ENV}-${RUN_ID}-${RUN_ATTEMPT}" \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group "$RG" \&lt;/span&gt;
      &lt;span class="s"&gt;--parameters "$PARAMS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I trigger deploys in two modes. &lt;strong&gt;Plan&lt;/strong&gt; runs the build, lint, and &lt;code&gt;what-if&lt;/code&gt;, then stops and tells me nothing was applied. &lt;strong&gt;Apply&lt;/strong&gt; does all of that and then actually deploys. The default is plan. So my normal loop is: run plan, read the diff, confirm it's doing what I expect, then re-run as apply.&lt;/p&gt;

&lt;p&gt;Reading the diff is the whole point. The discipline I hold myself to is simple: every resource that should already exist needs to show up as &lt;strong&gt;no change&lt;/strong&gt;. If something I wasn't expecting shows as Modify or, worse, Delete, that's the signal to stop and look before anything happens to live infrastructure. The dry run turns "I think this is safe" into "I can see that this is safe."&lt;/p&gt;

&lt;p&gt;That's why I call it treating infrastructure like a pull request. A pull request is a diff you read before you merge it. &lt;code&gt;what-if&lt;/code&gt; is a diff you read before you deploy it. Same loop, same safety, just pointed at Azure resources instead of source files. The change stops being an action I take from memory and becomes a proposal I review.&lt;/p&gt;

&lt;p&gt;Two supporting pieces, each worth only a sentence here because each could be its own post. The pipeline authenticates to Azure with GitHub's OIDC federation, so there are no long-lived Azure credentials stored anywhere; the deploy identity is allowed to provision resources but not to read secret values. And promotion across environments is gated: dev is easy, while staging and prod sit behind GitHub Environment approvals so nothing reaches them without a deliberate sign-off. Both of those deserve their own write-ups later.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;The real fix was turning that memory into an artifact, something I could read, review, and re-run. Trying to remember harder was never going to survive past one environment.&lt;/p&gt;

&lt;p&gt;Once the infrastructure was a set of files, the part that used to terrify me (standing up another environment) became boring in the best way. A new environment is a new parameter file pointed at the same modules. The sequence I used to carry in my head is now enforced by required parameters and module composition. The container app can't come up without joining its environment. The identity comes out as an output so its roles get granted in the same breath. The port is 8080 because the file says so.&lt;/p&gt;

&lt;p&gt;And the dry run changed how changes &lt;em&gt;feel&lt;/em&gt;. Before, every &lt;code&gt;az&lt;/code&gt; command against a real environment was a small leap of faith. Now I get to look first. The worst-case surprise moved from "I deleted something in production" to "the plan showed something I didn't expect, so I didn't run apply." That's a much better worst case to live with when there's no one else around to catch the mistake.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Left to Codify
&lt;/h2&gt;

&lt;p&gt;There's more to build on top of this. The role assignments and App Configuration seeding that I described as the painful hand-fixes are exactly the parts I want fully codified into the per-service templates next, so that "fresh environment" means one plan-and-apply with nothing left to remember. And the environment-gated promotion model deserves its own post, because the approval gates are doing real work.&lt;/p&gt;

&lt;p&gt;But the load-bearing change already happened. My infrastructure stopped being a thing I remember how to do and became a thing I can read. Provision from a file, not from memory. Look at the diff before it lands.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azure</category>
      <category>architecture</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>A Field Guide to Hand-Provisioning Azure Functions and Container Apps</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:41:06 +0000</pubDate>
      <link>https://dev.to/tatted_dev/a-field-guide-to-hand-provisioning-azure-functions-and-container-apps-3n4</link>
      <guid>https://dev.to/tatted_dev/a-field-guide-to-hand-provisioning-azure-functions-and-container-apps-3n4</guid>
      <description>&lt;p&gt;I asked the Azure CLI for my function app's hostname and it handed me back &lt;code&gt;null&lt;/code&gt;. The app was running. The hostname existed. The tool whose entire job is to tell me about the app just refused to say what it was. That was the first of a week's worth of moments where the platform looked me in the eye and lied.&lt;/p&gt;

&lt;p&gt;I'd been standing up cloud services on Azure by hand, one small repo at a time, some running as Azure Functions and some as Container Apps. I do this solo, so each one starts as a hello-world placeholder, gets something trivial running, then grows into the real service as I add whatever it turns out to need. The work itself was fine. The part that cost me evenings was Azure's own tooling handing me wrong answers while I did it. I'd ask the CLI a direct question and get back &lt;code&gt;null&lt;/code&gt;. I'd hit the hostname the docs implied and get a connection that failed completely. I'd reach for the official deploy action and find it blocked. Every one of these burned an evening of squinting at output that made no sense, usually around midnight, usually while googling the exact error and finding nothing.&lt;/p&gt;

&lt;p&gt;This is the post I wish I'd found on one of those nights. Five concrete gotchas, each with the symptom I actually saw and the fix that actually worked. If you're hand-standing-up Functions or Container Apps on Azure and the platform is gaslighting you, start here.&lt;/p&gt;

&lt;p&gt;A bit of grounding first, because this blog gets read by people who don't know my stack. &lt;strong&gt;Flex Consumption&lt;/strong&gt; is a newer Azure Functions hosting plan, the pay-for-what-you-use serverless tier with faster scaling than the old Consumption plan. &lt;strong&gt;Container Apps&lt;/strong&gt; is Azure's managed platform for running containers without standing up Kubernetes yourself. A &lt;strong&gt;managed identity&lt;/strong&gt; is an Azure-issued identity attached to your app so it can authenticate to other Azure services without you storing a password. An &lt;strong&gt;RBAC role assignment&lt;/strong&gt; is the grant that says "this identity is allowed to do this specific thing." Those four show up in every story below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 1: &lt;code&gt;az functionapp show&lt;/code&gt; returns null on Flex Consumption
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; I deployed a Function on the Flex Consumption plan and went to grab its basic facts the obvious way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az functionapp show &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;app&amp;gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"{host:defaultHostName, state:state, names:hostNames}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back came &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;defaultHostName&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;hostNames&lt;/code&gt;. Not an error. Not an empty string. Just null fields on an app I could see existed in the Portal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; On Flex Consumption those properties simply aren't populated on the object &lt;code&gt;az functionapp show&lt;/code&gt; reads. The command works fine elsewhere. On this plan it just doesn't surface those fields through that path, so you ask a perfectly reasonable question and the tool answers with a confident nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Go around it. Read the resource directly through the generic resource commands or the ARM REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generic resource read returns the populated properties&lt;/span&gt;
az resource show &lt;span class="nt"&gt;--ids&lt;/span&gt; &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-app-resource-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"properties.defaultHostName"&lt;/span&gt;

&lt;span class="c"&gt;# Or hit ARM directly&lt;/span&gt;
az rest &lt;span class="nt"&gt;--method&lt;/span&gt; get &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;function-app-resource-id&amp;gt;?api-version=&amp;lt;api-version&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;az resource show&lt;/code&gt; and &lt;code&gt;az rest&lt;/code&gt; read the underlying resource record, where the real values live. The lesson I took from this: when a high-level &lt;code&gt;az&lt;/code&gt; command hands you null on a newer plan, drop down a layer before you assume your app is broken. The app was fine. The convenience command just couldn't see it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 2: the hostname you expect returns HTTP 000
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; Once I had a hostname, I tried to hit the app at the address you'd guess from years of Azure muscle memory: &lt;code&gt;&amp;lt;app-name&amp;gt;.azurewebsites.net&lt;/code&gt;. The request didn't return a 404. It didn't return a 503. It returned HTTP 000, which is curl's way of telling you the connection never completed at all. The host wasn't answering on that name, period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; Flex Consumption apps answer only on a regional hostname shaped like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;app-name&amp;gt;-&amp;lt;hash&amp;gt;.&amp;lt;region&amp;gt;-01.azurewebsites.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bare &lt;code&gt;&amp;lt;app-name&amp;gt;.azurewebsites.net&lt;/code&gt; you'd expect, the one every older tutorial uses, just isn't a live endpoint for these apps. So you're hitting an address that resolves to nothing useful and getting a dead connection for your trouble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Use the regional hostname, and get it from the resource itself rather than constructing it by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az resource show &lt;span class="nt"&gt;--ids&lt;/span&gt; &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-app-resource-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"properties.defaultHostName"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is nasty because it compounds with Gotcha 1. The tool that should hand you the correct hostname returns null, so you fall back to guessing, and the address you guess refuses the connection. Two failures stacked: the right answer is hidden, and the wrong answer fails silently. Once I knew the regional shape was the only one that answers, the dead connections stopped being a mystery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 3: the official deploy GitHub Action was blocked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; I was deploying my Functions from GitHub Actions using &lt;code&gt;azure/functions-action&lt;/code&gt;, the official Microsoft-published action for the job. At some point it became unusable in my setup for terms-of-service reasons. The action I'd built my deploy step around was off the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; This one came down to a licensing and terms constraint on using that action in my context. Doesn't matter how clean your workflow is if the step it depends on is contractually off the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Drop the action and deploy with the &lt;code&gt;az&lt;/code&gt; CLI directly inside the workflow. The CLI does the same job without the dependency on a third-party action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filepath: .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy function app&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;az functionapp deployment source config-zip \&lt;/span&gt;
      &lt;span class="s"&gt;--name "$APP_NAME" \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group "$RG" \&lt;/span&gt;
      &lt;span class="s"&gt;--src "$ZIP_PATH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made this change in my Actions repo and never looked back (the commit that dropped the action lives in my history as &lt;code&gt;17b7362&lt;/code&gt;). The broader takeaway: a deploy step that leans on a single vendor action is a step that can disappear out from under you. The &lt;code&gt;az&lt;/code&gt; CLI is the stable substrate. When an action gets blocked or deprecated, the CLI underneath it almost always still does the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 4: Container Apps probes defaulted to port 80, my app listens on 8080
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; A hand-provisioned Container App came up and looked dead. Ingress was reachable in name but every health probe failed, so the platform kept treating the revision as unhealthy and cycling it. The container itself was running fine. The platform just couldn't get a healthy response out of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; The app came up with ingress and health probes pointed at port 80. My container listens on 8080. So every probe knocked on a door nobody was behind, marked the app unhealthy, and the app looked broken while being completely fine internally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Set the ingress target port and the probe ports to where the app actually listens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp ingress update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;app&amp;gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And make sure any liveness and readiness probes point at 8080 too, not the default. This is a small fix that wastes a lot of time, because "running but failing every health check" presents exactly like a crashed app from the outside. The first thing I check now on any new Container App is whether the probe port and the listen port agree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 5: role assignments threw MissingSubscription, so I used az rest
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; A freshly hand-made app has none of the permissions it needs. I went to grant its managed identity the roles it required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AcrPull&lt;/strong&gt;, so it can pull its own container image from the registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Vault&lt;/strong&gt; access, so it can read its secrets (Key Vault is Azure's managed secret store).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Configuration Data Reader&lt;/strong&gt;, so it can read its config (App Configuration is Azure's managed settings store).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The obvious command for each grant is &lt;code&gt;az role assignment create&lt;/code&gt;. In my environment it threw a &lt;code&gt;MissingSubscription&lt;/code&gt; error and refused to make the assignment. The subscription context was set, other commands worked, but this specific path insisted it couldn't find a subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; I never got a clean root cause for the &lt;code&gt;MissingSubscription&lt;/code&gt; failure on that command in my setup. What I could prove was that the same role assignment went through fine when I made it against the ARM REST API directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Create the role assignments through &lt;code&gt;az rest&lt;/code&gt;, calling the management API yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az rest &lt;span class="nt"&gt;--method&lt;/span&gt; put &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;scope&amp;gt;/providers/Microsoft.Authorization/roleAssignments/&amp;lt;guid&amp;gt;?api-version=&amp;lt;api-version&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s1"&gt;'{
    "properties": {
      "roleDefinitionId": "&amp;lt;role-definition-id&amp;gt;",
      "principalId": "&amp;lt;app-managed-identity-principal-id&amp;gt;",
      "principalType": "ServicePrincipal"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's more verbose, you build the request body by hand, but it works where the convenience command wouldn't.&lt;/p&gt;

&lt;p&gt;The deeper problem here is the shape of the whole situation, well beyond the one error. A freshly hand-made app is missing every one of these by default: AcrPull, Key Vault access, App Configuration Data Reader, joining the managed environment, and having its App Configuration store seeded with the keys it expects. Nothing tells you any of them are missing. The app doesn't announce "I have no permission to pull my image." It just fails quietly, one missing grant at a time, and you discover each one by watching the app fail in a slightly new way and reasoning backward to what it must have needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Took From All of It
&lt;/h2&gt;

&lt;p&gt;Every fix above was applied live, by hand, to a running dev environment, discovered by trial and error at the moment the thing broke. None of it was in a runbook, because there was no runbook. The whole sequence lived in trial-and-error scar tissue and nowhere a teammate, or a future me, could go look it up.&lt;/p&gt;

&lt;p&gt;There's a thread running through all five. The platform's convenient answer was the wrong one often enough that I learned to drop a layer down by reflex. &lt;code&gt;az functionapp show&lt;/code&gt; lies on Flex, so read the resource directly. The friendly hostname is dead, so pull the regional one off the resource. The official action is blocked, so call the CLI underneath it. The defaults point at the wrong port, so set them explicitly. The role-assignment helper fails, so hit ARM directly. The generic, lower-level path was the reliable one almost every time.&lt;/p&gt;

&lt;p&gt;This pile of hand-fixes is exactly what pushed me to describe my infrastructure in Bicep instead, so the next environment is reviewed as code before it exists rather than debugged after (that's its own post). But that's the cure. This post is the disease, written down, so the next person googling &lt;code&gt;HTTP 000 azurewebsites.net flex consumption&lt;/code&gt; at midnight finds something.&lt;/p&gt;

&lt;p&gt;When the platform tells you null, don't believe it. Go ask the resource.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>serverless</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Four Reviewers and a Gauntlet: Verifying AI-Authored Code</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:39:22 +0000</pubDate>
      <link>https://dev.to/tatted_dev/four-reviewers-and-a-gauntlet-verifying-ai-authored-code-2eob</link>
      <guid>https://dev.to/tatted_dev/four-reviewers-and-a-gauntlet-verifying-ai-authored-code-2eob</guid>
      <description>&lt;p&gt;Almost none of the code I ship was typed by me. I describe what I want, an AI coding agent writes the implementation, and I read what comes back. That one fact, still strange to say out loud, has quietly reorganized my entire job. I run a studio's worth of services solo, and across all of those repos the bulk of the actual authoring now happens on the other side of a prompt.&lt;/p&gt;

&lt;p&gt;That changes the job. When you're the one writing every line, your judgment lives in the writing. You feel the design as you type it. When an AI writes the lines, that feeling is gone, and you can fool yourself into thinking the work is done the moment the code looks plausible. Plausible is exactly the failure mode. AI-authored code is usually fluent, often correct, and occasionally confidently wrong in a way that reads great and breaks in production.&lt;/p&gt;

&lt;p&gt;So my judgment had to move somewhere. It moved to the verification layer. These days my attention goes to one thing: making sure what lands is solid. Every pull request in my repos runs a gauntlet of layered review before it's allowed to merge. This post is about that gauntlet, why it has so many layers, and why the economics of it actually favor a one-person shop.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "AI Writes the Code" Means Here
&lt;/h2&gt;

&lt;p&gt;Quick grounding before the jargon, because this is a public post and the setup is specific.&lt;/p&gt;

&lt;p&gt;I'm a solo founder building HoneyDrunk Studios. The bulk of the code authoring is done by AI coding agents (think command-line tools that take a task and produce a diff). My leverage is in two places now: specifying intent clearly enough that the agent builds the right thing, and verifying the output well enough that I trust it in production. The first half is prompts and design notes. The second half is the review gauntlet.&lt;/p&gt;

&lt;p&gt;A couple of terms I'll use repeatedly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;CI gate&lt;/strong&gt; is an automated check that runs on every pull request and has to pass before the code can merge. Build the project, run the tests, check formatting, measure test coverage. If any required gate fails, the merge button stays off. CI stands for continuous integration; for my purposes it's just "the robot that won't let bad code in."&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;pull request&lt;/strong&gt; (PR) is the unit of change. One proposed diff, opened for review, merged when it passes. Every layer below operates on a PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that established, here are the layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Layers
&lt;/h2&gt;

&lt;p&gt;Every PR I open can face up to five distinct reviewers before it merges. Each one is good at something different, and the overlap is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CodeRabbit.&lt;/strong&gt; An AI service that reviews pull requests. It reads the diff, comments inline on specific lines, flags likely bugs and convention violations, and summarizes the change. I deploy it across all my repos from one org-level configuration rather than copying a config file into every repo by hand. One file governs the whole studio, which matters when "the whole studio" is dozens of repos and one person maintaining them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. GitHub Copilot review.&lt;/strong&gt; GitHub's own built-in AI reviewer. It runs inside the GitHub PR interface and posts its own read of the diff. It's a second AI opinion from a different vendor with a different training and a different rubric, sitting right where I'm already looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. My own local AI review runner.&lt;/strong&gt; This is the one I built. It runs an AI code review on my own hardware, under my existing CLI subscriptions, and posts its verdict as a PR comment. On high-risk changes it runs the review through two different model families, Codex and Claude, and synthesizes their findings into one verdict. Two independent models, two independent blind spots. I wrote a whole separate post on how this runner's transport works (it polls GitHub and treats labels as a durable queue rather than waiting for a webhook); if you want the plumbing, read &lt;a href="https://tatteddev.com/blog/pull-based-grid-review-runner/" rel="noopener noreferrer"&gt;Stop Receiving Webhooks, Start Polling&lt;/a&gt;. Here it's just one layer in the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. GitHub Actions CI gates.&lt;/strong&gt; The automated required checks. Build succeeds, tests pass, coverage holds, formatting is clean, code quality thresholds are met. These are pass/fail facts rather than opinions, and a failing one blocks the merge outright. Where the AI reviewers say "this looks risky," the gates say "this is broken, full stop."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. SonarQube.&lt;/strong&gt; A static-analysis tool that scans the code without running it, hunting for bugs, code smells, security-sensitive patterns, and coverage gaps, then gates the PR on a quality threshold. It's the deterministic, rules-based counterweight to the four probabilistic AI opinions above it. SonarQube finds the same kind of issue the same way every single time, which is exactly what you want sitting next to a stack of models that each answer a little differently on every run.&lt;/p&gt;

&lt;p&gt;Five layers, three flavors: AI reviewers reading intent (CodeRabbit, Copilot, my runner), hard pass/fail gates (Actions), and deterministic static analysis (SonarQube). A change has to satisfy all of the ones that apply to it before it merges.&lt;/p&gt;




&lt;h2&gt;
  
  
  Risk-Based Escalation, and Why This Is Affordable
&lt;/h2&gt;

&lt;p&gt;A reasonable reaction here is that five reviewers on every pull request sounds slow and expensive. It would be, if every layer ran at full weight on every change. They don't.&lt;/p&gt;

&lt;p&gt;The stack is risk-scored. Each change gets a rough risk assessment, and the most expensive layers escalate only for the riskiest changes. A one-line copy fix in a README doesn't earn a dual-model deep review. A change to authentication, a database migration, or a deploy pipeline does. The heavyweight pass, my local runner spending the time to run both Codex and Claude and reconcile them, kicks in where the cost of a missed bug is highest. That's a deliberate cost-and-coverage tradeoff: spend the expensive attention where a mistake actually hurts, and let the cheap layers carry the routine changes.&lt;/p&gt;

&lt;p&gt;The economics work out in favor of layering, and that surprised me. Here's the shape of it for a solo dev:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CodeRabbit and Copilot review run on flat-rate subscriptions. Their cost is the same whether they review one PR a week or thirty.&lt;/li&gt;
&lt;li&gt;The GitHub Actions gates run on CI minutes that, at my volume, sit comfortably inside the free allowance.&lt;/li&gt;
&lt;li&gt;My local runner executes under CLI subscriptions I already pay for, on hardware I already own and leave running. Each review it performs adds no marginal token bill.&lt;/li&gt;
&lt;li&gt;The risk scoring caps how often the genuinely expensive path runs at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put those together and adding another reviewer to the gauntlet is mostly free at the margin. The fixed costs are paid; the per-PR cost of one more layer rounds to nothing. For a single operator, that's a rare situation where the safe choice and the cheap choice are the same choice. I'll take more independent eyes when more eyes are close to free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why So Many: The Thesis
&lt;/h2&gt;

&lt;p&gt;The reason for all of this fits in one observation about probability.&lt;/p&gt;

&lt;p&gt;A bug that one reviewer misses is common. Every reviewer, human or AI, has blind spots. A bug that two independent reviewers both miss is less common, because their blind spots rarely line up. A bug that survives an AI reviewer, a second AI reviewer from a different vendor, a dual-model local review, a battery of pass/fail gates, and a deterministic static analyzer is rare, because for that to happen, every one of those different rubrics has to fail in the same place at the same time. That's defense in depth: stack independent checks so that no single failure gets the whole way through.&lt;/p&gt;

&lt;p&gt;This is exactly why the high-risk path runs two different model families instead of running one model twice. Two passes of the same model share the same blind spots; they tend to agree, including when they're both wrong. Two different models disagree productively. One flags something the other waved through. The disagreement is the value. So the principle I hold is that the riskiest changes always get two genuinely independent model perspectives, two different rubrics from two different vendors looking at the same diff.&lt;/p&gt;

&lt;p&gt;And the human is still in this, just not where I used to be. I'm not reading every line as it's written anymore. I'm reading verdicts, resolving the cases where the layers disagree, and deciding what's actually true when CodeRabbit loves a change and SonarQube hates it. The judgment is still mine. It moved from the keyboard to the merge button.&lt;/p&gt;

&lt;p&gt;I want to be honest that this is an opinionated setup built for one specific situation: a solo dev whose code is overwhelmingly AI-authored. A team with humans writing and reviewing code line by line already has a lot of this coverage built into how they work, and stacking five automated layers on top might be redundant for them. For me, where no human wrote the code in the first place, the verification layer is the only place human judgment touches the work at all. So it gets all of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Close
&lt;/h2&gt;

&lt;p&gt;When you stop writing the code, you don't stop being the engineer. The engineering just moves downstream, from authoring to specifying and verifying. The gauntlet is where I spend my judgment now, and I built it deep on purpose.&lt;/p&gt;

&lt;p&gt;Let the cheap, fast layers catch the routine misses. Let two different models argue over the dangerous changes. Let the deterministic gates have the final, unarguable say. Write less code, verify it harder. That's the job now.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>devops</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Discord as My Operator Pager: One Timeline for a Whole Studio</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:37:00 +0000</pubDate>
      <link>https://dev.to/tatted_dev/discord-as-my-operator-pager-one-timeline-for-a-whole-studio-2aga</link>
      <guid>https://dev.to/tatted_dev/discord-as-my-operator-pager-one-timeline-for-a-whole-studio-2aga</guid>
      <description>&lt;p&gt;A deploy of mine broke, and I found out about it the way you never want to: days later, by stumbling onto it myself. The failure had been recorded. An email went out. A status flipped red in a dashboard somewhere. Every system did its job. I just never saw any of it, because the signal was scattered across so many places that none of them was the place I actually looked.&lt;/p&gt;

&lt;p&gt;That's the failure mode of running a lot of automation alone. Under my repos there's a constant hum of machinery: deploys firing, CI pipelines passing and failing, scheduled jobs waking up to do work, secrets quietly aging toward expiry. When you're one person, the hard question stops being "can I build it" and becomes "do I know what it's doing right now." For a long time the honest answer was no.&lt;/p&gt;

&lt;p&gt;The signal was everywhere and nowhere. GitHub notifications were supposed to be my feed, except they were buried under a huge issue backlog, so the inbox that should have told me a deploy broke was the same inbox screaming about forty open issues. Actions failures arrived as email, which I learned to skim past because most of them were noise. Some things only showed up if I went and looked: opening a dashboard, checking a deploy status, remembering to verify a job actually ran. There was no single place I could glance at and trust to tell me the truth. So I missed things. The information existed the whole time. It just had no home.&lt;/p&gt;

&lt;p&gt;This post is about giving it one. I made Discord the place where everything my automation does reports in, and I gave that reporting a structure so the important events stay glanceable.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Signal Has No Home
&lt;/h2&gt;

&lt;p&gt;The pattern was worse than any one missed alert.&lt;/p&gt;

&lt;p&gt;A deploy would go out and I'd find out it worked by using the thing, not by being told. A CI run would fail on a repo I hadn't touched in a week, and the email telling me sat unread between two GitHub digest mails. A scheduled job would skip a night and I'd discover the gap days later. None of these are catastrophic on their own. The problem is the pattern: every kind of operational event lived in a different place, in a different format, with a different chance of me ever seeing it.&lt;/p&gt;

&lt;p&gt;On a team, someone is usually watching the boards while someone else builds. Solo, the watching is also my job, layered on top of the building, and human attention does not scale across a dozen notification surfaces. I needed to collapse all of those surfaces into one.&lt;/p&gt;

&lt;p&gt;The requirement was simple to state. I wanted a single timeline where, if something happened that I'd care about, it showed up, and where the things I'd care about most were easy to pick out from the things that are just routine chatter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Webhook Is, and Why Discord
&lt;/h2&gt;

&lt;p&gt;The mechanism underneath all of this is a webhook, and it's worth a plain definition because the whole design rests on it.&lt;/p&gt;

&lt;p&gt;A Discord webhook is a URL you POST a message to. You send an HTTP request to that URL with some content, and the message appears in a Discord channel. That's the entire contract. There's no bot to host, no gateway connection to keep alive, no library to babysit. Any piece of automation that can make an HTTP request can speak into a channel, which means every part of my studio (a GitHub Action in the cloud, a script on a machine at home) can report in with the same trivial primitive.&lt;/p&gt;

&lt;p&gt;Discord earns the role for a boring reason: I'm already in it, on my phone and my desktop, all day. The pager has to be somewhere I already look. Anywhere else just becomes one more dashboard I forget to open. Channels give me natural separation. Mentions give me a way to make the loud things loud. It was already running, so I made it load-bearing.&lt;/p&gt;

&lt;p&gt;I'll borrow a word for the messages it carries: operator alerts. An operator alert is a message meant for me, the person running the studio, telling me something about the state of my own systems. A deploy finished. A pipeline failed. A secret is about to expire. The audience is exactly one person, and that fact ends up mattering a lot, which I'll get to.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Taxonomy: Every Event Has a Known Home
&lt;/h2&gt;

&lt;p&gt;A single channel with everything dumped into it would just be the email inbox again, reinvented. The structure is the point.&lt;/p&gt;

&lt;p&gt;So I split operator alerts into channels by signal type. Each kind of event has a known home, and I know what each home means without reading closely. The split looks roughly like this (these are examples of the shape, not a literal channel list):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploys.&lt;/strong&gt; Something went out, where it went, and whether it landed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI failures.&lt;/strong&gt; A pipeline broke, on which repo, with a link to the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and credential events.&lt;/strong&gt; A secret or credential did something I need to know about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation and escalation.&lt;/strong&gt; A scheduled or sensitive job needs my attention now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason this works is that the channel itself carries meaning before I read a word. A new message in the deploys channel is routine and I can let it scroll by. A new message in the security channel makes me stop. By routing each event type to its own home, the important signals stop competing with the routine ones for the same glance. Glanceability is a property of the layout, not of any single message.&lt;/p&gt;

&lt;p&gt;A webhook emits a structured message into the right channel. The emitter knows what kind of event it's reporting, so it knows which channel URL to POST to. That routing decision is the taxonomy, encoded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Stores, Because Alerts Come From Two Places
&lt;/h2&gt;

&lt;p&gt;There's a credential wrinkle here that shaped the implementation, and it's a good illustration of how a small system grows real edges.&lt;/p&gt;

&lt;p&gt;Those webhook URLs are secrets. Anyone holding one can post into your channel, so they have to be stored and handed to the emitters carefully. The catch is that my alerts originate from two very different places, and those two places have different ways of holding a secret.&lt;/p&gt;

&lt;p&gt;Some alerts come from GitHub Actions, running in the cloud. Those read their webhook URLs from organization secrets, which is GitHub's built-in store for values that workflows are allowed to use. Other alerts come from a local automation worker running on my own hardware. That worker reads its webhook URLs from Key Vault, which is Azure's cloud secret store. Two execution contexts, two secret stores, the same destination channels on the other end.&lt;/p&gt;

&lt;p&gt;I could have forced everything through one store, but the contexts genuinely live in different trust domains, and each already had a native, well-guarded place to keep a secret. So I let each emitter use the store that fits where it runs. The taxonomy is shared. The credential plumbing is local to the emitter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redaction Is Not Optional
&lt;/h2&gt;

&lt;p&gt;There's one rule in this system I treat as absolute: secrets get redacted before anything is posted.&lt;/p&gt;

&lt;p&gt;The reason is uncomfortable once you say it out loud. The whole point of a security and credential channel is to tell me when something is wrong with a secret. If the message announcing that a token is expiring includes the token, then my alerting surface just leaked a credential into a chat application. An alert that leaks the thing it's alerting about is its own incident, and a worse one than the event that triggered it.&lt;/p&gt;

&lt;p&gt;So redaction is a property of the emitter, enforced before the POST, every time. The emitter's job is to describe an event in enough detail that I can act, and never enough detail to compromise anything. "A secret in this vault is expiring in three days" is the alert. The secret's value never goes near the channel. I'd rather an alert be slightly too vague than carry a payload I'd regret.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Boundary: Operator Alerts and Customer Notifications Are Different Concerns
&lt;/h2&gt;

&lt;p&gt;This is the part I hold most firmly, so I'll state it plainly as two separate things.&lt;/p&gt;

&lt;p&gt;Operator alerts are signal for me, the person running the studio. The audience is one. The job is to tell me what my automation is doing so I can intervene when I need to. Discord is that pager, and this entire post is about it.&lt;/p&gt;

&lt;p&gt;Customer-facing notifications are a product capability, the features that email or notify the people who use what I build. The audience is users. The job is reliable, durable delivery to people who are not me. That's a separate system entirely, with its own delivery guarantees, its own retries, its own failure handling, and its own place in the architecture.&lt;/p&gt;

&lt;p&gt;These two have different audiences, different reliability needs, and different failure modes, so they live in different systems. If my operator pager misses a message, I notice the next time I glance and I go look. If a customer notification system misses a message, that's a product defect with someone else's expectations attached. Mixing them would drag the casual reliability of a personal pager into a place that demands real guarantees, or drag heavyweight delivery machinery into something that just needs to ping my phone. Keeping them apart lets each one be exactly as serious as it should be.&lt;/p&gt;

&lt;p&gt;Discord is the operator pager. The customer notification product is its own thing, and it's a different post.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Shipped, and What I Learned
&lt;/h2&gt;

&lt;p&gt;The taxonomy is in place, both webhook stores are wired, redaction is enforced at the emitter, and the most recent piece to land was a credential pager: alerts specifically for credential and secret events. A secret nearing expiry, a rotation that needs attention, these now route to their own channel instead of dissolving into the general stream. The events that are easiest to forget and most painful to miss got the most deliberate home.&lt;/p&gt;

&lt;p&gt;The thing I learned building this is that scattered signal gets solved by choosing one surface and committing to it, not by polishing the surfaces you already ignore. The technology here is almost embarrassingly simple. A webhook is a URL you POST to. All the real work lived in the taxonomy, the discipline of redaction, and the boundary that keeps my pager out of my product.&lt;/p&gt;

&lt;p&gt;A solo operator can't watch a dozen places. So I stopped trying, and built the one place worth watching.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>architecture</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>🍯 Announcing HoneyDrunk.Data: A Multi-Tenant Persistence Layer for Distributed .NET Applications</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Tue, 06 Jan 2026 23:34:07 +0000</pubDate>
      <link>https://dev.to/tatted_dev/announcing-honeydrunkdata-a-multi-tenant-persistence-layer-for-distributed-net-applications-4mp5</link>
      <guid>https://dev.to/tatted_dev/announcing-honeydrunkdata-a-multi-tenant-persistence-layer-for-distributed-net-applications-4mp5</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We just released HoneyDrunk.Data, a provider-agnostic persistence layer built for multi-tenant, distributed .NET 10 applications. It features automatic correlation tracking in SQL queries, tenant-aware repositories, and seamless integration with OpenTelemetry—all without coupling your domain logic to Entity Framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem We're Solving
&lt;/h2&gt;

&lt;p&gt;Building multi-tenant applications with proper observability is harder than it should be. You end up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scattered tenant checks&lt;/strong&gt; throughout your codebase&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No correlation&lt;/strong&gt; between your distributed traces and database queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tight coupling&lt;/strong&gt; between your domain logic and EF Core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test infrastructure&lt;/strong&gt; that requires spinning up real databases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We built HoneyDrunk.Data to solve these problems as part of &lt;a href="https://github.com/HoneyDrunkStudios" rel="noopener noreferrer"&gt;HoneyDrunk.OS&lt;/a&gt;—our distributed application framework for .NET.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Layers That Actually Make Sense
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│   HoneyDrunk.Data.Abstractions      │  ← Zero EF Core dependencies
│   (IRepository, IUnitOfWork, etc.)  │
└─────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│        HoneyDrunk.Data              │  ← Kernel integration, no EF Core
│   (Tenant accessor, telemetry)      │
└─────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│  HoneyDrunk.Data.EntityFramework    │  ← EF Core implementation
│   (EfRepository, EfUnitOfWork)      │
└─────────────────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────┐
│    HoneyDrunk.Data.SqlServer        │  ← SQL Server specifics
│   (Retry, Azure SQL, datetime2)     │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Your domain projects reference only &lt;code&gt;HoneyDrunk.Data.Abstractions&lt;/code&gt;. No EF Core leaking into your business logic. Swap providers without touching domain code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature Highlight: Correlation Tracking in SQL
&lt;/h2&gt;

&lt;p&gt;This is my favorite feature. Every SQL command is automatically tagged with the Grid correlation ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* correlation:01JXY7ABC123DEF456 */&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find the exact query from a slow API response&lt;/li&gt;
&lt;li&gt;Correlate database logs with distributed traces&lt;/li&gt;
&lt;li&gt;Debug production issues without guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation uses EF Core's &lt;code&gt;DbCommandInterceptor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CorrelationCommandInterceptor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DbCommandInterceptor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IDataDiagnosticsContext&lt;/span&gt; &lt;span class="n"&gt;_diagnosticsContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;InterceptionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;NonQueryExecuting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DbCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CommandEventData&lt;/span&gt; &lt;span class="n"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;InterceptionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;AddCorrelationComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NonQueryExecuting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eventData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddCorrelationComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_diagnosticsContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CorrelationId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// SQL comments don't affect query plan caching&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandText&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"/* correlation:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; */\n&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use SQL comments specifically because they don't affect query plan caching—your cached plans stay cached.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multi-Tenancy: From HTTP Header to Database Query
&lt;/h2&gt;

&lt;p&gt;Tenant context flows automatically from the HTTP request through to database queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Request                 Kernel                    Data Layer
     │                         │                          │
X-Tenant-Id: acme  →  IOperationContext.TenantId  →  ITenantAccessor
     │                         │                          │
                                                   TenantId.FromString("acme")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;KernelTenantAccessor&lt;/code&gt; bridges our Kernel context system with the data layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KernelTenantAccessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITenantAccessor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IOperationContextAccessor&lt;/span&gt; &lt;span class="n"&gt;_contextAccessor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentTenantId&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_contextAccessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; 
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your DbContext can then apply global query filters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Every query automatically filters by tenant&lt;/span&gt;
    &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HasQueryFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CurrentTenantId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more "did we remember to filter by tenant?" code reviews.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repository Pattern, Done Right
&lt;/h2&gt;

&lt;p&gt;I know, I know—"repository pattern is an anti-pattern with EF Core." Hear me out.&lt;/p&gt;

&lt;p&gt;Our repositories aren't about abstracting EF Core. They're about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Separating read from write concerns&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Providing a testable boundary&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabling future provider swaps&lt;/strong&gt; (Dapper for hot paths, anyone?)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IReadOnlyRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FindByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expression&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IReadOnlyRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEntity&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Unit of Work coordinates changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CheckoutAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cart&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InventoryItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inv&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProductId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Atomic save across all repositories&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Without Docker: SQLite In-Memory
&lt;/h2&gt;

&lt;p&gt;Spinning up SQL Server containers for every test run is slow. We provide SQLite-based test infrastructure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderRepositoryTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SqliteTestDbContextFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_factory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderRepositoryTests&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SqliteTestDbContextFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;TestDoubles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateTenantAccessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-tenant"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;TestDoubles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDiagnosticsContext&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;AddAsync_ShouldPersistOrder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;EfRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;99.99m&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test doubles included&lt;/strong&gt; for tenant and diagnostics contexts—no mocking frameworks required.&lt;/p&gt;




&lt;h2&gt;
  
  
  SQL Server Specifics: datetime2 and Retry Logic
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;HoneyDrunk.Data.SqlServer&lt;/code&gt; package handles SQL Server-specific concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHoneyDrunkDataSqlServer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableRetryOnFailure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxRetryCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Or for Azure SQL with optimized settings&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHoneyDrunkDataAzureSql&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AzureSql"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Model conventions automatically apply SQL Server best practices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnModelCreating&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelBuilder&lt;/span&gt; &lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;modelBuilder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseDateTime2ForAllDateTimeProperties&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// No more datetime truncation&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureDecimalPrecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// Explicit money precision&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full stack with SQL Server&lt;/span&gt;
dotnet add package HoneyDrunk.Data.SqlServer

&lt;span class="c"&gt;# Or just EF Core (bring your own provider)&lt;/span&gt;
dotnet add package HoneyDrunk.Data.EntityFramework

&lt;span class="c"&gt;# Or abstractions only (for domain libraries)&lt;/span&gt;
dotnet add package HoneyDrunk.Data.Abstractions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimal setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Register Kernel (required for context propagation)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkGrid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NodeId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order-service"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StudioId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"acme-corp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Register Data layer&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHoneyDrunkDataSqlServer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IUnitOfWork&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;unitOfWork&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Repository&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;FindByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This is v0.1.0—the foundation. On the roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL provider&lt;/strong&gt; (&lt;code&gt;HoneyDrunk.Data.PostgreSQL&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read replica support&lt;/strong&gt; for CQRS patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbox pattern&lt;/strong&gt; integration for reliable messaging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Query profiling&lt;/strong&gt; with automatic slow query detection&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/HoneyDrunkStudios/HoneyDrunk.Data" rel="noopener noreferrer"&gt;github.com/HoneyDrunkStudios/HoneyDrunk.Data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;code&gt;HoneyDrunk.Data.SqlServer&lt;/code&gt; (and friends)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://github.com/HoneyDrunkStudios/HoneyDrunk.Data/blob/main/HoneyDrunk.Data/docs/FILE_GUIDE.md" rel="noopener noreferrer"&gt;FILE_GUIDE.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with 🍯 by &lt;a href="https://github.com/HoneyDrunkStudios" rel="noopener noreferrer"&gt;HoneyDrunk Studios&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Got questions? Open an issue or find us on GitHub. PRs welcome—especially if you want to add that PostgreSQL provider.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://tatteddev.com/blog/honeydrunk-data-multi-tenant-persistence-layer/" rel="noopener noreferrer"&gt;https://tatteddev.com/blog/honeydrunk-data-multi-tenant-persistence-layer/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>dotnet</category>
      <category>programming</category>
      <category>data</category>
    </item>
    <item>
      <title>HoneyDrunk.Transport: A Transport Agnostic Messaging Framework for .NET</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Mon, 08 Dec 2025 13:56:08 +0000</pubDate>
      <link>https://dev.to/tatted_dev/honeydrunktransport-a-transport-agnostic-messaging-framework-for-net-2lca</link>
      <guid>https://dev.to/tatted_dev/honeydrunktransport-a-transport-agnostic-messaging-framework-for-net-2lca</guid>
      <description>&lt;p&gt;Distributed systems love to trap you in vendor gravity. One transport today becomes a rewrite tomorrow. When I started wiring together services for HoneyDrunk’s distributed grid, the usual messaging libraries all had the same problem: coupling. Everything leaked broker assumptions.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;HoneyDrunk.Transport&lt;/strong&gt; to break that pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Is
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk.Transport is a transport-agnostic messaging abstraction for .NET. Your code looks identical whether you're using Azure Service Bus, Azure Storage Queues, or the InMemory transport.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This code works identically whether you're using&lt;/span&gt;
&lt;span class="c1"&gt;// Azure Service Bus, Storage Queues, or InMemory transport&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMessagePublisher&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IGridContext&lt;/span&gt; &lt;span class="n"&gt;gridContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"orders.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;gridContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gridContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your handlers stay the same too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedHandler&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IMessageHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderCreated&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;MessageProcessingResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;HandleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;OrderCreated&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MessageContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;correlationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GridContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;CorrelationId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GridContext&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MessageProcessingResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why It Exists
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Swap transports without rewriting code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Development&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkTransportCore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkInMemoryTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Production&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkTransportCore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkServiceBusTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullyQualifiedNamespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mynamespace.servicebus.windows.net"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders-queue"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Distributed context propagation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node A (Order Service)                    Node B (Payment Service)
┌─────────────────────┐                   ┌─────────────────────┐
│ CorrelationId: abc  │ ──── Queue ────▶  │ CorrelationId: abc  │
│ TenantId: tenant-1  │                   │ TenantId: tenant-1  │
│ ProjectId: proj-1   │                   │ ProjectId: proj-1   │
└─────────────────────┘                   └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Middleware pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkTransportCore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableTelemetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// OpenTelemetry spans&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableLogging&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// Structured logging&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnableCorrelation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Grid context propagation&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Add custom middleware&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddMessageMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TenantResolutionMiddleware&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Configurable error handling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConfigurableErrorHandlingStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxDeliveryCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryOn&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryOn&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetterOn&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ValidationException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeadLetterOn&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IErrorHandlingStrategy&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture at a Glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                     Your Application                            │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐ │
│  │ OrderService    │  │ PaymentHandler  │  │ Custom          │ │
│  │ (IMessagePub)   │  │ (IMessageHandler)│ │ Middleware      │ │
│  └────────┬────────┘  └────────┬────────┘  └────────┬────────┘ │
└───────────┼─────────────────────┼─────────────────────┼─────────┘
            │                     │                     │
┌───────────▼─────────────────────▼─────────────────────▼─────────┐
│                   HoneyDrunk.Transport                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │ Abstractions │  │   Pipeline   │  │ Configuration│          │
│  │ IMessagePub  │  │  Middleware  │  │ ErrorStrategy│          │
│  │ IMessageHdlr │  │  Telemetry   │  │ RetryOptions │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────┬───────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌───────────────┐    ┌───────────────┐    ┌───────────────┐
│ ServiceBus    │    │ StorageQueue  │    │   InMemory    │
│   Transport   │    │   Transport   │    │   Transport   │
└───────────────┘    └───────────────┘    └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Transport Adapters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Azure Service Bus
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkServiceBusTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullyQualifiedNamespace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"mynamespace.servicebus.windows.net"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ServiceBusEntityType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders-topic"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SubscriptionName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order-processor"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxConcurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Blob fallback for large messages&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlobFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlobFallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccountUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://myaccount.blob.core.windows.net"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Azure Storage Queue
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkStorageQueueTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"notifications"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxConcurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BatchProcessingConcurrency&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// 40 total concurrent&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  InMemory Transport
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkTransportCore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHoneyDrunkInMemoryTransport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package HoneyDrunk.Transport
dotnet add package HoneyDrunk.Transport.AzureServiceBus
dotnet add package HoneyDrunk.Transport.StorageQueue
dotnet add package HoneyDrunk.Transport.InMemory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk.Transport exists because messaging shouldn’t require allegiance to a single broker. If you want clean abstractions, portable handlers, built-in telemetry, and automatic context propagation, it’s worth exploring.&lt;/p&gt;

&lt;p&gt;If you try it, let me know how it fits into your stack.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;strong&gt;&lt;a href="https://tatteddev.com/blog/honeydrunk-transport-messaging-framework/" rel="noopener noreferrer"&gt;TattedDev.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>opensource</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Honeypunk: A Semantic, Palette-Driven Theme Pipeline for Visual Studio 2026</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 16 Nov 2025 20:43:39 +0000</pubDate>
      <link>https://dev.to/tatted_dev/honeypunk-a-semantic-palette-driven-theme-pipeline-for-visual-studio-2026-1a5k</link>
      <guid>https://dev.to/tatted_dev/honeypunk-a-semantic-palette-driven-theme-pipeline-for-visual-studio-2026-1a5k</guid>
      <description>&lt;p&gt;Visual Studio 2026 shipped last week, and like a lot of devs, I installed it immediately. The new UI polish looked great. The editor surfaces felt modern. Then I opened a file and hit something familiar:&lt;/p&gt;

&lt;p&gt;My favorite themes had not updated.&lt;/p&gt;

&lt;p&gt;No previews. No experimental builds. No support for the new classification keys introduced in VS2026.&lt;/p&gt;

&lt;p&gt;Instead of waiting, I built the update myself and, in the process, created a full semantic theme pipeline.&lt;/p&gt;

&lt;p&gt;Honeypunk is a palette-driven, YAML-based system that lets you define color intent once and generate a complete Visual Studio theme automatically. No hex hunting. No drift. No fragile edits.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/tatteddev/Honeypunk" rel="noopener noreferrer"&gt;https://github.com/tatteddev/Honeypunk&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Manual Theme Editing Fails
&lt;/h2&gt;

&lt;p&gt;Visual Studio theming is powerful but tedious. Updating themes manually is slow and error prone, especially when a major version adds new editor surfaces.&lt;/p&gt;

&lt;p&gt;Common issues include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hex values scattered across huge JSON theme files
&lt;/li&gt;
&lt;li&gt;Small tweaks creating near-duplicate colors
&lt;/li&gt;
&lt;li&gt;New VS releases breaking old themes
&lt;/li&gt;
&lt;li&gt;One color change often requiring 20 or more key edits
&lt;/li&gt;
&lt;li&gt;No auditing for unused or duplicate colors
&lt;/li&gt;
&lt;li&gt;Experimentation requiring too much overhead
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Honeypunk introduces a semantic, declarative approach that avoids all of this.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pipeline Model
&lt;/h2&gt;

&lt;p&gt;Honeypunk follows a predictable, declarative flow:&lt;/p&gt;

&lt;p&gt;Palette → Roles → Mappings → Build → VSIX&lt;/p&gt;

&lt;h3&gt;
  
  
  Palette
&lt;/h3&gt;

&lt;p&gt;The palette is the root of the system. All colors are centralized into a single file with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Human-readable names
&lt;/li&gt;
&lt;li&gt;Contrast rules
&lt;/li&gt;
&lt;li&gt;Usage intent (foreground, accent, diagnostic, background)
&lt;/li&gt;
&lt;li&gt;Structure that allows global shifts with a single edit
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Roles
&lt;/h3&gt;

&lt;p&gt;Roles describe what a token represents, not what color it uses. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;function
&lt;/li&gt;
&lt;li&gt;variable
&lt;/li&gt;
&lt;li&gt;type
&lt;/li&gt;
&lt;li&gt;interface
&lt;/li&gt;
&lt;li&gt;keyword
&lt;/li&gt;
&lt;li&gt;comment
&lt;/li&gt;
&lt;li&gt;string
&lt;/li&gt;
&lt;li&gt;operator
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roles point to palette entries, never hex values.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mappings
&lt;/h3&gt;

&lt;p&gt;Mappings connect semantic roles to actual Visual Studio classification keys. This file handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VS2022 through VS2026 classifications
&lt;/li&gt;
&lt;li&gt;ReSharper keys
&lt;/li&gt;
&lt;li&gt;Background and foreground tokens
&lt;/li&gt;
&lt;li&gt;New surfaces introduced by 2026
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Build Scripts
&lt;/h3&gt;

&lt;p&gt;The Python tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loads palette, roles, mappings
&lt;/li&gt;
&lt;li&gt;Resolves semantic intent to actual colors
&lt;/li&gt;
&lt;li&gt;Validates for errors, typos, unmapped items
&lt;/li&gt;
&lt;li&gt;Preserves flags like transparency
&lt;/li&gt;
&lt;li&gt;Generates a deterministic theme file
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  VSIX
&lt;/h3&gt;

&lt;p&gt;The final packaging step creates a distributable Visual Studio extension supporting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VS2022
&lt;/li&gt;
&lt;li&gt;VS2025
&lt;/li&gt;
&lt;li&gt;VS2026
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire pipeline allows rebuilds in seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core Files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Honeypunk.yaml
&lt;/h3&gt;

&lt;p&gt;Defines high-level theme structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor
&lt;/li&gt;
&lt;li&gt;Chrome and toolbars
&lt;/li&gt;
&lt;li&gt;Tool windows
&lt;/li&gt;
&lt;li&gt;Accent states
&lt;/li&gt;
&lt;li&gt;Debugging and caret surfaces
&lt;/li&gt;
&lt;li&gt;Selection and inlay hints
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  palette.md
&lt;/h3&gt;

&lt;p&gt;A complete list of named colors with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hex values
&lt;/li&gt;
&lt;li&gt;Usage recommendations
&lt;/li&gt;
&lt;li&gt;Contrast notes
&lt;/li&gt;
&lt;li&gt;Accessibility considerations
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  roles.yaml
&lt;/h3&gt;

&lt;p&gt;Maps semantic intent to palette names, for example:&lt;/p&gt;

&lt;p&gt;functions: NeonYellow&lt;br&gt;&lt;br&gt;
variables: ChromeTeal&lt;br&gt;&lt;br&gt;
comments: SlateGray  &lt;/p&gt;

&lt;h3&gt;
  
  
  mappings.yaml
&lt;/h3&gt;

&lt;p&gt;Links roles to actual Visual Studio classification keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  apply_palette.py
&lt;/h3&gt;

&lt;p&gt;Reads all YAML files, applies semantic roles, enforces safety checks, and produces the resolved theme.&lt;/p&gt;

&lt;h3&gt;
  
  
  build_vsix.py
&lt;/h3&gt;

&lt;p&gt;Packages the final theme into a VSIX with the necessary manifest metadata.&lt;/p&gt;




&lt;h2&gt;
  
  
  Palette Philosophy
&lt;/h2&gt;

&lt;p&gt;The Honeypunk palette is intentionally structured for clarity and reduced fatigue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background tier
&lt;/h3&gt;

&lt;p&gt;Deep gunmetal and charcoal tones that minimize glare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text tier
&lt;/h3&gt;

&lt;p&gt;Neutral off-white and slate values optimized for long reading sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accent tier
&lt;/h3&gt;

&lt;p&gt;Electric blues, neon yellows, and magentas used sparingly and intentionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Diagnostic triad
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Orange for warnings
&lt;/li&gt;
&lt;li&gt;Magenta for attention
&lt;/li&gt;
&lt;li&gt;Green for success
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The palette supports fast code scanning and visual consistency across large files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Updating for Visual Studio 2026
&lt;/h2&gt;

&lt;p&gt;VS2026 introduced new:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classification keys
&lt;/li&gt;
&lt;li&gt;Chrome surfaces
&lt;/li&gt;
&lt;li&gt;Editor states
&lt;/li&gt;
&lt;li&gt;Accent highlights
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A manual update would require reviewing every affected surface.&lt;br&gt;&lt;br&gt;
Honeypunk reduced this to a few mapping adjustments and a single rebuild.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extending the System
&lt;/h2&gt;

&lt;p&gt;Honeypunk was built for growth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a new role
&lt;/h3&gt;

&lt;p&gt;Add to roles.yaml and map in mappings.yaml.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a new color
&lt;/h3&gt;

&lt;p&gt;Add to palette.md and reference it in roles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple theme variants
&lt;/h3&gt;

&lt;p&gt;Create separate YAML build configurations sharing the same palette.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coming soon
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Accessibility variants
&lt;/li&gt;
&lt;li&gt;JSON diff reports
&lt;/li&gt;
&lt;li&gt;Live preview experiments
&lt;/li&gt;
&lt;li&gt;CI checks for color drift
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;Common fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Theme not showing: verify VSIX manifest GUID.
&lt;/li&gt;
&lt;li&gt;Odd colors: check palette name for typos.
&lt;/li&gt;
&lt;li&gt;Build failure: YAML indentation error.
&lt;/li&gt;
&lt;li&gt;Missing token updates: mapping entry missing.
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;Clone the repo and modify a single role.&lt;br&gt;&lt;br&gt;
Rebuild the VSIX.&lt;br&gt;&lt;br&gt;
Install and restart Visual Studio.  &lt;/p&gt;

&lt;p&gt;You will immediately see how semantic theming makes iteration effortless.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/tatteddev/Honeypunk" rel="noopener noreferrer"&gt;https://github.com/tatteddev/Honeypunk&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Originally published at:&lt;br&gt;&lt;br&gt;
&lt;a href="https://tatteddev.com/blog/honeypunk-semantic-theme-pipeline-vs2026/" rel="noopener noreferrer"&gt;https://tatteddev.com/blog/honeypunk-semantic-theme-pipeline-vs2026/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>themes</category>
      <category>programming</category>
      <category>automation</category>
    </item>
    <item>
      <title>Building a Zero-Configuration .NET Standards Package</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Mon, 03 Nov 2025 21:58:37 +0000</pubDate>
      <link>https://dev.to/tatted_dev/building-a-zero-configuration-net-standards-package-2fgl</link>
      <guid>https://dev.to/tatted_dev/building-a-zero-configuration-net-standards-package-2fgl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Posted by &lt;a href="https://tatteddev.com" rel="noopener noreferrer"&gt;TattedDev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Code Standards
&lt;/h2&gt;

&lt;p&gt;Every .NET organization eventually fights the same battle: keeping code quality consistent across dozens of solutions. You copy &lt;code&gt;.editorconfig&lt;/code&gt; files, manually add analyzers, and hope developers remember to fix warnings. Over time, configurations drift, rules diverge, and build quality becomes uneven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/HoneyDrunkStudios/HoneyDrunk.Standards" rel="noopener noreferrer"&gt;HoneyDrunk.Standards&lt;/a&gt;&lt;/strong&gt; solves this by turning code quality into infrastructure. It’s a single NuGet package that enforces conventions, analyzers, and build configurations automatically across every project in your ecosystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Build-Transitive Package Design
&lt;/h3&gt;

&lt;p&gt;The package uses &lt;strong&gt;MSBuild’s &lt;code&gt;buildTransitive&lt;/code&gt; feature&lt;/strong&gt;, which allows its build logic to propagate to all downstream projects that reference it.&lt;br&gt;&lt;br&gt;
Unlike normal runtime packages, this one operates entirely at build time.&lt;/p&gt;

&lt;p&gt;When referenced, it automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enables &lt;strong&gt;StyleCop.Analyzers&lt;/strong&gt; and &lt;strong&gt;Microsoft.CodeAnalysis.NetAnalyzers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enforces &lt;strong&gt;nullable reference types&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Treats &lt;strong&gt;warnings as errors&lt;/strong&gt; in CI environments&lt;/li&gt;
&lt;li&gt;Enables &lt;strong&gt;deterministic builds&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Locks &lt;strong&gt;C# language version&lt;/strong&gt; to &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t modify your &lt;code&gt;.csproj&lt;/code&gt;. You don’t merge &lt;code&gt;.editorconfig&lt;/code&gt;. It’s zero-configuration by design.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Continuous Integration Awareness
&lt;/h3&gt;

&lt;p&gt;The package detects CI systems (like GitHub Actions or Azure Pipelines) through environment variables and automatically enables &lt;code&gt;ContinuousIntegrationBuild=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This provides two distinct build profiles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local development:&lt;/strong&gt; fast incremental builds with absolute source paths for debugging
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI builds:&lt;/strong&gt; deterministic, reproducible outputs with embedded source paths for traceability
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is parity between developer machines and build servers without requiring conditional logic in your projects.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Opt-Out Instead of Opt-In
&lt;/h3&gt;

&lt;p&gt;Every enforcement feature is enabled by default.&lt;br&gt;&lt;br&gt;
Developers can selectively disable rules via MSBuild properties, but the baseline assumes quality enforcement. This approach prevents “silent drift” where one repository quietly stops following standards.&lt;/p&gt;

&lt;p&gt;Opt-out defaults ensure consistency across all solutions while maintaining flexibility for legitimate edge cases.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Approach Works
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Consistent Code Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;StyleCop.Analyzers enforces over 100 naming, spacing, and ordering rules
&lt;/li&gt;
&lt;li&gt;Microsoft.CodeAnalysis.NetAnalyzers enforces performance, security, and reliability best practices
&lt;/li&gt;
&lt;li&gt;Violations fail CI builds automatically, maintaining organizational quality thresholds
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Configuration Centralization
&lt;/h3&gt;

&lt;p&gt;One version bump updates analyzers, language rules, and conventions across every project.&lt;br&gt;&lt;br&gt;
No more synchronizing &lt;code&gt;.editorconfig&lt;/code&gt; files or manually aligning rule sets across repositories.&lt;/p&gt;
&lt;h3&gt;
  
  
  Faster Onboarding
&lt;/h3&gt;

&lt;p&gt;New developers get immediate feedback inside Visual Studio or Rider.&lt;br&gt;&lt;br&gt;
Standards become visible and actionable in the IDE instead of hidden in documentation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Deterministic and Auditable Builds
&lt;/h3&gt;

&lt;p&gt;Every project builds reproducibly, producing identical binaries for the same source inputs.&lt;br&gt;&lt;br&gt;
That’s essential for compliance, debugging, and verifying production deployments.&lt;/p&gt;


&lt;h2&gt;
  
  
  Common Gotchas and Solutions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Large Warning Backlogs
&lt;/h3&gt;

&lt;p&gt;Adding the package to a legacy project often triggers hundreds of analyzer warnings.&lt;br&gt;&lt;br&gt;
Use incremental adoption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;NoWarn&amp;gt;&lt;/span&gt;$(NoWarn);SA*&lt;span class="nt"&gt;&amp;lt;/NoWarn&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then remove suppression rules as you clean up the codebase.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Missing &lt;code&gt;PrivateAssets="all"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Omitting this property allows analyzers to flow transitively into your consumers, which is rarely desirable.&lt;/p&gt;

&lt;p&gt;Always declare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"HoneyDrunk.Standards"&lt;/span&gt; &lt;span class="na"&gt;PrivateAssets=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents analyzer propagation and keeps the standards internal to your organization.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Test Method Naming (CA1707)
&lt;/h3&gt;

&lt;p&gt;Unit test naming conventions like &lt;code&gt;MethodName_Condition_ExpectedResult&lt;/code&gt; conflict with CA1707.&lt;br&gt;&lt;br&gt;
The package pre-suppresses this rule, prioritizing readability and established testing patterns.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Overuse of the Null-Forgiving Operator (&lt;code&gt;!&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The null-forgiving operator should be reserved for proven non-null scenarios.&lt;br&gt;&lt;br&gt;
Document any usage with an inline comment explaining why null cannot occur.&lt;/p&gt;


&lt;h3&gt;
  
  
  5. Local vs CI Output Differences
&lt;/h3&gt;

&lt;p&gt;When running locally, builds embed absolute file paths.&lt;br&gt;&lt;br&gt;
In CI mode, paths are normalized for reproducibility.&lt;/p&gt;

&lt;p&gt;To simulate CI locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build /p:ContinuousIntegrationBuild&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  6. Team Preference Conflicts
&lt;/h3&gt;

&lt;p&gt;Different teams sometimes prefer conflicting style rules (for example, requiring or omitting &lt;code&gt;this.&lt;/code&gt; prefixes).&lt;br&gt;&lt;br&gt;
The package allows per-project overrides in &lt;code&gt;.editorconfig&lt;/code&gt;, and StyleCop can be disabled completely if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;EnableStyleCopAnalyzers&amp;gt;&lt;/span&gt;false&lt;span class="nt"&gt;&amp;lt;/EnableStyleCopAnalyzers&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Implementation Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start with a sample project.&lt;/strong&gt; The included &lt;code&gt;Consumer.Sample&lt;/code&gt; demonstrates compliant patterns and deliberate violations.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your rationale.&lt;/strong&gt; The &lt;code&gt;CONVENTIONS.md&lt;/code&gt; file explains why specific rules exist.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use versioning semantics.&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Minor version → new rules or non-breaking changes
&lt;/li&gt;
&lt;li&gt;Major version → breaking rule severity or enforcement changes
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Treat standards packages as &lt;strong&gt;infrastructure&lt;/strong&gt;, not libraries. They’re a versioned part of your build system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Build-transitive packages are the cleanest way to enforce organization-wide build and analyzer standards.
&lt;/li&gt;
&lt;li&gt;Default-on enforcement guarantees consistency.
&lt;/li&gt;
&lt;li&gt;Gradual adoption keeps legacy code manageable.
&lt;/li&gt;
&lt;li&gt;Documentation matters as much as enforcement.
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PrivateAssets="all"&lt;/code&gt; prevents downstream dependency pollution.
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"HoneyDrunk.Standards"&lt;/span&gt; &lt;span class="na"&gt;PrivateAssets=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line brings your organization in line with modern, deterministic .NET build standards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/HoneyDrunkStudios/HoneyDrunk.Standards" rel="noopener noreferrer"&gt;github.com/HoneyDrunkStudios/HoneyDrunk.Standards&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; Conventions guide, naming policies, nullable handling, and migration strategies.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have you implemented your own internal standards package? What challenges did you hit during rollout? Share them below — the goal is a future where “code style review” isn’t a recurring meeting topic.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;🟡 &lt;em&gt;Written by &lt;a href="https://tatteddev.com" rel="noopener noreferrer"&gt;TattedDev&lt;/a&gt; — building the HoneyDrunk ecosystem, one node at a time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>devops</category>
      <category>codequality</category>
    </item>
    <item>
      <title>HoneyDrunk.Pipelines — Treat Your CI/CD as Infrastructure</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sat, 11 Oct 2025 21:02:40 +0000</pubDate>
      <link>https://dev.to/tatted_dev/honeydrunkpipelines-treat-your-cicd-as-infrastructure-38p2</link>
      <guid>https://dev.to/tatted_dev/honeydrunkpipelines-treat-your-cicd-as-infrastructure-38p2</guid>
      <description>&lt;p&gt;When you’re a small team—or even a solo dev—the friction of managing CI/CD pipelines across multiple repos adds up fast.&lt;br&gt;&lt;br&gt;
A missing permission, a stale PAT, or a malformed GUID can break your release.  &lt;/p&gt;

&lt;p&gt;That’s what pushed me to think differently: &lt;strong&gt;treat pipeline configuration like infrastructure you version, maintain, and compose&lt;/strong&gt;.  &lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through how I built &lt;strong&gt;HoneyDrunk.Pipelines&lt;/strong&gt;, a centralized, reusable template system for Azure DevOps, and the lessons that came out of it.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 The Problem: Pipeline Sprawl &amp;amp; Runtime Breakage
&lt;/h2&gt;

&lt;p&gt;Before building a unified system, here’s what I was dealing with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every repo had its own &lt;code&gt;azure-pipelines.yml&lt;/code&gt;, each slightly different
&lt;/li&gt;
&lt;li&gt;Copy-pasted feed GUIDs, mismatched naming, and inconsistent versioning
&lt;/li&gt;
&lt;li&gt;Permission and service connection issues that only surfaced at runtime
&lt;/li&gt;
&lt;li&gt;Changing SDK versions or build logic meant updating several repos manually
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single pipeline failure captured the pain perfectly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The build service identity didn’t have &lt;strong&gt;Contributor&lt;/strong&gt; on the feed
&lt;/li&gt;
&lt;li&gt;The service connection used a stale PAT
&lt;/li&gt;
&lt;li&gt;The feed URL (with GUID) was mis-typed
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these were independent problems that only appeared &lt;strong&gt;after&lt;/strong&gt; I pushed.&lt;br&gt;&lt;br&gt;
That’s when I decided: pipelines can’t be snowflakes anymore.&lt;/p&gt;


&lt;h2&gt;
  
  
  ⚙️ The Vision: Pipelines as Reusable Infrastructure
&lt;/h2&gt;

&lt;p&gt;I wrote down a few core principles before rebuilding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single source of truth.&lt;/strong&gt; One repo for templates, many consumers
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned logic.&lt;/strong&gt; Pipeline changes are reviewed and auditable
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable building blocks.&lt;/strong&gt; Stages, jobs, and steps can be mixed and matched
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of concerns.&lt;/strong&gt; Projects define &lt;em&gt;what&lt;/em&gt; to build; templates define &lt;em&gt;how&lt;/em&gt; to build
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🗂️ Repository Layout (Simplified)
&lt;/h3&gt;

&lt;p&gt;HoneyDrunk.Pipelines/&lt;br&gt;
├── stages/&lt;br&gt;
├── jobs/&lt;br&gt;
├── steps/&lt;br&gt;
└── README.md&lt;/p&gt;

&lt;p&gt;ConsumerRepo/&lt;br&gt;
└── azure-pipelines.yml # references templates from HoneyDrunk.Pipelines&lt;/p&gt;

&lt;p&gt;Consumer repos reference the template repo via Azure DevOps resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repositories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pipelines&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HoneyDrunk/HoneyDrunk.Pipelines&lt;/span&gt;
      &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;refs/heads/main&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stages/pr-validation.stage.yaml@pipelines&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;projectPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/MyLib/MyLib.csproj'&lt;/span&gt;
      &lt;span class="na"&gt;runTests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stages/dotnet-publish.stage.yaml@pipelines&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PR_Validation&lt;/span&gt;
      &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))&lt;/span&gt;
      &lt;span class="na"&gt;packagePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$(Build.ArtifactStagingDirectory)/**/*.nupkg'&lt;/span&gt;
      &lt;span class="na"&gt;feedName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HoneyDrunk-Internal'&lt;/span&gt;


&lt;span class="s"&gt;That’s about 10 lines of YAML per project — the rest lives in the infrastructure layer.&lt;/span&gt;

&lt;span class="s"&gt;🧩 Core Template Mechanics&lt;/span&gt;
&lt;span class="s"&gt;PR Validation Stage&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projectPath&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configuration&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Release'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runTests&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;boolean&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnetVersion&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9.x'&lt;/span&gt;

&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PR_Validation&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Build,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Test,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Validate'&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ubuntu-latest'&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../steps/install-dotnet-sdk.step.yaml&lt;/span&gt;
            &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;dotnetVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.dotnetVersion }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../steps/dotnet-restore-build.step.yaml&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../steps/dotnet-build.step.yaml&lt;/span&gt;
            &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;projectPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.projectPath }}&lt;/span&gt;
              &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.configuration }}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${{ if eq(parameters.runTests, &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt;) }}&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../steps/dotnet-test.step.yaml&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../steps/dotnet-pack.step.yaml&lt;/span&gt;
            &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;projectPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.projectPath }}&lt;/span&gt;
              &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.configuration }}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step is modular, reusable, and easy to evolve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;Package Publish Step&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;packagePath&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;feedName&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serviceConnection&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HoneyDrunk-AzureDevOps'&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NuGetAuthenticate@1&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authenticate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Artifacts'&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nuGetServiceConnections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.serviceConnection }}&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DotNetCoreCLI@2&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Push&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;parameters.feedName&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;push'&lt;/span&gt;
      &lt;span class="na"&gt;packagesToPush&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ parameters.packagePath }}&lt;/span&gt;
      &lt;span class="na"&gt;nuGetFeedType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;internal'&lt;/span&gt;
      &lt;span class="na"&gt;publishVstsFeed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HoneyDrunk/${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;parameters.feedName&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
      &lt;span class="na"&gt;allowPackageConflicts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to avoid brittle GUIDs, this script dynamically builds the feed URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;FEED_URL="https://pkgs.dev.azure.com/$(System.TeamFoundationCollectionUri | sed 's|https://dev.azure.com/||')/_packaging/${{ parameters.feedName }}/nuget/v3/index.json"&lt;/span&gt;
    &lt;span class="s"&gt;echo "##vso[task.setvariable variable=FeedUrl]$FEED_URL"&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Compute&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Feed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;URL'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more copy-paste errors, no more mismatched feed identifiers.&lt;/p&gt;

&lt;p&gt;🧰 Troubleshooting &amp;amp; Lessons Learned&lt;br&gt;
Permission Setup&lt;/p&gt;

&lt;p&gt;Ensure your Azure Pipelines build identity (Project Build Service) has Contributor access to the feed.&lt;br&gt;
Without it, pushes will silently fail.&lt;/p&gt;

&lt;p&gt;Service Connections&lt;/p&gt;

&lt;p&gt;Use NuGetAuthenticate@1 instead of personal PATs wherever possible — it leverages the build service identity.&lt;/p&gt;

&lt;p&gt;Parameterization&lt;/p&gt;

&lt;p&gt;Anything that can change — paths, feed names, versions — should be a parameter.&lt;br&gt;
Hardcoded values are time bombs.&lt;/p&gt;

&lt;p&gt;🚀 The Impact&lt;/p&gt;

&lt;p&gt;✅ Shared templates mean all projects follow the same structure&lt;br&gt;
✅ Versioning and naming stay consistent&lt;br&gt;
✅ Permission errors are practically gone&lt;br&gt;
✅ Onboarding a new project takes minutes&lt;/p&gt;

&lt;p&gt;⚖️ Before vs After&lt;/p&gt;

&lt;p&gt;Before&lt;/p&gt;

&lt;p&gt;Copy-paste massive YAML files&lt;/p&gt;

&lt;p&gt;Search &amp;amp; replace project names&lt;/p&gt;

&lt;p&gt;Debug GUIDs and PATs by trial and error&lt;/p&gt;

&lt;p&gt;After&lt;/p&gt;

&lt;p&gt;Reference templates&lt;/p&gt;

&lt;p&gt;Set parameters&lt;/p&gt;

&lt;p&gt;Push → it just works&lt;/p&gt;

&lt;p&gt;🐝 Why It Matters for Small Teams&lt;/p&gt;

&lt;p&gt;For indie devs or small studios like mine:&lt;/p&gt;

&lt;p&gt;Time is precious. Every failed build costs momentum&lt;/p&gt;

&lt;p&gt;Context switching is brutal. You shouldn’t be debugging YAML daily&lt;/p&gt;

&lt;p&gt;Future-you will forget. Document it once, reuse forever&lt;/p&gt;

&lt;p&gt;Treating pipelines as infrastructure is how you scale solo work.&lt;/p&gt;

&lt;p&gt;🔮 Next Steps&lt;/p&gt;

&lt;p&gt;Auto-generate release notes from commits&lt;/p&gt;

&lt;p&gt;Add semantic versioning via conventional commits&lt;/p&gt;

&lt;p&gt;Extend templates to handle multiple languages and deployment targets&lt;/p&gt;

&lt;p&gt;Build once. Reuse forever.&lt;br&gt;
That’s how HoneyDrunk Studios automates the grid.&lt;/p&gt;

&lt;p&gt;🧱 Originally published on &lt;a href="https://www.tatteddev.com/blog/honeypipelines-automating-the-grid/" rel="noopener noreferrer"&gt;TattedDev.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ci</category>
      <category>devops</category>
      <category>azure</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
