<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jesse Houwing</title>
    <description>The latest articles on DEV Community by Jesse Houwing (@jessehouwing).</description>
    <link>https://dev.to/jessehouwing</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F153902%2F2549a6a6-c745-4d10-b17b-3c8c7651681c.jpg</url>
      <title>DEV Community: Jesse Houwing</title>
      <link>https://dev.to/jessehouwing</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jessehouwing"/>
    <language>en</language>
    <item>
      <title>Fixing complex bugs with AI - A deepdive into the PowerShellScriptAnalyzer</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Thu, 17 Sep 2026 22:01:50 +0000</pubDate>
      <link>https://dev.to/jessehouwing/fixing-complex-bugs-with-ai-a-deepdive-into-the-powershellscriptanalyzer-38pj</link>
      <guid>https://dev.to/jessehouwing/fixing-complex-bugs-with-ai-a-deepdive-into-the-powershellscriptanalyzer-38pj</guid>
      <description>&lt;p&gt;&lt;a href="https://www.flickr.com/photos/mountainroamerimages/6458518567" rel="noopener noreferrer"&gt;Down the Rabbit Hole&lt;/a&gt; - &lt;a href="https://www.flickr.com/photos/mountainroamerimages/" rel="noopener noreferrer"&gt;Susan Dussaman&lt;/a&gt; - &lt;a href="https://creativecommons.org/licenses/by-sa/2.0/" rel="noopener noreferrer"&gt;CC BY-SA 2.0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some build failures are annoying. Others are simply impossible. Mine was the second kind.&lt;/p&gt;

&lt;p&gt;On a plain &lt;code&gt;ubuntu-latest&lt;/code&gt; runner, a recursive &lt;code&gt;Invoke-ScriptAnalyzer&lt;/code&gt; run would stop dead with an error saying that &lt;code&gt;Get-Command&lt;/code&gt; was not recognised as the name of a cmdlet. Not some obscure module. Not a typo in a script. &lt;code&gt;Get-Command&lt;/code&gt;. The single most fundamental discovery cmdlet in PowerShell, missing from PowerShell.&lt;/p&gt;

&lt;p&gt;The same workflow had been perfectly happy on PSScriptAnalyzer 1.24. The only thing that changed was the analyser version.&lt;/p&gt;

&lt;p&gt;What follows is the story of &lt;a href="https://github.com/PowerShell/PSScriptAnalyzer/pull/2206?ref=jessehouwing.net" rel="noopener noreferrer"&gt;PR #2206&lt;/a&gt;. It started as a bug fix, turned into a performance project, and nearly became a pile of AI-generated slop. Along the way, AI tooling helped me, led me in the wrong direction, and showed that getting the code ready is not the same as getting it released.&lt;/p&gt;

&lt;h2&gt;
  
  
  It starts because it is open source
&lt;/h2&gt;

&lt;p&gt;The broken workflow was mine. It was &lt;a href="https://github.com/jessehouwing/actions-semver-checker?ref=jessehouwing.net" rel="noopener noreferrer"&gt;actions-semver-checker&lt;/a&gt;, one of the small projects I maintain, and the only change in the failing pull request was the pinned analyser version. So I filed &lt;a href="https://github.com/PowerShell/PSScriptAnalyzer/issues/2205?ref=jessehouwing.net" rel="noopener noreferrer"&gt;issue #2205&lt;/a&gt; with links to both runs.&lt;/p&gt;

&lt;p&gt;This is easy to take for granted.&lt;/p&gt;

&lt;p&gt;In most of my working life, that would have been the end of my involvement. You raise a ticket. You attach logs. You wait. Somebody you will never meet triages it against priorities you cannot see, and your only remaining move is to pin to the last version that worked and hope the next release quietly fixes it. You are a spectator to a problem that is costing &lt;em&gt;you&lt;/em&gt; time.&lt;/p&gt;

&lt;p&gt;Here I could carry on. The repository is public, so I cloned it, diffed 1.24 against 1.25, and had a source-level diagnosis in the issue within twenty minutes: &lt;code&gt;AvoidAlias&lt;/code&gt; asks the engine about every non-alias command it sees; that question travels through &lt;code&gt;CommandInfoCache&lt;/code&gt;, which used a pool of child runspaces and ran &lt;code&gt;Get-Command&lt;/code&gt; inside them via &lt;code&gt;PowerShell.Create()&lt;/code&gt;; PR #2125 had recently taught that path to parse &lt;code&gt;Module\Command&lt;/code&gt; and pass &lt;code&gt;-Module&lt;/code&gt; along, and the build reference for &lt;code&gt;System.Management.Automation&lt;/code&gt; had moved from 7.4.7 to 7.4.14.&lt;/p&gt;

&lt;p&gt;So the error had a fairly ordinary cause. We were not losing &lt;code&gt;Get-Command&lt;/code&gt; from PowerShell. We were losing it from &lt;em&gt;our own child runspace&lt;/em&gt;, under concurrency, on a machine with a particular set of modules on disk.&lt;/p&gt;

&lt;p&gt;In that first write-up I was careful about what I could not prove. My repository does not use module-qualified invocations, so I could not show that #2125 alone caused it. I could only show that it was the most likely place to look. It is important to keep measurements and suspicions separate.&lt;/p&gt;

&lt;p&gt;No support ticket or NDA. I could read the code, form a theory, write a test and open a pull request. That ability to take ownership is one of the main reasons open source matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first fix was the obvious one, and it was not enough
&lt;/h2&gt;

&lt;p&gt;My first instinct was containment. Catch the transient failure, return &lt;code&gt;null&lt;/code&gt;, and let the rules follow their existing "I could not resolve that command" path. A failed lookup should not take down an entire analysis run.&lt;/p&gt;

&lt;p&gt;One detail mattered: the cache stored &lt;code&gt;Lazy&amp;lt;CommandInfo&amp;gt;&lt;/code&gt; values, and &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt; caches exceptions as well as results. One unlucky lookup would poison that entry for the rest of the process. So the fix evicted only the faulted entry rather than clearing everything.&lt;/p&gt;

&lt;p&gt;Then I went a step further and serialised all command lookups onto a single dedicated runspace behind a lock. If the engine is not thread-safe here, stop being concurrent here.&lt;/p&gt;

&lt;p&gt;That felt like the answer. It compiled, the tests went green, and the original failure disappeared.&lt;/p&gt;

&lt;p&gt;It was not the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The review that changed the question
&lt;/h2&gt;

&lt;p&gt;Twenty-six days later, one of the maintainers replied. And in a few short paragraphs he handed me a pile of context I had no way of acquiring on my own.&lt;/p&gt;

&lt;p&gt;He told me this was not the first time &lt;code&gt;CommandInfoCache&lt;/code&gt; had produced concurrency issues — there had been several, over a period, from different reporters. He told me the root cause as the maintainers understood it was PowerShell engine internals not being thread-safe, which turned my careful hedge ("I cannot prove #2125 alone triggers this") into a settled question. He pointed me at an attempt to fix it on the PowerShell side. And he mentioned, almost in passing, that he had previously seen lock-based optimisations in this area degrade performance badly by removing concurrency.&lt;/p&gt;

&lt;p&gt;I want to be fair about what I could have found on my own. Working through the 1.24-to-1.25 diff revealed links to the PowerShell runtime repository, where the underlying concurrency problems are documented.&lt;/p&gt;

&lt;p&gt;What he added was the knowledge that had not been written down. This was not a one-off. The maintainers already agreed on the likely cause. Most importantly, a previous lock-based fix had made this code much slower. It was tried and abandoned, so there was no merged change for me to find.&lt;/p&gt;

&lt;p&gt;He did not replace a day's research with five minutes of magic. He gave that research context that only a long-serving maintainer had. He told me which questions were already settled, which ideas had caused trouble before, and where the real risk was.&lt;/p&gt;

&lt;p&gt;Then he asked for one specific thing.&lt;/p&gt;

&lt;p&gt;Analyse PowerShell's own &lt;code&gt;build.psm1&lt;/code&gt; in a freshly started shell, then immediately again in that same shell. Do it for my branch and for the implementation in 1.25. Post the numbers.&lt;/p&gt;

&lt;p&gt;Those two measurements matter because PSScriptAnalyzer gets used in two very different ways, and the rest of this story hangs on the distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In a pipeline, it runs once.&lt;/strong&gt; A GitHub Actions job starts a runner, installs the module, runs &lt;code&gt;Invoke-ScriptAnalyzer -Recurse&lt;/code&gt;, and exits. Nothing is cached or warmed up. If it takes thirteen seconds instead of three, every contributor pays those ten seconds on every push and pull request. If it fails, the check is red and somebody has to find out why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In an editor, it keeps running.&lt;/strong&gt; The PowerShell extension in VS Code keeps the analyser alive in a background process and runs it again as you type. The first analysis after opening a workspace is a one-off start-up cost. After that, the caches are hot and .NET has optimised the busy code paths. Those later runs need to be fast and consistent, without warnings appearing and disappearing between keystrokes.&lt;/p&gt;

&lt;p&gt;So the benchmark measures both situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first run in a fresh process&lt;/strong&gt; stands in for the CI job. It is the number most users experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The immediate repeat run in the same process&lt;/strong&gt; stands in for the editor. It shows what happens once everything is populated and optimised.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two numbers can move in opposite directions. Aggressive caching makes the second look good at the expense of the first. Serialising everything behind a lock — my fix — protected correctness but made the first run much slower. Non-determinism is also easy to miss in timings: in an editor, it becomes a warning that appears and disappears while you work.&lt;/p&gt;

&lt;p&gt;Keep that trap in mind, because I walked straight into it later on and very nearly shipped the result.&lt;/p&gt;

&lt;p&gt;(In the pull request I call these the cold and warm runs.)&lt;/p&gt;

&lt;p&gt;He had seen this type of fix go wrong before and asked for the cheapest experiment that would settle the question.&lt;/p&gt;

&lt;p&gt;At that point the pull request stopped being "can we make it stop crashing" and became "can we make it stop crashing at a cost anyone would accept". The information that changed the work was not in the repository. It came from a person.&lt;/p&gt;

&lt;p&gt;The review was the most valuable input I received in the entire project. A maintainer is not an obstacle between you and a merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a benchmark
&lt;/h2&gt;

&lt;p&gt;So I built a benchmark. Not a stopwatch in a script — an isolated GitHub Actions matrix across Ubuntu and Windows, analysing two real projects (the PowerShell repository itself, and &lt;code&gt;actions-semver-checker&lt;/code&gt;, the very repository that had started all this), timing both the fresh-process run and the immediate repeat, and taking a SHA256 fingerprint of the normalised findings so I could prove the analyser's &lt;em&gt;output&lt;/em&gt; had not changed while its timings did.&lt;/p&gt;

&lt;p&gt;Every comparison from here on is my branch against the implementation in 1.25, built from source in the same job, on the same runner, analysing the same files.&lt;/p&gt;

&lt;p&gt;Then the benchmark told me my solution was bad.&lt;/p&gt;

&lt;p&gt;Analysing the PowerShell repository in a fresh process, 1.25 took 7.45 to 8.12 seconds. My branch took 25.90 to 27.54. On the semver project, 9.49–12.76 seconds became 33.55–36.80. Repeat runs in the same process were fine, even slightly better. But the fresh-process runs — the ones that represent almost every real use — were roughly three times slower.&lt;/p&gt;

&lt;p&gt;The maintainer's response was pragmatic: perhaps make it an opt-in switch, so people who need determinism can pay for it and people who need throughput can keep their concurrency.&lt;/p&gt;

&lt;p&gt;I did not want to ship that compromise. A correctness fix that you have to opt into is a correctness fix most people will never get.&lt;/p&gt;

&lt;p&gt;There was one encouraging result in that matrix. The 1.25 samples kept failing and needing reruns. Mine never did. The benchmark had also become a determinism test.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it nearly became slop
&lt;/h2&gt;

&lt;p&gt;The middle of the project is not the part people usually publish.&lt;/p&gt;

&lt;p&gt;Over a few hours on 14 September, the branch gained retry loops, telemetry counters, two build variants, per-rule exception handling and a summary job for the growing matrix. I then had to fix the matrix because a later change overwrote the results and made the retry variants indistinguishable.&lt;/p&gt;

&lt;p&gt;Every change was reasonable on its own. Copilot produced several of them quickly from a clear brief. I ran eleven separate agent tasks on my fork; eight were merged and three were closed as superseded. I would not have explored half as many directions without it.&lt;/p&gt;

&lt;p&gt;But I had built an increasingly complex system for &lt;em&gt;recovering from&lt;/em&gt; the problem, followed by another system for measuring the recovery. The diff and test suite kept growing, while none of it addressed why we were doing something dangerous in the first place.&lt;/p&gt;

&lt;p&gt;This is what I mean by a vibe-coded mess. The individual changes were not bad code and could have passed review on their own. The problem was the direction: an agent will keep adding layers unless you stop and ask a different question.&lt;/p&gt;

&lt;p&gt;The numbers did not improve enough to justify the complexity.&lt;/p&gt;

&lt;p&gt;So I deleted it. Retries, telemetry, build variants, rule-level catches — all of it came out in one commit. The repository-wide regression test, which the automated reviewer had flagged as depending on whatever happened to be in the repo, was replaced by a small, self-contained test that hammers the rules in parallel. The branch got smaller for the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking a different question
&lt;/h2&gt;

&lt;p&gt;With the scaffolding gone, I went back to instrumenting, but this time to answer one question: why are we calling into PowerShell so much?&lt;/p&gt;

&lt;p&gt;The answer was stark.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;failed&lt;/strong&gt; &lt;code&gt;Get-Command&lt;/code&gt; lookup cost around 59 ms. A hit cost around 0.8 ms. Seventy-four times more expensive, because a miss sends PowerShell off to autodiscover modules across the entire &lt;code&gt;PSModulePath&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On a real 111-file repository, 170 of 223 lookups — 76% — resolved to nothing. Those misses accounted for roughly 77% of total runtime.&lt;/li&gt;
&lt;li&gt;Priming the cache did not help. &lt;code&gt;Get-Command *&lt;/code&gt; took 78 seconds to enumerate around 116,000 commands, and later misses were no faster because PowerShell does not cache negative lookups.&lt;/li&gt;
&lt;li&gt;The cache was already shared across &lt;code&gt;Invoke-ScriptAnalyzer&lt;/code&gt; calls within a process. A second analysis in the same session performed zero lookups. The editor case was already in good shape. &lt;strong&gt;The pipeline case — one analysis, from nothing — was the whole problem.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the expensive operation was not "look up a command". It was "look up a command that does not exist". And once you frame it that way, the question becomes: which of these 170 non-existent commands should we never have asked about?&lt;/p&gt;

&lt;p&gt;That turned out to be a much better question, and the answers were not performance bugs. They were correctness bugs that happened to be slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four lookups we should never have made
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Probing &lt;code&gt;Get-&lt;/code&gt; in front of a hyphenated name.&lt;/strong&gt; &lt;code&gt;AvoidAlias&lt;/code&gt; has a sensible fallback: if it sees &lt;code&gt;process&lt;/code&gt;, it tries &lt;code&gt;Get-Process&lt;/code&gt;. But for an unrecognised &lt;code&gt;Test-ActionInput&lt;/code&gt; it produced &lt;code&gt;Get-Test-ActionInput&lt;/code&gt;, which matched none of the 116,289 commands on my machine and doubled the cost of every miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring functions the script itself defines.&lt;/strong&gt; If my script defines its own &lt;code&gt;Get-ChildItem&lt;/code&gt;, the analyser was resolving that name against the cmdlet installed on &lt;em&gt;my&lt;/em&gt; machine. &lt;code&gt;UseCorrectCasing&lt;/code&gt; could then advise me to rename a function based on a command my script deliberately shadows. Collecting &lt;code&gt;FunctionDefinitionAst&lt;/code&gt; names once per analysis fixed the advice and removed many 59 ms misses at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating member names as commands.&lt;/strong&gt; &lt;code&gt;UseShouldProcessCorrectly&lt;/code&gt; built a call graph with a vertex for every invocation member, so &lt;code&gt;$x.Substring(1)&lt;/code&gt; triggered &lt;code&gt;Get-Command Substring&lt;/code&gt;. Worse, &lt;code&gt;$dir.mkdir()&lt;/code&gt; could be &lt;em&gt;credited&lt;/em&gt; with &lt;code&gt;SupportsShouldProcess&lt;/code&gt;, because &lt;code&gt;mkdir&lt;/code&gt; happens to exist as a PowerShell function on Windows. A method call was being scored against an unrelated cmdlet. Tagging vertices reached through genuine command invocation fixed the semantics and the cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting that modules dot-source themselves.&lt;/strong&gt; A function in one library file routinely calls a function defined in a sibling file, with no import visible in either. I grouped files by directed dot-source relationship and gave each file the functions reachable through its own dot-source closure — deliberately &lt;em&gt;not&lt;/em&gt; merging entire connected components, so a test script that dot-sources one library does not inherit functions from its siblings.&lt;/p&gt;

&lt;p&gt;That last one took a 111-file module from 13.5 seconds to 2.9 in a fresh process. Repeat runs unchanged. Findings identical.&lt;/p&gt;

&lt;p&gt;Then I removed the instrumentation counters. They had done their job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete list of fixes
&lt;/h2&gt;

&lt;p&gt;Here is the full set, sorted by the kind of problem it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Threading and lifetime bugs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Concurrent use of a non-thread-safe engine surface.&lt;/strong&gt; &lt;code&gt;CommandInfoCache&lt;/code&gt; resolved commands through a &lt;em&gt;pool&lt;/em&gt; of child runspaces, each running &lt;code&gt;Get-Command&lt;/code&gt; via &lt;code&gt;PowerShell.Create()&lt;/code&gt;. PowerShell's command-discovery internals are not safe to drive this way from multiple threads. The symptoms included &lt;code&gt;CommandNotFoundException&lt;/code&gt; for &lt;code&gt;Get-Command&lt;/code&gt; itself, null reference exceptions, pipeline creation failures, even process aborts and hangs in PowerShell 5.1. The pool is replaced with one dedicated runspace behind a re-entrant lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt; exception poisoning.&lt;/strong&gt; The cache stored &lt;code&gt;Lazy&amp;lt;CommandInfo&amp;gt;&lt;/code&gt; values, and &lt;code&gt;Lazy&amp;lt;T&amp;gt;&lt;/code&gt; caches the exception just as faithfully as it caches a result. One transient failure permanently poisoned that command for the rest of the process, so a momentary race became a persistent wrong answer. Now only the faulted entry is evicted — and crucially, &lt;em&gt;genuine&lt;/em&gt; negative results are still cached, so "this command really does not exist" does not become a repeated 59 ms lookup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unsynchronised disposal.&lt;/strong&gt; The disposal paths did not take the runspace lock, so teardown could race an in-flight lookup. Both paths now lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata access escaping the lock.&lt;/strong&gt; Even with lookups serialised, rules were reaching back into the engine afterwards — resolving parameters on a &lt;code&gt;CommandInfo&lt;/code&gt; re-enters PowerShell on the caller's thread, outside the protection. Metadata reads are now serialised alongside the lookups through a &lt;code&gt;Func&amp;lt;CommandInfo, T&amp;gt;&lt;/code&gt; callback, and static export metadata is used in preference to dynamic parameter resolution where it is available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Get-Command&lt;/code&gt; resolved by unqualified name.&lt;/strong&gt; The internal call used the bare name, which is itself subject to shadowing and discovery failure. It is now fully qualified as &lt;code&gt;Microsoft.PowerShell.Core\Get-Command&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-deterministic output as a consequence.&lt;/strong&gt; All of the above combined into the symptom that bothers me most: the same input produced 154, 155 or 157 findings depending on which lookups happened to lose a race. In a pipeline that means a build that fails on a rerun of an unchanged commit. In an editor it means warnings that appear and vanish while you type. Static analysis that is not reproducible is not much use in either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The analyser could also crash.&lt;/strong&gt; This is what users saw.&lt;/p&gt;

