<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>KiCI blog</title><description>Walkthroughs of what you can build with KiCI, with real, tested TypeScript code you can copy.</description><link>https://kici.dev</link><language>en</language><item><title>Use GitHub Actions minutes as burst capacity for your CI</title><link>https://kici.dev/blog/2026-08-29-any-workflow-engine-is-a-compute-pool</link><guid isPermaLink="true">https://kici.dev/blog/2026-08-29-any-workflow-engine-is-a-compute-pool</guid><description>A KiCI agent is just a process, so a GitHub Actions run can host one. Burst onto minutes you already pay for, and know the trade-offs.</description><pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;bursting-ci-usually-means-booting-machines-you-own&quot;&gt;Bursting CI usually means booting machines you own&lt;/h2&gt;
&lt;p&gt;You want CI capacity that grows when the queue backs up and shrinks when it
drains. Today you pick between two costs. Reserve the machines and you pay for
the peak all month, even at 3am when nothing runs. Or wire up a cloud-provisioning
integration: an API to boot a VM, credentials to inject, a lifecycle to babysit,
a reaper to make sure a crash doesn’t leave an instance billing you for a week.&lt;/p&gt;
&lt;p&gt;Both cost you the same assumption: bursting means booting a server you own and
operate. That assumption is the expensive part.&lt;/p&gt;
&lt;h2 id=&quot;a-kici-agent-is-a-process-not-a-server&quot;&gt;A KiCI agent is a process, not a server&lt;/h2&gt;
&lt;p&gt;Here’s what a KiCI agent is: a plain Node process that opens one outbound
WebSocket to your orchestrator and runs the jobs it’s handed. In one-shot mode it
runs a single job and exits. It needs a Node runtime, a network path out, and
nothing else. It does not care what started it.&lt;/p&gt;
&lt;p&gt;So “add capacity” doesn’t have to mean “boot a VM.” It can mean “ask something
that can already run a process to run one.” And you have systems that do exactly
that, sitting idle between builds: your CI runners.&lt;/p&gt;
&lt;p&gt;KiCI’s &lt;code&gt;event&lt;/code&gt; scaler is built for this. It makes no cloud calls of its own. When
demand rises it emits a reserved &lt;code&gt;kici.scaler.scale-up&lt;/code&gt; event; when an agent
should go away it emits &lt;code&gt;kici.scaler.scale-down&lt;/code&gt;. You consume those in a
provisioning workflow you write in TypeScript, with the same &lt;code&gt;kiciEvent()&lt;/code&gt;
trigger you’d use for anything else. The
&lt;a href=&quot;https://docs.kici.dev/user/workflows/autoscaling-workflows/&quot;&gt;workflow-driven autoscaling guide&lt;/a&gt;
covers the event contract. What comes up, and how, is the one thing left open on
purpose.&lt;/p&gt;
&lt;h2 id=&quot;github-actions-as-the-pool&quot;&gt;GitHub Actions as the pool&lt;/h2&gt;
&lt;p&gt;The pool most teams already have is GitHub Actions. Its runner queue is
elastic, a run is already an ephemeral, one-shot sandbox (the exact shape of a
scale-to-zero agent), and the minutes are
&lt;a href=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-actions#free-use-of-github-actions&quot;&gt;free up to your plan’s quota&lt;/a&gt;
on private repos, and free outright on public repos using standard runners.&lt;/p&gt;
&lt;p&gt;Here’s the provisioning workflow. It subscribes to the scale-up event and
dispatches a &lt;code&gt;kici-agent.yml&lt;/code&gt; run in a GitHub repo:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  workflow,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  job,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  step,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  kiciEvent,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  SCALER_EVENT_NAMES,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ScalerScaleUpPayload,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;@kici-dev/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** Must match the `name:` of the `event` scaler in your `scalers.yaml`. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;github-actions&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** The one-shot runner workflow in your runner repo, and the ref to dispatch. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GH_WORKFLOW&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;kici-agent.yml&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GH_REF&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;main&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;github-actions-autoscale-provision&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kiciEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ name: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_EVENT_NAMES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.scaleUp, match: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;$.scalerName&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;provision&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // Binds the `github-actions` context, which carries both the dispatch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // token and this integration&apos;s two settings. Load-bearing: the job option&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // is `context`, and an unrecognised key is DROPPED at compile time rather&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // than rejected — so a typo here fails at run time on an unresolved&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // secret, never at compile time.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      context: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;github-actions&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      steps: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        step&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;dispatch&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; p&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ScalerScaleUpPayload.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx.rawPayload);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // Your runner repo, as `owner/repo`. Set it once on the context:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          //   kici-admin variable set &amp;#x3C;orgId&gt; github-actions \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          //     GITHUB_RUNNER_REPO --value myorg/ci-runners&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // or replace the fallback below in your own copy.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; runnerRepo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GITHUB_RUNNER_REPO&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;myorg/ci-runners&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // Optional. A release tag holding a `kici-admin agent package` tarball&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // the runner installs instead of the published npm agent. Leave it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // unset unless you pin exact builds or your runners cannot reach npm.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; agentBundleRelease&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GITHUB_AGENT_BUNDLE_RELEASE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; inputs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Record&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            claim_code: p.claimCode,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            orchestrator_url: p.orchestratorUrl,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            agent_id: p.agentId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            labels: p.labels.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;,&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (agentBundleRelease) inputs.agent_bundle_release &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; agentBundleRelease;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.secrets.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;GITHUB_DISPATCH_TOKEN&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            `https://api.github.com/repos/${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;runnerRepo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/actions/workflows/${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GH_WORKFLOW&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/dispatches`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;              method: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;              headers: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                Authorization: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Bearer ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;token&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                Accept: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/vnd.github+json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &apos;X-GitHub-Api-Version&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;2022-11-28&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;              },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;              body: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ ref: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GH_REF&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, inputs }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // GitHub answers a successful dispatch with 204 and no body.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;res.ok &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res.status &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 204&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            throw&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`dispatch failed: ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} ${&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Dispatched ${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GH_WORKFLOW&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} in ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;runnerRepo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} for agent ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the whole KiCI side. The step body speaks plain HTTP to GitHub’s REST API,
with no SDK to install. Everything specific to “how a machine comes up” lives
there; swap it and you’ve pointed KiCI at a different pool.&lt;/p&gt;
&lt;p&gt;The run it dispatches is a normal GitHub Actions workflow. It installs the agent
and runs it once:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  workflow_dispatch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    inputs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      claim_code&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Single-use claim code the agent exchanges for its token&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      orchestrator_url&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Orchestrator WebSocket URL the agent connects back to&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      agent_id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Agent id the orchestrator correlates the spawn with&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      labels&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Comma-separated label set the pending job needs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      agent_bundle_release&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;-&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          Release tag in this repo holding a `kici-admin agent package` tarball.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          Leave empty to install the published agent from npm.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        required&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;jobs&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  agent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    runs-on&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # A one-shot agent registers, runs one job and exits, so a run that is still&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # alive after this long is wedged (an agent the orchestrator refused, a job&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # that never dispatched) and must release the runner rather than bill the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # default six hours. Raise it if your jobs legitimately run longer.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    timeout-minutes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;15&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    permissions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;      contents&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;read&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    steps&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # Default path: the published agent, from npm.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Set up Node.js&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.agent_bundle_release == &apos;&apos; }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        uses&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;actions/setup-node@820762786026740c76f36085b0efc47a31fe5020&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; # v7.0.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          node-version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;24&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Install the KiCI agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.agent_bundle_release == &apos;&apos; }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;npm install -g kici-admin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # Pinned path: a self-contained bundle your orchestrator produced with&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # `kici-admin agent package`. It vendors its own Node, so it needs no&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # setup-node. Use it to pin an exact agent build, or for runners that&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # cannot reach npm. Prepending it to PATH keeps the run step below uniform.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Install the KiCI agent (packaged bundle)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.agent_bundle_release != &apos;&apos; }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        env&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          GH_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ github.token }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          RELEASE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.agent_bundle_release }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          gh release download &quot;$RELEASE&quot; --repo &quot;$GITHUB_REPOSITORY&quot; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            --pattern &apos;kici-agent-linux-x64.tar.gz*&apos; --clobber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          sha256sum -c kici-agent-linux-x64.tar.gz.sha256&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          mkdir -p kici-agent-dist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          tar xzf kici-agent-linux-x64.tar.gz -C kici-agent-dist&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;          echo &quot;$PWD/kici-agent-dist&quot; &gt;&gt; &quot;$GITHUB_PATH&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Run the one-shot KiCI agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        env&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_ORCHESTRATOR_URL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.orchestrator_url }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_SCALER_CLAIM_CODE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.claim_code }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_AGENT_ID&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.agent_id }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_LABELS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;${{ inputs.labels }}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          # Managed mode + zero idle timeout: register, run one job, exit.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_SCALER_MANAGED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;1&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_EXECUTION_MODE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;bare-metal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;          KICI_SCALER_IDLE_TIMEOUT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;0&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;        run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;exec kici-agent&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent runs directly on the runner, installed with &lt;code&gt;npm&lt;/code&gt; rather than wrapped
in a container, so job steps that need Docker use the runner’s own daemon. If
your runners can’t reach npm, or you want to pin an exact build, set
&lt;code&gt;agent_bundle_release&lt;/code&gt; to a release holding a &lt;code&gt;kici-admin agent package&lt;/code&gt; tarball
and it installs that instead. The bundle vendors its own Node, so that path
skips the Node setup. &lt;code&gt;KICI_SCALER_MANAGED&lt;/code&gt; and a zero &lt;code&gt;KICI_SCALER_IDLE_TIMEOUT&lt;/code&gt;
are the whole contract: register, run one job, exit.&lt;/p&gt;
&lt;p&gt;And the scaler config that ties it together: an &lt;code&gt;event&lt;/code&gt; scaler, no new backend
type:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;scalers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;github-actions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    maxAgents&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;20&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # Repo identifiers, NOT workflow names: the reserved events are delivered&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # with `target.repos = provisioningTargets`, and the router filters&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # registrations by repo. A workflow name here matches no registration and&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # the scale-up reaches no subscriber at all.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    provisioningTargets&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;myorg/infra&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    labelSets&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;labels&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;github-actions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;where-each-file-lives&quot;&gt;Where each file lives&lt;/h2&gt;
&lt;p&gt;The pieces live in different places, and the GitHub repos do different jobs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The workflow repo&lt;/strong&gt; (&lt;code&gt;myorg/infra&lt;/code&gt;) holds the provisioning and teardown
workflows in &lt;code&gt;.kici/workflows/&lt;/code&gt;. They’re ordinary workflows, not
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/&quot;&gt;global ones&lt;/a&gt;. The scaler’s
&lt;code&gt;provisioningTargets&lt;/code&gt; names this repo, and KiCI delivers the scale-up and
scale-down events only to workflows registered there. KiCI has to receive its
pushes, like any repo it runs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The runner repo&lt;/strong&gt; (&lt;code&gt;myorg/ci-runners&lt;/code&gt;) holds &lt;code&gt;kici-agent.yml&lt;/code&gt; in
&lt;code&gt;.github/workflows/&lt;/code&gt;. Its Actions minutes are the ones you spend. KiCI never
needs its pushes: the provisioning workflow dispatches runs in it through
GitHub’s API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;scalers.yaml&lt;/code&gt;&lt;/strong&gt; sits with your orchestrator, not in either repo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your application repos don’t change, apart from the label: a job that may wait
in GitHub’s queue says &lt;code&gt;runsOn: [&apos;github-actions&apos;]&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-credential-you-can-safely-put-in-a-dispatch-input&quot;&gt;The credential you can safely put in a dispatch input&lt;/h2&gt;
&lt;p&gt;There’s a real hazard hiding in “dispatch a workflow with inputs.” GitHub treats
&lt;code&gt;workflow_dispatch&lt;/code&gt; inputs as ordinary event data: the run reads them from
&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#providing-inputs&quot;&gt;&lt;code&gt;github.event.inputs&lt;/code&gt;&lt;/a&gt;,
and GitHub’s security guide says to
&lt;a href=&quot;https://docs.github.com/en/actions/reference/security/secure-use#use-secrets-for-sensitive-information&quot;&gt;mask any sensitive value&lt;/a&gt;
that isn’t a GitHub secret. Put an agent token in an input, and one stray &lt;code&gt;echo&lt;/code&gt;
writes a live credential into the run’s log.&lt;/p&gt;
&lt;p&gt;So KiCI never passes the token. The scale-up event carries a single-use &lt;strong&gt;claim
code&lt;/strong&gt;, and the agent redeems it itself, over the same WebSocket it has to open
anyway, for a short-lived token that never leaves that connection. The claim code
is safe to log: it’s one-time, short-lived, and useless once redeemed. Pass it
through GitHub’s inputs, a cloud-init file, anywhere. The thing worth stealing
never travels.&lt;/p&gt;
&lt;h2 id=&quot;teardown-you-get-for-free&quot;&gt;Teardown you get for free&lt;/h2&gt;
&lt;p&gt;The scary part of “boot compute on demand” is the instance nobody cleaned up.
Here you mostly don’t have that problem: a GitHub Actions run ends when its
one-shot agent exits, and GitHub caps every job on a hosted runner at
&lt;a href=&quot;https://docs.github.com/en/actions/reference/limits#existing-system-limits&quot;&gt;6 hours&lt;/a&gt;
as a hard backstop. The run can’t outlive its job.&lt;/p&gt;
&lt;p&gt;There’s still a teardown workflow, but it does less than the cloud version’s
reaper. It only cancels a run GitHub has not yet marked finished, and only when
the agent will never do useful work, because it never started or it went silent:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  workflow,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  job,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  step,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  kiciEvent,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  SCALER_EVENT_NAMES,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ScaleDownReason,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ScalerScaleDownPayload,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;@kici-dev/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** Must match the `name:` of the `event` scaler in your `scalers.yaml`. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;github-actions&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; GH_WORKFLOW&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;kici-agent.yml&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * The only two reasons that mean the run will never do useful work:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; *   spawn-timeout      no agent ever registered against the spawn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; *   heartbeat-timeout  the agent registered, then went silent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; *&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Every other reason is left alone. `shutdown` in particular is what a HEALTHY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * one-shot agent emits when it exits after finishing its job — cancelling on it&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * would kill a run that is already succeeding. An unrecognised reason falls&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * through to doing nothing, which is always the safe default here: the run&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * reaps itself.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; CANCELABLE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ScaleDownReason&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;spawn-timeout&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;heartbeat-timeout&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;github-actions-autoscale-teardown&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kiciEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ name: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_EVENT_NAMES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.scaleDown, match: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;$.scalerName&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;teardown&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // Binds the `github-actions` context. Required for BOTH halves: the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // dispatch token this job reads, and `GITHUB_RUNNER_REPO` below — context&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // variables resolve only from the contexts a job binds, so an unbound&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // teardown reads `undefined` for the repo.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      context: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;github-actions&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      steps: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        step&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;cancel-stranded-run&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; p&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ScalerScaleDownPayload.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx.rawPayload);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;CANCELABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;includes&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(p.reason)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`teardown reason=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} agent=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} action=skip`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; runnerRepo&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GITHUB_RUNNER_REPO&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;myorg/ci-runners&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.secrets.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;GITHUB_DISPATCH_TOKEN&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; headers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            Authorization: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Bearer ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;token&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            Accept: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/vnd.github+json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;X-GitHub-Api-Version&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;2022-11-28&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // `kici-agent.yml` sets its `run-name` to `kici-agent {agent_id}`,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // which is how a scale-down finds the run its agent belongs to. One&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // page bounds it for most pools: the run is at most one spawn-timeout&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // window old, so a pool creating fewer than 100 runs of this workflow&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // inside that window always finds it here. A busier pool needs&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // pagination. What decides is the age of the RUN — the list is newest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // first — not how fast the teardown follows the scale-down.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; listed&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            `https://api.github.com/repos/${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;runnerRepo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/actions/workflows/${&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;GH_WORKFLOW&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/runs?per_page=100`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            { headers },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;listed.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            throw&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`listing runs failed: ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;listed&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} ${&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; listed&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; body&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; listed.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            workflow_runs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Array&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;{ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; number&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; body.workflow_runs.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;find&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; r.name &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; `kici-agent ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // Anything GitHub has not marked `completed` is still live — that&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // covers `queued` and `in_progress` plus the pre-start states&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // (`requested`, `waiting`, `pending`), which is exactly where a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // spawn-timeout run sits. Listing the live states instead would&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // decline to cancel the case this workflow exists for.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;run &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; run.status &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;completed&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;              `teardown reason=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} agent=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} action=skip (no live run)`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; cancelled&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            `https://api.github.com/repos/${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;runnerRepo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/actions/runs/${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}/cancel`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            { method: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, headers },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // 409 is GitHub refusing the cancel, most often because the run&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // finished between the list above and this call. A teardown the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // orchestrator could not deliver is retried, so treating that as a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // failure would fail the workflow forever over a run that is already&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // in the state the teardown wanted.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (cancelled.status &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 409&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;              `teardown reason=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} agent=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} action=skip `&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                `(run ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} not cancelable: 409, most often already completed)`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;cancelled.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            throw&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`cancel failed: ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;cancelled&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} ${&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cancelled&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;()&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            `teardown reason=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;reason&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} agent=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} action=cancel run=${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a job that already finished, there’s nothing to cancel: the run is gone.
Compare that to a cloud VM, where teardown is a delete call you’d better not miss.&lt;/p&gt;
&lt;h2 id=&quot;the-trade-off-you-inherit-githubs-queue-and-limits&quot;&gt;The trade-off: you inherit GitHub’s queue and limits&lt;/h2&gt;
&lt;p&gt;Compute you already pay for is a real win, and it comes with a real cost you
should see clearly. When you borrow someone else’s pool, you inherit that pool’s
guarantees. For GitHub Actions that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Queue latency becomes your scheduling latency.&lt;/strong&gt; A dispatched run waits for a
hosted runner before the job even starts, and GitHub discards a queued run that
no hosted runner picks up
&lt;a href=&quot;https://docs.github.com/en/actions/concepts/runners/github-hosted-runners#workflow-continuity&quot;&gt;within 45 minutes&lt;/a&gt;.
If you need an agent up in under a second, this is the wrong pool.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The minutes are GitHub’s, and so are the prices.&lt;/strong&gt; Past your plan’s included
minutes, a private repo pays GitHub’s per-minute rate:
&lt;a href=&quot;https://docs.github.com/en/billing/reference/actions-runner-pricing&quot;&gt;$0.006 a minute&lt;/a&gt;
for a standard Linux runner, as of September 2026.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your fan-out ceiling is GitHub’s concurrency limit,&lt;/strong&gt; not KiCI’s reservation
math. Standard hosted runners run up to
&lt;a href=&quot;https://docs.github.com/en/actions/reference/limits#job-concurrency-limits-for-github-hosted-runners&quot;&gt;20 concurrent jobs on Free, 60 on Team and 500 on Enterprise&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You get best-effort placement.&lt;/strong&gt; The example asks for &lt;code&gt;ubuntu-latest&lt;/code&gt;, with
no say over instance class or region, and a GitHub incident is a KiCI capacity
incident.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;KiCI doesn’t paper over this. When a provision fails (GitHub down, dispatch
rejected, minutes exhausted) the scaler reports the failure and backs off a pool
that keeps failing, instead of hammering it. You see the problem; you don’t get a
silent stall.&lt;/p&gt;
&lt;p&gt;So this is a choice, and it’s per scaler. Latency-critical jobs point at a cloud
scaler or a warm pool you keep hot. Bursty, tolerant work (nightly matrices, PR
fan-out, background jobs that need to finish sometime) points at the GitHub
pool. Same orchestrator, different label. You decide which jobs are allowed to
wait in a queue.&lt;/p&gt;
&lt;h2 id=&quot;the-same-pattern-on-gitlab-ci-nomad-or-a-spare-box&quot;&gt;The same pattern on GitLab CI, Nomad or a spare box&lt;/h2&gt;
&lt;p&gt;None of this is specific to GitHub. Look at what each piece needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What KiCI brings&lt;/strong&gt; is fixed and engine-independent: noticing demand, minting
the claim code, handing the pending job to whatever registers, spawn and idle
timeouts, and the backoff when a pool keeps failing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What you write&lt;/strong&gt; is the one part that changes: the step body that asks your
engine to start a process with a few env vars. For GitHub it’s a
&lt;code&gt;workflow_dispatch&lt;/code&gt;. For GitLab CI it’s a pipeline trigger. For Nomad it’s a
batch job. For a spare box in the corner it’s a queued &lt;code&gt;docker run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What the agent needs&lt;/strong&gt; is small and the same everywhere: a Node runtime, a
way out to the orchestrator, and a handful of env vars.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Any system that can run a process on request is a burst pool for KiCI. GitHub
Actions is the one almost everyone already pays for. And the trade-off carries
over too: each engine you borrow brings its own queue and limits.&lt;/p&gt;
&lt;h2 id=&quot;set-it-up&quot;&gt;Set it up&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Let your coding agent do it.&lt;/strong&gt; Every step below is a file to write or a
command to run, which is the kind of work a coding agent is good at. Give yours
this post’s URL and ask it to set up a GitHub Actions burst pool by following
these steps. For the SDK details, point it at
&lt;a href=&quot;https://kici.dev/llms.txt&quot;&gt;llms.txt&lt;/a&gt; or pipe &lt;code&gt;kici docs llm sdk&lt;/code&gt; into its
context.&lt;/p&gt;
&lt;p&gt;The steps use &lt;code&gt;myorg/infra&lt;/code&gt; for the workflow repo and &lt;code&gt;myorg/ci-runners&lt;/code&gt; for the
runner repo. Every file shown above is in
&lt;a href=&quot;https://github.com/kici-dev/kici-public/tree/main/examples/github-actions-autoscale&quot;&gt;&lt;code&gt;examples/github-actions-autoscale/&lt;/code&gt;&lt;/a&gt;,
ready to copy.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sign up and bring up an orchestrator.&lt;/strong&gt; Create a free account at
&lt;a href=&quot;https://app.kici.dev&quot;&gt;app.kici.dev&lt;/a&gt; and follow the
&lt;a href=&quot;https://docs.kici.dev/user/quickstart/&quot;&gt;5-minute quickstart&lt;/a&gt; through Part 2,
so pushes from your GitHub App reach the orchestrator.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create the workflow repo.&lt;/strong&gt; Create a repo such as &lt;code&gt;myorg/infra&lt;/code&gt; and install
the quickstart’s GitHub App on it, so KiCI receives its pushes. Clone it and
run &lt;code&gt;kici init&lt;/code&gt; in the clone. That creates &lt;code&gt;.kici/&lt;/code&gt;, with the &lt;code&gt;package.json&lt;/code&gt;
that declares &lt;code&gt;@kici-dev/sdk&lt;/code&gt;. Delete any starter workflow it adds that you
don’t want to run.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make the orchestrator reachable from GitHub’s runners.&lt;/strong&gt; A hosted runner
dials the orchestrator’s agent endpoint (&lt;code&gt;/ws&lt;/code&gt;) from GitHub’s network, so
expose that endpoint at an address the runners can reach, such as
&lt;code&gt;wss://ci.example.com/ws&lt;/code&gt;. If it differs from the orchestrator’s own
&lt;code&gt;KICI_ORCHESTRATOR_URL&lt;/code&gt;, set it as &lt;code&gt;orchestratorUrl&lt;/code&gt; on the scaler entry in
step 7.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick the agent that runs the provisioning workflow.&lt;/strong&gt; The provisioning
workflow runs on an agent too, and it can’t wait for the pool it’s growing.
The quickstart’s scaler already spawns one: in both KiCI workflows, you’ll
change &lt;code&gt;runsOn: [&apos;default&apos;]&lt;/code&gt; to that scaler’s labels, &lt;code&gt;[&apos;linux&apos;, &apos;container&apos;]&lt;/code&gt;
for the Docker quickstart or &lt;code&gt;[&apos;linux&apos;, &apos;bare-metal&apos;]&lt;/code&gt; for bare metal. Those
agents keep their default roles, so they also pack the workflow’s source
before the job runs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add the runner workflow to the runner repo.&lt;/strong&gt; Commit &lt;code&gt;kici-agent.yml&lt;/code&gt; to
&lt;code&gt;.github/workflows/kici-agent.yml&lt;/code&gt; on the runner repo’s default branch.
GitHub dispatches only a workflow file that’s on the default branch. The
provisioning workflow dispatches ref &lt;code&gt;main&lt;/code&gt;, so change &lt;code&gt;GH_REF&lt;/code&gt; in it if your
default branch has another name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Store the token in a &lt;code&gt;github-actions&lt;/code&gt; context.&lt;/strong&gt; Create a fine-grained
GitHub token with &lt;strong&gt;Actions: read and write&lt;/strong&gt; on the runner repo. Then, where
you run &lt;code&gt;kici-admin&lt;/code&gt; against your orchestrator, create the context, store the
token in its scope, and point it at the runner repo:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --org&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --name&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; github-actions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; secret&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; github-actions&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; GITHUB_DISPATCH_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --prompt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bind&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --org&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --env&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; github-actions&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --scope&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; github-actions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; variable&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; github-actions&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; GITHUB_RUNNER_REPO&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --value&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; myorg/ci-runners&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Leave the context’s protection rules off. An approval hold on it would hold
every scale-up, and the agents would never appear.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Declare the &lt;code&gt;event&lt;/code&gt; scaler.&lt;/strong&gt; Open the &lt;code&gt;scalers.yaml&lt;/code&gt; the quickstart
created: next to &lt;code&gt;docker-compose.yaml&lt;/code&gt; for Docker, or
&lt;code&gt;~/.config/kici/scalers.yaml&lt;/code&gt; for bare metal. Add the &lt;code&gt;github-actions&lt;/code&gt; entry
shown above under &lt;code&gt;scalers:&lt;/code&gt;, next to the quickstart’s own.
&lt;code&gt;provisioningTargets&lt;/code&gt; is the workflow repo from step 2, written as
&lt;code&gt;owner/repo&lt;/code&gt;. Load it with &lt;code&gt;kici-admin scaler reload&lt;/code&gt;. It checks the file
and applies all of it or none of it, on the orchestrator it points at and
every orchestrator connected to it, and prints what changed or why it
refused the file. Here it prints &lt;code&gt;added: github-actions&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add the two KiCI workflows to the workflow repo.&lt;/strong&gt; In the clone from
step 2, save &lt;code&gt;provision.workflow.ts&lt;/code&gt; as
&lt;code&gt;.kici/workflows/github-actions-provision.ts&lt;/code&gt; and &lt;code&gt;teardown.workflow.ts&lt;/code&gt; as
&lt;code&gt;.kici/workflows/github-actions-teardown.ts&lt;/code&gt;. Make the &lt;code&gt;runsOn&lt;/code&gt; change from
step 4 in both. The workflow repo then looks like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;myorg/infra/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── .kici/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── package.json&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── workflows/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    │   ├── github-actions-provision.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    │   └── github-actions-teardown.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── kici.lock.json          # written in the next step&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compile and push to the default branch.&lt;/strong&gt; From the root of the clone:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; compile&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; .kici/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; commit&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;ci: add GitHub Actions burst pool workflows&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KiCI registers the scale-up and scale-down subscriptions only from the
default branch’s lock file, so push to that branch (&lt;code&gt;main&lt;/code&gt; here). Until that
push, a scale-up reaches no one. Confirm both workflows registered with
&lt;code&gt;kici-admin registration list --repo myorg/infra&lt;/code&gt;: it lists
&lt;code&gt;github-actions-autoscale-provision&lt;/code&gt; and
&lt;code&gt;github-actions-autoscale-teardown&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Send jobs to the pool and watch them.&lt;/strong&gt; In any repo KiCI runs, give a job
you’re happy to let wait in GitHub’s queue &lt;code&gt;runsOn: [&apos;github-actions&apos;]&lt;/code&gt;, and
push it. That repo needs no other change. A &lt;code&gt;kici-agent &amp;#x3C;agent-id&gt;&lt;/code&gt; run
appears in the runner repo’s Actions tab. It registers, runs the job, and
finishes. If no run appears, the provisioning workflow’s own run shows the
dispatch error, and &lt;code&gt;kici-admin diagnose&lt;/code&gt; names the failure on the
&lt;code&gt;scaler:github-actions&lt;/code&gt; row.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The machine that runs a KiCI job can be anything that runs a process, and the
TypeScript that brings it up is yours.&lt;/p&gt;</content:encoded><dc:creator>Alberto Marchetti</dc:creator><category>autoscaling</category><category>workflows</category><category>github-actions</category><category>ci</category></item><item><title>Make your org-wide pipeline wait for each repo&apos;s own tests</title><link>https://kici.dev/blog/2026-08-19-repo-declared-tests-in-org-pipelines</link><guid isPermaLink="true">https://kici.dev/blog/2026-08-19-repo-declared-tests-in-org-pipelines</guid><description>invokeSource lets an org-wide pipeline call each repo&apos;s own test workflow and wait for it, so a deploy never ships ahead of the tests.</description><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;a-shared-pipeline-and-each-repos-tests-end-up-racing&quot;&gt;A shared pipeline and each repo’s tests end up racing&lt;/h2&gt;
&lt;p&gt;The org pipeline knows how to build and deploy. What it does not know is how any
one repo tests itself: a service with a Postgres integration suite, a library
with a fuzz run, a frontend with a Playwright pass. Those live in the repo, and
they differ from repo to repo.&lt;/p&gt;
&lt;p&gt;That leaves you two bad options. You let the repo’s own CI run independently, so
the org’s deploy step and the repo’s tests fan out off the same push and race:
the deploy can ship before the tests finish. Or you copy each repo’s test steps
up into the org pipeline, and now the org pipeline is a pile of per-repo special
cases that drifts the moment a repo changes how it tests.&lt;/p&gt;
&lt;p&gt;The frame is right (one pipeline, every repo) but it has no extension point.
The org owns the frame; each repo needs to fill in its own slot, and the frame
needs to wait for it.&lt;/p&gt;
&lt;h2 id=&quot;invokesource-call-each-repos-tests-and-wait&quot;&gt;invokeSource: call each repo’s tests and wait&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;invokeSource&lt;/code&gt; is that extension point, and the
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/#invoking-a-source-repos-own-workflows&quot;&gt;global workflows guide&lt;/a&gt;
covers it in full. A job in the org pipeline calls it, and instead of running
steps on an agent, the job emits a named event &lt;strong&gt;at the repo that triggered the
pipeline&lt;/strong&gt; and waits for every workflow there that opted in. Each opted-in
workflow that fires becomes a job in the org run’s own graph, so you watch the
repo’s tests in the same run as the deploy, and the downstream jobs gate on them.&lt;/p&gt;
&lt;p&gt;Here is the org side. The &lt;code&gt;repo-tests&lt;/code&gt; job hands control back to the source repo,
and &lt;code&gt;deploy&lt;/code&gt; needs it, so the deploy runs only after the repo’s tests pass:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org-ci-with-repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ repos: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;myorg/*&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], branches: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;main&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Hand control to the source repo: emit `org.repo-tests` there and gate on&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // every workflow that subscribes. Required by default.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { invoke: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;invokeSource&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org.repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // Ships only after the source repo&apos;s own tests reported success.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;deploy&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      needs: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;kici:os:linux&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;      run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`./deploy.sh`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is a repo filling the slot. This workflow lives in an application repo,
not the org pipeline. It subscribes to the org’s event with &lt;code&gt;kiciEvent&lt;/code&gt;, so when
the org pipeline invokes the repo, this runs and the gate waits for it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kiciEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ name: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org.repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;unit&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;kici:os:linux&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;      run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`pnpm test`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the whole contract. The org names an event; a repo opts in by listening
for it. Add the one file and your tests are in the org pipeline; the org pipeline
never had to know they existed.&lt;/p&gt;
&lt;h2 id=&quot;required-by-default-a-repo-that-forgot-fails-loud&quot;&gt;Required by default: a repo that forgot fails loud&lt;/h2&gt;
&lt;p&gt;Here is the part that matters at three in the morning. What happens when a repo
&lt;strong&gt;never&lt;/strong&gt; wired up its tests, with no subscriber listening for the event?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;invokeSource&lt;/code&gt; is required by default. Zero subscribers is a failed gate, not a
pass. The &lt;code&gt;deploy&lt;/code&gt; job needs the gate, so it does not run. A repo that quietly
dropped its test workflow does not sail through the org pipeline green; it stops,
and you find out because the pipeline is red, not because production is.&lt;/p&gt;
&lt;p&gt;This is the opposite of the racing setup, where a repo with no tests looks
exactly like a repo whose tests happened to pass. Here, “no tests ran” is a
distinct, loud outcome.&lt;/p&gt;
&lt;h2 id=&quot;opt-out-on-purpose-with-optional&quot;&gt;Opt out on purpose with &lt;code&gt;optional&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Required-by-default is the safe default, but some repos have nothing to run: a
docs-only repo, a config repo. For those, the org pipeline sets
&lt;code&gt;optional: true&lt;/code&gt; on the gate:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { invoke: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;invokeSource&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org.repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { optional: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now zero subscribers is a green skip, and the downstream still runs. The
difference is intent: with &lt;code&gt;optional&lt;/code&gt;, “no tests” is a decision the org made, not
a repo that forgot. You choose which repos are allowed to have nothing to say.&lt;/p&gt;
&lt;h2 id=&quot;decide-per-repo-at-runtime&quot;&gt;Decide per repo at runtime&lt;/h2&gt;
&lt;p&gt;The gate can also be generated at runtime. A &lt;code&gt;DynamicJobFn&lt;/code&gt; runs with the source
repo checked out, so it can read the repo and decide whether to invoke at all.
This variant reads the repo’s &lt;code&gt;package.json&lt;/code&gt; and adds the gate only for a repo
that declares a &lt;code&gt;ci:test&lt;/code&gt; script, so the org invokes each repo’s tests exactly
where they exist:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; gateWhereTestsExist&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; DynamicJobFn&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;sourceRepo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; root&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sourceRepo?.path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;.&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; pkg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; readFile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(root, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;package.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;utf8&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; declaresTests&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;ci:test&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (pkg.scripts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // A repo that declares its own tests gets an invoke gate; one that does not is&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // left un-gated rather than failed.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; declaresTests &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, { invoke: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;invokeSource&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org.repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) })] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org-ci-matrix-repo-tests&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ repos: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;myorg/*&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], branches: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;main&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [gateWhereTestsExist],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is plain TypeScript against the tree on disk, so the rule can be anything you
can express in code: gate the repos that ship a Dockerfile, invoke a different
event for a repo tagged one way versus another, read a repo-local config file and
branch on it. The org pipeline decides per repo what to summon, and the repos
decide what to answer with.&lt;/p&gt;
&lt;h2 id=&quot;reading-the-results&quot;&gt;Reading the results&lt;/h2&gt;
&lt;p&gt;An invoked run can report outputs, and they cross back to the downstream job as
&lt;code&gt;ctx.needs[&apos;repo-tests&apos;].result&lt;/code&gt;: coverage numbers, a build id, whatever the
repo’s tests emit. Secret outputs stay masked and never cross the boundary, so a
repo can report a token to its own steps without leaking it into the org pipeline.
The org gets exactly what the repo chose to publish, and nothing it did not.&lt;/p&gt;
&lt;h2 id=&quot;when-you-dont-need-invokesource&quot;&gt;When you don’t need invokeSource&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A single repo.&lt;/strong&gt; A repo’s own workflow can already make its deploy job
&lt;code&gt;needs&lt;/code&gt; its test job. The extra hop through an event buys nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tests that are the same in every repo.&lt;/strong&gt; If every repo runs &lt;code&gt;npm test&lt;/code&gt; the
same way, the global workflow can run those tests itself, with
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/#generating-jobs-per-source-repo&quot;&gt;jobs generated per source repo&lt;/a&gt;.
&lt;code&gt;invokeSource&lt;/code&gt; earns its place when each repo tests itself differently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A slow repo suite holds the org deploy.&lt;/strong&gt; The gate waits for every run it
summoned, and a run held for an approval waits as long as the hold lasts. Give
the gate job a &lt;code&gt;timeout&lt;/code&gt;, in milliseconds, so the wait is bounded; the
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/#standard-job-options-apply&quot;&gt;guide&lt;/a&gt;
shows it next to the other job options.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;set-it-up&quot;&gt;Set it up&lt;/h2&gt;
&lt;p&gt;The steps use the event name &lt;code&gt;org.repo-tests&lt;/code&gt; from the snippets above.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sign up and bring up an orchestrator.&lt;/strong&gt; Create a free account at &lt;a href=&quot;https://app.kici.dev&quot;&gt;app.kici.dev&lt;/a&gt; and follow the &lt;a href=&quot;https://docs.kici.dev/user/quickstart/&quot;&gt;5-minute quickstart&lt;/a&gt; through Part 2, so KiCI receives your GitHub org’s events. Install the GitHub App it uses on the whole org, or on the workflow repo plus every repo the org pipeline’s &lt;code&gt;repos:&lt;/code&gt; glob covers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up global workflows.&lt;/strong&gt; Follow the
&lt;a href=&quot;https://kici.dev/blog/2026-08-14-org-wide-ci-with-global-workflows#set-it-up&quot;&gt;steps in the global-workflows post&lt;/a&gt;
from step 2: switch global workflows on, and set up a workflow repo that
holds the org pipeline.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick the event name.&lt;/strong&gt; It’s the contract between the org and every repo,
so choose one and keep it. Names that start with &lt;code&gt;kici.&lt;/code&gt; or &lt;code&gt;__&lt;/code&gt; are
reserved, and &lt;code&gt;invokeSource()&lt;/code&gt; rejects them when you compile.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add the gate to the org pipeline.&lt;/strong&gt; In the workflow repo’s
&lt;code&gt;.kici/workflows/&lt;/code&gt;, add a job with &lt;code&gt;invoke: invokeSource(&apos;org.repo-tests&apos;)&lt;/code&gt;
and make every job that must wait for the tests &lt;code&gt;needs&lt;/code&gt; it, as the first
snippet does. Pass &lt;code&gt;{ optional: true }&lt;/code&gt; if some repos may have nothing to run.
Give the gate job a &lt;code&gt;timeout&lt;/code&gt; if a repo’s test run can be held for an
approval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compile and push the org pipeline.&lt;/strong&gt; Run &lt;code&gt;kici compile&lt;/code&gt;, commit the
workflow with &lt;code&gt;.kici/kici.lock.json&lt;/code&gt;, and push to the workflow repo’s default
branch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Opt each repo in.&lt;/strong&gt; In each application repo that should report its tests,
run &lt;code&gt;npx kici init&lt;/code&gt; if it has no &lt;code&gt;.kici/&lt;/code&gt; folder yet. Add a workflow that
subscribes with &lt;code&gt;kiciEvent({ name: &apos;org.repo-tests&apos; })&lt;/code&gt;, like the second
snippet. It’s an ordinary workflow in that repo, so it can bind the repo’s
own contexts and secrets. Run &lt;code&gt;kici compile&lt;/code&gt;, commit it with
&lt;code&gt;.kici/kici.lock.json&lt;/code&gt;, and push to that repo’s default branch. KiCI registers
the subscription only from the default branch’s lock file, and until it does,
the gate finds no subscriber in that repo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push to an opted-in repo and watch the run.&lt;/strong&gt; Push to &lt;code&gt;main&lt;/code&gt; in that repo.
The org run shows the repo’s workflow as a job under &lt;code&gt;repo-tests&lt;/code&gt;, and
&lt;code&gt;deploy&lt;/code&gt; starts only after it passes. A repo with no subscriber shows a failed
&lt;code&gt;repo-tests&lt;/code&gt; gate instead, or a green skip when the gate is optional.&lt;/li&gt;
&lt;/ol&gt;</content:encoded><dc:creator>Alberto Marchetti</dc:creator><category>global-workflows</category><category>ci</category><category>monorepo</category><category>typescript</category></item><item><title>Autoscale CI onto any cloud with a TypeScript workflow</title><link>https://kici.dev/blog/2026-08-17-autoscaling-as-a-typescript-workflow</link><guid isPermaLink="true">https://kici.dev/blog/2026-08-17-autoscaling-as-a-typescript-workflow</guid><description>KiCI ships no cloud drivers. A TypeScript workflow you write boots the machine, so any cloud with an API can host your CI agents.</description><pubDate>Mon, 17 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;autoscaling-ci-usually-waits-on-a-driver-for-your-cloud&quot;&gt;Autoscaling CI usually waits on a driver for your cloud&lt;/h2&gt;
&lt;p&gt;You want your CI to burst onto cloud compute when the queue backs up, then give
it back when it drains. The common autoscalers get there with a &lt;strong&gt;driver per
cloud&lt;/strong&gt;. GitLab Runner’s autoscaler talks to each provider through a separate
&lt;a href=&quot;https://docs.gitlab.com/runner/fleet_scaling/fleeting/&quot;&gt;fleeting plugin&lt;/a&gt;: AWS,
Google Cloud and Azure officially, and others from the community. Buildkite ships
an &lt;a href=&quot;https://buildkite.com/docs/agent/self-hosted/aws/elastic-ci-stack&quot;&gt;Elastic CI Stack&lt;/a&gt;
per cloud. Each driver is provider SDK code that somebody has to write, test and
keep current as the provider’s API moves.&lt;/p&gt;
&lt;p&gt;If your cloud isn’t on the list, you wait for someone to build the driver. If it
is, you inherit whatever the driver’s authors decided about instance types,
images and lifecycle, and you configure around it. Your image, your network and
your budget all sit behind another team’s release cycle.&lt;/p&gt;
&lt;h2 id=&quot;the-event-scaler-hands-provisioning-to-your-workflow&quot;&gt;The event scaler hands provisioning to your workflow&lt;/h2&gt;
&lt;p&gt;KiCI’s scaler has an &lt;code&gt;event&lt;/code&gt; backend that performs no cloud calls of its own.
When demand rises, its &lt;code&gt;spawn()&lt;/code&gt; emits a reserved &lt;code&gt;kici.scaler.scale-up&lt;/code&gt; event;
when an instance should go away, &lt;code&gt;destroy()&lt;/code&gt; emits &lt;code&gt;kici.scaler.scale-down&lt;/code&gt;.
Those are ordinary KiCI custom events, and you consume them with the same
&lt;code&gt;kiciEvent()&lt;/code&gt; trigger you’d use for anything else, in a &lt;strong&gt;provisioning workflow
you write in TypeScript&lt;/strong&gt;. The
&lt;a href=&quot;https://docs.kici.dev/user/workflows/autoscaling-workflows/&quot;&gt;workflow-driven autoscaling guide&lt;/a&gt;
covers the whole contract.&lt;/p&gt;
&lt;p&gt;So “cloud autoscaling” stops being “which drivers ship this quarter” and becomes
“write a workflow.” KiCI keeps the hard, generic parts: demand detection,
capacity caps, reservations, dispatching the pending job to the new machine,
spawn timeouts, idle and heartbeat teardown. It leaves one thing open on
purpose: how a machine comes up. That’s the extension point. Plug in any cloud’s
API there and it works, with no driver required, so KiCI ships &lt;strong&gt;zero&lt;/strong&gt; cloud
SDK code and you’re never limited to a supported list.&lt;/p&gt;
&lt;p&gt;The credential handoff is built for this. The scale-up event carries a
single-use &lt;strong&gt;claim code&lt;/strong&gt;, not a token. Your workflow forwards that code into the
instance’s cloud-init, and the agent exchanges it for its own short-lived token
once it boots. The token is minted inside the machine that uses it, and never
transits provisioning or the persisted event log at all.&lt;/p&gt;
&lt;h2 id=&quot;writing-the-provisioning-workflow&quot;&gt;Writing the provisioning workflow&lt;/h2&gt;
&lt;p&gt;Here’s the reference provisioning workflow. It subscribes to the scale-up event,
forwards the single-use claim code into a cloud-init, and boots a labelled
instance whose agent claims its own token and registers under the scaler-chosen
&lt;code&gt;agentId&lt;/code&gt;. This one targets Hetzner; for another cloud, swap the boot call for
that cloud’s API and keep the shape.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  workflow,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  job,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  kiciEvent,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  buildAgentCloudInit,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  SCALER_EVENT_NAMES,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ScalerScaleUpPayload,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;@kici-dev/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;hetzner&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/** Hard lifetime cap for a provisioned instance (teardown layer L2). */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; MAX_LIFETIME_MINUTES&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;hetzner-autoscale-provision&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kiciEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ name: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_EVENT_NAMES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.scaleUp, match: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;$.scalerName&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;provision&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // Bind the context whose scope holds the credential this job reads.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // Without it `ctx.secrets` resolves nothing: the job option is `context`,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // and an unrecognised key is dropped at compile time rather than&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // rejected, so the step would fail on a missing secret at run time.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      context: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;hetzner-autoscale&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;      run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; payload&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ScalerScaleUpPayload.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx.rawPayload);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // Forward the single-use claim code into cloud-init; the agent claims&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        // its own token in-instance, so the token never transits provisioning.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; userData&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; buildAgentCloudInit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            claimCode: payload.claimCode,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            agentId: payload.agentId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            orchestratorUrl: payload.orchestratorUrl,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            labels: payload.labels,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            maxLifetimeMinutes: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;MAX_LIFETIME_MINUTES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // Container delivery starts the agent with `docker run`, and the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // stock `debian-12` image below ships no Docker — so install it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            // Drop this line when your image already carries Docker.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            deliveryMode: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;container&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            packages: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;docker.io&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            agentEnv: &lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;e2eAgentEnv&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx.env),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.secrets.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;HETZNER_API_TOKEN&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; HetznerClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(token);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;createServer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          name: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`kici-agent-${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // A current-generation shared-vCPU type available in the region below.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          server_type: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;cpx12&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          image: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;debian-12&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          user_data: userData,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;          // Every teardown layer keys off these labels.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          labels: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;kici-managed&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;hetzner-autoscale&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;kici-agent-id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: payload.agentId,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;kici-scaler&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            ...&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;e2eServerLabels&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx.env),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Provisioned Hetzner server ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} for agent ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything provider-specific lives in the &lt;code&gt;run&lt;/code&gt; body: the instance type, the
image, the labels, the API call. &lt;code&gt;HetznerClient&lt;/code&gt; here is a thin wrapper over
Hetzner’s REST API; the copy in the guide makes the same calls with &lt;code&gt;fetch&lt;/code&gt;, so
it needs nothing installed. The KiCI-specific parts are the &lt;code&gt;kiciEvent&lt;/code&gt; trigger,
the payload parse, and the &lt;code&gt;buildAgentCloudInit&lt;/code&gt; call. There’s no backend to
register and no plugin to install. Supporting a new cloud isn’t a feature
request; it’s a new workflow you write today.&lt;/p&gt;
&lt;h2 id=&quot;where-the-provisioning-workflows-live&quot;&gt;Where the provisioning workflows live&lt;/h2&gt;
&lt;p&gt;The provisioning and teardown workflows are ordinary workflows, not
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/&quot;&gt;global ones&lt;/a&gt;. They live in one
repo you pick, and the scaler’s &lt;code&gt;provisioningTargets&lt;/code&gt; names it. KiCI delivers the
scale-up and scale-down events only to workflows registered in that repo, so a
&lt;code&gt;kiciEvent&lt;/code&gt; subscription somewhere else never boots a server.&lt;/p&gt;
&lt;p&gt;A dedicated infra repo is the tidy choice: the provisioning code and its reviews
stay in one place, and your application repos don’t change at all. A job that
wants a Hetzner machine says &lt;code&gt;runsOn: [&apos;hetzner&apos;]&lt;/code&gt;, and that’s it. Here’s the
whole infra repo:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;myorg/infra/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── .kici/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── package.json            # declares @kici-dev/sdk (kici init writes it)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── workflows/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    │   ├── hetzner-provision.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    │   └── hetzner-teardown.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── kici.lock.json          # written by kici compile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things make the repo live. KiCI has to receive its pushes, and the two
workflows have to reach its default branch, because event triggers register only
from the default branch’s lock file. The setup steps below cover both.&lt;/p&gt;
&lt;h2 id=&quot;teardown-that-cant-leak-a-machine&quot;&gt;Teardown that can’t leak a machine&lt;/h2&gt;
&lt;p&gt;The scary part of “boot a cloud instance on demand” is the one you don’t see: the
instance that &lt;em&gt;doesn’t&lt;/em&gt; get cleaned up when something crashes mid-run, quietly
billing you for a week. So teardown doesn’t rely on any single path succeeding.&lt;/p&gt;
&lt;p&gt;The scale-down event fires the mirror-image &lt;strong&gt;teardown workflow&lt;/strong&gt;, which deletes
the servers for the scaled-down agent by label:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  workflow,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  job,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  kiciEvent,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  SCALER_EVENT_NAMES,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ScalerScaleDownPayload,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;@kici-dev/sdk&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;hetzner&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;hetzner-autoscale-teardown&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kiciEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ name: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_EVENT_NAMES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.scaleDown, match: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;$.scalerName&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SCALER_NAME&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;teardown&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // Bind the context whose scope holds the credential this job reads.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // Without it `ctx.secrets` resolves nothing: the job option is `context`,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // and an unrecognised key is dropped at compile time rather than&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      // rejected, so the step would fail on a missing secret at run time.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      context: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;hetzner-autoscale&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;      run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; payload&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ScalerScaleDownPayload.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx.rawPayload);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; token&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.secrets.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;HETZNER_API_TOKEN&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; client&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; HetznerClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(token);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; servers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;listByLabel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`kici-agent-id==${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (servers.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ===&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            `No Hetzner server found for agent ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}; nothing to tear down`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; server&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; servers) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;deleteServer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(server.id);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;          ctx.log.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`Deleted Hetzner server ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;server&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} for agent ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payload&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;agentId&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the happy path. Underneath it, the instance also shuts &lt;em&gt;itself&lt;/em&gt; down: the
cloud-init bakes in a hard lifetime cap that powers the machine off once it
expires, so a machine that loses contact with the orchestrator still stops on its
own. And every resource is labelled at creation
(&lt;code&gt;kici-managed=hetzner-autoscale&lt;/code&gt;), which lets a small &lt;strong&gt;reaper&lt;/strong&gt; run on a timer
and delete every labelled machine older than its TTL. The reaper is the backstop
for the cases nothing else covers (a hard kill, a crashed orchestrator, a
rebooted host) because it depends on nothing but the label and the clock. A
leaked instance is measured in minutes, not invoices.&lt;/p&gt;
&lt;p&gt;The reaper is a dependency-free script plus the thin API client it calls,
published as an example you can copy. Its core lists the labelled servers and
deletes the ones past the TTL:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Delete servers older than the TTL. A delete 404 (already gone) counts as a&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * success. A delete that fails is recorded and the run moves on, so one server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; * Hetzner refuses to delete never shields the servers listed after it.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; reap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;client&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ReapableClient&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;opts&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ReapOptions&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Promise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ReapResult&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // An empty selector lists every server in the project, so only an explicit&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // whole-project sweep may send one.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;opts.sweepWholeProject &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; opts.managedLabel.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;trim&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    throw&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;reap: managedLabel is empty; set sweepWholeProject to sweep every server&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; selector&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; opts.sweepWholeProject &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; opts.managedLabel;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; servers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;listByLabel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(selector);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; cutoff&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; opts.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;now&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; opts.olderThanMs;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; result&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ReapResult&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { deleted: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, failed: [] };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; server&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; of&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; servers) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; createdMs&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Date.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(server.created);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // A server whose timestamp cannot be parsed is left alone (fail-safe: never&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    // delete something we cannot age).&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (Number.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;isNaN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(createdMs) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; createdMs &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cutoff) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;continue&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;deleteServer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(server.id);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      result.deleted &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; error&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;instanceof&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Error&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; err.message &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(err);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      result.failed.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ id: server.id, name: server.name, error });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;when-you-dont-need-a-provisioning-workflow&quot;&gt;When you don’t need a provisioning workflow&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One host, or a fixed pool of machines.&lt;/strong&gt; The built-in
&lt;a href=&quot;https://docs.kici.dev/operator/orchestrator/auto-scaler/&quot;&gt;&lt;code&gt;container&lt;/code&gt; scaler&lt;/a&gt;
starts a one-shot agent container when a job arrives and removes it when the
job ends. There’s no workflow to write, and it’s the backend most setups start
with.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You’d rather not own the provisioning code.&lt;/strong&gt; With the &lt;code&gt;event&lt;/code&gt; scaler, the
provisioning workflow, the teardown workflow and the reaper are yours. When
your cloud’s API changes, you update them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jobs that can’t wait for a machine to boot.&lt;/strong&gt; A cloud instance has to boot
and start the agent before the job runs, so a queued job waits for that. A
&lt;a href=&quot;https://docs.kici.dev/operator/orchestrator/auto-scaler/common-config/#warm-pool&quot;&gt;warm pool&lt;/a&gt;
keeps ready agents for a label set, at the cost of paying for them while they
sit idle.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;set-it-up&quot;&gt;Set it up&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Let your coding agent do it.&lt;/strong&gt; Every step below is a file to write or a
command to run, which is the kind of work a coding agent is good at. Give yours
this post’s URL and ask it to set up a Hetzner event scaler (or one for your own
cloud) by following these steps. For the SDK details, point it at
&lt;a href=&quot;https://kici.dev/llms.txt&quot;&gt;llms.txt&lt;/a&gt; or pipe &lt;code&gt;kici docs llm sdk&lt;/code&gt; into its
context.&lt;/p&gt;
&lt;p&gt;The copy-ready provisioning and teardown workflows are in the
&lt;a href=&quot;https://docs.kici.dev/user/workflows/autoscaling-workflows/&quot;&gt;workflow-driven autoscaling guide&lt;/a&gt;;
they read their token as &lt;code&gt;HETZNER_API_TOKEN&lt;/code&gt; from a &lt;code&gt;hetzner-autoscale&lt;/code&gt; context.
Another cloud follows the same steps with its own API calls.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sign up and bring up an orchestrator.&lt;/strong&gt; Create a free account at
&lt;a href=&quot;https://app.kici.dev&quot;&gt;app.kici.dev&lt;/a&gt; and follow the
&lt;a href=&quot;https://docs.kici.dev/user/quickstart/&quot;&gt;5-minute quickstart&lt;/a&gt; through Part 2,
so pushes from your GitHub App reach the orchestrator. You also need a
Hetzner Cloud project.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create the repo that holds the provisioning workflows.&lt;/strong&gt; Create a repo
such as &lt;code&gt;myorg/infra&lt;/code&gt; and install the quickstart’s GitHub App on it, so KiCI
receives its pushes. Clone it and run &lt;code&gt;kici init&lt;/code&gt; in the clone. That creates
&lt;code&gt;.kici/&lt;/code&gt;, with the &lt;code&gt;package.json&lt;/code&gt; that declares &lt;code&gt;@kici-dev/sdk&lt;/code&gt;. Delete any
starter workflow it adds that you don’t want to run.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make the orchestrator reachable from your instances.&lt;/strong&gt; Each instance’s
agent dials the orchestrator’s agent endpoint (&lt;code&gt;/ws&lt;/code&gt;) from the cloud
network, so expose that endpoint where the instances can reach it. If the
address differs from the orchestrator’s own &lt;code&gt;KICI_ORCHESTRATOR_URL&lt;/code&gt;, set it
as &lt;code&gt;orchestratorUrl&lt;/code&gt; on the scaler entry in step 6.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pick the agent that runs the provisioning workflow.&lt;/strong&gt; The provisioning
workflow runs on an agent too, and it can’t wait for the pool it’s growing.
The quickstart’s scaler already spawns one: in both workflows, you’ll change
&lt;code&gt;runsOn: [&apos;default&apos;]&lt;/code&gt; to that scaler’s labels, &lt;code&gt;[&apos;linux&apos;, &apos;container&apos;]&lt;/code&gt; for
the Docker quickstart or &lt;code&gt;[&apos;linux&apos;, &apos;bare-metal&apos;]&lt;/code&gt; for bare metal. Those
agents keep their default roles, so they also pack the workflow’s source
before the job runs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Store the cloud token in a context.&lt;/strong&gt; Create a Read &amp;#x26; Write API token in
the Hetzner project. Then, where you run &lt;code&gt;kici-admin&lt;/code&gt; against your
orchestrator, create the context, store the token in its scope, and bind the
two:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --org&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --name&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; hetzner-autoscale&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; secret&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; hetzner-autoscale&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; HETZNER_API_TOKEN&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --prompt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici-admin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; bind&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --org&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;orgI&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --env&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; hetzner-autoscale&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --scope&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; hetzner-autoscale&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Leave the context’s protection rules off. An approval hold on it would hold
every scale-up, and the agents would never appear.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Declare the &lt;code&gt;event&lt;/code&gt; scaler.&lt;/strong&gt; Open the &lt;code&gt;scalers.yaml&lt;/code&gt; the quickstart
created: next to &lt;code&gt;docker-compose.yaml&lt;/code&gt; for Docker, or
&lt;code&gt;~/.config/kici/scalers.yaml&lt;/code&gt; for bare metal. Add a &lt;code&gt;hetzner&lt;/code&gt; entry under
&lt;code&gt;scalers:&lt;/code&gt;, next to the quickstart’s own. &lt;code&gt;provisioningTargets&lt;/code&gt; is the repo
from step 2, written as &lt;code&gt;owner/repo&lt;/code&gt;, and the label set is what your jobs
will ask for:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;scalers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  # ... the quickstart&apos;s scaler stays here ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;hetzner&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;event&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    maxAgents&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    provisioningTargets&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;myorg/infra&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;    labelSets&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      - &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;labels&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;hetzner&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Load it with &lt;code&gt;kici-admin scaler reload&lt;/code&gt;. It checks the file and applies all
of it or none of it, on the orchestrator it points at and every orchestrator
connected to it, and prints what changed or why it refused the file. Here it
prints &lt;code&gt;added: hetzner&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add the two workflows to the repo.&lt;/strong&gt; In the clone from step 2, save the
guide’s provisioning workflow as &lt;code&gt;.kici/workflows/hetzner-provision.ts&lt;/code&gt; and
its teardown workflow as &lt;code&gt;.kici/workflows/hetzner-teardown.ts&lt;/code&gt;. Make the
&lt;code&gt;runsOn&lt;/code&gt; change from step 4 in both. The provisioning workflow starts the
agent with &lt;code&gt;docker run&lt;/code&gt; and installs &lt;code&gt;docker.io&lt;/code&gt; on the stock &lt;code&gt;debian-12&lt;/code&gt;
image; drop that &lt;code&gt;packages&lt;/code&gt; line if you boot an image that already has
Docker.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Compile and push to the default branch.&lt;/strong&gt; From the root of the clone:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;kici&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; compile&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; add&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; .kici/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; commit&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;ci: add Hetzner autoscaling workflows&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; push&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; origin&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; main&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;KiCI registers the scale-up and scale-down subscriptions only from the
default branch’s lock file, so push to that branch (&lt;code&gt;main&lt;/code&gt; here). Until that
push, a scale-up reaches no one. Confirm both workflows registered with
&lt;code&gt;kici-admin registration list --repo myorg/infra&lt;/code&gt;: it lists
&lt;code&gt;hetzner-autoscale-provision&lt;/code&gt; and &lt;code&gt;hetzner-autoscale-teardown&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Schedule the reaper.&lt;/strong&gt; Copy &lt;code&gt;reap.ts&lt;/code&gt; and &lt;code&gt;hetzner-client.ts&lt;/code&gt; from
&lt;a href=&quot;https://github.com/kici-dev/kici-public/tree/main/examples/hetzner-autoscale&quot;&gt;&lt;code&gt;examples/hetzner-autoscale/&lt;/code&gt;&lt;/a&gt;
into one directory on a host that isn’t one of the pool’s servers and has
Node.js 22.18 or later. Put a project token in &lt;code&gt;HCLOUD_TOKEN&lt;/code&gt;, set
&lt;code&gt;KICI_HETZNER_REAP_TTL_MIN&lt;/code&gt; above your longest job, and run &lt;code&gt;node reap.ts&lt;/code&gt;
from a timer every few minutes. The example’s README has a systemd service and
timer to copy, and the
&lt;a href=&quot;https://docs.kici.dev/operator/orchestrator/hetzner-autoscale-reaper&quot;&gt;teardown and reaper runbook&lt;/a&gt;
covers the TTL to pick and the alerts to set on it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Send a job to the pool and watch it.&lt;/strong&gt; In any repo KiCI runs, give a job
&lt;code&gt;runsOn: [&apos;hetzner&apos;]&lt;/code&gt; and push it. That repo needs no other change. A
&lt;code&gt;kici-agent-&amp;#x3C;agent-id&gt;&lt;/code&gt; server appears in the Hetzner console, its agent
registers and runs the job, and the teardown workflow deletes the server
once the agent goes idle. If no agent registers, the provisioning
workflow’s own run shows the API error, and &lt;code&gt;kici-admin diagnose&lt;/code&gt; names the
failure on the &lt;code&gt;scaler:hetzner&lt;/code&gt; row.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The guide also covers the event contract, the &lt;code&gt;buildAgentCloudInit&lt;/code&gt; options, and
the AWS variant of the same workflow.&lt;/p&gt;</content:encoded><dc:creator>Alberto Marchetti</dc:creator><category>autoscaling</category><category>workflows</category><category>cloud</category><category>typescript</category></item><item><title>Run one CI pipeline across every repo in your org</title><link>https://kici.dev/blog/2026-08-14-org-wide-ci-with-global-workflows</link><guid isPermaLink="true">https://kici.dev/blog/2026-08-14-org-wide-ci-with-global-workflows</guid><description>Global workflows run one pipeline on every repo in your org from a single workflow repo, with no file added to the repos it covers.</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;sharing-ci-today-means-a-file-in-every-repo&quot;&gt;Sharing CI today means a file in every repo&lt;/h2&gt;
&lt;p&gt;Say you run forty repositories and you want the same test-and-build policy on all
of them. The usual answer is to factor the shared steps into a reusable unit,
then go to each of the forty repos and add a file that calls it. A GitHub Actions
workflow calls a &lt;a href=&quot;https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows#calling-a-reusable-workflow&quot;&gt;reusable workflow&lt;/a&gt;
with &lt;code&gt;uses:&lt;/code&gt;, a GitLab pipeline pulls shared YAML in with
&lt;a href=&quot;https://docs.gitlab.com/ci/yaml/includes/&quot;&gt;&lt;code&gt;include:&lt;/code&gt;&lt;/a&gt;, and a Buildkite pipeline
adds &lt;a href=&quot;https://buildkite.com/docs/pipelines/integrations/plugins/using&quot;&gt;plugins&lt;/a&gt;
to its own steps. The shared logic lives in one place, but every repo still opts
in by carrying a file that points at it.&lt;/p&gt;
&lt;p&gt;That’s forty pull requests to roll out a policy, forty more to change it, and
forty chances for a repo to drift out of sync or never adopt it at all.&lt;/p&gt;
&lt;p&gt;Some paid plans can push jobs from the top, as a compliance feature. GitLab
Ultimate’s &lt;a href=&quot;https://docs.gitlab.com/user/application_security/policies/pipeline_execution_policies/&quot;&gt;pipeline execution policies&lt;/a&gt;
enforce CI jobs on projects, even ones with no CI file. GitHub rulesets can
&lt;a href=&quot;https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#require-workflows-to-pass-before-merging&quot;&gt;require a workflow&lt;/a&gt;
from a central repo to pass before a pull request merges. Outside those settings,
sharing CI still means one file per repo.&lt;/p&gt;
&lt;h2 id=&quot;global-workflows-one-repo-pushes-ci-to-all-of-them&quot;&gt;Global workflows: one repo pushes CI to all of them&lt;/h2&gt;
&lt;p&gt;A KiCI &lt;a href=&quot;https://docs.kici.dev/user/global-workflows/&quot;&gt;global workflow&lt;/a&gt; turns
this around. One workflow repo declares the pipeline, its trigger carries a
&lt;code&gt;repos:&lt;/code&gt; glob, and the orchestrator runs it on events from every matching repo in
the org. Nothing is added to the source repos: no file, no reference, no opt-in
commit. You change the policy in one place, and it’s live everywhere on the next
push.&lt;/p&gt;
&lt;p&gt;The guardrails are built for that blast radius. Global workflows stay off until
the orchestrator’s operator switches them on. Each org then governs them on two
independent axes: an allow-list of which repos may &lt;strong&gt;author&lt;/strong&gt; org-wide
automation, and a deny-list of which &lt;strong&gt;source&lt;/strong&gt; repos may trigger it. Secrets
follow the same logic. A global job binds contexts one job at a time, and only
the jobs that ask for them get them. The context rules are checked as the
workflow repo’s, not the repo that pushed, so a context you limited to another
repo stays out of reach of your &lt;code&gt;ci-pipelines&lt;/code&gt; repo. Here’s the catch: a job that
binds a context also runs whatever the source repo’s code does, so keep the
source repo’s &lt;code&gt;npm install&lt;/code&gt; and tests in jobs that bind nothing, and save the
secrets for the jobs that publish or deploy. If a global workflow fires on pull
requests, set &lt;code&gt;minimumTrust&lt;/code&gt; on the contexts it uses, so a fork’s pull request
waits for a human before it gets anywhere near them.&lt;/p&gt;
&lt;p&gt;One pipeline for every repo would be a blunt tool if it ran the same way
everywhere. It doesn’t have to: a global workflow can adapt to each repo, at
three levels, from a one-line declarative gate to jobs generated from the repo.&lt;/p&gt;
&lt;h2 id=&quot;skip-the-repos-that-dont-need-it-with-a-content-filter&quot;&gt;Skip the repos that don’t need it with a content filter&lt;/h2&gt;
&lt;p&gt;The cheapest adaptivity is declarative. A &lt;code&gt;requires&lt;/code&gt; content filter tells the
orchestrator to dispatch the workflow only for repos whose files match a
condition — here, repos whose &lt;code&gt;package.json&lt;/code&gt; declares a &lt;code&gt;ci:test&lt;/code&gt; script. A repo
without that script is dropped &lt;strong&gt;before any agent is dispatched&lt;/strong&gt;, so you spend
zero compute on repos that opted out by not having the script.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org-ci-test&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      repos: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;myorg/*&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      branches: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;main&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      requires: [{ file: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;package.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, exists: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;$.scripts[&apos;ci:test&apos;]&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] }],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;test&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;kici:os:linux&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      steps: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        step&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ci-test&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`pnpm run ci:test`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the orchestrator evaluates this filter from file contents alone, it
never starts an agent for a non-matching repo. It’s the right tool when the gate
is a simple “does this repo have X” question.&lt;/p&gt;
&lt;h2 id=&quot;decide-per-repo-with-a-typescript-predicate&quot;&gt;Decide per repo with a TypeScript predicate&lt;/h2&gt;
&lt;p&gt;When the decision needs more than a JSON-path check, a workflow-level &lt;code&gt;filter&lt;/code&gt;
predicate runs on an evaluating agent with the source repo checked out. It’s
plain TypeScript with the tree on disk, so it can inspect anything. This one
runs the build pipeline only for repos that ship both a Dockerfile and a
&lt;code&gt;ci:build&lt;/code&gt; script.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org-ci-build&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ repos: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;myorg/*&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], branches: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;main&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;sourceRepo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; root&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sourceRepo.path;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;existsSync&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(root, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Dockerfile&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; false&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; pkg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; readFile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(root, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;package.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;utf8&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; typeof&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pkg?.scripts?.[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ci:build&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;string&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;build&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;kici:os:linux&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      steps: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        step&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ci-build&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`pnpm run ci:build`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The predicate reads the source repo through &lt;code&gt;sourceRepo.path&lt;/code&gt; and returns a
boolean. Unlike the content filter, the logic is arbitrary, and it works with any
provider that can clone the repo.&lt;/p&gt;
&lt;h2 id=&quot;generate-the-jobs-from-each-repos-own-scripts&quot;&gt;Generate the jobs from each repo’s own scripts&lt;/h2&gt;
&lt;p&gt;The most expressive level generates the jobs at runtime from the repo’s own
state. This workflow reads each source repo’s &lt;code&gt;package.json&lt;/code&gt; and emits one job
per &lt;code&gt;ci:*&lt;/code&gt; script it finds, so every repo runs exactly the CI it declares, all
defined once, centrally.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; perScript&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; DynamicJobFn&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;sourceRepo&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; root&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sourceRepo?.path &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;.&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; pkg&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;parse&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; readFile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;join&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(root, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;package.json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;utf8&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; ciScripts&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Object.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;keys&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(pkg.scripts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {}).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;filter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;s&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;startsWith&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ci:&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ciScripts.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    job&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(name.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;:&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;-&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      runsOn: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;kici:os:linux&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      steps: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;        step&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;run&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;          await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`pnpm run ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; workflow&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;org-ci-matrix&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  on: [&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;push&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ repos: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;myorg/*&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], branches: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;main&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] })],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  jobs: [perScript],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A static YAML matrix lists its entries up front. Here the set of jobs is
computed from the repository in front of you, in real code. A repo with
&lt;code&gt;ci:lint&lt;/code&gt;, &lt;code&gt;ci:test&lt;/code&gt; and &lt;code&gt;ci:build&lt;/code&gt; gets three jobs; a repo with one gets one,
with no configuration in the source repos and no fan-out you had to list by
hand.&lt;/p&gt;
&lt;h2 id=&quot;when-a-global-workflow-is-the-wrong-tool&quot;&gt;When a global workflow is the wrong tool&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Repos that have little in common.&lt;/strong&gt; If every repo builds and deploys in its
own way, one shared pipeline turns into a pile of per-repo branches. Keep
per-repo workflows there, and use a global workflow for the policy that’s the
same everywhere: lint, a security scan, a license check.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One bad change breaks CI on every repo at once.&lt;/strong&gt; That’s the other side of
changing the policy in one place. Roll a change out on a narrow &lt;code&gt;repos:&lt;/code&gt; glob
first, such as a single repo named in full, then widen it. The
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/#narrowing-to-the-repos-that-need-it&quot;&gt;narrowing guide&lt;/a&gt;
covers the other ways to limit where a global workflow runs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;set-it-up&quot;&gt;Set it up&lt;/h2&gt;
&lt;p&gt;The steps call the repo that holds the org’s pipelines the workflow repo,
&lt;code&gt;myorg/ci-pipelines&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sign up and bring up an orchestrator.&lt;/strong&gt; Create a free account at &lt;a href=&quot;https://app.kici.dev&quot;&gt;app.kici.dev&lt;/a&gt; and follow the &lt;a href=&quot;https://docs.kici.dev/user/quickstart/&quot;&gt;5-minute quickstart&lt;/a&gt; through Part 2, so an orchestrator is running and receives your GitHub org’s events. Install the GitHub App it uses on the whole org, or on the workflow repo plus every repo your &lt;code&gt;repos:&lt;/code&gt; glob should cover.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Switch global workflows on.&lt;/strong&gt; The orchestrator’s operator runs
&lt;code&gt;kici-admin cluster-settings set --global-workflows-enabled true&lt;/code&gt;. The switch
is off by default, and while it’s off KiCI doesn’t register &lt;code&gt;repos:&lt;/code&gt;
workflows at all. Turn it on before step 5’s push, or push again after.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up the workflow repo.&lt;/strong&gt; Run &lt;code&gt;npx kici init&lt;/code&gt; in &lt;code&gt;myorg/ci-pipelines&lt;/code&gt; if it
has no &lt;code&gt;.kici/&lt;/code&gt; folder yet. To keep other repos from shipping org-wide
automation, add it under &lt;strong&gt;Settings → Global workflows → Allowed author
repos&lt;/strong&gt; in the dashboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write the workflow.&lt;/strong&gt; Copy the level you need into the workflow repo’s
&lt;code&gt;.kici/workflows/&lt;/code&gt; and change &lt;code&gt;myorg/*&lt;/code&gt; to your org. None of the three
examples needs a secret. If you add a job that does, bind the context on that
job alone and keep the source repo’s install and test steps in jobs that bind
nothing. Then limit the context to the workflow repo, so no source repo can
bind it from its own workflows:
&lt;code&gt;kici-admin context set-policy --org &amp;#x3C;org-id&gt; --env &amp;#x3C;context&gt; --repo-patterns &apos;[&quot;myorg/ci-pipelines&quot;]&apos;&lt;/code&gt;.
If the workflow also fires on pull requests, add &lt;code&gt;--minimum-trust trusted&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compile and push to the default branch.&lt;/strong&gt; Run &lt;code&gt;kici compile&lt;/code&gt;, commit the
workflow with &lt;code&gt;.kici/kici.lock.json&lt;/code&gt;, and push to the workflow repo’s default
branch. KiCI registers global workflows only from that branch’s lock file, so
a push to a feature branch changes nothing yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push to a covered repo and watch the run.&lt;/strong&gt; Push to &lt;code&gt;main&lt;/code&gt; in any repo the
glob matches. The run shows up in the dashboard under that source repo, with a
&lt;strong&gt;Defined in&lt;/strong&gt; row naming the workflow repo. If nothing runs,
&lt;code&gt;kici-admin registration list&lt;/code&gt; shows whether the workflow registered, and the
&lt;a href=&quot;https://docs.kici.dev/user/global-workflows/#troubleshooting&quot;&gt;troubleshooting table&lt;/a&gt;
maps each symptom to its cause.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s one pipeline on every repo in the org, with nothing to merge into any of
them.&lt;/p&gt;</content:encoded><dc:creator>Alberto Marchetti</dc:creator><category>global-workflows</category><category>ci</category><category>monorepo</category><category>typescript</category></item></channel></rss>