<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ali Vahidinasab — Solution Architect &amp; Full-Stack Engineer</title><description>Writing on solution architecture and full-stack engineering by Ali Vahidinasab.</description><link>https://vahidinasab.com/</link><language>en</language><item><title>A coding CLI becomes a service when sessions have owners</title><link>https://vahidinasab.com/articles/a-coding-cli-becomes-a-service-when-sessions-have-owners</link><guid isPermaLink="true">https://vahidinasab.com/articles/a-coding-cli-becomes-a-service-when-sessions-have-owners</guid><description>Turning a coding agent into a shared service makes session ownership, reconnect behavior, authorization and cleanup part of the system contract.</description><pubDate>Mon, 26 Oct 2026 07:07:36 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-process-is-not-a-product-boundary&quot;&gt;A process is not a product boundary&lt;/h2&gt;
&lt;p&gt;When I run a coding agent in a terminal, the boundary feels obvious: I start it, give it a task, and watch what it does. When the agent runs as a background service with an API and SDK, that boundary moves. The agent can outlive the terminal that started it, and another client can interact with its session. Now I have to decide who owns that work and what it means for a client to attach, disappear, or come back.&lt;/p&gt;
&lt;p&gt;That is the architectural change I see in &lt;a href=&quot;https://anoma.ly/notes/opencode-2-0&quot;&gt;OpenCode 2.0’s announcement&lt;/a&gt;: the project describes a background service, an API and SDK, and Code Mode. The interesting shift is not simply that a CLI can be driven by another program. It is that the agent has become a service with a lifecycle beyond one interactive process.&lt;/p&gt;
&lt;p&gt;I would not infer from “shared service” that a deployment is automatically a safe multi-user service. Sharing among a person’s clients and sharing across an organization are different contracts. The first may mean a terminal, desktop app, and web client connect to one local agent. The second introduces questions about identity, tenant boundaries, and which workspace a user is allowed to reach. Those distinctions need to be designed, not assumed from the presence of an API.&lt;/p&gt;
&lt;p&gt;My starting position is simple: a session belongs to an explicitly identified owner, and a client connection is not that owner. If I blur those concepts, I make it hard to reason about who can resume work, who can approve an action, and who is responsible for cleaning up the resources it leaves behind.&lt;/p&gt;
&lt;h2 id=&quot;a-session-needs-an-owner-not-just-an-id&quot;&gt;A session needs an owner, not just an ID&lt;/h2&gt;
&lt;p&gt;An identifier tells a client which session it is asking about. It does not, by itself, tell the service whether that client is allowed to read, change, or stop the session. I would treat session ownership as a record the service can check on every operation, rather than as a convention in the user interface.&lt;/p&gt;
&lt;p&gt;For a single-user local service, the owner might be the local user and the project directory might further bound the session. For a hosted service, I would expect an authenticated principal and an explicit workspace or tenant boundary. Those are different deployments; I would not quietly move from one to the other while keeping the same assumptions about trust.&lt;/p&gt;
&lt;p&gt;A useful contract separates the concepts, even if the first implementation is small:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;// Illustrative service contract, not an OpenCode API example.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; SessionRecord&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  id&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  ownerId&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  workspaceId&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  state&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;running&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;waiting&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;finished&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;cancelled&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ClientConnection&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  principalId&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  connectionId&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt; canAccessSession&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  client&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ClientConnection&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  session&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; SessionRecord&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; client.principalId &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; session.ownerId&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important point is not this particular type shape. It is that session ownership remains true when the initiating client disconnects. If I instead tie ownership to a socket, browser tab, or terminal process, a temporary network interruption starts to look like an authorization change. That is the wrong coupling.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://opencode.ai/docs/sdk&quot;&gt;OpenCode SDK documentation&lt;/a&gt; exposes session operations such as creating, listing, retrieving, and deleting sessions, alongside APIs for handling permission requests. That surface is useful because it makes the session a managed resource rather than an invisible side effect of a command. It also makes the caller’s authority consequential: an API that can list or delete sessions needs the same deliberate access policy as one that sends a prompt.&lt;/p&gt;
&lt;h2 id=&quot;reconnection-is-a-consistency-decision&quot;&gt;Reconnection is a consistency decision&lt;/h2&gt;
&lt;p&gt;I would expect clients to disconnect. Laptops sleep, browser tabs reload, and network paths fail. Those are ordinary operating conditions, not exceptional cases that justify throwing away the session.&lt;/p&gt;
&lt;p&gt;So I would define reconnection as: authenticate again, prove access to the existing session, obtain its current state, then resume observing it. I would not let reconnect mean “start the prompt again.” Retrying a request that may already have reached the agent can duplicate work or produce a second set of side effects. A client needs a way to distinguish a new command from a retry of an earlier command; an idempotency key or another explicit request identity is one design option.&lt;/p&gt;
&lt;p&gt;I would also separate durable state from live delivery. A stream of events is useful while connected, but it is not a durable history. If the client misses events, it needs a way to recover from the service’s current session state or a stored event position. Otherwise the UI can show a stale “running” status after the agent has stopped, or miss a permission request it must answer.&lt;/p&gt;
&lt;p&gt;The service therefore needs to define which state survives a client reconnect, which state survives a service restart, and what a client can infer when it cannot get a definitive answer. “The connection dropped” is not a session state. I would make the service answer whether the session is running, waiting for input or permission, finished, or cancelled—and make the client handle each case without guessing.&lt;/p&gt;
&lt;h2 id=&quot;authorization-belongs-at-the-action-boundary&quot;&gt;Authorization belongs at the action boundary&lt;/h2&gt;
&lt;p&gt;A session may have access to a repository, tools, or credentials. A client that can send it instructions can potentially cause those capabilities to be used. That makes authorization more than a check when the session is created. I would check it when the client reads session data, sends a prompt, responds to a permission request, changes settings, or cancels work.&lt;/p&gt;
&lt;p&gt;I would keep the authority to perform an action distinct from the authority to approve it. If the same automated client can both initiate a sensitive operation and silently satisfy every approval gate, the gate is not doing useful work. The right policy depends on the deployment, but the boundary should be explicit and testable.&lt;/p&gt;
&lt;p&gt;Code Mode makes this consideration more important, not less. OpenCode’s &lt;a href=&quot;https://anoma.ly/notes/opencode-2-0&quot;&gt;2.0 announcement&lt;/a&gt; describes it as a way for the agent to compose tool calls through code. I see that as a change in orchestration: fewer isolated interactions may be packaged into a larger operation. That can be useful, but it means I should inspect the permissions of the tools and resources reachable through the service, rather than assuming that a friendly client interface is a security boundary.&lt;/p&gt;
&lt;p&gt;For an organization, I would begin with a narrow service identity and explicit workspace scope. I would not expose a service endpoint to a broader network and rely on possession of a session ID as the access policy. Nor would I assume that “the user is logged into the client” proves that every request arriving at the service is that user. Authentication, authorization, and network exposure are separate parts of the contract.&lt;/p&gt;
&lt;h2 id=&quot;lifecycle-is-part-of-the-api&quot;&gt;Lifecycle is part of the API&lt;/h2&gt;
&lt;p&gt;If sessions can outlive clients, the service needs a policy for when they stop. I would define who can cancel a running task, what happens when an owner is removed, and whether finished sessions are retained or deleted. Leaving all of that implicit turns an implementation detail into an accidental retention and resource policy.&lt;/p&gt;
&lt;p&gt;The workspace boundary needs equal care. A session that refers to a working directory cannot safely be treated as a free-floating conversation if the directory disappears, moves, or becomes available to a different principal. I would have the service validate that the workspace is still available and authorized before resuming work, rather than trusting a path remembered by an earlier client.&lt;/p&gt;
&lt;p&gt;I would design the first version for one clear ownership model, then add broader sharing only when there is a real requirement. A local service owned by one user is a coherent product. A team service needs explicit membership, role decisions, auditability, and cleanup rules. Calling both “the same agent with an API” hides the additional system I am taking responsibility for.&lt;/p&gt;
&lt;p&gt;That is the operational contract I would ask for before treating a coding CLI as a shared service: sessions have durable owners, reconnects recover state without replaying work, authorization is checked where actions happen, and lifecycle rules say who can stop and clean up. The SDK makes integration possible; the contract is what makes that integration safe to operate.&lt;/p&gt;</content:encoded><category>architecture</category><category>ai-assisted-engineering</category><category>opencode</category></item><item><title>Let durable orchestration earn its place</title><link>https://vahidinasab.com/articles/let-durable-orchestration-earn-its-place</link><guid isPermaLink="true">https://vahidinasab.com/articles/let-durable-orchestration-earn-its-place</guid><description>I explain how to decide whether long-running work needs persisted progress and recovery or is adequately handled by a queue and consumer.</description><pubDate>Mon, 26 Oct 2026 07:07:33 GMT</pubDate><content:encoded>&lt;h2 id=&quot;start-with-the-failure-you-need-to-recover-from&quot;&gt;Start with the failure you need to recover from&lt;/h2&gt;
&lt;p&gt;A long-running task is not automatically an orchestration problem. I would first ask what must still be true if the process handling it stops halfway through.&lt;/p&gt;
&lt;p&gt;If the work is a single, independent operation, a queue and a consumer may be enough. The consumer receives a message, performs the operation, and acknowledges the message only when its chosen success condition is met. If it fails, the system needs a defined way to retry or route the message for investigation. The operation also needs to tolerate being attempted again, or have a way to recognize that it has already taken effect.&lt;/p&gt;
&lt;p&gt;That is not a lesser design. It is a smaller one. The queue holds work until a consumer can act on it; the consumer owns the operation. I do not need a workflow history just because processing is asynchronous or might be retried.&lt;/p&gt;
&lt;p&gt;The case changes when the work is a sequence of steps whose progress matters. Suppose a process must request approval, wait for a person, and then continue with a later action. If a process restart should not make me reconstruct whether approval was requested, received, or acted on, then persisted progress is part of the requirement. A durable orchestrator can own that state and coordinate the next action after a delay or failure.&lt;/p&gt;
&lt;p&gt;The .NET blog’s &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/durable-task-in-the-real-world/&quot;&gt;October 12, 2026 article on problems durable task actually solves&lt;/a&gt; is a useful prompt for this question. I would not start by asking whether a particular durable-task framework is interesting. I would ask which recovery responsibility the system actually needs.&lt;/p&gt;
&lt;h2 id=&quot;keep-the-responsibilities-visible&quot;&gt;Keep the responsibilities visible&lt;/h2&gt;
&lt;p&gt;A queue-and-consumer design separates the producer, the message transport, and the handler. That separation is useful when a message describes work that can be handled on its own. The consumer decides how to execute that work, and the queue provides a place for work to wait. Retry policy, failure inspection, and idempotency still need deliberate design; the queue does not make those questions disappear.&lt;/p&gt;
&lt;p&gt;An orchestrator has a different job. It owns a process that spans steps and time: which step is current, what result allows the process to advance, and what should happen after interruption. Activities can still run as messages or independent workers. Orchestration does not replace messaging so much as give a larger process a durable owner.&lt;/p&gt;
&lt;p&gt;I find this boundary easier to see as a division of responsibility:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  accTitle: Queue work and orchestrated work
  accDescr: Independent work goes from a producer to a queue and consumer, while multi-step work is tracked by an orchestrator that invokes activities.
  Producer --&gt; Queue
  Queue --&gt; Consumer
  Request --&gt; Orchestrator
  Orchestrator --&gt; Activity
  Activity --&gt; Orchestrator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The diagram is a design distinction, not a mandate to introduce two separate platforms. A system might use the same messaging infrastructure to dispatch activities and still have an orchestration component tracking the larger process. What matters is that I can point to the owner of progress. If a request is partway through several steps, some component must be able to say what has happened and what should happen next.&lt;/p&gt;
&lt;p&gt;For a standalone task, that owner can simply be the consumer plus the data it writes. For a process with meaningful intermediate state, leaving progress implicit across handlers and messages can make recovery hard to reason about. I would add orchestration when the process itself needs to be durable, not merely because one of its steps uses a queue.&lt;/p&gt;
&lt;h2 id=&quot;make-the-extra-machinery-pay-for-itself&quot;&gt;Make the extra machinery pay for itself&lt;/h2&gt;
&lt;p&gt;Durable orchestration has a cost. I need to model the process as state and steps, decide what results are persisted, and account for how changes to the process interact with work already in flight. I also need to operate and inspect that state. Those are reasonable costs when they buy reliable recovery across a process that matters. They are overhead when the process is only a single handler doing work that can safely be retried.&lt;/p&gt;
&lt;p&gt;A queue-and-consumer design has costs too. The application must decide what a retry means, how it avoids repeating side effects, and how someone investigates work that cannot succeed. If a business process is spread across several consumers, each with its own local assumptions about what has completed, the system may be difficult to recover even though every individual message is durable. Adding more messages does not by itself give the overall process an owner.&lt;/p&gt;
&lt;p&gt;I would therefore distinguish between retrying an operation and resuming a process. A failed operation may be put back into the work stream and attempted again. A process that needs to resume must also know where it was, what prior steps established, and which later action is valid. Those are related needs, but they are not interchangeable.&lt;/p&gt;
&lt;p&gt;Here is the kind of boundary I would aim to make explicit in code. This is illustrative application code, not an API from a particular orchestration product:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; Task&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt; HandleAsync&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;GenerateReport&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; message&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;CancellationToken&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; cancellationToken&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; report&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; reportGenerator.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;GenerateAsync&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        message.ReportId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        cancellationToken);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; reportStore.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;SaveAsync&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(report, cancellationToken);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; messageAcknowledger.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AcknowledgeAsync&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(message, cancellationToken);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This handler expresses one operation and its completion boundary: save the result before acknowledging the work. If the actual requirement is to wait for a decision, remember that decision through a restart, and then carry out a later step, I would not hide that process in an increasingly elaborate handler. I would give it an explicit durable owner and let the handler remain responsible for its activity.&lt;/p&gt;
&lt;h2 id=&quot;choose-the-smallest-honest-owner&quot;&gt;Choose the smallest honest owner&lt;/h2&gt;
&lt;p&gt;I would choose a queue and consumer when work is independent, its completion can be described at the operation boundary, and the surrounding system can handle retry and duplicate attempts safely. I would choose durable orchestration when progress through multiple steps must survive process loss and the next action depends on that recorded progress.&lt;/p&gt;
&lt;p&gt;The uncomfortable middle is a workflow built from messages where nobody owns the whole workflow. It can be correct, but only if the state and recovery rules are explicit somewhere. If that means a coordinator, a database record, and carefully designed consumers, that may be a good choice. If those pieces amount to rebuilding orchestration without naming it, I would rather use an orchestrator and accept its operational cost openly.&lt;/p&gt;
&lt;p&gt;I would not route every asynchronous interaction through durable orchestration. I would use it for work whose progress deserves to be remembered; for the rest, a queue and a consumer are often the more direct design. The boundary is not about which pattern is more advanced. It is about who must know what happened when the process stops.&lt;/p&gt;</content:encoded><category>architecture</category><category>messaging</category><category>distributed-systems</category></item><item><title>Give alert triage its own token permissions</title><link>https://vahidinasab.com/articles/give-alert-triage-its-own-token-permissions</link><guid isPermaLink="true">https://vahidinasab.com/articles/give-alert-triage-its-own-token-permissions</guid><description>A dedicated GitHub Actions workflow can read Dependabot alerts without inheriting the broader permissions used to build, publish, or deploy software.</description><pubDate>Mon, 26 Oct 2026 07:07:30 GMT</pubDate><content:encoded>&lt;h2 id=&quot;separate-reading-alerts-from-shipping-software&quot;&gt;Separate reading alerts from shipping software&lt;/h2&gt;
&lt;p&gt;A workflow that classifies Dependabot alerts should not need the authority of a workflow that publishes a package. I would make the alert workflow’s permission explicit, then keep it separate from build and release jobs. GitHub’s early September 2026 Actions update added a read-only &lt;code&gt;vulnerability-alerts&lt;/code&gt; permission for &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; (&lt;a href=&quot;https://github.blog/changelog/2026-09-03-github-actions-early-september-2026-updates&quot;&gt;GitHub’s early September 2026 Actions update&lt;/a&gt;). That gives alert triage a narrower option than granting broader repository permissions just to retrieve vulnerability data.&lt;/p&gt;
&lt;p&gt;The design principle is straightforward: give a task the access its purpose requires, not the access a neighboring task happens to need. A triage workflow needs to read alert details. A publishing workflow may need to write packages. Combining those responsibilities means the token for one job can carry permissions that have nothing to do with the other job’s work.&lt;/p&gt;
&lt;p&gt;This is a boundary I would express in the workflow file, not leave to a convention about which steps are “supposed” to use a token. GitHub documents that actions may access &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; through the &lt;code&gt;github.token&lt;/code&gt; context even when the workflow does not explicitly pass it to them, so limiting permissions matters even when a token is not an obvious action input (&lt;a href=&quot;https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs&quot;&gt;GitHub’s guidance on using &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;grant-only-the-permission-the-job-needs&quot;&gt;Grant only the permission the job needs&lt;/h2&gt;
&lt;p&gt;A small manually triggered workflow makes the permission boundary visible. It lists alerts through GitHub CLI; the token is supplied to the CLI through its supported &lt;code&gt;GH_TOKEN&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Triage Dependabot alerts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  workflow_dispatch&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  triage&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      vulnerability-alerts&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;List Dependabot alerts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        env&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;          GH_TOKEN&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;${{ github.token }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;gh api --paginate &quot;repos/${GITHUB_REPOSITORY}/dependabot/alerts&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important line is not the API command; it is the permission declaration next to the job that uses it. GitHub’s workflow syntax says &lt;code&gt;vulnerability-alerts: read&lt;/code&gt; permits listing Dependabot alerts, and supports only &lt;code&gt;read&lt;/code&gt; and &lt;code&gt;none&lt;/code&gt; for this permission—not &lt;code&gt;write&lt;/code&gt; (&lt;a href=&quot;https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions&quot;&gt;GitHub’s workflow syntax for &lt;code&gt;vulnerability-alerts&lt;/code&gt;&lt;/a&gt;). Since the permission is scoped to the job, it describes what this job needs rather than silently extending a broad workflow-level grant to unrelated jobs.&lt;/p&gt;
&lt;p&gt;There is a useful consequence to declaring permissions explicitly: GitHub documents that when you specify individual permissions, unspecified permissions are set to &lt;code&gt;none&lt;/code&gt; (&lt;a href=&quot;https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions&quot;&gt;GitHub’s workflow syntax for token permissions&lt;/a&gt;). In this example, the triage job does not need repository contents, package publishing, or deployment permissions. If I later add a step that genuinely needs another capability, I must add that permission deliberately. That makes a change in authority visible in review alongside the code that requires it.&lt;/p&gt;
&lt;p&gt;The example is intentionally read-only. Listing alerts is a useful starting point for triage, but it does not claim to close alerts, create issues, or notify another system. Those are different actions and may require different permissions. I would add them only after deciding what the workflow should change and identifying the narrowest permission for that change. The &lt;a href=&quot;https://docs.github.com/en/rest/dependabot/alerts&quot;&gt;Dependabot alerts REST API documentation&lt;/a&gt; distinguishes listing alerts from updating an alert and describes the permissions required for those API operations.&lt;/p&gt;
&lt;h2 id=&quot;keep-triage-apart-from-build-and-release-authority&quot;&gt;Keep triage apart from build and release authority&lt;/h2&gt;
&lt;p&gt;I would not add alert triage as another step inside a release workflow simply because that workflow already has a token. A release pipeline has a different job: it may build artifacts, publish packages, or deploy software. Reading a vulnerability alert and publishing an artifact are separate capabilities. Keeping them in separate workflows lets each declare a permission set tied to its purpose.&lt;/p&gt;
&lt;p&gt;That separation is not a claim that every shared workflow is unsafe. It is a design choice about blast radius and review. If a triage action or its script behaves unexpectedly, a read-only alert token is a more limited authority than a token that can also publish or modify repository contents. If a release step needs write access, I would grant that access to the release job that needs it—not to a general-purpose workflow that happens to contain alert handling too.&lt;/p&gt;
&lt;p&gt;This is also why I prefer permissions close to the job rather than relying on a repository-wide default to communicate intent. GitHub allows permissions at workflow or job level, and recommends granting the minimum access required (&lt;a href=&quot;https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs&quot;&gt;GitHub’s &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permission guidance&lt;/a&gt;). A job-level declaration gives reviewers a compact answer to “what can this part of the pipeline do?” It also gives a reusable pipeline a clearer contract: the caller can see which jobs read security alerts and which jobs need authority to ship software.&lt;/p&gt;
&lt;h2 id=&quot;treat-the-permission-as-part-of-the-workflow-contract&quot;&gt;Treat the permission as part of the workflow contract&lt;/h2&gt;
&lt;p&gt;Adding &lt;code&gt;vulnerability-alerts: read&lt;/code&gt; does not by itself make a workflow safe. The workflow still runs code and actions, and the token is available to steps that can access the GitHub context. I would keep the job’s steps focused, avoid checking out repository code if the task does not need it, and review any third-party action as code that runs within the job’s permission boundary. GitHub’s security guidance recommends limiting &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions and notes that actions can access the token through the context (&lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/reference/security/secure-use&quot;&gt;GitHub’s secure use reference&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I would also keep the meaning of “triage” narrow. If triage means read, classify, and produce a report, then read-only access is an appropriate boundary. If it also means editing issues or closing alerts, that is a new requirement, not a reason to give the workflow broad permissions pre-emptively. I would split the read and write steps into separate jobs where practical, and give the write job only the additional permission its output requires.&lt;/p&gt;
&lt;p&gt;For teams using reusable pipelines across many repositories, this permission is especially useful as an explicit interface. The reusable workflow can state that the alert-reading job needs &lt;code&gt;vulnerability-alerts: read&lt;/code&gt;; repository owners can review that request without granting the same authority to every build or deployment job. The point is not to make permissions complicated. It is to stop unrelated capabilities from accumulating in one token because the pipeline grew one step at a time.&lt;/p&gt;
&lt;p&gt;I would give alert triage its own read-only token permission and keep it out of the path that builds, publishes, or deploys software. The smaller permission is not just a safer setting; it is a clearer description of the job.&lt;/p&gt;</content:encoded><category>github-actions</category><category>security</category><category>ci-cd</category></item><item><title>Treat workflow changes as governed releases</title><link>https://vahidinasab.com/articles/treat-workflow-changes-as-governed-releases</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-workflow-changes-as-governed-releases</guid><description>I explain how to inventory workflow changes across repositories, require trusted review, and keep credentials out of untrusted workflow runs.</description><pubDate>Mon, 26 Oct 2026 07:07:26 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-workflow-change-is-a-code-change&quot;&gt;A workflow change is a code change&lt;/h2&gt;
&lt;p&gt;A GitHub Actions workflow is executable code with access to whatever permissions and credentials its run receives. Treating it as ordinary repository configuration leaves a gap: a workflow can change the behavior of future builds and deployments, while a reviewer may see only a small YAML diff among routine changes.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://thehackernews.com/2026/10/credential-stealing-github-actions.html&quot;&gt;The Hacker News reported that Socket identified credential-stealing workflows committed by more than 500 GitHub accounts to tens of thousands of repositories&lt;/a&gt;. I take the organization-wide change path as the design problem. Cleaning malicious files out of affected repositories matters, but it happens after the workflow has spread. I would instead make workflow changes visible as a distinct class of change and require trusted review before they become part of the path to credentials.&lt;/p&gt;
&lt;p&gt;That does not mean every workflow edit needs a committee. It means that changes under &lt;code&gt;.github/workflows/&lt;/code&gt; deserve a clearly assigned owner, a review requirement that cannot be satisfied by the change author, and a release path that does not hand credentials to an untrusted run.&lt;/p&gt;
&lt;h2 id=&quot;put-the-review-boundary-in-the-protected-branch&quot;&gt;Put the review boundary in the protected branch&lt;/h2&gt;
&lt;p&gt;A practical starting point is &lt;code&gt;CODEOWNERS&lt;/code&gt; for the workflow directory, together with a branch rule that requires code-owner approval. GitHub documents that code-owner reviews can be required for protected branches, and that the &lt;code&gt;CODEOWNERS&lt;/code&gt; file used for review requests must exist on the pull request’s base branch. See &lt;a href=&quot;https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners&quot;&gt;GitHub’s documentation on code owners&lt;/a&gt; and &lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets&quot;&gt;the available rules for rulesets&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# .github/CODEOWNERS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/.github/workflows/ @example-org/ci-security&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The team name is a placeholder; the important part is that the owners are trusted to judge workflow changes and are not just the people who happened to write the application code. Require the review in the protected-branch rule. If the rule is optional, or contributors can merge around it, the file only expresses intent.&lt;/p&gt;
&lt;p&gt;I would also protect the ownership rule itself. A pull request that changes both a workflow and &lt;code&gt;CODEOWNERS&lt;/code&gt; must not be able to remove its own review requirement. Keep branch protections and bypass permissions under a smaller administrative boundary, and review who can bypass them. Those are design recommendations, not properties that a &lt;code&gt;CODEOWNERS&lt;/code&gt; file grants by itself.&lt;/p&gt;
&lt;p&gt;This review boundary controls what can merge. It does not make a workflow safe to execute while a pull request is open. Review and execution are separate controls, and confusing them is an easy way to grant a proposed change access before the required reviewer has approved it.&lt;/p&gt;
&lt;h2 id=&quot;separate-validation-from-credentialed-work&quot;&gt;Separate validation from credentialed work&lt;/h2&gt;
&lt;p&gt;I would make pull-request validation useful but deliberately unprivileged. It can check formatting, compile code, or run tests without receiving deployment credentials. Credentialed work should happen only after the change has crossed the trusted merge boundary, and its credentials should be scoped to the deployment job rather than made available to every job in the workflow.&lt;/p&gt;
&lt;p&gt;A minimal validation workflow might begin like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Validate pull request&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  validate&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@&amp;#x3C;reviewed-full-commit-sha&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet test&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SHA is intentionally a placeholder, not a value to copy. For an actual workflow, I would resolve and review the action’s full commit SHA. &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;GitHub’s secure-use reference recommends pinning actions to a full-length commit SHA&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The example shows the shape of the boundary, not a complete deployment pipeline. In particular, &lt;code&gt;contents: read&lt;/code&gt; is a starting point for this validation job, not a claim that it is the right permission set for every repository. I would add permissions only when a job needs them, and keep publishing or deployment in a separate job with its own narrowly scoped access. If a pull-request check needs a secret to run, I would first ask whether the check can be redesigned to run without it.&lt;/p&gt;
&lt;p&gt;A trusted review of a diff is still necessary: least privilege limits what a bad change can do, but does not make it harmless. Conversely, review is not a substitute for least privilege. One reviewer can miss an added permission, an altered trigger, or a new action. The controls are stronger together because they address different failure modes.&lt;/p&gt;
&lt;h2 id=&quot;make-unexpected-changes-visible-across-repositories&quot;&gt;Make unexpected changes visible across repositories&lt;/h2&gt;
&lt;p&gt;A review rule in each repository is only one part of the system. If workflow policy is copied into many repositories, differences accumulate: a repository may lack the owner rule, have a different workflow directory, or have an exception nobody remembers. I would treat the organization’s workflow estate as something to inventory, not assume that a template rollout made uniform.&lt;/p&gt;
&lt;p&gt;Keep a machine-readable record of each repository’s workflow paths and their current contents. Compare that record with changes proposed and merged, and alert on additions, removals, or edits outside the expected change process. The useful alert is not just “a file changed.” It should identify the repository, the changed workflow, whether the expected owner reviewed it, and whether the change alters triggers, permissions, actions, or credential access. That gives a responder a path to investigate without pretending every workflow edit is an incident.&lt;/p&gt;
&lt;p&gt;For a shared pipeline, I would prefer a small number of centrally maintained reusable workflows over copied implementations, while still making each repository’s call to the shared workflow subject to review. Centralization reduces duplicated policy, but it does not remove the need to govern the caller: a repository can change which workflow it calls, what inputs it passes, or what permissions it grants. I would make those changes visible in the same inventory and review process.&lt;/p&gt;
&lt;p&gt;There is an operational cost. An organization-wide alert can be noisy, and a central workflow team can become a merge bottleneck. I would keep the policy narrow: elevate changes that can alter execution or credential access, assign a clear owner, and document exceptions with an expiry or review point. If the alert cannot tell an engineer what changed and why it matters, it will become background noise.&lt;/p&gt;
&lt;h2 id=&quot;govern-the-path-not-just-the-incident&quot;&gt;Govern the path, not just the incident&lt;/h2&gt;
&lt;p&gt;Secret rotation and repository cleanup are necessary responses when a malicious workflow has run. They do not prevent the next workflow change from crossing the same boundary. My preference is to make the boundary explicit before that happens: inventory workflow files, require trusted review for changes, limit what pull-request runs can access, and reserve credentials for the trusted release path.&lt;/p&gt;
&lt;p&gt;That shifts the question from “How quickly can I clean up one compromised repository?” to “How did this workflow change become acceptable across repositories, and what would have stopped it?” The answer should be enforced in the change path, not left to someone noticing a suspicious YAML diff.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>security</category><category>architecture</category></item><item><title>Claude Code mods are an extension-governance decision</title><link>https://vahidinasab.com/articles/claude-code-mods-extension-governance-decision</link><guid isPermaLink="true">https://vahidinasab.com/articles/claude-code-mods-extension-governance-decision</guid><description>Claude Code mods can replace built-in behavior and interact in load order, so teams need an allow-list, compatibility policy, and combined-configuration tests.</description><pubDate>Mon, 26 Oct 2026 07:07:23 GMT</pubDate><content:encoded>&lt;h2 id=&quot;customization-now-crosses-a-boundary&quot;&gt;Customization now crosses a boundary&lt;/h2&gt;
&lt;p&gt;A customization that changes what Claude Code does is part of the agent’s behavior, not merely a personal preference. That distinction matters when a mod can replace a built-in feature or intercept an event. Anthropic’s &lt;a href=&quot;https://code.claude.com/docs/en/plugins/mods/overview&quot;&gt;mods overview&lt;/a&gt; describes mods as plugins whose handlers can watch, change, or take over events such as prompts, tool calls, and parts of the interface. Anthropic has also moved built-in features into mods, making some built-in behavior replaceable.&lt;/p&gt;
&lt;p&gt;I would treat this as an extension-boundary decision. Once a mod participates in an event flow, its effect can reach beyond the person who installed it. It may change how a prompt is presented, how a tool call is handled, or what a user sees. That creates a new responsibility: deciding which code is allowed to influence the agent’s work, and how the team knows what happens when approved extensions interact.&lt;/p&gt;
&lt;p&gt;The key issue is not whether customization is good. It is who owns the configuration that determines behavior. An individual can reasonably optimize their own setup for speed. A team needs to be able to explain why a shared workflow behaves as it does, and to reproduce that behavior for other people.&lt;/p&gt;
&lt;h2 id=&quot;load-order-is-part-of-the-design&quot;&gt;Load order is part of the design&lt;/h2&gt;
&lt;p&gt;Mods that handle the same event do not necessarily operate independently. Anthropic’s &lt;a href=&quot;https://code.claude.com/docs/en/plugins/mods/events&quot;&gt;event handling documentation&lt;/a&gt; describes how mods are ordered, including the relationship between dependencies and execution. The &lt;a href=&quot;https://code.claude.com/docs/en/plugins/mods/admin&quot;&gt;organization controls documentation&lt;/a&gt; also explains that administrators can decide which mods run and in what order. Order is therefore not a cosmetic installation detail; it is an input to behavior.&lt;/p&gt;
&lt;p&gt;That is where a collection of individually reasonable extensions can become difficult to reason about. One mod may rewrite an input, another may inspect it, and a third may change how a tool call proceeds. The result depends on the combined chain, not just on what each author says the mod does in isolation. If the team cannot see or control that chain, it cannot reliably tell which policy or customization actually took effect.&lt;/p&gt;
&lt;p&gt;I would start with an allow-list for shared use. Each entry should name the mod and its source, identify an owner, state what behavior it is allowed to affect, and record the version the team has reviewed. A mod that is useful for one person can remain a personal choice; it does not automatically belong in a team baseline. And if a mod is allowed to alter an important event, the approval should say why that influence is acceptable.&lt;/p&gt;
&lt;p&gt;The boundary should be explicit about responsibility, too. Anthropic notes that mods run with the permissions of the user who installed them and are not sandboxed in its &lt;a href=&quot;https://code.claude.com/docs/en/plugins/mods/admin&quot;&gt;organization guidance&lt;/a&gt;. That makes provenance and review more than tidy inventory practices. I would not treat a mod as safe merely because it is small, popular, or easy to install. If I would not trust its code to run with that user’s access, I would not approve it for the shared setup.&lt;/p&gt;
&lt;h2 id=&quot;make-compatibility-a-team-policy&quot;&gt;Make compatibility a team policy&lt;/h2&gt;
&lt;p&gt;An allow-list answers “which mods?” It does not answer “which combinations?” I would define a compatibility policy that covers at least three things: what event surfaces a mod may change, which other approved mods it is expected to work alongside, and how the team handles an update that changes behavior or ordering.&lt;/p&gt;
&lt;p&gt;For example, a team could keep a small, human-reviewed inventory alongside its agent configuration. This is a conceptual policy record, not Claude Code settings syntax:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;mods&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;reviewed-mod-id&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    source&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;reviewed-marketplace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    owner&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;platform-team&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    allowed-effects&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;prompt-handling&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    compatible-with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;team-policy-mod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    review-required-on-update&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The value is not this particular format. It is making the decision legible. A record that says only “approved” is weak: it does not tell a reviewer what the mod is trusted to do, who can answer questions about it, or which assumptions might break if it changes. I would keep the list deliberately small and make additions require a reason tied to a team workflow.&lt;/p&gt;
&lt;p&gt;Compatibility should be a property of the configuration as a whole. If the team depends on a particular order, record and review that order rather than assuming that separate approvals compose safely. If two mods both affect the same event, test them together. If a mod has no clear owner or its effect cannot be described, that is a reason to keep it out of the shared baseline until someone can resolve the uncertainty.&lt;/p&gt;
&lt;p&gt;This is a trade-off, not a free safety improvement. An allow-list and review process add friction. They make it slower for an individual to try a new idea in the shared setup. In exchange, the team can say which extensions are part of its workflow and reduce the chance that a quiet customization changes behavior for everyone. I would accept that cost for any configuration the team expects others to rely on.&lt;/p&gt;
&lt;h2 id=&quot;test-the-combination-not-just-the-pieces&quot;&gt;Test the combination, not just the pieces&lt;/h2&gt;
&lt;p&gt;A mod passing its own tests does not show that it behaves correctly beside every other approved mod. I would test the assembled configuration at the points where the extensions meet: the relevant event, the order they run in, and the outcome the team expects. That might mean checking that a policy handler still has its intended effect when another approved handler changes the same event, or that an update has not altered an interaction the team depends on.&lt;/p&gt;
&lt;p&gt;Anthropic documents a testing facility for mods in its &lt;a href=&quot;https://code.claude.com/docs/en/plugins/mods/test&quot;&gt;mod testing guide&lt;/a&gt;, including ways to load mods at different tiers and test with multiple plugins. That is useful because it makes combination testing a concrete part of the development process rather than a manual assumption. I would make those tests run when the approved set changes, and keep the reviewed set and its order visible with the test results.&lt;/p&gt;
&lt;p&gt;I would also make failures actionable. A failing combination should tell the owner which interaction broke and which configuration was under test. If the team cannot reproduce a failure from the recorded set of mods and their order, then the governance record is not complete enough. The aim is not to prove that every extension is harmless in every possible setup; it is to know what the team has chosen to run and to test the interactions it relies on.&lt;/p&gt;
&lt;p&gt;Claude Code mods make customization more powerful, but power changes the ownership question. For personal experimentation, freedom may be the right default. For a shared engineering workflow, I would choose a reviewed allow-list, an explicit compatibility policy, and tests for the combined configuration. Less freedom to customize quickly is a fair price for behavior the team can explain and trust.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>developer-tools</category><category>governance</category></item><item><title>Treat Kubernetes lifecycle data as a compatibility signal</title><link>https://vahidinasab.com/articles/treat-kubernetes-lifecycle-data-as-a-compatibility-signal</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-kubernetes-lifecycle-data-as-a-compatibility-signal</guid><description>A release-note check helps keep generated Kubernetes API lifecycle data from turning into an unverified platform support promise.</description><pubDate>Mon, 26 Oct 2026 07:07:21 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-generated-answer-is-not-a-support-promise&quot;&gt;A generated answer is not a support promise&lt;/h2&gt;
&lt;p&gt;A generated compatibility record can look authoritative because it is structured, versioned, and easy for tooling to consume. That does not make it a complete support statement. I would use API lifecycle data to identify what needs checking, then reconcile it with the release notes before declaring a Kubernetes version supported.&lt;/p&gt;
&lt;p&gt;A recent &lt;a href=&quot;https://github.com/abd-ulbasit/upgradescope/issues/407&quot;&gt;Kubernetes 1.38 readiness issue&lt;/a&gt; makes the distinction concrete. It describes lifecycle data generated from &lt;code&gt;k8s.io/api&lt;/code&gt; that already contains entries for APIs marked as removed in 1.38, while the knowledge base still identifies 1.37 as its newest known version. The issue also notes that the 1.38 release notes are not yet available to verify those entries. That is a useful early signal, but it is not enough evidence to call the data confirmed compatibility information.&lt;/p&gt;
&lt;p&gt;This is a general data-design problem, not a claim that the generated records are wrong. Derived data can be valuable and still be provisional. If a downstream tool quietly treats every generated row as confirmed, it can turn an upstream signal into a stronger promise than its evidence supports. A platform support matrix is not merely a convenient view of source data; it is a statement that people may rely on when planning upgrades.&lt;/p&gt;
&lt;p&gt;I am still learning the deeper mechanics of Kubernetes releases, so I would not present this as authoritative guidance on the release process. The design lesson seems clear without that expertise: distinguish what a source suggests from what the product has verified.&lt;/p&gt;
&lt;h2 id=&quot;keep-evidence-and-status-together&quot;&gt;Keep evidence and status together&lt;/h2&gt;
&lt;p&gt;I would model compatibility as a record with provenance, not as a bare version-to-answer mapping. The entry needs to say where its claim came from, what it has been checked against, and whether it is ready to influence a user-facing support decision.&lt;/p&gt;
&lt;p&gt;For example, a small record might look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;kubernetesVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;1.38&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;apiLifecycleSource&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;k8s.io/api&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;provisional&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;releaseNotesChecked&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;verifiedSources&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: []&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is illustrative, not a prescribed Kubernetes format. The important part is that &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;releaseNotesChecked&lt;/code&gt; are explicit fields rather than assumptions hidden in the generation process. If the record is rebuilt from a newer dependency, the build should not silently upgrade its status from provisional to confirmed. Verification is a separate act with separate evidence.&lt;/p&gt;
&lt;p&gt;I would also keep the generated fact distinct from the product decision. A lifecycle record can say that an API is associated with a removal version. The compatibility check can then report that the version needs review. The platform’s support matrix should change only when the relevant evidence has been reviewed and the resulting policy decision is recorded.&lt;/p&gt;
&lt;p&gt;That separation makes disagreement visible. If generated data and release notes differ, a reviewer sees a discrepancy to resolve instead of a single overwritten value. If the release notes do not yet exist, the system can still expose the early signal while accurately communicating its provisional status. Missing confirmation becomes useful information rather than an awkward hole in an apparently final answer.&lt;/p&gt;
&lt;h2 id=&quot;make-the-check-part-of-the-workflow&quot;&gt;Make the check part of the workflow&lt;/h2&gt;
&lt;p&gt;I would make the reconciliation a gate in the data pipeline, not a reminder in someone’s memory. Generation can happen early and often. Promotion to confirmed should require a deliberate review of the relevant release material, with the evidence stored alongside the result.&lt;/p&gt;
&lt;p&gt;A useful sequence is: generate the lifecycle data, mark new-version entries provisional, compare those entries with the release notes when they are available, record any differences, and then decide whether the support matrix can advance. The &lt;a href=&quot;https://github.com/kubernetes/sig-release/tree/master/releases/release-1.38&quot;&gt;Kubernetes 1.38 release schedule&lt;/a&gt; provides milestones for that cycle, including the planned release date. The schedule is a planning input; the version’s actual release notes remain the evidence to check before making a confirmed claim.&lt;/p&gt;
&lt;p&gt;I would want the system to make the workflow visible in its output. A provisional result should say that lifecycle data exists but has not been reconciled with release notes. A confirmed result should point to the reviewed source and record when the check occurred. If the check finds a mismatch, the result should be blocked or flagged for review rather than choosing whichever source happens to be easiest to parse.&lt;/p&gt;
&lt;p&gt;That can be represented as a small promotion rule:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;compatibilityDecision&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  provisionalWhen&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;lifecycleDataAvailable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;releaseNotesNotChecked&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  confirmedWhen&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;lifecycleDataAvailable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;releaseNotesChecked&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;discrepanciesResolved&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rule is deliberately conservative. It does not say that release notes alone determine every part of platform support. A support promise can depend on testing, provider behavior, add-ons, or an organization’s own policy. It says only that generated lifecycle data should not become a confirmed compatibility claim before it has passed the check the team chose to require.&lt;/p&gt;
&lt;h2 id=&quot;make-uncertainty-legible-to-users&quot;&gt;Make uncertainty legible to users&lt;/h2&gt;
&lt;p&gt;A compatibility tool that only returns supported or unsupported forces uncertain evidence into a false binary. I would rather expose a small vocabulary such as provisional, confirmed, and needs review, with a short explanation of what is missing. That gives an operator a practical next step without pretending the system knows more than it does.&lt;/p&gt;
&lt;p&gt;This distinction matters most when people use the output to plan upgrades. A provisional warning can prompt someone to investigate early. A confirmed warning can support a stronger decision. If both are displayed identically, users cannot tell whether they are seeing a verified change or a prediction based on incomplete evidence. The tool has then hidden the most important fact about its own answer: how much confidence the operator should place in it.&lt;/p&gt;
&lt;p&gt;The official &lt;a href=&quot;https://kubernetes.io/docs/reference/using-api/deprecation-guide#v1-32&quot;&gt;Kubernetes deprecated API migration guide&lt;/a&gt; is organized around APIs removed by release and provides migration information. That kind of release-oriented reference is a useful check against a derived dataset, but the underlying principle applies beyond Kubernetes: a generated index helps find relevant facts; it does not automatically inherit the authority of every source needed to make a support decision.&lt;/p&gt;
&lt;p&gt;I would keep the distinction visible in both the stored data and the user interface. Let generated lifecycle data raise the question. Let a recorded review answer it. Until then, label the result provisional.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>platform-engineering</category><category>architecture</category></item><item><title>Let customers choose the observability backend</title><link>https://vahidinasab.com/articles/let-customers-choose-the-observability-backend</link><guid isPermaLink="true">https://vahidinasab.com/articles/let-customers-choose-the-observability-backend</guid><description>A stable OpenTelemetry export contract lets customers change observability backends without turning your application into a vendor-specific product fork.</description><pubDate>Mon, 26 Oct 2026 07:07:18 GMT</pubDate><content:encoded>&lt;h2 id=&quot;make-export-part-of-the-product-boundary&quot;&gt;Make export part of the product boundary&lt;/h2&gt;
&lt;p&gt;If a customer has to change application code to send telemetry somewhere else, the backend has become part of the product. I would rather define telemetry export as a supported capability: the application produces signals through a stable, vendor-neutral contract, and the customer chooses where those signals go.&lt;/p&gt;
&lt;p&gt;That boundary matters for self-hosted software in particular. Customers may run it in their own data centre, in a cloud account they control, or on a platform that the vendor does not operate. They may also have requirements about where telemetry is stored or which team can access it. The &lt;a href=&quot;https://opentelemetry.io/blog/2026/otel-native-by-design&quot;&gt;OpenTelemetry post on OTel-native product design&lt;/a&gt; makes the same case for products that need to send logs, traces, and metrics to a customer’s observability stack.&lt;/p&gt;
&lt;p&gt;My preference is OpenTelemetry instrumentation in the application and OTLP as the export contract. The application should describe what happened in terms of the product and its operations, not encode assumptions about a particular dashboard, query language, or vendor’s ingestion API. OpenTelemetry’s &lt;a href=&quot;https://opentelemetry.io/docs/languages/dotnet/exporters/&quot;&gt;OTLP exporter documentation for .NET&lt;/a&gt; describes OTLP exporters as designed for the OpenTelemetry data model, which is a useful basis for keeping the application-facing boundary portable.&lt;/p&gt;
&lt;p&gt;Portable does not mean responsibility-free. Once I say a destination is supported, I own the compatibility boundary: the signals, attributes, configuration, and failure behaviour customers can reasonably expect. The customer gets choice; the product team takes on the work of making that choice usable.&lt;/p&gt;
&lt;h2 id=&quot;keep-destination-details-out-of-application-code&quot;&gt;Keep destination details out of application code&lt;/h2&gt;
&lt;p&gt;The application should know how to produce telemetry and how to send it to a configured endpoint. It should not contain a branch for each customer’s backend. Put endpoint, credentials, and transport settings in deployment configuration or in a Collector that the customer operates. Keep credentials out of source control, and make the configuration surface explicit enough to document and validate.&lt;/p&gt;
&lt;p&gt;For example, an application can be configured to send OTLP over HTTP to an endpoint supplied at deployment time:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;https://collector.example.invalid&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;http/protobuf&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hostname here is illustrative, not a real service. The important design choice is that the application’s endpoint can change without rebuilding it. For a product that supports a customer-managed Collector, the application can send to that Collector, and the customer can configure its downstream destination there. The &lt;a href=&quot;https://opentelemetry.io/docs/collector/configuration/&quot;&gt;OpenTelemetry Collector configuration guide&lt;/a&gt; describes pipelines built from receivers, processors, and exporters; that separation gives customers a place to route and process telemetry without adding backend-specific branches to product code.&lt;/p&gt;
&lt;p&gt;I would keep the product’s own configuration contract smaller than the full range of options a Collector can expose. A product might document the endpoint, protocol, authentication mechanism, and whether the customer can choose a Collector. It should not pretend that every Collector extension or backend-specific feature is part of the product’s support promise. A clear, limited contract is easier to test than an unbounded promise of compatibility.&lt;/p&gt;
&lt;p&gt;There is a trade-off here. Sending directly from the application may be simpler to deploy, but puts more destination and credential configuration close to the process. Sending through a customer-managed Collector gives the customer a control point for routing and processing, but introduces another component to operate. I would support the deployment model that fits the product’s trust and operational boundaries, and state plainly which one is covered. I would not silently require a vendor-managed service just because it is the easiest path for the product team.&lt;/p&gt;
&lt;h2 id=&quot;portability-is-not-identical-behaviour&quot;&gt;Portability is not identical behaviour&lt;/h2&gt;
&lt;p&gt;An OTLP endpoint can accept telemetry without making the downstream experience equivalent. A backend may interpret, store, index, or expose signals differently. The customer can still encounter differences in dashboards, alerting, query capabilities, retention, or cost. Those are consequences of the destination, not problems a wire format can erase.&lt;/p&gt;
&lt;p&gt;The contract therefore needs to say more than “we support OpenTelemetry.” It should describe the signals the product emits, the meaning of important attributes, and which fields the product considers stable. OpenTelemetry’s &lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/resource/&quot;&gt;resource specification&lt;/a&gt; describes a Resource as the observed entity associated with telemetry. Consistent resource identity gives a customer a useful way to recognize which service or deployment produced a signal, regardless of the backend receiving it.&lt;/p&gt;
&lt;p&gt;I would also be conservative about changing names or meanings once customers build queries and alerts against them. The OpenTelemetry &lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/versioning-and-stability/&quot;&gt;versioning and stability guidance&lt;/a&gt; explains that semantic conventions form a contract between instrumentation and analysis tools. That is the right mental model for product telemetry too: a field that looks internal to the application may become an input to customer-owned monitoring.&lt;/p&gt;
&lt;p&gt;This is where vendor neutrality can be misunderstood. It does not require the product team to guarantee that every backend renders the same view, nor that every vendor-specific feature has an equivalent elsewhere. It requires the application’s core instrumentation and export path not to depend on one backend. I would document the limits: portable signal delivery is supported; backend-specific queries, dashboards, and alert rules belong to the destination or to separately maintained integrations.&lt;/p&gt;
&lt;h2 id=&quot;test-a-destination-change-not-just-an-endpoint&quot;&gt;Test a destination change, not just an endpoint&lt;/h2&gt;
&lt;p&gt;A test that proves telemetry reaches one endpoint is a start, not a portability test. The useful question is what happens when the customer changes the destination while leaving the application unchanged. I would exercise that change in the product’s integration tests or release checks, using an OTLP-compatible test receiver or Collector and verifying the signals the product promises to emit.&lt;/p&gt;
&lt;p&gt;The checks should cover the application-facing contract: configuration is read from the documented source, the intended signals arrive, resource identity and important attributes are present, and sensitive values are not accidentally included. I would also test the operational edges that matter to the product: a destination that is unavailable, invalid configuration, and a credentials change. The expected outcome should be explicit for each case, such as a clear startup error or a bounded retry policy, rather than a silent loss that customers discover only after an incident.&lt;/p&gt;
&lt;p&gt;A destination-change check can be simple in principle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run the same application build against a test Collector configured for one destination.&lt;/li&gt;
&lt;li&gt;Change only the destination configuration and run it again.&lt;/li&gt;
&lt;li&gt;Compare the received signal names, resource attributes, and product-specific attributes.&lt;/li&gt;
&lt;li&gt;Confirm that the application code and its instrumentation configuration did not need a backend-specific change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That test does not prove compatibility with every backend. It proves something narrower and more valuable: the product’s own export contract survives a change in destination. I would add backend-specific integrations only when customers need them, and keep their support boundaries separate from the portable path.&lt;/p&gt;
&lt;p&gt;Treating telemetry export as a product capability is not a promise that all observability stacks are interchangeable. It is a decision to keep the product’s instrumentation stable, put destination choice at the deployment boundary, and test that boundary when it changes. The cost is owning compatibility and explaining the limits. I would pay that cost rather than make a customer’s observability backend a permanent product fork.&lt;/p&gt;</content:encoded><category>observability</category><category>opentelemetry</category><category>architecture</category></item><item><title>Keep process-metric release candidates out of stability dashboards</title><link>https://vahidinasab.com/articles/keep-process-metric-release-candidates-out-of-stability-dashboards</link><guid isPermaLink="true">https://vahidinasab.com/articles/keep-process-metric-release-candidates-out-of-stability-dashboards</guid><description>A practical way to test OpenTelemetry process-metric release candidates without turning provisional names and attributes into operational contracts.</description><pubDate>Mon, 26 Oct 2026 07:07:15 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-release-candidate-is-a-request-to-test&quot;&gt;A release candidate is a request to test&lt;/h2&gt;
&lt;p&gt;On October 13, 2026, OpenTelemetry announced that Collector Contrib v0.160.0 can emit release-candidate process semantic-convention metrics from the hostmetrics receiver’s process scraper. The announcement describes this as a step toward stable v1 and asks users to try the versioned output and report what breaks (&lt;a href=&quot;https://opentelemetry.io/blog/2026/hostmetrics-process-scraper-rc&quot;&gt;OpenTelemetry’s announcement of RC process metrics in hostmetrics&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I would treat that as an invitation to explore, not a reason to point production dashboards at new metric names. A release candidate can be useful before it is a dependable interface. The risk is not that the metrics are inherently bad; it is that a provisional shape can quietly become a dependency. Once a dashboard, alert, recording rule, or downstream consumer assumes that shape, changing it becomes an operational migration rather than a harmless collector upgrade.&lt;/p&gt;
&lt;p&gt;The distinction I care about is between exploratory telemetry and an operational contract. Exploratory telemetry helps me learn whether a proposed convention describes the system well. An operational contract is what I am willing to have on-call decisions depend on. The same metric can move from one category to the other, but I want that move to be deliberate.&lt;/p&gt;
&lt;h2 id=&quot;inventory-what-depends-on-the-current-output&quot;&gt;Inventory what depends on the current output&lt;/h2&gt;
&lt;p&gt;Before enabling the RC broadly, I would find out what already consumes process telemetry. That means more than grepping dashboards for a metric name. I would inventory the metric names and attributes used by dashboards, alert expressions, recording rules, and any downstream system that receives or transforms the data. I would also check the unit and meaning of each measurement: two metrics with similar names are not interchangeable if they describe different things.&lt;/p&gt;
&lt;p&gt;That inventory matters especially in Prometheus. A time series is identified by its metric name and its set of labels, so a change to either can change what a query selects and what data it sees (&lt;a href=&quot;https://prometheus.io/docs/concepts/data_model/&quot;&gt;Prometheus’s data model&lt;/a&gt;). Prometheus recommends choosing metric and label names deliberately, and notes that labels distinguish characteristics of the thing being measured (&lt;a href=&quot;https://prometheus.io/docs/practices/naming/&quot;&gt;Prometheus metric and label naming guidance&lt;/a&gt;). In practice, a query can stop matching after a rename, or continue matching while accidentally aggregating a changed set of series. Neither outcome is obvious just from the fact that collection is still running.&lt;/p&gt;
&lt;p&gt;I would make a small dependency map: which consumers use which measurements, which attributes they filter or group by, and which of those consumers are operationally important. That gives me a baseline to compare against the RC output. It also exposes dependencies that nobody meant to create—for example, a dashboard panel that has become part of an incident workflow even though its query was originally written just to inspect a new metric.&lt;/p&gt;
&lt;p&gt;I would not assume a collector upgrade is the same thing as a telemetry migration. The OpenTelemetry process conventions cover attributes, metric names, and units, and the conventions page warns existing instrumentation not to adopt breaking changes before the conventions are stable (&lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/system/process-metrics/&quot;&gt;OpenTelemetry’s OS process metrics conventions&lt;/a&gt;). That is a useful way to think about the boundary: verify the actual output and the consumers, rather than treating a version number as proof that the change is safe.&lt;/p&gt;
&lt;h2 id=&quot;give-exploration-its-own-path&quot;&gt;Give exploration its own path&lt;/h2&gt;
&lt;p&gt;The announcement says the RC output is opt-in through a feature gate. In v0.160.0, the gate is alpha and disabled by default; enabling it selects the new versioned output (&lt;a href=&quot;https://opentelemetry.io/blog/2026/hostmetrics-process-scraper-rc&quot;&gt;OpenTelemetry’s hostmetrics RC announcement&lt;/a&gt;). That opt-in is a good place to start a controlled evaluation:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;./otelcontribcol&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --config=collector.yaml&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --feature-gates=scraper.process.EmitV1SystemConventions&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would run that in a canary or other explicitly exploratory path and keep its output separate from the metrics that production dashboards and alerts treat as stable. The exact separation depends on the deployment: it might be a separate collector, a separate scrape target, or another boundary that keeps the RC series out of operational queries. The design goal is simple: a person looking at a production dashboard should not have to know whether a release candidate is enabled somewhere to interpret the chart.&lt;/p&gt;
&lt;p&gt;In that exploratory path, I would compare the RC output with the current output using representative hosts and realistic queries. I would check whether the new metrics convey the information I need, whether attributes support the filters and groupings that matter, and whether the new names and units require query changes. I would also check the downstream route, not just the collector’s own logs: an exporter or translation step can affect what a Prometheus-compatible backend actually sees.&lt;/p&gt;
&lt;p&gt;I would avoid making an experimental dashboard look like an operational one. Label it as an evaluation, keep its queries out of production alerts, and make the source of the data visible. This is not ceremony. It prevents a convenient exploratory panel from being copied into an on-call dashboard and becoming an undocumented promise.&lt;/p&gt;
&lt;h2 id=&quot;promote-the-change-explicitly&quot;&gt;Promote the change explicitly&lt;/h2&gt;
&lt;p&gt;After the comparison, I would write down the decision: adopt the RC output, defer it, or keep testing. If I adopt it, I would treat the change as a migration with named consumers and an owner for each one. I would update dashboards, alerts, recording rules, and downstream queries intentionally, then verify that each still expresses the question it was meant to answer. A query that returns data is not necessarily a query that still means the same thing.&lt;/p&gt;
&lt;p&gt;I would also define what happens to the old output. Keeping both forms indefinitely can make it unclear which one is authoritative; removing the old form before consumers have moved can break the very dashboards the transition is meant to improve. So I would make the overlap and removal conditions explicit, and keep the ability to roll back while the new path is being evaluated. The release-candidate label is a reason to expect feedback and possible change, not a guarantee that a particular name or attribute will remain untouched.&lt;/p&gt;
&lt;p&gt;The key decision is not whether to try the new process conventions. OpenTelemetry has made trying them possible without silently changing the existing configuration. The decision is whether I am ready to make their output part of the operational contract. I would let the RC earn that place through an isolated evaluation and a visible migration—not by letting it drift into a dashboard that promises stability.&lt;/p&gt;</content:encoded><category>opentelemetry</category><category>prometheus</category><category>observability</category><category>architecture</category></item><item><title>Treat agent uncertainty as a review hint</title><link>https://vahidinasab.com/articles/treat-agent-uncertainty-as-a-review-hint</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-agent-uncertainty-as-a-review-hint</guid><description>A coding agent’s uncertainty can help direct review, but tests, diffs, and human judgment—not confidence—must determine whether its work is sound.</description><pubDate>Mon, 26 Oct 2026 07:07:12 GMT</pubDate><content:encoded>&lt;h2 id=&quot;uncertainty-changes-attention-not-the-standard&quot;&gt;Uncertainty changes attention, not the standard&lt;/h2&gt;
&lt;p&gt;A coding agent’s uncertainty is useful only if it changes where I look, not what I accept. A confident answer is not evidence that a patch is correct, and a hesitant answer is not evidence that it is wrong. I would treat either as a prompt for investigation, then judge the work by what I can inspect and verify.&lt;/p&gt;
&lt;p&gt;The distinction matters because an agent’s statement about its own work is still part of the agent’s output. It might point to an uncertain dependency, an untested edge case, or a section where the implementation diverged from the request. That makes the statement potentially useful for directing a review. But it does not independently establish that the concern is real, or that everything outside the flagged area is sound.&lt;/p&gt;
&lt;p&gt;This is the practical question raised by &lt;a href=&quot;https://www.anthropic.com/news/claude-opus-4-8&quot;&gt;Anthropic’s Claude Opus 4.8 announcement&lt;/a&gt;. Anthropic says early testers found the model more likely to flag uncertainty and less likely to make unsupported claims. The announcement also reports an evaluation in which Opus 4.8 was around four times less likely than its predecessor to leave flaws in its own code unremarked. That is a claim about the model and Anthropic’s evaluation—not a guarantee about a particular patch, repository, or review process.&lt;/p&gt;
&lt;p&gt;I would not turn a self-reported uncertainty label into a quality score. I would use it as a map annotation: useful for deciding where to spend scarce attention, but not a substitute for reading the terrain.&lt;/p&gt;
&lt;h2 id=&quot;ask-for-a-review-map-then-inspect-the-work&quot;&gt;Ask for a review map, then inspect the work&lt;/h2&gt;
&lt;p&gt;A coding agent can make a review easier by separating what it changed from what it could not establish. I would ask it to identify relevant files and behavior, describe the reasoning behind the patch, and name the checks it ran or could not run. The point is not to get a persuasive final summary. It is to make the claims in that summary easy to test against the diff and the repository.&lt;/p&gt;
&lt;p&gt;For example, an agent’s handoff might use a structure like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;changed&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Request validation in the endpoint&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uncertain&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Whether the existing tests cover an empty optional field&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;checks&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Focused test command completed&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;not_checked&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Database behavior against a production-like schema&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These entries are prompts, not facts. I would verify that the listed files changed, that the test command really ran, and that its result applies to the behavior under review. If the agent says it did not check something, I can decide whether that gap matters. If it says nothing is uncertain, I still need to decide what the patch could break.&lt;/p&gt;
&lt;p&gt;The diff is a better starting point than the confidence of the explanation. I look at what changed, whether the changes match the requested behavior, and whether they introduce new assumptions. Then I use tests to check specific behavior. A passing test gives evidence about the cases it exercises; it cannot prove that an untested requirement or failure mode is handled. A test the agent reports running is not useful evidence until I can see what ran and what passed.&lt;/p&gt;
&lt;p&gt;That is why I would keep the normal review checks intact. The agent can help me find the tricky branch, but it should not get to skip the diff, the relevant tests, or the checks I expect for that kind of change. Confidence should never be a fast lane around those steps.&lt;/p&gt;
&lt;h2 id=&quot;test-the-usefulness-of-the-signal&quot;&gt;Test the usefulness of the signal&lt;/h2&gt;
&lt;p&gt;I would treat model-reported uncertainty as a workflow experiment, not a calibrated measurement. The first question is modest: does the signal help a reviewer notice useful areas sooner? To answer that, I would compare flagged concerns with observable evidence: the diff, test coverage, review comments, and issues found during the ordinary review. I would also inspect some unflagged areas. Otherwise, I would only learn whether the model can identify problems it has already pointed out, not whether its silence is informative.&lt;/p&gt;
&lt;p&gt;A useful flag should lead to a concrete check. “I am unsure about this change” is too broad to allocate attention well. “I did not verify how this branch behaves when the optional value is absent” points to a question a reviewer can investigate. If the concern turns out to be irrelevant, that is still useful feedback about the signal’s cost. If the patch contains a defect the agent did not flag, that is a reminder that silence cannot close the review.&lt;/p&gt;
&lt;p&gt;I would record the outcome in ordinary review language rather than assigning the agent a grade. Did the flagged area reveal a real gap? Did the diff expose a problem the agent missed? Did the suggested check add useful coverage, or repeat what existing tests already established? Those observations can guide how I ask for handoffs and where I spend attention. They do not justify treating an uncertainty label as a probability of correctness.&lt;/p&gt;
&lt;p&gt;There are limits to what this experiment can show. A reviewer may find something because the agent flagged it, and may overlook something it did not. The prompt, task, repository, and reviewer all shape the result. A reassuring set of reviews would not establish that a model is calibrated across other changes. I would keep conclusions local: this signal helped or failed to help with this review process, under these conditions.&lt;/p&gt;
&lt;h2 id=&quot;keep-the-decision-with-the-reviewer&quot;&gt;Keep the decision with the reviewer&lt;/h2&gt;
&lt;p&gt;The most useful workflow is not one where an agent declares its own work safe. It is one where the agent makes its limits and assumptions easier to inspect. Uncertainty can help prioritize attention when it is specific, tied to the patch, and followed by a human check. It becomes dangerous when a confident tone is allowed to stand in for evidence or when a blank uncertainty report is read as approval.&lt;/p&gt;
&lt;p&gt;I would ask the agent to show what it changed, what it checked, and what it could not establish. I would then use the diff, test results, and review findings to make the decision. If those forms of evidence disagree with the agent’s self-assessment, the self-assessment loses.&lt;/p&gt;
&lt;p&gt;That keeps the signal in its proper role: a hint about where to look, never permission to stop looking.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>code-review</category><category>software-quality</category></item><item><title>Pin C# language behavior separately from the .NET SDK</title><link>https://vahidinasab.com/articles/pin-csharp-language-behavior-separately-from-the-dotnet-sdk</link><guid isPermaLink="true">https://vahidinasab.com/articles/pin-csharp-language-behavior-separately-from-the-dotnet-sdk</guid><description>I explain why SDK selection and C# language policy belong in separate settings, and how to roll out language changes across a repository fleet.</description><pubDate>Mon, 26 Oct 2026 07:07:09 GMT</pubDate><content:encoded>&lt;h2 id=&quot;an-sdk-upgrade-is-not-a-language-policy&quot;&gt;An SDK upgrade is not a language policy&lt;/h2&gt;
&lt;p&gt;The .NET SDK supplies the compiler, but the target framework helps determine the default C# language version. That distinction matters now: the &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/overview&quot;&gt;.NET 11 overview&lt;/a&gt; says C# 15 is the default for projects targeting .NET 11, and its current release-candidate notes make that a live upgrade concern rather than a distant one.&lt;/p&gt;
&lt;p&gt;I would make two fleet-wide decisions, not one: which SDK builds the repositories, and which C# language version their code is allowed to use. Those decisions interact, but they answer different questions. The SDK determines which build tools are available. The language version sets the syntax and language features the compiler accepts. The target framework determines the APIs the application can compile against and, by default, influences the language version. Microsoft describes this relationship in its &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version&quot;&gt;C# language version reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That means an SDK upgrade by itself should not be treated as an intentional language-policy change. But an SDK upgrade that enables a move to .NET 11 can bring a new default along with the target-framework change. If the repository has no explicit policy, its accepted code can change as a side effect of a framework upgrade. I would rather make that change deliberate than ask every repository to notice it independently.&lt;/p&gt;
&lt;h2 id=&quot;put-the-language-choice-where-the-fleet-can-see-it&quot;&gt;Put the language choice where the fleet can see it&lt;/h2&gt;
&lt;p&gt;I would keep SDK selection in &lt;code&gt;global.json&lt;/code&gt; and language policy in shared MSBuild configuration. The &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/tools/global-json&quot;&gt;&lt;code&gt;global.json&lt;/code&gt; reference&lt;/a&gt; documents how the CLI selects an SDK from that file; it is the right place to express which SDK a repository expects. It is not where I would express which C# syntax the repository permits.&lt;/p&gt;
&lt;p&gt;For example, a repository that has not approved C# 15 can set its policy in &lt;code&gt;Directory.Build.props&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;xml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;Project&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;PropertyGroup&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;LangVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;14.0&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;LangVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  &amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;PropertyGroup&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;Project&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That setting is intentionally not inferred from the SDK or target framework. When the team chooses to adopt C# 15, it can change the value as a reviewed policy update. The &lt;a href=&quot;https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-by-directory?view=vs-2022&quot;&gt;MSBuild documentation on customizing builds by directory&lt;/a&gt; explains how &lt;code&gt;Directory.Build.props&lt;/code&gt; can provide settings to projects beneath it. I would check how each repository is structured before relying on one shared file: nested files, imports, and later property assignments can affect what a project actually receives.&lt;/p&gt;
&lt;p&gt;I would also avoid setting the language version to &lt;code&gt;latest&lt;/code&gt;. “Latest” describes a moving compiler choice, not a stable policy. Two builds using different SDKs could then accept different language features without a change to the repository’s stated intent. An explicit version makes the decision visible in code review and gives the build a consistent rule to enforce. The compiler still comes from the SDK; the language version is an independent input to that compiler.&lt;/p&gt;
&lt;p&gt;This separation does not mean every project should use the same language version forever. A repository targeting .NET 11 may intentionally adopt C# 15 while another stays on C# 14. The point is that those differences should be chosen, not inherited accidentally from a default. Shared configuration can establish a baseline, while a deliberate, documented exception can serve a project with a different migration plan.&lt;/p&gt;
&lt;h2 id=&quot;roll-out-a-change-as-a-compatibility-check&quot;&gt;Roll out a change as a compatibility check&lt;/h2&gt;
&lt;p&gt;I would not change the SDK, target framework, and language version across a fleet in one opaque step. Each change can expose a different class of issue. An SDK change changes the build toolchain; a target-framework change changes the platform surface; a language-version change permits new syntax and can affect how the compiler interprets code. Keeping those decisions distinct makes failures easier to diagnose and makes the review answerable: which policy changed, and why?&lt;/p&gt;
&lt;p&gt;A practical rollout starts by recording each repository’s current SDK, target framework, and effective language version. Then I would apply the shared language setting and verify that the existing build still passes under the existing toolchain. Next, I would test the proposed SDK and framework update against a representative set of repositories, chosen to cover different project shapes and build paths. The compatibility check should include the actual restore, build, and test steps used by CI, not just a local compile of one project.&lt;/p&gt;
&lt;p&gt;When the change is ready, I would make the language-policy update explicit in the same review that adopts the new language version. I would check for projects that override the central property and for build paths that do not consume the shared configuration. If the fleet has reusable pipelines, I would put the check there so a repository cannot silently escape the policy merely because its local build happens to use a different SDK installation.&lt;/p&gt;
&lt;p&gt;A pinned language version is not a promise that every new language feature works with every target framework. It controls what the compiler accepts; it does not supply runtime or framework APIs that the application may need. I would therefore test new syntax against the project’s actual target and dependencies, and keep framework compatibility review separate from the language-version change. The compiler’s own &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version&quot;&gt;language-version guidance&lt;/a&gt; is the reference for supported values and the relationship between language versions and target frameworks.&lt;/p&gt;
&lt;h2 id=&quot;make-the-default-change-reviewable&quot;&gt;Make the default change reviewable&lt;/h2&gt;
&lt;p&gt;C# 15 becoming the default for .NET 11 projects is a useful reminder that defaults are policy, even when nobody wrote them down. I would pin the SDK so builds are reproducible, and pin the language version so accepted code changes only when the team chooses to change it. Those pins solve different problems; treating them as interchangeable hides the decision that matters.&lt;/p&gt;
&lt;p&gt;When I am ready to adopt C# 15, I want the pull request to say so directly, the shared configuration to enforce it, and targeted compatibility checks to show that the affected repositories still build and test as intended. That costs some coordination up front. It costs less than discovering, repository by repository, that an infrastructure upgrade quietly changed the rules for writing application code.&lt;/p&gt;</content:encoded><category>dotnet</category><category>csharp</category><category>ci-cd</category><category>architecture</category></item><item><title>Use PostgreSQL 19 RC 1 to rehearse compatibility</title><link>https://vahidinasab.com/articles/use-postgresql-19-rc-1-to-rehearse-compatibility</link><guid isPermaLink="true">https://vahidinasab.com/articles/use-postgresql-19-rc-1-to-rehearse-compatibility</guid><description>I explain how to use PostgreSQL 19 RC 1 to test application integrations and upgrade procedures without treating an RC as a production commitment.</description><pubDate>Mon, 26 Oct 2026 07:07:06 GMT</pubDate><content:encoded>&lt;h2 id=&quot;test-the-system-around-postgresql&quot;&gt;Test the system around PostgreSQL&lt;/h2&gt;
&lt;p&gt;A benchmark can tell me whether a workload performs differently; it cannot tell me whether the application still migrates cleanly, whether its driver behaves as expected, or whether my restore procedure works. I would use PostgreSQL 19 RC 1 to rehearse those integrations, not to make a production adoption decision.&lt;/p&gt;
&lt;p&gt;The PostgreSQL project announced RC 1 on October 15, 2026, and describes release candidates as pre-release versions for testing, not production use. Its &lt;a href=&quot;https://www.postgresql.org/developer/beta/&quot;&gt;beta testing guidance&lt;/a&gt; specifically calls out testing by developers of platforms, drivers, tools, and utilities that depend on PostgreSQL. That is the useful frame for an application team too: treat the candidate as a chance to find compatibility work while there is still room to address it.&lt;/p&gt;
&lt;p&gt;I would not start by asking whether PostgreSQL 19 is faster for a representative query. I would start with the ways my application depends on PostgreSQL: schema and data migrations, driver behavior, extensions, database initialization, backups, restores, and the operational steps around a major-version upgrade. A performance test can be one part of qualification, but it is a narrow answer to a different question.&lt;/p&gt;
&lt;h2 id=&quot;make-the-test-resemble-the-real-upgrade&quot;&gt;Make the test resemble the real upgrade&lt;/h2&gt;
&lt;p&gt;A useful rehearsal needs more than a fresh database and a successful connection. I would bring up an isolated RC 1 environment using the same kind of installation and configuration I expect to operate, then run the application’s existing migration path against it. The goal is not to invent a new test suite for the candidate. It is to exercise the existing one against a different server version.&lt;/p&gt;
&lt;p&gt;I would record the environment and commands so that a failure can be reproduced, and I would keep the current-version run as a comparison. For example, a pipeline could make the server version an explicit input to a migration-and-integration job:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;PostgreSQL compatibility&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  workflow_dispatch&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  integration&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    strategy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      matrix&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        postgres&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;18&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;19-rc1&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Start PostgreSQL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./scripts/start-postgres.sh &quot;${{ matrix.postgres }}&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Apply migrations&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./scripts/migrate-test-database.sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Run integration tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./scripts/test-database-integration.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The image tags and startup commands need to match the way a team actually provisions PostgreSQL; the point of the example is to make the tested version visible in the job, not to prescribe a particular container image. I would also make sure the test checks the application’s important database paths, rather than stopping at a successful health check. A server that starts is not evidence that the application’s schema, queries, or migration sequence work.&lt;/p&gt;
&lt;p&gt;Migrations deserve their own attention. I would test both applying the full migration history to a clean database and upgrading a representative copy of the current schema and data through the normal migration process. Those cover different starting conditions. I would inspect the resulting schema and exercise the application flows that depend on the changed tables, constraints, indexes, or stored logic. If a migration has special operational steps, I would run those too, rather than assuming that a green test of the migration tool covers the whole procedure.&lt;/p&gt;
&lt;h2 id=&quot;include-dependencies-and-operational-procedures&quot;&gt;Include dependencies and operational procedures&lt;/h2&gt;
&lt;p&gt;The application does not integrate with the PostgreSQL server alone. I would list the database drivers, extensions, migration tools, and any other PostgreSQL-dependent components in the system, then verify their actual versions and installation paths in the test environment. For each dependency, I would ask a practical question: can I install or load it, can the application use the functions or types it relies on, and does the test environment behave as expected when it is present?&lt;/p&gt;
&lt;p&gt;That is more useful than treating a successful connection as a compatibility verdict. A driver may connect while a less frequently used query path still needs testing. An extension may be installed in one environment but missing from the process that builds the test database. A migration can apply while a subsequent operation assumes an object or behavior that the migration did not produce. These are test-design risks, not claims that PostgreSQL 19 RC 1 has a particular defect. The point is to make the dependencies visible and check the paths the application actually uses.&lt;/p&gt;
&lt;p&gt;I would include the operational runbook in the rehearsal. The &lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409&quot;&gt;PostgreSQL 19 RC 1 announcement&lt;/a&gt; says upgrading from earlier versions requires a major-version upgrade strategy, such as &lt;code&gt;pg_upgrade&lt;/code&gt; or &lt;code&gt;pg_dump&lt;/code&gt; and &lt;code&gt;pg_restore&lt;/code&gt;. I would test the strategy I expect to use, not just read about an alternative. That means rehearsing the surrounding steps as well: how I prepare the source, where I take and verify backups, how I provision the target, and how I establish that the application can use the upgraded database.&lt;/p&gt;
&lt;p&gt;A runbook test should produce evidence that another person can follow. I would record the starting version, the chosen upgrade method, the commands and configuration used, any required extension setup, the checks performed after the upgrade, and the conditions under which I would stop or roll back. If a step depends on a manual judgment, I would make that explicit. Hidden assumptions are exactly what a rehearsal should uncover.&lt;/p&gt;
&lt;h2 id=&quot;keep-qualification-separate-from-adoption&quot;&gt;Keep qualification separate from adoption&lt;/h2&gt;
&lt;p&gt;An RC is a useful test target precisely because it is not a production commitment. The PostgreSQL project says release candidates are made available so people can test before the initial release, and notes that changes can still be made before general availability in its &lt;a href=&quot;https://www.postgresql.org/developer/beta/&quot;&gt;beta testing guidance&lt;/a&gt;. I would keep the test environment isolated, avoid directing production traffic to it, and treat results as evidence about readiness—not as permission to upgrade.&lt;/p&gt;
&lt;p&gt;I would also keep a short findings list with three categories: confirmed compatible, needs investigation, and blocked. A failed test should say what failed and how to reproduce it; a passing test should say what path it covered. “The suite passed” is not enough if the suite never exercises an extension or the upgrade route we plan to use. Equally, a gap in test coverage is not proof of incompatibility. It is a reason to decide whether to add a check or accept an explicit uncertainty.&lt;/p&gt;
&lt;p&gt;If the rehearsal finds work, I would fix or document that work while the upgrade is still a qualification exercise. If it finds none, I would have stronger evidence for the eventual adoption decision, but not a substitute for that decision. I want an RC run to tell me where the integration surface needs attention before production planning begins; a benchmark alone cannot do that.&lt;/p&gt;</content:encoded><category>postgresql</category><category>database</category><category>testing</category></item><item><title>Give workflows an access model before secrets get finer-grained</title><link>https://vahidinasab.com/articles/give-workflows-an-access-model-before-secrets-get-finer-grained</link><guid isPermaLink="true">https://vahidinasab.com/articles/give-workflows-an-access-model-before-secrets-get-finer-grained</guid><description>A purpose-based access model helps teams reduce GitHub Actions secret exposure now and adopt finer-grained controls without confusing configuration for policy.</description><pubDate>Mon, 26 Oct 2026 07:07:01 GMT</pubDate><content:encoded>&lt;h2 id=&quot;start-with-what-the-workflow-does&quot;&gt;Start with what the workflow does&lt;/h2&gt;
&lt;p&gt;A workflow name is not an access policy. “CI” might run tests on an external pull request, build a release artifact, or deploy to production; those jobs should not inherit the same credentials just because they share a repository or a reusable workflow.&lt;/p&gt;
&lt;p&gt;I would classify workflows by purpose before deciding which secrets or tokens they need. Tests should usually prove something about the code without gaining authority to publish or deploy. Builds may need access to dependencies or a package registry, but that does not make them release jobs. Scans need to read the code and report findings. Releases may need permission to publish an artifact. Deployments need access to a target environment, and production access should be treated as a separate trust decision.&lt;/p&gt;
&lt;p&gt;That classification is the policy model. It gives the team a way to answer, for every workflow: what is this allowed to do, what credential would make that possible, and what condition must be true before the credential is available? I would write those answers down before changing secret settings. Otherwise, a new control can make an existing convention look more rigorous without correcting the underlying assumptions.&lt;/p&gt;
&lt;p&gt;This matters now because &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/&quot;&gt;GitHub’s 2026 Actions security roadmap&lt;/a&gt; describes scoped secrets tied to execution contexts such as branches, environments, workflow identities or paths, and trusted reusable workflows. The architectural opportunity is not just to move secrets into a more precise setting. It is to decide what those boundaries should mean for the work we run.&lt;/p&gt;
&lt;h2 id=&quot;make-current-access-resemble-the-intended-boundary&quot;&gt;Make current access resemble the intended boundary&lt;/h2&gt;
&lt;p&gt;The current model already offers some useful boundaries, but they do not automatically describe a team’s workflow policy. GitHub documents repository- and organization-level secrets, along with environment secrets. Environment secrets are available to jobs that reference the environment, and configured protection rules must pass before the job can access them. That makes environments useful for controlling deployment credentials, but an environment name alone does not decide whether a job belongs there or whether its permissions are appropriate. See &lt;a href=&quot;https://docs.github.com/actions/deployment/targeting-different-environments/using-environments-for-deployment&quot;&gt;GitHub’s documentation on managing environments&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Reusable workflows need particular care. A caller can pass specific secrets to a called workflow, or use &lt;code&gt;inherit&lt;/code&gt; to pass all secrets it can access. GitHub’s &lt;a href=&quot;https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions&quot;&gt;workflow syntax reference for reusable-workflow secrets&lt;/a&gt; describes both options. I prefer explicit passing: the call site then shows which credential crosses the boundary. &lt;code&gt;inherit&lt;/code&gt; is a convenience, not a policy statement. If the called workflow only needs a publishing credential, inheriting every secret available to the caller makes the boundary harder to reason about.&lt;/p&gt;
&lt;p&gt;I would begin by inventorying secrets and their consumers, then map each consumer to a purpose. A small table can make the intended policy reviewable:&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Workflow purpose&lt;/th&gt;&lt;th&gt;Typical access to allow&lt;/th&gt;&lt;th&gt;Access to keep out&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Tests&lt;/td&gt;&lt;td&gt;Read source and run checks&lt;/td&gt;&lt;td&gt;Publish or deploy credentials&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Builds&lt;/td&gt;&lt;td&gt;Read required inputs and produce artifacts&lt;/td&gt;&lt;td&gt;Production credentials&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Scans&lt;/td&gt;&lt;td&gt;Read source and report findings&lt;/td&gt;&lt;td&gt;Release and deployment credentials&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Releases&lt;/td&gt;&lt;td&gt;Publish the approved artifact&lt;/td&gt;&lt;td&gt;Unrelated environment credentials&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Deployments&lt;/td&gt;&lt;td&gt;Access the intended target environment&lt;/td&gt;&lt;td&gt;Credentials for other environments&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;These are starting boundaries, not a universal permission matrix. A test may need a narrowly scoped service credential, for example; the point is to make that exception deliberate and visible. For each exception, I would record why the workflow needs it and whether it can be replaced with a narrower token, an environment secret, or a short-lived identity.&lt;/p&gt;
&lt;h2 id=&quot;treat-workflow-code-as-one-layer-not-the-whole-policy&quot;&gt;Treat workflow code as one layer, not the whole policy&lt;/h2&gt;
&lt;p&gt;A workflow can request narrow permissions and pass only selected secrets, but its YAML is still code that can change. I would use workflow configuration to express the job’s needs, then use repository and organization settings, environment protection, and review practices to reinforce the boundary. The roadmap itself frames the direction as policy beyond YAML alone; &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/&quot;&gt;GitHub’s roadmap discussion of execution protections and secret governance&lt;/a&gt; is a useful reminder not to equate a workflow convention with an enforceable organization-wide rule.&lt;/p&gt;
&lt;p&gt;For example, a test workflow can make its token permissions explicit and keep release secrets out of the job:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet test&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example is intentionally limited: it tests code and does not request publishing or deployment access. In a real repository, I would also review which actions and scripts run in the job, because a secret exposed to a job is available to code running there, not just to the line that appears to use it. GitHub’s &lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/reference/security/secure-use&quot;&gt;secure use reference for Actions&lt;/a&gt; covers the risks of untrusted code and recommends auditing how secrets are handled.&lt;/p&gt;
&lt;p&gt;I would also avoid using a long-lived secret when a workflow can authenticate through a short-lived identity. GitHub documents using OpenID Connect to authenticate to supported cloud providers without storing those credentials as long-lived secrets in &lt;a href=&quot;https://docs.github.com/actions/security-guides/using-secrets-in-github-actions&quot;&gt;Using secrets in GitHub Actions&lt;/a&gt;. That does not remove the need for an access model: the identity still needs a defined audience, trust conditions, and permissions at the target. It changes the credential mechanism, not the question of which workflow should be trusted.&lt;/p&gt;
&lt;h2 id=&quot;carry-the-model-into-finer-grained-controls&quot;&gt;Carry the model into finer-grained controls&lt;/h2&gt;
&lt;p&gt;When finer-grained secret controls arrive, I would translate the existing purpose model into them rather than start by clicking through configuration. A test workflow should be scoped to test needs; a release workflow should receive publishing access only when the release conditions are satisfied; deployment credentials should follow the intended environment and approval boundary. The new feature should encode decisions the team has already made.&lt;/p&gt;
&lt;p&gt;That translation also exposes awkward cases. If one reusable workflow both builds and deploys, does it have two purposes or has it accumulated responsibilities that should be split? If a release path uses the same secret as routine builds, is that because the systems require it or because the distinction was never designed? If a workflow is authorized by its file path, who can change that file, and what review protects the change? A finer control can make the answer enforceable, but it cannot supply the answer.&lt;/p&gt;
&lt;p&gt;I would keep the policy legible in three places: a short purpose-to-access map, workflow files that request only the permissions they need, and platform controls that enforce boundaries outside an individual job’s convention. Then I would test changes against the map: a new workflow should declare its purpose, a new secret should name its consumers, and a reusable workflow should receive only what its contract requires.&lt;/p&gt;
&lt;p&gt;The useful preparation is not guessing the exact shape of a future settings page. It is agreeing now that tests, builds, scans, releases, and deployments are different kinds of work, and that their access should differ accordingly. When the platform offers finer-grained controls, I want to bring it a policy to implement—not ask its configuration screen to invent one for me.&lt;/p&gt;</content:encoded><category>github-actions</category><category>security</category><category>architecture</category></item><item><title>Put a hard budget around nested coding-agent work</title><link>https://vahidinasab.com/articles/put-a-hard-budget-around-nested-coding-agent-work</link><guid isPermaLink="true">https://vahidinasab.com/articles/put-a-hard-budget-around-nested-coding-agent-work</guid><description>I argue for treating nested coding agents as a resource-control problem, with enforceable limits on delegation, tools, spend and elapsed time.</description><pubDate>Mon, 26 Oct 2026 07:06:58 GMT</pubDate><content:encoded>&lt;h2 id=&quot;fan-out-is-a-resource-tree&quot;&gt;Fan-out is a resource tree&lt;/h2&gt;
&lt;p&gt;A coding agent that can delegate work is not just doing several things at once. It is creating a tree of work whose branches can consume model calls, tool access, wall-clock time and attention. I would design that tree as a resource-control problem before treating it as a productivity feature.&lt;/p&gt;
&lt;p&gt;A recent &lt;a href=&quot;https://www.developersdigest.tech/blog/claude-agent-sdk-whats-new-2026&quot;&gt;overview of Claude Agent SDK updates&lt;/a&gt; reports nested subagents with caps among its October changes. That is a useful prompt for an architectural question, not proof that a particular SDK setting makes an agent safe or economical. The &lt;a href=&quot;https://code.claude.com/docs/en/agent-sdk/subagents&quot;&gt;Claude Agent SDK subagents documentation&lt;/a&gt; describes controls for nesting depth, concurrent agents and spend. Those controls address different failure modes, and I would not collapse them into one idea of “the budget.”&lt;/p&gt;
&lt;p&gt;Consider a supervisor that delegates a code review to several agents, each of which can delegate again. A small initial request can turn into many active workers. Even if the SDK limits the depth of the tree, branching at every level can still produce a large workload. Conversely, a low concurrency cap can keep the active workload small while agents queue up and extend the run. These are separate dimensions: how many agents may exist, how many may work at once, and how much total work the run may consume.&lt;/p&gt;
&lt;p&gt;I would make the first decision about delegation structural: which roles are allowed to spawn agents at all? A top-level coordinator might need that capability; a file reader, test summarizer or leaf-level reviewer usually does not. If every agent can delegate, every prompt becomes part of an open-ended resource policy. If only designated supervisors can delegate, the shape of the work is easier to inspect and constrain.&lt;/p&gt;
&lt;h2 id=&quot;put-invariants-below-the-prompt&quot;&gt;Put invariants below the prompt&lt;/h2&gt;
&lt;p&gt;Prompts are useful for explaining intent: split independent investigations, avoid duplicate work, and stop when the evidence is sufficient. They are not where I would enforce a hard limit. The model is being asked to choose actions; the surrounding system should decide which actions remain possible.&lt;/p&gt;
&lt;p&gt;The SDK or orchestration layer should own limits that must hold regardless of what the model says: maximum delegation depth, concurrent-agent capacity and total spend. Tool access belongs there too. A worker asked to inspect code should not receive write or shell capabilities by default merely because the supervisor has them. I would give a supervisor the smallest set of tools needed to delegate and synthesize, then give each worker only the tools its assigned task requires. Where a task needs approval for a risky operation, that should be an explicit permission boundary rather than a sentence asking the agent to be careful.&lt;/p&gt;
&lt;p&gt;Here is the shape I would start with in a TypeScript SDK integration. The values are illustrative starting points, not recommendations; I would choose them against the task and the cost of interruption. The SDK documentation describes &lt;code&gt;maxBudgetUsd&lt;/code&gt; and the subagent depth and concurrency environment variables as controls for these boundaries (&lt;a href=&quot;https://code.claude.com/docs/en/agent-sdk/subagents&quot;&gt;subagent limits in the SDK docs&lt;/a&gt;). I have left the delegation tool out of the worker’s allow-list on purpose: this is a leaf worker, not another supervisor.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;process.env.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;1&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;process.env.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;3&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; message&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt; query&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  prompt: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Inspect the assigned module and report relevant risks.&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  options: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    cwd: repositoryPath,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    allowedTools: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Read&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Glob&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Grep&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    maxTurns: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;12&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    maxBudgetUsd: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;})) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;  handleMessage&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(message);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would treat this as a sketch, not a copy-paste guarantee. The precise option names and behavior depend on the SDK version, so I would verify them against the version actually deployed. The important design choice is that the cap is applied by the runtime around the agent, not entrusted to its prompt. A prompt can explain what happens when a limit is reached; it cannot be the mechanism that enforces the limit.&lt;/p&gt;
&lt;h2 id=&quot;budget-time-and-capability-as-well-as-spend&quot;&gt;Budget time and capability as well as spend&lt;/h2&gt;
&lt;p&gt;A spend cap is necessary but incomplete. It may bound model charges without bounding how long the task occupies a worker, how long a tool call hangs, or what a worker can do before the spend is reached. I would give each run an elapsed-time deadline in the host application and make tool calls subject to timeouts too. A deadline should stop or cancel outstanding work, not just stop the coordinator from waiting for it.&lt;/p&gt;
&lt;p&gt;Tool access is also a resource and a risk. Read-only tools let an agent investigate without changing the repository. Write access can create conflicts when multiple agents touch the same files; shell access can have effects beyond the files under review. I would grant these capabilities per role, and I would make write-capable work either sequential or explicitly partitioned so that parallel workers do not have competing ownership of the same change. That is my design preference, not a claim that a particular agent SDK automatically provides file-level isolation.&lt;/p&gt;
&lt;p&gt;I would also distinguish a per-agent turn limit from a run-wide budget. A local limit can stop one worker from looping, but it does not necessarily say how much work the whole tree may perform. The coordinator needs a shared envelope that accounts for delegated work, not a fresh allowance for every child that makes the aggregate invisible. If the SDK exposes a run-wide spend limit that includes subagents, I would use it as the hard stop and still keep local limits for containment. If its accounting does not cover a resource I care about, I would track that resource outside the SDK.&lt;/p&gt;
&lt;p&gt;Those distinctions matter operationally. A task may stop because it reached the spend cap, the deadline, a concurrency limit or a permission boundary. I would report which one happened and preserve the useful partial results. Otherwise, “agent failed” hides the difference between an unreasonable task and a sensible task that simply ran out of its allocation.&lt;/p&gt;
&lt;h2 id=&quot;make-the-limit-part-of-the-contract&quot;&gt;Make the limit part of the contract&lt;/h2&gt;
&lt;p&gt;Before enabling nested work, I would write down the budget in terms the system can enforce: who may delegate, how many workers may run, what each worker may use, and the run’s spend and time ceilings. I would also decide what the user sees when a limit is reached. Does the coordinator return partial findings, ask for permission to continue, or stop with a clear explanation? That behavior should be designed rather than left to a model improvising under pressure.&lt;/p&gt;
&lt;p&gt;Then I would test the boundary conditions. Give the coordinator a task that encourages unnecessary delegation. Make a worker attempt a tool it should not have. Let a tool stall. Check that hitting the aggregate budget stops outstanding work and that the final result says why it stopped. These are not elaborate benchmarks; they are checks that the controls actually sit on the execution path.&lt;/p&gt;
&lt;p&gt;Nested agents can be useful when work divides cleanly, but parallelism is not free. My default would be shallow delegation, narrow tool permissions and a run-wide hard budget, with deeper trees enabled only where the task justifies the added control problem. Prompts can guide good resource use. The SDK and host application must enforce the limits.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>architecture</category><category>agentic-ai</category></item><item><title>Treat optional device operations as a workload contract</title><link>https://vahidinasab.com/articles/treat-optional-device-operations-as-a-workload-contract</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-optional-device-operations-as-a-workload-contract</guid><description>I argue that optional Kubernetes device operations should change how platforms declare device capabilities and how workloads handle missing setup or cleanup.</description><pubDate>Mon, 26 Oct 2026 07:06:54 GMT</pubDate><content:encoded>&lt;h2 id=&quot;skipping-a-call-is-not-a-workload-guarantee&quot;&gt;Skipping a call is not a workload guarantee&lt;/h2&gt;
&lt;p&gt;A device that needs no node-local preparation may sound like a simpler device. But “the kubelet can skip a driver call” and “the workload can safely use this device” are different statements. The first is an operational fact about the device driver; the second is a contract the platform and workload must agree on.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/resource-management/dynamic-resource-allocation/dra-features&quot;&gt;The Kubernetes documentation on optional node operations&lt;/a&gt; describes a driver declaring that particular node-local operations can be skipped. The kubelet can then bypass driver lookup and gRPC calls for those devices. The operations in question are &lt;code&gt;NodePrepareResources&lt;/code&gt;, called before a container starts, and &lt;code&gt;NodeUnprepareResources&lt;/code&gt;, called when a Pod terminates. That declaration is about what Kubernetes needs to ask the node-local driver to do. It does not, by itself, describe the application’s device requirements or promise that every allocated device behaves identically.&lt;/p&gt;
&lt;p&gt;I would therefore treat optional operations as a boundary in the workload contract, not as a workload feature toggle. A workload should state what it needs from a device: for example, whether the device must be available to the container, whether some initialization must have happened before use, and whether cleanup must happen before the resource is reused. The platform then maps those needs onto devices and their supported operations. If a device has no node-local preparation, that may satisfy the contract. If setup is performed elsewhere, the platform must make that responsibility explicit rather than silently assuming the kubelet handled it.&lt;/p&gt;
&lt;p&gt;This distinction matters because “optional” is not the same as “unimportant.” Skipping a call is safe only when the omitted work is unnecessary or is reliably done by another part of the system. That is my design rule, not a Kubernetes guarantee.&lt;/p&gt;
&lt;h2 id=&quot;declare-capabilities-not-implementation-trivia&quot;&gt;Declare capabilities, not implementation trivia&lt;/h2&gt;
&lt;p&gt;I would keep the workload-facing contract focused on outcomes. A workload generally should not need to know whether a driver implements a gRPC method, whether a node-local plugin is installed, or which component performs a particular setup step. Those details belong to the platform’s device integration. The workload needs to know whether the device meets its requirements and what fallback is acceptable.&lt;/p&gt;
&lt;p&gt;For example, a workload might make its policy explicit in application configuration:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;# Illustrative application-level policy, not a Kubernetes API field.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;devicePolicy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  acceleration&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;preferred&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  initialization&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;required-before-use&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  fallback&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;cpu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The values here are an example of the kind of contract I mean, not Kubernetes schema. &lt;code&gt;preferred&lt;/code&gt; says that acceleration is useful but not essential; &lt;code&gt;required-before-use&lt;/code&gt; says the application must not consume the device before its initialization condition is met; &lt;code&gt;cpu&lt;/code&gt; states what the application can do if suitable acceleration is unavailable. A workload that cannot run correctly without the device would express a different policy and should not quietly fall back.&lt;/p&gt;
&lt;p&gt;The platform has a separate job: it must map the workload’s declared needs to resources that can meet them. A device declaration that skips &lt;code&gt;NodePrepareResources&lt;/code&gt; cannot be treated as evidence that the workload’s initialization requirement is satisfied unless the platform knows that no such preparation is needed, or can identify the other component responsible for it. Likewise, the application’s willingness to fall back does not mean the scheduler or allocator will automatically provide a CPU-only path. That fallback has to exist in the workload design and deployment configuration.&lt;/p&gt;
&lt;p&gt;This separation gives both sides a useful boundary. The workload describes what it can safely assume; the platform describes which devices satisfy those assumptions and who owns any work outside the kubelet’s node operations. When those statements cannot be matched, I would rather reject or visibly degrade the deployment than let a Pod start with a false promise.&lt;/p&gt;
&lt;h2 id=&quot;handle-differences-at-deployment-time&quot;&gt;Handle differences at deployment time&lt;/h2&gt;
&lt;p&gt;Optional operations make it easier for a platform to support devices that do not need a node-local driver for every operation. The &lt;a href=&quot;https://kubernetes.io/docs/concepts/resource-management/dynamic-resource-allocation/dra-features&quot;&gt;Kubernetes DRA feature documentation&lt;/a&gt; also warns that changing a driver’s node-operation requirements in place does not change the configuration used by existing claims. It recommends ensuring that no active claims exist for the driver before changing those requirements or removing node-local driver DaemonSets. That is a reminder that a declaration change is not merely a cleanup of deployment YAML: existing allocations may still rely on the old contract.&lt;/p&gt;
&lt;p&gt;I would handle a change in three stages. First, inventory the workload assumptions: which device capabilities are required, which operations are currently expected, and whether any setup or cleanup happens outside the driver. Second, make the new declaration available without removing the old path, and test workloads against both kinds of node. Third, move claims and workloads deliberately, then remove obsolete node-local components only after the platform has established that no remaining allocation depends on them. The exact rollout mechanism belongs to the cluster; the invariant is that a live claim must not be stranded between two versions of the contract.&lt;/p&gt;
&lt;p&gt;For deployments, I would expose the difference as a capability or placement constraint, not as a hidden node implementation detail. If a workload requires a device with a particular setup guarantee, the deployment should select only resources for which the platform can make that guarantee. If the workload can operate without the device, the deployment should say what fallback it supports and make that fallback observable to operators. A Pod being scheduled is not proof that an application-level requirement has been met.&lt;/p&gt;
&lt;h2 id=&quot;make-the-contract-reviewable&quot;&gt;Make the contract reviewable&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kubernetes/enhancements/issues/5945&quot;&gt;KEP 5945&lt;/a&gt; tracks DRA Optional Node Operations, with Beta targeted for Kubernetes v1.38. &lt;a href=&quot;https://lwkd.info/2026/20261015&quot;&gt;Kubernetes development coverage&lt;/a&gt; reports that the Beta update targets v1.38 with the feature gate on by default. That makes this a timely prompt to review assumptions, but the release milestone does not change the contract principle: the platform must not confuse an optional node operation with an optional workload requirement.&lt;/p&gt;
&lt;p&gt;I would put the contract where both workload and platform changes can be reviewed together: document the device capabilities the workload consumes, the conditions under which it can run without them, and the owner of any setup or cleanup that Kubernetes does not perform. Then test at least the two meaningful cases: a resource whose node operation is skipped, and one where the operation is performed. The test should check what the application can actually rely on, not merely whether the Pod reaches Running.&lt;/p&gt;
&lt;p&gt;When the declaration, workload policy, and deployment behavior agree, optional operations are an implementation choice the platform can make safely. When they do not, a skipped call is just an undocumented assumption waiting to become a production failure.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>architecture</category><category>dra</category></item><item><title>Decide what a Kubernetes workload class is allowed to mean</title><link>https://vahidinasab.com/articles/decide-what-a-kubernetes-workload-class-is-allowed-to-mean</link><guid isPermaLink="true">https://vahidinasab.com/articles/decide-what-a-kubernetes-workload-class-is-allowed-to-mean</guid><description>A workload class can organize Kubernetes workloads or drive platform behavior; I explain why its meaning, ownership and guarantees must be explicit first.</description><pubDate>Mon, 26 Oct 2026 07:06:50 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-new-name-is-not-yet-a-contract&quot;&gt;A new name is not yet a contract&lt;/h2&gt;
&lt;p&gt;A workload class sounds like a useful abstraction: give workloads a category, then let the platform treat similar ones consistently. But “similar” can mean several different things. Two services might share an operational owner, need the same deployment defaults, or require the same protection during disruption. Those are different facts, and a single label can quietly be asked to represent all of them.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://lwkd.info/2026/20261015&quot;&gt;Kubernetes development roundup for the week ending October 11, 2026&lt;/a&gt; reports that SIG Apps added &lt;code&gt;workload-class&lt;/code&gt; as a subproject. That is a reason to examine the design question, not evidence that the term already has a settled meaning. The &lt;a href=&quot;https://github.com/kubernetes/community/blob/master/sig-apps/README.md&quot;&gt;SIG Apps subprojects list&lt;/a&gt; describes the workload-class effort as a high-level control layer for managing workload outcomes and disruption guardrails during maintenance. That description points toward operational behavior, but it does not by itself tell a platform team what any particular class should promise, who assigns it, or what happens when a workload does not meet the conditions for it.&lt;/p&gt;
&lt;p&gt;I would not make a class platform policy until those questions have owners and answers. Until then, use it to organize workloads, if that is useful, and do not let the name imply a guarantee.&lt;/p&gt;
&lt;h2 id=&quot;choose-the-contract-before-the-mechanism&quot;&gt;Choose the contract before the mechanism&lt;/h2&gt;
&lt;p&gt;A class can serve at least three distinct purposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Organization:&lt;/strong&gt; group workloads so people can find, report on or discuss them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Defaults:&lt;/strong&gt; select configuration that teams can override, such as resource requests or rollout settings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational guarantees:&lt;/strong&gt; influence behavior that affects availability or fairness, such as disruption handling or scheduling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cost of getting the distinction wrong rises as the class moves down that list. Organization is usually low consequence. Defaults can create surprises if teams do not know what is being injected or how to override it. A guarantee changes what operators and application teams are entitled to expect, so it needs a defined scope, an accountable owner and a way to verify it.&lt;/p&gt;
&lt;p&gt;Kubernetes labels are a natural way to organize resources, but the &lt;a href=&quot;https://kubernetes.io/docs/concepts/overview/working-with-objects/labels&quot;&gt;Kubernetes documentation on labels and selectors&lt;/a&gt; says labels do not directly imply semantics to the core system. They can be used to identify and select objects; any additional meaning comes from the controllers and tools that read them. That gap is where hidden coupling starts. An application team adds a label because a template requires it. A platform controller interprets the same value as permission to apply a scheduling rule. A dashboard groups by it. Each tool has made a different assumption about one shared string.&lt;/p&gt;
&lt;p&gt;If a class is only organizational metadata, I would say so plainly and keep its consequences informational. For example, a team might use a label to filter workloads in a view, with no admission rule or controller changing the workload as a result. If a class selects defaults, document which defaults, whether teams can override them, and which component applies them. If it represents an operational guarantee, state the behavior and the limits in terms an operator can test. “Critical” is not a contract. “Protected from this class of voluntary disruption under these conditions” is closer, because it can be reviewed against actual behavior.&lt;/p&gt;
&lt;h2 id=&quot;make-policy-explicit-not-contagious&quot;&gt;Make policy explicit, not contagious&lt;/h2&gt;
&lt;p&gt;Here is an illustrative label for a platform that has deliberately chosen to use a class as an organizational tag only:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;apiVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;apps/v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Deployment&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;metadata&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;checkout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  labels&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    platform.example.com/workload-class&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;interactive&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;spec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  selector&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    matchLabels&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      app&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;checkout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  template&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    metadata&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      labels&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        app&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;checkout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        platform.example.com/workload-class&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;interactive&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    spec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      containers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;checkout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;          image&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example.invalid/checkout:1.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example, &lt;code&gt;interactive&lt;/code&gt; means only that the workload belongs in an organizational category. No controller should infer priority, resource sizing or a disruption promise from it. The value is deliberately not called &lt;code&gt;critical&lt;/code&gt; or &lt;code&gt;gold&lt;/code&gt;, terms that sound like someone has already bought a guarantee.&lt;/p&gt;
&lt;p&gt;If the platform later wants a class to affect behavior, I would make that transition explicit. A short policy definition should state:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Meaning:&lt;/strong&gt; what observable property qualifies a workload for the class?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership:&lt;/strong&gt; who defines the class, who may assign it, and who approves changes?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Effects:&lt;/strong&gt; which controllers, admission policies or deployment templates react to it?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limits:&lt;/strong&gt; what is not promised, and what happens when the cluster cannot satisfy the requested behavior?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation:&lt;/strong&gt; how can the team tell that the intended policy is actually in effect?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These questions are not ceremony. They separate policy from implementation. A label might be the input, but it should not be the whole specification. If several tools respond to it, the class owner needs to know which tools are consumers and what each one is expected to do. Otherwise, removing or renaming a value becomes a platform change disguised as metadata cleanup.&lt;/p&gt;
&lt;p&gt;Kubernetes already has narrower mechanisms for specific decisions. For example, &lt;a href=&quot;https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption&quot;&gt;Pod Priority and Preemption&lt;/a&gt; documents a mechanism for assigning relative pod importance and describes how priority can affect scheduling and preemption. A workload class that secretly toggles priority, disruption handling and resource defaults bundles several contracts together. I would rather have the class reference explicit policies, or keep those concerns separate, than make one broad word carry consequences no team can see from the workload definition.&lt;/p&gt;
&lt;h2 id=&quot;keep-ownership-visible-as-the-class-gains-power&quot;&gt;Keep ownership visible as the class gains power&lt;/h2&gt;
&lt;p&gt;The difficult question is not whether a platform can implement class-based behavior. It is whether anyone can explain the meaning consistently across application manifests, platform controllers and operational practice. When the answer differs by team, the class has become an informal API without an API owner.&lt;/p&gt;
&lt;p&gt;I would start with the smallest meaning that solves the immediate problem. If teams need a way to group workloads, let the class organize them. If repeated deployment defaults are the problem, define those defaults and their override rules rather than letting a category name imply them. If the platform intends to offer an operational guarantee, make that guarantee explicit and put an owner on the hook for its semantics and lifecycle.&lt;/p&gt;
&lt;p&gt;This also gives the class a sensible path to evolve. A label can remain organizational while a separately documented policy decides what behavior applies. If the class later becomes policy-bearing, teams can review the change as a change in contract—not discover it when a controller starts treating an old tag as an instruction. I would rather have a narrowly useful class whose meaning everyone can state than a powerful one whose behavior is distributed across tools and tribal knowledge.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>platform-engineering</category><category>architecture</category></item><item><title>Time-zone updates change application behavior</title><link>https://vahidinasab.com/articles/time-zone-updates-change-application-behavior</link><guid isPermaLink="true">https://vahidinasab.com/articles/time-zone-updates-change-application-behavior</guid><description>I explain why PostgreSQL time-zone data belongs in application change management and how to test future appointments across database and application updates.</description><pubDate>Mon, 19 Oct 2026 09:50:05 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-rule-update-can-change-a-product-decision&quot;&gt;A rule update can change a product decision&lt;/h2&gt;
&lt;p&gt;A time-zone update can change what an application tells a person about a future event, even when the database continues to store the same instant. PostgreSQL’s &lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365&quot;&gt;18.6 and 19 Beta 3 announcement&lt;/a&gt; calls out updated time-zone data: Alberta’s &lt;code&gt;America/Edmonton&lt;/code&gt; rules are changing to year-round UTC−06 beginning in November 2026, and Morocco’s &lt;code&gt;Africa/Casablanca&lt;/code&gt; rules move to permanent UTC+00 on September 20, 2026.&lt;/p&gt;
&lt;p&gt;That is not just a metadata refresh. It is an input to application behavior. A booking system, reminder service, or reporting job may use those rules to decide what local time to display, when to send a message, or which day an event belongs to. If the rules change, the result of that decision can change too.&lt;/p&gt;
&lt;p&gt;PostgreSQL’s &lt;a href=&quot;https://www.postgresql.org/docs/current/datatype-datetime.html&quot;&gt;date and time documentation&lt;/a&gt; explains the distinction: &lt;code&gt;timestamp with time zone&lt;/code&gt; values are stored internally in UTC, then converted for display, while future calculations rely on the latest known rules for a zone. Storing an instant in UTC protects the identity of that instant. It does not preserve the local-time rules that were used to calculate it, nor does it say what a person intended when they scheduled something.&lt;/p&gt;
&lt;p&gt;I would therefore treat a time-zone database update as a behavior change with a deployment and test plan. The relevant question is not only “Did the database package update?” It is “Which product decisions depend on these rules, and do those decisions still match the intended behavior?”&lt;/p&gt;
&lt;h2 id=&quot;an-instant-and-a-wall-clock-appointment-are-different-data&quot;&gt;An instant and a wall-clock appointment are different data&lt;/h2&gt;
&lt;p&gt;For a past event, the instant is usually the important fact: a payment cleared at a particular moment, or a request arrived at a particular moment. For a future appointment, the user may care more about the local clock reading: keep the appointment at 9:00 in the chosen place, even if local rules change before then.&lt;/p&gt;
&lt;p&gt;Those intentions are not interchangeable. A &lt;code&gt;timestamptz&lt;/code&gt; value represents an instant. If I store an appointment as an instant and the zone’s rules later change, converting that same instant to local time can produce a different wall-clock reading. Conversely, if I retain the local date and time plus a zone and recalculate the instant under new rules, the instant can move while the appointment remains at the intended local time.&lt;/p&gt;
&lt;p&gt;For future events where local time is the promise, I would preserve that promise explicitly, alongside the resolved instant used for ordering and execution. For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;CREATE&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt; appointments&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    id &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;bigint&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; GENERATED&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; ALWAYS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; AS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; IDENTITY&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; PRIMARY KEY&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    local_start &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;timestamp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; NOT NULL&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    time_zone &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; NOT NULL&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    starts_at &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;timestamptz&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; NOT NULL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;local_start&lt;/code&gt; and &lt;code&gt;time_zone&lt;/code&gt; capture the requested wall-clock time and the zone whose rules interpret it. &lt;code&gt;starts_at&lt;/code&gt; is the resolved instant. The schema alone does not decide which value is authoritative after a rule update; that is a product decision. A recurring meeting, a one-off appointment, and a transport departure may have different policies. I would write those policies down rather than quietly letting a conversion function make the choice.&lt;/p&gt;
&lt;p&gt;This also exposes an important edge case: a local time can be ambiguous or nonexistent around a clock transition. The product needs a defined response, such as asking the user to choose, selecting a documented offset policy, or rejecting the time. I would include that behavior in the scheduling contract and in tests, not assume every local timestamp maps neatly to one instant.&lt;/p&gt;
&lt;h2 id=&quot;map-every-consumer-of-the-rules&quot;&gt;Map every consumer of the rules&lt;/h2&gt;
&lt;p&gt;A database inventory is necessary, but it is not the whole dependency map. I would look for database queries that use named zones, application code that converts between UTC and local time, background jobs that calculate a local send time, and reports that group events by local date. I would also check deployment artifacts and runtime configuration: the application and database may not receive time-zone data from the same place or at the same time. I would verify this for the actual deployment rather than assume a database update updates every process.&lt;/p&gt;
&lt;p&gt;In PostgreSQL, a search for &lt;code&gt;AT TIME ZONE&lt;/code&gt; and zone names is a useful starting point, not proof that all dependencies have been found. A query can be driven by a zone stored in a row, and the application can do conversions outside SQL. I would trace each user-visible or operational behavior back to its conversion point and record which named zones it uses.&lt;/p&gt;
&lt;p&gt;Then I would compare the application and database on the same future cases. For a zone affected by the release, choose instants around the rule boundary and check both directions: convert the instant to local time, and resolve the intended local time to an instant. The PostgreSQL announcement identifies Alberta’s November 2026 change, so a test set should include cases on both sides of that change. The same test should run against the deployed database image and the application runtime that will actually ship.&lt;/p&gt;
&lt;p&gt;A database-side check might look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    timestamptz&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;2026-11-01 06:30:00+00&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;        AT&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; TIME&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; ZONE&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;America/Edmonton&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; AS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; local_time;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The date and instant are illustrative test inputs, not a claim about a specific appointment. I would assert the expected local result in automated tests, then run the equivalent conversion through the application’s own date-time library. Comparing only database output misses a mismatch where the app and database carry different rule data. Comparing only an application unit test misses a mismatch in SQL queries or database-side jobs.&lt;/p&gt;
&lt;h2 id=&quot;make-the-update-reviewable&quot;&gt;Make the update reviewable&lt;/h2&gt;
&lt;p&gt;I would handle a time-zone update like a small compatibility change. First, identify the updated rules and the affected zones from the release information. Next, map consumers and identify which stored records represent fixed instants versus local-time commitments. Then update the relevant runtime images or packages in a test environment and run the future-dated cases across both the database and application.&lt;/p&gt;
&lt;p&gt;The result of those tests should lead to an explicit choice for each affected workflow. Keep the instant fixed and accept a changed local display? Preserve the local appointment and recalculate the instant? Notify users or ask them to confirm? There is no universal answer; the correct behavior depends on what the product promised. What I would not do is let the answer emerge accidentally from whichever component happens to receive new time-zone data first.&lt;/p&gt;
&lt;p&gt;I would also make the deployment observable. Record the database and application versions used by the test, retain the test cases and expected outputs, and make it possible to rerun them when the underlying rules change again. That gives reviewers something concrete to approve and operators a way to distinguish a genuine behavior change from a deployment mismatch.&lt;/p&gt;
&lt;p&gt;UTC remains the right representation for many instants. It is not a substitute for preserving local intent or managing the rules that interpret it. When local time affects what a product promises, updating those rules is application behavior change management.&lt;/p&gt;</content:encoded><category>postgresql</category><category>time-zones</category><category>system-design</category></item><item><title>Benchmark .NET 11 before assuming it is faster</title><link>https://vahidinasab.com/articles/benchmark-dotnet-11-before-assuming-it-is-faster</link><guid isPermaLink="true">https://vahidinasab.com/articles/benchmark-dotnet-11-before-assuming-it-is-faster</guid><description>A controlled runtime comparison shows whether .NET 11 changes latency, throughput or resource use for your service—and what upgrade risk comes with the result.</description><pubDate>Mon, 19 Oct 2026 09:50:01 GMT</pubDate><content:encoded>&lt;h2 id=&quot;treat-release-notes-as-hypotheses&quot;&gt;Treat release notes as hypotheses&lt;/h2&gt;
&lt;p&gt;.NET 11 is in release candidate, with general availability expected in November 2026, according to Microsoft’s &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/overview&quot;&gt;What’s new in .NET 11&lt;/a&gt;. That makes it a useful point to test runtime behavior, not a reason to assume a production service will get faster.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/runtime&quot;&gt;“.NET runtime” section of the .NET 11 release notes&lt;/a&gt; describes changes across areas such as JIT optimization and async execution. Microsoft’s &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11&quot;&gt;performance improvements in .NET 11&lt;/a&gt; shows measured results for selected benchmarks. Those results are evidence about the cases tested; they are not a forecast for a different service, with different code, traffic, data and infrastructure.&lt;/p&gt;
&lt;p&gt;I would begin with a question tied to a workload: does the candidate runtime improve the service behavior I care about, under conditions that resemble production? That means comparing the runtime already used by the service with the .NET 11 release candidate, while keeping application changes out of the comparison. If I change code, dependencies and runtime together, a faster result tells me very little about which change helped—or what introduced a regression.&lt;/p&gt;
&lt;h2 id=&quot;compare-the-same-service-under-controlled-conditions&quot;&gt;Compare the same service under controlled conditions&lt;/h2&gt;
&lt;p&gt;I would use a representative service build and traffic pattern, not just a tight loop around a method that happens to look important. A microbenchmark can help isolate a suspected runtime or library change. It cannot tell me whether requests to my service are dominated by database work, serialization, network waits, queueing or some other bottleneck. The service-level test needs to reflect the paths and mix of requests that matter.&lt;/p&gt;
&lt;p&gt;For the comparison, I would hold the application source, dependencies, configuration, data set and infrastructure constant as far as practical. I would change the target runtime, record the SDK and runtime versions, and verify that both builds are running the intended runtime. I would also keep container base image, CPU limits, memory limits and other relevant deployment settings aligned. Otherwise, I risk measuring a changed environment rather than a changed runtime.&lt;/p&gt;
&lt;p&gt;Run the candidate and baseline under the same load shape, with enough warm-up for the service to reach its normal operating behavior. I would avoid running both under competing load on the same constrained host: that can make the result noisier and obscure which version handled the workload better. Repeat the test, and keep the raw measurements and environment details alongside the result. These controls are my experimental discipline, not a claim that a benchmark can reproduce every production condition.&lt;/p&gt;
&lt;p&gt;For example, &lt;a href=&quot;https://benchmarkdotnet.org/articles/guides/customizing-runtime.html&quot;&gt;BenchmarkDotNet’s runtime customization guidance&lt;/a&gt; explains how to compare benchmark jobs across runtimes. The .NET team also uses a runtime comparison in its &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11&quot;&gt;performance improvements in .NET 11&lt;/a&gt; post. A small benchmark can be a useful diagnostic, but I would not let it stand in for a service-level test.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;dotnet&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; Release&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; net10.0&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --filter&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --runtimes&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; net10.0&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; net11.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That command follows the form used in Microsoft’s benchmark article; the framework versions need to match the comparison I am actually running. The important point is to make the runtime contrast explicit, not to treat a benchmark harness as a miniature production environment.&lt;/p&gt;
&lt;h2 id=&quot;measure-outcomes-that-change-the-decision&quot;&gt;Measure outcomes that change the decision&lt;/h2&gt;
&lt;p&gt;I would look at latency, throughput and resource use together. Latency should include the tail, not only an average: a runtime change that improves the typical request while worsening slow requests may be a poor trade for a service with tight response-time expectations. Throughput helps show how much work the service completes under a defined load. CPU and memory help explain the cost of that work and whether a change shifts pressure elsewhere.&lt;/p&gt;
&lt;p&gt;I would interpret those signals against the service’s own objectives and operating constraints. If the candidate uses less CPU at the same throughput and comparable latency, that is a useful result. If it serves more requests but pushes tail latency outside an acceptable range, that is not an unqualified win. If the measurements are close to normal test variation, I would call the result inconclusive rather than manufacture a benefit from a small difference.&lt;/p&gt;
&lt;p&gt;The same logic applies to regressions. A change that looks worse in a single run deserves another controlled test, not an immediate verdict. If the difference persists, I would look for a workload-specific explanation: which request paths shifted, whether allocation or garbage collection behavior changed, and whether the service is now constrained by a different resource. I would use diagnostics to narrow the question, then rerun the representative workload to confirm whether the effect matters at the service boundary.&lt;/p&gt;
&lt;p&gt;I would also preserve a clean comparison point. First test the existing application against the candidate runtime without unrelated application edits. Then, if a code or dependency change is part of the planned upgrade, measure that as a separate comparison. Keeping those results distinct lets me explain what the runtime contributed, what the application changes contributed, and where the uncertainty remains.&lt;/p&gt;
&lt;h2 id=&quot;let-the-result-set-the-upgrade-risk&quot;&gt;Let the result set the upgrade risk&lt;/h2&gt;
&lt;p&gt;A performance result is only one input to a runtime upgrade. I would also check compatibility, deployment behavior, observability and the service’s rollback path. A release candidate is useful for learning, but I would not confuse a successful benchmark with production readiness. Microsoft’s &lt;a href=&quot;https://github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/11.0.0-rc.1.md&quot;&gt;release notes for .NET 11 RC 1&lt;/a&gt; identify it as a release candidate; the final decision still depends on the service and its operating requirements.&lt;/p&gt;
&lt;p&gt;The value of testing early is that it exposes both upside and uncertainty while there is still room to investigate. A repeatable gain on representative paths gives me a reason to plan adoption and validate it further. A regression tells me where to focus compatibility or performance work. No meaningful difference may still leave other reasons to upgrade, but I would not sell the move as a performance improvement.&lt;/p&gt;
&lt;p&gt;I would treat .NET 11 performance as a hypothesis until the service’s own measurements support it. Release notes tell me what might change; a controlled comparison tells me whether that change earns its risk in this workload.&lt;/p&gt;</content:encoded><category>dotnet</category><category>performance</category><category>benchmarking</category></item><item><title>Treat richer EF Core translation as a SQL change to verify</title><link>https://vahidinasab.com/articles/treat-richer-ef-core-translation-as-a-sql-change-to-verify</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-richer-ef-core-translation-as-a-sql-change-to-verify</guid><description>A more expressive EF Core query can change SQL, plans, and database constraints, so verify the emitted commands against your schema and workload.</description><pubDate>Mon, 19 Oct 2026 09:49:59 GMT</pubDate><content:encoded>&lt;h2 id=&quot;linq-is-only-the-front-of-the-boundary&quot;&gt;LINQ is only the front of the boundary&lt;/h2&gt;
&lt;p&gt;.NET 11 Preview 6 includes EF Core changes to LINQ translation and complex-type modeling. The &lt;a href=&quot;https://devblogs.microsoft.com/dotnet/dotnet-11-preview-6/&quot;&gt;Preview 6 announcement&lt;/a&gt; and &lt;a href=&quot;https://github.com/dotnet/core/blob/main/release-notes/11.0/preview/preview6/efcore.md&quot;&gt;EF Core release notes&lt;/a&gt; describe, among other things, support for translating full outer joins and for defining keys and indexes that reach into non-collection complex types. With .NET 11’s general release scheduled for November 2026, this is a useful point to examine what those capabilities mean at the database boundary, rather than treating richer LINQ as an automatic improvement. The &lt;a href=&quot;https://dotnet.microsoft.com/platform/support/policy/dotnet-core#release-types&quot;&gt;official .NET support policy&lt;/a&gt; lists the November release event.&lt;/p&gt;
&lt;p&gt;I treat a change in query expression as a reason to inspect the command EF Core produces. The expression tree is not the operation the database executes. Translation affects the SQL shape, and the database evaluates that SQL against its schema, indexes, statistics, and data. A query that reads more naturally in C# may still ask the database to do more work, return more rows, or use a plan that is a poor fit for the application’s workload.&lt;/p&gt;
&lt;p&gt;Full outer join support is a good example of why the distinction matters. Being able to express a join directly in LINQ can make the intent clearer than a workaround. But the application still needs the semantics it expects for unmatched rows on both sides, and the provider still needs to translate the expression appropriately. I would review the generated SQL and the results for the cases that matter, not assume that a shorter query is a faster one.&lt;/p&gt;
&lt;h2 id=&quot;check-the-sql-then-check-the-plan&quot;&gt;Check the SQL, then check the plan&lt;/h2&gt;
&lt;p&gt;I start by capturing the SQL for the query I am changing. &lt;code&gt;ToQueryString()&lt;/code&gt; is useful for inspecting an &lt;code&gt;IQueryable&lt;/code&gt;; logging executed commands is useful when I need to see what the application actually sends. These are complementary views: one helps me inspect the translation, and the other helps me understand the query path the application takes.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; query&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; context.Customers&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Where&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;customer&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; customer.Address.PostCode &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; postCode)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Select&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;customer&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; CustomerSummary&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        customer.Id,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        customer.Address.PostCode));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; sql&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; query.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;ToQueryString&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would compare the SQL before and after an EF Core upgrade for important queries, paying attention to joins, predicates, selected columns, and whether the translation still matches the intended result. I would also check how many commands the application executes for the use case, rather than inspecting one query in isolation. A query can look reasonable by itself while the surrounding access pattern repeatedly runs it or loads more data than the caller needs.&lt;/p&gt;
&lt;p&gt;SQL text is not the final verdict. I run the query with representative data and parameters, then inspect the database’s execution plan. Microsoft’s &lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/performance-diagnosis&quot;&gt;EF Core performance diagnosis guidance&lt;/a&gt; recommends examining the plan when investigating a problematic query; the plan reflects how the database intends to execute the statement using the available schema and indexes. I compare meaningful differences between versions, such as access paths, joins, and the amount of work indicated by the plan, rather than treating a changed SQL string as proof of a regression or a win.&lt;/p&gt;
&lt;p&gt;Representative matters. A tiny local database can hide a bad access pattern; a single convenient parameter can miss differences in data distribution. I use data and parameter values that reflect the application’s real reads and writes. If I do not have that representative workload, I treat a result as a preliminary check, not evidence that the change is safe in production.&lt;/p&gt;
&lt;h2 id=&quot;complex-types-reach-into-schema-decisions&quot;&gt;Complex types reach into schema decisions&lt;/h2&gt;
&lt;p&gt;The modeling change deserves the same scrutiny. EF Core 11 allows keys and indexes to target scalar properties inside non-collection complex types. For example, the &lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/modeling/complex-types&quot;&gt;complex types documentation&lt;/a&gt; shows configuring an index through a nested property:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;modelBuilder.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Entity&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;Customer&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;HasIndex&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;customer&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; customer.Address.PostCode);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a more direct way to express where the property belongs in the object model. It does not make the database consequence disappear. An index is a schema decision: I want to know which column or mapped value it covers, whether the migration changes the intended table, and whether the index fits the queries that actually use that property. The &lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/modeling/indexes&quot;&gt;EF Core indexes documentation&lt;/a&gt; explains how indexes are configured and how composite indexes relate to the columns a query filters on.&lt;/p&gt;
&lt;p&gt;I also check the model’s constraints, not just whether the new configuration compiles. The Preview 6 release notes say that properties used in keys or indexes become required, and that optional complex properties in keys are rejected. That behavior can affect whether the model represents the application’s existing rules. If an address is optional in the domain, I would not quietly make a nested address field part of a key just because the API now permits writing that configuration. I would resolve the mismatch deliberately, then inspect the migration and verify that the database and application agree.&lt;/p&gt;
&lt;p&gt;This is where I validate both directions: how the application reads the nested value and how it writes it. A model that makes a query pleasant but misstates nullability, uniqueness, or the ownership of the data is not a better model. I want the complex type to describe the application’s data accurately, and the relational constraints to support the actual access patterns.&lt;/p&gt;
&lt;h2 id=&quot;make-the-upgrade-a-workload-decision&quot;&gt;Make the upgrade a workload decision&lt;/h2&gt;
&lt;p&gt;For a critical query, I would keep a small review record: the LINQ expression, the generated SQL, the database provider, the relevant migration or model change, the representative parameters and data, and the execution plan before and after. That makes the review about a specific behavior rather than a general impression of the new release.&lt;/p&gt;
&lt;p&gt;I would not benchmark every query just because translation changed. I would prioritize the queries and writes that matter to the application, especially where the new LINQ feature replaces a workaround or where a complex-type property is added to a key or index. Then I would check the actual provider and database used by the application, because the translated command and the database’s plan are the things the workload depends on.&lt;/p&gt;
&lt;p&gt;My rule is simple: accept a richer expression when it makes the model or query clearer, but treat its SQL and schema effects as changes to verify. LINQ describes what I want. The emitted SQL, the plan, and the application’s read-and-write behavior tell me whether that choice fits the system.&lt;/p&gt;</content:encoded><category>ef-core</category><category>dotnet</category><category>database-performance</category></item><item><title>Treat model tools as a runtime contract</title><link>https://vahidinasab.com/articles/treat-model-tools-as-a-runtime-contract</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-model-tools-as-a-runtime-contract</guid><description>Model tool support can change which workflow is possible, so discover capabilities at runtime and define what happens when a tool is missing.</description><pubDate>Mon, 19 Oct 2026 09:49:56 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-model-choice-changes-the-workflow&quot;&gt;A model choice changes the workflow&lt;/h2&gt;
&lt;p&gt;An agent workflow often starts with a simple assumption: the selected model can use the tools the prompt describes. That assumption may hold until a model is swapped, a provider changes, or a workflow runs against a model with a different tool set. Then the failure shows up at the point of use, or worse, the workflow quietly produces a weaker answer while behaving as if nothing changed.&lt;/p&gt;
&lt;p&gt;I would make tool support part of the workflow’s runtime contract. Before the workflow starts, it should inspect the selected model’s capabilities, decide which paths are available, and state what to do when a required tool is absent. That does not make the agent reliable by itself. It makes an important dependency visible and gives the application a chance to respond deliberately.&lt;/p&gt;
&lt;p&gt;This is a timely design question because Anthropic’s October 6, 2026 announcement introduced server-tool capability fields in its &lt;a href=&quot;https://www.anthropic.com/news/agent-capabilities-api&quot;&gt;Models API update&lt;/a&gt;. The &lt;a href=&quot;https://docs.anthropic.com/en/api/models&quot;&gt;Models API reference&lt;/a&gt; describes support information for server tools such as web search and code execution. That is useful information to consult before building a request, rather than baking a model-specific assumption into a prompt or workflow.&lt;/p&gt;
&lt;h2 id=&quot;discover-then-choose-a-path&quot;&gt;Discover, then choose a path&lt;/h2&gt;
&lt;p&gt;Capability discovery should happen where the application selects or prepares a model, not be scattered through prompts. The workflow can translate provider metadata into a small set of decisions it understands: web search available, code execution available, or neither. Then it can select a supported path before asking the model to act.&lt;/p&gt;
&lt;p&gt;For example, a research workflow might have three legitimate modes. With web search, it can gather current sources. Without it, it can answer only from supplied context, or stop and tell the caller that current research is unavailable. It should not send the same instruction in both cases and hope the model will improvise its way around a missing tool.&lt;/p&gt;
&lt;p&gt;A small adapter keeps provider-specific metadata out of the workflow itself. This TypeScript illustrates the shape of that decision; it is not a claim about a particular SDK’s method names:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ModelInfo&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;  capabilities&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;    server_tools&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;      web_search&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;supported&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;      code_execution&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;supported&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; boolean&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  } &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; null&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ResearchMode&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;search&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;context-only&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;unavailable&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt; chooseResearchMode&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;model&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ModelInfo&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ResearchMode&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; canSearch&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    model.capabilities?.server_tools?.web_search?.supported &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; (canSearch) &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;search&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;context-only&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is not the helper. It is that the result affects the workflow’s behavior. In a production version, I would make the fallback a policy decision rather than quietly choosing &lt;code&gt;context-only&lt;/code&gt; for every task. If the user asks for current information, context-only may be an invalid answer. The workflow should then return an explicit unsupported-capability result, or route to a separately configured search implementation.&lt;/p&gt;
&lt;p&gt;Metadata needs careful interpretation. Anthropic’s documentation says a &lt;code&gt;true&lt;/code&gt; value for web search or code execution means the model supports at least one version of that tool, not necessarily every version. So a capability flag is a useful discovery signal, not a guarantee that any arbitrary tool configuration will work. I would still validate the exact request shape and handle provider errors at invocation time.&lt;/p&gt;
&lt;h2 id=&quot;make-fallback-behavior-part-of-the-contract&quot;&gt;Make fallback behavior part of the contract&lt;/h2&gt;
&lt;p&gt;There is no universal fallback for an unavailable tool. The right response depends on the task and the consequences of being wrong. A coding workflow might be able to continue without code execution if it clearly labels its output as untested. A workflow asked to calculate a result from a large dataset may need to stop rather than offer a guess. A research agent asked for recent facts should not pretend that static context is current.&lt;/p&gt;
&lt;p&gt;I would write those policies down alongside the workflow, in terms an operator can inspect:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Required capability:&lt;/strong&gt; Which tools must be present for this workflow to run as designed?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fallback:&lt;/strong&gt; Can it use a different tool, continue with a narrower answer, or must it stop?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; What should the caller be told when the workflow takes a reduced path?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure handling:&lt;/strong&gt; What happens if metadata says the tool is supported but the actual request is rejected or the tool fails?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A useful distinction is between an optional enhancement and a task prerequisite. If web search only adds citations to an answer that can be grounded entirely in supplied material, it may be optional. If the request is to find the latest documentation, search is a prerequisite. That difference should live in workflow policy, not in the model’s interpretation of an instruction buried in a prompt.&lt;/p&gt;
&lt;p&gt;I also would not silently substitute a different tool just because it appears to serve the same purpose. A provider-hosted search tool and an application-managed search service can have different source coverage, access controls, and result formats. Substitution can be a valid fallback, but it changes the workflow’s behavior and should be intentional.&lt;/p&gt;
&lt;h2 id=&quot;metadata-is-not-a-reliability-mechanism&quot;&gt;Metadata is not a reliability mechanism&lt;/h2&gt;
&lt;p&gt;Discovering capabilities answers a narrow question: what does the provider say this model can support? It does not prove that the tool will be available on this request, that the model will use it appropriately, or that the result will be correct. A supported tool can still fail, return irrelevant material, or be used when the task does not warrant it.&lt;/p&gt;
&lt;p&gt;That is why I would treat capability metadata as an input to workflow selection, not as the workflow’s safety system. The application still needs bounded retries, error handling, and an explicit decision about whether a failed tool call invalidates the answer. If the tool is essential, failure should usually fail the task rather than produce an answer that looks complete. If it is optional, the fallback should be limited and visible.&lt;/p&gt;
&lt;p&gt;The same principle applies when model information cannot be retrieved. I would not interpret missing metadata as support. The workflow can fail closed for required capabilities, use a documented conservative mode for optional ones, or rely on a cached capability record with a clear freshness policy. Which option fits depends on the system; the key is to decide instead of turning an unknown into an implicit yes.&lt;/p&gt;
&lt;p&gt;This boundary also helps when a workflow supports several providers or model families. The provider adapter translates their capability descriptions into the application’s own contract. The workflow then asks for a capability it needs, rather than checking vendor-specific fields throughout its logic. That keeps the policy stable even as model selection changes, while leaving the adapter responsible for understanding each provider’s actual metadata.&lt;/p&gt;
&lt;h2 id=&quot;choose-the-workflow-not-just-the-model&quot;&gt;Choose the workflow, not just the model&lt;/h2&gt;
&lt;p&gt;I would select a model and a workflow path together. A model is not a drop-in replacement if the task depends on a tool it does not support. Conversely, a workflow should not require a tool merely because one convenient model happens to offer it. Make the dependency explicit, check it before execution, and define the reduced mode or refusal path in advance.&lt;/p&gt;
&lt;p&gt;The useful promise of capability discovery is modest but practical: fewer hidden assumptions. It gives the application a chance to say, before execution, whether this model can run this path—and what it will do if it cannot.&lt;/p&gt;</content:encoded><category>ai-engineering</category><category>agent-architecture</category><category>api-design</category></item><item><title>KYAML is a toolchain decision, not just safer syntax</title><link>https://vahidinasab.com/articles/kyaml-is-a-toolchain-decision-not-just-safer-syntax</link><guid isPermaLink="true">https://vahidinasab.com/articles/kyaml-is-a-toolchain-decision-not-just-safer-syntax</guid><description>I explain how to assess KYAML across editors, linters, renderers, policy checks, and deployment workflows before changing repository conventions.</description><pubDate>Mon, 19 Oct 2026 09:49:53 GMT</pubDate><content:encoded>&lt;h2 id=&quot;safer-syntax-is-only-one-part-of-the-decision&quot;&gt;Safer syntax is only one part of the decision&lt;/h2&gt;
&lt;p&gt;KYAML addresses real sources of ambiguity in configuration: it quotes string values and uses braces for maps and brackets for lists. Those conventions make structure more explicit than indentation-sensitive YAML. The &lt;a href=&quot;https://www.kubernetes.dev/resources/keps/5295&quot;&gt;KYAML design proposal&lt;/a&gt; describes the format and its goals.&lt;/p&gt;
&lt;p&gt;The timing is relevant: the &lt;a href=&quot;https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release#kyaml&quot;&gt;Kubernetes v1.37 release announcement&lt;/a&gt; says &lt;code&gt;kubectl get -o kyaml&lt;/code&gt; has reached Stable. But a stable output option is not, by itself, a reason to rewrite manifests or make KYAML the repository standard. It answers whether Kubernetes offers this format; it does not answer whether the rest of a team’s configuration workflow handles it well.&lt;/p&gt;
&lt;p&gt;I would treat adoption as a toolchain compatibility decision. The question is not only whether the API server can accept a document. It is whether the tools that create, edit, validate, transform, review, and deploy that document can work with it consistently—and whether the safety gain is worth supporting another representation.&lt;/p&gt;
&lt;p&gt;That distinction matters because a parser accepting a file is a low bar. A formatter might rewrite it into a different style. A policy check might inspect text instead of parsed data. A deployment wrapper might assume that generated files look a certain way. None of those outcomes means KYAML is invalid; they mean the toolchain’s assumptions need to be checked before repository conventions change.&lt;/p&gt;
&lt;h2 id=&quot;test-the-whole-path-not-just-kubectl&quot;&gt;Test the whole path, not just kubectl&lt;/h2&gt;
&lt;p&gt;The release announcement says KYAML is valid YAML and can be used as input to &lt;code&gt;kubectl&lt;/code&gt;; existing manifests do not have to be rewritten. That makes a staged evaluation possible: keep existing files as they are, and test KYAML where it provides value without forcing a repository-wide migration.&lt;/p&gt;
&lt;p&gt;I would start by tracing a representative manifest through the actual workflow. Include local editing, linting, rendering or generation, policy checks, review, and deployment. The test should exercise the commands and versions used by contributors and automation, rather than relying on a single successful &lt;code&gt;kubectl apply&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For example, a small ConfigMap can show the syntax change without mixing it up with application behavior:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;apiVersion&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;v1&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;kind&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;ConfigMap&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;metadata&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;app-config&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;mode&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;NO&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;true&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would check whether the editor provides useful indentation, completion, and diagnostics for this style; whether the linter parses it and reports the same rules; and whether any renderer or generator preserves the intended format. Then I would run policy checks and the deployment workflow against the rendered result. The aim is not to insist every tool emits KYAML. The aim is to learn where the format enters, where it leaves, and whether a conversion or text-based assumption changes the result.&lt;/p&gt;
&lt;p&gt;I would pay particular attention to scripts that grep, replace, or compare manifest text. Structural tools that parse YAML can work with equivalent data regardless of layout, but a text-based check may depend on line breaks or indentation. That is a design risk to investigate, not a claim that a particular linter or renderer fails. A clear test gives the team evidence about its own toolchain.&lt;/p&gt;
&lt;p&gt;The same applies to review. Compact flow-style documents can make boundaries explicit, but a large nested resource may be harder for a person to scan or review as a diff. I would try the format on the kinds of resources the repository actually maintains. If the syntax reduces ambiguity for one file but makes routine changes harder to understand, that cost belongs in the decision too.&lt;/p&gt;
&lt;h2 id=&quot;mixed-formats-have-a-maintenance-cost&quot;&gt;Mixed formats have a maintenance cost&lt;/h2&gt;
&lt;p&gt;Because KYAML is a YAML subset, the format does not necessarily require a new parser at every stage. But compatibility at the parser boundary is not the same as a consistent authoring and review experience. A team can accept both forms while still needing conventions for which form to write, how generated files should look, and whether a formatter is allowed to convert between them.&lt;/p&gt;
&lt;p&gt;That mixed state can be intentional. For instance, a team might use KYAML for output from a specific workflow while leaving hand-maintained manifests unchanged. The cost is then limited but real: contributors need to recognize both styles, and tests or documentation should make clear where each belongs. If each tool chooses its own representation, review diffs can become noisy and people may start “fixing” formatting rather than discussing the change.&lt;/p&gt;
&lt;p&gt;I would avoid a broad migration until I can answer three questions. Which files are authored by people, and which are generated? Which tool is the source of truth for formatting? And can the repository enforce its chosen convention without custom glue that becomes another thing to maintain? If those answers are unclear, adding a second format is likely to make the workflow less predictable, even if each individual file remains valid.&lt;/p&gt;
&lt;p&gt;A conversion step deserves particular scrutiny. It can establish a consistent output format, but it also adds a boundary where comments, ordering, or formatting expectations may change. If conversion is necessary, I would make it explicit and testable rather than letting different editors and CI jobs make silent choices. If no conversion is needed, I would not add one just to claim that the repository has adopted KYAML.&lt;/p&gt;
&lt;h2 id=&quot;adopt-it-where-the-benefit-pays-for-the-support&quot;&gt;Adopt it where the benefit pays for the support&lt;/h2&gt;
&lt;p&gt;My default would be to evaluate KYAML first as an output format, then consider authoring conventions separately. Stable &lt;code&gt;kubectl&lt;/code&gt; output makes it reasonable to test KYAML in workflows that consume command output. It does not require hand-maintained manifests to change. The &lt;a href=&quot;https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release#kyaml&quot;&gt;Kubernetes release announcement&lt;/a&gt; explicitly notes that existing specifications and pipelines do not need to change for KYAML to be usable.&lt;/p&gt;
&lt;p&gt;I would adopt it for authored files only when the toolchain passes the same practical checks: contributors can edit and review it comfortably, linters and policies evaluate it as intended, renderers behave predictably, and deployment does not depend on an undocumented conversion. I would also document whether both formats are accepted or whether one is the repository standard. An ambiguous “use either” rule often shifts the decision from architecture to individual preference.&lt;/p&gt;
&lt;p&gt;The trade-off is straightforward. KYAML can make some syntax hazards less likely and the structure more explicit. In return, the team may need to update editor guidance, formatting rules, tests, and conventions, or support a period of mixed formats. I would not pay that cost just because the format is stable. I would pay it when a tested workflow shows the safety benefit is meaningful and the surrounding tools can support the choice consistently.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>configuration</category><category>tooling</category></item><item><title>Kubernetes 1.37 makes webhook concurrency an upgrade test</title><link>https://vahidinasab.com/articles/kubernetes-1-37-webhook-concurrency-upgrade-test</link><guid isPermaLink="true">https://vahidinasab.com/articles/kubernetes-1-37-webhook-concurrency-upgrade-test</guid><description>A practical upgrade check for whether your CRD conversion webhook can handle parallel calls during Kubernetes 1.37 API cache initialization.</description><pubDate>Mon, 19 Oct 2026 09:49:51 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-upgrade-changes-the-shape-of-the-load&quot;&gt;The upgrade changes the shape of the load&lt;/h2&gt;
&lt;p&gt;A conversion webhook that works reliably when calls arrive one at a time may behave differently when the API server calls it concurrently. That is the upgrade risk I would test before moving to Kubernetes 1.37.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/&quot;&gt;Kubernetes 1.37 release announcement&lt;/a&gt; describes concurrent decoding and transformation of watch events during watch-cache initialization. For CRDs that need conversion, this can mean up to ten conversion calls running concurrently against a webhook during cache initialization, rather than one at a time. The announcement says the total call volume is unchanged; the change is how many calls can be in flight together.&lt;/p&gt;
&lt;p&gt;That distinction matters. A webhook can process the same work successfully in total and still fail when the work is concentrated into a shorter interval. A limited worker pool, a connection limit, a serialized code path, or a downstream database that cannot absorb parallel queries can turn concurrent requests into longer queues, timeouts, or errors. Those are risks to check in your own system, not outcomes guaranteed by the release.&lt;/p&gt;
&lt;p&gt;I would make this a workload-level upgrade check, not just a question of whether the control plane starts. The release makes cache initialization a specific time to exercise the conversion path under parallel load. If a CRD is served in a version different from its stored version, conversion may be needed when the API server reads those objects; the &lt;a href=&quot;https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/&quot;&gt;Kubernetes documentation on CRD version conversion&lt;/a&gt; explains how webhook conversion fits into that process.&lt;/p&gt;
&lt;h2 id=&quot;test-the-path-you-actually-depend-on&quot;&gt;Test the path you actually depend on&lt;/h2&gt;
&lt;p&gt;Start by identifying which CRDs use webhook conversion and which version transitions matter in your cluster. A webhook may be healthy in a basic readiness check but still fail on a conversion request that exercises a particular resource shape or version pair. The test should use representative objects and the conversion directions your clients and controllers actually rely on.&lt;/p&gt;
&lt;p&gt;Then run the check in an environment where you can observe API-server cache initialization without putting production availability at risk. Exercise the webhook while the relevant cache is cold or initializing, and make calls overlap rather than arriving serially. The point is not to reproduce a whole cluster perfectly; it is to see whether the conversion service and its dependencies behave correctly when concurrent work reaches them.&lt;/p&gt;
&lt;p&gt;I would record at least these signals during the test:&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What to observe&lt;/th&gt;&lt;th&gt;What it can reveal&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;In-flight requests and queue depth&lt;/td&gt;&lt;td&gt;Whether parallel calls exceed the webhook’s worker or connection capacity&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Request latency, including tail latency&lt;/td&gt;&lt;td&gt;Whether queuing or downstream contention makes conversions too slow&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Conversion errors and timeouts&lt;/td&gt;&lt;td&gt;Whether the service fails under overlap, even if individual requests work alone&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CPU, memory, and downstream resource use&lt;/td&gt;&lt;td&gt;Whether the webhook’s dependencies become the actual bottleneck&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;API-server errors and cache initialization progress&lt;/td&gt;&lt;td&gt;Whether webhook trouble affects the control-plane operation being tested&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;These are observations, not pass criteria by themselves. Set thresholds from the service’s existing SLOs and operational limits. If the webhook has a deliberate concurrency cap, test that cap under the expected burst instead of assuming the Kubernetes-side change will respect it. If the webhook fans out to a database or another service, include those dependencies in the observation: a fast handler that transfers the queue downstream has not solved the capacity problem.&lt;/p&gt;
&lt;h2 id=&quot;decide-what-failure-looks-like-before-the-test&quot;&gt;Decide what failure looks like before the test&lt;/h2&gt;
&lt;p&gt;A successful test is not just one where the webhook returns valid conversions. I would also decide in advance what behavior counts as a failure: latency beyond the webhook’s budget, growing queues that do not drain, resource saturation, conversion errors, or API-server initialization that stalls. A test without explicit failure signals can pass while merely shifting the bottleneck somewhere less visible.&lt;/p&gt;
&lt;p&gt;I would test failure behavior deliberately in a controlled environment. For example, observe what happens when the webhook approaches its capacity limit or a dependency becomes slow. Does the service reject or time out requests predictably? Does recovery clear the queue, or do retries and overlapping work keep it saturated? The goal is not to provoke an outage for its own sake. It is to find out whether the system fails in a bounded, diagnosable way before the upgrade makes this load pattern relevant.&lt;/p&gt;
&lt;p&gt;Be careful not to apply admission-webhook settings to conversion webhooks by analogy. Kubernetes documents &lt;code&gt;failurePolicy&lt;/code&gt; and &lt;code&gt;timeoutSeconds&lt;/code&gt; for admission webhooks, but those are not a general configuration switch for CRD conversion. For conversion, inspect the CRD’s webhook configuration and the behavior of the conversion service itself; the &lt;a href=&quot;https://kubernetes.io/docs/reference/kubernetes-api/apiextensions/custom-resource-definition-v1/&quot;&gt;CRD API reference&lt;/a&gt; documents the conversion webhook fields. The useful question is how a failed or slow conversion affects the API operations that need it, not whether a setting with a familiar name exists elsewhere.&lt;/p&gt;
&lt;h2 id=&quot;make-the-result-an-upgrade-gate&quot;&gt;Make the result an upgrade gate&lt;/h2&gt;
&lt;p&gt;I would treat the result as a concrete upgrade gate for the webhook, separate from general release readiness. The release notes provide a workload-specific change to test: concurrent conversion calls during cache initialization. That makes it possible to ask a direct question of the system: can this webhook, with its actual limits and dependencies, handle overlapping conversion work without unacceptable latency or failure?&lt;/p&gt;
&lt;p&gt;If it cannot, I would fix the bottleneck or adjust capacity before upgrading. If it can, keep the test and its observations with the upgrade evidence so the result is repeatable when the webhook, its dependencies, or the cluster changes. The relevant result is not that Kubernetes 1.37 initializes its cache faster; it is that the conversion service stays within its own operating limits when initialization sends work in parallel.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>reliability</category><category>architecture</category></item><item><title>PostgreSQL 19 WAIT makes read-your-writes a contract</title><link>https://vahidinasab.com/articles/postgresql-19-wait-read-your-writes-contract</link><guid isPermaLink="true">https://vahidinasab.com/articles/postgresql-19-wait-read-your-writes-contract</guid><description>I explain when waiting for a PostgreSQL replica beats routing reads to the primary, and how to test the latency, timeout, and fallback behavior.</description><pubDate>Mon, 19 Oct 2026 09:49:47 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-replica-read-needs-a-decision&quot;&gt;A replica read needs a decision&lt;/h2&gt;
&lt;p&gt;A user updates a record and immediately opens it again. If the application sends that second read to an asynchronous standby, it may see the old value: the primary has committed the write, but the standby has not replayed it yet. PostgreSQL’s Beta 4 announcement describes the new &lt;code&gt;WAIT FOR&lt;/code&gt; command as a way to wait for a standby to reach a chosen log sequence number before running a &lt;code&gt;SELECT&lt;/code&gt; (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386&quot;&gt;PostgreSQL 19 Beta 4 announcement&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;That gives me a useful alternative to routing every read after a write back to the primary. But I would not treat it as a free consistency switch. The application is choosing to spend time waiting on the replica, and needs a defined answer for what happens when that wait takes too long or cannot succeed.&lt;/p&gt;
&lt;p&gt;I would use the wait when the user’s next read needs to reflect a particular committed write, and when keeping that read on the replica is worth a bounded delay. If the screen needs an immediate response more than it needs to use a replica, routing to the primary is simpler. If the read is unrelated to the write, waiting buys nothing. The decision belongs to the request path that knows whether this read depends on that write.&lt;/p&gt;
&lt;h2 id=&quot;carry-the-writes-position-to-the-read&quot;&gt;Carry the write’s position to the read&lt;/h2&gt;
&lt;p&gt;The wait needs a target LSN: a position in the WAL stream that represents progress the standby must reach. The safe shape is to commit the write on the primary, obtain a position at or after that commit, then wait on the standby before issuing the dependent read. PostgreSQL documents &lt;code&gt;WAIT&lt;/code&gt; modes for waiting until WAL is written, flushed, or replayed, and the default mode is &lt;code&gt;standby_replay&lt;/code&gt; (&lt;a href=&quot;https://www.postgresql.org/docs/19/sql-wait.html&quot;&gt;PostgreSQL 19 &lt;code&gt;WAIT&lt;/code&gt; command&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;For example, the application can keep the write and follow-up read on separate steps:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;-- On the primary, after the write transaction has committed.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; pg_current_wal_lsn();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;-- On the standby, using the LSN returned above.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;WAIT &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;FOR&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; LSN &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&apos;0/306EE20&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;  WITH&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; (MODE &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&apos;standby_replay&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;TIMEOUT&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;250ms&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, NO_THROW);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;-- Only after a successful wait, issue the dependent SELECT.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; id, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;status&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; 42&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The LSN shown is an example, not a value to reuse. In a real application, pass the LSN returned by the primary to the replica connection. PostgreSQL documents &lt;code&gt;pg_current_wal_lsn()&lt;/code&gt; as returning the current write-ahead log insert location (&lt;a href=&quot;https://www.postgresql.org/docs/19/functions-admin.html&quot;&gt;PostgreSQL WAL administration functions&lt;/a&gt;). Calling it after commit can give the application a conservative target: the position may include WAL beyond its own transaction, so the replica might wait for a little more progress than strictly necessary. I would rather make that trade-off explicit than pretend the target is an exact marker for only one row change.&lt;/p&gt;
&lt;p&gt;The read should use a fresh statement context after the wait. A wait cannot make an already established snapshot show a later commit. In particular, I would not place the wait inside a long-running transaction that has already taken the snapshot the read will use. I would also verify the behavior with the application’s actual transaction isolation and connection-pooling setup, rather than assuming that the SQL sequence alone guarantees a fresh view.&lt;/p&gt;
&lt;h2 id=&quot;a-timeout-is-part-of-the-api&quot;&gt;A timeout is part of the API&lt;/h2&gt;
&lt;p&gt;The example sets a timeout because the unbounded default is a poor fit for a user-facing request. The PostgreSQL &lt;code&gt;WAIT&lt;/code&gt; documentation says that a missing or zero timeout waits indefinitely; with &lt;code&gt;NO_THROW&lt;/code&gt;, the command reports the outcome as a status rather than raising an error (&lt;a href=&quot;https://www.postgresql.org/docs/19/sql-wait.html&quot;&gt;&lt;code&gt;WAIT&lt;/code&gt; timeout and status behavior&lt;/a&gt;). That turns an implementation detail into a product decision: how long is this request allowed to wait, and what should the caller do if the replica has not caught up by then?&lt;/p&gt;
&lt;p&gt;I would make the timeout shorter than the remaining request budget, leaving room for the read itself and for the application to handle the result. The value in the example is illustrative; I would choose a real limit from the service’s latency budget and test it against observed lag. A timeout is not a consistency success. If the application proceeds to read the standby after a timeout, it may return stale data. That can be acceptable for some screens, but it must be an intentional fallback rather than an accidental consequence of swallowing an error.&lt;/p&gt;
&lt;p&gt;There are a few honest choices after a timeout. The application can retry the wait if its remaining deadline allows; route the dependent read to the primary; return a response that asks the user to retry; or accept a potentially stale read where that is safe. I would generally route to the primary for a user action that explicitly expects its own update to appear, provided the primary is available and the request still has time. For a less critical view, I might return the latest replica value and communicate that it can lag. I would avoid automatic retries without a total deadline: repeated waits can turn replica lag into a request that occupies a connection for far longer than the caller expects.&lt;/p&gt;
&lt;h2 id=&quot;test-the-lag-not-just-the-happy-path&quot;&gt;Test the lag, not just the happy path&lt;/h2&gt;
&lt;p&gt;The new command can reduce application-side polling and make the consistency boundary clearer. It does not remove the need to decide what the application promises when replicas lag. PostgreSQL’s Beta 4 release notes also caution that beta features may change before general availability (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386&quot;&gt;PostgreSQL 19 Beta 4 announcement&lt;/a&gt;), so I would test against the exact PostgreSQL build planned for deployment and check the final documentation before relying on the syntax or behavior in production.&lt;/p&gt;
&lt;p&gt;I would test the whole request path with controlled replica lag: write on the primary, capture the target LSN, wait on the intended standby, and perform the dependent read only when the wait succeeds. Then I would exercise timeout and connection failure paths, including the fallback route, and verify that the application does not silently claim read-your-writes after a failed wait. I would also check what happens when a connection is sent to a server that is no longer operating as a standby; the documented standby wait modes can report an error if the server is promoted before reaching the target LSN (&lt;a href=&quot;https://www.postgresql.org/docs/19/sql-wait.html&quot;&gt;&lt;code&gt;WAIT&lt;/code&gt; command behavior&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The choice is not “replicas or consistency.” It is whether this particular read should wait for a replica, how much of the request budget that wait may consume, and what the application promises when the budget runs out. &lt;code&gt;WAIT&lt;/code&gt; makes that contract visible in SQL. It does not make the contract optional.&lt;/p&gt;</content:encoded><category>postgresql</category><category>replication</category><category>consistency</category></item><item><title>Validate .NET 11 tracing before changing instrumentation</title><link>https://vahidinasab.com/articles/validate-dotnet-11-tracing-before-changing-instrumentation</link><guid isPermaLink="true">https://vahidinasab.com/articles/validate-dotnet-11-tracing-before-changing-instrumentation</guid><description>A practical comparison of ASP.NET Core’s built-in traces and existing application spans helps identify duplication, missing context, and what to keep.</description><pubDate>Mon, 19 Oct 2026 09:49:43 GMT</pubDate><content:encoded>&lt;h2 id=&quot;treat-the-preview-as-an-evaluation-prompt&quot;&gt;Treat the preview as an evaluation prompt&lt;/h2&gt;
&lt;p&gt;.NET 11 Preview 2 announced native OpenTelemetry tracing for ASP.NET Core on March 10, 2026 (&lt;a href=&quot;https://devblogs.microsoft.com/dotnet/dotnet-11-preview-2&quot;&gt;the .NET 11 Preview 2 announcement&lt;/a&gt;). I would treat that as a reason to compare traces in a test environment, not as a reason to remove the instrumentation a service already relies on.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11?view=aspnetcore-10.0&quot;&gt;ASP.NET Core .NET 11 release notes&lt;/a&gt; describe subscribing to the &lt;code&gt;Microsoft.AspNetCore&lt;/code&gt; activity source to collect built-in request traces. They also say the framework populates HTTP semantic-convention attributes on the request activity and that the &lt;code&gt;OpenTelemetry.Instrumentation.AspNetCore&lt;/code&gt; package is not needed for that collection path. That is a useful change in where request instrumentation comes from. It does not answer whether the resulting trace has the shape my application needs, or whether other parts of my telemetry setup are still required.&lt;/p&gt;
&lt;p&gt;I would compare the old and new paths against the same request scenarios and inspect the exported traces side by side. The useful questions are practical: Is there one server span or more than one? Does it have the fields my dashboards and queries use? Do custom operations appear beneath the request in the right order? What important business context is still absent? Until I can answer those, I would not change production instrumentation.&lt;/p&gt;
&lt;h2 id=&quot;make-the-comparison-controlled&quot;&gt;Make the comparison controlled&lt;/h2&gt;
&lt;p&gt;Use a small test app or an isolated environment with the same relevant middleware, route handlers, custom spans, and telemetry exporter as the service under consideration. Send a repeatable request through each configuration. Keep the request and application behavior the same; change only which ASP.NET Core instrumentation path is enabled. Otherwise, differences in the trace could come from the test rather than the instrumentation.&lt;/p&gt;
&lt;p&gt;For example, a service using the OpenTelemetry instrumentation package might configure tracing along these lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;builder.Services.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddOpenTelemetry&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;WithTracing&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;tracing&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; tracing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddAspNetCoreInstrumentation&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddSource&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Orders&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddOtlpExporter&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The candidate built-in path subscribes to ASP.NET Core’s activity source instead. Keep the application source and exporter in place so the comparison isolates the server instrumentation:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;builder.Services.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddOpenTelemetry&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;WithTracing&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;tracing&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; tracing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddSource&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Microsoft.AspNetCore&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddSource&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Orders&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AddOtlpExporter&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;());&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These snippets show the distinction documented in the &lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-11?view=aspnetcore-10.0&quot;&gt;ASP.NET Core tracing guidance&lt;/a&gt;: the built-in path subscribes to &lt;code&gt;Microsoft.AspNetCore&lt;/code&gt;; the existing package registers its instrumentation for you. They are alternatives for collecting the server request activity, not a reason to remove unrelated sources or exporters. I would test them separately first. If both paths are active during a test, inspect whether they produce duplicate request spans rather than assuming either that duplication must happen or that it cannot.&lt;/p&gt;
&lt;p&gt;Pick representative requests, not just the easiest successful route. Include a request that returns an error, one that makes an outbound call, and one that exercises application-specific work if the service has those paths. The point is not to benchmark the preview. It is to learn whether the trace still explains the work the team needs to understand.&lt;/p&gt;
&lt;h2 id=&quot;compare-trace-shape-then-attributes&quot;&gt;Compare trace shape, then attributes&lt;/h2&gt;
&lt;p&gt;Start with the tree. For each request, check the root span, its parent-child relationships, and the number of server request spans. A repeated span representing the same inbound request can make a trace confusing and distort counts in tooling. A missing request span, or one that is detached from the expected trace, is a different failure. I would record what I actually observe in the exported data rather than infer it from the configuration.&lt;/p&gt;
&lt;p&gt;Then compare the request attributes that matter to the team. The release notes list examples such as &lt;code&gt;http.request.method&lt;/code&gt;, &lt;code&gt;url.path&lt;/code&gt;, &lt;code&gt;http.response.status_code&lt;/code&gt;, and &lt;code&gt;server.address&lt;/code&gt;. Check the fields your dashboards, alerts, and search queries depend on; do not assume that a familiar key or value survives a change in instrumentation. Also check whether sensitive values or high-cardinality details are being captured. The instrumentation source is not a substitute for deciding what telemetry is appropriate to export.&lt;/p&gt;
&lt;p&gt;Finally, compare the custom spans. A server span can describe the HTTP request without explaining the application operation that matters to someone debugging it. If a trace currently has a span for a meaningful operation, verify that it remains present and correctly parented. If it does, keep it. If an old custom span merely duplicates generic request bookkeeping that the framework now provides, that may be a candidate to simplify—but only after confirming that no team or telemetry query relies on it.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-collection-walkthroughs&quot;&gt;OpenTelemetry distributed tracing walkthrough for .NET&lt;/a&gt; shows the established pattern of collecting ASP.NET Core activities alongside other sources. That separation is useful: framework instrumentation can describe the request boundary, while application-owned instrumentation can explain domain work. I would preserve that distinction in the review instead of treating “native tracing” as an instruction to delete all manual spans.&lt;/p&gt;
&lt;h2 id=&quot;decide-from-evidence-not-from-the-word-native&quot;&gt;Decide from evidence, not from the word native&lt;/h2&gt;
&lt;p&gt;I would capture a short comparison record before changing the service: which configuration produced each trace, whether the request span count changed, which attributes changed, whether parentage stayed intact, and which custom spans remain necessary. A trace viewer screenshot or exported trace is more useful here than a checklist marked “enabled.” Keep the record specific to the test; don’t turn one route’s result into a claim about every service.&lt;/p&gt;
&lt;p&gt;If the built-in path produces the expected request activity and attributes without duplicate spans, then removing the ASP.NET Core instrumentation package may be a reasonable follow-up. Confirm what else that package configuration or adjacent setup was responsible for before removing it. If the trace is missing something the team needs, retain the current path while investigating the gap, or adapt the configuration after verifying the supported options. If both paths produce overlapping request spans, keep only the path that gives the desired trace and test again.&lt;/p&gt;
&lt;p&gt;I would make the production change separately from the evaluation. Update the target framework and instrumentation in a controlled change, then verify exported traces after deployment. A preview announcement is a useful signal to investigate, not production evidence. The decision should be based on whether the resulting trace remains readable and useful—not on whether the instrumentation now ships with ASP.NET Core.&lt;/p&gt;</content:encoded><category>dotnet</category><category>observability</category><category>opentelemetry</category><category>architecture</category></item><item><title>Test Claude Code hooks as behavior contracts</title><link>https://vahidinasab.com/articles/test-claude-code-hooks-as-behavior-contracts</link><guid isPermaLink="true">https://vahidinasab.com/articles/test-claude-code-hooks-as-behavior-contracts</guid><description>I explain why natural-language hooks need adversarial regression tests and where deterministic controls must enforce policy outside the agent.</description><pubDate>Mon, 19 Oct 2026 09:49:39 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-sentence-is-not-an-enforcement-boundary&quot;&gt;A sentence is not an enforcement boundary&lt;/h2&gt;
&lt;p&gt;A hook that says “block commands that delete production data” sounds like a policy. It is not proof that the action will be blocked. I would treat that sentence as a behavior contract: a claim about what the system should do that needs tests against what it actually does.&lt;/p&gt;
&lt;p&gt;That distinction became concrete in Claude Code’s &lt;a href=&quot;https://github.com/anthropics/claude-code/releases/tag/v2.1.294&quot;&gt;v2.1.294 release notes&lt;/a&gt;, published October 8, 2026. The notes report a fix for prompt and agent hooks written as instructions, such as “Block commands that…”, which had allowed operations they were meant to block. The important lesson is not that a particular version fixed a particular bug. It is that wording a rule in natural language does not, by itself, make that rule reliably enforced.&lt;/p&gt;
&lt;p&gt;I would not remove natural-language hooks. They can express intent, add useful review, and guide an agent toward safer choices. I would stop treating them as the only barrier around an operation whose execution must be prevented. If the consequence of a miss matters, enforcement belongs at a boundary that can refuse the operation without asking the model to interpret the rule correctly.&lt;/p&gt;
&lt;h2 id=&quot;turn-the-instruction-into-a-testable-contract&quot;&gt;Turn the instruction into a testable contract&lt;/h2&gt;
&lt;p&gt;A useful contract names the action, the conditions under which it must be blocked, the cases that should remain allowed, and what observable result counts as a block. “Be careful with destructive commands” is not testable. “A tool request that would delete the protected directory is denied before execution” is.&lt;/p&gt;
&lt;p&gt;For a hook intended to block shell commands, I would write down a small test plan before relying on it:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Contract: requests that can delete protected data must not execute.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Block:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- A direct deletion command targeting the protected directory.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- The same action with flags or whitespace rearranged.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- A shell command that invokes the deletion through a wrapper.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- A command whose arguments combine a harmless operation with the protected action.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Allow:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- A read-only listing of the protected directory.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- A deletion targeting an explicitly permitted temporary directory.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Observe:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Whether the tool action was denied before execution.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- Whether an independent marker confirms that the simulated action did not run.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a test fixture, not hook syntax. Its purpose is to make the intended behavior reviewable by someone who did not write the instruction. It also avoids a common testing mistake: trying one obvious prohibited phrasing and concluding that the rule is complete. A natural-language hook may respond differently when the same intent appears inside a longer command, is described indirectly, or is paired with a plausible reason to proceed.&lt;/p&gt;
&lt;p&gt;I would run these cases in an isolated test setup with inert targets, never against real data. For each hook change, I would keep both prohibited and permitted cases. A policy that blocks everything may pass a one-sided test while making the workflow unusable; a policy that allows the expected safe case but misses a transformed unsafe one is not a dependable guard. The contract should test the boundary, not just whether the hook emits a convincing explanation.&lt;/p&gt;
&lt;p&gt;I would also include an explicit adversarial case: the agent receives untrusted text that asks it to ignore the restriction or claims the operation is approved. The test is not about guessing every possible prompt injection. It checks whether the policy still holds when the instruction is challenged by competing text. OWASP’s &lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/LLM_Prompt_Injection_Prevention_Cheat_Sheet.html&quot;&gt;LLM Prompt Injection Prevention guidance&lt;/a&gt; recommends enforcing permissions in execution code rather than relying on prompt wording; that is the same boundary I want the test to expose.&lt;/p&gt;
&lt;h2 id=&quot;put-deterministic-enforcement-where-the-action-happens&quot;&gt;Put deterministic enforcement where the action happens&lt;/h2&gt;
&lt;p&gt;I separate two questions that are easy to conflate. Can the agent be instructed not to propose an action? And can the system prevent the action from executing if the agent proposes it anyway? The first is useful guidance. The second is a security property.&lt;/p&gt;
&lt;p&gt;Claude Code documents blocking behavior for hooks such as &lt;code&gt;PreToolUse&lt;/code&gt;, including denial through a hook decision; its &lt;a href=&quot;https://code.claude.com/docs/en/hooks#exit-code-output&quot;&gt;hooks reference&lt;/a&gt; describes the supported blocking behavior and exit-code handling. That gives a project a mechanism to build a deterministic check around a tool call. It still needs careful implementation and tests: the right hook must run for the relevant action, its result must be interpreted as intended, and failure behavior must not silently turn a deny into an allow.&lt;/p&gt;
&lt;p&gt;For an important restriction, I would layer controls. The natural-language hook can explain the rule and steer the agent away from a prohibited request. A command hook or other executable policy can inspect the actual tool input and reject known forbidden actions. The operating system, database, or service account should have only the permissions needed for the task, so a mistake in the agent layer does not grant powers that the task never required. OWASP’s guidance also recommends least privilege and validating tool calls against permissions outside the model.&lt;/p&gt;
&lt;p&gt;These layers have different costs. A prompt-based rule is easy to change and can express context that is awkward to encode. A deterministic check takes more care to implement and may be narrower than the policy people actually mean. Permission scoping can constrain legitimate work if it is too tight. I would accept those costs for actions with meaningful consequences, and keep the model’s judgment for decisions where a mistaken interpretation is recoverable.&lt;/p&gt;
&lt;p&gt;The key is not to call every hook deterministic simply because it runs at a named point in a workflow. I would ask what component makes the allow-or-deny decision, what input it evaluates, and what happens when that component errors or cannot run. If the answer is “the agent reads the instruction and should comply,” then I have a behavioral expectation, not an independent security boundary.&lt;/p&gt;
&lt;h2 id=&quot;make-regressions-visible&quot;&gt;Make regressions visible&lt;/h2&gt;
&lt;p&gt;A test is useful only if it runs again after the behavior changes. I would keep hook contract cases next to the hook configuration or its policy code, run them when either changes, and include them in the upgrade check for Claude Code. The release note is a reminder that behavior can change across versions; a passing test on one version is not a permanent guarantee about all later versions.&lt;/p&gt;
&lt;p&gt;For each case, I would record the input, the expected decision, and whether the simulated action actually ran. I would test the hook directly where practical, then verify the complete path through the tool integration. A unit test can show that a matcher rejects a string; it cannot alone show that the correct hook is registered or that the tool runner honors the rejection. Conversely, an end-to-end test with only one command may miss the input variations that matter. Both views are valuable because they catch different failures.&lt;/p&gt;
&lt;p&gt;I would keep a deliberate safe case alongside every block case and make test failures easy to diagnose. If a previously blocked case starts passing after a configuration edit or version update, that is a regression to investigate before relying on the hook. If a safe case starts blocking, the policy may have become broader than intended. The goal is not to prove that every imaginable adversarial input has been covered; it is to make known policy expectations executable and to expose uncertainty before people mistake a prompt for a guardrail.&lt;/p&gt;
&lt;p&gt;Natural-language hooks are worth using as instructions. They are not a substitute for enforcement. I would trust them to communicate intent, test them as behavior contracts, and put the final deny decision outside the agent whenever a missed block would matter.&lt;/p&gt;</content:encoded><category>claude-code</category><category>ai-assisted-engineering</category><category>security</category></item><item><title>Trace agent work without mistaking telemetry for evaluation</title><link>https://vahidinasab.com/articles/trace-agent-work-without-mistaking-telemetry-for-evaluation</link><guid isPermaLink="true">https://vahidinasab.com/articles/trace-agent-work-without-mistaking-telemetry-for-evaluation</guid><description>I explain how agent traces can help connect requests and diagnose failures, while keeping output quality, data exposure and access control as separate concerns.</description><pubDate>Mon, 19 Oct 2026 09:49:37 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-trace-answers-a-narrower-question&quot;&gt;A trace answers a narrower question&lt;/h2&gt;
&lt;p&gt;On October 14, 2026, Claude Platform announced beta OpenTelemetry export for Messages API requests and Managed Agents sessions, with SDK spans available in several languages, including C# (&lt;a href=&quot;https://platform.claude.com/docs/en/release-notes/overview&quot;&gt;Claude Platform release notes&lt;/a&gt;). That creates a useful design question: how do I make an agent’s work visible in the operational trace for the application around it?&lt;/p&gt;
&lt;p&gt;My answer starts with a boundary. A trace can help explain what happened during a request: which steps ran, where time was spent, and where an error surfaced. It cannot establish that the agent’s answer was correct, safe, or useful. Those are different questions and need different evidence.&lt;/p&gt;
&lt;p&gt;I find it tempting to treat a detailed trace as a kind of record of truth. It is not. A span records an instrumented operation and its attributes; it does not independently validate the operation’s result. A successful model request can produce a wrong answer. A tool call can complete without doing what the user intended. A trace that ends without an error says something about execution, not quality.&lt;/p&gt;
&lt;p&gt;That distinction matters when people use observability dashboards to make decisions about an AI feature. If I want to diagnose a timeout or a failed tool call, spans are a good place to start. If I want to know whether the system followed a policy or answered correctly, I need an evaluation process that checks outputs against explicit criteria. Mixing those goals into one “agent health” signal risks making operational success look like product success.&lt;/p&gt;
&lt;h2 id=&quot;join-the-agent-to-the-application-trace&quot;&gt;Join the agent to the application trace&lt;/h2&gt;
&lt;p&gt;Agent activity is rarely the whole request. A user may arrive through an API, pass through application logic, trigger a Managed Agents session, and then wait for the result to be stored or returned. Looking only at the agent’s spans leaves the caller’s work outside the picture; looking only at the application span hides what happened inside the agent.&lt;/p&gt;
&lt;p&gt;Claude Platform’s &lt;a href=&quot;https://platform.claude.com/docs/en/manage-claude/opentelemetry/api&quot;&gt;OpenTelemetry API guide&lt;/a&gt; describes connecting API spans to application traces: call the SDK inside an application span, propagate trace context, and send the spans to the same observability backend. The &lt;a href=&quot;https://platform.claude.com/docs/en/manage-claude/opentelemetry/sdk&quot;&gt;Claude SDK tracing guide&lt;/a&gt; also describes SDK spans nested in application traces. I would use that as the target shape: the application operation provides the context, and the provider and tool activity appears beneath it where propagation works.&lt;/p&gt;
&lt;p&gt;That last qualification is important. I would not assume every operation automatically joins the same trace just because all the data reaches one backend. Trace context needs to make it across the boundary. For a non-SDK client, the API guide shows copying the active context into request headers; it also notes that W3C trace context should be available when an application uses a different propagator. I would test the trace shape with a real request path before relying on it in a dashboard. A disconnected span may still be useful, but it is not the end-to-end view I intended.&lt;/p&gt;
&lt;p&gt;For a Managed Agents session that continues across separate requests, I would also keep the application’s own correlation model clear. The &lt;a href=&quot;https://platform.claude.com/docs/en/manage-claude/opentelemetry&quot;&gt;Claude Platform OpenTelemetry reference&lt;/a&gt; describes a conversation identifier that can group Managed Agents spans across traces. That is useful for finding related work, but I would not confuse a conversation grouping with a single end-to-end trace. A trace answers “what happened in this operation?” A conversation identifier can help answer “which operations belong to this interaction?” They are related links, not interchangeable concepts.&lt;/p&gt;
&lt;h2 id=&quot;record-deliberately-not-exhaustively&quot;&gt;Record deliberately, not exhaustively&lt;/h2&gt;
&lt;p&gt;The first question I would ask before enabling a new telemetry destination is not “Can I capture everything?” It is “Which failure do I need to diagnose, and what is the least data that lets me do it?” A trace can reveal more than timing and status if attributes, prompts, tool inputs, or outputs are attached. That may make debugging easier, but it can also put sensitive user or business data in a system with a different audience and retention policy than the application itself.&lt;/p&gt;
&lt;p&gt;I would define a collection policy before turning on broad capture. Start with operation names, durations, status, and carefully selected identifiers that help connect work. Decide separately whether any prompt or result content is necessary. If content is not needed to answer a known diagnostic question, do not record it by default. If it is needed for a narrow investigation, I would treat that as a deliberate exception, with access and retention reviewed rather than assuming that the observability platform is an appropriate place for unrestricted conversation data.&lt;/p&gt;
&lt;p&gt;The same care applies to baggage and propagated context. Context helps join spans, but I would avoid casually placing user-supplied values or secrets into fields that travel between services. I would keep identifiers opaque where possible and consider whether the people who can search the trace backend should be allowed to see the underlying interaction. These are design choices about data exposure, not small implementation details.&lt;/p&gt;
&lt;p&gt;I would also keep the exporter boundary visible in the architecture. Claude Platform’s &lt;a href=&quot;https://platform.claude.com/docs/en/manage-claude/opentelemetry/api&quot;&gt;OpenTelemetry setup guide&lt;/a&gt; describes configuring a destination in the Claude Console for API spans. The application’s SDK spans and provider-generated spans may therefore have different configuration points, even when they eventually appear together. I would document who can change each destination, which environment it applies to, and how to disable or restrict export if the data policy changes. A trace is only useful operationally if its route and audience are understood.&lt;/p&gt;
&lt;h2 id=&quot;keep-evaluation-as-a-separate-system&quot;&gt;Keep evaluation as a separate system&lt;/h2&gt;
&lt;p&gt;I would not put correctness into the trace by attaching a vague “good answer” label and calling that evaluation. Operational telemetry should help me see the path taken and investigate its failures. Evaluation should define what counts as an acceptable result, how that judgment is made, and how the result informs a release or a product decision.&lt;/p&gt;
&lt;p&gt;Those systems can be connected without being conflated. A trace identifier can help an investigator find the execution behind an evaluation result, provided the identifier is handled safely and retained appropriately. Evaluation data can tell me that a class of outputs needs attention; the trace can help explain how one example was produced. Neither replaces the other. The trace does not supply a correctness label, and the evaluation does not necessarily explain a timeout, a broken dependency, or a failed tool operation.&lt;/p&gt;
&lt;p&gt;I am still exploring how best to evaluate managed-agent behavior in real systems, so I would be cautious about prescribing a universal evaluation design. The operational boundary seems clearer: instrument the request path, connect spans where context is available, and keep the collected data limited to what the diagnostic use requires. Then define output evaluation on its own terms, rather than treating a clean trace as evidence that the agent did good work.&lt;/p&gt;
&lt;p&gt;That separation gives traces a valuable but honest role. They make execution legible; they do not certify the result.&lt;/p&gt;</content:encoded><category>observability</category><category>opentelemetry</category><category>ai</category></item><item><title>Plan GitHub Actions investigations around evidence</title><link>https://vahidinasab.com/articles/plan-github-actions-investigations-around-evidence</link><guid isPermaLink="true">https://vahidinasab.com/articles/plan-github-actions-investigations-around-evidence</guid><description>I explain how to define the evidence, retention and access an incident investigation needs before relying on GitHub Actions Data Stream.</description><pubDate>Mon, 19 Oct 2026 09:49:34 GMT</pubDate><content:encoded>&lt;h2 id=&quot;start-with-the-questions-not-the-stream&quot;&gt;Start with the questions, not the stream&lt;/h2&gt;
&lt;p&gt;A security investigation is only as good as the evidence it can still retrieve. Before I adopt a new feed from a CI platform, I would write down what an investigator must be able to establish after a workflow incident: which workflow ran, what code and dependencies it used, what it changed, and which identity or event started it. Then I would ask whether the record answers those questions after ordinary run logs have been changed or removed.&lt;/p&gt;
&lt;p&gt;That distinction matters in the GitHub Actions roadmap discussion. GitHub describes the forthcoming Actions Data Stream as near-real-time execution telemetry delivered centrally to destinations such as Amazon S3 and Azure Event Hub or Data Explorer in its &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap&quot;&gt;2026 Actions security roadmap&lt;/a&gt;. In a &lt;a href=&quot;https://github.com/orgs/community/discussions/190621&quot;&gt;GitHub community discussion about the roadmap&lt;/a&gt;, a GitHub representative describes the intended stream as an immutable source of truth for workflow activity and a replacement for run logs in security investigations.&lt;/p&gt;
&lt;p&gt;That is a promising direction, not an investigation plan. “Immutable” does not tell me which events are recorded, what fields they contain, how I correlate them, how long they remain available, or which roles can change the record or its destination. I would not treat the label as proof that the evidence is complete or durable. I would use it to ask sharper questions of the proposed feature and of my own operating model.&lt;/p&gt;
&lt;h2 id=&quot;write-the-investigation-contract&quot;&gt;Write the investigation contract&lt;/h2&gt;
&lt;p&gt;I would begin with a small set of investigation questions, phrased so that someone who did not build the workflow can answer them. For example: Which workflow and job ran? What revision and dependency references did the run resolve? Which actor, trigger and repository initiated it? What runner executed it? Which steps completed, failed or were skipped? What artifact was produced, and where did it go?&lt;/p&gt;
&lt;p&gt;Those are questions, not a claim that the stream records every answer. The roadmap describes execution telemetry, but the published description is not a field-by-field event contract. I would compare the documented event types and schema with the questions before promising responders that the stream supports them. If an answer depends on a separate source, such as a deployment record or artifact registry, I would name that source and test the correlation rather than silently assume the stream contains it.&lt;/p&gt;
&lt;p&gt;I would capture the contract in a short table that belongs with the incident-response runbook:&lt;/p&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Investigation question&lt;/th&gt;&lt;th&gt;Evidence source&lt;/th&gt;&lt;th&gt;Correlation key&lt;/th&gt;&lt;th&gt;Required retention&lt;/th&gt;&lt;th&gt;Owner&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Which workflow execution is under review?&lt;/td&gt;&lt;td&gt;Actions Data Stream, if the event schema supports it&lt;/td&gt;&lt;td&gt;Run identifier and repository&lt;/td&gt;&lt;td&gt;Set by incident policy&lt;/td&gt;&lt;td&gt;Platform team&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;What revision and dependencies did it use?&lt;/td&gt;&lt;td&gt;Stream fields, plus any necessary repository evidence&lt;/td&gt;&lt;td&gt;Run identifier and resolved reference&lt;/td&gt;&lt;td&gt;Set by incident policy&lt;/td&gt;&lt;td&gt;Workflow owners&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;What artifact did it publish, and where?&lt;/td&gt;&lt;td&gt;Stream plus artifact or deployment record&lt;/td&gt;&lt;td&gt;Run identifier and artifact digest&lt;/td&gt;&lt;td&gt;Set by incident policy&lt;/td&gt;&lt;td&gt;Release owners&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is a proposed working template, not a description of GitHub’s schema. I would replace each tentative source with a verified event or system, and leave a question marked unsupported if no reliable evidence answers it. That gap is useful: it exposes where a workflow needs better provenance, where a separate system needs to retain a record, or where responders must accept uncertainty.&lt;/p&gt;
&lt;h2 id=&quot;treat-retention-and-integrity-as-design-requirements&quot;&gt;Treat retention and integrity as design requirements&lt;/h2&gt;
&lt;p&gt;Evidence has to outlast the period in which someone might need it. A team should decide that period from its own incident-response and compliance requirements, rather than inherit a retention default without discussion. GitHub’s documentation says the configured retention policy applies to checks, workflow runs, commit statuses, artifacts and logs from October 1, 2026; it also explains how to configure the organization’s retention period in &lt;a href=&quot;https://docs.github.com/organizations/managing-organization-settings/configuring-the-retention-period-for-github-actions-artifacts-and-logs-in-your-organization&quot;&gt;GitHub’s retention-period documentation&lt;/a&gt;. That makes it especially important to review what the organization’s current setting covers and whether it meets the investigation contract.&lt;/p&gt;
&lt;p&gt;A centralized stream could help separate retained investigation evidence from the lifecycle of an individual run log. But I would verify the mechanics before relying on that separation. Does the destination retain events for the required period? Can an administrator or compromised credential delete or alter them? Are failed deliveries visible and recoverable? Can the team show that records arrived intact, and identify gaps in collection? “Immutable” at one point in the path does not automatically make every copy, export, or downstream system immutable.&lt;/p&gt;
&lt;p&gt;I would assign an owner to each part of the chain: configuring the source, operating the destination, monitoring delivery, controlling access, and restoring evidence for an investigation. I would also define what happens when the stream is unavailable. If the answer is “we will look at run logs,” then logs are still a dependency and their own retention and permissions belong in the plan. If there is no fallback, responders need to know that a telemetry gap may leave a question unanswered.&lt;/p&gt;
&lt;h2 id=&quot;prove-the-workflow-before-an-incident&quot;&gt;Prove the workflow before an incident&lt;/h2&gt;
&lt;p&gt;I would test the evidence path using a controlled workflow change, not wait for a real incident to find out what arrives. Start a run, make a harmless change that can be recognized in the output, and follow the evidence from its source into the destination. Confirm that the records can be searched by the identifiers responders will have, that timestamps and repository context are interpretable, and that the record answers the questions in the contract. Then test the less comfortable cases: a failed run, a cancelled run, a run that produces no artifact, and a missing or delayed event.&lt;/p&gt;
&lt;p&gt;For reusable pipelines, I would include the distinction between the shared workflow and the calling repository in those checks. A useful investigation needs to identify both where the execution logic came from and which repository invoked it. I would not assume that one run identifier or one repository name is enough to reconstruct the relationship; I would verify the actual fields and joins available in the stream and any linked records.&lt;/p&gt;
&lt;p&gt;Finally, I would have someone follow the runbook without relying on the person who designed the pipeline. Can they find a run, reconstruct the relevant execution path, explain any missing evidence, and state how long the records remain available? If not, the problem is not solved by adding another data feed. It is solved when the evidence contract, retention policy, permissions and responder procedure work together.&lt;/p&gt;
&lt;p&gt;I would adopt Actions Data Stream when its verified records answer the investigation questions the team has chosen, and when the organization can preserve and retrieve them under its own requirements. Until then, I would treat it as a capability to evaluate—not as a substitute for deciding what an investigation must prove.&lt;/p&gt;</content:encoded><category>github-actions</category><category>security</category><category>incident-response</category></item><item><title>PostgreSQL 19 parallel autovacuum is a resource-budget decision</title><link>https://vahidinasab.com/articles/postgresql-19-parallel-autovacuum-resource-budget</link><guid isPermaLink="true">https://vahidinasab.com/articles/postgresql-19-parallel-autovacuum-resource-budget</guid><description>I explain how to assess PostgreSQL 19 parallel autovacuum against foreground CPU and I/O demand, and how to test limits and monitoring before enabling it.</description><pubDate>Mon, 19 Oct 2026 09:49:31 GMT</pubDate><content:encoded>&lt;h2 id=&quot;faster-maintenance-spends-shared-capacity&quot;&gt;Faster maintenance spends shared capacity&lt;/h2&gt;
&lt;p&gt;PostgreSQL 19 Beta 4 is available, and its &lt;a href=&quot;https://www.postgresql.org/docs/19/release-19.html&quot;&gt;release notes for PostgreSQL 19&lt;/a&gt; list parallel autovacuum as a feature. I would treat it as a change to the database’s concurrency budget, not as a free reduction in maintenance time.&lt;/p&gt;
&lt;p&gt;The feature allows autovacuum to use parallel workers for index vacuuming and cleanup. It does not turn every part of a vacuum into parallel work. The &lt;a href=&quot;https://www.postgresql.org/message-id/E1w9p0X-003HAO-3B%40gemulon.postgresql.org&quot;&gt;PostgreSQL commit introducing parallel autovacuum&lt;/a&gt; describes those phases and the new controls: &lt;code&gt;autovacuum_max_parallel_workers&lt;/code&gt; sets a per-autovacuum-worker ceiling, while the table storage parameter &lt;code&gt;autovacuum_parallel_workers&lt;/code&gt; can cap or disable parallelism for a particular table. The commit also says the new global setting defaults to &lt;code&gt;0&lt;/code&gt;, preserving the previous behavior unless an operator enables it.&lt;/p&gt;
&lt;p&gt;That scope matters. More workers may shorten an index-heavy maintenance phase, but they also create more simultaneous work for the CPU, storage, and shared worker pool. Whether that is a win depends on what else the database is doing when maintenance runs. If foreground queries already compete for CPU or storage bandwidth, bringing vacuum work forward can trade maintenance lag for slower requests. That is not automatically a bad trade: it is a choice about which work gets access to scarce capacity, and when.&lt;/p&gt;
&lt;p&gt;I would start with the pressure that prompted the change. Is maintenance falling behind, or is the goal simply to make a vacuum finish sooner? If the system is accumulating dead tuples or struggling to keep up with routine maintenance, parallelism may be worth testing. If maintenance is already completing comfortably and foreground latency is the constraint, increasing concurrency may solve the wrong problem.&lt;/p&gt;
&lt;h2 id=&quot;enable-it-as-a-measured-experiment&quot;&gt;Enable it as a measured experiment&lt;/h2&gt;
&lt;p&gt;I would not begin by enabling extra workers across every table. First, I would identify the relations that are responsible for meaningful maintenance work and check whether their index work is a plausible target. Then I would test those relations under a workload that resembles the production mix: foreground reads and writes, plus the maintenance activity that normally overlaps with them.&lt;/p&gt;
&lt;p&gt;The configuration below shows the controls, not a recommended production setting. The &lt;code&gt;0&lt;/code&gt; on the global setting leaves the feature disabled; the table parameter can be used to limit the scope of a test. The &lt;a href=&quot;https://www.postgresql.org/message-id/E1w9p0X-003HAO-3B%40gemulon.postgresql.org&quot;&gt;upstream commit introducing the settings&lt;/a&gt; documents that positive per-table values cap the worker count, subject to the global setting.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;-- Keep parallel autovacuum disabled globally while preparing a test.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;ALTER&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; SYSTEM&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; SET&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; autovacuum_max_parallel_workers &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;-- For a test, set an explicit cap on one selected relation.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;ALTER&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; app&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;events&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;  SET&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; (autovacuum_parallel_workers &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would change the global setting only for a controlled test, and verify the effective configuration before interpreting results. A per-table cap is not a guarantee that workers will always launch: the global limit and available worker capacity still constrain execution. Nor does a configured cap prove that parallel work improved the maintenance outcome. The &lt;a href=&quot;https://www.postgresql.org/docs/19/release-19.html&quot;&gt;PostgreSQL release notes&lt;/a&gt; describe the feature; the useful evidence for my system is what happened under its workload.&lt;/p&gt;
&lt;p&gt;For a comparison, I would hold the workload and observation window steady, and compare the baseline with a deliberately limited parallel setting. I would record maintenance duration and whether maintenance keeps up, alongside foreground query latency, CPU pressure, and storage activity. I would also include the periods when the database is busiest. A test that shows shorter vacuum duration while the system is otherwise idle does not tell me whether the change is safe when application demand peaks.&lt;/p&gt;
&lt;p&gt;I would avoid changing several vacuum settings at once. If I raise worker counts, cost limits, and memory settings together, I lose the ability to tell which change affected the result. Parallel autovacuum is specifically about adding workers to eligible maintenance work; it is not a substitute for understanding why vacuum is behind or for testing the effect of that extra concurrency.&lt;/p&gt;
&lt;h2 id=&quot;put-limits-where-the-contention-is&quot;&gt;Put limits where the contention is&lt;/h2&gt;
&lt;p&gt;A global maximum is a guardrail, not a complete resource budget. It limits the parallel workers an autovacuum worker may launch, while the table parameter gives a narrower control. The &lt;a href=&quot;https://www.postgresql.org/message-id/E1w9p0X-003HAO-3B%40gemulon.postgresql.org&quot;&gt;commit description&lt;/a&gt; also makes clear that the available parallel-worker pool constrains the actual number. I would therefore assess parallel autovacuum alongside other uses of that shared capacity, rather than treating its setting as an isolated knob.&lt;/p&gt;
&lt;p&gt;My preference is to start with a small, explicit scope: a selected table, a known maintenance problem, and a test that can be stopped or rolled back. I would keep the global ceiling conservative until measurements show that the additional work does not materially harm foreground queries. If the workload is CPU-bound, I would be especially cautious about allowing more simultaneous work. If storage is the limiting factor, more workers can increase contention there even when they reduce the elapsed time of one maintenance operation.&lt;/p&gt;
&lt;p&gt;The right limit depends on the machine and the workload, so I would not copy a worker count from another database. A value that is safe on an underused test instance can be wrong on a saturated production system. I would also avoid treating a faster individual vacuum as proof of success: the operational goal is to keep maintenance healthy without making the service less responsive to the work users are waiting on.&lt;/p&gt;
&lt;h2 id=&quot;monitor-the-trade-not-just-the-setting&quot;&gt;Monitor the trade, not just the setting&lt;/h2&gt;
&lt;p&gt;I would watch the maintenance process and the foreground service together. PostgreSQL’s &lt;a href=&quot;https://www.postgresql.org/docs/19/progress-reporting.html#VACUUM-PROGRESS-REPORTING&quot;&gt;&lt;code&gt;pg_stat_progress_vacuum&lt;/code&gt; documentation&lt;/a&gt; describes the view for monitoring vacuum progress. PostgreSQL 19’s &lt;a href=&quot;https://www.postgresql.org/docs/19/release-19.html&quot;&gt;release notes&lt;/a&gt; also mention additional vacuum initiation details in that view. Progress data can help establish what maintenance is doing, but it does not answer whether the workload is better off. I would pair it with the application’s latency and error signals, and with host or platform measurements for CPU and storage pressure.&lt;/p&gt;
&lt;p&gt;I would keep the change only if the test demonstrates a useful maintenance improvement while foreground behavior stays within the service’s own limits. If maintenance completes sooner but pushes important queries outside their latency budget, the setting is not safe at that scope. If foreground behavior is stable but maintenance remains behind, I would investigate the remaining bottleneck rather than assume another worker will fix it.&lt;/p&gt;
&lt;p&gt;That is the decision I would carry into a PostgreSQL 19 rollout: parallel autovacuum is a way to spend more of the database’s concurrent capacity on maintenance. I would enable it where the evidence shows that trade is worth making, and keep the limits and monitoring close to the workload it is meant to help.&lt;/p&gt;</content:encoded><category>postgresql</category><category>database-operations</category><category>performance</category></item><item><title>A runner-image SBOM is not build provenance</title><link>https://vahidinasab.com/articles/a-runner-image-sbom-is-not-build-provenance</link><guid isPermaLink="true">https://vahidinasab.com/articles/a-runner-image-sbom-is-not-build-provenance</guid><description>A runner-image SBOM inventories preinstalled software, while build provenance connects an artifact to the workflow, actions, and inputs that produced it.</description><pubDate>Mon, 19 Oct 2026 09:47:58 GMT</pubDate><content:encoded>&lt;h2 id=&quot;what-the-runner-sbom-tells-me&quot;&gt;What the runner SBOM tells me&lt;/h2&gt;
&lt;p&gt;A runner-image SBOM answers a useful, bounded question: what software was preinstalled in this build environment? GitHub says SBOMs are available for its maintained Ubuntu, Windows, and macOS runner images, and that they can be found among the assets attached to image releases. Its &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;secure-use reference&lt;/a&gt; describes using the inventory to inspect software on the runner and scan it for vulnerabilities.&lt;/p&gt;
&lt;p&gt;That is valuable evidence. If an image contains a vulnerable preinstalled tool, its inventory can help identify which image release needs attention. It can also help explain the starting environment a job received. I would preserve the relevant SBOM alongside build records rather than treating it as disposable documentation.&lt;/p&gt;
&lt;p&gt;But “what was installed on the runner” is not the same question as “what influenced this artifact.” The SBOM describes the image, not necessarily the full execution of a particular job. A workflow can download dependencies, execute actions, run scripts from the repository, call external services, or produce intermediate files during the build. Those inputs and operations are not established merely by listing the image’s preinstalled software. That distinction follows from the scope of the inventory: it describes the image, while the artifact results from a process using that image.&lt;/p&gt;
&lt;h2 id=&quot;the-build-has-more-inputs-than-its-machine-image&quot;&gt;The build has more inputs than its machine image&lt;/h2&gt;
&lt;p&gt;A workflow definition is part of the recipe. It identifies the jobs and steps, their order, and the actions or commands used. The source revision matters too, as do dependency versions and configuration passed to the build. Some inputs are fixed in the repository; others may be fetched or resolved while the job runs.&lt;/p&gt;
&lt;p&gt;Consider a small workflow:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  push&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  build&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet restore&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet publish --configuration Release&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This snippet says something about the runner label and the steps, but it does not, by itself, identify the exact runner image revision used for a particular execution. The label is a selection, not a permanent image identity. Nor does the snippet record the resolved dependency graph produced by restore. The action reference is a version tag rather than an immutable commit reference. GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;secure-use guidance&lt;/a&gt; recommends pinning actions to a full-length commit SHA, describing that as the only way to use an action as an immutable release.&lt;/p&gt;
&lt;p&gt;Pinning helps make the action reference more specific, but it does not make the runner SBOM a record of everything else. A complete account of the build would still need to connect the chosen image and its inventory to the workflow revision, source revision, resolved action references, dependency inputs, and the artifact itself. Depending on the build, it may also need to capture parameters or external materials that affected the result.&lt;/p&gt;
&lt;p&gt;I find it helpful to keep two questions separate. An SBOM describes components or software in an inventory. Provenance describes how an output was produced and what inputs and platform were involved. The &lt;a href=&quot;https://slsa.dev/spec/v1.1/terminology&quot;&gt;SLSA terminology&lt;/a&gt; defines provenance as metadata describing how outputs were produced, including identification of the platform and external parameters. That is a wider record than a runner-image inventory.&lt;/p&gt;
&lt;h2 id=&quot;connect-evidence-to-a-particular-build&quot;&gt;Connect evidence to a particular build&lt;/h2&gt;
&lt;p&gt;Evidence becomes useful when it has an unambiguous relationship to the build and output it is meant to explain. A runner-image SBOM attached to a workflow run without an image identity or release reference may be difficult to match later. Even when the SBOM is known, a reviewer needs to know which workflow and source revision produced the artifact, and which external actions and dependencies the job consumed.&lt;/p&gt;
&lt;p&gt;I would make the evidence chain explicit. Record the artifact’s digest so the record refers to the produced bytes rather than just a mutable filename or tag. Record the workflow identity and source revision. Record the runner image identifier and associate it with the matching image SBOM. Record the action references and dependency inputs used by the run. Keep those records together or link them through stable identifiers, and make the evidence available to the people who need to verify the artifact.&lt;/p&gt;
&lt;p&gt;This is not a claim that every build can capture every possible influence perfectly. Builds can depend on network responses, environment variables, generated files, or other state that is easy to overlook. The practical goal is to identify the inputs that matter, preserve enough detail to trace the artifact back to them, and be clear about what remains outside the record. A list of installed software alone cannot close those gaps.&lt;/p&gt;
&lt;p&gt;GitHub offers artifact attestations as another part of this picture. Its &lt;a href=&quot;https://docs.github.com/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds&quot;&gt;documentation on build provenance attestations&lt;/a&gt; explains how workflows can generate attestations for artifacts. GitHub describes these as signed claims that can associate an artifact with information such as its workflow, repository, environment, commit SHA, and triggering event. That makes an attestation more directly relevant to the question of where an artifact came from than a runner-image SBOM alone. I would still inspect what the attestation actually covers and what inputs the build record captures; the existence of an attestation is not a reason to assume every relevant influence has been recorded.&lt;/p&gt;
&lt;h2 id=&quot;use-the-sbom-for-the-question-it-answers&quot;&gt;Use the SBOM for the question it answers&lt;/h2&gt;
&lt;p&gt;I would keep the runner-image SBOM, but label its role accurately: it is evidence about preinstalled software in a runner image. It can support vulnerability review and help describe the environment. It cannot, on its own, prove which exact workflow, actions, source revision, or fetched dependencies produced a particular artifact.&lt;/p&gt;
&lt;p&gt;That distinction changes how I would design a CI evidence bundle. Start with the artifact identity, then link the build record to the exact source and workflow, the runner image and its SBOM, and the actions and dependencies used. If a link is missing, mark the gap instead of letting an inventory stand in for evidence it does not contain. A useful SBOM is one component of the record; the value comes from connecting it to the build it is supposed to describe.&lt;/p&gt;</content:encoded><category>software-supply-chain</category><category>ci-cd</category><category>github-actions</category></item><item><title>QuickGrid virtualization is a data contract</title><link>https://vahidinasab.com/articles/quickgrid-virtualization-is-a-data-contract</link><guid isPermaLink="true">https://vahidinasab.com/articles/quickgrid-virtualization-is-a-data-contract</guid><description>QuickGrid virtualization depends on stable ordering, useful filtering, and predictable range queries, not only on rendering fewer rows.</description><pubDate>Mon, 19 Oct 2026 09:47:54 GMT</pubDate><content:encoded>&lt;h2 id=&quot;virtualization-moves-work-it-does-not-remove-it&quot;&gt;Virtualization moves work; it does not remove it&lt;/h2&gt;
&lt;p&gt;The .NET 11 Release Candidate 2 announcement says QuickGrid virtualization APIs are no longer experimental (&lt;a href=&quot;https://devblogs.microsoft.com/dotnet/dotnet-11-rc-2&quot;&gt;.NET 11 Release Candidate 2&lt;/a&gt;). That makes this a good moment to look past the component switch and ask what the data source must promise. Virtualization can keep a browser from rendering every row, but it cannot make an ambiguous or expensive database query reliable.&lt;/p&gt;
&lt;p&gt;QuickGrid’s &lt;code&gt;ItemsProvider&lt;/code&gt; makes that contract visible. It receives a request with a start index, a maximum row count, sort information, and a cancellation token. The provider returns matching rows and a total item count, which QuickGrid uses for paging and virtualization (&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/quickgrid?view=aspnetcore-10.0&quot;&gt;QuickGrid component documentation&lt;/a&gt;). Those are not merely implementation details. They define how the grid asks for data and what the server must mean by “the next range.”&lt;/p&gt;
&lt;p&gt;I would treat the provider as an API boundary, even when it lives inside the same application as the component. Make its guarantees explicit: which filters apply, how a sort is interpreted, how ties are resolved, what a range means when records change, and how the total is calculated. If those rules are left implicit, a grid can look responsive while showing inconsistent rows or issuing costly queries.&lt;/p&gt;
&lt;h2 id=&quot;a-range-needs-a-stable-order&quot;&gt;A range needs a stable order&lt;/h2&gt;
&lt;p&gt;A start index only has meaning against a well-defined sequence. Suppose a grid sorts by creation time, and several records share the same timestamp. If the query orders only by that timestamp, the database has no unique rule for those tied records. One request can end at a different tied row than the next request starts from. The result can be a duplicate or a gap as the user scrolls.&lt;/p&gt;
&lt;p&gt;The fix is not specific to QuickGrid: make the ordering fully unique. The EF Core pagination guidance gives the same advice, using a unique property such as an ID as a final ordering key (&lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/pagination&quot;&gt;EF Core pagination guidance&lt;/a&gt;). In practice, I would translate a requested sort into an allowed database ordering, then append a unique tie-breaker. The tie-breaker should be part of the contract, not an accidental property of the current query plan.&lt;/p&gt;
&lt;p&gt;For example, a provider with a fixed default ordering could look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ValueTask&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;GridItemsProviderResult&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;OrderRow&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;GetOrders&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;    GridItemsProviderRequest&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; filtered&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; db.Orders&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;AsNoTracking&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Where&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;order&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; order.IsVisible);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; totalCount&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; filtered.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;CountAsync&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(request.CancellationToken);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; (request.Count &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;is&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; int&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; count&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;        throw&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; InvalidOperationException&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;A range size is required.&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; items&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; filtered&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;OrderBy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;order&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; order.CreatedAt)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;ThenBy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;order&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; order.Id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Skip&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(request.StartIndex)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Take&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(count)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;Select&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;order&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; OrderRow&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(order.Id, order.CreatedAt))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;        .&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;ToListAsync&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(request.CancellationToken);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; GridItemsProviderResult.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;From&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(items, totalCount);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example keeps filtering in the query and uses a unique final key so the ordering is deterministic for a given data state. A production provider also needs to apply the grid’s requested sort. I would map supported sort choices to explicit query expressions rather than treating a client-supplied property name as an arbitrary database instruction; whichever sort is selected, the unique tie-breaker remains. QuickGrid exposes request sorting helpers for applying or inspecting its sort rules (&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.griditemsproviderrequest-1?view=aspnetcore-10.0&quot;&gt;&lt;code&gt;GridItemsProviderRequest&lt;/code&gt; API reference&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id=&quot;filtering-and-counting-are-part-of-the-same-promise&quot;&gt;Filtering and counting are part of the same promise&lt;/h2&gt;
&lt;p&gt;A grid filter changes the sequence being paged. Apply it before both the count and the range query. Counting the unfiltered set while returning filtered rows gives the component a false picture of how much data exists. Applying a filter after retrieving a range is worse: the server may return too few rows, even though matching rows exist just beyond that range.&lt;/p&gt;
&lt;p&gt;The total count is not free. QuickGrid’s provider contract calls for a total item count, and a filtered count can be meaningful work for the database (&lt;a href=&quot;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/quickgrid?view=aspnetcore-10.0&quot;&gt;QuickGrid component documentation&lt;/a&gt;). I would count the filtered query, not fetch every matching row just to count them. If exact counts are too expensive for the product’s data shape, that is a design constraint to address explicitly, not a reason to quietly return a made-up total.&lt;/p&gt;
&lt;p&gt;The row query also needs to fit the database’s access pattern. A filter and sort that look simple in C# can still require substantial database work if the query cannot use a suitable index. EF Core’s efficient-querying guidance notes that indexes and their column order matter to query performance, and that offset-based pagination can become inefficient for large offsets (&lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying&quot;&gt;EF Core efficient querying guidance&lt;/a&gt;). I would inspect the actual generated SQL and query plan, then design indexes around the filters and ordering the grid really uses. Virtualization reduces the number of rows sent to the browser; it does not guarantee that the database can find those rows cheaply.&lt;/p&gt;
&lt;h2 id=&quot;offset-ranges-and-live-data-have-a-cost&quot;&gt;Offset ranges and live data have a cost&lt;/h2&gt;
&lt;p&gt;QuickGrid’s virtualization request is expressed as a start index and count. That is a natural fit for offset pagination, but an offset query may need to pass over earlier rows to reach a distant range. EF Core recommends keyset pagination for sequential next-or-previous navigation, where the next query can continue from the last row’s sort keys instead of skipping an offset (&lt;a href=&quot;https://learn.microsoft.com/en-us/ef/core/querying/pagination&quot;&gt;EF Core pagination guidance&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I would not assume keyset pagination is a drop-in replacement for a virtualized grid. A scrollbar can request a range far from the current one; a cursor that only knows the previous row does not identify that arbitrary position. If the product needs both arbitrary scrolling and large datasets, the access pattern deserves deliberate design: perhaps offset ranges are acceptable for the expected use, or perhaps the product should constrain navigation, maintain another way to locate ranges, or change the interaction. The important part is to choose knowingly rather than discover deep-offset costs after the grid ships.&lt;/p&gt;
&lt;p&gt;Stable ordering also does not freeze the dataset. If rows are inserted or removed between requests, a later offset can refer to a shifted sequence even when the sort is deterministic. I would define whether the grid is a live view that may shift while data changes, or whether the user expects a consistent browsing session. That choice affects how the application refreshes data and whether it needs a stronger snapshot boundary. It is a product-level behavior, not something rendering fewer rows can decide.&lt;/p&gt;
&lt;h2 id=&quot;cancellation-and-query-shape-belong-in-the-provider&quot;&gt;Cancellation and query shape belong in the provider&lt;/h2&gt;
&lt;p&gt;Scrolling, sorting, and filtering can make an earlier request obsolete before its database query finishes. QuickGrid’s request includes a cancellation token, and I would pass it through database operations rather than allowing stale work to pile up (&lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.quickgrid.griditemsproviderrequest-1?view=aspnetcore-10.0&quot;&gt;&lt;code&gt;GridItemsProviderRequest&lt;/code&gt; API reference&lt;/a&gt;). Cancellation does not replace query limits or indexing, but it lets the data source respond to the grid’s changing needs instead of treating every request as equally current.&lt;/p&gt;
&lt;p&gt;The useful mental model is not “turn on virtualization and fetch fewer rows.” It is “the grid issues range requests, and the server defines a stable, affordable meaning for each one.” I would ship virtualization only after the sort, filter, count, mutation, and range semantics are clear. Otherwise the browser may do less work while the database does more—and the user still cannot trust what the grid shows.&lt;/p&gt;</content:encoded><category>dotnet</category><category>architecture</category><category>databases</category></item><item><title>AI review findings need a queue, not a gate</title><link>https://vahidinasab.com/articles/ai-review-findings-need-a-queue-not-a-gate</link><guid isPermaLink="true">https://vahidinasab.com/articles/ai-review-findings-need-a-queue-not-a-gate</guid><description>AI review can surface defects faster than a team can resolve them; I argue for managing findings as work with explicit triage and release capacity.</description><pubDate>Mon, 19 Oct 2026 09:47:51 GMT</pubDate><content:encoded>&lt;h2 id=&quot;finding-more-defects-changes-the-release-problem&quot;&gt;Finding more defects changes the release problem&lt;/h2&gt;
&lt;p&gt;AI-assisted review can increase the number of reported defects without increasing the team’s ability to investigate, fix, and verify them. That changes the release problem: the team now has to manage a flow of findings, not just decide whether a check passed.&lt;/p&gt;
&lt;p&gt;PostgreSQL offers a timely example. In an &lt;a href=&quot;http://peter.eisentraut.org/blog/2026/10/09/the-reverts-will-continue-until-morale-improves&quot;&gt;October 9, 2026 post about the reverts&lt;/a&gt;, committer Peter Eisentraut described a long tail of defects surfaced during development, including reports from LLM-assisted code review. He noted that even a small fix carries overhead, and that some defects were architectural problems that would be hard to address quickly during beta. The &lt;a href=&quot;https://www.theregister.com/databases/2026/10/16/postgres-19-delayed-after-ai-reviewers-find-more-bugs-than-humans-can-fix/5303471&quot;&gt;report on PostgreSQL 19’s delay&lt;/a&gt; describes the release moving to November 19, 2026, with features held back.&lt;/p&gt;
&lt;p&gt;I would not read that as an argument against AI review. Finding a defect earlier can be valuable. But a finding is not a fix, and a pile of findings is not evidence of higher quality by itself. Each report consumes attention: someone must understand the relevant code, determine whether the issue is real, judge its severity, and decide what action is safe. A review system that produces more reports than a team can process has moved the bottleneck; it has not removed it.&lt;/p&gt;
&lt;p&gt;That is why I would treat AI-generated findings as queued work, not as an automatic quality gate. A gate assumes a simple relationship: check fails, therefore release stops. A queue makes the operational questions visible: who triages the finding, what information accompanies it, how quickly it must be handled, and what happens when the queue grows faster than it drains?&lt;/p&gt;
&lt;h2 id=&quot;separate-the-finding-from-the-decision&quot;&gt;Separate the finding from the decision&lt;/h2&gt;
&lt;p&gt;An automated reviewer should be allowed to raise a concern without being allowed to decide that the code cannot ship. Those are different responsibilities. The tool can point to a possible defect; a person who understands the code and its consequences should decide whether it is valid and what to do about it.&lt;/p&gt;
&lt;p&gt;I would make the first decision a triage decision, not a fix decision. Is the report reproducible? Does it identify a concrete failure path, or only a possible concern? Does the affected code sit on a security boundary, a data consistency boundary, or a less critical path? Is the report a duplicate of something already known? Until someone answers those questions, counting it as a confirmed defect confuses machine output with engineering judgment.&lt;/p&gt;
&lt;p&gt;A useful intake record needs enough context to make that judgment efficiently: the affected file or behavior, a concise explanation of the suspected failure, a way to reproduce it if possible, and the reviewer or model that raised it. I would also require the report to state uncertainty plainly. A plausible explanation is not proof, and a confident tone is not evidence.&lt;/p&gt;
&lt;p&gt;The release policy should then distinguish classes of findings. A verified defect that risks data loss or bypasses an authorization check may block a release. A speculative report should be triaged before it can block anything. A confirmed but low-impact issue may be scheduled, documented, or rejected with a reason. The exact policy depends on the system’s risk, but it should be explicit and consistent. If every report blocks, noise becomes an unbounded veto. If no report blocks, the gate is decorative.&lt;/p&gt;
&lt;p&gt;This separation also makes disagreement useful. A human can mark a finding as invalid, duplicate, accepted for later, or requiring a fix, and record why. That gives the team a basis for improving prompts and review configuration without pretending that every false positive can be eliminated. I would rather have a smaller set of well-understood decisions than a large green-or-red indicator that hides the reasoning.&lt;/p&gt;
&lt;h2 id=&quot;put-capacity-in-the-release-path&quot;&gt;Put capacity in the release path&lt;/h2&gt;
&lt;p&gt;The architectural question is where this work sits. If findings arrive only after a feature is effectively complete, the team has little room to investigate or redesign. If they arrive continuously while code is changing, developers may be able to act while the context is still fresh. But continuous review still consumes attention, and interrupting every developer with every report simply moves the queue into people’s working memory.&lt;/p&gt;
&lt;p&gt;I would route findings into a visible triage queue, with an owner responsible for keeping it moving. That owner need not personally fix every issue. Their job is to confirm the report, assign an appropriate severity, identify the team that can act, and make unresolved risk visible to the release decision. The queue needs a clear relationship to the existing review and release process; a second, unowned inbox is just hidden work.&lt;/p&gt;
&lt;p&gt;For example, a repository could describe its review policy in a small, reviewable configuration file:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;ai_review&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  mode&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;report&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  require_human_triage&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  block_on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;confirmed-security-defect&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;confirmed-data-integrity-defect&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  unresolved_findings&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;visible-to-release-owner&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an illustrative policy, not a universal schema. The important choice is &lt;code&gt;mode: report&lt;/code&gt;: the reviewer contributes evidence, while a separate human decision determines whether the evidence should block a release. The blocking categories should match the system’s real risks, and the team should be able to explain them without relying on the tool’s score alone.&lt;/p&gt;
&lt;p&gt;When the queue becomes the bottleneck, the answer should not automatically be to add more review agents. First, look at where work is waiting. Are reports too vague to reproduce? Is triage overloaded? Are several reviewers reporting the same issue? Are fixes waiting for an owner, or are verified fixes waiting for a release decision? Each failure points to a different constraint. Increasing the rate of incoming findings before understanding the constraint can make the queue longer without making the product safer.&lt;/p&gt;
&lt;h2 id=&quot;make-the-queue-part-of-quality&quot;&gt;Make the queue part of quality&lt;/h2&gt;
&lt;p&gt;A review process should track the state of findings, not just the volume produced. I would want to know whether a report is awaiting triage, confirmed, assigned, fixed, verified, deferred with a reason, or rejected. Those states are useful because they describe work. A raw finding count does not tell me whether the team is learning about real defects, drowning in duplicates, or leaving serious issues unresolved.&lt;/p&gt;
&lt;p&gt;The same principle applies to release readiness. A release owner needs to see confirmed, unresolved risks and the reason each one is accepted or blocking. That does not mean every low-risk concern must be fixed before shipping. It means the decision is made deliberately by someone accountable for the release, rather than being smuggled in by a tool’s default status.&lt;/p&gt;
&lt;p&gt;I would also keep the verification step separate from the claim that a fix exists. A patch can address the wrong cause, introduce a regression, or fail to cover the reported path. The finding should not leave the queue as resolved merely because a commit references it. Someone should check that the behavior changed as intended, using a test or another concrete means of verification appropriate to the defect.&lt;/p&gt;
&lt;p&gt;AI review can raise the quality bar by making more possible defects visible. The cost is that visibility creates work, and the work needs an owner, a place in the release process, and enough capacity to reach a decision. I would make the review tool report; I would make the team decide. A gate that cannot explain what is waiting behind it is not quality control—it is a bottleneck with a green light.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>code-review</category><category>software-architecture</category></item><item><title>Keep roadmap commitments independent of PostgreSQL 19’s release date</title><link>https://vahidinasab.com/articles/keep-roadmap-commitments-independent-of-postgresql-19s-release-date</link><guid isPermaLink="true">https://vahidinasab.com/articles/keep-roadmap-commitments-independent-of-postgresql-19s-release-date</guid><description>I explain how to evaluate PostgreSQL 19 features without making a delivery plan depend on an unsettled release or feature set.</description><pubDate>Mon, 19 Oct 2026 09:47:48 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-release-date-is-not-an-architecture-guarantee&quot;&gt;A release date is not an architecture guarantee&lt;/h2&gt;
&lt;p&gt;As of October 19, 2026, PostgreSQL 19 is a release candidate, and the project lists November 19, 2026 as its planned general availability date, subject to further issues or release candidates (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409&quot;&gt;PostgreSQL 19 RC 1 announcement&lt;/a&gt;). That is useful planning information. It is not a date I would make a product commitment depend on.&lt;/p&gt;
&lt;p&gt;The distinction matters because the schedule has already moved. The Register reports that PostgreSQL 19 had been expected in late September or early October, and that the release was pushed back while a number of features were held back (&lt;a href=&quot;https://www.theregister.com/databases/2026/10/16/postgres-19-delayed-after-ai-reviewers-find-more-bugs-than-humans-can-fix/5303471&quot;&gt;The Register’s report on the PostgreSQL 19 delay&lt;/a&gt;). The official release candidate announcement gives the current planned date; the report describes how the project arrived here. Neither turns an unreleased capability into a dependable premise for a roadmap.&lt;/p&gt;
&lt;p&gt;I would separate a feature that is interesting to test from one the architecture requires. Testing a new database capability is reversible: I can try it, learn what it does to a workload, and decide not to adopt it. Making a product delivery depend on that capability is a different decision. It ties the product’s schedule, design, and potentially its data model to a release date and feature set that are not yet settled.&lt;/p&gt;
&lt;p&gt;If the design only works when that feature ships on schedule, it is already a hidden delivery dependency. Calling it a database choice does not make the dependency less real.&lt;/p&gt;
&lt;h2 id=&quot;keep-a-baseline-that-works-without-the-feature&quot;&gt;Keep a baseline that works without the feature&lt;/h2&gt;
&lt;p&gt;I would first write down the smallest design that meets the product requirement on a supported PostgreSQL version the team can use now. That is the baseline. It should not be a knowingly inferior placeholder that can only survive until PostgreSQL 19 arrives. It needs to remain a plausible design if the new feature ships later than expected, changes before general availability, or does not make the release at all.&lt;/p&gt;
&lt;p&gt;Then I would describe the new capability as an option with an explicit adoption condition. For example, the decision record might say that a feature can be adopted after it is present in a final release, the team has tested the relevant behavior, and the operating and migration implications are understood. Those are decision gates, not promises about when the work will finish.&lt;/p&gt;
&lt;p&gt;That distinction changes how I would present the roadmap. I would not promise “deliver the feature using PostgreSQL 19.” I would promise the product outcome, with a baseline implementation that can deliver it, and track the database capability as an opportunity to simplify or improve that implementation. If the capability is ready and proves useful, I can adopt it. If not, the product plan does not need to be rewritten around the database calendar.&lt;/p&gt;
&lt;p&gt;This is not an argument against using new PostgreSQL features. It is an argument against confusing the possibility of a feature with a dependable delivery input. A new feature may ultimately be the right choice; until I can validate its final behavior in the release I plan to run, I would not make the rest of the design contingent on it.&lt;/p&gt;
&lt;h2 id=&quot;make-the-dependency-visible-in-the-design&quot;&gt;Make the dependency visible in the design&lt;/h2&gt;
&lt;p&gt;A useful test is to ask what would have to change if the feature were absent. If the answer is “only the implementation behind a boundary,” then the feature may be an optional implementation choice. If the answer is “the data model, the service contract, the customer promise, and the delivery date,” then it is already carrying architectural weight.&lt;/p&gt;
&lt;p&gt;I would make that difference visible in the decision record rather than leaving it implicit in a ticket or a conversation:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;markdown&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-dark-font-weight:bold;--shiki-light:#023B95;--shiki-light-font-weight:bold&quot;&gt;## Decision&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;Deliver the product requirement using the supported PostgreSQL baseline.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-dark-font-weight:bold;--shiki-light:#023B95;--shiki-light-font-weight:bold&quot;&gt;## Optional capability&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;Evaluate the PostgreSQL 19 feature after its release and feature set are settled.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-dark-font-weight:bold;--shiki-light:#023B95;--shiki-light-font-weight:bold&quot;&gt;## Adoption conditions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; The final release includes the required behavior.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; Tests cover the application&apos;s actual read and write paths.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; Operations, upgrades, and recovery remain acceptable.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; The capability provides enough value to justify changing the baseline.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-dark-font-weight:bold;--shiki-light:#023B95;--shiki-light-font-weight:bold&quot;&gt;## If the conditions are not met&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;Keep the baseline implementation and revisit the decision later.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example does not assume what a particular feature will do. It makes the commitment boundary explicit: the product requirement is the commitment; the database feature is a candidate means of meeting it. That also gives reviewers a concrete question to ask when the release changes: does this alter the product outcome, or only the optional implementation path?&lt;/p&gt;
&lt;p&gt;The same discipline applies to data. I would avoid shaping a permanent schema or service interface around a capability that is still under development unless I can explain how the system remains valid without it. A database feature can be local in the code and still create a broad dependency through stored data, migrations, or the behavior other services expect. The fallback has to account for those consequences, not just whether a query can be rewritten.&lt;/p&gt;
&lt;h2 id=&quot;revisit-after-the-release-settles&quot;&gt;Revisit after the release settles&lt;/h2&gt;
&lt;p&gt;PostgreSQL’s own announcement describes the planned release date as conditional: a further issue could require a delay or another release candidate (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409&quot;&gt;PostgreSQL 19 RC 1 announcement&lt;/a&gt;). The right response is not to guess whether the date will hold. It is to set a review point and keep the current plan viable until that review.&lt;/p&gt;
&lt;p&gt;At that point, I would check the final feature set and test the actual application behavior. A feature being present is not, by itself, a reason to adopt it. I would compare it with the baseline against the workload and operational constraints that matter to the design. That is a separate question from whether the feature exists, and it should not be answered by excitement about a release announcement.&lt;/p&gt;
&lt;p&gt;The release may arrive on its planned date and include the capability I want. It may arrive later, or the capability may not be there. In either case, a roadmap built around a working baseline can keep its product commitment intact. I would treat PostgreSQL 19 as an option to evaluate, not a date the architecture needs to be right.&lt;/p&gt;</content:encoded><category>postgresql</category><category>architecture</category><category>roadmaps</category></item><item><title>Treat repository-relative workflow references as a versioning choice</title><link>https://vahidinasab.com/articles/treat-repository-relative-workflow-references-as-a-versioning-choice</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-repository-relative-workflow-references-as-a-versioning-choice</guid><description>A repository-relative GitHub Actions reference ties a reusable workflow to the caller&apos;s version, trading centralized upgrades for co-versioned changes.</description><pubDate>Mon, 19 Oct 2026 09:47:44 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-workflow-reference-chooses-who-owns-the-version&quot;&gt;A workflow reference chooses who owns the version&lt;/h2&gt;
&lt;p&gt;A reusable workflow reference is not just a path to some YAML; it determines how a caller gets changes to that YAML. I would choose a repository-relative reference when I want the workflow and its caller to move together. I would choose a separately versioned shared workflow when I want to control upgrades independently.&lt;/p&gt;
&lt;p&gt;GitHub documents &lt;code&gt;$/.github/workflows/{filename}&lt;/code&gt; as the recommended syntax for a reusable workflow in the same repository. The reference has no tag, branch, or commit suffix: it identifies a workflow in the caller’s repository rather than selecting a separately versioned ref in another repository. GitHub also documents the older &lt;code&gt;./.github/workflows/{filename}&lt;/code&gt; form and says the &lt;code&gt;$&lt;/code&gt; syntax is unavailable on GitHub Enterprise Server. &lt;a href=&quot;https://docs.github.com/enterprise-cloud@latest/actions/how-tos/reuse-automations/reuse-workflows&quot;&gt;GitHub’s guide to reusing workflows&lt;/a&gt; describes the available forms.&lt;/p&gt;
&lt;p&gt;For example, the same-repository form looks like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  verify&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;$/.github/workflows/verify.yml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The significant choice is not the punctuation. It is whether the caller’s change set also carries the workflow version it will run. With a repository-relative reference, a change to the reusable workflow can be reviewed and merged alongside the caller’s code. With a reference to a shared repository and a pinned ref, the caller chooses when to move to a different workflow version.&lt;/p&gt;
&lt;p&gt;I would not call one option inherently safer. They put the burden of change in different places. Co-versioning makes a workflow change easier to understand in the context of a caller’s code. Centralized versioning makes rollout and upgrade timing easier to manage across callers. The important thing is to decide which burden the team wants, rather than letting a convenient-looking path silently decide it.&lt;/p&gt;
&lt;h2 id=&quot;co-versioning-favors-local-consistency&quot;&gt;Co-versioning favors local consistency&lt;/h2&gt;
&lt;p&gt;A repository-relative reference is a good fit when the workflow’s correctness depends closely on the repository that calls it. The caller can change a build or deployment workflow at the same time as the code, configuration, or conventions that workflow assumes. A reviewer can inspect the caller and the workflow change together, instead of asking whether a shared workflow release is compatible with a particular repository.&lt;/p&gt;
&lt;p&gt;That arrangement also makes a deliberate workflow change local in effect: the repository’s own history records the change, and other repositories do not adopt it merely because they point at a shared moving branch. I find that appealing for workflows whose inputs and assumptions are specific to their callers. It keeps the relationship between the code and the automation visible in the same review boundary.&lt;/p&gt;
&lt;p&gt;There is a cost. If many repositories need the same improvement, a repository-relative design does not, by itself, provide one centrally managed workflow version for all of them. Each caller needs to carry or receive its own change. That can mean repeated review and rollout work, or a separate mechanism to keep the workflow copies aligned. The local reference simplifies the caller’s version choice; it does not make shared maintenance disappear.&lt;/p&gt;
&lt;p&gt;I would use this model when local control matters more than a single upgrade point, or when the workflow is intentionally tailored to one repository. I would not describe it as a centralized shared pipeline merely because several repositories happen to contain similar YAML. Similarity is not a release mechanism.&lt;/p&gt;
&lt;h2 id=&quot;centralized-versioning-favors-controlled-upgrades&quot;&gt;Centralized versioning favors controlled upgrades&lt;/h2&gt;
&lt;p&gt;A workflow stored in a shared repository can give callers an explicit version boundary. A caller can reference a tag or commit, then update that reference when the team is ready to adopt a new workflow version. GitHub’s reuse-workflow documentation shows cross-repository references in the form &lt;code&gt;owner/repository/.github/workflows/workflow.yml@ref&lt;/code&gt;. &lt;a href=&quot;https://docs.github.com/enterprise-cloud@latest/actions/how-tos/reuse-automations/reuse-workflows&quot;&gt;The same GitHub guide&lt;/a&gt; explains the supported reference forms.&lt;/p&gt;
&lt;p&gt;That boundary is useful when the shared workflow has a stable interface and many callers should adopt changes on a managed schedule. A change can be prepared in the shared repository, reviewed as a versioned release, and then taken up by callers deliberately. The cost is that someone must maintain the shared workflow’s compatibility with its callers. The reference may be easy to update centrally, but the effects of changing its inputs, permissions, or assumptions still need to be understood across the repositories that use it.&lt;/p&gt;
&lt;p&gt;I would pin a shared workflow to an immutable commit when I need a fixed, inspectable dependency, and make upgrades explicit. A branch reference may be convenient, but it can move without a change to the caller’s file; that makes the workflow version less visible in the caller’s history. This is a design trade-off, not a universal rule: a team might prefer automated updates, but it should know that it is choosing a moving dependency rather than an independently reviewed upgrade at each caller.&lt;/p&gt;
&lt;p&gt;There is also a practical maintenance distinction. GitHub’s secure-use reference says Dependabot does not update locally referenced actions or reusable workflows. &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;The Dependabot guidance in GitHub’s secure-use reference&lt;/a&gt; is worth checking when choosing a local-reference strategy. If a workflow is local, teams should not assume that a dependency-update tool will provide the same update path as it does for repository-based dependencies. That does not make local references wrong; it means the workflow’s maintenance owner and update process need to be clear.&lt;/p&gt;
&lt;h2 id=&quot;portability-belongs-in-the-decision&quot;&gt;Portability belongs in the decision&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;$&lt;/code&gt; syntax is not available on GitHub Enterprise Server, according to GitHub’s workflow reuse documentation. That makes the hosting environment part of the versioning decision, not a detail to discover after a workflow has been adopted. A team that needs the same workflow design to run on GitHub.com and GitHub Enterprise Server should validate the reference form against both environments before standardizing on it.&lt;/p&gt;
&lt;p&gt;If portability to GitHub Enterprise Server is a requirement, the documented alternatives matter. GitHub lists &lt;code&gt;./.github/workflows/{filename}&lt;/code&gt; for a reusable workflow in the same repository and the &lt;code&gt;owner/repository/...@ref&lt;/code&gt; form for a workflow in another repository. The right substitute depends on whether the team still wants caller-coupled changes or needs a separately managed shared workflow. I would not switch syntaxes without revisiting that intent: a replacement that works in both environments may change what is being versioned and who controls its updates.&lt;/p&gt;
&lt;p&gt;My default is to use a repository-relative reference when the workflow is part of the caller’s own change boundary and should move with it. For a pipeline that is genuinely shared across repositories and needs controlled, independently reviewed upgrades, I would use a separately versioned workflow and make adoption explicit. Portability can rule out a syntax, but it should not make the architectural choice on the team’s behalf.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>architecture</category></item><item><title>Treat GitHub Actions cache reuse as a trust boundary</title><link>https://vahidinasab.com/articles/github-actions-cache-reuse-trust-boundary</link><guid isPermaLink="true">https://vahidinasab.com/articles/github-actions-cache-reuse-trust-boundary</guid><description>A cache can expose data to readers or let an untrusted workflow influence a trusted build, so I make cache access part of pipeline security design.</description><pubDate>Tue, 13 Oct 2026 11:21:32 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-cache-is-shared-state&quot;&gt;A cache is shared state&lt;/h2&gt;
&lt;p&gt;I treat a build cache as shared state, not as a harmless speed switch. A workflow that restores a cache reads files produced by another run; one that saves a cache makes its output available to later runs. That creates two distinct questions: who can write data that another workflow will consume, and who can read data a workflow has saved?&lt;/p&gt;
&lt;p&gt;The risk is not theoretical. GitHub warns that untrusted workflows can read sensitive cache contents, and that a poisoned cache can lead to code execution in a trusted workflow. Its &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#cache-access-for-low-trust-workflow-triggers&quot;&gt;dependency caching reference&lt;/a&gt; describes cache access restrictions for low-trust triggers and the risk of overriding them. A cache does not need to contain an obvious secret or executable for it to matter. Build tools may interpret restored files, and a trusted job may rely on cached state without checking who created it.&lt;/p&gt;
&lt;p&gt;That is why I ask about the writer and reader before I ask how much time a cache saves. Cache keys help select entries; they are not an authorization policy. A key that looks specific does not prove that its contents came from a workflow I trust. GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#cache-key-matching&quot;&gt;cache key matching documentation&lt;/a&gt; explains exact and partial matches, including how restore keys can select a partial match. I would not treat a key name as a boundary between trusted and untrusted work.&lt;/p&gt;
&lt;h2 id=&quot;make-access-explicit&quot;&gt;Make access explicit&lt;/h2&gt;
&lt;p&gt;GitHub now provides &lt;code&gt;cache-mode&lt;/code&gt; to control a job’s cache access. The documented modes are &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;write-only&lt;/code&gt;, and &lt;code&gt;none&lt;/code&gt;; access is enforced with scoped cache tokens. The &lt;a href=&quot;https://docs.github.com/actions/reference/workflow-syntax-for-github-actions#cache-mode&quot;&gt;workflow syntax for &lt;code&gt;cache-mode&lt;/code&gt;&lt;/a&gt; also allows the setting at workflow or job level, with a job setting overriding the workflow setting.&lt;/p&gt;
&lt;p&gt;For a workflow that handles pull requests, I would say what it needs rather than leave a security-sensitive default implicit:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Pull request checks&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;cache-mode&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/cache@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;          path&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;~/.cache/example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;          key&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example-${{ runner.os }}-${{ hashFiles(&apos;dependency.lock&apos;) }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./build-and-test.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an illustration of the access policy, not a complete build configuration. The cache path, key, and command need to match the project. The important choice is &lt;code&gt;read&lt;/code&gt;: the job can benefit from a cache but cannot publish its own contents for a trusted job to consume.&lt;/p&gt;
&lt;p&gt;GitHub documents a trigger-based default when &lt;code&gt;cache-mode&lt;/code&gt; is omitted: trusted triggers receive &lt;code&gt;write&lt;/code&gt;, while low-trust triggers receive &lt;code&gt;read&lt;/code&gt;. It also warns that explicitly setting &lt;code&gt;write&lt;/code&gt; or &lt;code&gt;write-only&lt;/code&gt; on a low-trust trigger overrides the read-only restriction. I prefer to make the intended mode visible in the workflow or reusable-workflow interface anyway. The &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#cache-access-for-low-trust-workflow-triggers&quot;&gt;cache access defaults and override guidance&lt;/a&gt; are worth reading before standardising a shared pipeline.&lt;/p&gt;
&lt;p&gt;For cache creation, I would use a trusted workflow that runs after a trusted push, and let pull-request checks read what that workflow produced. GitHub recommends saving caches from trusted triggers and maintaining the default-branch cache with a trusted workflow. That separation makes the policy understandable: untrusted input can benefit from existing cache state without getting permission to update state used by later trusted builds.&lt;/p&gt;
&lt;h2 id=&quot;reusable-workflows-need-a-ceiling&quot;&gt;Reusable workflows need a ceiling&lt;/h2&gt;
&lt;p&gt;A shared workflow is not automatically a security boundary. It is a way to reuse pipeline behavior, and its cache permissions need to be part of the contract between caller and callee. GitHub says &lt;code&gt;cache-mode&lt;/code&gt; propagates to reusable workflows, and an explicit mode on the calling job can limit the access the called workflow may request. The &lt;a href=&quot;https://docs.github.com/enterprise-cloud@latest/actions/how-tos/reuse-automations/reuse-workflows&quot;&gt;reusable workflow cache access documentation&lt;/a&gt; describes this behavior.&lt;/p&gt;
&lt;p&gt;There is a detail I would review carefully: if a calling job neither sets nor inherits an explicit &lt;code&gt;cache-mode&lt;/code&gt;, a called workflow can explicitly request &lt;code&gt;write&lt;/code&gt; even when a low-trust trigger would otherwise default to &lt;code&gt;read&lt;/code&gt;. So I would not assume the trigger default alone constrains every layer of a shared pipeline. I would set the caller’s limit and inspect what the reusable workflow requests. That turns least privilege into an enforceable interface rather than a convention buried in a template.&lt;/p&gt;
&lt;p&gt;I also would not grant write access simply because a job needs caching. A job that only consumes cached dependencies needs &lt;code&gt;read&lt;/code&gt;; one that must create cache entries needs a reason to write. If a low-trust job must save something for its own later use, &lt;code&gt;write-only&lt;/code&gt; may be an option to examine, but it still permits a write from untrusted input. I would keep that state away from trusted release paths unless I can establish that the job cannot put untrusted material into it. GitHub’s guidance is explicit that write-capable modes on low-trust triggers can reintroduce cache-poisoning risk.&lt;/p&gt;
&lt;h2 id=&quot;keep-cache-contents-out-of-the-trust-decision&quot;&gt;Keep cache contents out of the trust decision&lt;/h2&gt;
&lt;p&gt;I would design a pipeline so that a cache miss costs time, not confidence. A trusted build should still validate its inputs and produce its result from the checked-out source and dependency definitions; the cache should accelerate that work, not decide whether the output is trustworthy. If restoring a cache can silently replace an input that the build assumes is authoritative, I would narrow the cached path or remove that cache from the trusted path.&lt;/p&gt;
&lt;p&gt;This also affects what I choose to cache. I would avoid paths that may contain credentials or other sensitive material, because cache readers may be able to access saved contents. GitHub specifically advises against storing sensitive information in a cache in its &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#cache-access-restrictions&quot;&gt;dependency caching security guidance&lt;/a&gt;. I would cache reproducible, regenerable data where possible, and make the build work correctly when no cache is available.&lt;/p&gt;
&lt;p&gt;For teams sharing pipeline patterns across many repositories, I would put the access decision in the reusable workflow and make callers inherit or set an explicit ceiling. Then I would review changes to cache paths, keys, restore keys, triggers, and cache modes as security-relevant changes. The performance optimization stays useful; the trust decision stops being accidental.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>security</category></item><item><title>A replication-slot status metric is not proof of replication health</title><link>https://vahidinasab.com/articles/replication-slot-status-is-not-replication-health</link><guid isPermaLink="true">https://vahidinasab.com/articles/replication-slot-status-is-not-replication-health</guid><description>I explain what Azure’s logical replication slot sync status can tell you, what it cannot prove, and which signals I would pair with it.</description><pubDate>Tue, 13 Oct 2026 11:21:28 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-useful-signal-with-a-narrow-job&quot;&gt;A useful signal with a narrow job&lt;/h2&gt;
&lt;p&gt;A logical replication slot can be synchronized across a high-availability pair while the data pipeline that depends on it is still behind, stopped, or unable to recover. That is why I would treat Azure Database for PostgreSQL’s &lt;code&gt;logical_replication_slot_sync_status&lt;/code&gt; as evidence about one part of the system—not as a green light for replication as a whole.&lt;/p&gt;
&lt;p&gt;Azure’s &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/postgresql/release-notes/release-notes&quot;&gt;PostgreSQL release notes&lt;/a&gt; list the metric, and Microsoft describes it as a binary signal for whether logical replication slots are synchronized between the HA primary and standby in its &lt;a href=&quot;https://techcommunity.microsoft.com/blog/adforpostgresql/march-2026-recap-azure-database-for-postgresql/4511432&quot;&gt;March 2026 Azure Database for PostgreSQL recap&lt;/a&gt;. That answers an important operational question: if the primary fails, is the slot state available on the standby for a failover scenario?&lt;/p&gt;
&lt;p&gt;It does not answer whether a consumer is connected, whether it is applying changes, how far behind it is, or whether the whole path can resume after promotion. Those are different questions with different evidence. I would keep the new metric, but put it beside signals for slot health, consumer progress, data freshness, and recovery—not use it in place of them.&lt;/p&gt;
&lt;h2 id=&quot;separate-slot-state-from-data-movement&quot;&gt;Separate slot state from data movement&lt;/h2&gt;
&lt;p&gt;A slot is a mechanism for retaining the WAL a logical consumer needs. Its state is not the same thing as the consumer’s progress. PostgreSQL exposes slot details such as whether a consumer is active and the slot’s WAL positions in &lt;a href=&quot;https://www.postgresql.org/docs/current/view-pg-replication-slots.html&quot;&gt;&lt;code&gt;pg_replication_slots&lt;/code&gt;&lt;/a&gt;. On the subscriber, &lt;a href=&quot;https://www.postgresql.org/docs/current/logical-replication-monitoring.html&quot;&gt;&lt;code&gt;pg_stat_subscription&lt;/code&gt;&lt;/a&gt; exposes information about subscription workers.&lt;/p&gt;
&lt;p&gt;I would monitor both sides. On the publisher, this query makes it possible to see whether each logical slot has an active consumer and how much WAL lies between the current WAL position and the slot’s restart point:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  slot_name,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  active,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  restart_lsn,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  confirmed_flush_lsn,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; wal_bytes_from_restart&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; pg_replication_slots&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; slot_type &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;logical&apos;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The byte difference is a useful indication of WAL retained behind a slot, not a direct measurement of subscriber lag. A slot may be inactive because its consumer is disconnected; an active slot does not prove the consumer is applying changes at an acceptable rate. And a growing retained-WAL amount deserves attention even if the slot synchronization metric says the HA copies are in sync.&lt;/p&gt;
&lt;p&gt;On the subscriber, I would look for a running worker and inspect its reported receive and apply-related positions and timestamps:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  s&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;subname&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  st&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;pid&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  st&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;received_lsn&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  st&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;latest_end_lsn&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  st&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;last_msg_receipt_time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; pg_stat_subscription &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; st&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;JOIN&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; pg_subscription &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;AS&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; s &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;ON&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; s&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;oid&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; st&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;subid&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These observations need interpretation over time and in the context of the workload. A single sample cannot tell me whether progress is advancing. Nor would I treat a worker row or a recent message receipt as proof that the subscriber’s application-visible data is fresh. PostgreSQL’s &lt;a href=&quot;https://www.postgresql.org/docs/current/logical-replication-monitoring.html&quot;&gt;logical replication monitoring guidance&lt;/a&gt; describes the subscription statistics, but the operational question still belongs to the system I am building: are changes arriving and being applied quickly enough for the consumer’s purpose?&lt;/p&gt;
&lt;h2 id=&quot;measure-the-outcome-the-consumer-needs&quot;&gt;Measure the outcome the consumer needs&lt;/h2&gt;
&lt;p&gt;I want an end-to-end freshness signal as well as database internals. If a downstream system must reflect source changes within a defined window, then the useful measurement is the age of the newest change it has actually made available—not just the state of a slot or a replication worker.&lt;/p&gt;
&lt;p&gt;One practical pattern is to include a small, identifiable heartbeat in the published data and check when it becomes visible downstream. The details depend on the publication and consumer, so I would not assume that a heartbeat table is automatically included or processed. The important design point is to test the path the real data takes: source commit, logical decoding, transport, consumer processing, and the read that users or dependent services rely on. If that check stops advancing, I know the outcome is unhealthy even if individual components report normal state.&lt;/p&gt;
&lt;p&gt;I would also watch trends rather than only thresholds. Increasing WAL retained behind a slot, a consumer position that stops moving, or a growing age of downstream data each points to a different failure mode. The point is not to add every available number to one dashboard. It is to make each signal answer a distinct question, so an alert tells the on-call engineer what has stopped working and where to begin looking.&lt;/p&gt;
&lt;h2 id=&quot;make-failover-readiness-an-explicit-test&quot;&gt;Make failover readiness an explicit test&lt;/h2&gt;
&lt;p&gt;Slot synchronization improves evidence about failover readiness, but readiness has more dependencies than slot state. After promotion, the consumer must connect to the right endpoint, find a usable slot, and resume from a position that preserves the required continuity. The system also needs enough retained WAL for its recovery path. I would not infer all of that from one status metric.&lt;/p&gt;
&lt;p&gt;I would turn recovery into a runbook exercise: check the slot synchronization signal, verify that the expected slot exists and is usable after promotion, confirm that the consumer reconnects and advances, and validate that downstream data becomes fresh again. The exact steps depend on the managed service and the consumer, so I would follow the service’s documented failover behavior rather than assume that a synchronized slot guarantees a successful recovery.&lt;/p&gt;
&lt;p&gt;The useful alert is therefore not simply “slot status is bad” or “slot status is good.” I would alert on slot synchronization because it tells me something important about the HA pair. Separately, I would alert when consumer progress stalls, retained WAL moves in the wrong direction, or end-to-end freshness exceeds the system’s requirement. A green slot-sync signal is reassuring evidence about one boundary. Replication health is the larger claim, and I would only make it when the data is moving and the recovery path has been shown to work.&lt;/p&gt;</content:encoded><category>postgresql</category><category>monitoring</category><category>replication</category><category>azure</category></item><item><title>Kubernetes node swap is a workload policy, not a sizing strategy</title><link>https://vahidinasab.com/articles/kubernetes-node-swap-is-a-workload-policy-not-a-sizing-strategy</link><guid isPermaLink="true">https://vahidinasab.com/articles/kubernetes-node-swap-is-a-workload-policy-not-a-sizing-strategy</guid><description>I explain when Kubernetes node swap can soften memory pressure, when it can hide a capacity problem, and how to pilot it without mistaking swap for free memory.</description><pubDate>Tue, 13 Oct 2026 11:21:24 GMT</pubDate><content:encoded>&lt;h2 id=&quot;swap-changes-the-failure-mode&quot;&gt;Swap changes the failure mode&lt;/h2&gt;
&lt;p&gt;Swap is tempting when a node runs out of physical memory: move less-active pages to disk and avoid killing a process immediately. That can be a useful trade. It is not extra memory with the same performance characteristics. If an application needs pages that have been swapped out, it has to wait for them to come back. A workload that stays alive may therefore become slow in a way that is harder to predict than a clear limit breach.&lt;/p&gt;
&lt;p&gt;That distinction is the point of the &lt;a href=&quot;https://kubernetes.io/blog/2026/10/05/scaling-kubernetes-workloads-with-node-swap&quot;&gt;Kubernetes blog post on scaling workloads with node swap&lt;/a&gt;. It discusses swap in the context of growing memory demands from agentic workloads and describes using fast local storage for dormant agent state. I would treat that as a workload-specific option, not a general argument to raise node density. The useful question is not simply whether swap prevents an out-of-memory event. It is whether the application remains useful while some of its memory is slower to access.&lt;/p&gt;
&lt;p&gt;A memory limit and swap also express different operational expectations. Kubernetes documents that container memory limits are enforced reactively by the kernel, which may terminate a container when memory pressure is detected. Swap can change what happens before a process is killed, but it does not make a workload’s memory use smaller or guarantee acceptable response times. &lt;a href=&quot;https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/&quot;&gt;The Kubernetes resource-management documentation&lt;/a&gt; is clear about the reactive nature of memory limits; my architectural concern is that adding swap can make the path to failure less obvious without removing the underlying pressure.&lt;/p&gt;
&lt;p&gt;So I would not describe swap as “more capacity.” I would describe it as permission for selected workloads to trade some memory-pressure failures for a risk of degraded, potentially erratic performance. That trade belongs in the workload’s policy and service expectations.&lt;/p&gt;
&lt;h2 id=&quot;choose-workloads-by-their-tolerance-for-delay&quot;&gt;Choose workloads by their tolerance for delay&lt;/h2&gt;
&lt;p&gt;I would start with workloads whose usefulness does not depend on consistently fast access to every resident page. Background jobs that can slow down and catch up later may be candidates. So might a workload with genuinely dormant state that can tolerate a pause when that state becomes active again. Those are hypotheses to test, not categories I would enable by default.&lt;/p&gt;
&lt;p&gt;I would be much more cautious with latency-sensitive request paths, interactive applications, and stateful services where a pause affects other work or violates a response-time expectation. For these workloads, a process that is technically alive but intermittently stalled may be worse than a restart or a clear failure. I would also ask what happens when several containers on the same node need their swapped-out memory at once. A policy that seems acceptable for one quiet process may create contention when the node is busy.&lt;/p&gt;
&lt;p&gt;Kubernetes exposes swap behavior through kubelet configuration. The current &lt;a href=&quot;https://kubernetes.io/docs/reference/node/swap-behavior/&quot;&gt;Kubernetes swap-behavior documentation&lt;/a&gt; describes &lt;code&gt;NoSwap&lt;/code&gt; as the default for Pod workloads and &lt;code&gt;LimitedSwap&lt;/code&gt; as allowing them to use swap. An illustrative configuration fragment is:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;failSwapOn&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;memorySwap&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  swapBehavior&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;LimitedSwap&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows the policy choice; it is not a complete deployment recipe. I would check the documentation for the Kubernetes and node configuration in use before applying it. More importantly, I would not enable &lt;code&gt;LimitedSwap&lt;/code&gt; across a mixed-purpose node pool just because one background workload could benefit. If two workloads have different tolerance for stalls, they should not inherit the same swap policy by accident.&lt;/p&gt;
&lt;h2 id=&quot;observe-the-behavior-not-just-the-setting&quot;&gt;Observe the behavior, not just the setting&lt;/h2&gt;
&lt;p&gt;A successful rollout is not proof that swap is harmless. I would define what trouble looks like before testing: rising request latency, longer job completion times, growing swap use, memory pressure, restarts, or evictions. The &lt;a href=&quot;https://kubernetes.io/docs/concepts/cluster-administration/swap-memory-management/&quot;&gt;Kubernetes swap-management documentation&lt;/a&gt; describes observing swap use at node and container level. I would pair those signals with the application’s own measures of useful work and responsiveness; node-level numbers alone cannot tell me whether the workload is still meeting its purpose.&lt;/p&gt;
&lt;p&gt;I would also keep the failure signals visible. The &lt;a href=&quot;https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/&quot;&gt;Kubernetes node-pressure eviction documentation&lt;/a&gt; describes how kubelet uses resource signals and thresholds when deciding whether to evict Pods. Swap does not make those mechanisms irrelevant. A pilot should watch whether pressure is being postponed, whether the node is still reclaiming or evicting workloads, and which Pods are affected. If users see slower responses before the platform reports an obvious failure, that is important evidence—not a reason to declare the test successful because the process survived.&lt;/p&gt;
&lt;p&gt;For a pilot, I would use an isolated node pool and a workload with a clear owner and a measurable tolerance for delay. I would compare the same workload with swap unavailable and with the proposed policy, under representative load and memory pressure. I would set stop conditions in advance: for example, revert if the workload breaches its own latency or completion-time objective, or if swap activity coincides with pressure or instability elsewhere on the node. I would avoid packing unrelated workloads onto the test nodes, because that makes it harder to tell whether the outcome belongs to the candidate workload or its neighbors.&lt;/p&gt;
&lt;p&gt;The point of the comparison is not to prove that swap can keep more Pods running. It is to learn whether the workload delivers acceptable service while using it, and what the node and its other workloads experience in the process. If the test only records fewer restarts, it has measured survival, not success.&lt;/p&gt;
&lt;h2 id=&quot;keep-capacity-planning-honest&quot;&gt;Keep capacity planning honest&lt;/h2&gt;
&lt;p&gt;Swap can make apparent headroom look larger, but scheduled memory requests and actual working-set needs still matter. I would not lower requests, raise density, or defer node sizing on the strength of swap space alone. Those changes can let the scheduler place more work on a node without making the physical-memory constraint disappear. The result may be a cluster that accepts more workload and then pays for it in unpredictable latency.&lt;/p&gt;
&lt;p&gt;My default would remain &lt;code&gt;NoSwap&lt;/code&gt; for general-purpose nodes. I would opt into &lt;code&gt;LimitedSwap&lt;/code&gt; only for workloads whose owners accept the performance trade, whose behavior I can observe, and whose placement is separated enough to contain the experiment. If the workload needs prompt access to its memory to meet its service expectation, I would size memory for that need rather than use swap to disguise a shortfall.&lt;/p&gt;
&lt;p&gt;That is why I see node swap as a workload policy. It can soften one kind of failure, but the cost is a less deterministic path under pressure. A careful pilot can establish whether that cost is acceptable; it cannot turn the extra apparent capacity into free capacity.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>architecture</category><category>infrastructure</category></item><item><title>PostgreSQL’s concurrent REPACK moves the maintenance risk</title><link>https://vahidinasab.com/articles/postgresql-concurrent-repack-moves-maintenance-risk</link><guid isPermaLink="true">https://vahidinasab.com/articles/postgresql-concurrent-repack-moves-maintenance-risk</guid><description>PostgreSQL 19’s concurrent REPACK can reduce reliance on maintenance windows, but it shifts the risks to write load, storage, replicas and recovery.</description><pubDate>Tue, 13 Oct 2026 11:21:20 GMT</pubDate><content:encoded>&lt;h2 id=&quot;online-does-not-mean-cost-free&quot;&gt;Online does not mean cost-free&lt;/h2&gt;
&lt;p&gt;A table rewrite has always asked me to choose which pain I prefer: block access while reclaiming space, or add operational machinery to avoid most of that block. PostgreSQL 19’s new &lt;code&gt;REPACK&lt;/code&gt; command makes that choice more interesting. Its &lt;code&gt;CONCURRENTLY&lt;/code&gt; option is intended to rewrite a table without access-exclusive locking, while the ordinary command retains the simpler, more disruptive path. PostgreSQL describes &lt;code&gt;REPACK&lt;/code&gt; as rewriting table contents to reclaim storage occupied by dead tuples, unlike ordinary &lt;code&gt;VACUUM&lt;/code&gt;, which does not rewrite the table into a new file. (&lt;a href=&quot;https://www.postgresql.org/docs/19/sql-repack.html&quot;&gt;PostgreSQL 19 &lt;code&gt;REPACK&lt;/code&gt; documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;That matters because a maintenance window is not the only cost of a rewrite. Avoiding a long block on reads and writes is valuable, but a concurrent operation still competes for resources and creates work that the rest of the system must absorb. I would treat “online” as a statement about availability during much of the operation—not a guarantee that production load, replication, storage, or recovery remain unaffected.&lt;/p&gt;
&lt;p&gt;PostgreSQL 19 Beta 4 is a feature preview, and the project warns that details may change before general availability. I would evaluate this as a capability to test, not a reason to change a production maintenance plan before the final behavior and provider support are clear. (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386&quot;&gt;PostgreSQL 19 Beta 4 announcement&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id=&quot;reconsider-the-dependency-not-just-the-command&quot;&gt;Reconsider the dependency, not just the command&lt;/h2&gt;
&lt;p&gt;The new command changes the dependency question. Teams that use the &lt;code&gt;pg_repack&lt;/code&gt; extension to perform online rewrites may be able to reduce their reliance on that extension once they can run PostgreSQL 19. The project describes &lt;code&gt;pg_repack&lt;/code&gt; as a tool for reorganizing tables with minimal locks; PostgreSQL’s release notes describe the new &lt;code&gt;REPACK&lt;/code&gt; as combining the functions of &lt;code&gt;VACUUM FULL&lt;/code&gt; and &lt;code&gt;CLUSTER&lt;/code&gt;, with concurrent rewriting as an option. (&lt;a href=&quot;https://github.com/reorg/pg_repack&quot;&gt;&lt;code&gt;pg_repack&lt;/code&gt; project&lt;/a&gt;, &lt;a href=&quot;https://www.postgresql.org/docs/19/release-19.html&quot;&gt;PostgreSQL 19 release notes&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;That does not make every existing extension use obsolete. I would compare the actual feature set, supported versions, provider constraints, and operational procedures before removing a dependency. A built-in command can simplify installation and compatibility management, but the migration still needs a tested invocation, monitoring, permissions, and a response plan if the operation cannot finish as expected. Nor does the feature make scheduled maintenance disappear: it gives me another way to carry out a rewrite when a blocking window is difficult to arrange.&lt;/p&gt;
&lt;p&gt;For a database that already has a tolerable maintenance window, I would not automatically choose the concurrent path. The ordinary rewrite may be easier to schedule and reason about if its lock is acceptable. For a busy table where blocking access would be the dominant risk, I would test the concurrent option and ask whether the risks it introduces are manageable. That is a system-specific decision, not a universal upgrade rule.&lt;/p&gt;
&lt;h2 id=&quot;follow-the-work-into-writes-disk-and-replicas&quot;&gt;Follow the work into writes, disk and replicas&lt;/h2&gt;
&lt;p&gt;Concurrent rewriting changes when users encounter the operation; it does not make the work vanish. If applications continue to write to the target table while the rewrite runs, I would test at representative write rates and watch for increased contention, slower transactions, or a longer operation. I would not infer safety from a quiet test database: the important question is how the rewrite behaves alongside the real mix of reads, writes, and maintenance tasks.&lt;/p&gt;
&lt;p&gt;I would also budget for the rewrite as a period of overlapping storage use. The old table has to remain available while a replacement is built, so free space that looks adequate for the steady state may not be adequate during maintenance. I would check the space available where the table and its indexes live, include headroom for normal growth, and define a stop condition before starting. If the available space is tight, I would rather use a planned window or defer the operation than gamble on reaching the final swap.&lt;/p&gt;
&lt;p&gt;Replicas and recovery deserve the same attention. PostgreSQL 19 adds &lt;code&gt;max_repack_replication_slots&lt;/code&gt; in connection with concurrent repacking, which is a reminder that the feature has replication-related operational settings, not just a SQL command. (&lt;a href=&quot;https://www.postgresql.org/docs/19/release-19.html&quot;&gt;PostgreSQL 19 release notes&lt;/a&gt;) Before adopting it, I would test the effect on write-ahead log generation, replica lag, and retained WAL in the topology I actually run. Those are measurements to make in a representative environment, not outcomes I would assume from the word “concurrent.” I would verify that replicas catch up within acceptable limits and that the system has enough storage if they do not.&lt;/p&gt;
&lt;h2 id=&quot;make-the-decision-reversible&quot;&gt;Make the decision reversible&lt;/h2&gt;
&lt;p&gt;My evaluation would start with a small, representative table and a written runbook: prerequisites, command, monitoring signals, stop conditions, and cleanup or recovery steps. I would test normal completion, interruption, a replica falling behind, and a restart after a failed attempt. If a test cannot establish what happens to the table and any temporary state after interruption, I would not call the process operationally ready.&lt;/p&gt;
&lt;p&gt;Then I would compare the result with the current plan. Does the concurrent path remove a maintenance window that is genuinely hard to get, or merely replace a scheduled interruption with a longer period of elevated resource use? Does it let me retire an extension I would otherwise need to install and support, or does my environment impose separate constraints? Can I absorb the extra storage and replication work without threatening normal operations?&lt;/p&gt;
&lt;p&gt;I would adopt concurrent &lt;code&gt;REPACK&lt;/code&gt; where avoiding the blocking rewrite is worth the added resource and recovery planning. I would keep a scheduled window where that is the simpler, safer trade. PostgreSQL has moved the maintenance trade-off; it has not removed the need to make one.&lt;/p&gt;</content:encoded><category>postgresql</category><category>database-maintenance</category><category>operations</category></item><item><title>CI egress policy should follow workflow trust</title><link>https://vahidinasab.com/articles/ci-egress-policy-should-follow-workflow-trust</link><guid isPermaLink="true">https://vahidinasab.com/articles/ci-egress-policy-should-follow-workflow-trust</guid><description>Runner identity is not enough to decide what a CI job may contact; I explain how to map dependencies and separate policy by job purpose.</description><pubDate>Tue, 13 Oct 2026 11:21:16 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-runner-pool-is-not-a-trust-boundary&quot;&gt;A runner pool is not a trust boundary&lt;/h2&gt;
&lt;p&gt;A test job that runs untrusted pull-request code and a release job that publishes artifacts should not reach the same destinations just because both use the same runner pool. The pool tells me where a job can run; it does not tell me what that job ought to be able to contact.&lt;/p&gt;
&lt;p&gt;That distinction matters because outbound access is a capability. A compromised action or dependency can use it to reach a host the workflow author never intended. GitHub’s &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/&quot;&gt;2026 Actions security roadmap&lt;/a&gt; calls out unrestricted outbound access on GitHub-hosted runners and describes a native egress firewall as a way to observe and control it. The &lt;a href=&quot;https://github.com/orgs/community/discussions/190621&quot;&gt;roadmap discussion in GitHub Community&lt;/a&gt; also includes a request for rules scoped per workflow, rather than only global policy.&lt;/p&gt;
&lt;p&gt;I agree with the direction of that request. I would not start by asking, “Which runners need access to this host?” I would ask, “Which job purpose needs access, and why?” A test job may need package registries and a test service. A release job may need an artifact store or deployment endpoint. Giving both jobs the union of those destinations is convenient, but it turns convenience into a standing permission.&lt;/p&gt;
&lt;p&gt;This is not an argument that every job needs a unique firewall rule. Fine-grained policy can become so costly to maintain that people disable it or add broad exceptions. The goal is a small number of understandable trust profiles, with each profile tied to work the job actually performs.&lt;/p&gt;
&lt;h2 id=&quot;map-dependencies-before-enforcing-rules&quot;&gt;Map dependencies before enforcing rules&lt;/h2&gt;
&lt;p&gt;I would first make a dependency map from observed traffic and workflow definitions. For each job type, record the hosts it contacts, the reason for each connection, and whether the dependency is direct or introduced by a tool, action, package manager, or container build. The map should include the less obvious destinations: a package manager’s mirror, an identity provider, an artifact registry, or a service used only during setup.&lt;/p&gt;
&lt;p&gt;The reason matters as much as the hostname. “The job connects to this registry” is a starting point, not a policy justification. Is it fetching test dependencies, pulling a build image, or publishing a release artifact? If the same host supports both downloading and publishing, the network allowlist alone may not distinguish those actions. I would then look for other controls—credentials scoped to the job, repository permissions, or a separate publishing step—rather than pretending a domain rule provides finer control than it does.&lt;/p&gt;
&lt;p&gt;I would classify jobs by purpose before choosing policy profiles. For example, tests, builds, and releases are useful starting categories, but I would split them further only when their required destinations or trust differ materially. A test job that runs code from an untrusted contribution deserves different treatment from a test job that runs only after code is trusted. The label “test” does not make them equivalent.&lt;/p&gt;
&lt;p&gt;The first pass should be observational, not a block-everything launch. The roadmap describes a monitor mode intended to help teams understand runner traffic before enforcement. I would use that visibility to discover dependencies, then check whether each observed connection is legitimate. A traffic log is evidence of what happened, not proof that the connection should remain allowed.&lt;/p&gt;
&lt;p&gt;Once I have that map, I can define the policy in terms people can review: this job profile may fetch build dependencies, while that profile may additionally publish to the release destination. I would keep the explanation next to the policy. A bare list of hostnames decays quickly because maintainers cannot tell whether an entry is essential, stale, or an accidental workaround.&lt;/p&gt;
&lt;h2 id=&quot;put-the-policy-where-it-can-be-maintained&quot;&gt;Put the policy where it can be maintained&lt;/h2&gt;
&lt;p&gt;A reusable workflow is a good place to standardize job purpose and invocation across repositories. GitHub documents reusable workflows as callable workflows, with inputs and secrets passed by the caller; the &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows&quot;&gt;reusable workflows guide&lt;/a&gt; explains that model. I can use a shared workflow to keep the job definition consistent, but I should not assume that reusing YAML automatically creates an egress boundary. The runner infrastructure still needs to enforce the network policy associated with the job.&lt;/p&gt;
&lt;p&gt;For self-hosted runners, I would express the distinction in runner placement and apply network controls outside the workflow process where possible. GitHub’s &lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/reference/security/secure-use&quot;&gt;self-hosted runner security guidance&lt;/a&gt; recommends reducing runner exposure and using runner groups to limit which repositories can access runners. Those controls help determine who may schedule work onto a runner; they are not a substitute for restricting what that runner can reach over the network.&lt;/p&gt;
&lt;p&gt;An illustrative caller might make the job’s purpose visible in the runner labels and reusable workflow it invokes:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build and release&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  push&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    branches&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example-org/engineering-workflows/.github/workflows/test.yml@main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      runner_class&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ci-test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  release&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    needs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example-org/engineering-workflows/.github/workflows/release.yml@main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      runner_class&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ci-release&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      packages&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;write&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The labels here are an organizational example, not a built-in firewall feature. I would make them select runner pools whose network policy is deliberately different, and ensure the workflow caller cannot quietly choose a more privileged pool without review. In a real setup, I would also pin shared workflow references to reviewed revisions rather than letting an unreviewed change silently alter every caller. The important property is not the string in &lt;code&gt;runner_class&lt;/code&gt;; it is the enforceable relationship between job purpose, runner assignment, and outbound policy.&lt;/p&gt;
&lt;p&gt;Reusable workflows also have to avoid turning centralization into a blind spot. A shared workflow can hide network calls inside actions or scripts, so I would make its required destinations visible and reviewed as part of the shared contract. When a workflow changes its dependencies, that change should trigger a policy review, not be discovered only after a blocked build.&lt;/p&gt;
&lt;h2 id=&quot;keep-enforcement-strict-and-changes-survivable&quot;&gt;Keep enforcement strict and changes survivable&lt;/h2&gt;
&lt;p&gt;The common failure mode is an allowlist that is either too broad to matter or so brittle that normal maintenance repeatedly breaks builds. I would avoid both by separating discovery from enforcement and giving policy changes an explicit owner. For a newly required destination, the job should fail with enough context to identify the workflow and attempted connection; the fix should be a reviewed policy change, not a hurried global exception.&lt;/p&gt;
&lt;p&gt;I would also watch for policy drift. If a destination has not appeared in observed traffic for a while, that is a reason to ask whether it is still needed, not to remove it blindly. A rarely run release workflow may still need its publishing endpoint. Conversely, an endpoint that appears in every run may be an unnecessary dependency carried by a tool. The policy review should consider purpose and workflow frequency, not just traffic volume.&lt;/p&gt;
&lt;p&gt;There is a trade-off in where to enforce. Runner-level rules are easier to administer and useful as a coarse baseline, especially for self-hosted infrastructure. But if one pool serves jobs with different trust and responsibilities, a single permissive rule grants every job the union of their access. Workflow- or job-aware policy can make narrower permissions possible, but it requires dependable identity, usable logs, and a manageable way to keep shared workflow definitions aligned with infrastructure policy.&lt;/p&gt;
&lt;p&gt;I would therefore keep a baseline at the runner or network-zone level, then separate pools or policy profiles where job responsibilities genuinely differ. I would not create a bespoke rule for every repository when a shared test or release profile expresses the same need. And I would not treat GitHub’s announced firewall as a complete answer for self-hosted runners: the roadmap describes the native capability for GitHub-hosted runners, while teams running their own infrastructure still need to enforce and maintain their own network boundaries.&lt;/p&gt;
&lt;p&gt;The design test is simple: if I move a job from testing to publishing, does its outbound capability change because its responsibility changed, or does it inherit whatever the runner pool happens to allow? If the answer is the latter, runner identity is doing too much work. Egress policy should follow the trust and purpose of the workflow, with the runner acting as the enforcement point—not as the definition of trust.&lt;/p&gt;</content:encoded><category>ci-cd</category><category>security</category><category>github-actions</category></item><item><title>Audit who owns your Kubernetes service IPs</title><link>https://vahidinasab.com/articles/audit-who-owns-your-kubernetes-service-ips</link><guid isPermaLink="true">https://vahidinasab.com/articles/audit-who-owns-your-kubernetes-service-ips</guid><description>An ownership-first audit of Services using externalIPs helps you choose a safe migration path before Kubernetes removes that behavior.</description><pubDate>Tue, 13 Oct 2026 11:21:13 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-deprecated-field-points-to-an-ownership-gap&quot;&gt;A deprecated field points to an ownership gap&lt;/h2&gt;
&lt;p&gt;A Service with &lt;code&gt;.spec.externalIPs&lt;/code&gt; can make a workload reachable at an address without making clear who is responsible for that address. That is the part I would investigate first.&lt;/p&gt;
&lt;p&gt;In &lt;a href=&quot;https://kubernetes.io/blog/2026/05/14/kubernetes-v1-36-deprecation-and-removal-of-service-externalips/&quot;&gt;Kubernetes v1.36’s announcement on deprecating Service &lt;code&gt;externalIPs&lt;/code&gt;&lt;/a&gt;, the project explains that the field is deprecated and that kube-proxy support is expected to be disabled in a future release. The announcement also distinguishes this field from other uses of the phrase “external IP,” including addresses associated with &lt;code&gt;LoadBalancer&lt;/code&gt; Services. So the first task is not to replace every address that looks external. It is to find Services that set this specific field, then trace what actually makes their addresses reachable.&lt;/p&gt;
&lt;p&gt;The risk is broader than future incompatibility. The Kubernetes announcement connects &lt;code&gt;externalIPs&lt;/code&gt; with &lt;a href=&quot;https://kubernetes.io/blog/2026/05/14/kubernetes-v1-36-deprecation-and-removal-of-service-externalips/&quot;&gt;CVE-2020-8554&lt;/a&gt;, a security issue involving the ability to intercept traffic. My architectural reading is that the field can obscure an important boundary: Kubernetes can direct matching traffic to Service endpoints, but the field itself does not establish who is entitled to use an address or how the surrounding network routes it.&lt;/p&gt;
&lt;p&gt;That leaves questions the manifest may not answer. Who allocates the address? Which network equipment advertises or routes it? Who prevents two Services from claiming it? Which team is allowed to expose a service there? If the answers live in separate systems and nobody owns the whole path, changing the Service type alone may preserve the same weakness under a new name.&lt;/p&gt;
&lt;h2 id=&quot;inventory-the-traffic-path-not-just-the-yaml&quot;&gt;Inventory the traffic path, not just the YAML&lt;/h2&gt;
&lt;p&gt;I would start by listing every Service that sets &lt;code&gt;externalIPs&lt;/code&gt;, with its namespace, name, address, type, and owning workload or team. A quick query can find the field, but it cannot tell you whether traffic reaches that address, whether the address is still in use, or what external infrastructure makes it work.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; get&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; services&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --all-namespaces&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; jsonpath=&apos;{range .items[?(@.spec.externalIPs)]}{.metadata.namespace}{&quot;/&quot;}{.metadata.name}{&quot;\t&quot;}{.spec.type}{&quot;\t&quot;}{.spec.externalIPs}{&quot;\n&quot;}{end}&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Treat that list as the start of an investigation, not a migration plan. Compare it with the source of truth for address allocation and with network configuration outside the cluster. Check the routes, firewall rules, load balancers, DNS records, and monitoring that might depend on each address. Then establish whether clients connect directly to it, whether another proxy forwards traffic to it, and what happens if the Service or its endpoints are unavailable.&lt;/p&gt;
&lt;p&gt;I would record the result as an ownership map. For each address, name the system that allocates it, the system that advertises or routes it, the controller that reconciles the Kubernetes object, and the team accountable for approving exposure. If one address is managed manually, say so. If a detail cannot be established, mark it as unknown rather than assuming Kubernetes owns it because the address appears in a Service manifest.&lt;/p&gt;
&lt;p&gt;This distinction matters in both cloud-backed and self-hosted clusters. A cloud controller may provision an external load balancer for a &lt;code&gt;LoadBalancer&lt;/code&gt; Service, while a bare-metal cluster may rely on a separate implementation and network integration. &lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/&quot;&gt;The Kubernetes Service documentation&lt;/a&gt; describes how &lt;code&gt;LoadBalancer&lt;/code&gt; Services are typically implemented through an external load balancer. &lt;a href=&quot;https://metallb.io/configuration/&quot;&gt;MetalLB’s configuration documentation&lt;/a&gt; shows one bare-metal approach: allocate addresses from configured pools and advertise them through network protocols. These are different ownership arrangements, not interchangeable spellings for the same thing.&lt;/p&gt;
&lt;h2 id=&quot;choose-the-replacement-after-you-know-the-owner&quot;&gt;Choose the replacement after you know the owner&lt;/h2&gt;
&lt;p&gt;Once the traffic path is understood, choose a mechanism that puts address allocation and reachability under an explicit owner. I would not start by changing &lt;code&gt;externalIPs&lt;/code&gt; to &lt;code&gt;type: LoadBalancer&lt;/code&gt; in every manifest. That might be right when a load-balancer controller can allocate and advertise the address, but it can also change who controls allocation, how an address is reserved, or which network boundary receives traffic.&lt;/p&gt;
&lt;p&gt;For a cloud-backed cluster, a &lt;code&gt;LoadBalancer&lt;/code&gt; Service may be the natural replacement if the cloud integration owns the load balancer and the required address lifecycle. Confirm how it handles existing addresses, access restrictions, health checks, and deletion before migrating. The &lt;a href=&quot;https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/&quot;&gt;Kubernetes guide to creating an external load balancer&lt;/a&gt; explains that Kubernetes automates configuration through the relevant external load-balancer integration; that does not mean every provider has identical behavior or supports every reservation requirement.&lt;/p&gt;
&lt;p&gt;For a self-hosted cluster, first identify which component should own the address pool and route advertisement. MetalLB is one possible implementation: its documentation describes address pools for assigning IPs to Services and separate configuration for announcing them. The key decision is not “cloud or bare metal”; it is whether the controller and network configuration together provide a clear, reviewable allocation and announcement path.&lt;/p&gt;
&lt;p&gt;Other traffic may belong behind an ingress controller, gateway, or an existing network load balancer. In that design, the Service can remain an internal backend while the gateway owns the client-facing address and policy. I would choose this when the organization needs a deliberate application-layer boundary, not just a stable IP. The cost is an additional component and a dependency to operate; the benefit is that exposure policy and address ownership can be managed at that boundary rather than inferred from arbitrary Service fields.&lt;/p&gt;
&lt;p&gt;Before changing anything, verify that the selected owner can satisfy the actual requirements: address preservation where necessary, protocols in use, source-address handling, allowed clients, and failure behavior. These are properties of the complete path. A manifest that applies successfully is not evidence that clients will keep reaching the same destination or that the same security controls remain in force.&lt;/p&gt;
&lt;h2 id=&quot;make-the-migration-prove-the-boundary&quot;&gt;Make the migration prove the boundary&lt;/h2&gt;
&lt;p&gt;I would migrate one clearly understood path at a time. Keep the old and new paths distinguishable during testing, and verify reachability from the client networks that matter, not only from inside the cluster. Check that the new controller reports the expected allocation, that routing and firewall policy match the intended exposure, and that observability can identify failures at the new boundary. I would also test deletion and recreation behavior where retaining an address is a requirement; address lifecycle is part of the contract, not an incidental implementation detail.&lt;/p&gt;
&lt;p&gt;A small manifest change can illustrate the destination, but the controller and network setup determine whether it works:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;apiVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;metadata&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;spec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  type&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;LoadBalancer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  selector&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    app&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  ports&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;port&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;80&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      targetPort&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;8080&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example does not reserve an address, configure a provider, or define who may expose the Service. Those details must be expressed through the chosen implementation and the surrounding infrastructure. I would make the ownership decision reviewable alongside the Service configuration, rather than leaving it in an undocumented operational convention.&lt;/p&gt;
&lt;p&gt;After migration, prevent the old pattern from returning. The Kubernetes deprecation announcement recommends considering the &lt;code&gt;DenyServiceExternalIPs&lt;/code&gt; admission controller as a way to block future use of the field. That is a useful guardrail once existing dependencies have been found and addressed. A blanket block applied before the inventory, however, could reject changes without revealing which production traffic still depends on the old path.&lt;/p&gt;
&lt;p&gt;The useful outcome of this audit is not a uniform Service type. It is a defensible answer, for every externally reachable Service, to who assigns its address, who makes it reachable, and who is accountable for that exposure. Once those owners are explicit, the migration choice becomes a consequence of the traffic design rather than a hurried search for a replacement field.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>architecture</category><category>security</category><category>networking</category></item><item><title>Deeper workflow nesting needs a legible call graph</title><link>https://vahidinasab.com/articles/deeper-workflow-nesting-needs-a-legible-call-graph</link><guid isPermaLink="true">https://vahidinasab.com/articles/deeper-workflow-nesting-needs-a-legible-call-graph</guid><description>Deeper reusable workflows make shared CI/CD more flexible, but clear ownership, inputs, permissions, and deployment paths are what keep changes reviewable.</description><pubDate>Tue, 13 Oct 2026 11:21:10 GMT</pubDate><content:encoded>&lt;h2 id=&quot;more-depth-is-not-a-design-goal&quot;&gt;More depth is not a design goal&lt;/h2&gt;
&lt;p&gt;A reusable workflow should hide repeated implementation, not hide what a pipeline does. That distinction matters more as a workflow grows deeper: GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/sharing-automations/reuse-workflows#nesting-reusable-workflows&quot;&gt;reusable workflow documentation&lt;/a&gt; allows up to ten connected workflow levels, including the caller, and a maximum of 50 unique reusable workflows from a single workflow file. Those limits make more compositions possible. They do not make a long chain easier to understand.&lt;/p&gt;
&lt;p&gt;I would use the extra depth to clarify ownership and reuse. A repository workflow should show the stages relevant to that repository. Shared workflows should own cohesive capabilities, such as building an artifact or deploying it, and call further workflows only where that removes real duplication. If a reviewer has to follow a chain of generic wrappers just to learn which environment receives a deployment, the abstraction has gone too far.&lt;/p&gt;
&lt;p&gt;The practical test I use is simple: can someone reviewing the caller identify its inputs, the permissions it grants, and the path from a change to deployment without reconstructing hidden behavior from several files? If not, the graph needs to be clearer before it needs another layer.&lt;/p&gt;
&lt;h2 id=&quot;give-every-edge-a-reason&quot;&gt;Give every edge a reason&lt;/h2&gt;
&lt;p&gt;A call graph is more useful when each edge corresponds to a boundary someone can explain. A top-level workflow might call a build workflow, then a deployment workflow. The build workflow might delegate a language-specific check to a shared workflow. That can be sensible if the language check has a clear owner and stable interface. It is less sensible if every file exists only to pass through a call to another file.&lt;/p&gt;
&lt;p&gt;For example, this caller makes its two main responsibilities and their ordering visible. The references are illustrative; a real repository would point to workflows it can access and maintain.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build and deploy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  push&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    branches&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  build&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./.github/workflows/build.yml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      runtime&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  deploy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    needs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example-org/platform-ci/.github/workflows/deploy.yml@v3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      environment&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;production&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      id-token&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;write&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The point is not that every pipeline should have exactly these two calls. It is that a call site should answer the first questions a reviewer has: what capability is invoked, what does it receive, what can it access, and when does it run? If the deployment target is hidden several layers away, make that path easy to follow in the shared workflow’s name, interface, and documentation.&lt;/p&gt;
&lt;p&gt;I would keep workflow names and inputs aligned with the work they perform. &lt;code&gt;deploy.yml&lt;/code&gt; with an &lt;code&gt;environment&lt;/code&gt; input is easier to reason about than &lt;code&gt;common.yml&lt;/code&gt; with a string that changes meaning by caller. A small, explicit interface also makes it easier to identify which repositories depend on a shared workflow when that interface changes.&lt;/p&gt;
&lt;h2 id=&quot;make-permissions-and-inputs-part-of-the-graph&quot;&gt;Make permissions and inputs part of the graph&lt;/h2&gt;
&lt;p&gt;A call graph that shows only file names is incomplete. Inputs determine what a called workflow does; permissions determine what it can do. Both belong in the review, at the call site and at the reusable workflow boundary.&lt;/p&gt;
&lt;p&gt;GitHub documents that permissions for &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; can be maintained or reduced as workflows are nested, but not elevated by a called workflow; it also notes that secrets are not automatically passed to reusable workflows. See &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations&quot;&gt;GitHub’s guidance on reusing workflow configurations&lt;/a&gt; and &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/sharing-automations/reuse-workflows#passing-secrets-to-nested-workflows&quot;&gt;passing secrets to reusable workflows&lt;/a&gt;. I take that as a reason to make the permission path deliberate, rather than scattering broad permissions across a chain and hoping the final job uses them safely.&lt;/p&gt;
&lt;p&gt;For each edge, I want to know which inputs cross it, whether any secret crosses it, and what token permissions are available downstream. A deploy workflow should not receive a powerful credential merely because an intermediate wrapper forwards every secret it was given. Conversely, a workflow that needs a permission should declare that need plainly enough that the caller and the reviewer can see it. The shape of the graph should help explain the security boundary, not obscure it.&lt;/p&gt;
&lt;p&gt;This is also where narrow inputs pay off. Passing a specific environment or artifact identifier communicates intent. Passing an ambiguous flag, or a bundle of loosely related values that downstream workflows interpret differently, turns the caller into a poor contract. When a shared workflow’s behavior depends on a choice, expose that choice as a named input and keep its meaning consistent. If a choice is only relevant to one consumer, it may belong in that consumer’s workflow instead of in a shared interface.&lt;/p&gt;
&lt;h2 id=&quot;test-the-composition-not-just-the-pieces&quot;&gt;Test the composition, not just the pieces&lt;/h2&gt;
&lt;p&gt;A workflow can be valid in isolation and still be wrong in composition. A caller may pass an unexpected value, a nested workflow may be inaccessible to the initial caller, or a change to a shared workflow may alter what downstream repositories do. GitHub’s documentation notes that every workflow in a nested chain must be accessible to the caller, and that loops in the workflow tree are not permitted. Those constraints are worth checking alongside the YAML syntax, not after a change has already spread.&lt;/p&gt;
&lt;p&gt;I would test shared workflow changes against representative callers and make the supported interface explicit. That means checking more than whether the reusable workflow starts: verify its inputs, permissions, expected outputs, and the deployment path that results. A test caller should exercise the composition a real repository uses, rather than just invoke the deepest workflow directly. When a shared workflow is used across many repositories, I would also make its version reference and the process for adopting a breaking change visible. A reference that can move independently of a caller makes review and diagnosis harder; choose a pinning and update policy that fits the team’s release practice.&lt;/p&gt;
&lt;p&gt;GitHub provides a &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/monitor-workflows#using-the-visualization-graph&quot;&gt;visualization graph for monitoring a workflow run&lt;/a&gt;. That is useful for following an actual run, but it is not a substitute for a legible design-time graph: the caller and shared workflow files still need to reveal ownership, inputs, permissions, and intent when someone reviews a change.&lt;/p&gt;
&lt;p&gt;Deeper nesting is useful when it gives a shared capability a clear owner and keeps callers concise without hiding their behavior. If another layer makes the deployment route or security boundary harder to explain, I would flatten it. The limit is not the number of files GitHub permits; it is whether a reviewer can trace the change.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>software-architecture</category></item><item><title>Treat Helm 4 adoption as a deployment-toolchain migration</title><link>https://vahidinasab.com/articles/treat-helm-4-adoption-as-a-deployment-toolchain-migration</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-helm-4-adoption-as-a-deployment-toolchain-migration</guid><description>A Helm major-version upgrade can leave charts unchanged while changing assumptions in clients, plugins, pipelines, and cluster interactions.</description><pubDate>Tue, 13 Oct 2026 11:21:07 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-chart-is-only-one-part-of-the-deployment&quot;&gt;The chart is only one part of the deployment&lt;/h2&gt;
&lt;p&gt;A chart can stay unchanged while the deployment path around it changes. That is why I would evaluate Helm 4 as a toolchain migration, not as a binary replacement that is safe whenever the chart repository has no diff.&lt;/p&gt;
&lt;p&gt;Helm 4 was released in November 2025 as the first new major version in six years, according to the &lt;a href=&quot;https://helm.sh/blog/helm-4-released&quot;&gt;Helm 4 release announcement&lt;/a&gt;. The project says most Helm 3 charts and workflows are expected to remain compatible, but its &lt;a href=&quot;https://github.com/helm/helm/releases/tag/v4.0.0&quot;&gt;v4.0.0 release notes&lt;/a&gt; also call out backward-incompatible CLI flags and output, SDK changes, a redesigned plugin system, and post-renderers becoming plugins. The notes explicitly recommend testing chart installations and upgrades; they also warn that changes such as server-side apply may affect the experience.&lt;/p&gt;
&lt;p&gt;That combination matters. “Most charts should work” is a compatibility expectation, not evidence that a particular pipeline, plugin, cluster, and set of deployment habits will behave as before. The unit I want to test is the complete deployment path: the Helm client that runs, the tools it loads, the shared steps that invoke it, and the clusters that receive the rendered resources.&lt;/p&gt;
&lt;h2 id=&quot;map-the-path-before-changing-it&quot;&gt;Map the path before changing it&lt;/h2&gt;
&lt;p&gt;I would start by finding every place the client enters the system. It may be installed in a shared CI image, downloaded by a pipeline step, or provided by a developer’s local setup. I would check the version actually used rather than infer it from a repository file or a step name. I would also inventory plugins, post-renderers, scripts that inspect command output, and any code that calls the Helm SDK.&lt;/p&gt;
&lt;p&gt;This inventory is not bureaucracy. A deployment path can have assumptions that are invisible in the chart itself: a plugin may be installed by a bootstrap script; a reusable workflow may select the client version; a wrapper may parse CLI output; a post-renderer may be passed as an executable. Helm’s release notes make the CLI and plugin changes relevant areas to review, but the inventory tells me which of those changes could affect my own path.&lt;/p&gt;
&lt;p&gt;I would capture the current workflow before editing it. For example, a team could record the commands and inputs used by a representative deployment in a small test script:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;helm&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;helm&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; plugin&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; list&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;helm&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; lint&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; ./charts/example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;helm&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; template&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; example&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; ./charts/example&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  --namespace&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  --values&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; ./charts/example/values-test.yaml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;helm&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; upgrade&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --install&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; example&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; ./charts/example&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  --namespace&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  --create-namespace&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  --values&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; ./charts/example/values-test.yaml&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;  --wait&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a starting point, not a universal test harness. The chart path, values, credentials, and release lifecycle need to reflect the deployment being tested. If production depends on a plugin or post-renderer, the test must exercise that integration too. If a pipeline has a wrapper around these commands, I would test the wrapper rather than only running Helm by hand.&lt;/p&gt;
&lt;h2 id=&quot;build-a-compatibility-matrix-around-real-deployments&quot;&gt;Build a compatibility matrix around real deployments&lt;/h2&gt;
&lt;p&gt;The cost of this approach is a compatibility test matrix. I think that cost is justified: the alternative is discovering a toolchain assumption during a rollout, when diagnosing whether the problem is the client, a plugin, a chart, or a cluster is harder.&lt;/p&gt;
&lt;p&gt;I would keep the matrix representative rather than attempt to test every possible combination. Include the client versions involved in the transition, the deployment paths that use different plugins or wrappers, and the kinds of clusters the organisation actually runs. Select charts that exercise meaningfully different deployment patterns, such as one with custom resource definitions or one that uses a post-renderer, if those patterns exist in the estate. These are selection criteria, not claims that every team uses those features.&lt;/p&gt;
&lt;p&gt;For each selected path, I would compare more than whether the command exits successfully. Check rendered output for unexpected changes, confirm that the install and upgrade complete against the intended cluster, and inspect the resulting resources and release state. Where a deployment depends on a plugin, verify that it is available and behaves as expected in the client environment. Where an SDK consumer exists, test that separately: a successful CLI run says nothing about code compiled against an SDK whose APIs have changed.&lt;/p&gt;
&lt;p&gt;The cluster part of the test is important. A local render can help identify template and output differences, but it does not prove that an install or upgrade works against the Kubernetes versions and configurations the team operates. I would run the representative install and upgrade against those environments, using an isolated namespace or other safe test target. The point is not to assume a failure; it is to make compatibility observable before the production rollout.&lt;/p&gt;
&lt;h2 id=&quot;put-the-version-change-in-the-shared-path&quot;&gt;Put the version change in the shared path&lt;/h2&gt;
&lt;p&gt;Once the test is clear, I would change the shared pipeline step or image that selects Helm, rather than scatter a new version across repositories. A central version choice makes the evaluation reproducible and gives teams one place to follow the transition. It also fits my preference for reusable pipeline logic over copied deployment scripts.&lt;/p&gt;
&lt;p&gt;I would make the version explicit and keep the test attached to the shared path. In a pipeline, the shape might be:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Install selected Helm client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./scripts/install-helm &quot;$HELM_VERSION&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Check client and plugins&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;      helm version&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;      helm plugin list&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Test representative chart deployment&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./scripts/test-helm-deployment ./charts/example&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is illustrative YAML, not a drop-in workflow: the installation script, environment variable, and test command need to match the pipeline in use. The important property is that the selected client, plugin setup, and representative deployment test are visible in the shared path, instead of being implicit in a runner image or left to individual repositories.&lt;/p&gt;
&lt;p&gt;I would keep the previous deployment path available during the transition, and move production workloads in a controlled sequence after the tests pass. If an issue appears, a known client version and pipeline definition make it easier to isolate the change. I would not treat an unchanged chart as a rollback plan; the executable toolchain is part of the deployment too.&lt;/p&gt;
&lt;h2 id=&quot;make-compatibility-a-release-decision&quot;&gt;Make compatibility a release decision&lt;/h2&gt;
&lt;p&gt;I would approve Helm 4 for a deployment path only when the client setup is reproducible, the plugins and wrappers it needs have been checked, and representative installs and upgrades pass against the clusters that path targets. That is a more useful release criterion than “the charts did not change.”&lt;/p&gt;
&lt;p&gt;The matrix costs time to build and maintain. It is also a record of what the deployment depends on, which is valuable beyond this upgrade. Helm 4 is a prompt to make those dependencies explicit: charts describe what is deployed, but the toolchain determines how the description becomes a cluster change.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>helm</category><category>ci-cd</category></item><item><title>Execution policy belongs outside the workflow</title><link>https://vahidinasab.com/articles/execution-policy-belongs-outside-the-workflow</link><guid isPermaLink="true">https://vahidinasab.com/articles/execution-policy-belongs-outside-the-workflow</guid><description>GitHub Actions execution protections can block disallowed runs before workflow code starts, but they cannot make trusted workflow code safe.</description><pubDate>Tue, 13 Oct 2026 11:21:03 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-shared-workflow-is-still-workflow-code&quot;&gt;A shared workflow is still workflow code&lt;/h2&gt;
&lt;p&gt;A reusable workflow is a good way to stop teams copying pipeline logic. It gives repositories a maintained path for common work: build, test, package, and perhaps deploy. I have helped design pipelines reused across many repositories, and I would still choose reuse over asking every team to maintain its own copy.&lt;/p&gt;
&lt;p&gt;But reuse and enforcement solve different problems. A shared workflow is a building block that repository workflow code can choose to call. If the security boundary is the workflow file itself, a repository that can change that file may also change whether it calls the shared workflow, what it passes to it, and what else runs around it. Requiring every team to use the right YAML is not the same as a control outside that YAML.&lt;/p&gt;
&lt;p&gt;GitHub’s &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap&quot;&gt;2026 Actions security roadmap&lt;/a&gt; makes that distinction explicit: execution protections are intended to govern whether a workflow runs, rather than relying on the workflow file alone. The feature entered public preview in June 2026 and became generally available on September 17, 2026, according to GitHub’s &lt;a href=&quot;https://github.blog/changelog/2026-06-18-control-who-and-what-triggers-github-actions-workflows&quot;&gt;public-preview announcement&lt;/a&gt; and &lt;a href=&quot;https://github.blog/changelog/2026-09-17-workflow-execution-protections-in-github-actions-generally-available&quot;&gt;general-availability announcement&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That is the architectural shift worth paying attention to. A reusable workflow standardizes how an approved run is implemented. An execution policy decides whether the run is allowed to begin.&lt;/p&gt;
&lt;h2 id=&quot;put-the-decision-before-the-code&quot;&gt;Put the decision before the code&lt;/h2&gt;
&lt;p&gt;Workflow execution protections let administrators define allowlists for who can trigger workflows and which events can start them. GitHub evaluates those protections before a run, and the generally available version can target individual workflow files. The &lt;a href=&quot;https://docs.github.com/actions/how-tos/administer/control-workflow-execution&quot;&gt;GitHub documentation on controlling workflow execution&lt;/a&gt; describes how policies can be layered across enterprise, organization, and repository levels.&lt;/p&gt;
&lt;p&gt;That moves part of the trust decision out of the file under review. A workflow can say it should run on a pull request, but that declaration is not, by itself, an organization-level guarantee that every actor or event is acceptable. The protection is useful precisely because a policy owner can set limits that a repository’s workflow author cannot quietly remove by editing YAML.&lt;/p&gt;
&lt;p&gt;For example, this repository workflow describes a normal pull-request build:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build and test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    branches&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;main&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet test&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The snippet is workflow configuration, not an execution protection. It expresses the repository’s intent: run tests for pull requests and grant the token read access to repository contents. An administrator’s execution policy is a separate decision about whether that workflow may run for a particular actor and event. GitHub’s &lt;a href=&quot;https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions&quot;&gt;workflow syntax reference&lt;/a&gt; documents workflow-level permissions; its &lt;a href=&quot;https://docs.github.com/actions/how-tos/administer/control-workflow-execution&quot;&gt;execution protection guidance&lt;/a&gt; describes the distinct policy layer.&lt;/p&gt;
&lt;p&gt;That separation matters most when the workflow file is part of the change being evaluated. A pull request can alter workflow code as well as application code. If I expect the workflow itself to enforce the only restriction, I am trusting code that the policy is meant to constrain. Moving the run/no-run decision outside the file makes that particular bypass harder: the workflow cannot author its way around a centrally managed actor or event rule.&lt;/p&gt;
&lt;h2 id=&quot;policy-is-not-a-code-auditor&quot;&gt;Policy is not a code auditor&lt;/h2&gt;
&lt;p&gt;I would not mistake this boundary for a verdict on everything that happens after execution starts. An allowed workflow can still be badly written, call an unsafe action, expose a credential to untrusted input, or request more token access than its job needs. Execution protections govern who and what may trigger a run; they do not prove that the workflow contents are safe. GitHub’s &lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/reference/security/secure-use&quot;&gt;secure use reference&lt;/a&gt; separately recommends minimizing &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions and reviewing action code.&lt;/p&gt;
&lt;p&gt;That means I would keep two questions separate in a design review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Is this actor and event allowed to start this workflow?&lt;/li&gt;
&lt;li&gt;Given that it starts, what code, credentials, permissions, and runner environment can it reach?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first is a good fit for an external policy boundary. The second still depends on workflow design, repository protections, credential scoping, and review. A reusable workflow can help with the second question by giving teams a safer implementation to call, but it cannot replace the first. Nor does centralizing the workflow automatically make every caller safe: inputs, permissions, and surrounding jobs still deserve scrutiny.&lt;/p&gt;
&lt;p&gt;I prefer this division because it makes ownership legible. Platform or security owners set the limits on execution. Product teams own the workflow changes within those limits. Shared pipeline maintainers provide well-tested building blocks. Those are related responsibilities, but they should not be collapsed into a single large reusable YAML file that is expected to act as both a library and a policy engine.&lt;/p&gt;
&lt;h2 id=&quot;central-control-has-a-shipping-cost&quot;&gt;Central control has a shipping cost&lt;/h2&gt;
&lt;p&gt;A policy boundary is not free. If administrators make every workflow depend on a centrally approved actor list and event list, an innocent new development path can be blocked along with a dangerous one. A team may need to test a branch, handle a release event, or use a manual run for a legitimate operational task. A rule that does not account for those needs creates friction, workarounds, and pressure to weaken the policy globally.&lt;/p&gt;
&lt;p&gt;I would therefore start with the workflows where the consequences of an unauthorized run are meaningful: deployment, release, or workflows with access to sensitive environments. Keep ordinary contribution checks as open as their risk allows. GitHub’s &lt;a href=&quot;https://github.blog/changelog/2026-09-17-workflow-execution-protections-in-github-actions-generally-available&quot;&gt;general-availability announcement&lt;/a&gt; describes targeting protections to specific workflow files, which supports this kind of distinction inside a repository.&lt;/p&gt;
&lt;p&gt;Where evaluate mode is available, I would use it to learn which existing runs a proposed policy would affect before enforcing it; the &lt;a href=&quot;https://docs.github.com/actions/how-tos/administer/control-workflow-execution&quot;&gt;policy documentation&lt;/a&gt; describes evaluation as a rollout option. That is not an argument for observing indefinitely. It is a way to find mismatches between the written rule and how teams actually ship, then make a deliberate choice about exceptions and enforcement.&lt;/p&gt;
&lt;p&gt;I would also keep the policy small enough to explain. Broad rules that make sense at the enterprise boundary should be non-negotiable. More specific constraints can live closer to the repositories that understand their release paths, provided lower-level policy adds restrictions rather than weakening the boundary. GitHub recommends layering protections across account levels in its &lt;a href=&quot;https://docs.github.com/actions/how-tos/administer/control-workflow-execution&quot;&gt;guidance for workflow execution policies&lt;/a&gt;. The governance principle is sound even without turning every special case into a global rule: centralize the minimum that must not vary, and let teams control the rest.&lt;/p&gt;
&lt;p&gt;The useful question is not whether shared YAML or centralized policy is better. I want both, doing different jobs. Reusable workflows reduce duplication and give teams a supported implementation path. Execution protections put the permission to run outside the code being constrained. I would not treat the former as a substitute for the latter.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>security</category><category>architecture</category></item><item><title>PostgreSQL 14’s end of life is a compatibility-planning problem</title><link>https://vahidinasab.com/articles/postgresql-14-end-of-life-compatibility-planning</link><guid isPermaLink="true">https://vahidinasab.com/articles/postgresql-14-end-of-life-compatibility-planning</guid><description>PostgreSQL 14 reaches end of life on November 12, 2026; I explain how to map compatibility checks and sequence migration work around each workload.</description><pubDate>Tue, 13 Oct 2026 11:21:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-date-is-a-trigger-not-a-migration-plan&quot;&gt;The date is a trigger, not a migration plan&lt;/h2&gt;
&lt;p&gt;PostgreSQL 14 is scheduled to reach end of life on November 12, 2026, according to the &lt;a href=&quot;https://www.postgresql.org/support/versioning/&quot;&gt;PostgreSQL versioning policy&lt;/a&gt;. I would treat that date as a reason to start making decisions now, not as a date by which every database must follow the same upgrade procedure.&lt;/p&gt;
&lt;p&gt;A major-version change is not complete when the new server starts. The application still needs to behave correctly; extensions need to work; replication needs to resume in the intended direction; backups need to remain restorable; and the chosen upgrade path needs to fit the workload’s downtime and recovery constraints. Those connections are where planning gets expensive. A server that starts successfully is evidence that one step worked, not proof that the system is compatible.&lt;/p&gt;
&lt;p&gt;I would begin by mapping those dependencies for each workload, rather than treating “PostgreSQL 14” as a single item on an infrastructure inventory. Two databases on the same major version can have different extensions, clients, replication arrangements, recovery requirements, and tolerance for downtime. They may need different target versions and different migration paths. The cutoff is shared; the plan does not have to be.&lt;/p&gt;
&lt;h2 id=&quot;map-the-compatibility-chain-before-picking-a-path&quot;&gt;Map the compatibility chain before picking a path&lt;/h2&gt;
&lt;p&gt;I’d make the first pass an inventory that answers practical questions, not just records server versions. Which application services connect to the database? Which driver versions and database features do they depend on? Which extensions are installed, and who provides packages compatible with the target server? Is replication physical, logical, or both? What backup format is in use, and has a restore been tested against the proposed destination? What downtime can the workload tolerate?&lt;/p&gt;
&lt;p&gt;That inventory helps turn a vague upgrade into explicit checks. For application behavior, I would identify queries and features the application relies on, then test representative reads and writes against the candidate version. A successful connection or a green startup check does not exercise query semantics, migrations, scheduled tasks, or the paths an application runs under load. I would use the application’s real test suite where possible and add targeted checks where it leaves important behavior uncovered.&lt;/p&gt;
&lt;p&gt;Extensions deserve their own line in the plan. I would compare the installed extension list with the packages and supported versions available for the target server, then check whether each extension needs a separate update step. PostgreSQL’s &lt;a href=&quot;https://www.postgresql.org/docs/18/extend-extensions.html&quot;&gt;extension packaging documentation&lt;/a&gt; describes how an extension can provide update scripts that &lt;code&gt;ALTER EXTENSION UPDATE&lt;/code&gt; runs. That mechanism does not mean every extension has a suitable update path for every target. I would confirm availability and test the actual update sequence on a restored copy rather than assume that an extension present on the old server will be ready on the new one.&lt;/p&gt;
&lt;p&gt;Replication and recovery also belong in the compatibility map, not in a last-minute operations checklist. PostgreSQL documents a specific &lt;a href=&quot;https://www.postgresql.org/docs/18/logical-replication-upgrade.html&quot;&gt;logical replication procedure for major-version upgrades&lt;/a&gt;; it is a procedure with prerequisites and steps, not a promise that an existing topology will transfer unchanged. For backups, I would distinguish logical dumps from file-level backups. The PostgreSQL &lt;a href=&quot;https://www.postgresql.org/docs/18/backup-dump.html&quot;&gt;SQL dump documentation&lt;/a&gt; notes that &lt;code&gt;pg_dump&lt;/code&gt; output can generally be loaded into newer PostgreSQL versions, while file-level backups and continuous archiving are server-version-specific. Either way, I would test the restore path: a backup that has not been restored is an assumption, not a demonstrated recovery option.&lt;/p&gt;
&lt;h2 id=&quot;choose-the-path-around-the-workload&quot;&gt;Choose the path around the workload&lt;/h2&gt;
&lt;p&gt;I would choose the upgrade method only after understanding those dependencies and constraints. PostgreSQL’s &lt;a href=&quot;https://www.postgresql.org/docs/18/pgupgrade.html&quot;&gt;&lt;code&gt;pg_upgrade&lt;/code&gt; documentation&lt;/a&gt; explains that it can move data files to a later major version without the dump-and-restore process; it is not needed for minor-version upgrades. That can make it an appropriate option when the workload needs a direct major-version transition and its environment meets the method’s requirements. It does not remove the need to validate application behavior, extensions, replication, and recovery around that transition.&lt;/p&gt;
&lt;p&gt;Logical replication can support a staged move, but it has its own coordination work. The official &lt;a href=&quot;https://www.postgresql.org/docs/18/logical-replication-upgrade.html&quot;&gt;major-version logical replication upgrade procedure&lt;/a&gt; includes steps for upgrading nodes and refreshing publications. I would evaluate it where a workload benefits from separating data movement from the final cutover, then test the full sequence—including the point at which writes change direction. It is not a free way to avoid downtime or compatibility checks; it moves some of the work into topology, replication, and cutover planning.&lt;/p&gt;
&lt;p&gt;Dump and restore is another path to consider when portability or a clean rebuild matters more than the time required to move and reload data. The SQL dump documentation describes the cross-version usefulness of logical dumps, but the practical cost depends on the workload and how long the restore and validation take. I would test that duration using a representative copy rather than assume it from a small development database.&lt;/p&gt;
&lt;p&gt;The relevant choice is not “Which upgrade method is best?” It is “Which method can this workload execute, validate, and recover from within its constraints?” I would write down the expected cutover, the checks that let the operator proceed, and the conditions that mean stop or roll back. If the rollback plan depends on an old server continuing to accept writes after cutover, that dependency needs to be designed and tested—not discovered during the change.&lt;/p&gt;
&lt;h2 id=&quot;sequence-the-work-dont-force-one-fleet-wide-move&quot;&gt;Sequence the work; don’t force one fleet-wide move&lt;/h2&gt;
&lt;p&gt;I would sequence the migration as a set of workload decisions. First, classify databases by dependency and operational constraint: extensions, replication, restore method, application coupling, and acceptable interruption. Next, identify a target and a candidate upgrade path for each group. Then rehearse the path on a restored copy, checking application behavior and the operational steps together. Finally, schedule production moves in cohorts, leaving room to apply what the earlier moves teach before progressing to workloads with different constraints.&lt;/p&gt;
&lt;p&gt;This is not an argument for delaying the decision until every detail is known. The date is close enough that unknowns should be made visible immediately: an extension with no confirmed target package, an untested restore, or a service with no representative test is a planning item with an owner, not a reason to assume compatibility. Nor would I make “move everything together” the default just because one shared deadline appears in a version policy. Workloads that can move independently should be able to do so; tightly coupled ones need an explicitly coordinated plan.&lt;/p&gt;
&lt;p&gt;I would call a migration ready when the surrounding system has been exercised on the target version and the team knows what it will do if a check fails. The server starting is a necessary milestone. The evidence that applications, extensions, replication, and recovery still fit together is what makes the upgrade complete.&lt;/p&gt;</content:encoded><category>postgresql</category><category>databases</category><category>architecture</category></item><item><title>Kubernetes 1.37 has two adoption gates</title><link>https://vahidinasab.com/articles/kubernetes-1-37-has-two-adoption-gates</link><guid isPermaLink="true">https://vahidinasab.com/articles/kubernetes-1-37-has-two-adoption-gates</guid><description>I separate the decision to adopt Kubernetes 1.37 from the decision to rely on it in production, then explain how to keep that policy portable.</description><pubDate>Tue, 13 Oct 2026 11:20:56 GMT</pubDate><content:encoded>&lt;h2 id=&quot;upstream-release-is-not-a-production-decision&quot;&gt;Upstream release is not a production decision&lt;/h2&gt;
&lt;p&gt;Kubernetes 1.37 was released upstream on August 26, 2026, but that date alone does not tell me when to run it in production. &lt;a href=&quot;https://kubernetes.io/blog/2026/08/26/kubernetes-v1-37-release/&quot;&gt;The Kubernetes v1.37 release announcement&lt;/a&gt; establishes that the project has shipped the version. It does not establish that every managed platform offers it in every region, that my workloads are ready for it, or that I should make it a production dependency now.&lt;/p&gt;
&lt;p&gt;I treat those as two separate gates. The first is whether I want to adopt the upstream version: read the release notes, understand the changes that matter to my workloads, and test the upgrade path. The second is whether I can depend on that version in production: confirm that the platform I actually run supports it in the places I need, and that the operational work fits the team’s capacity.&lt;/p&gt;
&lt;p&gt;The distinction matters because “available” has different meanings. For a team running its own cluster, upstream availability may mean the components can be obtained and the team can begin testing. The team also owns the upgrade plan and the consequences of maintaining that cluster. On a managed platform, the provider controls when a version becomes available and how its rollout proceeds. The &lt;a href=&quot;https://learn.microsoft.com/azure/aks/release-tracker&quot;&gt;AKS release tracker&lt;/a&gt; exposes release order by version and region; that is a practical reminder that a provider rollout is not necessarily one universal switch.&lt;/p&gt;
&lt;p&gt;I would not read a release announcement as an upgrade instruction. It is the start of an evaluation, not an automatic change to the production baseline.&lt;/p&gt;
&lt;h2 id=&quot;keep-the-baseline-separate-from-the-exception&quot;&gt;Keep the baseline separate from the exception&lt;/h2&gt;
&lt;p&gt;My default policy is to keep a common Kubernetes baseline wherever portability matters. That means I do not make a new upstream version mandatory across every environment just because the project has released it. I pick a supported baseline that all the relevant environments can meet, then move that baseline when the actual platforms and workloads are ready.&lt;/p&gt;
&lt;p&gt;A useful policy can be plain language rather than a complicated matrix:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;kubernetes-version-policy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  common-baseline&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;latest version supported across required platforms&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  upstream-evaluation&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;begin when a new minor version is released&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  production-adoption&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;after platform availability and workload validation&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  provider-specific-version&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;exception, with an owner and review date&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a policy sketch, not a Kubernetes configuration file. The important distinction is between &lt;em&gt;evaluating&lt;/em&gt; a version and &lt;em&gt;depending&lt;/em&gt; on it. Evaluation can begin when upstream ships. Production adoption waits for evidence about the environment where the cluster runs.&lt;/p&gt;
&lt;p&gt;A common baseline has a cost: a team may choose not to use a new capability as soon as it appears upstream. I accept that cost when the capability does not justify splitting the platform policy. A shared baseline makes it easier to reason about compatibility, write reusable automation, and move workloads between environments without turning every upgrade into a provider-specific exercise. Those are architectural benefits, not guarantees that every platform behaves identically.&lt;/p&gt;
&lt;p&gt;The upstream project’s &lt;a href=&quot;https://kubernetes.io/blog/2021/07/20/new-kubernetes-release-cadence/&quot;&gt;release cadence explanation&lt;/a&gt; is useful context for planning: releases arrive on a project schedule, while a team’s adoption still has to account for its own integrations and maintenance window. I use that schedule to start planning, not to assume that every downstream platform will match it.&lt;/p&gt;
&lt;h2 id=&quot;let-provider-timing-be-an-explicit-exception&quot;&gt;Let provider timing be an explicit exception&lt;/h2&gt;
&lt;p&gt;Sometimes waiting for the common baseline is the wrong call. A provider may offer a version or capability that solves a real problem, and a team may decide that the benefit is worth operating a separate path. I would allow that, but I would make the split visible in the version policy rather than letting it happen through an undocumented cluster-by-cluster choice.&lt;/p&gt;
&lt;p&gt;For every exception, I want a reason that is specific enough to review: what capability or constraint makes the provider-specific version worthwhile, which workloads depend on it, and what would let us return to the common baseline. If there is no meaningful benefit beyond “it is available,” I would not create the split. The extra version becomes another thing to test, document, and support. That burden is especially direct when the team operates its own clusters; with a managed service, the provider handles parts of the platform, but the team still has to understand its supported versions and the effect of its choice on workloads.&lt;/p&gt;
&lt;p&gt;I would also distinguish a preview from a production option. A preview can be useful for evaluation, but it should not silently become the version on which a production service depends. The policy should say what evidence is required before that transition, such as successful workload testing and confirmation that the provider’s support status matches the service’s needs.&lt;/p&gt;
&lt;p&gt;This is why I check the provider’s current version information rather than relying on a release-day assumption. The &lt;a href=&quot;https://learn.microsoft.com/azure/aks/supported-kubernetes-versions&quot;&gt;AKS supported Kubernetes versions page&lt;/a&gt; describes the service’s supported versions and release calendar. The relevant details can change over time, so I would check the current tracker for the target region and version when making the decision rather than infer availability from an upstream date.&lt;/p&gt;
&lt;h2 id=&quot;gate-production-on-the-whole-operating-path&quot;&gt;Gate production on the whole operating path&lt;/h2&gt;
&lt;p&gt;For a self-managed cluster, the production gate is a decision to take responsibility for the full upgrade path, not just to install a new control plane. I need to account for the cluster components, the nodes, the workloads, and the process for recovering if the change exposes a problem. That is operational work the team must plan and own. I would not take on that work for version currency alone.&lt;/p&gt;
&lt;p&gt;For a managed cluster, the provider changes the shape of that work, but not the need for a decision. I still need to know whether the target version is available where the cluster runs, what support applies, and whether the workload is ready. Provider documentation, such as the &lt;a href=&quot;https://learn.microsoft.com/azure/aks/supported-kubernetes-versions&quot;&gt;AKS version support policy&lt;/a&gt;, is part of that decision; it is not a substitute for validating my own application and dependencies.&lt;/p&gt;
&lt;p&gt;My policy for Kubernetes 1.37 is therefore simple: upstream release opens the evaluation gate; platform availability and workload validation open the production gate. I keep one common baseline where portability matters, and I accept provider-specific timing only when I can name the benefit and own the operational split. That keeps an upstream release from becoming an accidental production commitment.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>architecture</category><category>platform-engineering</category></item><item><title>A workflow security incident needs a recovery design</title><link>https://vahidinasab.com/articles/workflow-security-incident-recovery-design</link><guid isPermaLink="true">https://vahidinasab.com/articles/workflow-security-incident-recovery-design</guid><description>A recovery-ready CI architecture maps compromised actions to repositories and credentials, then restores safe shipping without reopening the same blast radius.</description><pubDate>Tue, 13 Oct 2026 11:20:53 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-patched-reference-is-not-a-recovery-plan&quot;&gt;A patched reference is not a recovery plan&lt;/h2&gt;
&lt;p&gt;Changing a compromised action reference is necessary, but it answers only one question: what code should run next time? It does not tell me which repositories used the action, what credentials those workflows could access, whether those credentials were exposed, or how to keep releases moving while I contain the risk.&lt;/p&gt;
&lt;p&gt;That gap is timely. In its &lt;a href=&quot;https://blog.gitguardian.com/ghostaction-github-actions-supply-chain-attack-returns/&quot;&gt;October 2026 report on the returning GhostAction campaign&lt;/a&gt;, GitGuardian reported that it had identified 772 affected repositories and that only 124 had been effectively cleaned in observed public history by October 5, 2026. The distinction matters: that cleanup figure describes what GitGuardian could observe publicly, not a verified inventory of every affected repository.&lt;/p&gt;
&lt;p&gt;I would treat this as a CI architecture problem, not a sequence of emergency edits. Recovery depends on being able to connect three things: the dependency that changed, the workflows that consume it, and the credentials those workflows could reach. If any of those relationships are opaque, a team cannot confidently bound the incident. It may either leave access open or revoke so broadly that routine shipping stops.&lt;/p&gt;
&lt;h2 id=&quot;build-the-map-before-an-incident&quot;&gt;Build the map before an incident&lt;/h2&gt;
&lt;p&gt;A reusable pipeline gives me a useful place to standardize workflow behavior, but reuse alone does not create visibility. I need an inventory that can answer concrete questions: which repositories call a shared workflow, which repositories reference a particular action, and where are credentials exposed to those jobs?&lt;/p&gt;
&lt;p&gt;I would keep that inventory alongside the pipeline definitions and update it as part of the change process. For each repository, I want the workflow entry points, reusable workflows and third-party actions it invokes, the permissions granted to &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, and the names and owners of credentials available to publishing or deployment jobs. I do not need to copy secret values into the inventory. The point is to know which credential to revoke, who can rotate it, and what depends on it.&lt;/p&gt;
&lt;p&gt;This is also why I prefer explicit hand-offs over passing every secret through a shared workflow. GitHub documents that &lt;code&gt;secrets: inherit&lt;/code&gt; passes all secrets available to the calling workflow to the called workflow in its &lt;a href=&quot;https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsecretsinherit&quot;&gt;reference for workflow syntax&lt;/a&gt;. That convenience widens the set of credentials a called workflow can access. I would pass only the specific secret a job needs, or avoid a long-lived secret where an identity-based mechanism fits the deployment target.&lt;/p&gt;
&lt;p&gt;A workflow can make the permission boundary visible instead of relying on a broad default:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  push&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    branches&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;main&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  test&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Check out source&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@&amp;#x3C;full-commit-SHA&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Run tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;./ci/test.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SHA is a placeholder for a reviewed, full commit identifier, not a value to paste literally. I would pin third-party actions to full commit SHAs and grant &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; only the permissions the workflow needs. GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token&quot;&gt;guidance on token permissions&lt;/a&gt; describes setting permissions at workflow or job level and recommends least privilege. These controls do not prove that an action is safe. They limit how much it can do if it is not.&lt;/p&gt;
&lt;h2 id=&quot;contain-access-without-disabling-every-release&quot;&gt;Contain access without disabling every release&lt;/h2&gt;
&lt;p&gt;When an action is suspected, I would first identify the repositories and workflow runs that consumed it, then determine which jobs could reach credentials. The answer is not always “every secret in the repository.” It depends on where a secret is configured, which workflow can access it, and whether a job actually ran with the affected action. GitHub’s &lt;a href=&quot;https://docs.github.com/en/enterprise-server@3.0/actions/security-guides/security-hardening-for-github-actions&quot;&gt;security hardening guidance for Actions&lt;/a&gt; warns that a compromised action can access secrets configured for its repository and may be able to use &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; to write to that repository. That is why I would scope the investigation to actual workflow reachability rather than infer safety from a successful reference update.&lt;/p&gt;
&lt;p&gt;I would separate credentials by purpose and blast radius: read-only access for tests, publishing credentials only in the publishing job, and deployment credentials available only to the relevant environment. Then a response can pause or disable the affected path while leaving unrelated checks and releases alone. That is a more deliberate choice than turning off Actions organization-wide and discovering later that the team has no tested way to ship a security fix.&lt;/p&gt;
&lt;p&gt;Containment still means revoking credentials that could have been exposed, not merely rotating them eventually. The recovery map should name the authority that can revoke each credential and the replacement process. For a credential shared across many repositories, the safe sequence is to establish the replacement path, revoke or replace the old credential, and verify that each consumer uses the new access before declaring recovery complete. GitHub also recommends auditing and rotating registered secrets in its &lt;a href=&quot;https://docs.github.com/en/enterprise-server@3.0/actions/security-guides/security-hardening-for-github-actions&quot;&gt;security hardening guidance&lt;/a&gt;. The architectural point is that rotation must be executable under incident conditions, not just written in a policy.&lt;/p&gt;
&lt;h2 id=&quot;keep-one-trusted-path-to-ship&quot;&gt;Keep one trusted path to ship&lt;/h2&gt;
&lt;p&gt;The hardest recovery trade-off is between cutting off a potentially compromised path and retaining enough CI capability to deliver a fix. I would design a constrained release path that does not depend on the suspect action or on a shared credential whose exposure is still uncertain. That path can be narrower and more manual than normal delivery; it should still be documented and rehearsed before an incident.&lt;/p&gt;
&lt;p&gt;For a shared pipeline, I would make the consumer relationship visible and use a controlled update process. A single change to a reusable workflow can affect many repositories, so the pipeline owner needs a way to identify callers, roll out a corrected version, and see which callers have not adopted it. The same visibility makes an emergency response tractable: stop the compromised version from being used, direct consumers to a reviewed version, and track completion rather than assuming a central edit fixed every caller.&lt;/p&gt;
&lt;p&gt;I would define recovery completion as more than “the workflow is green.” I want evidence that affected references have been removed or replaced, reachable credentials have been revoked or rotated, permissions have been reduced where they were broader than necessary, and the intended release path works without the compromised dependency. If the inventory cannot answer those questions, improving it is part of the remediation.&lt;/p&gt;
&lt;p&gt;That is the shift I want in CI design: from asking whether a reference was updated to asking whether I can identify, contain, and recover from the dependency’s full blast radius while keeping a trusted way to ship.&lt;/p&gt;</content:encoded><category>ci-cd</category><category>security</category><category>github-actions</category><category>architecture</category></item><item><title>Worktrees move agent risk to integration contracts</title><link>https://vahidinasab.com/articles/worktrees-move-agent-risk-to-integration-contracts</link><guid isPermaLink="true">https://vahidinasab.com/articles/worktrees-move-agent-risk-to-integration-contracts</guid><description>I explain why isolated agent worktrees reduce edit collisions but leave task ownership, interface compatibility and integration decisions unresolved.</description><pubDate>Tue, 13 Oct 2026 11:20:49 GMT</pubDate><content:encoded>&lt;h2 id=&quot;isolation-solves-only-one-kind-of-conflict&quot;&gt;Isolation solves only one kind of conflict&lt;/h2&gt;
&lt;p&gt;Coverage of Meta’s reported Muse Code launch describes persistent subagents working in isolated Git worktrees, a useful prompt to think about parallel coding as a concurrency problem rather than an invitation to start more agents (&lt;a href=&quot;https://devops.com/meta-launches-ai-coding-agent-to-challenge-openai-and-anthropic/&quot;&gt;DevOps.com’s coverage of Muse Code&lt;/a&gt;). I use coding agents, but I am not claiming expertise in building agent systems. The architectural question is familiar, though: what changes when several workers can make progress at once?&lt;/p&gt;
&lt;p&gt;A Git worktree gives each worker a separate working directory and checked-out branch while keeping those worktrees associated with the same repository (&lt;a href=&quot;https://git-scm.com/docs/git-worktree&quot;&gt;Git’s worktree documentation&lt;/a&gt;). That separation can prevent one worker’s uncommitted edits from directly overwriting another’s. It does not make their changes compatible. Two workers can independently change the same public method, assume different meanings for a field, or make incompatible choices about where validation belongs. Each branch may be internally consistent and still fail when brought together.&lt;/p&gt;
&lt;p&gt;I would describe worktrees as an isolation boundary, not a coordination protocol. They reduce direct edit collisions; they do not decide who owns a concern, establish an interface, or choose between conflicting designs. Those responsibilities do not disappear when the workers are agents. They move toward task definition and integration.&lt;/p&gt;
&lt;h2 id=&quot;agree-on-ownership-before-parallel-work&quot;&gt;Agree on ownership before parallel work&lt;/h2&gt;
&lt;p&gt;Before I split a task, I want a clear boundary for each worker’s change. “Work on the API” is not a boundary. “Add the request model and validation; do not change the endpoint signature or persistence layer” is closer. The point is not to dictate every line. It is to identify which files or responsibilities have one owner, and which seams workers must treat as shared contracts.&lt;/p&gt;
&lt;p&gt;I would write the assignment in terms of an outcome, allowed scope, and explicit exclusions. For example:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Task: Add request validation for the create-order endpoint.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Own: request model and validator.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Do not change: endpoint signature, persistence model, or response format.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Assume: the endpoint calls the validator before creating an order.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Return: commit, tests run, and any contract assumption that needs review.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a sketch of a task contract, not a claim about a particular tool’s configuration. It makes an assumption visible before an implementation hardens it into code. If another worker owns the endpoint, both assignments can refer to the same agreed behavior without editing the same surface area.&lt;/p&gt;
&lt;p&gt;I also want the interface written down before implementation proceeds in parallel. That might be a method signature, a request and response shape, an event schema, or simply a short statement of behavior. If a worker cannot do useful work without inventing that contract, the task is not ready to fan out. I would either define the contract first or keep the work with one owner until the uncertainty is resolved.&lt;/p&gt;
&lt;p&gt;This is where more agents can make a task worse. If every worker independently fills in an ambiguous requirement, parallelism produces multiple plausible designs and pushes the cost of choosing among them to the end. The faster the branches arrive, the less time there is to discover that their assumptions diverged.&lt;/p&gt;
&lt;h2 id=&quot;treat-shared-interfaces-as-owned-work&quot;&gt;Treat shared interfaces as owned work&lt;/h2&gt;
&lt;p&gt;A parallel plan should identify not only separate tasks, but also the places where their outputs meet. I would make one person responsible for the shared contract: its shape, semantics, and compatibility expectations. That person might implement the interface, or might only approve it before others build against it. What matters is that the decision has an owner.&lt;/p&gt;
&lt;p&gt;For a service change, the contract could cover a route, message, or database boundary. For a library change, it could be a method signature and the behavior callers can rely on. In either case, naming the files alone is not enough. Two changes can touch different files and still conflict semantically. A new status value in one branch, for instance, is not useful if another branch assumes the old set is exhaustive. That is a hypothetical example of the class of mismatch, not an account of an event I have witnessed.&lt;/p&gt;
&lt;p&gt;I would ask each worker to report assumptions as part of its result, not just code and test status. Tests can show that a branch passes in isolation; they cannot prove that another branch made the same assumptions. A concise handoff should say what changed, what it depends on, what it deliberately did not change, and which decisions remain open.&lt;/p&gt;
&lt;p&gt;This contract-first approach has a cost: it asks for design work before all the code exists. I consider that a good trade when parallel changes cross a shared boundary. If the contract is still genuinely uncertain, the right next step may be a small investigation or a single implementation that establishes the seam—not extra parallel implementations of competing guesses.&lt;/p&gt;
&lt;h2 id=&quot;keep-integration-as-a-human-decision&quot;&gt;Keep integration as a human decision&lt;/h2&gt;
&lt;p&gt;I would not treat a successful merge as proof that integration is complete. Git can identify textual conflicts and combine changes that do not overlap; neither outcome establishes that the combined behavior is right. The integration owner needs to review the joined change against the contract, run checks across the combined result, and decide what to do when two workers made incompatible choices.&lt;/p&gt;
&lt;p&gt;That decision may be to keep one implementation, revise both, or change the contract. It is not a mechanical cleanup step. The person integrating needs enough context to understand why each change exists and what each worker assumed. If the handoffs omit that context, the apparent time saved through parallel work can reappear as investigation at the merge boundary.&lt;/p&gt;
&lt;p&gt;I would also keep the final integration path narrow. A designated owner should bring changes together in a deliberate order, check the full result, and make the final call on disagreements. This does not mean every line needs a human to type it. It means someone remains accountable for deciding whether the system, rather than each isolated branch, satisfies the requirement.&lt;/p&gt;
&lt;p&gt;Persistent workers make that discipline more important, not less. A worker that continues operating after the initial assignment still needs a stable scope and a way to surface changed assumptions. Persistence is useful only if the work remains attached to a contract someone can review. Otherwise, it can extend the life of an incorrect assumption alongside the life of the task.&lt;/p&gt;
&lt;p&gt;I would start parallel agent work only when I can name each owner, state the shared interface, and identify who will integrate the result. Worktrees help keep edits apart. The contract and the integration decision are what make the changes fit.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>architecture</category><category>git</category></item><item><title>Trial .NET 11 without changing the production build</title><link>https://vahidinasab.com/articles/trial-dotnet-11-without-changing-the-production-build</link><guid isPermaLink="true">https://vahidinasab.com/articles/trial-dotnet-11-without-changing-the-production-build</guid><description>A reusable pipeline can test services with the .NET 11 preview SDK and runtime while keeping its established production toolchain unchanged.</description><pubDate>Tue, 13 Oct 2026 11:20:46 GMT</pubDate><content:encoded>&lt;h2 id=&quot;make-preview-testing-an-opt-in-lane&quot;&gt;Make preview testing an opt-in lane&lt;/h2&gt;
&lt;p&gt;I would add .NET 11 preview testing to a shared pipeline as a separate, opt-in job—not as a version change to the job that builds and releases production. That separation is the point: teams get an early compatibility signal without changing what an ordinary build means across every repository using the pipeline.&lt;/p&gt;
&lt;p&gt;The timing makes the distinction useful. &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/overview&quot;&gt;Microsoft’s .NET 11 overview&lt;/a&gt; says general availability is expected in November 2026. The .NET site says &lt;a href=&quot;https://dotnet.microsoft.com/en-us/apps/ai&quot;&gt; .NET Conf 2026 begins November 10&lt;/a&gt;. Until the release is generally available, a preview result should help a team investigate; it should not quietly become a new production requirement.&lt;/p&gt;
&lt;p&gt;I would expose the lane through a boolean input on the reusable workflow, defaulting to off. A repository can opt in when it is ready to test, while repositories that do nothing keep using the established path. In GitHub Actions, the shape might look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  workflow_call&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    inputs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      dotnet11-preview&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Run the .NET 11 preview compatibility lane&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        type&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;boolean&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        default&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  production-build&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;      # Keep the existing SDK setup, build, test, and release steps here.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  dotnet11-preview&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;${{ inputs.dotnet11-preview }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/setup-dotnet@v6&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;          dotnet-version&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;11.0.x&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;          dotnet-quality&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;preview&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet --info&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet test src/Service.Tests/Service.Tests.csproj --framework net11.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a pattern, not a drop-in workflow: the project must support the target framework being tested, and the existing production steps belong in the production job unchanged. The &lt;a href=&quot;https://github.com/marketplace/actions/setup-net-core-sdk&quot;&gt;setup-dotnet action documentation&lt;/a&gt; describes its SDK and preview-quality inputs.&lt;/p&gt;
&lt;p&gt;The preview lane should report separately from the production build. It can fail and give the team something to investigate without blocking a release that still uses the normal toolchain. If the organization later decides that preview compatibility should be required for a particular repository, that should be an explicit repository-level choice—not a side effect of updating a shared workflow.&lt;/p&gt;
&lt;h2 id=&quot;keep-sdk-selection-explicit&quot;&gt;Keep SDK selection explicit&lt;/h2&gt;
&lt;p&gt;Installing another SDK does not automatically mean every &lt;code&gt;dotnet&lt;/code&gt; command will use it. A repository may have a &lt;code&gt;global.json&lt;/code&gt; that selects the SDK version. The &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/tools/global-json&quot;&gt;global.json overview&lt;/a&gt; explains how that file affects SDK selection. I would check this before treating a green preview job as evidence that the service was tested with the preview SDK.&lt;/p&gt;
&lt;p&gt;There are a few ways to keep the two lanes independent. A repository can use a separate preview-specific SDK configuration and run the preview commands from the directory where that configuration applies. Or the shared pipeline can provide a controlled preview working directory and configuration. The detail depends on how the repository is organized; the invariant does not: installing the preview SDK for one job must not change the SDK selected by the production job.&lt;/p&gt;
&lt;p&gt;I would also make the preview SDK selection visible in the job log. Printing &lt;code&gt;dotnet --info&lt;/code&gt; makes it easier to distinguish “the preview job ran” from “the preview SDK actually ran.” If SDK resolution still points to the pinned production SDK, the lane is not testing what its name claims.&lt;/p&gt;
&lt;h2 id=&quot;test-a-clear-compatibility-surface&quot;&gt;Test a clear compatibility surface&lt;/h2&gt;
&lt;p&gt;A useful first lane should answer a narrow question: does this service restore, build, and pass its tests when targeted at .NET 11 using the preview SDK and runtime? It should not try to answer every question about adoption, deployment, or production behavior at once.&lt;/p&gt;
&lt;p&gt;For a multi-targeted test project, a command such as &lt;code&gt;dotnet test --framework net11.0&lt;/code&gt; selects the target framework to exercise. I would keep the target-framework change opt-in as well, so adding preview coverage does not silently alter the framework used by the normal build. Where a repository cannot target the new framework yet, the lane can still test what it can—such as SDK compatibility—provided the job reports that narrower scope honestly rather than implying that it tested the application on the new runtime.&lt;/p&gt;
&lt;p&gt;I would not publish artifacts, update deployment manifests, or promote a preview-built package from this job. Those actions blur a compatibility check into a release path. The preview lane should produce diagnostic output and a test result; the established pipeline remains responsible for the artifacts that production consumes.&lt;/p&gt;
&lt;p&gt;A separate job also keeps the failure signal easier to interpret. If the preview lane fails while the regular build passes, the team can inspect whether the cause is a framework change, a dependency constraint, or a test assumption. That is a useful result even when the team chooses not to make a change immediately. The lane is there to discover friction early, not to manufacture pressure to adopt.&lt;/p&gt;
&lt;h2 id=&quot;reuse-the-mechanism-not-the-decision&quot;&gt;Reuse the mechanism, not the decision&lt;/h2&gt;
&lt;p&gt;A shared pipeline should standardize how a preview test runs: SDK setup, logging, test commands, and where results appear. It should not decide that every repository must test every preview, or that a preview failure blocks every release. Different services have different reasons to opt in, and that decision belongs with the team responsible for each service.&lt;/p&gt;
&lt;p&gt;This is the trade-off I would choose: centralize the mechanics, leave participation explicit, and keep production on its established toolchain until someone deliberately changes it. A reusable pipeline is valuable because it lets many repositories share a safe capability. It becomes a risk when a small shared edit silently changes the build contract for all of them.&lt;/p&gt;</content:encoded><category>dotnet</category><category>ci-cd</category><category>architecture</category></item><item><title>Give Claude Code workflows explicit stop conditions</title><link>https://vahidinasab.com/articles/give-claude-code-workflows-explicit-stop-conditions</link><guid isPermaLink="true">https://vahidinasab.com/articles/give-claude-code-workflows-explicit-stop-conditions</guid><description>A repo-scale coding agent needs defined checks, evidence to return, and clear points where human judgment takes over.</description><pubDate>Tue, 13 Oct 2026 11:20:41 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-longer-prompt-is-still-a-delegation-problem&quot;&gt;A longer prompt is still a delegation problem&lt;/h2&gt;
&lt;p&gt;Anthropic describes Claude Code dynamic workflows as a way for Claude to plan larger tasks, run parallel subagents, and verify outputs before reporting back. Its &lt;a href=&quot;https://www.anthropic.com/news/claude-opus-4-8&quot;&gt;announcement of Claude Opus 4.8 and dynamic workflows&lt;/a&gt; names migrations across large codebases as one example. That changes the scale of the work an agent can attempt. It does not change who is accountable for accepting the result.&lt;/p&gt;
&lt;p&gt;I would treat repo-scale agent work as a change in delegation, not as a reason to write a larger prompt and hope for the best. A prompt says what outcome I want. A workflow also says what the agent may change, what must be true before it reports success, what evidence it must return, and when it must stop for a person.&lt;/p&gt;
&lt;p&gt;That distinction matters because “the tests pass” is not a complete definition of done. A migration can compile while missing a call site, changing behavior that a test does not cover, or leaving obsolete paths behind. A refactor can meet its local checks while crossing a boundary I did not intend to move. The agent should not be allowed to turn uncertainty into an unqualified success message.&lt;/p&gt;
&lt;p&gt;I would make autonomy a workflow decision. The model can do more of the work when the task is bounded, the checks are meaningful, and the result is easy to inspect. It should do less when the work changes external behavior, relies on a disputed product decision, or cannot be verified from the repository.&lt;/p&gt;
&lt;h2 id=&quot;define-the-finish-line-before-the-agent-starts&quot;&gt;Define the finish line before the agent starts&lt;/h2&gt;
&lt;p&gt;Before delegating a migration or refactor, I would write down the acceptance conditions in terms someone else could check. “Modernize the clients” is an intention, not a finish line. Which clients are in scope? What should replace the old interface? What must not change? Which tests and static checks are required?&lt;/p&gt;
&lt;p&gt;A useful task description separates the desired change from the boundaries around it. The following is a policy sketch, not Claude Code configuration syntax:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Replace the deprecated payment client interface&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;scope&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  include&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;production code that calls the deprecated interface&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;tests for those call sites&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  exclude&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;public API changes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;unrelated dependency upgrades&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;must_pass&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;repository build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;relevant test suites&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;formatting and static analysis&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;must_return&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;files changed and why&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;commands run and their outcomes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;remaining deprecated call sites, or evidence none remain&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;unresolved assumptions and risks&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;stop_if&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;a public contract must change&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;a required check cannot run&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;behavior is ambiguous in the existing code or tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;the change requires expanding the stated scope&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The point is not the YAML. The point is that scope, checks, evidence, and stop conditions are separate parts of the delegation. If I leave one implicit, the agent may make a reasonable choice that still isn’t the choice I wanted.&lt;/p&gt;
&lt;p&gt;I would also name the checks concretely. “Run tests” leaves room to run a convenient subset and call the task complete. “Run the repository build and the payment test suite; report any suite that could not run” makes the gap visible. If a check is slow or unavailable, the workflow should say whether the agent may continue with implementation and then stop before claiming completion.&lt;/p&gt;
&lt;h2 id=&quot;require-evidence-not-confidence&quot;&gt;Require evidence, not confidence&lt;/h2&gt;
&lt;p&gt;The final response should let me review what happened without trusting the agent’s summary alone. I would ask for the changed areas, the exact checks run, their outcomes, and any checks skipped. For a migration, I would also ask for a search result or equivalent evidence that the old interface is no longer used in the agreed scope.&lt;/p&gt;
&lt;p&gt;That evidence is useful even when every check passes. It gives a reviewer a trail from the stated acceptance conditions to the proposed change. It also exposes mismatches: perhaps the agent edited generated files, skipped an integration suite, or interpreted “in scope” more broadly than I did. Those are review questions, not details to bury under “done.”&lt;/p&gt;
&lt;p&gt;A passing test suite is evidence about the tests that ran. It is not proof that every requirement was met. I would phrase the agent’s completion condition accordingly: report success only when each required check has a recorded outcome and each requested piece of evidence is present. If a check fails, the agent can investigate within the agreed scope, but it must report the failure rather than silently weaken the acceptance bar.&lt;/p&gt;
&lt;p&gt;This is also where parallel work needs a clear coordinator. Independent agents can inspect separate areas, but their results still need to be reconciled against one scope and one set of acceptance conditions. If two agents make incompatible assumptions, the coordinator should surface the conflict. It should not resolve a product or architecture decision merely to keep the task moving.&lt;/p&gt;
&lt;h2 id=&quot;make-the-stop-points-explicit&quot;&gt;Make the stop points explicit&lt;/h2&gt;
&lt;p&gt;I would make the agent stop and ask for a human decision when it reaches a boundary that cannot be settled by repository evidence. Examples include a change to a public contract, a choice between competing behavior interpretations, a security-sensitive change outside the task’s stated design, or a need to expand scope. These are not failures of autonomy. They are the workflow working as intended.&lt;/p&gt;
&lt;p&gt;Some stops can be automatic: a required check cannot run, the repository state is unexpected, or the planned change would touch an excluded area. Others require judgment. If a migration reveals two plausible replacement behaviors, the agent can explain the options and the consequences it found, but it should not choose one just because one is easier to implement.&lt;/p&gt;
&lt;p&gt;I would state the stop behavior directly, rather than rely on a general instruction to “ask if unsure”:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;If a stop condition is reached:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- do not make the decision by inference&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- do not continue with a broader substitute task&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- report the condition and the evidence that exposed it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- give the smallest set of options that need a human decision&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- wait for direction before changing the affected behavior&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps a pause from becoming a vague handoff. The person reviewing can see what triggered it, what has already changed, and which decision would let work resume. The agent may be able to continue on independent, in-scope work, but only if doing so cannot hide or compound the unresolved decision.&lt;/p&gt;
&lt;h2 id=&quot;scale-autonomy-with-verifiability&quot;&gt;Scale autonomy with verifiability&lt;/h2&gt;
&lt;p&gt;I would not use the same delegation for every repository task. A mechanical, well-bounded migration with reliable tests can support more autonomous execution than a change whose acceptance depends on product intent or operational risk. That is not a ranking of model capability. It is a judgment about how well the workflow can detect a wrong turn before it becomes an accepted change.&lt;/p&gt;
&lt;p&gt;For repo-scale work, I want the agent to return a reviewable proposal, not to define success for itself. The workflow should encode the scope, required checks, evidence, and human decision points before execution begins. Then more capable agents can take on longer tasks without making review depend on confidence, or on a reviewer reconstructing what was never specified.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>claude-code</category><category>software-architecture</category></item><item><title>Treat custom runner images as build dependencies</title><link>https://vahidinasab.com/articles/treat-custom-runner-images-as-build-dependencies</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-custom-runner-images-as-build-dependencies</guid><description>I argue for versioning custom GitHub Actions runner images as shared build dependencies, with clear ownership for patching, validation and rollout.</description><pubDate>Thu, 08 Oct 2026 19:42:12 GMT</pubDate><content:encoded>&lt;h2 id=&quot;an-image-is-part-of-the-build&quot;&gt;An image is part of the build&lt;/h2&gt;
&lt;p&gt;A runner image decides what software is present before a workflow starts. That makes it more than a convenience layer: it is an input to the build, alongside the workflow, actions and project dependencies. I would version and review it accordingly.&lt;/p&gt;
&lt;p&gt;GitHub announced custom images for GitHub-hosted runners in public preview in October 2025, describing image lifecycle controls and the ability to use a custom image in later workflows (&lt;a href=&quot;https://github.blog/changelog/2025-10-28-custom-images-for-github-hosted-runners-are-now-available-in-public-preview/&quot;&gt;GitHub’s custom-images announcement&lt;/a&gt;). The capability reached general availability in May 2026, making the image lifecycle a production design decision rather than a preview experiment. GitHub has since added support for building custom images from other custom images, a pattern that can separate a shared foundation from team-specific additions (&lt;a href=&quot;https://github.blog/changelog/2026-06-18-actions-build-custom-images-from-custom-images/&quot;&gt;GitHub’s custom-image layering update&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The architectural question is not simply which tools belong in an image. It is who owns that image, how a change is tested, and how the repositories using it move to a new version. If many repositories share one runner foundation, an image change is a shared dependency update. Treating it casually makes the blast radius shared too.&lt;/p&gt;
&lt;h2 id=&quot;put-stable-tools-in-the-image&quot;&gt;Put stable tools in the image&lt;/h2&gt;
&lt;p&gt;I would put stable, broadly useful tools and configuration in a custom image: the runtimes and system packages common to a class of builds, required certificates, or organization-wide configuration. The image should provide a dependable starting point, not try to encode every project’s exact build environment.&lt;/p&gt;
&lt;p&gt;Fast-changing items belong elsewhere. Pin workflow actions separately, and pin project dependencies through the project’s own dependency-management and lock files. That division makes it easier to identify what changed when a build breaks. If an image update also silently changes a project library or an action version, the runner becomes a catch-all bundle with poorly defined ownership.&lt;/p&gt;
&lt;p&gt;For example, a workflow can select a runner backed by the organization’s image while keeping its action references visible in the workflow:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  push&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;permissions&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  contents&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  build&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;org-linux-build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;actions/checkout@&amp;#x3C;reviewed-commit-sha&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;dotnet build --configuration Release&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runner name is illustrative; configure it to match the larger runner or runner group using the custom image. Replace the action placeholder with the reviewed commit SHA before using this example. The important boundary is that the image supplies the stable host environment, while the workflow states which action it invokes and the project controls its own dependencies.&lt;/p&gt;
&lt;p&gt;This separation is not a claim that every tool should be installed at workflow time. Repeated setup can add delay and variation. It is a choice to keep the image’s responsibilities narrow enough that an image update can be reviewed as an image update, rather than as an opaque change to the whole build.&lt;/p&gt;
&lt;h2 id=&quot;version-test-and-roll-out-deliberately&quot;&gt;Version, test and roll out deliberately&lt;/h2&gt;
&lt;p&gt;An image needs an identity that a workflow can refer to and a history that an owner can review. I would build it from a declared definition, publish a new version when its contents change, and avoid silently replacing the contents behind a name that workflows already trust. The exact mechanism depends on the runner setup; the principle is to make the change and its adoption visible.&lt;/p&gt;
&lt;p&gt;I would test a candidate image with representative workflows before making it the default for every repository. That means checking not only whether the image builds, but whether the kinds of jobs that depend on it still work: compilation, tests, packaging and any required system-level setup. Where repositories have different needs, I would validate against more than one representative rather than treating one successful build as universal proof.&lt;/p&gt;
&lt;p&gt;Rollout should also have an explicit path back. A repository that fails after an image update needs to be able to identify which image version it used and, where practical, return to a known-good version while the problem is investigated. That does not mean keeping every old image indefinitely. It means deciding how long versions remain available and who can authorize a rollback, instead of discovering those questions during an incident.&lt;/p&gt;
&lt;p&gt;The operational cost is real. Someone must update base software, rebuild and validate images, communicate changes, and retire versions that should no longer be used. A custom image can improve consistency and reduce repeated setup, but it does not remove maintenance. It moves some maintenance from individual workflows into a shared artifact with a larger potential blast radius.&lt;/p&gt;
&lt;h2 id=&quot;shared-foundations-need-shared-ownership&quot;&gt;Shared foundations need shared ownership&lt;/h2&gt;
&lt;p&gt;The case for a custom image is strongest when many repositories need the same runner foundation. Reusing one image can avoid each workflow independently assembling a slightly different environment. But every shared dependency needs an owner. Without one, repositories can keep depending on an image that nobody is patching, or a well-intended update can break consumers that had no part in the change.&lt;/p&gt;
&lt;p&gt;I would assign ownership for the image definition and its release process, then make consumers’ migration visible. The image owner is responsible for producing and validating versions; repository owners remain responsible for their workflows and project dependencies. That division avoids the assumption that whoever owns a shared runner automatically owns every build running on it.&lt;/p&gt;
&lt;p&gt;Layering can help where teams share a common base but need different tools. GitHub’s update on building custom images from other custom images describes this kind of shared-foundation pattern (&lt;a href=&quot;https://github.blog/changelog/2026-06-18-actions-build-custom-images-from-custom-images/&quot;&gt;GitHub’s image-layering announcement&lt;/a&gt;). I would use it only when the ownership boundary is clear: the base layer has a maintainer and release policy, and each derived image has an owner for its additional contents. Otherwise, layers can obscure where a tool came from and which team should update it.&lt;/p&gt;
&lt;p&gt;My default is therefore straightforward: use a custom runner image when there is a stable environment worth sharing, keep volatile dependencies pinned in their own places, and treat every image revision as a build-input change that needs review and a rollout plan. The speed and consistency are useful; the patching responsibility is the price of getting them.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>software-supply-chain</category></item><item><title>Test Kubernetes security defaults at the workload level</title><link>https://vahidinasab.com/articles/test-kubernetes-security-defaults-at-the-workload-level</link><guid isPermaLink="true">https://vahidinasab.com/articles/test-kubernetes-security-defaults-at-the-workload-level</guid><description>I explain how to test Kubernetes v1.36 security behavior against real workloads and make the intended posture explicit before upgrading.</description><pubDate>Thu, 08 Oct 2026 19:42:08 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-safer-mechanism-can-still-change-workload-behavior&quot;&gt;A safer mechanism can still change workload behavior&lt;/h2&gt;
&lt;p&gt;Kubernetes v1.36 makes faster SELinux volume labeling generally available, replacing recursive relabeling with a mount option that applies a label to the volume at mount time. The &lt;a href=&quot;https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/&quot;&gt;Kubernetes v1.36 release notes&lt;/a&gt; describe the change and note a potential risk when a volume is shared between privileged and unprivileged Pods.&lt;/p&gt;
&lt;p&gt;That is a security improvement, but I would not translate “more secure by default” into “safe to upgrade without workload tests.” The mechanism that applies a label can affect how a workload accesses a volume, especially when multiple Pods or different privilege levels are involved. An API compatibility check will not tell me whether a database can still start, whether a sidecar can read its files, or whether a shared volume behaves as intended.&lt;/p&gt;
&lt;p&gt;I would treat the release as a reason to test security behavior, not only whether manifests still apply. The point is not to preserve every old assumption. It is to discover which assumptions exist, decide which are legitimate, and make the intended posture visible in configuration and policy.&lt;/p&gt;
&lt;h2 id=&quot;test-the-relationships-not-just-the-manifest&quot;&gt;Test the relationships, not just the manifest&lt;/h2&gt;
&lt;p&gt;A Pod becoming Ready is useful evidence, but it does not cover every security boundary. I would start by identifying workloads that use persistent volumes and asking how those volumes are mounted and shared. Then I would test the combinations that matter to the application: the workload’s configured identity, the volume’s access mode, any sidecars or init containers that touch the files, and whether another Pod uses the same storage.&lt;/p&gt;
&lt;p&gt;For each representative workload, I want tests that answer practical questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can the main container read and write only the paths it is meant to use?&lt;/li&gt;
&lt;li&gt;Can sidecars and init containers access the files they need without gaining broader access?&lt;/li&gt;
&lt;li&gt;Does a restart, reschedule, or remount preserve the expected behavior?&lt;/li&gt;
&lt;li&gt;Does sharing a volume across Pods with different privilege levels still work, or does the new behavior correctly expose an unsafe design?&lt;/li&gt;
&lt;li&gt;Do admission policies, security policies, and monitoring still report the posture I intend?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I would test both the expected path and a deliberately disallowed one. A successful write by the application is not enough if an unrelated container can also write to that data. Conversely, a denied access is not automatically a regression: it may be the security boundary doing its job. The test needs to say which result is intended.&lt;/p&gt;
&lt;p&gt;A small smoke-test sequence can make the operational intent explicit. The manifests below stand for the workload and its test fixture; I would run equivalent checks against the actual release candidate and storage driver, rather than assume a generic fixture represents every cluster.&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; apply&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -f&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; workload.yaml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; wait&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --for=condition=Ready&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; pod&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -l&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; app=workload&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --timeout=5m&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; exec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; deploy/workload&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; sh&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;test -r /data/expected-file&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; exec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; deploy/workload&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; sh&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &apos;test -w /data/application-owned&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;kubectl&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; logs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; deploy/workload&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those checks are only a starting point. I would add a negative test for paths the process should not access, and an integration test for any volume sharing the application relies on. I would also run the same tests after a restart and with the storage configuration used in the target cluster. If a test only passes when the workload runs with broader permissions than its intended posture allows, I want that failure before promotion, not as an incident after the upgrade.&lt;/p&gt;
&lt;h2 id=&quot;make-the-intended-posture-explicit&quot;&gt;Make the intended posture explicit&lt;/h2&gt;
&lt;p&gt;Defaults are useful because they reduce the amount of configuration teams must repeat. They are a poor long-term record of a workload’s security requirements. If a team relies on a particular identity, label, access boundary, or sharing pattern, I want that requirement represented deliberately and reviewed with the workload.&lt;/p&gt;
&lt;p&gt;That does not mean copying every default into every manifest. Explicitness should clarify an important decision, not create a forest of redundant settings. I would make a setting explicit when its absence makes the intended behavior ambiguous, when a policy depends on it, or when an upgrade could change the assumption. Then I would validate it in the same place as the workload: its deployment tests, admission checks, or policy tests.&lt;/p&gt;
&lt;p&gt;For the SELinux volume behavior in v1.36, I would record which workloads depend on shared-volume access and which are expected to keep their data isolated. I would check whether the relevant Pod and storage configurations express that intent, and verify the result on the target node and storage path. If the design depends on a specific compatibility behavior, I would document why and test it as an exception rather than leaving it implicit.&lt;/p&gt;
&lt;p&gt;This is also where policy needs workload-level tests. A cluster-wide rule can be syntactically correct and still reject an important workload, or allow a workload that violates the intended boundary. I would test representative Pods through the same admission and deployment path used in production. The output should tell the team whether a failure comes from the Kubernetes change, the storage integration, or its own policy—not just report that an upgrade test failed.&lt;/p&gt;
&lt;h2 id=&quot;apply-the-same-discipline-to-self-hosted-clusters&quot;&gt;Apply the same discipline to self-hosted clusters&lt;/h2&gt;
&lt;p&gt;Self-hosted does not mean exempt from upstream behavior. The &lt;a href=&quot;https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release/&quot;&gt;Kubernetes release notes&lt;/a&gt; describe a change in Kubernetes’ volume-labeling behavior; whether the resulting workload behavior is acceptable still depends on the node’s security enforcement, storage driver, and workload design. I would not assume a cloud-managed control plane is the deciding factor.&lt;/p&gt;
&lt;p&gt;For a self-hosted upgrade, I would run the tests on representative nodes with the actual kernel, container runtime, SELinux mode, and storage setup. I would include the node replacement or maintenance path used by the cluster, because a test on one prepared node cannot establish that every worker behaves the same way. If the cluster has more than one storage class or node configuration, I would include the combinations that are materially different.&lt;/p&gt;
&lt;p&gt;My upgrade gate would therefore include more than API discovery and deployment health. It would require workload checks for expected and forbidden access, policy checks for the intended security posture, and a review of any exception that preserves older behavior. I would roll forward only when those results are understood. If the change reveals that a workload relies on an unsafe shortcut, I would fix that dependency rather than treat the old behavior as the compatibility contract.&lt;/p&gt;
&lt;p&gt;A tighter default is valuable when it makes the safer path easier. It still needs to be tested where it meets a real workload. I want the upgrade to expose assumptions while there is time to change them, and I want the final security posture to come from an explicit decision—not from whichever implicit default happened to be in force.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>security</category><category>platform-engineering</category></item><item><title>Treat observability integrations as operational dependencies</title><link>https://vahidinasab.com/articles/treat-observability-integrations-as-operational-dependencies</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-observability-integrations-as-operational-dependencies</guid><description>A support-window plan for cluster integrations helps teams schedule upgrades deliberately and preserve options when vendor compatibility timelines differ.</description><pubDate>Thu, 08 Oct 2026 19:42:04 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-support-date-can-become-an-upgrade-deadline&quot;&gt;A support date can become an upgrade deadline&lt;/h2&gt;
&lt;p&gt;An observability integration is part of the platform’s operational dependency graph, not just a way to collect telemetry. It has versions, compatibility limits, an owner, and a support window. I would track those alongside Kubernetes versions and application instrumentation, because an integration’s support policy can turn into a constraint on when a cluster must change.&lt;/p&gt;
&lt;p&gt;New Relic’s &lt;a href=&quot;https://docs.newrelic.com/eol/2026/10/k8s-supported-version-changes&quot;&gt;Kubernetes supported-version notice&lt;/a&gt; says that, effective October 19, 2026, its Kubernetes integration will support only the three most recent Kubernetes minor versions. The notice says the integration may continue to work on older versions, but New Relic cannot guarantee its quality there and will not accept support requests for versions that have reached end of life. That is a useful distinction: software continuing to run is not the same as having a supported operational path.&lt;/p&gt;
&lt;p&gt;The cutoff does not automatically mean every cluster should be upgraded immediately. It does mean the platform team needs to know which clusters depend on that integration, which Kubernetes versions they run, and what happens if their upgrade schedule does not line up with the integration’s support window. If that information is missing, a vendor policy can quietly become an emergency change request.&lt;/p&gt;
&lt;p&gt;I would put the integration on the same dependency register as the cluster version and the instrumentation libraries. The register should make it easy to answer practical questions: Which clusters use this integration? Who owns it? Which versions are supported today? When does that support change? What is the tested alternative if the cluster cannot move in time? Those answers are more useful than a general statement that “monitoring is installed.”&lt;/p&gt;
&lt;h2 id=&quot;separate-the-telemetry-contract-from-the-vendor-integration&quot;&gt;Separate the telemetry contract from the vendor integration&lt;/h2&gt;
&lt;p&gt;A direct vendor integration often makes sense. It may provide a supported installation path, cluster-specific data, and a straightforward route into the platform the team already uses. The cost is coupling: the integration’s compatibility schedule becomes one more schedule that can affect cluster operations.&lt;/p&gt;
&lt;p&gt;I would look for a boundary between collecting and exporting telemetry, and the vendor-specific behavior around it. For example, an OpenTelemetry Collector can receive telemetry through a pipeline and export it to a destination; the &lt;a href=&quot;https://opentelemetry.io/docs/collector/configuration/&quot;&gt;Collector configuration documentation&lt;/a&gt; describes the receiver, processor, exporter, and pipeline model. A simplified configuration could look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;receivers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    protocols&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      grpc&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;exporters&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    endpoint&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;telemetry-gateway.internal:4317&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    tls&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      insecure&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;service&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pipelines&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    metrics&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      receivers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      processors&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      exporters&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an illustrative configuration, not a drop-in production recipe. It shows the boundary I mean: instrumentation sends telemetry to a collector, and the collector exports it onward. A team could then assess whether to change the export destination without rewriting every application’s instrumentation. The actual path still needs authentication, certificates, resource attributes, batching, failure handling, and tests suited to the environment.&lt;/p&gt;
&lt;p&gt;That boundary is not free. The collector becomes another component to deploy, upgrade, monitor, and troubleshoot. The team must test not only whether telemetry arrives, but whether the data remains useful: names and attributes have to make sense, dashboards and alerts need to keep working, and failures in the export path must be visible. A layer that preserves theoretical choice but nobody tests is not a useful option; it is an additional failure mode.&lt;/p&gt;
&lt;p&gt;So I would not insert a collector just to claim vendor independence. I would use one when the option value is worth its operational cost: for example, when the organization needs a stable telemetry interface across multiple destinations or wants to decouple application instrumentation from a vendor-specific agent. If the direct integration is easy to test, supported for the cluster lifecycle, and acceptable to the team, the simpler path may be the better architecture.&lt;/p&gt;
&lt;h2 id=&quot;make-the-support-window-part-of-upgrade-planning&quot;&gt;Make the support window part of upgrade planning&lt;/h2&gt;
&lt;p&gt;For every integration, I would record its supported platform range and the date or condition that changes that range. Then I would compare those facts with the cluster team’s own upgrade plan. The point is not to force every team onto a single calendar. It is to identify mismatches early enough to choose between upgrading, changing the integration, or accepting a defined support gap.&lt;/p&gt;
&lt;p&gt;That choice should have an owner. If the cluster team owns Kubernetes upgrades and a different team owns observability, neither team should discover during an incident that the other assumed it was handling compatibility. I would make the dependency visible in the change review: which cluster versions are in scope, which integration version has been tested, what telemetry is expected, and what rollback or fallback path exists.&lt;/p&gt;
&lt;p&gt;The same review should cover application instrumentation. A cluster integration and an application library may have different support windows and different failure consequences. One may affect infrastructure signals while the other affects traces or application metrics. Treating them as one undifferentiated “monitoring version” hides the dependencies that matter. I would list them separately, then test the complete signal path that operators rely on.&lt;/p&gt;
&lt;p&gt;For a support change approaching, I would ask the owners to produce a small, explicit plan: identify affected clusters, check their upgrade constraints, validate the next integration version against a representative environment, and decide what to do where those dates do not align. If a telemetry gateway is part of the design, test its export path and its behavior when the destination is unavailable. The plan should state what signals become unavailable or unsupported under each choice, rather than treating continued operation as proof of support.&lt;/p&gt;
&lt;h2 id=&quot;keep-options-real-not-theoretical&quot;&gt;Keep options real, not theoretical&lt;/h2&gt;
&lt;p&gt;Loose coupling is valuable only when the alternative path has been exercised. A second exporter that has never been deployed, a collector configuration no one owns, or dashboards built around vendor-specific attributes can leave a team just as locked in as a direct integration. I would preserve options selectively and pay for them with tests, ownership, and documentation.&lt;/p&gt;
&lt;p&gt;My default is to treat each cluster integration as an operational dependency with a support window, then decide whether a telemetry boundary is worth maintaining. That reframes the question from “Can we collect the data?” to “Can we keep collecting the signals we depend on, within a support window our platform team can accept?”&lt;/p&gt;</content:encoded><category>observability</category><category>kubernetes</category><category>architecture</category></item><item><title>A connection-count metric is not a pool health check</title><link>https://vahidinasab.com/articles/connection-count-is-not-a-pool-health-check</link><guid isPermaLink="true">https://vahidinasab.com/articles/connection-count-is-not-a-pool-health-check</guid><description>A database connection count shows how many clients are present, not whether an application pool is saturated, queuing work or holding connections idle.</description><pubDate>Thu, 08 Oct 2026 19:42:01 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-count-answers-only-one-question&quot;&gt;A count answers only one question&lt;/h2&gt;
&lt;p&gt;A connection count can tell me how many clients a database sees. It cannot tell me whether an application is waiting for a connection, whether every pooled connection is busy, or whether many connections are simply open and idle.&lt;/p&gt;
&lt;p&gt;Azure Database for PostgreSQL added &lt;code&gt;client_connections_total&lt;/code&gt; in its &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/postgresql/release-notes-maintenance/2026-september&quot;&gt;September 2026 release notes&lt;/a&gt;. I see that as a useful prompt to ask a more careful question: what does this particular metric count, and what operational decision should it support? The name alone is not enough to settle whether it represents a current count or an accumulated total, how Azure aggregates it, or what dimensions are available. I would confirm those details in the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/postgresql/monitor/concepts-monitoring&quot;&gt;Azure PostgreSQL monitoring documentation&lt;/a&gt; before building an alert around it.&lt;/p&gt;
&lt;p&gt;Even with its exact semantics understood, a database-side connection count is only one view of the system. It describes connections visible at the database boundary. The application pool has a different job: it manages requests competing for connections before those requests reach PostgreSQL. A high database count might be harmless when the application has capacity and requests are completing promptly. A modest count might still accompany a bad user experience if a small pool is saturated, or if connections are held for too long.&lt;/p&gt;
&lt;p&gt;I would treat the database metric as context, not as a verdict on pool health. For the verdict, I need signals from the pool and from the requests using it.&lt;/p&gt;
&lt;h2 id=&quot;instrument-the-application-side-of-the-boundary&quot;&gt;Instrument the application side of the boundary&lt;/h2&gt;
&lt;p&gt;The pool knows facts the database cannot infer. It can report how many connections are open, how many are in use or idle, how many callers are waiting, and how long they wait. OpenTelemetry’s &lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/db/database-metrics/&quot;&gt;database client metric conventions&lt;/a&gt; describe connection-state, maximum-connection and pending-request metrics. Those signals answer different questions from a server-side count: not just how many connections exist, but whether application work is competing for them.&lt;/p&gt;
&lt;p&gt;In a .NET application using Npgsql, I would start with the driver’s pool metrics rather than trying to reconstruct pool pressure from PostgreSQL sessions. Npgsql documents connection counts by state, including idle and used connections, along with the pool name, so separate pools can be examined independently in its &lt;a href=&quot;https://www.npgsql.org/doc/diagnostics/metrics.html&quot;&gt;OpenTelemetry metrics documentation&lt;/a&gt;. The exact instrumentation and names depend on the client library; I would check what the deployed driver actually emits instead of assuming that every application exposes the same measurements.&lt;/p&gt;
&lt;p&gt;I would put those pool signals next to request signals: request rate, latency, errors, and traces or spans around database acquisition and execution. That combination helps distinguish a pool wait from a slow query. If acquisition time rises while query execution time remains steady, the application may be queuing before it reaches PostgreSQL. If the database work itself slows, adding pool capacity could make contention worse rather than fix it. That is a diagnosis to validate with the workload, not a conclusion a connection counter can provide.&lt;/p&gt;
&lt;p&gt;Idle connections need their own interpretation. An idle connection is not automatically unhealthy; pools commonly retain connections for reuse. But a rising count of idle database sessions alongside a high client count can point to a different capacity question than a pool with few idle connections and sustained waiters. Npgsql’s &lt;a href=&quot;https://www.npgsql.org/doc/connection-string-parameters.html&quot;&gt;connection-string parameter documentation&lt;/a&gt; describes settings for maximum pool size and idle-connection lifetime. Those settings express policy; the metrics show whether the policy fits actual demand.&lt;/p&gt;
&lt;h2 id=&quot;build-a-dashboard-around-a-decision&quot;&gt;Build a dashboard around a decision&lt;/h2&gt;
&lt;p&gt;I would not put every available counter on one dashboard and call that observability. I would organize the view around the question an operator needs to answer: are requests waiting for a connection, waiting on database work, or simply using a healthy pool with connections available?&lt;/p&gt;
&lt;p&gt;A useful first view pairs the Azure-side client count with application-side pool state and request behavior. The database count helps identify what the server sees. Pool usage and wait signals show whether the application is under pressure. Request latency and errors show whether that pressure matters to callers. When the picture is ambiguous, query and trace details help identify whether the delay is at acquisition, execution, or elsewhere in the request path.&lt;/p&gt;
&lt;p&gt;I would also keep the boundaries visible. A database metric may aggregate connections across more than one application instance or pool; an application metric may describe only one process or pool. Comparing those numbers without aligning scope can produce a confident but wrong story. Use dimensions, labels, and resource identity to line up what is being compared, and document whether the metric is a gauge or a cumulative counter before interpreting its trend. Azure’s monitoring guide describes available dimensions and how metrics can be filtered or split; I would use those capabilities where they match the question, not simply because they exist.&lt;/p&gt;
&lt;p&gt;Alerts should follow the same principle. I would not alert just because a connection count crossed a threshold unless that threshold represents a real server-side limit or operational risk. For application impact, I would alert on sustained pool waits, acquisition timeouts, or request degradation, and use the database count to give responders context. The threshold and duration should come from the service’s capacity and workload, not a number copied from another system.&lt;/p&gt;
&lt;h2 id=&quot;correlate-then-choose-an-action&quot;&gt;Correlate, then choose an action&lt;/h2&gt;
&lt;p&gt;When an alert fires, I want the dashboard to narrow the next step. A rising client count with no pool waits and healthy request behavior may call for observation, not a pool change. Waiters and acquisition delays with unused database capacity suggest investigating pool limits or connection lifetimes. A full pool combined with slow database operations suggests examining query behavior and database load before increasing the pool. These are hypotheses to test against traces and workload data, not automatic mappings from one metric to one fix.&lt;/p&gt;
&lt;p&gt;That distinction matters because increasing a pool limit can move pressure rather than remove it. If many application instances each open more connections, the database may receive more concurrent work. Conversely, lowering pool limits to reduce the server count can create longer queues inside the application. I would make that trade-off with measurements from both sides of the boundary.&lt;/p&gt;
&lt;p&gt;A connection count is valuable when I know exactly what it counts and what decision it informs. It becomes misleading when I treat it as a proxy for contention, queueing, or user impact. Pair the new Azure metric with pool and request signals, then design the dashboard to answer an operational question. More counters show more data; the right signals show where work is waiting.&lt;/p&gt;</content:encoded><category>postgresql</category><category>observability</category><category>architecture</category></item><item><title>Treat SELinux mount labeling as a workload-level change</title><link>https://vahidinasab.com/articles/treat-selinux-mount-labeling-as-a-workload-level-change</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-selinux-mount-labeling-as-a-workload-level-change</guid><description>Kubernetes 1.36 can avoid recursive SELinux volume relabeling, but the startup benefit depends on node configuration, storage, and workload behavior.</description><pubDate>Thu, 08 Oct 2026 19:41:57 GMT</pubDate><content:encoded>&lt;h2 id=&quot;ga-does-not-mean-every-volume-takes-the-fast-path&quot;&gt;GA does not mean every volume takes the fast path&lt;/h2&gt;
&lt;p&gt;Recursive SELinux relabeling can turn volume setup into work proportional to the contents of a volume. Mount-time labeling changes that cost: instead of walking files to change their labels, Kubernetes can apply a label through a mount option. The &lt;a href=&quot;https://kubernetes.io/blog/2026/04/22/kubernetes-v1-36-release&quot;&gt;Kubernetes 1.36 release announcement&lt;/a&gt; describes the improvement as generally available and says it can reduce Pod startup delays on SELinux-enforcing systems.&lt;/p&gt;
&lt;p&gt;I would not read that announcement as a guarantee that upgrading to Kubernetes 1.36 makes every Pod start faster. The details matter. The &lt;a href=&quot;https://kubernetes.io/blog/2026/04/22/breaking-changes-in-selinux-volume-labeling&quot;&gt;Kubernetes SELinux volume-labeling guidance&lt;/a&gt; clarifies that the broader &lt;code&gt;SELinuxMount&lt;/code&gt; feature gate is beta and disabled by default in 1.36. The ReadWriteOncePod path and the &lt;code&gt;SELinuxChangePolicy&lt;/code&gt; field have different graduation status. Whether a volume uses mount-time labeling depends on the applicable feature gates and volume and Pod conditions.&lt;/p&gt;
&lt;p&gt;That distinction changes the operational question. It is not simply, “Did I upgrade?” It is, “Which of my workloads now use mount-time labeling, on which nodes, and what changes for those workloads?” I would confirm the effective behavior in the cluster before attributing a startup change to the release. Otherwise, a before-and-after comparison can tell me little: the relevant path may not have changed at all.&lt;/p&gt;
&lt;h2 id=&quot;the-gain-depends-on-the-path-through-the-system&quot;&gt;The gain depends on the path through the system&lt;/h2&gt;
&lt;p&gt;A volume with many files is an obvious candidate for a measurable difference if it previously required recursive relabeling. But file count alone is not a useful workload profile. I would also look at the storage type and CSI driver, how the volume is mounted, whether Pods share it, and whether those Pods use compatible SELinux labels. These details determine both whether mount-time labeling applies and whether a single mount label fits the way the workload uses the data. The Kubernetes &lt;a href=&quot;https://kubernetes.io/blog/2026/04/22/breaking-changes-in-selinux-volume-labeling&quot;&gt;SELinux labeling change guidance&lt;/a&gt; describes conflicts when Pods with different SELinux labels share a volume.&lt;/p&gt;
&lt;p&gt;This is why I would treat the change as an operational trade-off, not a free optimization. The shorter setup path is useful only if it preserves the sharing and access behavior the workload needs. A startup improvement that turns a previously valid shared-volume pattern into a Pod start failure is not an improvement for that workload. I would not loosen SELinux policy or assume a storage driver behaves like another one just to make a benchmark look good.&lt;/p&gt;
&lt;p&gt;The node matters too. The feature is aimed at Linux systems that use SELinux; a cluster with mixed node configurations should not be assessed as if every Pod encounters the same mount path. I would group results by the node and storage combinations the scheduler can actually place the workload on. If a service moves between node pools, its observed startup time may reflect that difference as much as the feature itself.&lt;/p&gt;
&lt;h2 id=&quot;measure-the-part-that-changed&quot;&gt;Measure the part that changed&lt;/h2&gt;
&lt;p&gt;I would compare the same workload before and after enabling the relevant path, keeping the volume contents and placement conditions as consistent as I can. I would include the ordinary case and the cases most likely to expose trade-offs: large or file-dense volumes, the actual CSI driver, concurrent starts, and any shared-volume patterns in use. I would not use a tiny empty volume as evidence for a workload whose startup depends on a populated one.&lt;/p&gt;
&lt;p&gt;For each run, I would track Pod creation through readiness, but I would also break that interval into observable stages. Scheduling, image pulls, volume attachment, mounting, container startup, and readiness checks can all contribute to the total. If only the overall time changes, I still do not know whether SELinux labeling was responsible. If the mount stage shrinks but the Pod reaches readiness at the same time, the feature may be working while another stage remains the actual bottleneck.&lt;/p&gt;
&lt;p&gt;I would compare repeated runs and look at both the typical result and the slow tail. Cold and warm image-cache states should not be mixed without being recorded, and concurrent starts should be tested separately from isolated starts. Those are not claims that every cluster needs a formal benchmark suite; they are ways to avoid mistaking unrelated startup variation for a storage-labeling gain.&lt;/p&gt;
&lt;p&gt;For a workload where I need to explicitly choose the policy, the Pod field gives me a visible control. For example, a workload that must retain recursive relabeling can state that intent:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;apiVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Pod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;metadata&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;spec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  securityContext&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    seLinuxChangePolicy&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Recursive&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  containers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      image&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example-image&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would not copy that setting into every Pod as a precaution. It opts that workload out of the mount-option path; applying it indiscriminately could also hide the very behavior I am trying to evaluate. The &lt;a href=&quot;https://kubernetes.io/blog/2026/04/22/breaking-changes-in-selinux-volume-labeling&quot;&gt;Kubernetes SELinux volume-labeling guidance&lt;/a&gt; explains the policy field and the feature-gate conditions. I would use it where the workload needs recursive behavior, then document why that exception exists.&lt;/p&gt;
&lt;h2 id=&quot;upgrade-for-the-capability-not-an-assumed-speedup&quot;&gt;Upgrade for the capability, not an assumed speedup&lt;/h2&gt;
&lt;p&gt;A platform upgrade can make a better implementation available, but it cannot tell me whether my own bottleneck was recursive relabeling. I would first inventory the node operating systems, feature-gate state, CSI drivers, volume access patterns, and workloads that share storage. Then I would test representative cases and check both startup behavior and compatibility before changing the default posture.&lt;/p&gt;
&lt;p&gt;If the measured mount work falls and the workload still starts correctly, I have evidence to expand adoption. If there is no meaningful change, I would investigate whether the volume took the mount-time path and whether another startup stage dominates. If a shared-volume workload fails or behaves differently, I would treat that as a compatibility issue to resolve, not as a reason to disable SELinux across the cluster.&lt;/p&gt;
&lt;p&gt;That is the performance claim I would trust: not that Kubernetes 1.36 is faster in general, but that a specific workload, on a known node and storage path, starts better without changing the security or sharing behavior it depends on.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>performance</category><category>operations</category></item><item><title>Model upgrades should earn their place in coding-agent workflows</title><link>https://vahidinasab.com/articles/model-upgrades-should-earn-their-place-in-coding-agent-workflows</link><guid isPermaLink="true">https://vahidinasab.com/articles/model-upgrades-should-earn-their-place-in-coding-agent-workflows</guid><description>A repeatable set of repository-specific tasks can show whether a new coding model improves your workflow enough to replace its current default.</description><pubDate>Thu, 08 Oct 2026 19:41:54 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-release-note-is-a-reason-to-test-not-switch&quot;&gt;A release note is a reason to test, not switch&lt;/h2&gt;
&lt;p&gt;A new coding model can make a task possible that an older one struggled with. That is a good reason to try it. It is not, by itself, a good reason to change the default for every coding-agent task.&lt;/p&gt;
&lt;p&gt;I use coding agents as part of engineering work, and I would treat a model change like a dependency change: assess it against the workflows that matter before making it the new default. A model card or benchmark can tell me something about a model in a particular evaluation. It cannot tell me whether the model handles my repositories, instructions, tools, and review standards better.&lt;/p&gt;
&lt;p&gt;That distinction matters because a coding agent is more than its underlying model. The prompt, available tools, repository instructions, permissions, and test commands all shape the result. If I change several of those at once, I cannot tell what caused the difference. If I compare models on a task that does not resemble the work I actually delegate, I may get a clear result that answers the wrong question.&lt;/p&gt;
&lt;p&gt;Notes from &lt;a href=&quot;https://chrisebert.net/notes-from-code-with-claude-2026&quot;&gt;Code with Claude 2026&lt;/a&gt; describe the value of revisiting use cases when models improve, including tasks that did not work before. I agree with the impulse, but I would pair another attempt with a repeatable evaluation. Otherwise, “this model feels better” is easy to mistake for “this model is better for our workflow.”&lt;/p&gt;
&lt;h2 id=&quot;keep-a-small-set-of-real-shaped-tasks&quot;&gt;Keep a small set of real-shaped tasks&lt;/h2&gt;
&lt;p&gt;I would start with tasks that represent the work I want the agent to do, not tasks selected because they are easy to score. The set should be small enough to run whenever a model changes, but varied enough to reveal different failure modes.&lt;/p&gt;
&lt;p&gt;For a codebase, that might mean a bounded bug fix with a failing test, a change that crosses an API and its caller, and a request to explain or review a piece of unfamiliar code. Those are examples of task shapes, not claims about work I have measured. A team should choose its own cases from work it is willing to share and maintain.&lt;/p&gt;
&lt;p&gt;For each task, I would record the same inputs the agent needs: the request, repository state, relevant instructions, tools, and permitted commands. I would also write down what counts as success before comparing models. A short fixture might look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;update-validation&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Reject an empty account name and add a regression test.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;starting_state&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;fixtures/update-validation&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;allowed_commands&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;dotnet test&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;success&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;The new regression test fails before the change and passes after it.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Existing tests pass.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;The change is limited to validation and its tests.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The wording and success conditions above are illustrative; they are not results from a real repository. In an actual evaluation, I would make the fixture point to a controlled repository state and verify that the test genuinely distinguishes the broken behavior from the corrected one.&lt;/p&gt;
&lt;p&gt;I would include tasks where a plausible-looking patch can still be wrong. Tests can check behavior, but they may miss an unnecessarily broad edit, a misleading explanation, or a change that ignores a repository convention. Those need a review criterion as well. Anthropic’s &lt;a href=&quot;https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents&quot;&gt;guide to evaluations for AI agents&lt;/a&gt; describes combining graders, including code-based checks and human judgment; that is a useful reminder that a single pass/fail signal rarely captures the whole quality question.&lt;/p&gt;
&lt;h2 id=&quot;compare-the-whole-workflow-fairly&quot;&gt;Compare the whole workflow fairly&lt;/h2&gt;
&lt;p&gt;The comparison only means something if the conditions stay steady. I would run the candidate model and the current default against the same task inputs, with the same agent configuration and the same tools. I would avoid tuning the prompt for one model while leaving the other with the old instructions. If I do need model-specific configuration, I would treat that as a separate change and document it.&lt;/p&gt;
&lt;p&gt;I would inspect the resulting patch, not just whether the agent produced an answer. Did the code meet the stated behavior? Did the agent run the expected checks? Did it respect the scope? Was its explanation useful enough for a developer to review the change? If a task needs human judgment, I would use a simple rubric and apply it consistently rather than inventing a precise score that suggests more certainty than the review supports.&lt;/p&gt;
&lt;p&gt;I would also note practical friction: whether the agent got stuck, needed repeated intervention, or produced a patch that took longer to understand than to write. These observations do not need to become a synthetic benchmark. A brief record of what happened and why is often more valuable than a single averaged number, especially when the evaluation set is deliberately small.&lt;/p&gt;
&lt;p&gt;A rerun should be possible. Keep the task definition, starting state, configuration, and result notes together. If the task changes because the codebase or requirement changed, record that too. Otherwise, a later result may look like an improvement when the test itself quietly became easier.&lt;/p&gt;
&lt;h2 id=&quot;promote-the-model-only-where-it-helps&quot;&gt;Promote the model only where it helps&lt;/h2&gt;
&lt;p&gt;I would not require a new model to win every task before using it. A model may be better for unfamiliar, multi-file work and no better for routine edits. The useful decision can be scoped: use the candidate for a particular class of task, keep the existing default elsewhere, or leave the default unchanged until there is stronger evidence.&lt;/p&gt;
&lt;p&gt;The reverse is worth checking as well. A new model can make a previously awkward task worth trying again. I would add that task to the evaluation set if it matters to the workflow, then check whether the agent now completes it under the same conditions. That turns an interesting release-note claim into something the team can verify in its own environment.&lt;/p&gt;
&lt;p&gt;This process costs time to set up and maintain. A fixture can become stale, and a small task set cannot represent every request a team will make. I would accept those limits rather than pretend a broad public benchmark settles a local engineering decision. The point is not to certify a model or prove it is universally better. It is to make a change to the workflow for reasons the team can inspect.&lt;/p&gt;
&lt;p&gt;When a model upgrade earns its place, I want to know what changed, on which tasks, and what trade-off I accepted. Until then, I keep it as a candidate—not the default.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>software-engineering</category><category>evaluation</category></item><item><title>Managed coding agents change the control-plane question</title><link>https://vahidinasab.com/articles/managed-coding-agents-change-the-control-plane-question</link><guid isPermaLink="true">https://vahidinasab.com/articles/managed-coding-agents-change-the-control-plane-question</guid><description>A practical way to compare managed coding agents with agents running in your own environment is to trace who controls access, execution and evidence.</description><pubDate>Tue, 06 Oct 2026 14:04:43 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-model-is-not-the-first-boundary&quot;&gt;The model is not the first boundary&lt;/h2&gt;
&lt;p&gt;InfoQ’s &lt;a href=&quot;https://www.infoq.com/news/2026/05/code-with-claude/&quot;&gt;coverage of Anthropic’s Code with Claude&lt;/a&gt; describes managed agents and proactive workflows. The important architecture question is not whether a managed agent uses a smarter model than one I run inside my engineering environment. It is where the control plane sits: who grants the agent access, where it executes, and who can reconstruct what it did.&lt;/p&gt;
&lt;p&gt;That question matters because a coding agent is not just a model receiving a prompt. It is a process that may read source, run commands, call tools, reach services and produce changes. Moving that process to a managed service can change where those actions happen and which system enforces the boundaries. Keeping the agent inside my environment does not automatically make it safe, either. A self-hosted model can still receive excessive permissions or run in a poorly isolated workspace.&lt;/p&gt;
&lt;p&gt;I would compare the options by drawing the trust boundaries, not by comparing product feature lists. For each action, I want to know which system makes the decision, which identity it uses, and where the evidence is recorded. If those answers are vague, a polished orchestration interface has not solved the hard part.&lt;/p&gt;
&lt;h2 id=&quot;trace-the-agents-path&quot;&gt;Trace the agent’s path&lt;/h2&gt;
&lt;p&gt;I start with the repository. Does the agent receive access to a full repository, a branch, or a deliberately limited checkout? Can it read sibling repositories or shared configuration? Can its proposed changes reach a protected branch, or must a person or existing review process approve them? The more repositories and credentials an agent can see, the larger the consequences of a mistaken instruction or a compromised tool.&lt;/p&gt;
&lt;p&gt;Next I trace credentials. A token passed into an agent’s environment is part of its effective authority, whether or not the model itself knows the token’s value. I want credentials scoped to the task and the systems it needs, not a developer’s broad personal token. I also want to know which service stores them, how they are supplied to execution, and whether they can be revoked independently of the agent session.&lt;/p&gt;
&lt;p&gt;Then I trace network access and execution. Can the agent reach only the repository host and approved package sources, or can it call internal services and arbitrary endpoints? Where do shell commands run? What separates the workspace from other jobs, internal networks and persistent data? “Runs in a sandbox” is not enough detail for an architecture decision; I need to know what that sandbox can reach and what it retains.&lt;/p&gt;
&lt;p&gt;Finally, I trace the audit record. I want a useful connection between the task, the identity, the repository revision, the commands or tools used, and the resulting change. A transcript alone may not answer which credentials were available or whether a command reached an internal service. The evidence should live somewhere the team responsible for the repository and its security controls can inspect.&lt;/p&gt;
&lt;p&gt;A simple boundary sketch makes the comparison concrete:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
  accTitle: Coding agent trust boundaries
  accDescr: A team grants access to an agent runtime, which reads a repository, uses credentials, reaches tools, and emits an auditable change.
  Team --&gt;|policy and approval| Runtime
  Runtime --&gt;|scoped checkout| Repository
  Runtime --&gt;|task-scoped access| Credentials
  Runtime --&gt;|permitted calls| Tools
  Runtime --&gt;|execution evidence| Audit
  Audit --&gt; Team
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The point is not that every system needs this exact shape. It is that repository access, credentials, network permissions, execution and records are separate decisions. A managed service may operate some of these layers; an internally hosted agent may operate others. Neither label tells me where the boundary actually lies.&lt;/p&gt;
&lt;h2 id=&quot;keep-the-policy-legible&quot;&gt;Keep the policy legible&lt;/h2&gt;
&lt;p&gt;I would write down the intended boundary before choosing a service. A vendor-neutral policy sketch could look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;agent_task&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  repository&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;selected-repository&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  checkout&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;isolated-task-workspace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  credentials&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;task-scoped&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  network&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;approved-destinations-only&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  writes&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;proposed-change-only&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  evidence&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;team-accessible&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  merge&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;existing-human-review&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not a vendor configuration or a guarantee that the settings are enforceable. It is a list of questions I expect a design to answer. If a provider manages the runtime, I still need to understand which controls it exposes, which controls it owns, and what evidence I can retrieve. If I run the agent inside my environment, I still need to implement and maintain the isolation, credential handling, network policy and records myself.&lt;/p&gt;
&lt;p&gt;Some controls should remain under the team’s ownership even when the execution service is managed. The team owns the repository’s permission model and the rules for approving changes. It owns which internal systems an agent task is allowed to reach. It owns the decision about what evidence is sufficient for review and incident investigation. A service can provide mechanisms for these policies, but the team should be able to state the policy without relying on a provider’s marketing term for it.&lt;/p&gt;
&lt;p&gt;That does not mean every control must run on infrastructure the team operates. There is a real cost to building and maintaining execution isolation, credential brokering and observability. A managed service can be a reasonable choice if its boundaries fit the work and the organization can verify them. Running agents internally can be the better choice when repository or network constraints require direct control, but it makes the operating burden ours. I would choose based on those constraints, not on a general preference for hosted or self-hosted systems.&lt;/p&gt;
&lt;h2 id=&quot;orchestration-is-not-reliability&quot;&gt;Orchestration is not reliability&lt;/h2&gt;
&lt;p&gt;A service that creates sessions, resumes work or coordinates multiple agents can make a workflow easier to operate. It does not establish that the code is correct, that a test is meaningful, or that an agent interpreted the task as intended. Nor does a record of actions prove that the actions were safe. Those are separate questions, and I would not treat orchestration as a substitute for review, tests or a controlled release path.&lt;/p&gt;
&lt;p&gt;I would keep the change path boring: agents propose changes; the repository’s existing checks run; a person reviews the result under the same rules as other changes. If the work is consequential, I would make the allowed scope narrower rather than grant broad access in the hope that a better prompt will compensate. A smaller permission boundary limits what a bad decision can affect; it does not guarantee the decision will be good.&lt;/p&gt;
&lt;p&gt;The comparison I want from a managed-agent proposal is therefore a boundary map: repository access, credential flow, network reach, execution isolation, and the audit trail. For an internally run agent, I want the same map plus an honest account of who will build and operate each control. Once those are explicit, model choice becomes a meaningful part of the decision—but it is no longer standing in for the decision.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>architecture</category><category>security</category></item><item><title>A GitHub Actions lockfile needs an update policy</title><link>https://vahidinasab.com/articles/github-actions-lockfile-update-policy</link><guid isPermaLink="true">https://vahidinasab.com/articles/github-actions-lockfile-update-policy</guid><description>A lockfile can make workflow dependencies easier to audit, but shared pipelines still need owners, cross-repository tests, and an urgent-fix path.</description><pubDate>Tue, 06 Oct 2026 14:04:39 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-lockfile-makes-a-run-more-auditable-not-self-maintaining&quot;&gt;A lockfile makes a run more auditable, not self-maintaining&lt;/h2&gt;
&lt;p&gt;A workflow dependency can change underneath a pipeline when a workflow refers to a mutable tag or branch. GitHub’s &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/&quot;&gt;2026 Actions security roadmap&lt;/a&gt; describes planned lockfiles that record direct and transitive dependencies by commit SHA, addressing dependencies that are resolved at runtime and can be difficult to see. The roadmap also describes a move toward immutable releases. Those are useful directions, but a lockfile does not make its contents stay safe or current.&lt;/p&gt;
&lt;p&gt;I’d treat the lockfile as an audit record and a review boundary, not as a maintenance strategy. It can help answer what dependency versions a workflow resolved to. It cannot decide when a dependency should be updated, who is responsible for making that change, or how to test the change against the repositories that use a shared pipeline. Those are policy decisions, and leaving them implicit makes the lockfile stale by default.&lt;/p&gt;
&lt;p&gt;That distinction matters more when a workflow is shared. A single update to a central pipeline can affect many callers, while the same lockfile change may be reviewed in only one repository. The shared workflow is one dependency; the actions and other dependencies it uses are another. Knowing which reusable workflow a caller invoked is useful provenance, but it is not the same as knowing which dependencies that workflow resolved. I would track both rather than treating either record as a substitute for the other. GitHub documents &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows&quot;&gt;how reusable workflows are called and reused&lt;/a&gt;; the lockfile roadmap addresses the separate dependency-resolution problem.&lt;/p&gt;
&lt;h2 id=&quot;assign-an-owner-and-a-cadence&quot;&gt;Assign an owner and a cadence&lt;/h2&gt;
&lt;p&gt;For application dependencies, I expect a team to know how updates arrive and who reviews them. I would apply the same expectation to workflow dependencies. A lockfile without an update owner is a file that can drift out of date while continuing to give a false sense of control.&lt;/p&gt;
&lt;p&gt;In a shared-pipeline setup, I would assign responsibility to the maintainers of the pipeline, not distribute it vaguely across every consuming repository. The maintainers should be accountable for proposing updates, reviewing dependency changes, and communicating what callers need to do. Each consuming team still needs a clear way to learn whether an update changes the workflow contract, but it should not have to independently rediscover every underlying action update.&lt;/p&gt;
&lt;p&gt;I’d also define an update cadence, while leaving room for urgent changes outside it. A regular review gives maintainers a predictable moment to inspect the lockfile and decide which updates are appropriate. It is not a promise to merge every available update. A changed dependency can alter behavior or permissions, and a new pin is not automatically a good pin. GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;secure use reference&lt;/a&gt; recommends pinning actions to full-length commit SHAs and verifying that a selected SHA comes from the action’s repository. That makes review of the proposed change important: the question is not just whether the SHA changed, but whether the new reference is the intended dependency and whether its behavior is acceptable.&lt;/p&gt;
&lt;p&gt;The policy should make the review path concrete. For example, a pull request updating a workflow lockfile should identify the direct dependency that prompted the change, show the resulting transitive changes, and name the person or team approving it. GitHub describes &lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/actions/reference/security/secure-use&quot;&gt;dependency review&lt;/a&gt; as a way to understand dependency changes and their security impact in pull requests. That visibility can inform review; it does not replace a decision about rollout or compatibility.&lt;/p&gt;
&lt;h2 id=&quot;test-the-shared-change-where-it-will-run&quot;&gt;Test the shared change where it will run&lt;/h2&gt;
&lt;p&gt;A successful test in the pipeline’s own repository is not enough evidence that an update is safe for every caller. Reusable workflows are called from other repositories, and those callers can differ in inputs, permissions, secrets, and surrounding workflow behavior. My preference is to test a proposed dependency update against representative callers before making it the default shared version.&lt;/p&gt;
&lt;p&gt;That does not mean reproducing every consuming repository in one enormous test job. I would choose callers that exercise meaningfully different inputs or security boundaries, run the relevant workflow checks there, and use a staged rollout when the impact is uncertain. The point is to make the test set intentional: a maintainer should be able to explain which caller behaviors it covers and what remains untested. If no representative caller can exercise a relevant path, say so rather than presenting a green check as proof of universal compatibility.&lt;/p&gt;
&lt;p&gt;The central workflow should make the update review legible. A simplified caller might look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;pull_request&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  build&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    uses&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;your-org/ci/.github/workflows/build.yml@&amp;#x3C;reviewed-commit-sha&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      run-tests&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The placeholder is intentional: replace it with a reviewed reference, not a floating branch name. GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows&quot;&gt;reusable workflow documentation&lt;/a&gt; explains the &lt;code&gt;uses&lt;/code&gt; form for calling a reusable workflow, and its &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;secure use guidance&lt;/a&gt; explains SHA pinning for actions. A dependency lockfile and the caller’s reusable-workflow reference answer different questions. The first concerns the dependencies resolved inside the workflow; the second identifies the workflow the caller asked to run. I would review and record both as their respective mechanisms allow.&lt;/p&gt;
&lt;p&gt;A lockfile also has a boundary: it can only help make the dependencies it covers repeatable. It does not, by itself, establish that the runner environment, toolchain, inputs, or external services are fixed. That is a design implication of the lockfile’s scope, not a reason to dismiss it. I would document the scope clearly so that “locked workflow” does not get mistaken for “identical execution in every environment.”&lt;/p&gt;
&lt;h2 id=&quot;make-room-for-an-urgent-fix&quot;&gt;Make room for an urgent fix&lt;/h2&gt;
&lt;p&gt;A policy that only permits routine updates can turn a pin into an obstacle when a dependency needs an urgent security fix. The answer is not to quietly bypass the lockfile or leave the workflow floating. Define an expedited path: who can authorize the change, what minimum checks must pass, how callers are notified, and how the change is recorded for later review.&lt;/p&gt;
&lt;p&gt;For an urgent update, I would prioritize validating the intended source and SHA, checking the changed dependency set, and running the most relevant available caller tests. If the full compatibility set cannot run before the fix is needed, document the gap and follow up with the broader rollout checks. Afterward, review whether the emergency process exposed a missing test or unclear ownership. The exception should be visible and bounded, not a second, informal way of maintaining dependencies.&lt;/p&gt;
&lt;p&gt;That is the practical policy I want beside an Actions lockfile: an owner, a routine update path, tests that reflect shared use, and an explicit emergency route. Pinning makes a dependency choice easier to inspect. The update policy is what keeps that choice deliberate.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>software-supply-chain</category></item><item><title>Ingress controller retirement is an architecture migration</title><link>https://vahidinasab.com/articles/ingress-controller-retirement-is-an-architecture-migration</link><guid isPermaLink="true">https://vahidinasab.com/articles/ingress-controller-retirement-is-an-architecture-migration</guid><description>A practical way to replace a retired ingress controller is to map the edge responsibilities first, then preserve the external-to-internal contract deliberately.</description><pubDate>Tue, 06 Oct 2026 14:04:34 GMT</pubDate><content:encoded>&lt;h2 id=&quot;start-with-the-boundary-not-the-manifest&quot;&gt;Start with the boundary, not the manifest&lt;/h2&gt;
&lt;p&gt;The retirement of Ingress NGINX in March 2026 is a reason to revisit an important service boundary, not just to translate one set of Kubernetes resources into another. The &lt;a href=&quot;https://kubernetes.io/blog/2026/01/29/ingress-nginx-statement&quot;&gt;Kubernetes retirement statement&lt;/a&gt; says the project will receive no further releases or security patches after retirement, and warns that available alternatives are not direct drop-in replacements.&lt;/p&gt;
&lt;p&gt;That last point is the architectural one. A controller migration changes what happens between an external request and an application. If I begin by searching for equivalent annotations, I risk preserving the visible configuration while missing responsibilities that were quietly attached to it: TLS termination, redirects, access restrictions, request limits, or routing decisions. Some of those may belong at the edge; others may belong in an application or a surrounding platform. The right split depends on the contract I need to preserve.&lt;/p&gt;
&lt;p&gt;I would start by writing down what the current edge promises to callers and what it requires from services. That means more than listing hostnames and paths. For each route, I want to know which traffic it accepts, where it sends requests, what TLS behavior callers observe, and what policy is enforced before traffic reaches the application. I also want to identify controller-specific configuration whose purpose is not obvious from its name.&lt;/p&gt;
&lt;p&gt;This inventory is not a Kubernetes manifest. It is a working description of responsibilities, with enough detail to expose assumptions before I choose a replacement:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;routes&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;public-api&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    match&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;api.example.invalid/*&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    destination&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;api-service&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    edge-responsibilities&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;tls-termination&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;redirect-http-to-https&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;request-size-policy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    application-responsibilities&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;authentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;authorization&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    assumptions-to-check&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Clients depend on the current redirect behavior&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;The service does not rely on controller-added headers&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example uses reserved &lt;code&gt;.invalid&lt;/code&gt; names and describes a hypothetical service, not a claim about a real deployment. The purpose is to force questions into the open. Is a policy genuinely an edge concern, or did it land there because the old controller made it convenient? Does the application make an assumption about headers or the original scheme? Is a redirect part of the public behavior, or an implementation detail? I would answer those before changing the resources.&lt;/p&gt;
&lt;h2 id=&quot;separate-the-public-contract-from-its-implementation&quot;&gt;Separate the public contract from its implementation&lt;/h2&gt;
&lt;p&gt;A replacement should preserve the behavior that external callers rely on, while allowing the implementation behind that boundary to change. I would write the contract in terms a client can observe: the hostname and path, the TLS expectations, the response to an HTTP request, and the behavior when a request is rejected or routed to an unavailable service. Then I would note the internal assumptions that make that behavior possible.&lt;/p&gt;
&lt;p&gt;This distinction stops two opposite mistakes. The first is treating every old controller setting as essential and reproducing it blindly. That carries old coupling forward, including settings nobody can explain. The second is assuming that a new controller’s defaults are equivalent because the basic route works. A successful request proves only that one path works; it does not establish that the security policy, redirects, or failure behavior survived.&lt;/p&gt;
&lt;p&gt;I would be especially deliberate about the division between edge and application security. Authentication and authorization often need application context, while some traffic controls may be enforced before a request reaches a service. I would not move a policy just to make a migration easier, nor keep it at the edge just because it was there before. For each policy, I would name its owner, the information it needs, and the failure mode if it is absent. If ownership is unclear, that is a design gap to resolve rather than a YAML detail.&lt;/p&gt;
&lt;p&gt;Portability matters here, but I do not equate it with choosing the most abstract-looking API. I would prefer a boundary contract that is understandable without knowing a particular controller’s annotations, and keep unavoidable implementation-specific choices visible. That gives a future migration a better starting point. It does not promise that another controller can implement every behavior in the same way, or that changing the API makes the underlying operational choice portable by itself.&lt;/p&gt;
&lt;h2 id=&quot;choose-a-replacement-against-the-inventory&quot;&gt;Choose a replacement against the inventory&lt;/h2&gt;
&lt;p&gt;Only after mapping responsibilities would I compare replacement options. I would check whether each candidate can express the required routing and policy, where it expects TLS material and configuration to live, and how its operational model fits the platform. I am not treating this as a claim about Kubernetes networking internals; it is a checklist for asking the right questions of the implementation and its maintainers.&lt;/p&gt;
&lt;p&gt;I would also distinguish what should move from what should be retired. A controller-specific annotation may encode a real requirement, or it may be a workaround that is no longer needed. For every item, I would record one of three outcomes: preserve at the edge, move to the application or another platform component, or remove after confirming no caller depends on it. That makes review more useful than a line-by-line translation because it gives reviewers a reason for each behavior.&lt;/p&gt;
&lt;p&gt;A small migration slice is a better test than a broad rewrite. I would choose a route that exercises meaningful parts of the contract, compare the old and new behavior, and make discrepancies explicit. I would include both expected requests and requests that should be rejected. The point is not to claim that one test proves the migration safe; it is to discover which assumptions the inventory missed before applying the design widely.&lt;/p&gt;
&lt;p&gt;I would keep the contract and the migration notes close to the relevant configuration, ideally where changes to the route can be reviewed alongside its documented behavior. If the team uses diagrams, I prefer text-based diagrams reviewed with code rather than an image that can drift separately. The diagram should show the boundary and responsibility handoffs, not pretend to explain networking details it does not cover.&lt;/p&gt;
&lt;h2 id=&quot;keep-the-boundary-understandable-after-cutover&quot;&gt;Keep the boundary understandable after cutover&lt;/h2&gt;
&lt;p&gt;A migration is not complete just because traffic reaches the same service. It is complete when the new arrangement has an accountable owner for each edge responsibility and the observable contract still makes sense to callers. That includes documenting which policies are implemented where, which configuration is specific to the chosen controller, and how someone can tell whether a change is safe.&lt;/p&gt;
&lt;p&gt;I would resist calling the result portable if its essential behavior depends on undocumented annotations, controller defaults, or conventions known only to one operator. Some implementation coupling is unavoidable. The useful goal is to make that coupling explicit and contained, so the external-to-internal contract remains stable even when the mechanism changes.&lt;/p&gt;
&lt;p&gt;The retirement creates urgency, but urgency is not a reason to mistake syntax for architecture. I would inventory the boundary, choose deliberately where each responsibility belongs, and validate the caller-visible contract before treating the replacement as done.&lt;/p&gt;</content:encoded><category>architecture</category><category>kubernetes</category><category>security</category><category>platform-engineering</category></item><item><title>Long-running coding agents need checkpoints, not just larger tasks</title><link>https://vahidinasab.com/articles/long-running-coding-agents-need-checkpoints</link><guid isPermaLink="true">https://vahidinasab.com/articles/long-running-coding-agents-need-checkpoints</guid><description>I explain why inspectable changes, explicit stopping points, and deliberate resumption make extended coding-agent work easier to review and recover.</description><pubDate>Mon, 05 Oct 2026 07:22:45 GMT</pubDate><content:encoded>&lt;h2 id=&quot;duration-is-not-the-control&quot;&gt;Duration is not the control&lt;/h2&gt;
&lt;p&gt;A coding agent that can keep working for longer does not automatically make a long task safer to delegate. For work that crosses files, layers, or decisions, I care less about how much code arrives in one run than whether I can inspect what happened along the way and choose what happens next.&lt;/p&gt;
&lt;p&gt;That distinction matters as agents take on broader work. Anthropic’s &lt;a href=&quot;https://www.anthropic.com/news/claude-opus-4-8&quot;&gt;Claude Opus 4.8 announcement&lt;/a&gt; describes dynamic workflows in Claude Code for larger tasks: the agent plans work, runs parallel subagents, and verifies outputs before reporting back. The announcement names codebase-scale migrations and refactors as examples. That is a useful opening, but the engineering question is not just whether a system can attempt more work. It is how a person stays in control when the work takes many steps.&lt;/p&gt;
&lt;p&gt;I would not treat an agent’s internal plan, conversation history, or final summary as the authoritative record of a change. Those are useful inputs, but the repository is what I need to review. A good workflow leaves visible evidence in the files and history, sets clear points where a person can stop, and makes it possible to continue from the evidence rather than trusting a long session to remember everything correctly.&lt;/p&gt;
&lt;h2 id=&quot;make-progress-inspectable&quot;&gt;Make progress inspectable&lt;/h2&gt;
&lt;p&gt;A large task should be divided around meaningful changes, not arbitrary time intervals. A migration might have separate stages for identifying affected code, changing one bounded area, updating callers, and validating the result. A refactor might isolate a structural change from behavior changes. The exact boundaries depend on the task; the principle is to make each stage reviewable on its own.&lt;/p&gt;
&lt;p&gt;I would ask the agent to state, before it starts, what it intends to change and what evidence would show that a stage is complete. Then I would have it stop after a bounded stage, summarize the files it changed, and report checks it actually ran. A summary is a navigation aid, not proof. I would inspect the diff and run or review the relevant checks myself before accepting the checkpoint.&lt;/p&gt;
&lt;p&gt;For example, a checkpoint can be recorded as a normal commit after reviewing the staged changes:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sh&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; status&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --short&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; diff&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; path/to/change&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; path/to/change&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; diff&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; --cached&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; commit&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt; &quot;Complete bounded migration step&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a workflow sketch, not a command sequence to paste blindly: the path should name the intended files, and the staged diff should be checked before committing. I prefer checkpoints that describe a coherent state over one commit per tool call. A commit should make it easier to understand or recover the work, not merely prove that the agent was busy.&lt;/p&gt;
&lt;p&gt;The checkpoint also needs a useful handoff. I would ask for the goal of the stage, decisions made, unresolved questions, changed files, checks run, and the next proposed step. That record should be short enough to read and specific enough to challenge. “Tests passed” is weak without saying which checks ran; “finished the refactor” is weak if the diff still contains unrelated changes. If the agent cannot clearly explain the state it is handing over, I would not ask it to continue yet.&lt;/p&gt;
&lt;h2 id=&quot;stop-before-uncertainty-compounds&quot;&gt;Stop before uncertainty compounds&lt;/h2&gt;
&lt;p&gt;A stopping point is valuable when it gives a person a decision to make. It might be a choice between two data shapes, a change to an external contract, or a point where an automated check cannot establish the intended behavior. The agent should not silently turn an unresolved design choice into implementation simply because it can keep going.&lt;/p&gt;
&lt;p&gt;That adds friction. I have to review intermediate work instead of waiting for a polished final result, and too many tiny checkpoints can turn a useful workflow into ceremony. I would set checkpoints where the cost of a wrong assumption rises: before a broad mechanical change, after the first representative slice, and before the change crosses an important boundary. Routine edits that are easy to review can stay together. Decisions with wide consequences should not be buried in a long, uninterrupted run.&lt;/p&gt;
&lt;p&gt;Passing checks is one kind of evidence, not a substitute for judgment. A test suite can show that existing tests still pass; it cannot, by itself, prove that the agent chose the right migration boundary or preserved an unstated requirement. I would ask the agent to report the checks it ran and the gaps it could not resolve. If a required behavior has no test or other clear verification, that is a reason to pause and make the gap explicit, not to infer success from a green command.&lt;/p&gt;
&lt;p&gt;This is also why I would keep the task’s acceptance criteria outside the agent’s private working state. Put them in an issue, a review note, or a file in the repository where another person can compare them with the diff. The agent can help refine the criteria, but its own account of completion should not be the only record of what completion means.&lt;/p&gt;
&lt;h2 id=&quot;resume-from-the-repository&quot;&gt;Resume from the repository&lt;/h2&gt;
&lt;p&gt;Long-running work will be interrupted: a person may stop it, a tool may fail, or a checkpoint may reveal a problem that needs correction. Resumption should start from a known repository state. I would ask the agent to inspect the current branch, working tree, recent commits, and relevant task notes before continuing. It should explain what it believes is complete and what remains, then wait for correction if that understanding is wrong.&lt;/p&gt;
&lt;p&gt;I would not rely on a saved transcript alone. Conversation history can help recover intent, but it can also contain outdated assumptions or intermediate plans that no longer match the code. The committed diff, current tests, and an explicit handoff are better anchors. If those disagree with the old summary, I would trust the present repository and investigate the mismatch.&lt;/p&gt;
&lt;p&gt;A practical instruction for the agent could be as direct as this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Work on one bounded stage at a time.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Before each stage, state the intended change and how you will verify it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;After the stage, stop. Report changed files, decisions, checks run, and unresolved questions.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Do not continue past an unresolved design choice or failing check.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Resume only after inspecting the current repository state and confirming what remains.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The value is not in those exact words. It is in making the control points part of the workflow rather than hoping the agent will remember to offer them. I want the agent to do useful work independently, but I want the repository to preserve enough evidence that I can disagree, roll back, or redirect without treating the agent’s memory as the source of truth.&lt;/p&gt;
&lt;p&gt;Longer tasks are worth delegating when the work stays legible while it is in progress. Larger task limits may let an agent attempt more; checkpoints let me decide whether the attempt is still going in the right direction.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>software-architecture</category><category>developer-workflow</category></item><item><title>Kubernetes Pod user namespaces are worth piloting</title><link>https://vahidinasab.com/articles/kubernetes-pod-user-namespaces-are-worth-piloting</link><guid isPermaLink="true">https://vahidinasab.com/articles/kubernetes-pod-user-namespaces-are-worth-piloting</guid><description>I explain how Pod user namespaces change the host-side impact of a container escape, what they do not replace, and what I would test before relying on them.</description><pubDate>Mon, 05 Oct 2026 07:22:38 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-different-identity-at-the-host-boundary&quot;&gt;A different identity at the host boundary&lt;/h2&gt;
&lt;p&gt;A process can be root inside a container without being root on the node. That distinction is the useful part of Pod user namespaces: Kubernetes maps the Pod’s users to different, unprivileged user IDs on the host. The &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces&quot;&gt;Kubernetes user namespaces documentation&lt;/a&gt; describes this separation and explains that the process has privileges within its user namespace, but not outside it.&lt;/p&gt;
&lt;p&gt;Kubernetes 1.36 makes Pod user namespaces generally available. A Pod opts in with &lt;code&gt;hostUsers: false&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;apiVersion&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;kind&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;Pod&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;metadata&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;namespace-pilot&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;spec&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  hostUsers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#6CB6FF;--shiki-light:#023B95&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  containers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    - &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;app&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      image&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;example/app:stable&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The relevant setting is on the Pod spec, so it belongs in the template for a Deployment or another workload controller as well. I would treat the setting as a change to the workload’s security boundary, not as a claim that the workload is now isolated from the host in every respect. The &lt;a href=&quot;https://kubernetes.io/blog/2026/04/23/kubernetes-v1-36-userns-ga&quot;&gt;Kubernetes 1.36 announcement for user namespaces&lt;/a&gt; confirms the feature’s stable status and the &lt;code&gt;hostUsers: false&lt;/code&gt; opt-in.&lt;/p&gt;
&lt;p&gt;The security gain is about what happens if a process crosses the container boundary. Without a user namespace, a process that runs as root in a container may have a more consequential identity on the host if another weakness lets it escape. With a user namespace, the host sees a mapped, unprivileged user instead. That can reduce the damage of a compromise; it does not stop the compromise itself, prevent every escape, or make the shared host kernel disappear. I would regard it as another layer between a compromised workload and the node, not as a substitute for deciding which workloads I trust to share a node.&lt;/p&gt;
&lt;h2 id=&quot;the-flag-is-the-easy-part&quot;&gt;The flag is the easy part&lt;/h2&gt;
&lt;p&gt;A Pod manifest does not tell me whether the node can actually run that Pod. User namespaces are Linux-specific and depend on support in the kernel, the CRI container runtime, its OCI runtime, and the filesystem used for the relevant mounts. The &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces#before-you-begin&quot;&gt;Kubernetes prerequisites and filesystem notes&lt;/a&gt; are the starting point for checking those dependencies.&lt;/p&gt;
&lt;p&gt;That dependency chain matters especially when I run Kubernetes on my own infrastructure. A cluster can have nodes with different kernels, runtime versions, runtime configuration, or storage layouts. A manifest accepted by the API is not proof that every eligible node can start it. I would verify the requirements against the actual node pools and storage paths, then test scheduling and startup on each class of node that could receive the workload.&lt;/p&gt;
&lt;p&gt;Storage deserves its own test rather than a checkbox in a platform spreadsheet. User namespaces rely on idmapped mounts, and support depends on the filesystem and kernel combination. The Kubernetes documentation, for example, notes that NFS volumes cannot currently be mounted by a user-namespace Pod because the Linux NFS client does not support idmapped mounts. I would test the real volume types, mount options, ownership expectations, and write paths used by the workload instead of assuming that a successful test with an ephemeral filesystem proves persistent storage will behave the same way.&lt;/p&gt;
&lt;p&gt;I would also inspect the image and its assumptions about file ownership. Container UIDs and GIDs are interpreted inside the namespace, while files and mounted volumes still have to work with the resulting mapping. An image that expects to write to a path owned by a particular numeric user, or a workload that depends on particular ownership when sharing files, may expose assumptions that were invisible before. The &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces#understanding-user-namespaces-for-pods&quot;&gt;Kubernetes documentation on UID and GID mappings&lt;/a&gt; describes how IDs are mapped and the default range. I would check actual reads and writes, not infer compatibility from the fact that the process starts.&lt;/p&gt;
&lt;h2 id=&quot;what-it-does-not-replace&quot;&gt;What it does not replace&lt;/h2&gt;
&lt;p&gt;User namespaces address the relationship between container users and host users. They do not replace a deliberate Pod security context, least-privilege capabilities, seccomp, or controls on which workloads may run together. Those controls answer different questions: what the process may do in its own environment, which kernel interfaces it may reach, and who is allowed to deploy it. My design would keep those controls and ask what additional risk the user namespace reduces.&lt;/p&gt;
&lt;p&gt;Nor should I assume that a Pod can use every host integration unchanged. The Kubernetes documentation lists restrictions for user-namespace Pods, including use of host namespaces and raw block devices. Workloads that depend on host-level process, IPC, or networking access—or on devices exposed through &lt;code&gt;volumeDevices&lt;/code&gt;—need particular scrutiny. Some host-networking support is handled separately and has its own feature status and node requirements; the &lt;a href=&quot;https://www.sysdig.com/blog/kubernetes-1-36-new-security-features&quot;&gt;Kubernetes 1.36 security feature notes&lt;/a&gt; describe that distinction. I would not turn on the setting across a fleet before checking each workload’s actual Pod spec and dependencies.&lt;/p&gt;
&lt;p&gt;This is where the feature’s limits become an architectural question. If a workload needs host access that conflicts with the namespace boundary, I would make that exception explicit and consider whether the workload belongs in a separate node pool or needs a different isolation approach. I would not weaken the boundary for every Pod just to avoid examining one exceptional workload. Conversely, I would not force the setting onto a workload whose storage or device requirements make the feature incompatible and then treat the resulting operational friction as an implementation detail.&lt;/p&gt;
&lt;h2 id=&quot;what-i-would-pilot-before-relying-on-it&quot;&gt;What I would pilot before relying on it&lt;/h2&gt;
&lt;p&gt;I would pick a workload that benefits from reducing host-side privilege but does not depend on host namespaces or unusual storage. Then I would test the failure paths as well as the happy path: scheduling onto each relevant node type, container startup, persistent volume mounts, file ownership, init containers, ephemeral debugging, and the operational workflows used to inspect or replace the Pod. Those are not claims that each workflow will fail; they are questions I would want answered before changing the default.&lt;/p&gt;
&lt;p&gt;I would observe startup errors and compare them with the platform’s existing signals. Kubernetes exposes kubelet metrics for user-namespaced Pod starts and errors, listed in the &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces#metrics-and-observability&quot;&gt;user namespaces documentation’s metrics section&lt;/a&gt;. I would use those alongside events and application-level checks. A Pod that starts is not necessarily a workload that behaves correctly, and a metric without a clear owner or response path does not make the rollout safer.&lt;/p&gt;
&lt;p&gt;I would also write down the boundary I expect the feature to improve and the boundaries it does not. If the goal is to reduce the host privileges of a process after a container escape, user namespaces are relevant. If the requirement is protection from a shared-kernel vulnerability, this feature alone does not provide a separate kernel. If the workload requires host access, I need to decide whether that access is essential and whether another isolation design fits better.&lt;/p&gt;
&lt;p&gt;My position is to pilot Pod user namespaces, not to assume that general availability makes them operationally universal. The feature gives me a useful layer: container root need not mean host root. I would rely on it only after validating the runtime, filesystem, workload compatibility, and node placement assumptions that make that layer real.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>security</category><category>architecture</category></item><item><title>Treat OpenTelemetry as an application-to-platform boundary</title><link>https://vahidinasab.com/articles/treat-opentelemetry-as-an-application-to-platform-boundary</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-opentelemetry-as-an-application-to-platform-boundary</guid><description>I explain which telemetry decisions belong in application code, which belong in the platform, and how to keep the seam portable without hiding ownership.</description><pubDate>Mon, 05 Oct 2026 07:22:32 GMT</pubDate><content:encoded>&lt;h2 id=&quot;keep-the-contracts-separate&quot;&gt;Keep the contracts separate&lt;/h2&gt;
&lt;p&gt;OpenTelemetry is not one switch that turns observability on. Its APIs give application code a way to create telemetry; SDKs implement those APIs and control how telemetry is recorded and exported; instrumentation observes libraries and application behavior; the protocol defines how telemetry is exchanged; and the Collector can receive, process, and export it. The &lt;a href=&quot;https://opentelemetry.io/blog/2026/exploring-instrumentation-ecosystem/&quot;&gt;OpenTelemetry instrumentation ecosystem overview&lt;/a&gt; lays out those distinct pieces.&lt;/p&gt;
&lt;p&gt;I treat that separation as an architecture boundary. The application should describe useful facts about its work. The platform should decide how to move, process, and deliver those facts. When those concerns get mixed together, a change to a backend or deployment topology can turn into application code changes across many services. When they are separated without an ownership agreement, the result is often telemetry that is technically flowing but nobody can explain or safely change.&lt;/p&gt;
&lt;p&gt;The boundary is not “all instrumentation in code, all configuration elsewhere.” Automatic instrumentation may observe common framework behavior, and application code may need to add context that no generic agent can infer. The useful question is narrower: which decisions express what this application means, and which decisions express how the organization operates telemetry?&lt;/p&gt;
&lt;h2 id=&quot;put-application-meaning-in-application-code&quot;&gt;Put application meaning in application code&lt;/h2&gt;
&lt;p&gt;An application knows whether it is accepting a payment, loading a profile, or rejecting an invalid request. That meaning belongs close to the code that performs the work. I want service-owned spans and measurements to describe operations and meaningful outcomes, not the name of the observability vendor or the current collector address.&lt;/p&gt;
&lt;p&gt;For example, a small .NET instrumentation point can name an operation and attach an application-level attribute:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;csharp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;using&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; System&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt;Diagnostics&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;private&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; static&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; readonly&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; ActivitySource&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; Source&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;Example.Service&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt; HandleRequest&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;    using&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; var&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F69D50;--shiki-light:#702C00&quot;&gt; activity&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt; Source.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;StartActivity&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;request.handle&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;    activity&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#F47067;--shiki-light:#A0111F&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#DCBDFB;--shiki-light:#622CBC&quot;&gt;SetTag&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;app.operation&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;&quot;handle&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#768390;--shiki-light:#66707B&quot;&gt;    // Application work happens here.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is illustrative, not a complete tracing setup. The &lt;a href=&quot;https://opentelemetry.io/docs/languages/dotnet/instrumentation/&quot;&gt;OpenTelemetry .NET instrumentation guide&lt;/a&gt; shows the &lt;code&gt;ActivitySource&lt;/code&gt; pattern. The important design choice is that the code expresses an operation in the application’s vocabulary. It does not select a destination, embed credentials, or decide the organization’s retention policy.&lt;/p&gt;
&lt;p&gt;That does not mean application teams should invent a new vocabulary for every service. I would agree on naming and attribute rules across services, then keep the instrumentation that needs domain knowledge with the code. A platform team can help define the shared contract, but it cannot reliably infer every meaningful business boundary from runtime data alone. Conversely, application teams should not individually configure backend routing just because their SDK can export directly.&lt;/p&gt;
&lt;h2 id=&quot;keep-transport-and-processing-replaceable&quot;&gt;Keep transport and processing replaceable&lt;/h2&gt;
&lt;p&gt;The Collector is a natural place for platform-owned transport and processing decisions. The &lt;a href=&quot;https://opentelemetry.io/docs/collector/&quot;&gt;Collector documentation&lt;/a&gt; describes it as a vendor-agnostic way to receive, process, and export telemetry. Its configuration can route data and apply processing without requiring each application to implement those operational choices itself.&lt;/p&gt;
&lt;p&gt;A simplified traces pipeline might look like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-dark-dimmed github-light-high-contrast&quot; style=&quot;--shiki-dark:#adbac7;--shiki-light:#0e1116;--shiki-dark-bg:#22272e;--shiki-light-bg:#ffffff; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;receivers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    protocols&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      grpc&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;processors&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  batch&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;exporters&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    endpoint&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;collector:4317&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;service&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;  pipelines&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;    traces&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      receivers&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      processors&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;batch&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-dark:#8DDB8C;--shiki-light:#024C1A&quot;&gt;      exporters&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#96D0FF;--shiki-light:#032563&quot;&gt;otlp&lt;/span&gt;&lt;span style=&quot;--shiki-dark:#ADBAC7;--shiki-light:#0E1116&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The endpoint here is illustrative; a real deployment must define its own connectivity and security. The point is that the service can send telemetry to a stable platform endpoint while the platform owns the onward path. The Collector can also be useful for concerns such as retries, batching, encryption, and filtering, as described in the &lt;a href=&quot;https://opentelemetry.io/docs/collector/&quot;&gt;Collector guide to sending data&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Direct export from an application can be a reasonable starting point, and the Collector documentation notes that it may work well for development or smaller environments. I would still avoid making direct-to-backend configuration the default architecture for a fleet of services. It spreads credentials, destination knowledge, and operational policy across application deployments. A Collector adds another component to run and monitor, but that cost buys a place to change transport and processing independently of application releases.&lt;/p&gt;
&lt;h2 id=&quot;portability-needs-an-owner&quot;&gt;Portability needs an owner&lt;/h2&gt;
&lt;p&gt;Using OpenTelemetry does not make every application automatically portable between every backend. The &lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/vendors/&quot;&gt;vendor support requirements&lt;/a&gt; describe what it means for a vendor to support OpenTelemetry, including accepting telemetry through the protocol or an exporter. That is an important interoperability contract, not a promise that every backend interprets every attribute, query, or feature identically.&lt;/p&gt;
&lt;p&gt;I would therefore define portability as a set of explicit responsibilities, not as a label on the SDK. Application teams own the meaning and quality of the telemetry they create: operation names, useful context, and avoiding accidental disclosure of sensitive data. The platform team owns the SDK configuration, collector deployment, routing, credentials, and backend-specific processing. Both sides own the conventions and compatibility checks that cross the boundary.&lt;/p&gt;
&lt;p&gt;That shared contract should cover resource identity, attribute naming, what data is safe to emit, and which SDK or instrumentation changes need validation. It should also distinguish stable application signals from backend-specific conveniences. If a backend requires a proprietary field for a useful feature, I would make that dependency visible rather than quietly putting it into every service’s instrumentation.&lt;/p&gt;
&lt;h2 id=&quot;test-the-seam-when-things-change&quot;&gt;Test the seam when things change&lt;/h2&gt;
&lt;p&gt;A boundary is only useful if teams can change either side without guessing what the other side expects. Before changing an SDK, instrumentation package, Collector pipeline, or backend, I would check a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the application still emit the telemetry its owners consider meaningful?&lt;/li&gt;
&lt;li&gt;Does the platform still receive and route the signals the service sends?&lt;/li&gt;
&lt;li&gt;Are resource attributes and shared conventions preserved through processing?&lt;/li&gt;
&lt;li&gt;Does a changed exporter or backend requirement introduce application coupling?&lt;/li&gt;
&lt;li&gt;Is the change safe for services that adopt instrumentation on a different schedule?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are compatibility questions as much as observability questions. I would version and review shared conventions deliberately, just as I would other interfaces between teams. The application should be free to evolve its description of its work; the platform should be free to evolve how that description travels. Neither freedom is real unless the other side knows what it can rely on.&lt;/p&gt;</content:encoded><category>opentelemetry</category><category>observability</category><category>architecture</category></item><item><title>What Claude Code auto mode can decide—and what I would gate</title><link>https://vahidinasab.com/articles/what-claude-code-auto-mode-can-decide-and-what-i-would-gate</link><guid isPermaLink="true">https://vahidinasab.com/articles/what-claude-code-auto-mode-can-decide-and-what-i-would-gate</guid><description>I explain where Claude Code auto mode can reduce approval friction, and which actions I would still keep behind explicit human approval.</description><pubDate>Sun, 04 Oct 2026 16:03:21 GMT</pubDate><content:encoded>&lt;h2 id=&quot;auto-mode-changes-who-decides&quot;&gt;Auto mode changes who decides&lt;/h2&gt;
&lt;p&gt;At Anthropic’s &lt;a href=&quot;https://www.anthropic.com/events/code-with-claude&quot;&gt;Code with Claude event in May 2026&lt;/a&gt;, the Claude Code team described auto mode as a way to delegate permission decisions to a classifier that screens for destructive actions and prompt injection. That is a useful change to the permission boundary, not merely a switch for making prompts go away. The question is no longer just whether Claude can call a tool. It is which decisions I am willing to delegate, and what the classifier is being asked to judge.&lt;/p&gt;
&lt;p&gt;Anthropic describes auto mode as a middle path between reviewing each action and skipping permissions altogether: a classifier reviews proposed tool calls, lets actions it judges safe proceed, and blocks actions it considers risky. If blocked, Claude can try a different approach or eventually ask for approval. (&lt;a href=&quot;https://claude.com/blog/auto-mode&quot;&gt;Anthropic’s overview&lt;/a&gt;) The important phrase is “it judges.” A classifier can reduce interruptions; it does not turn a judgment into a guarantee.&lt;/p&gt;
&lt;p&gt;I would not treat this as a reason to distrust the feature. Repeated approval prompts are not a perfect security control either. When a person has to approve every routine read, edit, or test command, the prompts can become noise. The boundary should be designed around consequences: what could this action change, expose, or make difficult to undo? Then I can decide which actions are routine enough to delegate and which need an explicit human decision.&lt;/p&gt;
&lt;h2 id=&quot;delegate-the-narrow-reversible-work&quot;&gt;Delegate the narrow, reversible work&lt;/h2&gt;
&lt;p&gt;I am comfortable letting an agent explore a codebase: read files, trace a call path, inspect tests, and report how a behavior appears to work. Exploration is useful precisely because it reduces the amount of code I have to inspect manually. It is also usually low consequence: reading a file does not itself change the repository or reach another system. Claude Code’s security documentation describes its permission model and built-in protections, including controls for what the agent can read and do. (&lt;a href=&quot;https://code.claude.com/docs/en/security&quot;&gt;Claude Code security documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;I would also allow ordinary, scoped edits when the task is clear and the working tree is isolated. A focused change to the requested files, followed by local tests or formatting, is a sensible candidate for fewer interruptions. The key is scope and reversibility, not the claim that every edit is harmless. A broad rewrite, an unexpected change to configuration, or an edit outside the task’s likely boundary deserves a closer look. I want the diff to remain understandable, and I want a clean way to discard it.&lt;/p&gt;
&lt;p&gt;That is why I think about the workspace along with the permission mode. A separate branch or worktree can limit the cost of an incorrect edit by keeping it away from unrelated work. It does not make a dangerous command safe, and it does not protect data the agent can access outside that workspace. It simply gives me a more manageable place to review and undo code changes. I would rather give an agent room to make a bounded change than pretend that constant permission prompts are the only way to supervise it.&lt;/p&gt;
&lt;p&gt;Running tests is also often a reasonable delegation, provided I know what the test command does. A test suite may write files, start services, or use configured credentials; the name “test” does not make its effects predictable. I would start with known project commands and inspect unfamiliar scripts before allowing them to run. This is my rule of thumb, not a claim that auto mode can infer every repository-specific consequence.&lt;/p&gt;
&lt;h2 id=&quot;gate-actions-that-cross-a-boundary&quot;&gt;Gate actions that cross a boundary&lt;/h2&gt;
&lt;p&gt;I would keep explicit approval for actions that reach outside the code change or have a broad, hard-to-reverse effect. Examples include deleting or overwriting a large set of files, changing access controls, installing or upgrading dependencies, publishing a package, deploying a service, and running commands against production data. These are not equivalent risks, and the exact boundary depends on the environment. But each can affect more than the local patch, so I want a person to understand the target and the consequence before it runs.&lt;/p&gt;
&lt;p&gt;Network access deserves particular care. A command that sends data to a remote service can expose source code, secrets, or user data; a command that downloads and executes something can bring untrusted behavior into the environment. Anthropic’s &lt;a href=&quot;https://code.claude.com/docs/en/security&quot;&gt;security guidance&lt;/a&gt; discusses prompt injection and recommends layered safeguards rather than relying on a single defense. I would treat content from issues, web pages, logs, and repository files as input to assess—not as authority to expand the task or grant new permissions.&lt;/p&gt;
&lt;p&gt;The same principle applies to credentials and privileged environments. If an agent can use a token that can publish, deploy, or modify shared infrastructure, the consequences of an incorrect action are larger than a local code edit. I would restrict what credentials are available to the session and keep approval for operations that use privileged access. A classifier may notice a suspicious action, but I would not make it the only barrier between an agent and a credential with broad reach.&lt;/p&gt;
&lt;p&gt;I also would not let the word “classifier” stand in for a security model. Anthropic presents auto mode as screening tool calls for risk, including destructive behavior and prompt injection. (&lt;a href=&quot;https://claude.com/blog/auto-mode&quot;&gt;Auto mode overview&lt;/a&gt;) That is useful, but the safety question depends on context: which files are reachable, which credentials are present, what commands mean in this repository, and what external systems they can affect. I can’t assume a general-purpose classifier knows all of those local facts or will always interpret them correctly.&lt;/p&gt;
&lt;h2 id=&quot;make-the-boundary-visible&quot;&gt;Make the boundary visible&lt;/h2&gt;
&lt;p&gt;For a team, I would write down what auto mode may do without a prompt and what remains gated. “Routine local exploration and scoped edits” is a starting point, not a complete policy. The policy should name the consequential operations relevant to the team—such as deployments, remote writes, credential changes, and destructive filesystem commands—and explain who approves them. If the list is vague, each engineer will make a different assumption, and the permission mode will look more consistent than the practice really is.&lt;/p&gt;
&lt;p&gt;I would pair that policy with technical limits: use a disposable or isolated working area where practical, avoid giving a coding session credentials it does not need, and preserve review of the resulting diff. These measures address different failure modes. A worktree can contain a bad edit; restricted credentials can limit what a command can reach; human review can catch a change that is technically valid but wrong for the task. None replaces the others.&lt;/p&gt;
&lt;p&gt;My position is to use auto mode for bounded work where the likely cost of a mistaken action is small and recovery is straightforward. I would keep a human in the loop when an action crosses into shared, external, privileged, or difficult-to-reverse territory. That is not a claim that the classifier is ineffective, nor a demand to approve every keystroke. It is a decision about where I want the permission boundary to sit—and a decision I would make explicit before handing an agent more room to act.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>security</category></item><item><title>Compute-storage separation does not remove lock-in</title><link>https://vahidinasab.com/articles/compute-storage-separation-does-not-remove-lock-in</link><guid isPermaLink="true">https://vahidinasab.com/articles/compute-storage-separation-does-not-remove-lock-in</guid><description>Azure HorizonDB shows why PostgreSQL portability depends on operational boundaries and exit paths, not just SQL compatibility.</description><pubDate>Sun, 04 Oct 2026 16:03:17 GMT</pubDate><content:encoded>&lt;h2 id=&quot;portability-is-more-than-speaking-postgresql&quot;&gt;Portability is more than speaking PostgreSQL&lt;/h2&gt;
&lt;p&gt;A PostgreSQL-compatible interface does not, by itself, make a database portable. I would ask a harder question: if the service’s compute and storage are designed as one managed system, what exactly can I take with me when I leave?&lt;/p&gt;
&lt;p&gt;Azure HorizonDB Preview is a useful case for asking that question. Microsoft describes an architecture that separates compute from storage, with storage shared by compute nodes; its documentation also describes a database-as-a-log design (&lt;a href=&quot;https://techcommunity.microsoft.com/blog/adforpostgresql/whats-new-with-postgres-at-microsoft-2026-edition/4526963&quot;&gt;Microsoft’s 2026 Postgres overview&lt;/a&gt;, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/horizondb/overview&quot;&gt;HorizonDB architecture documentation&lt;/a&gt;). That is not merely a different way to buy PostgreSQL. It changes which parts of the system the service owns and which assumptions an architect should test.&lt;/p&gt;
&lt;p&gt;The appeal is straightforward: compute and storage need not be scaled as a single unit. But an architectural benefit inside a service can also define a boundary around that service. I would not treat “PostgreSQL” as the complete portability story. I would separate portability into at least three questions: can my application move, can my data move, and can I operate the destination with the same assumptions?&lt;/p&gt;
&lt;p&gt;The first question is often the easiest. If application code relies on ordinary PostgreSQL behavior and avoids provider-specific features, it may be easier to point it at another PostgreSQL deployment. That is a design aim, not a guarantee. Compatibility tests, extensions, authentication, connection handling, and operational integrations can still make a move more involved than changing a connection string.&lt;/p&gt;
&lt;p&gt;The data question is different. The service’s internal storage design does not automatically tell me what export, restore, or replication paths are available to a customer. I need to establish those separately. And even if I can extract the data, the destination may have different assumptions about restore procedures, capacity, recovery, and maintenance. I would not infer an exit path from the fact that the service is built on PostgreSQL.&lt;/p&gt;
&lt;h2 id=&quot;shared-storage-moves-the-boundary&quot;&gt;Shared storage moves the boundary&lt;/h2&gt;
&lt;p&gt;With a self-operated PostgreSQL deployment, I expect the team to make explicit choices about compute, disks, backups, replication, and failure recovery. A managed service takes some of that operational work away, but it does not make the choices disappear. It relocates responsibility and changes what I can inspect or control.&lt;/p&gt;
&lt;p&gt;That distinction matters when compute and storage are separated. If compute nodes share a storage layer, I want to understand which failures the service handles, which failures surface to my application, and what recovery behavior I should design around. The architecture documentation can explain the model, but an architect still needs answers tied to the service contract: what happens during a compute failure, a storage disruption, a regional incident, or a maintenance event? What does the application observe? Which recovery actions are available to the customer?&lt;/p&gt;
&lt;p&gt;These are not reasons to reject the design. They are reasons to draw the operational boundary accurately. A team may be comfortable delegating storage durability and recovery to a provider, especially if it does not want to run that machinery itself. Another team may need more control over the database’s failure modes, or may already have the operational capability to run PostgreSQL on its own infrastructure. The right choice depends on which responsibilities the team is willing and able to own.&lt;/p&gt;
&lt;p&gt;I would be careful not to compare a managed service with an imaginary self-hosted database that runs itself. Operating PostgreSQL means owning the surrounding work as well as the server: deployment, upgrades, backup verification, monitoring, access control, and recovery exercises. Conversely, choosing a managed service does not remove the need to understand application-level retries, connection behavior, data protection, and the consequences of an outage. It changes the division of labor, not the need to design for failure.&lt;/p&gt;
&lt;h2 id=&quot;ask-what-a-move-would-require&quot;&gt;Ask what a move would require&lt;/h2&gt;
&lt;p&gt;Portability becomes concrete when I describe the exit, rather than when I label a design “portable.” I would write down a plausible move from the managed service to a PostgreSQL deployment the team operates itself, then identify the assumptions that plan depends on.&lt;/p&gt;
&lt;p&gt;First, I would inventory the database surface the application actually uses. Which extensions, database settings, authentication mechanisms, and provider-specific capabilities are required? Which are optional conveniences? If the application depends on a feature that exists only in the managed environment, that may be a sensible choice—but it is a dependency to record, not a surprise to discover during migration.&lt;/p&gt;
&lt;p&gt;Second, I would ask how data is extracted and kept consistent during the transition. A one-time export may be enough for some workloads; others may require a controlled period in which writes continue while the destination is prepared. The available mechanisms, their limitations, and the service’s documented guarantees need to be verified for the chosen product and workload. I would not assume that an architectural description of shared storage answers these questions.&lt;/p&gt;
&lt;p&gt;Third, I would test the destination as a real operating environment. A successful restore is only one part of the move. The team also needs to confirm that its monitoring, backup policy, access model, capacity planning, and recovery procedures work there. A design that is portable at the SQL layer but depends on provider-specific operational tooling has only partial portability.&lt;/p&gt;
&lt;p&gt;Finally, I would decide whether that degree of portability is worth paying for. Avoiding every provider-specific feature can make a system less useful without making an exit meaningfully easier. The goal is not to preserve a theoretical ability to leave at zero cost. It is to understand the cost, keep it within the organization’s tolerance, and avoid accidental dependence where the consequences would be unacceptable.&lt;/p&gt;
&lt;h2 id=&quot;put-the-exit-path-in-the-decision&quot;&gt;Put the exit path in the decision&lt;/h2&gt;
&lt;p&gt;For HorizonDB Preview, I would evaluate the promised benefits of separated compute and shared storage against the operational boundaries the service creates. Microsoft’s description establishes the architectural direction; it does not, by itself, settle how a particular team should move data, respond to failures, or operate elsewhere. Those questions belong in product evaluation, not in a migration plan written after the system depends on the service.&lt;/p&gt;
&lt;p&gt;When comparing a managed PostgreSQL service with PostgreSQL that a team operates itself, I would make the trade explicit. A managed design may reduce the work the team must perform directly. A self-operated design may expose more of the stack to the team, while also making that team accountable for running it. Neither is inherently more portable. Portability depends on the application’s dependencies, the data’s exit path, and whether the destination’s operational model is one the team can sustain.&lt;/p&gt;
&lt;p&gt;I would choose the managed architecture when its operating model solves a problem the team is prepared to hand over—and when the service’s failure and exit behavior are clear enough to accept. If those answers are missing, I would treat that as an unresolved architecture decision, not as a detail to defer because the database speaks PostgreSQL.&lt;/p&gt;</content:encoded><category>postgresql</category><category>architecture</category><category>cloud</category></item><item><title>Treat coding-agent memory as a cache</title><link>https://vahidinasab.com/articles/treat-coding-agent-memory-as-a-cache</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-coding-agent-memory-as-a-cache</guid><description>Learn why coding-agent memory is useful context, not an architectural record, and how to keep reviewed decisions authoritative when memory goes stale.</description><pubDate>Sun, 04 Oct 2026 16:03:13 GMT</pubDate><content:encoded>&lt;h2 id=&quot;memory-is-an-input-not-a-record&quot;&gt;Memory is an input, not a record&lt;/h2&gt;
&lt;p&gt;When an assistant carries knowledge between threads, a useful convenience changes shape: context becomes a mutable input to future work. Anthropic’s September 2026 Claude Projects update describes threads that both add to and draw from shared project memory, including details such as a changed release plan or why a feature was dropped (&lt;a href=&quot;https://claude.com/blog/projects-redesigned&quot;&gt;Anthropic’s announcement&lt;/a&gt;). That can reduce repeated setup. It does not make the memory a dependable account of why the system is designed as it is.&lt;/p&gt;
&lt;p&gt;I would treat coding-agent memory as a cache. A cache helps retrieve information quickly, but it is not where I would establish the authoritative meaning of a design. It may be incomplete, outdated, or too compressed to preserve the reasoning behind a choice. And when several threads contribute to the same memory, a statement that was useful in one context can be carried into another without its original qualifications.&lt;/p&gt;
&lt;p&gt;That distinction matters because architectural decisions are not just facts to recall. “The service uses PostgreSQL” is different from “we chose PostgreSQL because these queries need relational constraints, and we rejected a document store for this access pattern.” The first may describe the current state. The second records a decision, its context, and the trade-off that made it reasonable. If the assistant remembers only the conclusion, it may confidently apply it where the original reasoning no longer fits.&lt;/p&gt;
&lt;p&gt;The assistant’s memory can still be valuable. It can remind a new thread where to look, what terms the team uses, or which document might answer a question. But I would not ask it to settle a disagreement about the system. When memory and reviewed documentation conflict, the documentation wins until someone changes it deliberately.&lt;/p&gt;
&lt;h2 id=&quot;put-decisions-where-change-can-be-reviewed&quot;&gt;Put decisions where change can be reviewed&lt;/h2&gt;
&lt;p&gt;I want architectural decisions in version-controlled documents next to the code they explain. That gives a reviewer a place to see what changed, discuss it, and connect the choice to the implementation. A short decision record can capture the problem, the options considered, the choice, and the costs accepted. A diagram can show the boundaries and interactions that prose alone makes harder to inspect.&lt;/p&gt;
&lt;p&gt;The important property is not a particular template or file format. It is that a person can review the record as a change, and that the record has an owner who can update it when the design changes. If the service boundary moves, update the diagram and the decision. If the reason for a constraint no longer applies, say so rather than letting an old explanation quietly persist in a prompt or memory store.&lt;/p&gt;
&lt;p&gt;I would keep the documentation concise enough to maintain. A sprawling architecture document that nobody changes is not more trustworthy just because it lives in the repository. Record decisions that affect future work: boundaries, data ownership, important integration contracts, security constraints, and consequential trade-offs. Link to relevant code where that helps readers verify the current behavior. Treat the document as a maintained explanation, not an attempt to describe every implementation detail forever.&lt;/p&gt;
&lt;p&gt;Agent memory serves a different purpose. It can point toward those records and carry lightweight working context between tasks. For example, it might say, “Check the service-boundary decision before changing this integration.” That is a useful pointer. It should not replace the decision record with a compressed recollection of what the assistant thinks the decision was.&lt;/p&gt;
&lt;h2 id=&quot;make-stale-memory-cheap-to-correct&quot;&gt;Make stale memory cheap to correct&lt;/h2&gt;
&lt;p&gt;A cache is useful partly because I do not need to preserve every cached entry forever. The same attitude helps with agent memory: make it easy to check, correct, pause, or discard. If an assistant has learned a deadline, a local naming convention, or a temporary constraint, that information can help the next task. If it has retained an old assumption about a service or a design choice, keeping it around may do more harm than asking it to inspect the current source of truth.&lt;/p&gt;
&lt;p&gt;I would give the assistant an explicit precedence rule in its project instructions: use memory as a lead, not as authority; check reviewed architecture documents and current code for consequential decisions; surface conflicts instead of resolving them silently. This does not guarantee the assistant will always follow the rule. It does make the intended ownership clear and gives a reviewer a basis for spotting when the process failed.&lt;/p&gt;
&lt;p&gt;There is also a difference between a helpful reminder and a durable decision. “Check with the owning team before changing this boundary” may be useful operational context. “This boundary exists to keep customer data isolated” is a design claim that deserves a reviewable record and evidence in the system. If the assistant stores both, the first can be a convenience; the second should point back to the documentation that people maintain.&lt;/p&gt;
&lt;p&gt;When memory appears wrong, I would correct the underlying record first if the design changed, then correct or remove the memory. Otherwise, the system of record and the assistant’s carried context keep disagreeing. If the memory is merely a mistaken summary, discard or rewrite it rather than bending the architecture document to match. The correction path should preserve that distinction.&lt;/p&gt;
&lt;h2 id=&quot;decide-who-owns-the-truth&quot;&gt;Decide who owns the truth&lt;/h2&gt;
&lt;p&gt;The useful question is not whether the assistant remembers. It is who owns the truth when its memory goes stale. I would make the answer explicit: people own architectural decisions through reviewed documentation; the assistant can help retrieve and apply context, but does not become the authority by remembering it.&lt;/p&gt;
&lt;p&gt;That choice has a cost. Maintaining decision records takes attention, and the assistant may ask for context that already exists somewhere else. I would accept that friction over treating mutable memory as a hidden design repository. The hidden version is harder to review, harder to update deliberately, and harder to distinguish from a current decision.&lt;/p&gt;
&lt;p&gt;When I ask an assistant to change a system, I want it to find the relevant decision, show how the proposed work fits it, and flag any conflict. If the decision is missing, that is a reason to ask for judgment or document a new choice—not a reason to let the most recent remembered sentence silently become architecture.&lt;/p&gt;</content:encoded><category>ai-assisted-engineering</category><category>architecture</category><category>documentation</category></item><item><title>Stable Kubernetes metadata in OpenTelemetry production pipelines</title><link>https://vahidinasab.com/articles/stable-kubernetes-metadata-in-opentelemetry-production-pipelines</link><guid isPermaLink="true">https://vahidinasab.com/articles/stable-kubernetes-metadata-in-opentelemetry-production-pipelines</guid><description>Learn where Kubernetes metadata enrichment belongs, how to limit noisy attributes, and what to check when operating the OpenTelemetry Collector.</description><pubDate>Fri, 02 Oct 2026 16:27:26 GMT</pubDate><content:encoded>&lt;h2 id=&quot;metadata-makes-signals-joinable&quot;&gt;Metadata makes signals joinable&lt;/h2&gt;
&lt;p&gt;A trace can tell me that a request slowed down; Kubernetes context can help me connect that signal to the Pod, namespace, or workload that produced it. Without that context, I may have to infer where the signal came from using a service name, a timestamp, and knowledge of what was deployed. With it, I can ask more directly whether the problem is isolated to a Pod, shared across a Deployment, or associated with a particular part of the cluster.&lt;/p&gt;
&lt;p&gt;The OpenTelemetry Kubernetes attributes processor discovers Pod metadata and adds selected attributes to telemetry as resource attributes. It supports traces, metrics, and logs, giving those signals a common place to carry workload identity. The processor’s documentation describes Pod association as the step that connects incoming telemetry with cached Kubernetes metadata; connection information and existing resource attributes are among the ways to make that association. (&lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md&quot;&gt;Processor documentation&lt;/a&gt;; &lt;a href=&quot;https://opentelemetry.io/blog/2022/k8s-metadata&quot;&gt;OpenTelemetry Kubernetes troubleshooting guide&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;That distinction matters: enrichment is not just adding labels after collection. The Collector has to identify which Kubernetes object a signal belongs to before it can attach the right context. If that association is wrong or unavailable, adding more metadata options will not fix it. I would first verify how telemetry reaches the Collector and which identity information survives that path.&lt;/p&gt;
&lt;p&gt;The processor reaching v1.0.0 is timely because it accompanies the move of Kubernetes semantic conventions to stable status. Stable names make it more reasonable to build dashboards, queries, and alert logic around a consistent vocabulary—but the milestone is a reason to review compatibility, not to assume every existing pipeline can be upgraded without checking its emitted attributes. (&lt;a href=&quot;https://opentelemetry.io/blog/2026/k8s-attributes-processor-v1&quot;&gt;OpenTelemetry announcement&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id=&quot;put-enrichment-where-identity-is-still-available&quot;&gt;Put enrichment where identity is still available&lt;/h2&gt;
&lt;p&gt;I would place enrichment near the point where telemetry first enters a Collector path that can reliably identify its originating Pod. The &lt;code&gt;k8sattributes&lt;/code&gt; processor can associate telemetry using connection information, but the Collector documentation notes that this context can be removed by processors such as batching or tail sampling. When relying on connection-based association, the processor therefore needs to run before those processors. (&lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md&quot;&gt;Processor documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;This makes deployment topology part of the design. A Collector close to workloads may see the connection details needed for association. A gateway receiving traffic forwarded from another Collector or network hop may not. In that case, I would establish which resource attributes the upstream path can supply and configure association around those attributes, rather than trusting the gateway to reconstruct information that has already been lost. The processor supports association from resource attributes as well as connection context. (&lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md&quot;&gt;Processor documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;A practical pipeline review should follow one signal from the application to the exporter. I would check where the resource identity is created, whether it survives forwarding, when the Kubernetes attributes processor runs, and whether later processors alter or remove the fields I need. If I intend to filter or sample based on Kubernetes metadata, enrichment must happen before that decision. If I only need the metadata at the final backend, I still need to ensure it is attached before any processing step that discards the association context.&lt;/p&gt;
&lt;p&gt;I would also treat permissions and filtering as part of the same design. The processor discovers Kubernetes resources through the Kubernetes API, so its service account and RBAC configuration need to permit the discovery it is expected to perform. Its configuration supports filtering which Pods it observes; limiting that scope can make sense when a Collector serves only a subset of a cluster. (&lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md&quot;&gt;Processor documentation&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id=&quot;keep-the-useful-context-not-every-label&quot;&gt;Keep the useful context, not every label&lt;/h2&gt;
&lt;p&gt;The temptation is to extract every label and annotation because they might help one day. I would resist it. Metadata is useful when it answers an operational question, such as which namespace or workload owns a signal. A broad extraction policy can instead turn incidental deployment details into permanent telemetry fields. It can also make the Collector track and attach more data than the investigation needs.&lt;/p&gt;
&lt;p&gt;The processor’s documentation calls out resource and configuration factors that affect memory use, including the number of Pods monitored, the metadata fields extracted, label and annotation extraction rules, and workload metadata. That makes selection a production concern, not just a matter of keeping dashboards tidy. (&lt;a href=&quot;https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/k8sattributesprocessor/README.md&quot;&gt;Processor documentation&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;I would start with a small set of stable identifiers that support routine navigation: namespace, Pod, and the owning workload where available. Then I would add a label only when a specific query, ownership workflow, or incident investigation requires it. Before extracting an annotation, I would ask who maintains it, whether its value is bounded and suitable for telemetry, and whether the same information belongs in a different system. Those are design judgments, but they help prevent “metadata available” from becoming “metadata attached everywhere.”&lt;/p&gt;
&lt;p&gt;This restraint matters especially for metrics. Resource attributes provide a way to attach Kubernetes identity to telemetry, and Kubernetes metric conventions describe metrics as belonging to a Kubernetes resource and inheriting its attributes. (&lt;a href=&quot;https://opentelemetry.io/docs/specs/semconv/system/k8s-metrics&quot;&gt;OpenTelemetry Kubernetes metrics conventions&lt;/a&gt;) In my view, that is a good reason to decide deliberately which attributes should travel with a metric: useful context can improve investigation, while attributes that vary unnecessarily can make downstream querying and storage harder to manage. I would test the chosen set against real queries before expanding it.&lt;/p&gt;
&lt;p&gt;The v1.0.0 milestone is also a good prompt to inspect names end to end. The announcement ties the processor milestone to stable Kubernetes semantic conventions. I would compare the attributes configured for extraction with the names consumed by dashboards, alerts, and backend mappings, then test a representative signal from each pipeline. A processor upgrade that starts emitting a stable name is only helpful if the rest of the observability path agrees on that name. (&lt;a href=&quot;https://opentelemetry.io/blog/2026/k8s-attributes-processor-v1&quot;&gt;OpenTelemetry announcement&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id=&quot;operate-the-collector-as-part-of-the-system&quot;&gt;Operate the Collector as part of the system&lt;/h2&gt;
&lt;p&gt;The processor is one component in a collection path, so I would monitor the Collector as carefully as the applications it serves. I would check whether it can discover the expected Pods, whether association succeeds for each signal type, whether the intended attributes arrive at the exporter, and whether the Collector remains within its resource limits as monitored workloads and extracted metadata change. A healthy process is not enough if it is silently enriching only some of the telemetry.&lt;/p&gt;
&lt;p&gt;I would make those checks concrete. Send or select a trace, metric, and log from a known workload; inspect the resource attributes at the receiving backend; and compare them with the Pod and workload identity in Kubernetes. Test both the normal path and the forwarded path if there is a gateway. Then review Collector logs and internal telemetry for discovery, permission, or processing problems. This is my operational checklist, not a guarantee that a particular deployment mode is correct for every cluster.&lt;/p&gt;
&lt;p&gt;The recent Prometheus and OpenTelemetry interoperability survey offers a useful backdrop, but it should not be mistaken for evidence that Kubernetes enrichment alone solves interoperability. OpenTelemetry reports that respondents’ average ease-of-use rating for using Prometheus and OpenTelemetry together rose from 3.1 in the 2024 survey to 3.6 in 2026, while the share who found the combination hard to use fell from 29% to 10%. The same project describes the survey as a look at how users collect, process, and store metrics. (&lt;a href=&quot;https://opentelemetry.io/blog/2026/otel-prometheus-interoperability&quot;&gt;Survey results&lt;/a&gt;) Those results suggest progress alongside continued integration work; they do not establish that any single Collector processor caused it.&lt;/p&gt;
&lt;p&gt;For me, the practical value of stable Kubernetes metadata is straightforward: it gives signals a more consistent route back to the workloads behind them. The processor helps only when association works, enrichment happens before identity is lost, and the extracted fields stay focused. I would treat its v1.0.0 milestone as a good moment to verify those conditions—not as a reason to attach every piece of Kubernetes metadata to every signal.&lt;/p&gt;</content:encoded><category>opentelemetry</category><category>kubernetes</category><category>observability</category></item><item><title>Make secret access explicit in reusable workflows</title><link>https://vahidinasab.com/articles/make-secret-access-explicit-in-reusable-workflows</link><guid isPermaLink="true">https://vahidinasab.com/articles/make-secret-access-explicit-in-reusable-workflows</guid><description>Learn how scoped secrets could change reusable workflow contracts, and how to roll out explicit access and verify it with execution telemetry.</description><pubDate>Fri, 02 Oct 2026 16:27:21 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-reusable-workflow-is-a-contract&quot;&gt;A reusable workflow is a contract&lt;/h2&gt;
&lt;p&gt;A shared pipeline makes a promise to its callers: provide the inputs it needs, and it will perform a defined job. Secret access belongs in that promise. If a workflow can use credentials merely because they happen to be available in the calling repository, the contract is incomplete.&lt;/p&gt;
&lt;p&gt;I have helped design and roll out CI/CD pipelines reused across hundreds of repositories. That experience makes me wary of treating a workflow as a convenient YAML fragment. A shared workflow is also a security boundary, and its callers need to know what crosses that boundary.&lt;/p&gt;
&lt;p&gt;GitHub’s &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/&quot;&gt;2026 Actions security roadmap&lt;/a&gt; proposes scoped secrets for reusable workflows, alongside near-real-time execution telemetry. The roadmap describes a shift toward binding secrets to trusted execution contexts rather than broadly inheriting them. These are proposed capabilities, not controls I would assume are already available. But the direction is useful now: it asks platform teams to make credential access deliberate and verifiable.&lt;/p&gt;
&lt;p&gt;Today, GitHub supports explicitly passing named secrets to a reusable workflow, as well as using &lt;code&gt;secrets: inherit&lt;/code&gt; in supported cases. The &lt;a href=&quot;https://docs.github.com/actions/reference/workflow-syntax-for-github-actions&quot;&gt;workflow syntax documentation&lt;/a&gt; describes both patterns and the &lt;code&gt;workflow_call&lt;/code&gt; secret declarations. In my view, explicit passing is the better default for shared workflows because it makes the caller’s grant visible where the call is made. Inheritance can be convenient, but convenience should not silently define the trust boundary.&lt;/p&gt;
&lt;h2 id=&quot;inventory-access-before-changing-yaml&quot;&gt;Inventory access before changing YAML&lt;/h2&gt;
&lt;p&gt;The first rollout task is not to edit every caller. It is to learn what credentials the workflows actually need.&lt;/p&gt;
&lt;p&gt;I would start with an inventory of reusable workflows and the secrets each one reads. For each credential, record its purpose, which workflow uses it, which execution paths need it, and what authority it carries. A workflow that builds and tests code may need no long-lived deployment credential at all. A publishing workflow might need a package registry token; a deployment workflow might need access to a target environment. Those are different jobs with different trust requirements, even if they currently sit in one shared pipeline.&lt;/p&gt;
&lt;p&gt;The inventory should distinguish direct use from accidental availability. A secret being present in a repository does not prove that every reusable workflow called from that repository needs it. Ask workflow owners to confirm each dependency, and treat an unexplained credential as a reason to investigate rather than as a reason to preserve broad access.&lt;/p&gt;
&lt;p&gt;Then define the contract at both ends. The reusable workflow should declare the secrets it expects, with names that communicate purpose. The caller should pass only those secrets, using the names the reusable workflow declares. GitHub documents this named-secret pattern, including a validation error when a caller passes a secret that the called workflow has not specified. See the &lt;a href=&quot;https://docs.github.com/actions/reference/workflow-syntax-for-github-actions&quot;&gt;workflow syntax reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That contract is not just a list. It should also say which jobs or steps consume a credential, and why. If a secret is only needed during deployment, keeping it out of build and test paths is a better design than making the entire reusable workflow depend on it. If one reusable workflow performs unrelated tasks with different credentials, split it or separate the jobs so the access boundary matches the work.&lt;/p&gt;
&lt;h2 id=&quot;make-trust-boundaries-visible&quot;&gt;Make trust boundaries visible&lt;/h2&gt;
&lt;p&gt;Explicit passing improves the caller’s view of access, but it is not a complete security model. A grant only makes sense in the context of the code that receives it, the event that starts it, and the permissions available to the workflow. A narrowly named secret passed to an overly broad workflow is still a broad trust decision.&lt;/p&gt;
&lt;p&gt;I would review callers by trust level, not just by repository. Which events can invoke the workflow? Can untrusted contributions reach a job that handles credentials? Is the reusable workflow pinned and reviewed according to the organization’s policy? Does the job’s &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permission set match its work? GitHub’s &lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use&quot;&gt;secure use guidance&lt;/a&gt; covers risks in workflows and reused code; secret scoping should sit alongside those controls, not stand in for them.&lt;/p&gt;
&lt;p&gt;The practical test is whether a reviewer can follow the path from caller to credential without guessing. The caller shows what it grants; the reusable workflow declares what it accepts; the job shows where it is used. If a credential appears only through implicit inheritance, that path is harder to see in review. I would migrate away from inheritance where possible, and document any exception with the reason it remains necessary.&lt;/p&gt;
&lt;p&gt;This is also why I would resist a single organization-wide “pipeline secret” available to every shared workflow. One reusable workflow may need to publish an artifact while another deploys it. Giving both the same powerful credential erases the distinction between those responsibilities. Separate credentials by purpose and limit which caller-workflow combinations can receive them. Where a platform offers a short-lived identity mechanism that meets the use case, prefer evaluating that over adding another long-lived secret; do not assume it eliminates the need to define who is trusted to request it.&lt;/p&gt;
&lt;h2 id=&quot;roll-out-in-stages-then-measure&quot;&gt;Roll out in stages, then measure&lt;/h2&gt;
&lt;p&gt;A fleet-wide change should begin with a small pilot, not a mass edit. Choose representative workflows: one that needs no secret, one that needs a narrowly scoped credential, and one with a more complex call chain. Make the intended contracts explicit, run them through normal changes, and ask maintainers to identify where the new model is unclear. The pilot should expose naming problems and hidden dependencies before they are multiplied across repositories.&lt;/p&gt;
&lt;p&gt;Next, migrate by workflow family. Publish the new contract and a short migration guide, then update callers in manageable batches. For each batch, track which callers have been migrated, which still rely on inheritance, and why any exception remains. Keep a rollback path while teams verify the behavior. Avoid turning a temporary compatibility measure into a permanent permission model simply because it was convenient during rollout.&lt;/p&gt;
&lt;p&gt;Telemetry can make that process more than a checklist. The roadmap’s proposed &lt;a href=&quot;https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/&quot;&gt;Actions Data Stream&lt;/a&gt; points toward centralized, near-real-time execution data. If the eventual data includes useful workflow and secret-access context, I would use it to answer operational questions: which workflows are still using the old pattern, whether a migrated caller runs as expected, and whether unexpected execution paths are reaching sensitive jobs. The roadmap describes the direction; teams should verify the actual fields and availability before designing controls around them.&lt;/p&gt;
&lt;p&gt;I would define those questions before the telemetry arrives. Decide what evidence proves migration, who reviews exceptions, and how a team will investigate an unexpected execution. Then connect the evidence to the workflow inventory and ownership model. Telemetry without an owner or a response process is just another stream of events. Conversely, a dashboard that only counts successful runs cannot establish that secret access is appropriately scoped.&lt;/p&gt;
&lt;p&gt;The goal is not to make every reusable workflow complicated. It is to make the important boundary legible: a caller grants a particular workflow the credentials required for a particular job, and the organization can verify that the grant is being used as intended. Scoped secrets could make that contract enforceable at the platform level. The work of deciding what should be trusted still belongs to the teams designing the pipeline.&lt;/p&gt;</content:encoded><category>github-actions</category><category>ci-cd</category><category>security</category><category>architecture</category></item><item><title>Test your workload before redesigning for PostgreSQL 19</title><link>https://vahidinasab.com/articles/test-your-workload-before-redesigning-for-postgresql-19</link><guid isPermaLink="true">https://vahidinasab.com/articles/test-your-workload-before-redesigning-for-postgresql-19</guid><description>A disciplined way to test PostgreSQL 19 Beta 4 against real read and write patterns before deciding whether its behavior warrants a data-model change.</description><pubDate>Fri, 02 Oct 2026 16:27:17 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-beta-is-a-test-target-not-a-design-brief&quot;&gt;A beta is a test target, not a design brief&lt;/h2&gt;
&lt;p&gt;PostgreSQL announced Beta 4 on September 24 2026, giving teams a timely chance to try the next major release against their own workloads (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386&quot;&gt;release announcement&lt;/a&gt;). I would treat that as an invitation to gather evidence, not as a reason to start redesigning a schema around a feature headline.&lt;/p&gt;
&lt;p&gt;A database model is a set of trade-offs between how data is stored and how the application needs to read and change it. A new release may alter those trade-offs, but the relevant question is not whether a capability sounds useful. It is whether it changes the cost, complexity, or behavior of the operations this system actually performs.&lt;/p&gt;
&lt;p&gt;That question requires more than running a generic benchmark or a handful of queries in a development database. I want a test that reflects the application’s access patterns closely enough to reveal useful differences, while remaining isolated from production. PostgreSQL’s beta guidance says beta and release-candidate versions are pre-release testing versions, not for production use, and warns that they may contain serious bugs or changes that are incompatible with earlier behavior (&lt;a href=&quot;https://www.postgresql.org/developer/beta&quot;&gt;beta information&lt;/a&gt;). That makes the boundary clear: explore and test; do not make a beta part of a production plan.&lt;/p&gt;
&lt;h2 id=&quot;start-with-the-workload-you-need-to-understand&quot;&gt;Start with the workload you need to understand&lt;/h2&gt;
&lt;p&gt;I would begin by listing the reads and writes that define the system’s important data paths. Include more than the idealized query shape. For reads, note the filters, joins, ordering, pagination, and the amount of data returned. For writes, include inserts, updates, deletes, constraints, and any transactions that combine several operations. A model that looks efficient for one isolated read may behave differently when those reads compete with writes or when multiple transactions touch the same data.&lt;/p&gt;
&lt;p&gt;Then choose representative cases. “Representative” does not mean every query in the application; it means the cases that exercise different access patterns and matter to the design decision. If one path looks up a record by a key and another gathers a filtered set for a screen, test both. If writes arrive in batches as well as individually, include both shapes. Preserve meaningful data distribution: an even spread of values can hide behavior that appears when a few values are much more common than others.&lt;/p&gt;
&lt;p&gt;I would also keep the workload definition separate from the result. Record the input data shape, the SQL or application operation, transaction boundaries, concurrency assumptions, and the measures I intend to compare. That makes it possible to repeat the test after changing the schema or test setup without quietly changing the question at the same time.&lt;/p&gt;
&lt;p&gt;A tool such as &lt;code&gt;pgbench&lt;/code&gt; can run repeated transactions, including custom SQL scripts, but its default workload is a simplified benchmark rather than a description of any particular application. The PostgreSQL documentation explains that custom transaction scripts can be used for other cases (&lt;a href=&quot;https://www.postgresql.org/docs/current/pgbench.html&quot;&gt;&lt;code&gt;pgbench&lt;/code&gt; documentation&lt;/a&gt;). I would use it when a script captures the behavior I need to study, not treat its default result as evidence that my own workload will improve.&lt;/p&gt;
&lt;h2 id=&quot;compare-the-same-question-on-both-versions&quot;&gt;Compare the same question on both versions&lt;/h2&gt;
&lt;p&gt;I would run the same schema, data shape, and workload against the current PostgreSQL version and the beta. Keep the relevant environment as consistent as possible: hardware, configuration, connection behavior, and test method. If those differ, a measured change may be telling me about the test setup rather than the database version.&lt;/p&gt;
&lt;p&gt;For each case, look beyond a single headline result. I would compare query plans, latency distributions, throughput under the intended concurrency, error behavior, and resource use. I would also repeat runs and record the conditions. A faster result that appears once but disappears on repetition is not a basis for changing a data model. Neither is an improvement in one read path if the corresponding write path gets materially worse.&lt;/p&gt;
&lt;p&gt;This is where a workload-specific test is more useful than a feature tour. A feature can be technically impressive and still have no bearing on the queries that dominate a particular system. Conversely, a change that looks modest in a release summary may matter if it improves a frequently used operation without imposing a cost elsewhere. The test should answer the architectural question, not reward the feature for existing.&lt;/p&gt;
&lt;p&gt;If a result is surprising, I would reduce the test to the smallest reproducible case before drawing a conclusion. Check that the input data and workload are what I intended, inspect the plan, and rerun under controlled conditions. A beta is also an opportunity to report a reproducible problem to the project; PostgreSQL’s Beta 4 announcement links to its issue and bug-reporting information (&lt;a href=&quot;https://www.postgresql.org/about/news/postgresql-19-beta-4-released-3386&quot;&gt;release announcement&lt;/a&gt;). A surprising observation is a lead to investigate, not automatically a property to design around.&lt;/p&gt;
&lt;h2 id=&quot;let-evidence-earn-a-model-change&quot;&gt;Let evidence earn a model change&lt;/h2&gt;
&lt;p&gt;I would only consider changing a model when the test connects an observed behavior to a real workload need. If a different representation improves the important reads but makes writes, constraints, or application logic harder, that is a trade-off to evaluate, not a free win. If the result depends on a narrow synthetic case that the application rarely produces, it should carry little weight in the design decision.&lt;/p&gt;
&lt;p&gt;The comparison also needs a reason to change beyond “the new version can do this.” A schema change has costs: migration, compatibility, operational understanding, and the need to keep future code aligned with the new representation. Those costs may be justified, but a beta experiment should first show a credible benefit in the workload that motivated the experiment. If it does not, I would keep the existing model and revisit the question when there is better evidence.&lt;/p&gt;
&lt;p&gt;I would record what was tested, what changed between versions, and what remains unknown. That makes the result useful even when the answer is to do nothing. It also keeps a promising early observation from becoming an unexamined assumption in a migration plan.&lt;/p&gt;
&lt;p&gt;PostgreSQL 19 Beta 4 is a reason to test against real access patterns, not a reason to redesign before testing. Run representative reads and writes in an isolated environment, compare like with like, and change the model only when the workload evidence makes the trade-off worthwhile.&lt;/p&gt;</content:encoded><category>postgresql</category><category>database-design</category><category>performance-testing</category></item><item><title>Dual-exporting .NET metrics without making it permanent</title><link>https://vahidinasab.com/articles/dual-exporting-dotnet-metrics-without-making-it-permanent</link><guid isPermaLink="true">https://vahidinasab.com/articles/dual-exporting-dotnet-metrics-without-making-it-permanent</guid><description>Learn when dual-exporting .NET metrics to Prometheus and OTLP helps a migration, what to validate, and how to retire the temporary path.</description><pubDate>Fri, 02 Oct 2026 16:27:14 GMT</pubDate><content:encoded>&lt;h2 id=&quot;two-paths-can-bridge-a-real-boundary&quot;&gt;Two paths can bridge a real boundary&lt;/h2&gt;
&lt;p&gt;A .NET service does not have to switch its metrics destination in one cutover. It can expose a Prometheus scrape endpoint for the monitoring system that already runs the service while also sending metrics over OTLP to a new backend. That is the bridge described in the &lt;a href=&quot;https://opentelemetry.io/blog/2026/dual-dotnet-metrics-export-with-otlp-and-prometheus#otlp-receiver&quot;&gt;OpenTelemetry .NET dual-exporting guide&lt;/a&gt;: configure both exporters in the application, and expose the Prometheus endpoint for scraping.&lt;/p&gt;
&lt;p&gt;I would use that arrangement when it reduces a specific migration risk. Perhaps the service team needs to keep its existing dashboards and alerts working while an observability team checks whether the new OTLP route receives the same useful signals. The old and new paths then serve different operational purposes during a transition: one keeps the current service boundary observable; the other proves the replacement path in parallel.&lt;/p&gt;
&lt;p&gt;That is different from enabling two exporters because more destinations must be better. Each route adds configuration, ownership questions, and a chance for monitoring to disagree with itself. Before I dual-export, I want to name the two systems, the people responsible for each, and the decision the temporary second route is meant to enable. If I cannot state that decision, I would keep one path.&lt;/p&gt;
&lt;h2 id=&quot;decide-where-each-copy-goes&quot;&gt;Decide where each copy goes&lt;/h2&gt;
&lt;p&gt;First draw the actual data path. In the common transition shape, Prometheus scrapes the application’s endpoint while an OTLP exporter sends to a collector or an OTLP-compatible backend. These are not two labels for the same mechanism: one is a pull from a scrape target; the other is an application-initiated export. The .NET guide shows this combined approach, and its summary describes using both systems in parallel before committing to an OpenTelemetry-compatible backend (&lt;a href=&quot;https://opentelemetry.io/blog/2026/dual-dotnet-metrics-export-with-otlp-and-prometheus&quot;&gt;guide&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Be precise about what “both” means in your topology. If OTLP goes to a collector and the collector forwards metrics to another backend, that is distinct from also pushing OTLP directly into Prometheus. Prometheus supports an OTLP receiver, but its documentation says to use OTLP ingestion cautiously for specific low-volume cases and not as a replacement for scraping (&lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/querying/api&quot;&gt;Prometheus OTLP receiver documentation&lt;/a&gt;). I would not make that receiver part of a migration by accident just because the application already has an OTLP exporter.&lt;/p&gt;
&lt;p&gt;The boundary matters because duplicate data is a routing problem as much as an instrumentation problem. If both paths land in one store, or if a collector forwards data into a system that also scrapes the service, one logical measurement may arrive twice. Even when the two copies live in separate systems, teams can mistake them for interchangeable sources and count both in a dashboard or alert. I would record the expected destination for each route and test whether any downstream component copies, scrapes, or forwards the same series again.&lt;/p&gt;
&lt;p&gt;A useful smoke test is to emit a recognizable measurement from a non-production instance, then inspect it in each destination. Confirm that it appears once where expected, with the intended service identity and dimensions. Also check what happens when one destination is unavailable: does the other path continue, and can its failure be noticed without flooding logs or causing the service to fail? Those are operational questions to answer in your own environment, not guarantees supplied by enabling two exporters.&lt;/p&gt;
&lt;h2 id=&quot;compare-meaning-not-just-names&quot;&gt;Compare meaning, not just names&lt;/h2&gt;
&lt;p&gt;The same instrument can look different after crossing the two export formats. Names may be translated, units represented differently, and attributes become labels or resource context according to the exporter and backend. The &lt;a href=&quot;https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/&quot;&gt;OpenTelemetry Prometheus compatibility specification&lt;/a&gt; describes how resource attributes are associated with scraped metrics and how attributes such as service identity relate to Prometheus labels. The practical point is to compare what your queries and alerts actually see, not assume that identical .NET instrumentation creates identical query surfaces.&lt;/p&gt;
&lt;p&gt;I would check several representative instruments: a request counter, a duration histogram, and a service-level gauge if the application has one. For each, compare the exported name, unit, type, description, and dimensions in both destinations. Confirm that one service instance can be distinguished from another, and that deployment or environment context has not been lost or turned into an unexpected label. If an alert depends on a particular label or histogram bucket, test that query against the new route before relying on it.&lt;/p&gt;
&lt;p&gt;Labels deserve particular scrutiny. A dimension that seems convenient in application code can create many distinct series when its values vary widely. During a dual-export period, that cost and its operational effect can be harder to see if the two systems have different conventions or controls. I would start with a small, intentional set of dimensions, inspect the resulting series, and avoid adding user-specific or request-specific values as labels. That is a design rule I apply because the cardinality risk grows with the combinations of values, not because two exporters somehow make a label safe.&lt;/p&gt;
&lt;h2 id=&quot;make-the-exit-part-of-the-design&quot;&gt;Make the exit part of the design&lt;/h2&gt;
&lt;p&gt;Running two paths means maintaining two configurations and two ways for data to fail. The Prometheus scrape target can be missing while OTLP continues, or OTLP can be rejected while the scrape still looks healthy. The team needs to know which system is authoritative for an operational question during the transition, where to look when signals diverge, and who owns exporter and collector changes. The OpenTelemetry guide characterizes the additional runtime overhead as minimal (&lt;a href=&quot;https://opentelemetry.io/blog/2026/dual-dotnet-metrics-export-with-otlp-and-prometheus&quot;&gt;guide&lt;/a&gt;); I would still treat “minimal” as a reason to measure rather than as a reason to ignore the extra path. Operational overhead includes configuration, access control, troubleshooting, dashboards, and alert ownership—not only CPU use.&lt;/p&gt;
&lt;p&gt;Before enabling both routes, write down the condition for removing one. It might be that the new backend has the required service coverage, the key dashboards and alerts have been checked against its data, and the people on call know where to investigate. The condition should be observable and agreed by the teams that run the service and monitoring systems. “We have been dual-exporting for a while” is not an exit criterion.&lt;/p&gt;
&lt;p&gt;Then give the temporary route an owner and review point. At that point, either retire it or record a concrete unresolved dependency and choose a new review point. Keeping both indefinitely leaves future maintainers guessing whether the duplication is intentional, whether both feeds are needed, and which one they should trust.&lt;/p&gt;
&lt;p&gt;Dual-exporting is useful when it protects a real service boundary while a replacement is tested. It is not a default architecture. I would instrument once, verify the meaning and delivery of each copy, and make the second route earn its place by helping the team reach a named migration decision.&lt;/p&gt;</content:encoded><category>dotnet</category><category>prometheus</category><category>opentelemetry</category></item><item><title>Treat Pod resizing as an operations tool, not a shortcut</title><link>https://vahidinasab.com/articles/treat-pod-resizing-as-an-operations-tool</link><guid isPermaLink="true">https://vahidinasab.com/articles/treat-pod-resizing-as-an-operations-tool</guid><description>Learn how in-place CPU and memory resizing can shorten resource tuning, and what to verify about workloads, scheduling, observability, and cluster support.</description><pubDate>Fri, 02 Oct 2026 16:27:10 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-shorter-loop-with-conditions&quot;&gt;A shorter loop, with conditions&lt;/h2&gt;
&lt;p&gt;Kubernetes 1.35 makes changing a running Pod’s CPU and memory requests and limits a stable API capability, rather than a feature to approach as an experiment. That can shorten the resource-tuning loop: adjust a workload’s allocation, observe how it behaves, and decide what to keep without automatically replacing the Pod. The Kubernetes project describes in-place Pod resize as generally avoiding a container restart, not as a guarantee that every resize is seamless or even immediately possible (&lt;a href=&quot;https://kubernetes.io/blog/2025/12/17/kubernetes-v1-35-release&quot;&gt;Kubernetes 1.35 release&lt;/a&gt;, &lt;a href=&quot;https://kubernetes.io/blog/2025/12/19/kubernetes-v1-35-in-place-pod-resize-ga&quot;&gt;in-place resize overview&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;That distinction matters operationally. Before this feature, changing a Pod’s resource requirements commonly meant changing its template and waiting for a replacement Pod. That is a reasonable rollout mechanism, but it mixes two questions: did the new resource allocation help, and did replacing the Pod affect the workload? In-place resize gives operators a way to change the allocation on a running Pod and inspect the result without making replacement the default step (&lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources&quot;&gt;Kubernetes resize documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I would use that to make tuning more deliberate, not to turn production workloads into a stream of ad hoc edits. A resize is useful only if the application responds sensibly to the resource change, the node can accommodate it, and the operator can tell whether Kubernetes actually applied it. The capability changes the mechanics of an adjustment; it does not decide what the right allocation is.&lt;/p&gt;
&lt;h2 id=&quot;first-test-the-workloads-behavior&quot;&gt;First test the workload’s behavior&lt;/h2&gt;
&lt;p&gt;CPU and memory changes do not have identical operational consequences. CPU is a compressible resource: changing its limit can change how much CPU time the container may use, while changing its request affects the amount Kubernetes accounts for when placing workloads. Memory is different. A workload may already be holding memory that exceeds a proposed lower limit, and its behavior under memory pressure depends on the application as well as the container runtime. Kubernetes exposes a per-resource &lt;code&gt;resizePolicy&lt;/code&gt; so that a team can specify whether changing CPU or memory may require the container to restart (&lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources&quot;&gt;resize documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I would test the actual application with the same policy and resource changes intended for production. Check whether it adapts to a new CPU limit while running, whether it assumes memory availability will stay fixed, and whether a restart caused by the configured policy is acceptable. A process that reads its limits only at startup may not make use of a change just because the container’s allocation changed. Treat that as a workload question to verify, rather than assuming that an API update automatically changes application behavior.&lt;/p&gt;
&lt;p&gt;A successful API request is also not proof that the resize completed. Kubernetes reports resize progress and pending conditions in Pod status, including reasons that can distinguish a deferred operation from an infeasible one. The kubelet can accept a request and still be applying it, so the operational procedure needs to wait for observed state and handle a resize that remains pending (&lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources&quot;&gt;resize status documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;This is why I would start with a controlled workload and a reversible change. Define in advance what counts as a good outcome: the application remains healthy, the requested resources are reflected in the running container, and the workload’s own signals move in the direction the team expected. If the resize policy permits a container restart, include that in the test. “In place” describes the capability, not a promise that no process will restart under any configuration.&lt;/p&gt;
&lt;h2 id=&quot;requests-still-meet-a-real-node&quot;&gt;Requests still meet a real node&lt;/h2&gt;
&lt;p&gt;The scheduler’s role is easy to misunderstand here. Resizing a Pod does not mean Kubernetes can move its existing process to a more suitable node. The Pod is already assigned to a node; a resource increase must be feasible for that placement, or the resize can remain pending or fail. The resize documentation describes pending states and node-side resource constraints, so I would treat the assigned node’s available capacity as part of the change plan, not as something solved automatically by editing the resource values (&lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources&quot;&gt;resize documentation&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;A request increase can also change how much capacity is accounted for on that node once accepted. That has consequences for the next Pod that needs to be scheduled there. A request decrease may free accounted capacity after the new allocation takes effect, but it does not relocate the current Pod or guarantee that another workload can use the freed capacity immediately. The useful mental model is local: resizing changes the resource requirements of this Pod on its current node; ordinary scheduling and placement remain separate concerns.&lt;/p&gt;
&lt;p&gt;That makes the surrounding workload policy important. A controller may keep the Pod around, while a human or automation changes its resources; later, a rollout or reconciliation can bring the Pod back in line with the template. I would decide which system owns the desired values before enabling operational resizing. If a deployment tool, autoscaler, or human operator can each write resource settings independently, the team needs a clear source of truth and a way to prevent one controller from undoing another’s change.&lt;/p&gt;
&lt;p&gt;For both managed and self-hosted clusters, I would test a modest increase and decrease under realistic node pressure. Include a case where the node has room and one where it does not. Observe whether the resize is applied, deferred, or rejected; check what happens to subsequent scheduling; and verify that the workflow has a clear recovery path. In-place resize does not remove capacity planning. It makes resource changes more flexible while leaving the underlying capacity constraint intact.&lt;/p&gt;
&lt;h2 id=&quot;make-the-change-observable-and-supported&quot;&gt;Make the change observable and supported&lt;/h2&gt;
&lt;p&gt;A safe tuning loop needs to distinguish the requested configuration from what the node has actually applied. Kubernetes exposes resize conditions and status fields for this purpose; the documentation identifies &lt;code&gt;status.containerStatuses[].resources&lt;/code&gt; as a useful view of resources reported for the running container, alongside allocation information and the Pod’s observed generation (&lt;a href=&quot;https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources&quot;&gt;resize status documentation&lt;/a&gt;). Kubernetes 1.35 also highlights new kubelet metrics and Pod events associated with resize, intended to help operators track and debug resource changes (&lt;a href=&quot;https://kubernetes.io/blog/2025/12/19/kubernetes-v1-35-in-place-pod-resize-ga&quot;&gt;Kubernetes 1.35 resize announcement&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I would put those signals in the same operational view as application health, restart counts, and resource-use trends. Alerting only on the desired spec can tell you that someone asked for a change; it cannot establish that the runtime applied it. A useful procedure records the change, watches its status through completion, and checks both the workload and the node afterward. If the request is deferred or infeasible, the next action should be clear: wait for capacity, choose a smaller change, or use a controlled replacement or rescheduling path instead.&lt;/p&gt;
&lt;p&gt;Stable upstream support is not the same as immediate support in every cluster you operate. The Kubernetes project lists the currently maintained release branches on its &lt;a href=&quot;https://kubernetes.io/releases/&quot;&gt;release page&lt;/a&gt;, but managed services choose when to offer specific versions and may have their own lifecycle and upgrade policies. For example, &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/aks/supported-kubernetes-versions&quot;&gt;AKS publishes its supported Kubernetes versions and lifecycle&lt;/a&gt;, and &lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html&quot;&gt;EKS documents version availability and support&lt;/a&gt;. In self-hosted clusters, I would check the versions and configuration of the control plane and nodes, as well as the runtime and observability setup. In managed clusters, I would confirm the provider’s version availability and any service-specific constraints rather than infer support from the upstream release alone.&lt;/p&gt;
&lt;p&gt;My adoption bar is therefore practical: the cluster actually supports the feature, the workload has been tested with its resize policy, the node has a realistic path to satisfy the change, and the team can observe completion or failure. If those checks pass, in-place resize can make tuning less disruptive and faster to learn from. If they do not, a normal rollout remains the clearer operational tool.&lt;/p&gt;</content:encoded><category>kubernetes</category><category>operations</category><category>resource-management</category></item><item><title>Separate the .NET support deadline from .NET 11 adoption</title><link>https://vahidinasab.com/articles/separate-the-dotnet-support-deadline-from-dotnet-11-adoption</link><guid isPermaLink="true">https://vahidinasab.com/articles/separate-the-dotnet-support-deadline-from-dotnet-11-adoption</guid><description>A practical way to inventory .NET services, prioritize upgrade risk, test on .NET 10 and meet the support deadline without waiting for .NET 11.</description><pubDate>Fri, 02 Oct 2026 16:26:54 GMT</pubDate><content:encoded>&lt;h2 id=&quot;the-deadline-is-a-support-decision&quot;&gt;The deadline is a support decision&lt;/h2&gt;
&lt;p&gt;On November 10, 2026, both &lt;a href=&quot;https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core&quot;&gt;.NET 8 and .NET 9 reach end of support&lt;/a&gt;. .NET 11 is currently a release candidate, with Microsoft’s launch event scheduled for November 10–13. That timing can make it tempting to wait and move straight to the newest release. I would not make the support deadline depend on that choice.&lt;/p&gt;
&lt;p&gt;There are two separate questions: how will a service remain on a supported .NET version after its current version expires, and when does it make sense to adopt .NET 11? The first has a fixed date. The second is a platform decision that should account for compatibility, dependencies, operational readiness and the team’s appetite for change.&lt;/p&gt;
&lt;p&gt;.NET 10 is already a released LTS version, and Microsoft lists its support through November 14, 2028 (&lt;a href=&quot;https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core&quot;&gt;support policy&lt;/a&gt;). That makes it a practical target for teams that need a supported landing place now. Moving to it does not prevent a later move to .NET 11. It gives that later decision its own schedule instead of turning an approaching support date into a reason to rush onto a new major release.&lt;/p&gt;
&lt;h2 id=&quot;make-the-inventory-useful&quot;&gt;Make the inventory useful&lt;/h2&gt;
&lt;p&gt;I would start with an inventory of deployable applications, not just a list of repositories. A repository can contain several services, workers, scheduled jobs or applications with different release and runtime requirements. The inventory should let someone answer, for each production workload:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What target framework and runtime does it use?&lt;/li&gt;
&lt;li&gt;Where does it run, and how is it deployed?&lt;/li&gt;
&lt;li&gt;Which libraries, SDKs, hosting components and external integrations does it depend on?&lt;/li&gt;
&lt;li&gt;Who owns its code and can validate it in a production-like environment?&lt;/li&gt;
&lt;li&gt;What operational or business process would be affected if the upgrade exposed a problem?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The point is not to build a perfect catalogue before taking action. It is to find work that a framework-version report alone will miss: a service whose dependency has no compatible release, a background process nobody remembers to test, or a deployment image that still carries an old runtime. Dependencies include more than NuGet packages. Build agents, container base images, runtime installations, database providers and other services in the call path can all be part of the upgrade surface.&lt;/p&gt;
&lt;p&gt;I would record uncertainty rather than mark it as resolved. “Owner unknown” and “integration not tested” are useful inventory findings; they are not reasons to quietly assume the migration will be simple. If several services share a library or build pipeline, record that relationship too. It can reveal a single fix that unblocks multiple applications, or a shared change whose failure would affect many of them.&lt;/p&gt;
&lt;h2 id=&quot;prioritize-by-risk-not-by-repository-order&quot;&gt;Prioritize by risk, not by repository order&lt;/h2&gt;
&lt;p&gt;Once the inventory exists, I would sort by risk and readiness rather than upgrade every application alphabetically. A useful first pass considers the consequence of staying on an unsupported version, the complexity of the application’s dependencies, how much of its behavior can be exercised automatically, and whether someone can verify the result.&lt;/p&gt;
&lt;p&gt;A service with a straightforward build, well-understood dependencies and a reliable test path may be a good early migration. That does not mean it is the most important service; it means it can test the path the rest of the work will use. A high-impact service with a difficult integration boundary may deserve an early assessment, but not necessarily the first production deployment. Separating assessment from rollout helps expose the hard work before the calendar is tight.&lt;/p&gt;
&lt;p&gt;I would also identify blockers explicitly: packages without a compatible version, platform-specific behavior, outdated deployment images, missing test coverage or an unavailable service owner. Then assign each blocker an action and a person responsible. “Upgrade all services” is a goal, not a plan. A list of services, known risks and decisions needed is something a team can actually manage.&lt;/p&gt;
&lt;h2 id=&quot;test-on-net-10-then-decide-about-net-11&quot;&gt;Test on .NET 10, then decide about .NET 11&lt;/h2&gt;
&lt;p&gt;For production services still on .NET 8 or .NET 9, I would create a migration branch targeting .NET 10 and use it to learn what the upgrade requires. Build and restore first, then investigate compiler and dependency warnings rather than suppressing them to get a green build. Review the &lt;a href=&quot;https://learn.microsoft.com/en-us/dotnet/core/compatibility/10&quot;&gt;documented .NET 10 breaking changes&lt;/a&gt;, and test the behaviors that matter to the service: startup, configuration, authentication, database access, messaging, background work, health checks and deployment.&lt;/p&gt;
&lt;p&gt;A successful compile is only an early signal. I would run the application’s automated tests, exercise its key integration paths, and validate its deployment using the same kind of environment and release process used in production. If a workload has no adequate tests, I would identify a small set of high-value checks before treating the framework change as safe to ship. That work is part of the migration, not optional polish.&lt;/p&gt;
&lt;p&gt;After the service is on .NET 10, decide separately whether and when to adopt .NET 11. That choice can be based on actual compatibility findings and operational needs, rather than an assumption that the newest version must be the deadline target. A team that has completed a supported move can evaluate .NET 11 on a deliberate schedule; a team that waits for its release before beginning has coupled two changes and left less room to find problems.&lt;/p&gt;
&lt;p&gt;The useful milestone is not “everyone is on the newest .NET.” It is that every production service has a supported runtime and a tested route to get there. Meet that milestone on its own schedule, then choose the next platform version on its merits.&lt;/p&gt;</content:encoded><category>dotnet</category><category>software-maintenance</category><category>upgrade-planning</category></item></channel></rss>