&lt;p&gt;Your check goes red with an error saying &lt;code&gt;Get-Command&lt;/code&gt; does not exist. Nothing in that message points at the analyser's internals. You re-read your own diff and wonder whether the runner image changed, a module failed to install, or PowerShell itself is broken. Eventually you rerun the job. A fresh run succeeded between 55 and 59 times out of 100, so it may turn green without explaining anything.&lt;/p&gt;

&lt;p&gt;When that happens often enough, people pin to an older version, add &lt;code&gt;continue-on-error&lt;/code&gt;, exclude the rule, or remove the analyser from the pipeline. That is what I did first: I reverted to 1.24 and continued with other work. People rarely file an issue to say they have stopped using your tool. An unreliable check can leave you with silence and a linter nobody trusts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logic and semantic bugs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Script-defined functions did not shadow installed commands.&lt;/strong&gt; If a script defined its own &lt;code&gt;Get-ChildItem&lt;/code&gt;, the analyser resolved that name against whatever was installed on the machine running the analysis. &lt;code&gt;UseCorrectCasing&lt;/code&gt; would then recommend casing taken from a cmdlet the script deliberately overrides. The analyser was modelling the host instead of the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dot-sourced siblings were invisible.&lt;/strong&gt; The same bug at module scale. A library file calling a function defined in a sibling file — the normal way a PowerShell module composes itself — resolved against the host rather than the module. Fixed by giving each file the functions reachable through its directed dot-source closure, deliberately &lt;em&gt;without&lt;/em&gt; merging whole connected components, so a test script that dot-sources one library does not inherit unrelated siblings' functions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Member names were resolved as commands.&lt;/strong&gt; &lt;code&gt;UseShouldProcessCorrectly&lt;/code&gt; created a call-graph vertex for every invocation member, so &lt;code&gt;$x.Substring(1)&lt;/code&gt; produced a &lt;code&gt;Get-Command Substring&lt;/code&gt;. The real damage was the false negative: &lt;code&gt;$dir.mkdir()&lt;/code&gt; was credited with &lt;code&gt;SupportsShouldProcess&lt;/code&gt;, because &lt;code&gt;mkdir&lt;/code&gt; exists as a PowerShell function on Windows. A method call was silently scored against an unrelated cmdlet, and a rule violation went unreported. Only vertices reached through genuine command invocation are resolved now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Get-&lt;/code&gt; prefix probe applied to hyphenated names.&lt;/strong&gt; The fallback exists to turn &lt;code&gt;process&lt;/code&gt; into &lt;code&gt;Get-Process&lt;/code&gt;. Applied to an unresolved &lt;code&gt;Test-ActionInput&lt;/code&gt; it asked about &lt;code&gt;Get-Test-ActionInput&lt;/code&gt;, which matches none of the 116,289 discoverable commands on my machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A rule's own test fixture was passing for the wrong reason.&lt;/strong&gt; A fixture declared &lt;code&gt;SupportsShouldProcess&lt;/code&gt; and never called &lt;code&gt;ShouldProcess&lt;/code&gt;, which should be a violation. But it defined a local &lt;code&gt;Set-Service&lt;/code&gt; that was resolved as the system cmdlet, masking the result. The shadowing fix exposed that false negative in the test suite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance problems
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Failed lookups dominated the fresh-process run.&lt;/strong&gt; 59 ms for a miss against 0.8 ms for a hit, because a miss sends PowerShell autodiscovering modules across the whole &lt;code&gt;PSModulePath&lt;/code&gt;. On a real 111-file repository, 170 of 223 lookups resolved to nothing — 76% of the calls, roughly 77% of the runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every semantic fix above is also a performance fix.&lt;/strong&gt; Script-local and dot-source resolution answer from the syntax tree instead of the engine. Dropping the &lt;code&gt;Get-&lt;/code&gt; probe halves the cost of the misses that remain. Not resolving member names removes an entire category of lookup that was never meaningful. The speedup comes from &lt;em&gt;asking fewer questions&lt;/em&gt;, not from answering them faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Files are parsed once per run.&lt;/strong&gt; The dot-source pre-pass retains the syntax trees it builds rather than discarding them, so a recursive analysis no longer reparses. &lt;code&gt;-Fix&lt;/code&gt; opts out, because it rewrites each file before analysing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serialisation stopped mattering.&lt;/strong&gt; The lock cost that made fresh-process runs three times slower in September is still there — I did not remove it, it is what makes the analysis deterministic. It simply stopped being on the hot path once lookup volume collapsed.&lt;/p&gt;

&lt;p&gt;One thing is deliberately not changed: parameter and metadata re-evaluation. I assumed it was a bottleneck, but instrumentation showed around 97 lock acquisitions per pass. It was not worth optimising.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade I nearly accepted
&lt;/h2&gt;

&lt;p&gt;By this point I was pleased with the progress.&lt;/p&gt;

&lt;p&gt;The pipeline case had gone from thirteen and a half seconds to under three. That was the number I had been chasing since the maintainer's review, it was the number in the issue that started all this, and it was the number almost every user would feel. I had been staring at it for days.&lt;/p&gt;

&lt;p&gt;The editor case, meanwhile, was a couple of seconds slower on repeat runs. I started telling myself that was fine: CI pays for the first run on every push, while a second or two in a long-lived editor session is barely noticeable. A four-times improvement in CI for a small editor regression seemed acceptable.&lt;/p&gt;

&lt;p&gt;But it was the trade I had told myself not to make when I decided to measure both cases. It is easy to ignore a principle when one of the numbers looks so good.&lt;/p&gt;

&lt;p&gt;I would have had to explain that trade-off to a maintainer who had already warned me about regressions in this area. Instead of arguing it away, I investigated it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark was lying to me, twice
&lt;/h2&gt;

&lt;p&gt;Two things then made the harness itself the suspect.&lt;/p&gt;

&lt;p&gt;The repeat-run numbers looked like a regression, but the first run wrote its findings to disk &lt;em&gt;before&lt;/em&gt; the second measurement began. Garbage collection for those allocations landed on the second run's clock. Running a collection before each timed run removed this effect.&lt;/p&gt;

&lt;p&gt;The second problem was less obvious. Once the fresh-process run became three times faster, the second run was no longer a steady state. .NET starts with quick, unoptimised code and recompiles methods after enough use. That work used to fit inside a 13-second first pass. Now it continued into passes two and three, so my "fully warmed up" run was still warming up. A few discarded analyses before the measurement also removed this effect. Disabling tiered compilation confirmed the cause: in that case the second pass was already stable.&lt;/p&gt;

&lt;p&gt;The lesson applies beyond this project: &lt;strong&gt;a benchmark is software, with its own bugs.&lt;/strong&gt; Improving the product had invalidated assumptions in the measurement.&lt;/p&gt;

&lt;p&gt;Chasing the repeat-run numbers also revealed work the analyser repeated &lt;em&gt;after&lt;/em&gt; everything was cached: files were reparsed and lookups repeated. Almost all of that work also happened on the first run, so improving the editor case improved the pipeline case too.&lt;/p&gt;

&lt;p&gt;The regression was not the price of a faster first run. It showed that I had more work to remove. I did not foresee that; I found it because I was unwilling to accept the regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence reviewers can check
&lt;/h2&gt;

&lt;p&gt;My concern with AI-assisted open source contributions is not the code but the &lt;em&gt;claims&lt;/em&gt;. A pull request saying "fixes a race condition, improves performance" gives a maintainer little they can verify.&lt;/p&gt;

&lt;p&gt;So I set out to produce evidence that a reviewer could check without trusting me.&lt;/p&gt;

&lt;p&gt;The final cross-platform CI results. "First run" is a freshly started process analysing the project once; "repeat run" is a second analysis immediately afterwards in that same process.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operating system&lt;/th&gt;
&lt;th&gt;Project analysed&lt;/th&gt;
&lt;th&gt;First run, 1.25&lt;/th&gt;
&lt;th&gt;First run, this PR&lt;/th&gt;
&lt;th&gt;Repeat run, 1.25&lt;/th&gt;
&lt;th&gt;Repeat run, this PR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu&lt;/td&gt;
&lt;td&gt;PowerShell&lt;/td&gt;
&lt;td&gt;6.62 s&lt;/td&gt;
&lt;td&gt;2.80 s&lt;/td&gt;
&lt;td&gt;0.10 s&lt;/td&gt;
&lt;td&gt;0.11 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu&lt;/td&gt;
&lt;td&gt;semver checker&lt;/td&gt;
&lt;td&gt;12.92 s&lt;/td&gt;
&lt;td&gt;5.21 s&lt;/td&gt;
&lt;td&gt;1.96 s&lt;/td&gt;
&lt;td&gt;1.82 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;PowerShell&lt;/td&gt;
&lt;td&gt;8.49 s&lt;/td&gt;
&lt;td&gt;2.60 s&lt;/td&gt;
&lt;td&gt;0.10 s&lt;/td&gt;
&lt;td&gt;0.07 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;semver checker&lt;/td&gt;
&lt;td&gt;12.96 s&lt;/td&gt;
&lt;td&gt;6.52 s&lt;/td&gt;
&lt;td&gt;2.29 s&lt;/td&gt;
&lt;td&gt;2.23 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pipeline case is two to three times faster, while the editor case is unchanged or slightly better. What looked like a trade-off was work I had not removed yet.&lt;/p&gt;

&lt;p&gt;On the 111-file project across two CI attempts, 1.25 crashed 27 times. The runs that survived reported 154, 155, or &lt;em&gt;157&lt;/em&gt; findings for the same input. The proposed changes completed all 40 measurements and always produced the same fingerprint and 154 findings. One caveat: the benchmark retries 1.25's failures which also means the timings exclude its worst runs and make it look faster than it actually is.&lt;/p&gt;

&lt;p&gt;I proceeded with local stress testing, 100 freshly started processes per configuration: 1.25 succeeded 59 times out of 100 on PowerShell 7.6.6, and 55 out of 100 on Windows PowerShell 5.1. With the proposed changes applied: 100 out of 100 in both. Across 400 analyses in total, 86 runs of 1.25 died outright — 58 command-not-found errors, 22 null references, several pipeline creation errors, two process aborts and two timeouts. All of those signatures went into the PR description, because "it crashes sometimes" is an anecdote and a list of distinct failure modes is evidence.&lt;/p&gt;

&lt;p&gt;One experiment also justified deleting the retry code. In 100 freshly started PowerShell 7 sessions, 35 hit a caught failure on their first attempt. Twelve recovered later in the same session. Twenty-three kept failing until a 120-second cut-off. &lt;strong&gt;Retrying is not recovery when the failure may have corrupted the state you are retrying in.&lt;/strong&gt; Twelve recoveries out of thirty-five failures is not a reliable strategy. I only know that because I measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and test problems along the way
&lt;/h2&gt;

&lt;p&gt;This project also took many hours on things that had nothing to do with the bug but were necessary to work on it.&lt;/p&gt;

&lt;p&gt;The build pins .NET SDK 8.0.4xx, and having SDK 10 installed does not help. Building with &lt;code&gt;-PSVersion 7&lt;/code&gt; produces only the net8 output; the module root DLL is the net462 build that Windows PowerShell 5.1 loads, and it stays stale until you build &lt;code&gt;-All&lt;/code&gt;. I spent some time investigating why fixed code still failed before I understood that.&lt;/p&gt;

&lt;p&gt;My editor's built-in test runner was worse than useless here, because it runs Pester without importing the repository build — so &lt;code&gt;Invoke-ScriptAnalyzer&lt;/code&gt; silently resolved to the globally installed PSScriptAnalyzer. The symptom is beautifully misleading: your new behaviour tests fail while every control test passes. The fix is to replicate the harness and prepend &lt;code&gt;out&lt;/code&gt; to &lt;code&gt;PSModulePath&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PSUseCorrectCasing&lt;/code&gt; is a configurable rule and is off by default, so &lt;code&gt;-IncludeRule&lt;/code&gt; alone does not switch it on. For A/B comparisons, build the baseline in a &lt;code&gt;git worktree&lt;/code&gt; rather than stashing; otherwise it is easy to lose track of which build is running. Test fixtures that use &lt;code&gt;Add-Type&lt;/code&gt; must also be C# 5 compatible, because the compiler Windows PowerShell 5.1 uses rejects null-conditional operators and expression-bodied members.&lt;/p&gt;

&lt;p&gt;These detours are often called yak shaving. Copilot handled much of it: it read build errors, found the SDK mismatch and noticed the stale module. Work that might have taken my evening happened while I did something else. But each loop used tokens, and the agent focused on the immediate problem rather than the state of the machine. It installed .NET 8 into a temporary folder at least five times before I noticed and installed it system-wide in about a minute. It had solved the local problem each time without asking why it kept returning.&lt;/p&gt;

&lt;p&gt;It also changed how I worked. I could start a task, do other work, check the result and correct the direction. I steered part of this project from my phone in a school car park while waiting to collect my kids.&lt;/p&gt;

&lt;p&gt;Those detours still had to be correct. The test runner using the globally installed module could have sent me changing code that was already right. This work is mostly invisible in the finished pull request, but it is a normal part of contributing to an established project. AI can handle much of it, but somebody still has to notice when it solves the same problem five times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI helped
&lt;/h2&gt;

&lt;p&gt;Copilot was very good at bounded, well-specified work. Build an isolated benchmark matrix. Make this fixture cross-platform. Trace where these 106 unresolved lookups come from. Port this test to C# 5 syntax. With a clear brief and a way to verify the result, the agents produced work I merged with minor edits. They were also useful for the tedious middle of the investigation: another stress-test variation, readable result formatting, or broken links in a pull request description. The instrumentation that produced the 59 ms figure took minutes rather than an afternoon. That made the difference between running the experiment and deciding it was too much work.&lt;/p&gt;

&lt;p&gt;What it did not do was tell me when to stop. It did not question the four layers of retry machinery, notice that a local &lt;code&gt;Set-Service&lt;/code&gt; made a test pass for the wrong reason, or ask whether the repeat-run slowdown came from the product or the benchmark. I changed direction because the numbers looked wrong.&lt;/p&gt;

&lt;p&gt;Could an agent have got there by itself? Possibly. An agent reviewing a diff can identify these problems, and repeated review and revision can improve the result. I have not found a reliable way to know when that loop is finished. It may stop at a plausible answer or continue until it loses direction.&lt;/p&gt;

&lt;p&gt;That is why I kept steering it by hand. A few minutes deciding &lt;em&gt;where&lt;/em&gt; to look is cheaper than asking the machine to discover the direction. "Trace where these unresolved lookups come from" is targeted and verifiable. "Find out why this is slow and fix it" is open-ended and can use many tokens exploring ideas I could reject quickly. Often the human contribution is simply narrowing the search before the machine starts.&lt;/p&gt;

&lt;p&gt;This is the difference between vibe-coded code and code written with the same tools but properly reviewed. It is not the amount of code the model produced. It is whether the person directing it can ask a precise question. "Is the dot-source closure transitive, and should it be?" requires enough knowledge to understand why transitivity could be dangerous. Without that knowledge, you are left with "make it faster" and "fix the failing test", and may not be able to spot a plausible but wrong answer.&lt;/p&gt;

&lt;p&gt;Expertise can also narrow the questions you ask. I spent this project asking "why is this lookup happening?" and much less time asking "should command resolution work this way at all?" or "how would I design this today?". People familiar with a codebase tend to accept its existing shape. An agent does not, which can be annoying but also useful. Next time I want to spend some time asking the basic questions before I choose a direction.&lt;/p&gt;

&lt;p&gt;There is another limit: what an agent can know. An assistant &lt;em&gt;can&lt;/em&gt; read an issue tracker and follow links into another repository. That is how the PowerShell runtime issues behind this bug came to light.&lt;/p&gt;

&lt;p&gt;It cannot see what was never committed: an approach tried two years ago and abandoned because it halved throughput, or a conclusion a maintainer never wrote down. A repository records what survived, not everything that was attempted. A model can compare an approach with the code that exists. A maintainer can also tell you which ideas have failed before.&lt;/p&gt;

&lt;p&gt;AI made me much faster at producing possible solutions and &lt;em&gt;evidence&lt;/em&gt;. It did not decide which agent branches should survive, what to remove, or when the numbers were misleading. Without these tools, however, I would almost certainly never have done the work.&lt;/p&gt;

&lt;p&gt;This is not my job. It is a side project on somebody else's repository, fitted between client work, family and everything else. I was never going to spend three uninterrupted evenings fixing the build, writing a benchmark harness and running four hundred stress analyses by hand. I would have pinned to 1.24 and moved on. Instead I could work in twenty-minute pieces, with an agent continuing while I did something else.&lt;/p&gt;

&lt;p&gt;So the wasted tokens and repeated SDK installs need that context. The alternative was not a cleaner version of this pull request. It was no pull request, an unfixed bug, and a linter that a few more people might stop trusting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part comes next
&lt;/h2&gt;

&lt;p&gt;The technical work is nearly done. The crashes are gone, a first analysis in a fresh process is two to three times faster, and there is a benchmark anyone can rerun. Now it needs to be released.&lt;/p&gt;

&lt;p&gt;That carries risk. Three of the four main improvements are &lt;strong&gt;behavioural&lt;/strong&gt; changes to command resolution. Functions defined by a script now shadow installed cmdlets. Member invocations are no longer resolved as commands. Dot-sourced files now share their function definitions. These are fixes, but they also mean that some users will see different findings.&lt;/p&gt;

&lt;p&gt;Some of those differences are the whole point — the false &lt;code&gt;SupportsShouldProcess&lt;/code&gt; credit from &lt;code&gt;$dir.mkdir()&lt;/code&gt; should never have been there. But "we removed a false negative" reads very differently when it lands in your pipeline as a new build failure on a Friday afternoon.&lt;/p&gt;

&lt;p&gt;Then there are custom rules that I cannot see or test. I have checked the projects I can reach: internal ones, &lt;code&gt;actions-semver-checker&lt;/code&gt;, PSScriptAnalyzer's own build and test suite, and a handful of open source repositories. The results are consistent, but that is not proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  So please go and break it
&lt;/h2&gt;

&lt;p&gt;This is where I need your help.&lt;/p&gt;

&lt;p&gt;If you use PSScriptAnalyzer in CI, in your editor, or with custom rules, please run the proposed build against your code and tell me what you find. Ten minutes of your time is worth more than another day of mine because I cannot test your code myself.&lt;/p&gt;

&lt;p&gt;Build it from the pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install the .NET 8 SDK if you do not have it (on Linux/macOS: https://dot.net/download)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Microsoft.DotNet.SDK.8&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Requires the GitHub CLI&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/PowerShell/PSScriptAnalyzer.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-Location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PSScriptAnalyzer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2206&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# -All also builds the net462 output that Windows PowerShell 5.1 loads&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/build.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Configuration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Release&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-All&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nv"&gt;$proposed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;/out/PSScriptAnalyzer/1.25.0/PSScriptAnalyzer.psd1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$project&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'C:\src\your-project'&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;Then analyse your project twice, once with the version you use today and once with the proposed one, and compare the results. Use a fresh process for each so neither run can contaminate the other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$analysis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ModulePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProjectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Stop'&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$temporaryPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Import-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ModulePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Analysing with PSScriptAnalyzer &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PSScriptAnalyzer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.Version)"&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="c"&gt;# Collect everything before writing anything, so a failed run cannot leave a partial CSV.&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$findings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Invoke-ScriptAnalyzer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProjectPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$temporaryPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ItemType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nv"&gt;$findings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RuleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ScriptName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RuleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ScriptName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="n"&gt;Export-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$temporaryPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoTypeInformation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Move-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$temporaryPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;finally&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$temporaryPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$temporaryPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$details&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-String&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nx"&gt;Error.WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$details&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;New-AnalyzerSnapshot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ModulePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoProfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$analysis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ModulePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Remove-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OutputPath&lt;/span&gt;&lt;span class="s2"&gt;.tmp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PSScriptAnalyzer failed with exit code &lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="s2"&gt;. No comparison was made."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# What you get today, followed by what you get with this pull request&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-AnalyzerSnapshot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PSScriptAnalyzer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="bp"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/before.csv"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-AnalyzerSnapshot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$proposed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="bp"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/after.csv"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Anything listed here is a behaviour change worth knowing about&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Compare-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Import-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;/before.csv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Import-Csv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;/after.csv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Property&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RuleName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ScriptName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Message&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;-CustomRulePath&lt;/code&gt; and &lt;code&gt;-Settings&lt;/code&gt; to the &lt;code&gt;Invoke-ScriptAnalyzer&lt;/code&gt; call if you use them. Those are the cases I can predict least and would most like to hear about.&lt;/p&gt;

&lt;p&gt;The longer script is deliberate. When I tested a simple pipeline against &lt;code&gt;actions-semver-checker&lt;/code&gt; using my installed 1.25 version, the baseline printed &lt;code&gt;The term 'Get-Command' is not recognized&lt;/code&gt;, exited with code 1, and still wrote 48 findings to &lt;code&gt;before.csv&lt;/code&gt;. The proposed build wrote 154. The apparent 106 additions were not behaviour changes; the failed run had dropped those findings. This script collects the complete result before replacing the CSV, deletes stale output, and stops if either analysis fails.&lt;/p&gt;

&lt;p&gt;Some differences are expected: a finding may disappear because a member call is no longer resolved as a command, casing advice may stop firing on a function defined by the script, or a new &lt;code&gt;PSUseShouldProcess&lt;/code&gt; warning may reveal a previous false negative. If something else changes — a rule stops working, a custom rule throws, a finding moves without a clear reason, or the analyser crashes — please report it on &lt;a href="https://github.com/PowerShell/PSScriptAnalyzer/pull/2206?ref=jessehouwing.net" rel="noopener noreferrer"&gt;PR #2206&lt;/a&gt;. The rule name and a small example are enough; you do not need to diagnose it.&lt;/p&gt;

&lt;p&gt;An identical result is useful too. "No change on a 300-file module with four custom rules" is real evidence, and I need more of it.&lt;/p&gt;

&lt;p&gt;Being able to suggest a fix to an open source project also means taking responsibility for what the fix does to its users. I can give the maintainers evidence, explain the behaviour changes, and respond when something breaks. I cannot test code I do not have.&lt;/p&gt;

&lt;p&gt;Generating code has become cheap. Earning a maintainer's trust has not. I would not want it to.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/PowerShell/PSScriptAnalyzer/pull/2206?ref=jessehouwing.net" rel="noopener noreferrer"&gt;PR #2206&lt;/a&gt; is open at the time of writing. The benchmark workflow is on the branch, so you can run it yourself — and I would rather you did than take my word for any of the above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>githubcopilot</category>
      <category>ai</category>
      <category>debugging</category>
    </item>
    <item>
      <title>A trust checklist for GitHub Actions</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Fri, 04 Sep 2026 17:09:14 +0000</pubDate>
      <link>https://dev.to/jessehouwing/a-trust-checklist-for-github-actions-4nck</link>
      <guid>https://dev.to/jessehouwing/a-trust-checklist-for-github-actions-4nck</guid>
      <description>&lt;p&gt;Every action you reference in a workflow runs on your runner, with access to your workspace and whatever token you hand it. &lt;code&gt;uses: some-org/some-action@v3&lt;/code&gt; is an instruction to execute someone else's code inside your CI environment. It deserves at least as much scrutiny as a production dependency, and usually receives considerably less.&lt;/p&gt;

&lt;p&gt;What follows is the checklist I use when assessing whether an action can be trusted. A caveat before the list: &lt;strong&gt;this is partly a wish list.&lt;/strong&gt; Some of these attributes are present on very few actions today. Not all of them can be detected automatically or queried through an API, and several require manual review or AI assistance to establish. Treat it as a set of weighted signals rather than a pass/fail gate.&lt;/p&gt;

&lt;p&gt;The list is split into two tiers: signals that carry real weight, and signals that are worth a glance but should not be decisive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tier 1: signals that carry weight
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tooling and maintenance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The repository uses GitHub Advanced Security for Actions, ActionLint, and/or zizmor&lt;/td&gt;
&lt;td&gt;PR checks and workflow run logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The repository uses Dependabot or Renovate — for both its action references and its underlying implementation dependencies — and is current with updates&lt;/td&gt;
&lt;td&gt;Pull requests and workflow logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The repository configures a Renovate/Dependabot cooldown (minimum release age)&lt;/td&gt;
&lt;td&gt;Configuration file on the default branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The action has unit and/or integration tests&lt;/td&gt;
&lt;td&gt;Repository contents and workflow runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The repository uses StepSecurity Harden-Runner or GitHub's new egress firewall in its own workflows&lt;/td&gt;
&lt;td&gt;Workflow files in the repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On that last point: check that the firewall is not running in audit mode only. Audit mode records egress without blocking it, which is a useful first step but provides no actual containment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime and dependency hygiene
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Uses a currently supported Node handler (JavaScript actions)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses an up-to-date base image (Docker actions)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does not depend on unpinned actions, npm packages or Python packages, or on versions that cannot be controlled through inputs — no bare &lt;code&gt;npx eslint&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not always straightforward. AI-assisted review, such as Copilot code review, frequently finds these&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Does not use floating Docker image tags such as &lt;code&gt;latest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;action.yml&lt;/code&gt; for Docker actions. Harder for actions that invoke the Docker CLI internally; AI review often catches these&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;For composite actions: references pinned, locked or immutable actions&lt;/td&gt;
&lt;td&gt;Largely verifiable with GHAS for Actions, plus manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validates remotely downloaded artefacts&lt;/td&gt;
&lt;td&gt;Sometimes detectable with AI. Otherwise manual review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The unpinned-dependency category is the one I would prioritise. An action that pins its own action references but then runs &lt;code&gt;npx some-tool&lt;/code&gt; at execution time has simply moved the unpinned dependency one level down, where it is harder to see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release integrity
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Versioning is implemented correctly — semantic versioning, correctly maintained floating tags&lt;/td&gt;
&lt;td&gt;Difficult to inspect. I wrote a &lt;a href="https://github.com/jessehouwing/actions-semver-checker/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;GitHub Action and PowerShell library&lt;/a&gt; for it. A surprising number of actions have a questionable history where version numbers are concerned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses immutable releases&lt;/td&gt;
&lt;td&gt;Release metadata. A recently introduced feature; many actions still need to adopt it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has release attestation, and the attestation validates&lt;/td&gt;
&lt;td&gt;Should be attached to the GitHub release, and validation should be performed against the original source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passes an anti-virus scan&lt;/td&gt;
&lt;td&gt;You must do this yourself — GitHub does not virus-scan action releases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One detail worth knowing about attestations: they are bound to the source repository and &lt;strong&gt;cannot be transferred&lt;/strong&gt;. If your organisation mirrors actions into an internal repository, the attestation does not travel with the mirror. Validation has to happen against the original source.&lt;/p&gt;

&lt;h3&gt;
  
  
  Governance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;How to verify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Has licence, security and contributing policies&lt;/td&gt;
&lt;td&gt;Policy files on the main branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uses a licence your organisation accepts&lt;/td&gt;
&lt;td&gt;Licence file. In practice it is often sufficient that a licence is defined at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Tier 2: weaker signals
&lt;/h2&gt;

&lt;p&gt;These are worth looking at. None of them should decide the matter on its own, and each comes with a meaningful caveat.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Why it is weak&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Published on the Marketplace&lt;/td&gt;
&lt;td&gt;Several widely used actions are not, including GitHub's own CodeQL actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No dependencies with known security issues&lt;/td&gt;
&lt;td&gt;Sometimes you are simply waiting for a fix to propagate through the dependency chain. Sometimes a vulnerability does not apply to the Actions hosting model at all and can safely be ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No open security advisories for recent versions&lt;/td&gt;
&lt;td&gt;Useful, but absence of advisories is not evidence of absence of problems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A reasonable OpenSSF Scorecard score&lt;/td&gt;
&lt;td&gt;Genuinely hard to achieve. Few organisations or individuals make the effort. I attempted it for my own actions and abandoned it — a great deal of work for very little recognition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More than one maintainer&lt;/td&gt;
&lt;td&gt;Wishful thinking for many niche actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosted by an organisation rather than a personal account&lt;/td&gt;
&lt;td&gt;Same caveat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The repository has required PR checks&lt;/td&gt;
&lt;td&gt;Only visible in recent pull requests and workflow logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recent commits went through pull requests&lt;/td&gt;
&lt;td&gt;Wishful thinking for many niche actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recently updated&lt;/td&gt;
&lt;td&gt;Often unnecessary for PowerShell or Bash actions. Considerably more important for Docker and JavaScript actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verified creator badge&lt;/td&gt;
&lt;td&gt;The badge adds very little and represents no additional security guarantee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many stars&lt;/td&gt;
&lt;td&gt;Stars can be bought in bulk, and cheaply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Few open issues&lt;/td&gt;
&lt;td&gt;Some actions accumulate many issues precisely because the community is active — Renovate, for example. Others run aggressive stale-bot configurations to keep the count low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Few open pull requests&lt;/td&gt;
&lt;td&gt;Same reasoning. Pull request cycle time would be a better indicator, but it is not easy to read out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used by organisations you trust&lt;/td&gt;
&lt;td&gt;The dependents view shows which organisations use the action directly on public GitHub. A reasonable indication of who else has extended trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has a clear feedback channel (issues, community, email, support link)&lt;/td&gt;
&lt;td&gt;Indicates responsiveness rather than security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;From a well-known organisation (AWS, Microsoft, Azure, GitHub)&lt;/td&gt;
&lt;td&gt;No guarantee. Large vendors have been caught by recent supply-chain worms such as Shai-Hulud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The release is a signed commit&lt;/td&gt;
&lt;td&gt;Commit signing remains awkward in public open source, and its meaning is limited — anyone can sign a commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The repository has Copilot code review, CodeRabbit or a similar integration enabled&lt;/td&gt;
&lt;td&gt;AI offers no guarantee and is sometimes wrong, but it does often catch things that would otherwise slip past&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The recurring theme in this tier is that most of these signals measure &lt;strong&gt;popularity or project health&lt;/strong&gt; , not security. They are easy to query, which is precisely why they get over-weighted. Ease of measurement is not the same as relevance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Working through it in practice
&lt;/h2&gt;

&lt;p&gt;Not every signal costs the same to check. It helps to separate them by effort:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run automated checks on the candidate action. ✅&lt;/li&gt;
&lt;li&gt;Is there a clear disqualifier? ❓

&lt;ul&gt;
&lt;li&gt;Yes: reject the action or request changes. ❌&lt;/li&gt;
&lt;li&gt;No: continue with AI-assisted review. 🤖&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Are there findings that need confirmation? ❓

&lt;ul&gt;
&lt;li&gt;Yes: perform a manual review. 🔎&lt;/li&gt;
&lt;li&gt;No: manually review high-risk areas only. ⚠️&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Make a decision. ✅&lt;/li&gt;
&lt;li&gt;Lock the resolved commit regardless of the decision. 🔒&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Automatable or queryable:&lt;/strong&gt; presence of policy files, licence type, Node handler version, &lt;code&gt;action.yml&lt;/code&gt; contents, immutable release status, marketplace presence, advisory status, maintainer count, update recency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requires AI assistance to find reliably:&lt;/strong&gt; unpinned transitive dependencies, floating Docker tags in shell invocations, unvalidated remote downloads. These are the categories where reading every line manually is impractical and pattern matching is unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requires manual review:&lt;/strong&gt; versioning history, artefact validation logic, whether an egress firewall is actually enforcing, and the overall question of whether the action does what it claims.&lt;/p&gt;




&lt;h2&gt;
  
  
  The control that matters most
&lt;/h2&gt;

&lt;p&gt;The checklist tells you whether an action is worth trusting &lt;strong&gt;at the moment you look at it&lt;/strong&gt;. Something else has to make that assessment hold, because a floating tag can be force pushed at any time — by the maintainer, or by whoever compromises the account. The version you assessed is not necessarily the version that executes tomorrow.&lt;/p&gt;

&lt;p&gt;Three mechanisms address this, and they achieve broadly the same effect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Where it applies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Commit SHA pinning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumer side, per reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://docs.github.com/en/actions/how-tos/create-and-publish-actions/using-immutable-releases-and-tags-to-manage-your-actions-releases?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Immutable releases&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Producer side, per release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/github/gh-actions-lock/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Action lock files&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumer side, per repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three ensure the code you reviewed is the code that runs. My clear preference is for immutable releases and lock files over raw SHA pinning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immutable releases
&lt;/h3&gt;

&lt;p&gt;When a repository has immutable releases enabled, a release published with a release-specific tag such as &lt;code&gt;v1.0.0&lt;/code&gt; becomes unchangeable. The tag cannot be moved afterwards.&lt;/p&gt;

&lt;p&gt;Note that this applies to tags tied to a GitHub release. Floating convenience tags such as &lt;code&gt;v1&lt;/code&gt; or &lt;code&gt;v1.1&lt;/code&gt; — which maintainers deliberately move to the latest compatible patch — are not covered, and are not intended to be. That is precisely why consumers still need a locking mechanism of their own.&lt;/p&gt;

&lt;p&gt;For maintainers, this is one of the cheapest items in Tier 1 to adopt, and it improves the situation for every consumer at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Action lock files
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/github/gh-actions-lock/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;code&gt;gh-actions-lock&lt;/code&gt;&lt;/a&gt; is a &lt;code&gt;gh&lt;/code&gt; CLI extension, part of GitHub's Workflow Dependency Pinning effort. It produces a lockfile at &lt;code&gt;.github/workflows/actions.lock&lt;/code&gt; that pins every workflow dependency to a verified commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh extension &lt;span class="nb"&gt;install &lt;/span&gt;github/gh-actions-lock
gh actions-lock &lt;span class="c"&gt;# scan workflows, pin, write the lockfile&lt;/span&gt;
gh actions-lock &lt;span class="nt"&gt;--relock&lt;/span&gt; &lt;span class="c"&gt;# re-resolve refs that have legitimately moved&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The important point is that this is &lt;strong&gt;not merely SHA pinning with extra steps&lt;/strong&gt;. The lockfile carries verification that a bare SHA in a &lt;code&gt;uses:&lt;/code&gt; line cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Onboarded workflows enforce that every dependency is present in the lockfile, and that the locked commit is what executes on the runner.&lt;/li&gt;
&lt;li&gt;Lockfiles are verified against forgery: the SHA must exist in the refs it claims to exist in.&lt;/li&gt;
&lt;li&gt;Repository identity is recorded, and redirects or mismatches are blocked at runtime.&lt;/li&gt;
&lt;li&gt;A locked action must have a branch containing the locked commit, which makes impostor-commit attacks considerably harder.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point deserves emphasis. A raw SHA in a workflow file offers no protection against an impostor commit — a commit pushed to a fork, which remains reachable through the upstream repository's object store and therefore resolves successfully. It looks entirely legitimate in a &lt;code&gt;uses:&lt;/code&gt; line. The lockfile's reachability check is what closes that gap.&lt;/p&gt;

&lt;p&gt;Do note that the extension is a technical preview and pre-1.0; the lockfile format and flags may change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not raw SHA pinning
&lt;/h3&gt;

&lt;p&gt;SHA pinning works, and it is better than a floating tag. But as a practice it has real drawbacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It destroys readability.&lt;/strong&gt; &lt;code&gt;uses: some-org/some-action@a1b2c3d…&lt;/code&gt; conveys nothing. The version has to be carried in a trailing comment, which nothing validates and which drifts from reality over time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is per-reference.&lt;/strong&gt; Every workflow file must be maintained individually, rather than governed by one lockfile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It offers no verification.&lt;/strong&gt; As above, a SHA alone does not prove the commit belongs to the repository's history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lock file gives you the same guarantee with better ergonomics and meaningfully stronger verification. Immutable releases give you upstream guarantees that need no consumer-side maintenance at all.&lt;/p&gt;

&lt;p&gt;Whichever you choose, combine it with dependency automation: let Renovate or Dependabot propose updates, configured with a cooldown period, so that a compromised release has time to be discovered before it reaches your pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using the list sensibly
&lt;/h2&gt;

&lt;p&gt;Very few actions will satisfy the whole of Tier 1. That is not a reason to discard the list; it is a reason to use it comparatively. When two actions solve the same problem, the checklist gives you a defensible basis for choosing one. When only one action solves the problem, it tells you which risks you are accepting, and where compensating controls are needed.&lt;/p&gt;

&lt;p&gt;It also gives maintainers a roadmap. Much of Tier 1 is achievable by a single maintainer with focused effort — policy files, dependency automation with a cooldown, locked references, immutable releases, an attestation on the release. That is a materially different security posture from where most actions currently sit, and it does not require an organisation behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tooling that covers part of this
&lt;/h3&gt;

&lt;p&gt;StepSecurity's &lt;a href="https://app.stepsecurity.io/github-action-advisor?ref=jessehouwing.net" rel="noopener noreferrer"&gt;GitHub Action Advisor&lt;/a&gt; implements a portion of the automatable side of this list — maintenance and release signals, dependency and pinning hygiene, policy files, advisories — and presents them as a per-action risk assessment. It also adds signals this checklist cannot easily produce on its own, such as observed runtime behaviour drawn from Harden-Runner telemetry across the organisations that use it.&lt;/p&gt;

&lt;p&gt;Organisations that have granted the StepSecurity GitHub App access to their repositories get more from it: the advisor can then evaluate the actions actually referenced across the estate, rather than only the one you happen to be looking up.&lt;/p&gt;

&lt;p&gt;It does not replace Tier 1 entirely — the AI-assisted and manual categories above remain yours to cover — but it is a reasonable way to get the queryable signals in one place instead of assembling them by hand.&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Can one agent start another? Queueing sub-tasks in GitHub Agentic Workflows</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Fri, 04 Sep 2026 16:56:32 +0000</pubDate>
      <link>https://dev.to/jessehouwing/can-one-agent-start-another-queueing-sub-tasks-in-github-agentic-workflows-3l6b</link>
      <guid>https://dev.to/jessehouwing/can-one-agent-start-another-queueing-sub-tasks-in-github-agentic-workflows-3l6b</guid>
      <description>&lt;p&gt;Consider a "dark factory" setup: a software factory with almost no people on the production line. A small number of people orchestrate the system, but agents do most of the work: planning, coding, reviewing, securing, deploying and reporting back. The goal is to define the factory itself as code.&lt;/p&gt;

&lt;p&gt;For that to work, agents need to trigger on each other's work with minimal human interaction. One agent may create an issue, another may turn it into a plan, another may implement the code, another may review or secure it, and another may prepare the deployment. The hand-off can happen through an IDE, issues, pull requests, comments, workflow dispatches or other integration points, but the operating model assumes that most work is performed by agents.&lt;/p&gt;

&lt;p&gt;That ambition quickly runs into GitHub's accountability model. GitHub has designed Copilot coding agent and Actions interactions so that certain work cannot silently cascade from one agent to another without a human or accountable identity in the loop. The visible symptom may look like an API problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /agents/repos/{owner}/{repo}/tasks
→ 404

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

&lt;/div&gt;



&lt;p&gt;The 404 is the product of deliberate interaction boundaries around identity, billing, legal accountability and human oversight. Understanding those boundaries determines which orchestration patterns are viable today and which remain unavailable.&lt;/p&gt;




&lt;h2&gt;
  
  
  The accountability rule that explains everything
&lt;/h2&gt;

&lt;p&gt;From &lt;a href="https://docs.github.com/en/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-via-the-api?ref=jessehouwing.net" rel="noopener noreferrer"&gt;GitHub's docs on the agent tasks API&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent tasks API only supports &lt;strong&gt;user-to-server&lt;/strong&gt; tokens. You can authenticate using a personal access token, an OAuth app token or a GitHub App user-to-server token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-to-server tokens, such as GitHub App installation access tokens, are not supported.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; your workflow receives automatically is an &lt;em&gt;installation access token&lt;/em&gt;. It is server-to-server, and there is no user behind it — which is precisely its purpose.&lt;/p&gt;

&lt;p&gt;So when a workflow attempts to start a cloud agent session with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, GitHub is not confused about permissions. It rejects the request because &lt;strong&gt;there is no accountable party&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That accountability matters for three concrete reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Licensing and cost.&lt;/strong&gt; A cloud agent session consumes AI Credits (AIC), and those are charged to &lt;strong&gt;the token owner&lt;/strong&gt;. With no user behind the token, there is nobody to charge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abuse prevention.&lt;/strong&gt; If a workflow token could spawn agents, any repository with a workflow could provision unlimited agent capacity without a single Copilot licence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsibility.&lt;/strong&gt; When an autonomous agent produces questionable changes, there must be an identity on the audit trail.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;gh-aw states this plainly in its own source (&lt;a href="https://github.com/github/gh-aw/blob/main/pkg/workflow/github_token.go?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;code&gt;pkg/workflow/github_token.go&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Note: The default GITHUB_TOKEN is NOT included as a fallback because it does not have&lt;/span&gt;
&lt;span class="c1"&gt;// permission to create agent sessions, assign issues to bots, or add bots as reviewers.&lt;/span&gt;

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  The distinction that matters: sub-agent vs. new agent session
&lt;/h2&gt;

&lt;p&gt;This is the misconception at the root of the problem, and it is worth stating explicitly.&lt;/p&gt;

&lt;p&gt;The two options may sound similar, but they run in different places, use different identities and carry different accountability boundaries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sub-agent&lt;/th&gt;
&lt;th&gt;Cloud agent session&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;Same runner, same job&lt;/td&gt;
&lt;td&gt;GitHub-hosted, separate lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;In-process, same run&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /agents/.../tasks&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Inherits the run's context&lt;/td&gt;
&lt;td&gt;Needs a &lt;strong&gt;user&lt;/strong&gt; token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who's accountable&lt;/td&gt;
&lt;td&gt;Whoever/whatever triggered the workflow&lt;/td&gt;
&lt;td&gt;The token owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost model&lt;/td&gt;
&lt;td&gt;Part of the run's inference&lt;/td&gt;
&lt;td&gt;AI Credits charged to the token owner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A sub-agent is comparable to a function call. A cloud agent session is comparable to delegating work to an external party.&lt;/p&gt;

&lt;p&gt;Which raises the question that resolves the whole problem: &lt;em&gt;why is a separate agent session needed at all? Why can the workflow's own sub-agent not implement the code?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It can. The agent already running in your workflow has a checked-out repository, a filesystem, and a shell. It can write the code, run the tests, and open the pull request. Spawning a second agent to perform work the first agent is already positioned to do adds indirection — and it is precisely that indirection which requires the token you do not have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 1: Do the work in-run (start here)
&lt;/h2&gt;

&lt;p&gt;This is the pattern that works today, requires no PAT for the coding path, and is most likely what you actually need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;labeled&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;copilot-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# org-billed inference, no PAT&lt;/span&gt;
&lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;copilot&lt;/span&gt;
&lt;span class="na"&gt;safe-outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create-pull-request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;add-comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Implement the labelled issue&lt;/span&gt;

Only proceed if the triggering issue carries the label &lt;span class="sb"&gt;`agentic:code-needed`&lt;/span&gt;.
&lt;span class="p"&gt;
1.&lt;/span&gt; Read the issue body and any linked design notes.
&lt;span class="p"&gt;2.&lt;/span&gt; Implement the change directly in the working tree.
&lt;span class="p"&gt;3.&lt;/span&gt; Run the test suite. If tests fail, fix and re-run — do not open a PR with red tests.
&lt;span class="p"&gt;4.&lt;/span&gt; Open a &lt;span class="gs"&gt;**draft**&lt;/span&gt; pull request describing what you changed and why.
&lt;span class="p"&gt;5.&lt;/span&gt; Comment on the originating issue linking the PR.

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

&lt;/div&gt;



&lt;p&gt;The agent edits files on the runner. gh-aw's safe-output layer takes the resulting diff, creates the branch, and opens the pull request through a controlled, permission-scoped code path — the agent itself never holds a write token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One constraint to be aware of:&lt;/strong&gt; a pull request opened with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; produces a restricted event. From the &lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Actions events reference&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When a pull request is created or updated by a workflow using &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, &lt;code&gt;pull_request&lt;/code&gt; events [...] create workflow runs that &lt;strong&gt;require approval&lt;/strong&gt;. [...] With the exception of &lt;code&gt;workflow_dispatch&lt;/code&gt; and &lt;code&gt;repository_dispatch&lt;/code&gt;, other &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;-triggered events &lt;strong&gt;do not create workflow runs at all&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So your CI may wait for manual approval. This is the same accountability principle in a different form. If you need CI to run automatically, use &lt;code&gt;repository_dispatch&lt;/code&gt;, or configure gh-aw's CI-trigger token with a PAT.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 2: Two-phase with a gate
&lt;/h2&gt;

&lt;p&gt;If you genuinely want plan-then-implement as separate runs, split them across an artifact GitHub can observe — an issue, a label, a comment.&lt;/p&gt;

&lt;p&gt;One practical shape is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A schedule, issue or other trigger starts a planner workflow.&lt;/li&gt;
&lt;li&gt;The planner workflow creates or updates an issue and applies a label such as &lt;code&gt;agentic:code-needed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A gate is applied, either by a human or by a separate approval signal such as &lt;code&gt;agentic:approved&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The builder workflow starts on the approved label or dispatch event.&lt;/li&gt;
&lt;li&gt;The builder workflow's own agent or sub-agent implements the change and runs tests.&lt;/li&gt;
&lt;li&gt;The workflow opens a pull request using &lt;code&gt;create-pull-request&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# planner.md&lt;/span&gt;
&lt;span class="na"&gt;safe-outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create-issue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;agentic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;code-needed&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;


&lt;span class="c1"&gt;# builder.md&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;labeled&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Same caveat: an issue created by &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; won't trigger the &lt;code&gt;issues.labeled&lt;/code&gt; run. Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have a human apply the approval label — often exactly the gate you want anyway.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;repository_dispatch&lt;/code&gt; from the planner.&lt;/li&gt;
&lt;li&gt;Give the planner's safe output a PAT so the created issue carries a user identity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that the third option reintroduces the human implicitly: the PAT owner &lt;em&gt;is&lt;/em&gt; the accountable party, having pre-authorised the work rather than approving each instance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 3: Spawning a cloud agent session (supported, with a PAT)
&lt;/h2&gt;

&lt;p&gt;If you require a genuinely independent agent session, gh-aw supports it. The requirement is an explicit user identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;safe-outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create-agent-session&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;# default 1, maximum 10&lt;/span&gt;
    &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_AW_AGENT_TOKEN }}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Or, for existing issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;safe-outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;assign-to-agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;triggering&lt;/span&gt;
    &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_AW_AGENT_TOKEN }}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The token requirements, per gh-aw's &lt;a href="https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/copilot-cloud-agent.mdx?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Copilot Cloud Agent reference&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fine-grained PAT&lt;/strong&gt; — repository permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read: &lt;strong&gt;metadata&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Read &amp;amp; write: &lt;strong&gt;actions&lt;/strong&gt; , &lt;strong&gt;contents&lt;/strong&gt; , &lt;strong&gt;issues&lt;/strong&gt; , &lt;strong&gt;pull requests&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Classic PAT&lt;/strong&gt; — the &lt;code&gt;repo&lt;/code&gt; scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh aw secrets &lt;span class="nb"&gt;set &lt;/span&gt;GH_AW_AGENT_TOKEN &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-pat&amp;gt;"&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GH_AW_AGENT_TOKEN&lt;/code&gt; is a &lt;em&gt;magic secret&lt;/em&gt; — gh-aw looks for it by name, so you don't reference it in frontmatter. The resolution chain the compiler emits is &lt;code&gt;GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN&lt;/code&gt;, and that last fallback is the one that 404s.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A GitHub App is not an alternative here.&lt;/strong&gt; gh-aw's documentation is explicit: &lt;em&gt;"The Copilot assignment API requires a Personal Access Token [...] GitHub App installation tokens are rejected regardless of permissions."&lt;/em&gt; An App is server-to-server, so the same constraint applies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This pattern works, but consider what it implies. A PAT is a &lt;strong&gt;standing delegation of a specific person's identity, Copilot entitlement, and AI Credit balance&lt;/strong&gt;. The "no human in the loop" claim becomes "one person pre-authorised everything, indefinitely, until the token expires." That is a legitimate architecture, but it should not be mistaken for having removed the human.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pattern 4: Fan-out inside a single run
&lt;/h2&gt;

&lt;p&gt;For parallelism without spawning sessions, keep the work inside the run. gh-aw caps every safe output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;safe-outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;create-issue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
  &lt;span class="na"&gt;create-agent-session&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="c1"&gt;# each still needs the PAT&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Your workflow agent can analyse, decide, and emit up to &lt;code&gt;max&lt;/code&gt; outputs from a single run. The bound is deliberate: an agent able to spawn agents without limit is a fork bomb with a cost attached.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Supported today
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent writes code and opens a PR in one run&lt;/td&gt;
&lt;td&gt;✅ No PAT needed for the code path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-agents / custom agents inside a run&lt;/td&gt;
&lt;td&gt;✅ Same runner, same job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-workflow chains via issues/labels/comments&lt;/td&gt;
&lt;td&gt;✅ Subject to the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; event rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spawning cloud agent sessions&lt;/td&gt;
&lt;td&gt;✅ With a user PAT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assigning Copilot to issues/PRs&lt;/td&gt;
&lt;td&gt;✅ With a user PAT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-repo agent sessions&lt;/td&gt;
&lt;td&gt;✅ Explicit &lt;code&gt;target-repo&lt;/code&gt; / &lt;code&gt;allowed-repos&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Org-billed inference, zero PATs&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;permissions: copilot-requests: write&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Not supported
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud agent session with plain &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ Server-to-server rejected by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub App installation token for agent tasks or assignment&lt;/td&gt;
&lt;td&gt;❌ Same reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Org billing as a substitute for user identity&lt;/td&gt;
&lt;td&gt;❌ Covers &lt;strong&gt;inference&lt;/strong&gt; , not &lt;strong&gt;spawning&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unbounded self-replicating agents&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;max&lt;/code&gt; caps, hard ceiling of 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent workflow chain reactions on &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; events&lt;/td&gt;
&lt;td&gt;❌ Only &lt;code&gt;workflow_dispatch&lt;/code&gt; / &lt;code&gt;repository_dispatch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;target-repo: "*"&lt;/code&gt; on &lt;code&gt;create-agent-session&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ Explicit repos only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Inference vs. spawning: a distinction worth being precise about
&lt;/h2&gt;

&lt;p&gt;gh-aw does offer a genuine "no PAT required" path, and it is easily over-interpreted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;permissions: copilot-requests: write&lt;/code&gt; lets the &lt;strong&gt;Actions token&lt;/strong&gt; pay for Copilot inference, with the AI Credits charged against your organisation's Copilot tenant rather than any individual. No &lt;code&gt;COPILOT_GITHUB_TOKEN&lt;/code&gt;, no PAT. It requires the organisation policy &lt;strong&gt;Settings → Copilot → Policies → Copilot CLI → "Allow use of Copilot CLI billed to the organization"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a significant convenience. It also applies &lt;strong&gt;only to inference&lt;/strong&gt; — the model calls made by the agent running inside your workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot-requests: write → the agent in your workflow can THINK, AIC billed to the org ✅
copilot-requests: write → the agent in your workflow can SPAWN another agent ❌

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

&lt;/div&gt;



&lt;p&gt;Spawning still requires a user-to-server token, and the resulting session's AI Credits are charged to &lt;strong&gt;that token's owner&lt;/strong&gt; , not to the organisation tenant. I expect this to relax eventually in an enterprise context, where cost can roll up to an enterprise-level identity that is itself an accountable principal — but I am not aware of any preview flag for it today, and the API documentation gives no indication of one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to tell your agent
&lt;/h2&gt;

&lt;p&gt;Copilot performs poorly on this topic at present, largely for reasons outside its control. The surface changed recently (gh-aw renamed &lt;code&gt;create-agent-task&lt;/code&gt; to &lt;code&gt;create-agent-session&lt;/code&gt; in 0.4.0), there is very little public example code, and models fill the gaps with confident invention — such as recommending a support ticket for a feature flag that does not exist.&lt;/p&gt;

&lt;p&gt;Two measures help considerably:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clone &lt;code&gt;github/gh-aw&lt;/code&gt; and add it to your workspace.&lt;/strong&gt; Then instruct the agent: &lt;em&gt;"When in doubt about how agentic workflows function, read the actual implementation instead of making assumptions."&lt;/em&gt; (This warrants a separate article — it is the most effective technique I know for fast-moving dependencies.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Put the constraint in your instructions file.&lt;/strong&gt; Something like:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- .github/copilot-instructions.md --&amp;gt;&lt;/span&gt;
&lt;span class="gu"&gt;## Agentic workflow constraints&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`GITHUB_TOKEN`&lt;/span&gt; CANNOT create Copilot cloud agent sessions. The agent tasks API
  requires a user-to-server token. Do not suggest support tickets or feature flags.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer implementing code in-run with a sub-agent + &lt;span class="sb"&gt;`create-pull-request`&lt;/span&gt; over
  spawning a new agent session.
&lt;span class="p"&gt;-&lt;/span&gt; Only use &lt;span class="sb"&gt;`create-agent-session`&lt;/span&gt; / &lt;span class="sb"&gt;`assign-to-agent`&lt;/span&gt; when &lt;span class="sb"&gt;`GH_AW_AGENT_TOKEN`&lt;/span&gt;
  is configured, and say so explicitly.
&lt;span class="p"&gt;-&lt;/span&gt; Run &lt;span class="sb"&gt;`gh aw compile`&lt;/span&gt; before committing any workflow change.

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

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The instinct to "queue a sub-task" by spawning a new agent is inherited from human organisations, where delegation is how parallelism is achieved. In an agentic workflow it is usually an anti-pattern: you already have an agent running on a runner, with the repository checked out and a shell available. Use it.&lt;/p&gt;

&lt;p&gt;Use a cloud agent session when you genuinely need what it uniquely provides — an independent lifecycle, work that outlives your run, and its own review surface. Then treat the PAT as a requirement rather than an obstacle. It is GitHub's answer to the question of who authorised the work, and that question deserves an answer.&lt;/p&gt;




&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/copilot/how-tos/use-copilot-agents/cloud-agent/use-cloud-agent-via-the-api?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Using Copilot cloud agent via the API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows?ref=jessehouwing.net" rel="noopener noreferrer"&gt;Events that trigger workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/copilot-cloud-agent.mdx?ref=jessehouwing.net" rel="noopener noreferrer"&gt;gh-aw: Copilot Cloud Agent reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/github/gh-aw/blob/main/docs/src/content/docs/reference/safe-outputs.md?ref=jessehouwing.net" rel="noopener noreferrer"&gt;gh-aw: Safe Outputs reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/github/gh-aw?ref=jessehouwing.net" rel="noopener noreferrer"&gt;github/gh-aw&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Ground your GitHub Copilot agent in a local clone instead of letting it guess</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Fri, 04 Sep 2026 15:34:13 +0000</pubDate>
      <link>https://dev.to/jessehouwing/ground-your-github-copilot-agent-in-a-local-clone-instead-of-letting-it-guess-1f51</link>
      <guid>https://dev.to/jessehouwing/ground-your-github-copilot-agent-in-a-local-clone-instead-of-letting-it-guess-1f51</guid>
      <description>&lt;p&gt;A long debugging session lost to GitHub Agentic Workflows. Not because the tool was broken, but because the agent kept inventing how it worked. The model was not being unreasonable. It was being asked an impossible question.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;github/gh-aw&lt;/code&gt; renamed a core safe output from &lt;code&gt;create-agent-task&lt;/code&gt; to &lt;code&gt;create-agent-session&lt;/code&gt; in version 0.4.0. There are perhaps a few dozen real-world examples of the tool in public. A significant portion of what exists online describes the previous shape. An LLM asked about a topic with thin, contradictory, recently changed training data will not say "I don't know" — it will interpolate, and it will do so fluently.&lt;/p&gt;

&lt;p&gt;The remedy is straightforward: &lt;strong&gt;stop asking it to recall, and give it something to read.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The technique
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/src
git clone https://github.com/github/gh-aw.git

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

&lt;/div&gt;



&lt;p&gt;Then in VS Code: &lt;strong&gt;File → Add Folder to Workspace…&lt;/strong&gt; → pick the clone.&lt;/p&gt;

&lt;p&gt;Then, in your Copilot session:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I've added the &lt;code&gt;github/gh-aw&lt;/code&gt; repo to this workspace at &lt;code&gt;~/src/gh-aw&lt;/code&gt;. That's the actual implementation and documentation for GitHub Agentic Workflows. When you're unsure how something works, read the source there instead of assuming.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the entire technique. The agent now has grep access to ground truth.&lt;/p&gt;

&lt;p&gt;I use this regularly — for debugging VS Code extensions, for comparing a framework's actual behaviour against its documented behaviour, and for any dependency that moves faster than model training cycles. It consumes a considerable number of tokens, and it is highly effective.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is preferable to the alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Limitation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model memory&lt;/td&gt;
&lt;td&gt;Fast and free&lt;/td&gt;
&lt;td&gt;Often stale on fast-moving projects, and can be confidently wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic web search&lt;/td&gt;
&lt;td&gt;Reasonably current&lt;/td&gt;
&lt;td&gt;Finds blog posts, old documentation and partial examples; usually not the actual implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub MCP code search&lt;/td&gt;
&lt;td&gt;Searches the source repository directly, so it is much better grounded than generic web search&lt;/td&gt;
&lt;td&gt;Slower than local grep and may consume more tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local clone&lt;/td&gt;
&lt;td&gt;Exact code you depend on; greppable, cite-able and testable&lt;/td&gt;
&lt;td&gt;Requires setup and can be token-intensive if the agent reads broadly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Documentation describes intent. Source code describes behaviour. When the two disagree — and in a fast-moving repository they will — you want your agent reading the artefact that actually runs. GitHub MCP code search is a useful alternative when you do not have a local clone: it searches the repository directly, so it is better grounded than generic web search, but it is slower than local grep and may consume more tokens.&lt;/p&gt;




&lt;h2&gt;
  
  
  A worked example
&lt;/h2&gt;

&lt;p&gt;While researching the agent-session problem, I compared gh-aw's documentation against its code. Two discrepancies emerged immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discrepancy one — cross-repository support.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The safe-outputs specification says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Type: create_agent_session&lt;/strong&gt; — Cross-Repository Support: &lt;strong&gt;No&lt;/strong&gt; (same repository only)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The reference doc, and the actual Go source, say otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// pkg/workflow/create_agent_session.go&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CreateAgentSessionConfig&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BaseSafeOutputConfig&lt;/span&gt; &lt;span class="s"&gt;`yaml:",inline"`&lt;/span&gt;
    &lt;span class="n"&gt;Base&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`yaml:"base,omitempty"`&lt;/span&gt;
    &lt;span class="n"&gt;TargetRepoSlug&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`yaml:"target-repo,omitempty"`&lt;/span&gt; &lt;span class="c"&gt;// ← cross-repo&lt;/span&gt;
    &lt;span class="n"&gt;AllowedRepos&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`yaml:"allowed-repos,omitempty"`&lt;/span&gt; &lt;span class="c"&gt;// ← cross-repo&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;An agent relying on the specification document would have reported that cross-repository use was impossible. The struct definition settles the question immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discrepancy two — the API version header.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub's public docs show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;-H "X-GitHub-Api-Version: 2022-11-28"

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

&lt;/div&gt;



&lt;p&gt;gh-aw's implementation sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// actions/setup/js/create_agent_session.cjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;githubClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST /agents/repos/{owner}/{repo}/tasks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repoParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repoParts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taskDescription&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;base_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;baseBranch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-GitHub-Api-Version&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-03-10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;If you were debugging a raw &lt;code&gt;curl&lt;/code&gt; request against a preview API, that difference is decisive — and no amount of careful prompting would have surfaced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The discrepancy that mattered most.&lt;/strong&gt; The token resolution chain is not documented in prose anywhere, but the source comment sets it out in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/**
 * Token precedence:
 * 1. config["github-token"] — per-handler PAT configured in the workflow frontmatter
 * 2. GH_AW_AGENT_SESSION_TOKEN — agent token injected by the compiler
 * (evaluates to: GH_AW_AGENT_TOKEN || GH_AW_GITHUB_TOKEN || GITHUB_TOKEN)
 * 3. global github — step-level token (fallback)
 */

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

&lt;/div&gt;



&lt;p&gt;That is the answer: the silent fallback to &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;, and the precise reason a correctly written workflow returns 404 with no useful error message. A targeted grep resolved what a long prompting session could not.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bake it into your instructions
&lt;/h2&gt;

&lt;p&gt;The clone only helps if the agent actually reaches for it. Make that automatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- .github/copilot-instructions.md --&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Grounding sources&lt;/span&gt;

The &lt;span class="sb"&gt;`github/gh-aw`&lt;/span&gt; repository is available in this workspace at &lt;span class="sb"&gt;`../gh-aw`&lt;/span&gt;.
It contains the implementation and documentation for GitHub Agentic Workflows.
&lt;span class="p"&gt;
-&lt;/span&gt; When you hit a snag, or are unsure how agentic workflows behave, &lt;span class="ge"&gt;**&lt;/span&gt;read the
  implementation&lt;span class="ge"&gt;**&lt;/span&gt; rather than making assumptions.
&lt;span class="p"&gt;-&lt;/span&gt; Prefer, in order: &lt;span class="sb"&gt;`pkg/workflow/**`&lt;/span&gt; (Go compiler) →
  &lt;span class="sb"&gt;`actions/setup/js/**`&lt;/span&gt; (runtime handlers) → &lt;span class="sb"&gt;`docs/src/content/docs/**`&lt;/span&gt; →
  &lt;span class="sb"&gt;`CHANGELOG.md`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; When docs and code disagree, &lt;span class="gs"&gt;**the code wins**&lt;/span&gt; — and say so in your answer.
&lt;span class="p"&gt;-&lt;/span&gt; Cite file paths and line numbers for any claim about behaviour.
&lt;span class="p"&gt;-&lt;/span&gt; Always run &lt;span class="sb"&gt;`gh aw compile`&lt;/span&gt; before committing workflow changes.

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

&lt;/div&gt;



&lt;p&gt;The citation requirement is more valuable than it first appears. It serves two purposes: it gives you a quick way to verify a claim, and it makes fabrication considerably harder for the model. An answer citing &lt;code&gt;pkg/workflow/create_agent_session.go:12&lt;/code&gt; is much easier to check. An answer without a citation cannot be checked directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The complementary half: GitHub MCP for what a clone cannot provide
&lt;/h2&gt;

&lt;p&gt;A clone gives you code. It does not give you discussion. Issues, pull requests, discussions, and release notes — the layer that tells you whether something is a known problem — live on GitHub.&lt;/p&gt;

&lt;p&gt;Enable the GitHub MCP server and your agent can query that too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Search gh-aw issues for &lt;code&gt;create-agent-session&lt;/code&gt; 404"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What changed in gh-aw between the version I have and main?"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Are there open PRs touching the agent session token chain?"&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the undocumented reality surfaces: a maintainer explaining in an issue comment why something behaves as it does, a pull request that changed semantics recently, or a discussion thread describing the workaround most people use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing between a clone and MCP
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Preferred source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"How does X actually work?"&lt;/td&gt;
&lt;td&gt;Clone — read the source. If unavailable, GitHub MCP code search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What are the valid config fields?"&lt;/td&gt;
&lt;td&gt;Clone — read the struct/schema. If unavailable, GitHub MCP code search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Why does this fail?"&lt;/td&gt;
&lt;td&gt;Clone first, then MCP for known issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Is this a known bug?"&lt;/td&gt;
&lt;td&gt;MCP — search issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What changed recently?"&lt;/td&gt;
&lt;td&gt;Clone &lt;code&gt;CHANGELOG.md&lt;/code&gt; + &lt;code&gt;git log&lt;/code&gt;, then MCP for PRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"How do others use this?"&lt;/td&gt;
&lt;td&gt;MCP — code search across GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Is a fix coming?"&lt;/td&gt;
&lt;td&gt;MCP — open PRs and milestones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can skip the clone and point custom instructions at the repository URL, letting the agent search the repository through GitHub MCP. This is still much better grounded than generic web search, because the agent is searching the actual source repository. It is slower than a local clone, requires more remote calls, and may consume more tokens. I clone by default and use MCP for the discussion layer and for repository code search when cloning is not practical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical notes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Match the version you actually run.&lt;/strong&gt; A clone of &lt;code&gt;main&lt;/code&gt; describes the future. If you're on a release, check out the matching tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-C&lt;/span&gt; ~/src/gh-aw fetch &lt;span class="nt"&gt;--tags&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; ~/src/gh-aw checkout v0.4.0

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

&lt;/div&gt;



&lt;p&gt;Otherwise you risk implementing a feature that is not yet present in your version, which is a particularly frustrating class of bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh it deliberately.&lt;/strong&gt; Run &lt;code&gt;git pull&lt;/code&gt; before a debugging session, and note the SHA you grounded against so that it remains clear later what "the code says" referred to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope the search.&lt;/strong&gt; Left unbounded, an agent will search across a repository of ten thousand files. Direct it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Look only in &lt;code&gt;pkg/workflow/&lt;/code&gt; and &lt;code&gt;actions/setup/js/&lt;/code&gt; for how the token is resolved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Budget for the cost.&lt;/strong&gt; This is not inexpensive; you are paying tokens to read someone else's codebase. It remains considerably cheaper than a long debugging session based on an invented diagnosis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat fetched content as untrusted input.&lt;/strong&gt; Issue bodies, pull request descriptions, and README files can be influenced by third parties in public repositories. If your agent reads content that begins issuing it instructions, that is prompt injection rather than documentation. Worth being aware of even if you never encounter it.&lt;/p&gt;




&lt;h2&gt;
  
  
  When it's worth it
&lt;/h2&gt;

&lt;p&gt;Grounding is worthwhile when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The dependency changed recently&lt;/li&gt;
&lt;li&gt;Public examples are scarce&lt;/li&gt;
&lt;li&gt;Behaviour is subtle — auth chains, token precedence, compilation order&lt;/li&gt;
&lt;li&gt;Docs and reality have visibly diverged&lt;/li&gt;
&lt;li&gt;You're debugging something that "should work"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skip it for stable, well-documented libraries with many public examples. There is no need to clone React to establish what &lt;code&gt;useEffect&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;The heuristic I apply: &lt;strong&gt;if the model sounds confident but the result does not work, it is guessing.&lt;/strong&gt; That is the point at which to clone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A great deal of effort goes into prompt engineering — better phrasing, better structure, better framing. That effort is worthwhile, but a larger improvement is available before any of it: give the model the actual source of truth and instruct it to read.&lt;/p&gt;

&lt;p&gt;Most agent failures I investigate are not reasoning failures. They are retrieval failures. The model reasoned correctly over information that was wrong.&lt;/p&gt;

&lt;p&gt;You would not ask a new team member to implement against a library from memory. You would point them at the repository. Apply the same approach to your agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;the-thing-you-depend-on&amp;gt;
&lt;span class="c"&gt;# File → Add Folder to Workspace&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Then instruct it to read.&lt;/p&gt;




&lt;h3&gt;
  
  
  Quick-start checklist
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ Clone the dependency repo locally&lt;/li&gt;
&lt;li&gt;✅ Check out the tag matching your installed version&lt;/li&gt;
&lt;li&gt;✅ Add it to your VS Code workspace&lt;/li&gt;
&lt;li&gt;✅ Add a "grounding sources" section to &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;✅ Include "when in doubt, read the implementation" as an explicit instruction&lt;/li&gt;
&lt;li&gt;✅ Require file:line citations in answers&lt;/li&gt;
&lt;li&gt;✅ Enable the GitHub MCP server for issues, PRs, and discussions&lt;/li&gt;
&lt;li&gt;✅ Refresh the clone before serious debugging sessions&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>githubcopilot</category>
      <category>visualstudiocode</category>
    </item>
    <item>
      <title>Configure Azure Pipelines app in ghe.com</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Fri, 19 Jun 2026 13:50:47 +0000</pubDate>
      <link>https://dev.to/jessehouwing/configure-azure-pipelines-app-in-ghecom-43lp</link>
      <guid>https://dev.to/jessehouwing/configure-azure-pipelines-app-in-ghecom-43lp</guid>
      <description>&lt;p&gt;I recently helped a client migrate from Azure DevOps to GitHub Enterprise Managed Users with Data Residency (ghe.com for short). And as part of the migration we rewired the exiting Azure Pipelines from the old repository located on Azure Repos to the new one on GitHub.&lt;/p&gt;

&lt;p&gt;With GitHub Enterprise this is quite easy, create a new service connection in Azure DevOps and click the magic authorize button to install the app into GitHub automatically. Unfortunately no such luck in ghe.com, even though there is a special service connection type:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xbt3eymxbqwtjuiuy3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8xbt3eymxbqwtjuiuy3f.png" alt="Configure Azure Pipelines app in ghe.com" width="613" height="370"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select the GitHub Enterprise Cloud with data residency service connection type to link Azure Pipelines with ghe.com&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The screen mentions: "GitHub Organization should have azure pipelines app already installed", but there is no documentation anywhere on how to actually do that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlyaazldhtvgj98lx9bk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftlyaazldhtvgj98lx9bk.png" alt="Configure Azure Pipelines app in ghe.com" width="613" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Unfortunately, this already expects the Azure Pipelines app to be installed in the target GitHub organization&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And unfortunately, it turned out &lt;a href="https://developercommunity.azure.com/t/11051246?ref=jessehouwing.net" rel="noopener noreferrer"&gt;we weren't the first to hit this snag&lt;/a&gt;. And the error message is not very helpful either:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foj8wwihsrqv8zthajbnd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foj8wwihsrqv8zthajbnd.png" alt="Configure Azure Pipelines app in ghe.com" width="582" height="462"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The error message you receive is "Could not complete OAuth Authorization; verify you have enough permissions to perform this operation"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I reached out to my contacts in GitHub and Azure DevOps to see if they knew what to do, but unfortunately, even they did not know.&lt;/p&gt;

&lt;p&gt;I decided to play around some more and stumbled upon this well hidden url based on the url of the Azure Boards app by replacing &lt;code&gt;boards&lt;/code&gt; with &lt;code&gt;pipeline&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://{enterprise-slug}.ghe.com/apps/external-app/azure-boards
https://{enterprise-slug}.ghe.com/apps/external-app/azure-pipelines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And lo and behold, from this location you can install the Azure Pipelines app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78fq428koj6yv9fs7axb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78fq428koj6yv9fs7axb.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="437"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;By navigating directly to the hidden Azure Pipelines GitHub App installation page you can install the app&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Install&lt;/strong&gt; to and select your GitHub organization, after which the configuration of the service connection will work without a hitch:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5opbzu7sdp5h3glqo5g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5opbzu7sdp5h3glqo5g.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="335"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select the GitHub Organization you want to link to your Azure DevOps Project.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Select the repos you want to enable for Azure Pipelines:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wz1oxuc2q1wlqsl260y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wz1oxuc2q1wlqsl260y.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="696"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select which repos in GitHub should be available to Azure Pipelines&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After which you are redirected to Azure DevOps to complete the installation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff52me9286q3dglcgqscm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff52me9286q3dglcgqscm.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select the correct Azure DevOps organization and project&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you now go back into the settings of your Azure DevOps Project, you should be able to create the new service connection:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmq511c8fqoveez4pvt67.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmq511c8fqoveez4pvt67.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="354"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Create the new service connection, which should work without any issues.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Authorize&lt;/strong&gt; after which you'll be able to &lt;strong&gt;Save&lt;/strong&gt; the new service connection.&lt;/p&gt;

&lt;p&gt;To rewire a pipeline, navigate to the pipeline you want to rewire and click &lt;strong&gt;Edit&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4tyigwi6k4d84vuq222e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4tyigwi6k4d84vuq222e.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="277"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Edit your existing Azure Pipelines Definitions to rewire them to GitHub&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;⁞&lt;/strong&gt; button and then select &lt;strong&gt;Triggers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4he45e8c1wvdn7zmenhm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4he45e8c1wvdn7zmenhm.png" alt="Configure Azure Pipelines app in ghe.com" width="800" height="281"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Use the triggers editor to navigate to the classic Pipelines editor&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This brings you to the classic pipelines editor on which you can navigate to the &lt;strong&gt;YAML&lt;/strong&gt; tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpepglh9fffvh8s9p4w2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpepglh9fffvh8s9p4w2.png" alt="Configure Azure Pipelines app in ghe.com" width="799" height="297"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;In the classic Pipelines editor, navigate to the YAML tab to change the source location&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Select the &lt;strong&gt;Get Souces&lt;/strong&gt; element in the pipeline&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F55qgtt289vw6fz9ummi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F55qgtt289vw6fz9ummi2.png" alt="Configure Azure Pipelines app in ghe.com" width="799" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select the Get Sources step to rewire the pipeline to GitHub&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And rewire the pipeline to &lt;strong&gt;GitHub&lt;/strong&gt; by selecting the GitHub tile and picking the Service Connection we previously created:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy24obgye1ab1csiihs4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy24obgye1ab1csiihs4t.png" alt="Configure Azure Pipelines app in ghe.com" width="799" height="337"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After selecting GitHub, change the service connection, repository and default branch&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>azurepipelines</category>
      <category>azuredevops</category>
      <category>continuousdelivery</category>
    </item>
    <item>
      <title>Auto-assign GitHub Copilot AI Credit budget to users</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Fri, 29 May 2026 13:26:23 +0000</pubDate>
      <link>https://dev.to/jessehouwing/auto-assign-github-copilot-ai-credit-budget-to-users-5fc1</link>
      <guid>https://dev.to/jessehouwing/auto-assign-github-copilot-ai-credit-budget-to-users-5fc1</guid>
      <description>&lt;p&gt;The deadline for GitHub Copilot Usage Based Billing is approaching rapidly. And today I've been busy setting up automation to assign each user their own bucket of Copilot AIC budget.&lt;/p&gt;

&lt;p&gt;On June 1st, all organizations should get access to the User Based Budgets and Universal User Budgets features in GitHub, allowing you to setup a a single &lt;em&gt;universal&lt;/em&gt; budget for all your users as well as individual overrides which can either be higher or lower.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up universal budget
&lt;/h2&gt;

&lt;p&gt;To setup a universal budget, go to the enterprise's &lt;strong&gt;Billing &amp;amp; Licensing&lt;/strong&gt; panel, select &lt;strong&gt;Budgets and Alerts&lt;/strong&gt; in the left menu an create a new budget of type "Bundled AI Credits budget":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the UI shows "premium request" until June 1st. Budgets should automatically transfer to "AI Credits".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlgozkhexqqldagxpbiz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlgozkhexqqldagxpbiz.png" alt="Auto-assign GitHub Copilot AI Credit budget to users" width="627" height="346"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select the Bundled Premium Requests (before June 1st)/AI Credits budget (after June 1st)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then set a user level budget, but &lt;em&gt;do not select any specific user&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff84loo0kshf7lk5rqe10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff84loo0kshf7lk5rqe10.png" alt="Auto-assign GitHub Copilot AI Credit budget to users" width="640" height="661"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Select the User budget scope, but do not select a user&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finally select the default budget for all user without an individual budget override:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dac750b3matfrd68eo2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9dac750b3matfrd68eo2.png" alt="Auto-assign GitHub Copilot AI Credit budget to users" width="626" height="497"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Set the Budget amount for all users without an individual budget&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting an individual user budget
&lt;/h2&gt;

&lt;p&gt;Setting an individual user budget follows the same flow, but in step 2 you need to select a specific user to which the budget applies:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89vrensnlcvrp5uvsid0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89vrensnlcvrp5uvsid0.png" alt="Auto-assign GitHub Copilot AI Credit budget to users" width="625" height="632"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;To set an individual user's budget, select a specific user in step 2&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately you can't select a team or multiple users in this step, so each user needs their own individual budget. As you can imagine, this is a lot of work if you want to have specific control over different sets of users.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automating the problem away
&lt;/h2&gt;

&lt;p&gt;Of course my employer is one of those places that wants control over specific groups of users. And because I'm a lazy administrator by nature, I'd like to be able to leverage our existing approval infrastructure as much as possible. Most approvals in our organization are handled by EntraID group memberships and there are existing approval workflows in Entra to allow people to request and approve access to specific groups.&lt;/p&gt;

&lt;p&gt;GitHub, unfortunately, doesn't allow linking a user budget to a group or a team, so there needs to be a mapping from group membership to budgets somewhere. And I ended up putting that in a GitHub Actions workflow of course.&lt;/p&gt;

&lt;p&gt;The workflows is quite simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It pulls all copilot users from the assigned seats API.&lt;/li&gt;
&lt;li&gt;It pulls all users from EntraID&lt;/li&gt;
&lt;li&gt;It pulls all group memberships for the copilot users from EntraId&lt;/li&gt;
&lt;li&gt;It pulls all user level budgets from GitHub&lt;/li&gt;
&lt;li&gt;It compares the membership to the budget setup and either creates, updates or deletes the budget in GitHub.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And it sets the universal budget based on a preconfigured value.&lt;/p&gt;

&lt;p&gt;All of this runs in a single GitHub Actions workflow an executes a single PowerShell script.&lt;/p&gt;

&lt;p&gt;I won't share the full script here in the blog post, but I will share a few of the basic building blocks so you can implement your own logic based on your own criteria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get existing Copilot budgets from the GitHub enterprise billing API.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-ExistingBudgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$allBudgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@()&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;do&lt;/span&gt;&lt;span class="w"&gt; 
        &lt;/span&gt;&lt;span class="c"&gt;# Does not follow standard github paging, so need to manually page through the results&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/enterprises/&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="s2"&gt;/settings/billing/budgets?page=&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$allBudgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;budgets&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_next_page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$allBudgets&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Create a new individual per-user budget.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;New-IndividualBudget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$GitHubLogin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Amount&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$GitHubLogin&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BundlePricing"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_product_sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;prevent_further_usage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_alerting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;will_alert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;alert_recipients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@()&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Creating individual budget of &lt;/span&gt;&lt;span class="se"&gt;`$&lt;/span&gt;&lt;span class="nv"&gt;$Amount&lt;/span&gt;&lt;span class="s2"&gt; for user &lt;/span&gt;&lt;span class="nv"&gt;$GitHubLogin&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertTo-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Depth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enterprises/&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="nx"&gt;/settings/billing/budgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Update an existing budget amount.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Update-Budget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$BudgetId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Amount&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;budget_product_sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nx"&gt;prevent_further_usage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Updating budget &lt;/span&gt;&lt;span class="nv"&gt;$BudgetId&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="se"&gt;`$&lt;/span&gt;&lt;span class="nv"&gt;$Amount&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertTo-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Depth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enterprises/&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="nx"&gt;/settings/billing/budgets/&lt;/span&gt;&lt;span class="nv"&gt;$BudgetId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Delete a budget.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Remove-Budget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$BudgetId&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deleting budget &lt;/span&gt;&lt;span class="nv"&gt;$BudgetId&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DELETE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enterprises/&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="nx"&gt;/settings/billing/budgets/&lt;/span&gt;&lt;span class="nv"&gt;$BudgetId&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Ensure the universal budget exists and has the correct amount.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Update-UniversalBudget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$ExistingBudgets&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$universalBudget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ExistingBudgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;budget_scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multi_user_customer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;budget_product_sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$universalBudget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$universalBudget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;budget_amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Updating universal budget from &lt;/span&gt;&lt;span class="se"&gt;`$&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$universalBudget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;budget_amount&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="se"&gt;`$&lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Update-Budget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Enterprise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-BudgetId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$universalBudget&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ProductSku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Universal budget already set to &lt;/span&gt;&lt;span class="se"&gt;`$&lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Creating universal budget of &lt;/span&gt;&lt;span class="se"&gt;`$&lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;budget_amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$TargetAmount&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;budget_scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multi_user_customer"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;budget_type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BundlePricing"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;budget_product_sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ProductSku&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;prevent_further_usage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nx"&gt;budget_alerting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nx"&gt;will_alert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$false&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nx"&gt;alert_recipients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@()&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertTo-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Depth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/enterprises/&lt;/span&gt;&lt;span class="nv"&gt;$Enterprise&lt;/span&gt;&lt;span class="nx"&gt;/settings/billing/budgets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of the change from per-user premium request budget to AI credits and the additionally included credits during the promotional period from June 1st to August 31st I've added a few helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Determine the correct budget_product_sku based on the current date.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Before June 1 2026: premium_requests&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# From June 1 2026 onwards: ai_credits&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-BudgetProductSku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$AsOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$cutoverDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.DateTimeKind&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$AsOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cutoverDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"premium_requests"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai_credits"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Determine the universal budget amount based on the current date.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Until May 31 2026: $0 (no included credits)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# June 1 - August 31 2026: $70&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# September 1 2026 onwards: $39&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-UniversalBudgetAmount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$AsOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$june1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.DateTimeKind&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$sept1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.DateTimeKind&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$AsOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$june1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$AsOf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-lt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sept1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="mf"&gt;70.00&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="mf"&gt;39.00&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your own logic to link a budget to a specific user, then call this from a simple workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assign Copilot Budgets&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;15&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd&lt;/span&gt; &lt;span class="c1"&gt;# v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure CLI Login&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43&lt;/span&gt; &lt;span class="c1"&gt;#v3.0.0&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
        &lt;span class="na"&gt;allow-no-subscriptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Assign Copilot Budgets&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
         &lt;span class="s"&gt;.\assign-copilot-budget.ps1&lt;/span&gt;
      &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GH_TOKEN }}&lt;/span&gt;
      &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pwsh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need to use a Classic PAT token with at least the following scopes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manage_billing:enterprise
manage_billing:copilot
read:enterprise
read:user
user:email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>github</category>
      <category>ai</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Measuring the Value of AI</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Tue, 26 May 2026 13:16:04 +0000</pubDate>
      <link>https://dev.to/jessehouwing/measuring-the-value-of-ai-2f0k</link>
      <guid>https://dev.to/jessehouwing/measuring-the-value-of-ai-2f0k</guid>
      <description>&lt;h2&gt;
  
  
  We've deployed it everywhere. Now what?
&lt;/h2&gt;

&lt;p&gt;The honeymoon is over. After months of aggressive rollouts, enterprises across the globe have given their employees access to AI coding assistants, copilots, and chat interfaces. The credit card bills are arriving. And now the inevitable question lands on someone's desk: &lt;em&gt;What are we getting for this?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Organizations are scrambling to measure the return on their AI investment. They're building dashboards tracking token consumption, counting AI-assisted pull requests, and monitoring how many lines of code were generated versus written by hand. Some are going further — tracking tokens that led to PRs that led to production deployments, as if the pipeline from keystrokes to customer value were a simple conveyor belt.&lt;/p&gt;

&lt;p&gt;It isn't. And the uncomfortable truth is that this measurement problem isn't new at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  This isn't a new problem
&lt;/h2&gt;

&lt;p&gt;Long before AI assistants existed, businesses struggled to quantify the value of knowledge work. Agile coaches spent decades watching organizations try — and fail — to measure the output of Scrum teams. Lines of code, velocity points, features shipped: none of these correlated reliably with business outcomes.&lt;/p&gt;

&lt;p&gt;The problem is structural. As Barber (2008) demonstrated in research on value chain measurement, the further you move from tangible, physical processes toward intangible value creation, the harder measurement becomes. Software development, creative work, and strategic thinking exist almost entirely in that intangible space. Baruch Lev's seminal work &lt;em&gt;Intangibles: Management, Measurement, and Reporting&lt;/em&gt; (2000) — cited over 6,000 times — established that organizations systematically fail to account for intangible value creation, precisely because it resists the neat quantification that accounting systems demand.&lt;/p&gt;

&lt;p&gt;AI hasn't created a new measurement problem. It has amplified an existing one and made it impossible to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Traps We Keep Falling Into
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Goodhart's Law: When the Metric Becomes the Target
&lt;/h3&gt;

&lt;p&gt;"When a measure becomes a target, it ceases to be a good measure." Charles Goodhart formulated this principle in 1975 about monetary policy, but Fritz (2016) demonstrated its devastating applicability to software development. The moment lines of code became a productivity metric, developers produced more lines of code — without producing more value.&lt;/p&gt;

&lt;p&gt;We're walking into the same trap with AI. The moment "tokens consumed per PR" becomes a metric, teams will optimize for it. They'll either starve their AI of context to appear efficient, or feed it bloated prompts to appear productive, depending on which direction management rewards. Neither behavior correlates with value produced.&lt;/p&gt;

&lt;h3&gt;
  
  
  The McNamara Fallacy: Counting What's Countable
&lt;/h3&gt;

&lt;p&gt;During the Vietnam War, Secretary of Defense Robert McNamara insisted on measuring success through body counts and territory captured — metrics that were quantifiable but ultimately meaningless for determining who was winning. The McNamara Fallacy, as van Nieuwenhuizen describes it, is "prioritizing quantitative metrics while ignoring equally vital qualitative factors that resist easy measurement."&lt;/p&gt;

&lt;p&gt;Token usage is today's proxy. It tells you something is happening. It tells you resources are being consumed. It tells you absolutely nothing about whether the right things are being built, whether customers are happier, or whether your product is improving.&lt;/p&gt;

&lt;p&gt;The fallacy is seductive because the alternative — measuring qualitative outcomes — is genuinely hard. So organizations default to measuring what their billing systems already capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Serving Attribution Bias: We Believe Our Own Hype
&lt;/h3&gt;

&lt;p&gt;There's another problem lurking beneath the measurement challenge. Research by Shepperd, Malone, and others (2008) on self-serving attribution bias shows that people systematically over-attribute positive outcomes to their own efforts and under-attribute them to external factors. Libby and Rennekamp (2012) demonstrated this in forecasting contexts: after a success, people attribute it internally, leading to overconfident predictions about future performance.&lt;/p&gt;

&lt;p&gt;When a developer says "AI made me 10x more productive," they genuinely believe it. When a manager says "Our AI investment led to a 30% increase in shipping velocity," they genuinely believe it too. But these are uncontrolled observations colored by confirmation bias and the sunk-cost pressure of having championed the investment in the first place.&lt;/p&gt;

&lt;p&gt;Even when organizations attempt to estimate value &lt;em&gt;beforehand&lt;/em&gt;, those estimates are rarely tracked after the fact. The hypothesis that a feature would produce X value is tested at go/no-go time, then forgotten. Nobody goes back six months later to verify whether the value actually materialized. Estimates don't correlate with reality — they correlate with what was politically convenient to claim at funding time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shifted Filter: From Pre-Build to Post-Build
&lt;/h2&gt;

&lt;p&gt;Here's what AI has fundamentally changed about value creation in organizations: the capacity constraint used to be a natural quality filter.&lt;/p&gt;

&lt;p&gt;In the pre-AI world, building anything required convincing a team or a product owner to spend their limited time on it. Plenty of good ideas died in this filter, but the most important ones usually survived. Scarcity imposed discipline.&lt;/p&gt;

&lt;p&gt;Now that filter is evaporating. With AI-assisted development, the cost of building something has dropped dramatically. When capacity is infinite (or feels that way), everything gets built. Business stakeholders who used to have to fight for engineering time can now point agents at problems themselves.&lt;/p&gt;

&lt;p&gt;The result? The filter moves from pre-build to post-build. Instead of asking "Should we build this?" teams must now ask "Now that we built this, should we ship it?" And crucially: &lt;em&gt;most organizations have never developed the muscle for this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A/B testing, eye tracking, funnel optimization — these post-build validation techniques exist in marketing departments and growth teams. But they're almost entirely absent from internal business software development. And you can tell just by using most enterprise software.&lt;/p&gt;

&lt;p&gt;Imagine a product like SAP with infinite capacity to add even more schemas, tables, and configuration options. The complexity crisis of enterprise software isn't a resource problem — it's a judgment problem. AI removes the resource constraint without addressing the judgment gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Polishing Problem
&lt;/h2&gt;

&lt;p&gt;There's a related dysfunction: AI makes it trivially easy to generate an MVP-quality implementation of anything, but finishing things — truly finishing them — still requires human judgment, taste, and iteration.&lt;/p&gt;

&lt;p&gt;I recently rewrote a piece of software I'd been maintaining for ten years. The AI-assisted rewrite took roughly a week. Making myself &lt;em&gt;proud&lt;/em&gt; of the result took three more months of tweaks and refinements. The initial rewrite was the easy part. The value was in the polish.&lt;/p&gt;

&lt;p&gt;Organizations optimizing for velocity — for things shipped, story points earned, PRs merged, tokens consumed, features delivered — are implicitly de-prioritizing this polish. Every half-baked, AI-generated feature that ships without refinement represents a small quality-of-life tax on every user who has to interact with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Non-Technical Blind Spot
&lt;/h2&gt;

&lt;p&gt;If measuring AI value for developers is hard, measuring it for non-technical departments can sometimes be even harder. At least with engineers, we have many crude proxies, like commits and deployments. With HR, finance, and operations teams paying per-seat for AI assistants, organizations typically have &lt;em&gt;no visibility whatsoever&lt;/em&gt; into usage or impact or the ability to directly correlate AI usage to measurable productivity improvements.&lt;/p&gt;

&lt;p&gt;Yet sometimes the value story for non-technical work is actually simpler. The use of AI can enable teams to quickly automate tasks they occasionally do manually. Of which the results can multiply if the "occasionally" turns into "frequently" suddenly. I recently supported a team who applies content updates on a website once in a while, and was face with a request to update 75 pages all at once. &lt;em&gt;From word templates&lt;/em&gt;. Automating that away saves 2 weeks of manual labor, and also removes the pain of smaller batches in the future.&lt;/p&gt;

&lt;p&gt;It's the ambient, non-targeted AI features that defy measurement. The auto-complete suggestions, the email drafts, the meeting summaries. Each one might save 30 seconds. Multiply by thousands of employees and the theoretical value is enormous. But theoretical value isn't the same as &lt;em&gt;realized&lt;/em&gt; value, and nobody is checking whether those 30-second savings actually compound into better outcomes rather than simply expanding the time available for other low-value work.&lt;/p&gt;

&lt;p&gt;Plus, sometimes the devil is in the details, not the summarized conversation of a 2 hour teams conversation. The quirked eyebrow of a participant as something is said. The person dropping from the call after a comment was made...&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Measure Better
&lt;/h2&gt;

&lt;p&gt;If pure output metrics are misleading and self-reported value is biased, what alternatives exist? Here are approaches grounded in measurement science:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Measure Outcomes, Not Outputs
&lt;/h3&gt;

&lt;p&gt;Douglas Hubbard's framework from &lt;em&gt;How to Measure Anything&lt;/em&gt; (2014) argues that most "immeasurable" things can be measured — but only if you define what you actually mean by value. Stop counting tokens, PRs, and features shipped. Instead, identify the business outcomes you care about (customer retention, time-to-resolution, revenue per feature, support ticket volume) and measure &lt;em&gt;those&lt;/em&gt; over time. The AI investment is a confounding variable, not the thing you're measuring.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use Controlled Comparisons
&lt;/h3&gt;

&lt;p&gt;Rather than measuring before/after (which conflates AI impact with everything else that changed), run controlled experiments. Give some teams AI access for specific tasks and others not. Rotate. Compare outcomes, not effort. This is operationally inconvenient, which is exactly why almost nobody does it — and why everyone's "measurement" is actually just observational storytelling.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Track Value Hypothesis Closure Rates
&lt;/h3&gt;

&lt;p&gt;For every feature or project that gets built, require a falsifiable value hypothesis &lt;em&gt;before&lt;/em&gt; build starts. Then measure whether that hypothesis proved true &lt;em&gt;after&lt;/em&gt; deployment. Track the ratio of hypotheses confirmed versus falsified. If AI is helping you ship more things, but the hypothesis hit rate is falling, you're producing more waste faster. That's negative ROI regardless of what the token dashboard says.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Measure What Didn't Get Built
&lt;/h3&gt;

&lt;p&gt;One of the most important potential values of AI is &lt;em&gt;compression&lt;/em&gt; — not building more things, but building the same things with fewer resources, freeing capacity for quality, exploration, or rest. If your team is shipping the same amount as before but with less overtime, less burnout, and fewer shortcuts, that's value. It just doesn't show up in velocity charts.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Adopt the Post-Build Filter
&lt;/h3&gt;

&lt;p&gt;If AI is shifting your constraint from capacity to judgment, invest in judgment infrastructure. Feature flags, gradual rollouts, user research, usage analytics. The organizations that will get the most value from AI aren't the ones building the most — they're the ones with the tightest feedback loop between &lt;em&gt;shipping&lt;/em&gt; and &lt;em&gt;learning whether they should have shipped.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Accept Irreducible Uncertainty
&lt;/h3&gt;

&lt;p&gt;Some value is genuinely unmeasurable in the moment. How much value was produced by allowing Microsoft Word to italicize text? Probably immense — if it never had basic formatting, we'd all be using something else. But no ROI calculation could have predicted that in 1985. Some infrastructure investments only reveal their value in retrospect, through the things they enabled that nobody foresaw. Not everything needs a dashboard. Some things need trust and taste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Answer
&lt;/h2&gt;

&lt;p&gt;Companies want a clean number. "Our AI investment produced €X in value." That number doesn't exist and it won't exist, for the same reason that organizations never successfully quantified the value of email, spreadsheets, or the internet itself. These are enabling technologies whose value is diffused across every process they touch, modified by how skillfully they're used, and contingent on decisions that haven't been made yet.&lt;/p&gt;

&lt;p&gt;What you &lt;em&gt;can&lt;/em&gt; measure is cost. What you &lt;em&gt;can&lt;/em&gt; observe is whether your customers are happier, your employees are less burned out, your products are more refined, and your judgment about &lt;em&gt;what to build&lt;/em&gt; is getting sharper.&lt;/p&gt;

&lt;p&gt;The hardest pill to swallow: maybe that feature your AI-powered team just shipped in record time should never have been built in the first place. And no token dashboard in the world will tell you that.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;References:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.emerald.com/ijpdlm/article/38/9/685/389929?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;Barber, E. (2008). "How to measure the 'value' in value chains." International Journal of Physical Distribution &amp;amp; Logistics Management, 38(9), 685.&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sciencedirect.com/science/article/pii/B9780128042069000131?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;Fritz, T. (2016). "Measuring individual productivity." Perspectives on Data Science for Software Engineering. Elsevier.&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://books.google.com/books?id=IyPwAgAAQBAJ&amp;amp;ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;Hubbard, D.W. (2014). How to Measure Anything: Finding the Value of Intangibles in Business. Wiley.&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://books.google.com/books?id=Xgx4EQAAQBAJ&amp;amp;ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;Lev, B. (2000). Intangibles: Management, Measurement, and Reporting. Brookings Institution Press.&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1475-679X.2011.00430.x?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;Libby, R. &amp;amp; Rennekamp, K. (2012). "Self-serving attribution bias, overconfidence, and the issuance of management forecasts." Journal of Accounting Research.&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://compass.onlinelibrary.wiley.com/doi/abs/10.1111/j.1751-9004.2008.00078.x?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;Shepperd, J., Malone, W., &amp;amp; Sweeny, K. (2008). "Exploring causes of the self-serving bias." Social and Personality Psychology Compass, 2(2), 895-908.&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.academia.edu/download/123891267/The_McNamara_Fallacy.pdf?ref=jessehouwing.net" rel="noopener noreferrer"&gt;&lt;em&gt;van Nieuwenhuizen, P. "The McNamara Fallacy: Relearning Old Lessons."&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>The why for Usage Based Billing for GitHub Copilot</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Mon, 11 May 2026 12:58:31 +0000</pubDate>
      <link>https://dev.to/jessehouwing/the-why-for-usage-based-billing-for-github-copilot-4eb3</link>
      <guid>https://dev.to/jessehouwing/the-why-for-usage-based-billing-for-github-copilot-4eb3</guid>
      <description>&lt;p&gt;Last month GitHub announced Usage Based Billing for GitHub Copilot. This completely changes the way its costs are calculated. A lot has already been written about the impact and a lot is still unknown.&lt;/p&gt;

&lt;p&gt;In this post I want to explore the reasons why this switch was inevitable and how it enables new scenarios to make Copilot perform better than it did before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick recap of copilots current pricing model
&lt;/h2&gt;

&lt;p&gt;GitHub Copilot is currently available in 5 different plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Copilot Free - severely rate limited free version with no option to buy additional requests.&lt;/li&gt;
&lt;li&gt;GitHub Copilot Pro - Individual license with limited Premium Request Units.&lt;/li&gt;
&lt;li&gt;GitHub Copilot Pro+ - Individual license with more extensive Premium Request Units.&lt;/li&gt;
&lt;li&gt;GitHub Copilot for Business - License for organizations with centralized policies and limited Premium Request Units.&lt;/li&gt;
&lt;li&gt;GitHub Copilot Enterprise - License for organizations with centralized policies and more extensive Premium Request Units.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these licenses has a fixed monthly price. GitHub Copilot includes unlimited suggestions and a limited set of free models.&lt;/p&gt;

&lt;p&gt;And about a year ago, GitHub introduced the concept of Premium Request Units to pay for more powerful models and and PRUs are the used to charge for specific features like the Cloud Coding Agent and Copilot Code Review.&lt;/p&gt;

&lt;p&gt;When the Cloud Coding Agent was introduced, it could use more than 1 PRU per request. This made the cost of the agent unpredictable and GitHub soon changed the price of the Cloud Coding Agent to a single PRU per request.&lt;/p&gt;

&lt;p&gt;When a user has exhausted their included PRU budget, additional PRUs are charged. Additional PRUs have a fixed cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usages of PRUs
&lt;/h2&gt;

&lt;p&gt;The primary use of PRUs are to charge the costs of requests to the users of GitHub Copilot. Each model is assigned a multiplier of 0, 1, up to 50 (highest so far) PRUs.&lt;/p&gt;

&lt;p&gt;The model multiplier wasn't just used to charge the cost, it's an open secret that it was also used to divert users to other models to load balance them. Most people are more hesitant to use expensive models carelessly. The introduction of the "Auto" mode rests on this principle. Users get a 10% discount on PRUs if they let GitHub select their model. While most users seem to assume this will route them to the "best model for the task", this generally sends the request to the "model with the best availability".&lt;/p&gt;

&lt;p&gt;Premium Request Units were also reduced for promotional purposes. When Claude Opus 4.7 was introduced, it's multiplier was temporarily set to 3x, later increased to 15x. This promotional use isn't just to get us "hooked" on the more powerful model features, it's also to allow GitHub to get crucial statistics on how these new models perform and to help them iron out bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How GitHub pays for model usage
&lt;/h2&gt;

&lt;p&gt;The model providers, Anthropic, Google, OpenAI and X, don't charge the cost of their models in Premium Request Units. They charge the cost in terms of Tokens. Input Tokens, Output tokens and Cached tokens at the highest level.&lt;/p&gt;

&lt;p&gt;Given the amount GitHub uses, they have probably got a really good deal.&lt;/p&gt;

&lt;p&gt;But GitHub needs to balance the cost for the Tokens it spends against what it can charge its users in Premium Request Units.&lt;/p&gt;

&lt;p&gt;GitHub also balances the costs between Tokens and PRUs by limiting the maximum token window for models. For example, if you're using Claude Opus 4.6 through GitHub Copilot, the maximum token window is 200k tokens. If you use the same model directly through Anthropic you have a maximum of 1m tokens.&lt;/p&gt;

&lt;p&gt;As Agent Mode and Cloud Coding Agent got more powerful, more tools appeared, the models are using more and more tokens for every request, reducing the available token window for your requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other ways GitHub control costs
&lt;/h2&gt;

&lt;p&gt;GitHub has a few other options to control the maximum number of tokens you can use for each Premium Request Unit they charge you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Mode -&lt;/strong&gt; Agent Mode spends tokens. GitHub can control the token spend by forcing the agent to come to an early conclusion or to postpone work to a future session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud Coding Agent&lt;/strong&gt; - The cloud coding agent spends tokens and action minutes. GitHub can control the token spend by forcing the agent to come to an early conclusion or to postpone work to a future session. The workflows running the Cloud Coding Agent runs on is limited to 2 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code Review&lt;/strong&gt; - Similar to the Cloud Coding Agent, when using GitHub's Code Review feature, GitHub can control the token spend by limiting the total time and the total of tokens the feature can spend.&lt;/p&gt;

&lt;p&gt;When using these features, GitHub can also control which model is used to perform these tasks.&lt;/p&gt;

&lt;p&gt;Some features, like tool optimization in Visual Studio Code also limit the total number of tokens used for each session by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recent creative uses
&lt;/h2&gt;

&lt;p&gt;New features, like subagent and fleets have multiplied the number of tokens you can use in a single Agent request. Visual Studio Code extensions have allowed users to steer the agent without completing the Agent request.&lt;/p&gt;

&lt;p&gt;These creative usages, some people might call them abuse, have allowed some people to use significantly more tokens costs than the PRU is worth. In some cases thousands time more.&lt;/p&gt;

&lt;p&gt;And instead of figuring out how to charge its users in Premium Request Units in this new model, GitHub will charge us for the actual tokens instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what changes
&lt;/h2&gt;

&lt;p&gt;On June 1st 2026 GitHub will change the way we all pay for our AI usage.&lt;/p&gt;

&lt;p&gt;Instead of the abstract thing, called Premium Request Units, acting as a proxy for the real cost of AI usage, we're going to be charged in tokens, the same unit in which GitHub is charged.&lt;/p&gt;

&lt;p&gt;Unfortunately, the different model providers, each charge different prices for different kinds of tokens. Your usage of each model is translated from the price per token into AI Credits, the credits are deducted from the budget included in your account.&lt;/p&gt;

&lt;p&gt;GitHub explains &lt;a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/?ref=jessehouwing.net" rel="noopener noreferrer"&gt;how the new pricing model works&lt;/a&gt; on the GitHub Blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benefits
&lt;/h2&gt;

&lt;p&gt;The introduction of tokens isn't meant to simply increase the cost of AI, but it is also expected to more equally spread the load across the available models as users will be more likely to try to find the most cost effective model to use for the work they're trying to accomplish, instead of using most powerful model available to them for all their tasks. This should result in better model availability and higher performance.&lt;/p&gt;

&lt;p&gt;A key benefit of GitHub Copilot is that you get access to the models of Anthropic, Google and OpenAI through a single subscription. And now that we are charged for the actual usage of tokens, GitHub can start raising the tokens available for each session, making the models more comparable to subscribing directly through each model provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional changes for Business accounts
&lt;/h2&gt;

&lt;p&gt;In addition to the new pricing, GitHub is introducing a number of welcome changes for business accounts (Copilot for Business and Copilot Enterprise).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pooled usage&lt;/strong&gt; - Under the current model, when a user has spent all of its budget, additional requests are automatically charged. Under the new model, the budget of all users is pooled together. So the unused budget from less frequent users can be consumed by more frequent users in the same organization. Only when the full pooled budget is used up, will additional usage be charged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Individual additional budgets&lt;/strong&gt; - Under the current model, additional charges can be controlled through budgets, these are pooled together for all users in the same cost center. In the new model it's possible to set a per-user budget as well as a universal budget for all users in addition to the already existing budget control options.&lt;/p&gt;

&lt;p&gt;In addition to these changes, existing policy options to control costs are still available:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enterprise, Organization&lt;/strong&gt; and &lt;strong&gt;Cost Center Budgets&lt;/strong&gt; - Existing budget options to limit the total spend at the Enterprise, Organization and Cost Center can still be configured to limit the total spend on AI costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limit model availability&lt;/strong&gt; - Enterprise and Organization level policies allow admins to control the available models. This can remove less effective and more expensive model options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limit feature availability&lt;/strong&gt; - Enterprise and Organization level policies allow admins to control the available features. This can remove more expensive features like Code Review and Cloud Coding Agent for specific users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't hamstring your users, educate them
&lt;/h2&gt;

&lt;p&gt;But, ultimately, limiting the cost isn't the path forward. Optimizing the value delivered through AI is. Use the available limits to bridge the time it takes to educate your users.&lt;/p&gt;

</description>
      <category>github</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Keep your examples in sync with your Action</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Mon, 02 Mar 2026 09:53:49 +0000</pubDate>
      <link>https://dev.to/jessehouwing/keep-your-examples-in-sync-with-your-action-631</link>
      <guid>https://dev.to/jessehouwing/keep-your-examples-in-sync-with-your-action-631</guid>
      <description>&lt;p&gt;Last month I added a few more GitHub Actions to the Marketplace and while working on them found that it was easy to forget to update my examples as the implementation of the Action evolved.&lt;/p&gt;

&lt;p&gt;So, what started as a small script to &lt;a href="https://github.com/marketplace/actions/actions-example-checker?ref=jessehouwing.net" rel="noopener noreferrer"&gt;automatically validate the examples against the &lt;code&gt;action.yaml&lt;/code&gt; quickly grew into its own action&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing the Actions Example Checker
&lt;/h2&gt;

&lt;p&gt;The Actions Example Checker is a simple action you can add to your repository. It will scan for all &lt;code&gt;action.yaml&lt;/code&gt; files and extract the metadata. It will then search through all markdown files in your repo and extract all YAML script blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="s"&gt;yaml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your/action@version&lt;/span&gt;
&lt;span class="err"&gt;```&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any examples that reference your action will be checked against the schema.&lt;/p&gt;

&lt;p&gt;Additionally, you can add a &lt;code&gt;action.schema.yaml&lt;/code&gt; with extended metadata about each input, like: allowed values, datatype, regex. I've got a few more plans with this, but for now it's purely used to check the assigned values in your examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample workflow
&lt;/h2&gt;

&lt;p&gt;In order to add the Actions example checker to your repo, add this workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate Documentation&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate-docs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd&lt;/span&gt; &lt;span class="c1"&gt;# v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sparse-checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;**/action.yaml&lt;/span&gt;
            &lt;span class="s"&gt;**/action.yml&lt;/span&gt;
            &lt;span class="s"&gt;**/action.schema.yaml&lt;/span&gt;
            &lt;span class="s"&gt;**/action.schema.yml&lt;/span&gt;
            &lt;span class="s"&gt;**/*.md&lt;/span&gt;
          &lt;span class="na"&gt;sparse-checkout-cone-mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jessehouwing/actions-example-checker@ea0821955c7ecf2c98e5b184054ba31486914a7b&lt;/span&gt; &lt;span class="c1"&gt;# v0.0.4&lt;/span&gt;
        &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example schemas
&lt;/h2&gt;

&lt;p&gt;I've already added schema validation to most of my own actions, they may be able to help you setup your own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/jessehouwing/actions-example-checker/blob/main/action.schema.example.yml?ref=jessehouwing.net" rel="noopener noreferrer"&gt;actions-example-checker/action.schema.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jessehouwing/actions-semver-checker/blob/main/action.schema.yml?ref=jessehouwing.net" rel="noopener noreferrer"&gt;actions-semver-checker/action.schema.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jessehouwing/actions-dependency-submission/blob/main/action.schema.yaml?ref=jessehouwing.net" rel="noopener noreferrer"&gt;actions-dependency-submission/action.schema.yaml&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/jessehouwing/azdo-marketplace/blob/main/action.schema.yaml?ref=jessehouwing.net" rel="noopener noreferrer"&gt;azdo-marketplace/action.schema.yaml&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me know if you find this useful!&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
      <category>continuousintegratio</category>
    </item>
    <item>
      <title>Automatic versioning for GitHub Actions authors</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Sat, 07 Feb 2026 10:34:01 +0000</pubDate>
      <link>https://dev.to/jessehouwing/automatic-versioning-for-github-actions-authors-2e19</link>
      <guid>https://dev.to/jessehouwing/automatic-versioning-for-github-actions-authors-2e19</guid>
      <description>&lt;p&gt;As a maintainer of 3 GitHub Actions, I've always struggled a bit with the versioning of them. GitHub's documentation provides guidance, but no easy way to verify you did everything right. Based on a quick scan of the actions on the marketplace I'm not alone, even some of GitHub's own actions have some inconsistencies when it comes to it.&lt;/p&gt;

&lt;p&gt;This is why I cobbled together v1 of my Actions Semver Checker action a couple of years ago and it has served me well. But as I threw the action together in a short amount of time way back when, it contained a few bugs, was very hard to maintain and I always dreaded making changes to it, since I had fixed my own repos, so it was hard to test whether I broke anything or accidentally introduced unwanted side effects.&lt;/p&gt;

&lt;p&gt;This is why I had Copilot Agent use the existing v1 implementation as a blueprint to build a new version. But with a lot of improvements. I gave Copilot the order to refactor the action with the following constraints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make sure the old implementation and the new one can be called side-by-side so it's easy to compare the differences.&lt;/li&gt;
&lt;li&gt;Extract the validation rules into separate scripts so they're easier to maintain and test.&lt;/li&gt;
&lt;li&gt;Provide manual remediation options wherever possible.&lt;/li&gt;
&lt;li&gt;Implement an autofix functionality which tries to execute the manual remediation steps.&lt;/li&gt;
&lt;li&gt;Add unit tests for everything.&lt;/li&gt;
&lt;li&gt;Add integration tests for everything.&lt;/li&gt;
&lt;li&gt;Try to remain backwards compatible with v1 as much as possible when it comes to the inputs.&lt;/li&gt;
&lt;li&gt;I gave it links to all the relevant GitHub docs and blogs for:

&lt;ul&gt;
&lt;li&gt;GitHub CLI release commands&lt;/li&gt;
&lt;li&gt;Releases REST API&lt;/li&gt;
&lt;li&gt;Tags REST API&lt;/li&gt;
&lt;li&gt;GitHub's GraphQL API&lt;/li&gt;
&lt;li&gt;Blogs discussing immutable releases&lt;/li&gt;
&lt;li&gt;GitHub Actions versioning &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I used Claude Opus to generate a plan for this, since it's a pretty big effort. I went over the plan and made a few tweaks and then had the Copilot Coding Agent spend a good 45 minutes to do the work. And this resulted in my first vibe coded GitHub Action. And the result? It did mostly what it needed to do. It surfaced a couple of gaps in the initial plan and the request to keep both old and new code side-by-side caused some interesting interweaving between the 2 implementations.&lt;/p&gt;

&lt;p&gt;But now that I had a test suite of 90+ tests, I felt much better changing the actual behavior. And after many round trips with the Copilot Coding Agent as well as a few hands-on sessions inside Visual Studio Code with Copilot, I am proud to release &lt;a href="https://github.com/jessehouwing/actions-semver-checker/" rel="noopener noreferrer"&gt;v2 of the GitHub Actions Semver Checker Action&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it do exactly?
&lt;/h2&gt;

&lt;p&gt;Good question, the action is meant to be installed into the repository which hosts a GitHub action, like &lt;code&gt;actions/checkout&lt;/code&gt; or &lt;code&gt;jessehouwing/actions-semver-checker&lt;/code&gt;. It analyzes all branches, tags and releases and validates them based on a number of rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There should be a vX tag or branch (configurable) which points to the latest vX.y.z tag.&lt;/li&gt;
&lt;li&gt;There should be a vX.Y tag or branch (configurable) which points to the latest vX.Y.z tag.&lt;/li&gt;
&lt;li&gt;There should be a vx.y.z tag&lt;/li&gt;
&lt;li&gt;There should be &lt;em&gt;no&lt;/em&gt; vx.y.z branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; There should be a release for every vx.y.z tag

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; That release should be immutable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; The release for the highest vx.y.z should be set to "latest"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;new -&lt;/strong&gt; There should be &lt;em&gt;no&lt;/em&gt; release for vX and vX.Y tags&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;new -&lt;/strong&gt; The action should be published to the marketplace

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; The action.yml should have the correct metadata elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;new -&lt;/strong&gt; The latest version of the action should be published to the marketplace&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;new -&lt;/strong&gt; Immutable releases should be turned on&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;And for almost all of these validations, I managed to build an autofix option which automatically corrects any issues found.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new
&lt;/h2&gt;

&lt;p&gt;As you can see in the list above, v2 adds a lot of new validations and it also adds the autofix feature to automatically resolve most of the issues found. If the action is not able to fix the issue, manual remediation steps are logged to the workflows summary pages.&lt;/p&gt;

&lt;p&gt;Autofixes available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delete/update/create branches&lt;/li&gt;
&lt;li&gt;delete/update/create tags&lt;/li&gt;
&lt;li&gt;convert branch to tag&lt;/li&gt;
&lt;li&gt;convert tag to branch&lt;/li&gt;
&lt;li&gt;delete/update/create/publish releases&lt;/li&gt;
&lt;li&gt;automatically set the latest version&lt;/li&gt;
&lt;li&gt;republish releases to make them immutable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are a few other improvements worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully configurable which suites of rules you want to run&lt;/li&gt;
&lt;li&gt;You no longer need to check out the whole repo to run the action&lt;/li&gt;
&lt;li&gt;Retry logic and handling of rate limits&lt;/li&gt;
&lt;li&gt;Available as module on the PowerShell Gallery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is one unfortunate change to v1 I could not work around&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You should pass the &lt;code&gt;${{ secrets.GUTHUB_TOKEN }}&lt;/code&gt; to the &lt;code&gt;token:&lt;/code&gt; input of the action&lt;/li&gt;
&lt;li&gt;For some validations the token must have &lt;code&gt;contents: write&lt;/code&gt; permissions (to read draft releases)&lt;/li&gt;
&lt;li&gt;For autofix functionality the token must have &lt;code&gt;contents: write&lt;/code&gt; permissions&lt;/li&gt;
&lt;li&gt;To fix certain issues, the token needs permissions which cannot be granted to GitHub Actions, such as &lt;code&gt;workflows: write&lt;/code&gt;. Use a GitHub App or Fine-grained Personal Access Token instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recommended usage
&lt;/h2&gt;

&lt;p&gt;I recommend you install the action into your action repository with its default settings, this will analyze your repo and will detect all rule violations.&lt;/p&gt;

&lt;p&gt;Change the settings of the action to match your desired behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;floating-versions-use&lt;/code&gt; can be set to &lt;code&gt;tags&lt;/code&gt; or &lt;code&gt;branches&lt;/code&gt; depending on how you want to manage the &lt;code&gt;vx&lt;/code&gt; and &lt;code&gt;vx.y&lt;/code&gt; versions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-minor-version&lt;/code&gt; can be set to &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; to validate the existence of &lt;code&gt;vx.y&lt;/code&gt; versions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-releases&lt;/code&gt; can be set to &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; to validate the existence of releases for &lt;code&gt;vx.y.z&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-release-immutability&lt;/code&gt; can be set to &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt; to validate whether releases are immutable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignore-preview-releases&lt;/code&gt; can be set to &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; to exclude preview releases when checking floating versions (&lt;code&gt;vx&lt;/code&gt; an &lt;code&gt;vx.y&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ignore-versions&lt;/code&gt; can exclude versions from validation completely. This can be used to ignore really old versions or versions that were accidentally made immutable and can no longer be fixed.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Check SemVer

on:
  push:
    branches: ['main']
    tags:
      - '*'
  workflow_dispatch:

permissions: {}

jobs:
  check-semver:
    permissions:
      contents: write

    concurrency:
      group: '${{ github.workflow }}'
      cancel-in-progress: true

    runs-on: ubuntu-latest

    steps:
      - uses: jessehouwing/actions-semver-checker@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          check-release-immutability: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ be extra careful with the &lt;code&gt;check-release-immutability&lt;/code&gt; setting prior to turning on autofix. Most automatic fixes can be easily undone using the git commandline, but once a release is made immutable, there is no way back.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once your existing tags have been cleaned up and you understand the implications, turn on release immutability checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    steps:
      - uses: jessehouwing/actions-semver-checker@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          check-release-immutability: error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the action in read-only mode for a while, manually performing the suggested remediation steps.&lt;/p&gt;

&lt;p&gt;When you're confident the action does what you want it to do, consider turning on &lt;code&gt;autofix: true&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    steps:
      - uses: jessehouwing/actions-semver-checker@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          autofix: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With autofix turned on, the only thing you need to do to release a new version and update all tags and releases, is to push a new tag to to create a new release with the correct name: &lt;code&gt;vx.y.z&lt;/code&gt;. This will trigger autofix to do the rest on your behalf.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
    </item>
    <item>
      <title>Improved Dependency Submission for GitHub Actions</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Sat, 27 Dec 2025 18:47:09 +0000</pubDate>
      <link>https://dev.to/jessehouwing/improved-dependency-submission-for-github-actions-1b29</link>
      <guid>https://dev.to/jessehouwing/improved-dependency-submission-for-github-actions-1b29</guid>
      <description>&lt;p&gt;If you've read some of my earlier blogs about GitHub Actions Security, you may have followed my advice to pin your actions and configure Dependabot. Recently someone brought to my attention that you then Security Advisories for GitHub actions do not show up in the Dependency Graph and Dependabot won't create security updates.&lt;/p&gt;

&lt;p&gt;I decided to solve that problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;When a security vulnerability is found and a security advisory is created, it will have the action and its affected versions as metadata. &lt;strong&gt;Versions&lt;/strong&gt; , not &lt;strong&gt;SHAs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When Automatic Dependency Submission submits the versions of actions used by a repository it records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@v1.2.3&lt;/code&gt; as &lt;code&gt;1.2.3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@v1&lt;/code&gt; as &lt;code&gt;1.*.*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;8e8c483db84b4bee98b60c0593521ed34d9990e8&lt;/code&gt; as &lt;code&gt;8e8c483db84b4bee98b60c0593521ed34d9990e8&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Security Advisory Database can't match &lt;code&gt;8e8c483db84b4bee98b60c0593521ed34d9990e8&lt;/code&gt; against the vulnerable version range and won't show the vulnerability in the dependency graph.&lt;/p&gt;

&lt;p&gt;Another issue exists when actions are forked to an organization (a common technique to create a private actions marketplace). The dependency is recorded using the organization name that holds the fork, not the upstream.&lt;/p&gt;

&lt;p&gt;The Security Advisory Database can't match &lt;code&gt;myorg/actions_checkout&lt;/code&gt; against the vulnerable action &lt;code&gt;actions/checkout&lt;/code&gt; and won't show the vulnerably in the dependency graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impacted features
&lt;/h2&gt;

&lt;p&gt;Without recording the exact version of each action used, a number of features won't work as you might expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency Graph won't show which actions you use are vulnerable.&lt;/li&gt;
&lt;li&gt;Dependabot Security Updates won't be created for workflows using vulnerable actions.&lt;/li&gt;
&lt;li&gt;Dependency Review Action won't block pull requests introducing vulnerable actions into your workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;To solve the issue I created a new manual submission action which mimics the automatic submission, but adds a few extra features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It resolves the SHA to the highest, most specific version and records that as an additional dependency.&lt;/li&gt;
&lt;li&gt;It resolves a wildcard version to the current specific version and records that as an additional dependency.&lt;/li&gt;
&lt;li&gt;It can be configured to look up the upstream of a forked action and records that as an additional dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these additional dependencies recorded, the Dependency Graph now contains all the metadata to determine whether the GitHub actions you depend on are vulnerable and all features should now work as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to
&lt;/h2&gt;

&lt;p&gt;Changing how Actions' dependencies are reported, you simply add my &lt;code&gt;jessehouwing/actions-dependency-submission&lt;/code&gt; action to your workflows (and optionally add &lt;code&gt;actions/dependency-review-action&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Submit and validate dependencies of GitHub Actions

on:
  push:
    branches:
      - main
  pull_request:
  schedule:
    - cron: '33 4 * * *'

permissions: {}

jobs:
  submit-actions-dependencies:
    runs-on: ubuntu-latest
    permissions:
      # submitting dependencies requires 'contents: write'
      contents: write

    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
      - uses: jessehouwing/actions-dependency-submission@e848a29fd84b874cce3e45ceb00619bc72dbeca3 # 1.0.2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # 4.8.2
        if: github.event_name == 'pull_request'
        with:
          retry-on-snapshot-warnings: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can test whether the action is working correctly by adding a vulnerable action to your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Submit Dependencies
on:
  workflow_dispatch:

jobs:
  submit-dependencies:
    if: false
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should show as a &lt;strong&gt;high&lt;/strong&gt; vulnerability in the Dependency graph of the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1h3mw72qvf0wzlbrf7n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq1h3mw72qvf0wzlbrf7n.png" alt="Improved Dependency Submission for GitHub Actions" width="800" height="275"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dependency Graph showing &lt;code&gt;actions/download-artifact@v4.1.2&lt;/code&gt; as vulnerable.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And you'll also see the original report using the SHA:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqx4ncq8tvechctofm9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyqx4ncq8tvechctofm9f.png" alt="Improved Dependency Submission for GitHub Actions" width="800" height="72"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dependency Graph showing actions/download-artifact referenced by SHA.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://github.com/jessehouwing/actions-dependency-submission" rel="noopener noreferrer"&gt;find the action and additional documentation and examples here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>github</category>
      <category>supplychainsecurity</category>
      <category>security</category>
    </item>
    <item>
      <title>GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub</title>
      <dc:creator>Jesse Houwing</dc:creator>
      <pubDate>Wed, 24 Sep 2025 08:48:31 +0000</pubDate>
      <link>https://dev.to/jessehouwing/github-billing-let-organization-and-repository-admins-set-the-cost-center-in-github-1ad2</link>
      <guid>https://dev.to/jessehouwing/github-billing-let-organization-and-repository-admins-set-the-cost-center-in-github-1ad2</guid>
      <description>&lt;p&gt;We rely on GitHub Cost Centers to split the GitHub bill across the different entities that make up our company. What resource is tied to which cost center is managed in the GitHub Enterprise Settings and for now this can only be done be a user with Enterprise Owner or Billing Administrator permissions.&lt;/p&gt;

&lt;p&gt;I'd been searching for a way to allow Organization Owners and Repository Administrators to set the Cost Center on a repository themselves without having to involve one of these highly privileged accounts. We found a solution using GitHub Actions and Repository Custom Properties.&lt;/p&gt;

&lt;p&gt;Since my last blog post on Cost Centers, a few small improvements were released by GitHub:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;There is now an API to associate a Repository with a Cost Center.&lt;/li&gt;
&lt;li&gt;It's now possible to associate a Repository to a different Cost Center than the Organization that owns it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These two improvements allow us to solve this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Custom Property
&lt;/h2&gt;

&lt;p&gt;GitHub has a feature called Custom Properties for repositories which allows you to link arbitrary data to a repo. You can define these properties at the Enterprise, Organization and Repository level.&lt;/p&gt;

&lt;p&gt;Since our cost centers are defined at the Enterprise level, I chose to create this custom property at the same level. To add a new custom property navigate to your enterprise, select the &lt;strong&gt;Policies&lt;/strong&gt; tab and then drill down into &lt;strong&gt;Repository&lt;/strong&gt; and then &lt;strong&gt;Custom Properties&lt;/strong&gt;. Choose &lt;strong&gt;New Property&lt;/strong&gt; to create your new property:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipkkak8h8owq5ukb6pq6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipkkak8h8owq5ukb6pq6.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Navigate to the Custom Properties section and click new property&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Give the property a clear &lt;strong&gt;Name&lt;/strong&gt; and &lt;strong&gt;Description&lt;/strong&gt;. Since our list of cost centers is clearly defined, I've chosen the &lt;strong&gt;Type&lt;/strong&gt;  &lt;strong&gt;single-select&lt;/strong&gt; to allow our users to pick a cost center from a predefined list.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8k5yho61ng6opij6dg4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8k5yho61ng6opij6dg4t.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Give the property a unique name and set the type to single select&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Next, add the list of cost centers you want people to be able to select from as options. I've also included an option &lt;strong&gt;inherit&lt;/strong&gt; which I'll explain later:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkwor8jp2orvquernj95.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkwor8jp2orvquernj95.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Add the required cost-center options.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And finally make the field required and set the &lt;strong&gt;default value&lt;/strong&gt; to &lt;strong&gt;inherit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2i0xyxs9lvv8vars9lu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2i0xyxs9lvv8vars9lu5.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Set the permission to allow users with properties permission to edit this field, make it required and set the default value.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now when you create a new repository or navigate to the settings of an existing repository, you can set the cost-center custom field:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1277a5oqgektmnw0ope.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1277a5oqgektmnw0ope.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Users are required to select a cost center when creating a new repository.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And existing repositories will now show the custom property in the repository settings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe32htzq7z8h3h7xny3g3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe32htzq7z8h3h7xny3g3.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The reason for Inherit
&lt;/h2&gt;

&lt;p&gt;In the above example we added a special value &lt;strong&gt;inherit&lt;/strong&gt; , when this value is set, we &lt;em&gt;don't actually set any cost center on the repository&lt;/em&gt; and this causes all costs generated by this repository to automatically flow to the cost center associated to the Organization. If the Organization isn't associated to a cost center, the costs will flow to the Enterprise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making it all work using GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Creating the Custom Property itself doesn't do anything by itself. In order to actually associate the Repository to the Cost Center, we still need to update the configuration in the GitHub Enterprise Billing &amp;amp; Licensing section:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx88z91ra5ytbgim7zg6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx88z91ra5ytbgim7zg6f.png" alt="GitHub Billing - Let Organization and Repository admins set the Cost Center in GitHub"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Billing Administrators can associate Organizations, Repositories and Users to Cost Centers&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Without additional automation each repository needs to be manually added to a cost center by editing the Cost Center and adding the Repository. This is a tedious manual process.&lt;/p&gt;

&lt;p&gt;Instead, to update the Cost Center configuration I've created a GitHub Actions workflow that runs once a day and compares the Cost Center configuration against the values of the Custom Properties and performs updates to the Cost Center configuration if needed.&lt;/p&gt;

&lt;p&gt;The basic workflow is quite simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query the current Cost Center configuration&lt;/li&gt;
&lt;li&gt;Query all Organizations in our GitHub Enterprise &lt;/li&gt;
&lt;li&gt;Query all Repositories in each GitHub Organization&lt;/li&gt;
&lt;li&gt;Query the Custom Properties for each Repository&lt;/li&gt;
&lt;li&gt;Update the Cost Center configuration in case it doesn't match the Custom Property&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow relies on the GitHub CLI t0 query the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$costCenters = (invoke-gh -fromJson -- api /enterprises/$enterprise/settings/billing/cost-centers).costCenters

$orgs = get-allorganizations $enterprise | % { $_.login }

update-repocostcenters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;update-repocostcenters&lt;/code&gt; function checks which updates are needed:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function update-repocostcenters {
    Write-Output "Updating Costcenters for repositories in $enterprise." 

    foreach ($org in $orgs) {

        Write-Output "Processing organization $org"
        $repos = invoke-gh -slurp api /orgs/$org/repos --paginate --jq '.[] | { name: .name, full_name: .full_name }' | ConvertFrom-Json

        foreach ($repo in $repos) { 

            $customProperties = invoke-gh api /repos/$($repo.full_name)/properties/values --jq '.' | ConvertFrom-Json
            $repoCostCenterProperty = $customProperties | Where-Object { $_.property_name -eq "cost-center" } | Select-Object -ExpandProperty value -ErrorAction Ignore
            $repo | Add-Member -NotePropertyName cost_center -NotePropertyValue $repoCostCenterProperty -Force

            $currentCostCenter = $costCenters | ? { $_.resources | ? { $_.type -eq "Repo" -and $_.name -eq $repo.full_name } }

            if ( $repo.cost_center -eq "inherit" ) {
                $repo.cost_center = $null
            }

            $targetCostCenter = $null

            if ($repo.cost_center) {
                $targetCostCenter = $costCenters | ? { $_.name -eq $repo.cost_center }
                if (-not $targetCostCenter) {
                    Write-Warning "Costcenter for repository $($repo.full_name) not found."
                }
            }

            if ($null -eq $currentCostCenter) {
                if ($null -ne $targetCostCenter) {
                    Write-Output "Updating costcenter for repository $($repo.full_name) from unassigned to $($targetCostCenter.name)."
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Add" -CostCenter $targetCostCenter -Enterprise $enterprise
                }
                else {
                    Write-Verbose "Repository $($repo.full_name) does not have a cost center assigned."
                }
            }
            else {
                if ($null -eq $targetCostCenter) {
                    Write-Verbose "Repository $($repo.full_name) does not have a cost center assigned."
                    Write-Output "Updating costcenter for repository $($repo.full_name) from $($currentCostCenter.name) to unassigned."
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Delete" -CostCenter $currentCostCenter -Enterprise $enterprise
                }
                elseif ($currentCostCenter.id -ne $targetCostCenter.id) {
                    Write-Output "Updating costcenter for repository $($repo.full_name) from $($currentCostCenter.name) to $($targetCostCenter.name)."
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Delete" -CostCenter $currentCostCenter -Enterprise $enterprise
                    Update-CostCenterResources -Handles $repo.full_name -ResourceType "Repo" -Action "Add" -CostCenter $targetCostCenter -Enterprise $enterprise
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Unfortunately, you can't simply update the associated Cost Center in a single call, when changing the current value, an add plus delete is required.&lt;/p&gt;

&lt;p&gt;I shared the &lt;code&gt;Update-CostCenterResources&lt;/code&gt; function in a previous blogpost. GitHub added the ability to set Organizations and Repositories recently, so here is the updated version:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function Update-CostCenterResources {
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$Handles,

        [ValidateSet('User', 'Repo', 'Org')]
        [string]$ResourceType = "User",

        [Parameter(Mandatory = $true)]
        [ValidateSet('Add', 'Delete')]
        [string]$Action,

        [Parameter(Mandatory = $true)]
        $CostCenter,

        [Parameter(Mandatory = $true)]
        [string]$Enterprise
    )

    switch ($Action) {
        'Add' {
            $method = 'POST'
            $Handles = $Handles | Where-Object { 
                $handle = $_
                return (($costCenter.resources | ? { $_.type -eq $ResourceType } | ? { $_.name -eq $handle }).Count -eq 0)
            }
        }
        'Delete' {
            $method = 'DELETE'
            $Handles = $Handles | Where-Object { 
                $handle = $_
                return (($costCenter.resources | ? { $_.type -eq $ResourceType } | ? { $_.name -eq $handle }).Count -gt 0)
            }
        }
    }

    # Call fails when processing too many users at once. Thus batching the calls...
    $count = 0
    do {
        $batch = $Handles | Select-Object -Skip $count -First 30
        $count += $batch.Count

        if ($batch.Count -gt 0) {
            switch ($ResourceType) {
                'User' { 
                    $body = @{
                        users = [string[]]$batch
                    }
                }
                'Org' { 
                    $body = @{
                        organizations = [string[]]$batch
                    }
                }
                'Repo' { 
                    $body = @{
                        repositories = [string[]]$batch
                    }
                }
            }


            $_ = ($body | ConvertTo-Json) | gh api --method $method /enterprises/$Enterprise/settings/billing/cost-centers/$($CostCenter.id)/resource --input -
        }
    } while ($batch.Count -gt 0)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;All of this runs from a simple GitHub workflow which has access to a Personal Access token with the required permissions to access the Cost Center information as well as sufficient permissions to query all Repositories in all Organizations in the enterprise.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Set Costcenters

on:
  schedule:
    - cron: "5 4 * * *"

jobs:
  update:
    permissions: 
      contents: read

    runs-on: ubuntu-latest

    steps:      
    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
      with:
        fetch-depth: 0

    - name: Assign Costcenters
      run: |
         .\assign-costcenters.ps1
      env:
        GH_TOKEN: ${{ secrets.GH_TOKEN }}
      shell: pwsh 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can find the whole script here:&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



</description>
      <category>github</category>
      <category>githubactions</category>
    </item>
  </channel>
</rss>
