<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>PHP Boy Scout</title><link>https://blog-570662.gitlab.io/</link><description>Recent content on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Wed, 10 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog-570662.gitlab.io/index.xml" rel="self" type="application/rss+xml"/><item><title>Why I still write code</title><link>https://blog-570662.gitlab.io/why-i-still-write-code/</link><pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/why-i-still-write-code/</guid><description>&lt;img src="https://blog-570662.gitlab.io/why-i-still-write-code/cover-why-i-still-write-code.png" alt="Featured image of post Why I still write code" /&gt;&lt;p&gt;By any sensible reading of an org chart, I have no business being in this file.
I&amp;rsquo;m a Head of Software Engineering. My calendar reckons I should be in a room
somewhere talking about headcount and roadmaps. Instead it&amp;rsquo;s late, everyone
sensible has logged off, and I&amp;rsquo;m three retries deep into
&lt;a class="link" href="https://blog-570662.gitlab.io/same-config-two-answers/" &gt;a release that refuses to tag itself&lt;/a&gt;,
muttering at a Rust workspace I built with my own hands.&lt;/p&gt;
&lt;p&gt;So why am I here? I&amp;rsquo;ve been asking myself a version of that question for about
twenty-five years, and I think I&amp;rsquo;ve finally got an answer. It&amp;rsquo;s just not a
flattering one.&lt;/p&gt;
&lt;h2 id="im-a-builder-and-that-isnt-really-a-choice"&gt;I&amp;rsquo;m a builder, and that isn&amp;rsquo;t really a choice
&lt;/h2&gt;&lt;p&gt;Strip away the job titles and I&amp;rsquo;m a builder. I like to make things, I like to
solve problems, I like to learn how something works by taking it apart and
putting it back together slightly differently. That urge predates every role
I&amp;rsquo;ve ever held and it has survived all of them. In jobs where I wasn&amp;rsquo;t allowed
to scratch it, I went and built in the open instead, which is a polite way of
saying open source has spent years absorbing energy my day job wouldn&amp;rsquo;t take.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll go further, because being coy about it helps no one: it&amp;rsquo;s closer to an
addiction than a hobby. I don&amp;rsquo;t fully switch off. The current outlet, when I&amp;rsquo;m
not in a terminal, is converting a campervan, which is just software engineering
with worse error messages and a real risk of electrocution. The shape of the
thing changes. The compulsion doesn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;Underneath the building there&amp;rsquo;s a less charming engine, and I might as well name
it: a fairly grim case of impostor syndrome. I wrote about it years ago when I
&lt;a class="link" href="https://blog-570662.gitlab.io/goodbye-dev-charge/" &gt;stopped calling myself &amp;ldquo;Dev in Charge&amp;rdquo;&lt;/a&gt;,
and a decade on it hasn&amp;rsquo;t gone anywhere. The only thing that ever quiets the
anxiety is staying genuinely good at the thing, and staying good at the thing
means using it. I&amp;rsquo;m a firm believer in use it or lose it. People say technical
skill is like riding a bike, you never forget. Maybe. But step away for a few
years and when you climb back on, someone&amp;rsquo;s bolted a jet engine to the frame and
moved the pedals. The bike doesn&amp;rsquo;t wait for you.&lt;/p&gt;
&lt;h2 id="what-it-actually-buys"&gt;What it actually buys
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part that justifies the indulgence, because on its own &amp;ldquo;I enjoy it&amp;rdquo;
isn&amp;rsquo;t a reason to stay technical as a leader, it&amp;rsquo;s a reason to have a hobby.&lt;/p&gt;
&lt;p&gt;The load-bearing belief is simple, and it&amp;rsquo;s the one line I&amp;rsquo;d carve into the desk:
&lt;strong&gt;I will never ask an engineer to do something I&amp;rsquo;m not willing to do myself.&lt;/strong&gt;
Everything good about staying hands-on flows from that. Because I&amp;rsquo;m still in the
work, I can give my engineers proper support, the right tools and a clear path,
rather than guessing at what they need from a slide. I can steer them through a
genuinely hard technical call instead of nodding along. I can sniff out a duff
estimate, mine or theirs, because I know what the work actually costs. And I can
hold them to account with a straight face, because the accountability runs both
ways. They answer to me for what they ship, and they get to hold me to account
for what I contribute. That second half is the bit a lot of technical leaders
quietly drop, and it&amp;rsquo;s the half that earns you the right to the first.&lt;/p&gt;
&lt;h2 id="the-bill-and-who-paid-it"&gt;The bill, and who paid it
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;d be selling you a fairy tale if I stopped there, so here&amp;rsquo;s the cost, and some
of it is steep.&lt;/p&gt;
&lt;p&gt;The obvious one is burnout. I&amp;rsquo;ve been there more than once over the years, and
it&amp;rsquo;s the single biggest reason I now pitch myself deliberately as a &lt;em&gt;Technical
Leader&lt;/em&gt; rather than an &lt;em&gt;Engineering Manager&lt;/em&gt;. I can do the manager stuff, the HR
and the planning and the project-management bollocks, and after enough years in
the role I do it well, because it demanded that I did. But competence isn&amp;rsquo;t
appetite. Given the choice I&amp;rsquo;ll take a technical problem or a bit of mentoring
over running the process around either, every time, and spending your days on work
you&amp;rsquo;re good at but don&amp;rsquo;t much enjoy is its own slow road back to the wall.
Sticking to my strengths isn&amp;rsquo;t ego, and it isn&amp;rsquo;t an admission I can&amp;rsquo;t do the rest.
It&amp;rsquo;s self-preservation, learned the hard way.&lt;/p&gt;
&lt;p&gt;The steeper bill came due at home. When my kids were small I poured my own time
into pushing my skills and chasing the next rung, even
&lt;a class="link" href="https://blog-570662.gitlab.io/time-change/" &gt;starting my own agency&lt;/a&gt;. Between
that and the burnout, I missed big chunks of their early years, and that is one
of the real regrets of my life. I&amp;rsquo;m not going to dress it up or hide it behind a
lesson. It was my decision, I made it, and I own it. I&amp;rsquo;m immensely proud of the
people they&amp;rsquo;ve grown into, and since their mum and I separated I&amp;rsquo;ve put
everything I have into giving them a stable home, the builder instinct quietly
turning into a nest-building one, which is the better use of it. I put this here,
plainly, because if you&amp;rsquo;re reading this with a young family asleep upstairs, I&amp;rsquo;d
sooner you heard it from someone who got the balance wrong than learn it the way
I did. The code will still be there next year. They won&amp;rsquo;t be five next year.&lt;/p&gt;
&lt;p&gt;And there&amp;rsquo;s a smaller, daily cost that I still haven&amp;rsquo;t fully mastered: knowing
when to put the keyboard down. A builder who can&amp;rsquo;t stop building is exactly the
person who becomes the bottleneck, disappears down a rabbit hole, or hoards the
interesting problem that would have stretched someone on the team. Stepping back
to let them solve it, when every instinct I have is screaming to just fix the
bloody thing, is genuinely one of the hardest skills I&amp;rsquo;ve had to learn, and some
days it still feels like walking a knife edge. Open source is a big part of how I
manage that. It&amp;rsquo;s a release valve, somewhere I can let the compulsion run with no
brakes on, precisely so I&amp;rsquo;m not stealing the meaty work off the people I&amp;rsquo;m meant
to be growing.&lt;/p&gt;
&lt;h2 id="does-it-still-count-when-the-robot-types"&gt;Does it still count when the robot types?
&lt;/h2&gt;&lt;p&gt;Fair challenge, given the year. I build solo now with an AI pair, to the point
where it&amp;rsquo;s &lt;a class="link" href="https://blog-570662.gitlab.io/same-config-two-answers/" &gt;changed how I branch and release&lt;/a&gt;.
So when a model writes a good chunk of the actual characters, am I still &amp;ldquo;writing
the code&amp;rdquo;?&lt;/p&gt;
&lt;p&gt;I think I&amp;rsquo;m doing it more than ever, and I&amp;rsquo;m certainly learning faster. My typing
is genuinely terrible, a quarter-century of practice and still mostly thumbs, so
being freed from being the typist is no loss at all. What&amp;rsquo;s left when you take the
keystrokes away is the part that was always the point: reading, reviewing,
judging, steering. I can review more code, faster, than I ever could when I was
the one hammering it out, and I can run several projects at once by pointing my
judgement at each in turn. That is leadership work and engineering work at the
same time, which is rather the whole thesis.&lt;/p&gt;
&lt;p&gt;It did not come free, mind. I was elbow-deep in AI and ML long before GPT made it
fashionable, and I&amp;rsquo;ve seen the messy version up close. Getting to the point where
the tools are good enough &lt;em&gt;and&lt;/em&gt; I&amp;rsquo;ve built the guardrails and habits that make
them safe took a long time and a lot of getting it wrong. Owning the judgement
when the machine does the typing is harder than it sounds, not easier. The typing
was never the hard bit.&lt;/p&gt;
&lt;h2 id="what-id-actually-put-my-name-to"&gt;What I&amp;rsquo;d actually put my name to
&lt;/h2&gt;&lt;p&gt;Not that every leader should write code. Plenty of excellent ones don&amp;rsquo;t, and
they&amp;rsquo;re brilliant at the parts of the job I&amp;rsquo;m middling at. The narrower, truer
claim is the only one worth making: I lead better when I stay in the work,
because it&amp;rsquo;s the only way I know to support, steer and be held to account without
faking any of it, and because I meant that line about never asking for what I
won&amp;rsquo;t do myself.&lt;/p&gt;
&lt;p&gt;Staying technical isn&amp;rsquo;t the job. It&amp;rsquo;s the thing that lets me do the job honestly.
I&amp;rsquo;m a builder who learned, slowly and at a price I&amp;rsquo;d rather have not paid, how to
keep building without it costing the people around me what it once cost the people
closest to me. That&amp;rsquo;s the balance I&amp;rsquo;m still working at. I suspect I always will
be.&lt;/p&gt;</description></item><item><title>A signature the platform can't forge</title><link>https://blog-570662.gitlab.io/a-signature-the-platform-cant-forge/</link><pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-signature-the-platform-cant-forge/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-signature-the-platform-cant-forge/cover-a-signature-the-platform-cant-forge.png" alt="Featured image of post A signature the platform can't forge" /&gt;&lt;p&gt;A self-updating tool has a chicken-and-egg problem baked into it. The thing doing the
updating is the thing being updated, so when it reaches out and pulls down a newer
version of itself, it&amp;rsquo;s the one that has to decide whether to trust what just landed.
No human in the loop, nobody to ask. I&amp;rsquo;ve been closing that gap in go-tool-base&amp;rsquo;s
self-updater in two phases. The
&lt;a class="link" href="https://blog-570662.gitlab.io/verifying-your-own-downloads/" &gt;first&lt;/a&gt; gave it a
checksum: download the new binary, hash it, compare it against the release&amp;rsquo;s
&lt;code&gt;checksums.txt&lt;/code&gt;. That catches the accidents, the truncated download, the flipped bit
on a dodgy mirror. And I said at the time, plainly, that it does nothing about a
determined attacker who owns the release platform&amp;hellip; the checksums file sits right
next to the binary, so whoever can swap one can swap both. I left that as an IOU.
This second phase is me paying it.&lt;/p&gt;
&lt;h2 id="the-thing-a-checksum-cant-do"&gt;The thing a checksum can&amp;rsquo;t do
&lt;/h2&gt;&lt;p&gt;A checksum is a promise that the bytes you got match the manifest. It says nothing
about &lt;em&gt;who wrote the manifest&lt;/em&gt;. So if GitLab, or my account, or a leaked CI token
gets compromised, the attacker rewrites the binary and the &lt;code&gt;checksums.txt&lt;/code&gt; in the
same breath, and the hash matches perfectly, because they&amp;rsquo;re the one who computed it.
It&amp;rsquo;s the same wall I keep walking into whenever I think about
&lt;a class="link" href="https://blog-570662.gitlab.io/nobody-is-coming-to-clean-your-supply-chain/" &gt;supply-chain trust&lt;/a&gt;:
a checksum is only ever as good as whatever&amp;rsquo;s standing behind it, and the thing
standing behind a checksum is the very platform that just handed you the file. Same
hands, both times.&lt;/p&gt;
&lt;p&gt;To get past that, you need a signature whose root of trust lives somewhere the
platform can&amp;rsquo;t reach.&lt;/p&gt;
&lt;h2 id="the-crypto-is-the-easy-part"&gt;The crypto is the easy part
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the bit that caught me slightly off guard while I was building this: the
cryptography is the easy part. Verifying a detached OpenPGP signature is a library
call, and go-tool-base&amp;rsquo;s &lt;code&gt;TrustSet&lt;/code&gt; wraps it up in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/v0.12.2/pkg/setup/signing.go#L237-L257" target="_blank" rel="noopener"
 &gt;one method&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;TrustSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;VerifyManifestSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;openpgp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CheckArmoredDetachedSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ErrSignatureInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ErrSignatureInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;no signer in trust set matched&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hand it the manifest, the detached signature, and a set of trusted public keys (the
&lt;code&gt;entities&lt;/code&gt;), and it tells you whether any one of them signed it. That&amp;rsquo;s the whole of
the cryptography, and it&amp;rsquo;s genuinely not where the hard work lives.&lt;/p&gt;
&lt;p&gt;The hard work is that set of trusted public keys. Where do they come from? Because if
the answer is &amp;ldquo;we ship them right next to the binary&amp;rdquo;, well&amp;hellip; you&amp;rsquo;re straight back
to the checksum problem. Whoever can swap the binary can swap the key too, sign with
their own, and the check waves it through none the wiser.&lt;/p&gt;
&lt;h2 id="pulling-the-two-questions-apart"&gt;Pulling the two questions apart
&lt;/h2&gt;&lt;p&gt;So the design splits along exactly that seam. The verification half is fixed, and
deliberately boring (the method above). The trust anchor, the actual keys, comes from
a swappable
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/v0.12.2/pkg/setup/signing.go#L259-L274" target="_blank" rel="noopener"
 &gt;&lt;code&gt;KeyResolver&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// The interface separates &amp;#34;where the trust anchor comes from&amp;#34; from &amp;#34;how a&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// signature is verified against it&amp;#34;, so SelfUpdater can be wired with&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// whichever resolver chain a tool needs without changing verification logic.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;KeyResolver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;TrustSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That little seam is really the whole game. Everything interesting about standing up
to a compromised platform comes down to which resolver you hand the updater, and the
verification code never has to know the difference.&lt;/p&gt;
&lt;h2 id="three-answers-to-where-does-the-key-live"&gt;Three answers to &amp;ldquo;where does the key live&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;The first option is to embed it. Bake the public key straight into the binary at
build time (&lt;code&gt;NewEmbeddedResolver&lt;/code&gt;), so it rides along inside a release you already
trusted enough to run. Tidy and self-contained. The catch is that a &lt;em&gt;future&lt;/em&gt;
malicious release could embed a different key, so on its own, embedding really just
trusts whoever cut the most recent binary.&lt;/p&gt;
&lt;p&gt;The second is WKD, the Web Key Directory. Fetch the key over HTTPS from a well-known
path on a domain you control (&lt;code&gt;NewWKDResolver&lt;/code&gt;), nothing to do with where the release
itself is hosted. Now the key isn&amp;rsquo;t in the binary at all, so poisoning a release
doesn&amp;rsquo;t touch it. You haven&amp;rsquo;t made the problem disappear, mind&amp;hellip; you&amp;rsquo;ve moved the
trust onto your domain&amp;rsquo;s host and its DNS. A different blast radius, but a blast
radius all the same.&lt;/p&gt;
&lt;p&gt;The third option is to do both, and make them agree. Run embedded &lt;em&gt;and&lt;/em&gt; WKD, and
insist they &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/v0.12.2/pkg/setup/signing_composite.go#L61-L82" target="_blank" rel="noopener"
 &gt;agree&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;CompositeResolver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;TrustSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// ... run each child resolver concurrently ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;checkAgreement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ErrKeyResolverMismatch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Think of it as the two-key rule on a safe deposit box, or two witnesses who&amp;rsquo;ve never
met telling you the same story. One source on its own you might quietly doubt. But if
the key baked into the binary and the key sitting on my domain hand back the same
fingerprint, that agreement is worth a great deal more than either of them alone. And
if they ever come back different, that&amp;rsquo;s not a maybe, that&amp;rsquo;s an alarm:
&lt;code&gt;ErrKeyResolverMismatch&lt;/code&gt;. Poison one source and the mismatch is the thing that gives
the game away.&lt;/p&gt;
&lt;p&gt;That composite is the real answer, and it&amp;rsquo;s why the interface exists at all. There&amp;rsquo;s
nothing a single attacker can get their hands on that holds the whole thing up by
itself. The key is baked into a release you trusted, &lt;em&gt;and&lt;/em&gt; fetched from a domain well
off the release platform, &lt;em&gt;and&lt;/em&gt; the two have to match before a single byte of the
update is allowed through.&lt;/p&gt;
&lt;h2 id="the-separation-is-the-whole-point"&gt;The separation is the whole point
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s easy to nod along at &amp;ldquo;two sources&amp;rdquo; and miss the part that actually does the work.
The agreement between the embedded key and the WKD key is only worth something if an
attacker can&amp;rsquo;t reach both of them from the same place. If the key I bake into the
binary and the key I serve over WKD both came out of the same release pipeline,
whoever owns that pipeline swaps the pair of them, the fingerprints still match, and
the cross-check happily waves the forgery through. Same hands, both times. Again.&lt;/p&gt;
&lt;p&gt;So they don&amp;rsquo;t share a pipeline, and that&amp;rsquo;s the entire design, not an accident of how
things ended up. The binary, and the key embedded in it, are built and signed in
GitLab CI, which federates into AWS KMS to do the signing itself. The WKD key lives
somewhere else completely: a Cloudflare Pages site serving &lt;code&gt;openpgpkey.phpboyscout.uk&lt;/code&gt;,
deployed by hand at rotation time with the Wrangler CLI and a token allowed to do
nothing but edit that one Pages project. No Git integration, no webhook, nothing that
lets a push to the repo or a run of the release pipeline so much as touch it. The
Cloudflare account is even administered under a different email and a different second
factor from the GitLab and AWS ones, so the three anchors really are independent
rather than just feeling that way.&lt;/p&gt;
&lt;p&gt;Which is what makes them fail independently, and that independence is the only thing
that makes the agreement worth checking. To forge a release that survives the
cross-check, an attacker doesn&amp;rsquo;t have to beat one system, they have to beat two
unrelated ones, on different platforms, behind different credentials, in the same
window, without either of them noticing.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a quieter benefit in the cadence, too. Releases go out constantly and
automatically; the WKD key changes rarely, and only ever by hand. So the busy,
automated path, the one an attacker is most likely to prise open, is exactly the one
with no power to rewrite the key everyone checks against.&lt;/p&gt;
&lt;h2 id="requiring-it-without-breaking-everyone"&gt;Requiring it, without breaking everyone
&lt;/h2&gt;&lt;p&gt;Now, a check nobody ever switches on is just theatre. But switch it on before the
keys are actually out there in people&amp;rsquo;s installs, and you&amp;rsquo;ve handed everyone a
self-inflicted outage instead. So the default is deliberately timid. The framework
ships
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/v0.12.2/pkg/setup/signing.go#L47-L57" target="_blank" rel="noopener"
 &gt;&lt;code&gt;DefaultRequireSignature = false&lt;/code&gt;&lt;/a&gt;:
a tool built on go-tool-base doesn&amp;rsquo;t suddenly start rejecting its own updates the day
its author bumps the framework version.&lt;/p&gt;
&lt;p&gt;The tool author flips it to &lt;code&gt;true&lt;/code&gt; in &lt;code&gt;main()&lt;/code&gt;, but only &lt;em&gt;after&lt;/em&gt; they&amp;rsquo;ve shipped a
release that embeds the key, so every install out there already holds the trust
anchor before the first release that insists on one. Ship the key, then turn the
lock: the same leave-yourself-a-way-back discipline as any migration you&amp;rsquo;d like to
still have a job after. And the end user still gets an override
(&lt;code&gt;update.require_signature&lt;/code&gt;, or an env var) for the day it all goes sideways and they
need out.&lt;/p&gt;
&lt;h2 id="what-it-actually-buys"&gt;What it actually buys
&lt;/h2&gt;&lt;p&gt;The first phase stopped accidents. This one stops the platform. And not because the
cryptography is clever, OpenPGP checks the signature in a single call, but because the
trust anchor is arranged so that nothing the attacker can actually reach holds the
whole thing up on its own. A signature only ever proves the sender, never the
contents. All of this is really about making &amp;ldquo;the sender&amp;rdquo; something a compromised
release host can&amp;rsquo;t quietly fake its way into being.&lt;/p&gt;
&lt;p&gt;Which leaves one last thread dangling. The &lt;em&gt;verifying&lt;/em&gt; key gets fetched from
somewhere, fine&amp;hellip; but the &lt;em&gt;signing&lt;/em&gt; key, the private half that actually produces
these signatures, has to live somewhere the platform can&amp;rsquo;t reach either, or none of
the rest holds up. That&amp;rsquo;s the capstone, and where this series ends: where that key
lives, and why it never leaves the box it&amp;rsquo;s born in.&lt;/p&gt;</description></item><item><title>Anything under an 8</title><link>https://blog-570662.gitlab.io/anything-under-an-8/</link><pubDate>Mon, 08 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/anything-under-an-8/</guid><description>&lt;img src="https://blog-570662.gitlab.io/anything-under-an-8/cover-anything-under-an-8.png" alt="Featured image of post Anything under an 8" /&gt;&lt;p&gt;I read the news about the National Vulnerability Database over a coffee that
went cold while I sat there muttering at my phone. The short version: the NVD,
the free public catalogue that quietly props up half the security tooling you
and I run every day, is going under in slow motion. And the more I dug into
&lt;em&gt;why&lt;/em&gt;, the worse the taste in my mouth got.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m an open-source person. I think of myself as part of that community, and the
NVD is one of those public goods the whole community leans on without ever
really thinking about it. So my first reaction wasn&amp;rsquo;t clever or measured. It was
a kick in the teeth.&lt;/p&gt;
&lt;h2 id="the-carcass-and-the-vultures"&gt;The carcass and the vultures
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where things actually are. In February 2024 the NVD had around 13,000
unprocessed vulnerabilities sitting in a queue waiting to be analysed. By the end
of 2025 that backlog had passed
&lt;a class="link" href="https://www.helpnetsecurity.com/2026/06/01/nist-nvd-management-problems/" target="_blank" rel="noopener"
 &gt;27,000&lt;/a&gt;.
This April, NIST effectively
&lt;a class="link" href="https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth" target="_blank" rel="noopener"
 &gt;admitted it can&amp;rsquo;t dig out&lt;/a&gt;:
everything published before 1 March 2026 that hadn&amp;rsquo;t been enriched got swept into
a bucket marked &amp;ldquo;Not Scheduled&amp;rdquo;, and going forward only the highest-risk entries
get the full treatment. The rest you&amp;rsquo;re on your own with.&lt;/p&gt;
&lt;p&gt;The reasons are grimly ordinary. The
&lt;a class="link" href="https://www.helpnetsecurity.com/2026/06/01/nist-nvd-management-problems/" target="_blank" rel="noopener"
 &gt;Cybersecurity and Infrastructure Security Agency stopped funding the
programme&lt;/a&gt;
in 2024. The enrichment contract lapsed that same February, and despite NIST
having two years&amp;rsquo; notice it needed a replacement, the database limped along
understaffed until late November. And the volume kept climbing regardless:
&lt;a class="link" href="https://jerrygamblin.com/2026/01/01/2025-cve-data-review/" target="_blank" rel="noopener"
 &gt;48,185 CVEs in 2025&lt;/a&gt;,
roughly 131 a day, with forecasts of the annual figure topping 60,000, getting on
for ten times what it was a decade ago. No money, a fumbled handover, and a
firehose. That&amp;rsquo;s the whole story.&lt;/p&gt;
&lt;p&gt;The bit that turns my stomach is what comes next. When a free public good fails,
the gap doesn&amp;rsquo;t stay empty. It gets filled, and it gets filled by people selling
something. There are already commercial vulnerability databases that are better
resourced and more current than the NVD, and the moment the free one is visibly
on the floor, every one of them sees a market. Plenty of those subscriptions cost
more in a year than a small open-source project will see in donations in its
lifetime. So the catalogue the little projects relied on most is exactly the one
about to be priced out of their reach. Vultures circling a carcass, and the
carcass is something we all built on.&lt;/p&gt;
&lt;h2 id="the-number-we-never-checked"&gt;The number we never checked
&lt;/h2&gt;&lt;p&gt;And then I read the part that stopped me blaming everyone else.&lt;/p&gt;
&lt;p&gt;A Department of Commerce Inspector General audit went through the NVD&amp;rsquo;s work and
found that NIST&amp;rsquo;s own severity scores
&lt;a class="link" href="https://therecord.media/nist-mistakes-vulnerability-database-inspector-general" target="_blank" rel="noopener"
 &gt;matched independent assessors only 12% of the
time&lt;/a&gt;.
Read that again. Not that NIST was wrong 88% of the time, that&amp;rsquo;s not quite what
it says, but that two competent parties looking at the same vulnerability landed
on the same severity barely one time in eight. The score was never an objective
fact handed down from on high. It was always an estimate, a judgement call, the
kind of thing reasonable people disagree about most of the time.&lt;/p&gt;
&lt;p&gt;Which is awkward, because I have spent years treating that number as gospel. And
I know I&amp;rsquo;m not alone, because I&amp;rsquo;ve watched whole engineering organisations do the
same thing in writing. More than one large employer I&amp;rsquo;ve had bakes the CVSS score
straight into policy: anything scored 8 or above blocks the build and gets a
meeting, and anything under an 8 goes through at an engineer&amp;rsquo;s discretion. When
time is money, and it always is in those places, &amp;ldquo;it&amp;rsquo;s only a 6.4, ship it&amp;rdquo; is the
easiest decision you&amp;rsquo;ll make all week. I&amp;rsquo;ve made it. I&amp;rsquo;ve made it without opening
the advisory, without checking whether the vulnerable code path was even reachable
in what we&amp;rsquo;d built, on the strength of a single number that, it turns out, two
experts wouldn&amp;rsquo;t have agreed on anyway.&lt;/p&gt;
&lt;p&gt;So before I get cross about the funding, I have to sit with my own part in this.
We took a contestable estimate and bolted it to the door as a gatekeeper. We
turned &amp;ldquo;a rough signal worth a closer look&amp;rdquo; into &amp;ldquo;the closer look&amp;rdquo;, and then we
stopped looking. The database didn&amp;rsquo;t promise us a safety net. We just decided it
was one and stopped checking underneath.&lt;/p&gt;
&lt;h2 id="dont-blame-the-robots-for-this-one"&gt;Don&amp;rsquo;t blame the robots for this one
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s an easy villain on offer here, and I want to wave you off it. It would be
tidy to say AI did this, that the flood drowning the NVD is a tide of
machine-generated slop, the same dynamic I wrote about when
&lt;a class="link" href="https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/" &gt;curl&amp;rsquo;s bug bounty buckled under unverifiable
reports&lt;/a&gt;. It&amp;rsquo;s
tempting, it&amp;rsquo;s topical, and it&amp;rsquo;s mostly wrong.&lt;/p&gt;
&lt;p&gt;The people who actually crunch the numbers are clear that the surge is largely
&lt;a class="link" href="https://bishopfox.com/blog/understanding-the-cve-ecosystem-and-nists-changing-role" target="_blank" rel="noopener"
 &gt;legitimate growth&lt;/a&gt;.
There are now more than 484 CVE Numbering Authorities, far more organisations
reporting far more bugs far more thoroughly than they did a decade ago. That isn&amp;rsquo;t
a quality collapse, it&amp;rsquo;s the system working as designed and simply getting bigger
than its funding. Pinning it on AI would be scapegoating, and scapegoating the
robots for an underfunding-and-mismanagement problem is just a way of letting the
people who defunded it off the hook.&lt;/p&gt;
&lt;p&gt;None of which means AI gets a free pass. It just isn&amp;rsquo;t the arsonist. The same
machine-assisted discovery tools that found genuine bugs are also forecast to push
CVE volumes
higher still, and yes, one of the tools named in that forecast is the very one I
&lt;a class="link" href="https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/" &gt;poked fun at over curl&lt;/a&gt;.
AI is an accelerant on a fire that was already burning for thoroughly human
reasons. It&amp;rsquo;s a beat in this story, not the spine.&lt;/p&gt;
&lt;h2 id="the-version-im-betting-on"&gt;The version I&amp;rsquo;m betting on
&lt;/h2&gt;&lt;p&gt;Where does this leave the working engineer? In a harder spot than before, because
the easy answer stopped being easy. My usual line, the one I keep ending these pieces on, is that
&lt;a class="link" href="https://blog-570662.gitlab.io/nobody-is-coming-to-clean-your-supply-chain/" &gt;the diligence is the
job&lt;/a&gt;:
pin, lock, audit, and read the actual advisory instead of trusting a number. All
of that still holds. But it just got more expensive, because the data underneath
the diligence is thinner and, as it turns out, was shakier than we let ourselves
believe.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m not going to pretend there&amp;rsquo;s a clean fix. This problem won&amp;rsquo;t solve itself,
and it won&amp;rsquo;t be solved by any one of us. It needs all of us to actually support
the services we depend on, with money, with contributions, with attention, so the
public goods that underpin our craft are still standing in ten years. That&amp;rsquo;s the
unglamorous, grown-up part.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;ll end this one looking up rather than down, because for once I can. I think
the next few years bend towards safer software almost in spite of us. Modern
languages are quietly closing off whole categories of vulnerability at the source:
every memory-safety bug that a borrow checker refuses to compile is one that never
reaches a database to be mis-scored in the first place, which is rather the point
of building
&lt;a class="link" href="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/" &gt;a framework that contains no &lt;code&gt;unsafe&lt;/code&gt;&lt;/a&gt;.
Used with proper guidance instead of left to spew slop, AI can be a genuine help
finding and triaging the things that do slip through. And the
&lt;a class="link" href="https://blog-570662.gitlab.io/the-greybeards-edge-was-never-typing/" &gt;junior engineers we keep sawing off the bottom
rung&lt;/a&gt; are
exactly the people who, mentored by the greybeards before they retire, could build
the next generation of vulnerability identification that the current model clearly
can&amp;rsquo;t sustain.&lt;/p&gt;
&lt;p&gt;As for the vultures&amp;hellip; it&amp;rsquo;s a coin toss. A lot of firms will look at the NVD on
its back and see a land grab. I&amp;rsquo;d love to be proved an optimist and watch at least
one of them stand tall, take all that better-resourced data and open it to
open-source projects for nothing, because it&amp;rsquo;s the right thing to do and because
the whole industry drinks from that well. One of them doing the decent thing would
be worth more than all the press releases about responsible AI put together.&lt;/p&gt;
&lt;p&gt;The catalogue is wobbling. The number was never as solid as we treated it. Neither
of those is the end of the world, as long as we stop outsourcing our judgement to a
free service we never funded and never checked, and start paying, in every sense,
for the foundations we build on. Boring, unfashionable, and the only thing that
ever works. I think we&amp;rsquo;re up to it.&lt;/p&gt;</description></item><item><title>The consent you can't ask for</title><link>https://blog-570662.gitlab.io/the-consent-you-cant-ask-for/</link><pubDate>Sat, 06 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-consent-you-cant-ask-for/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-consent-you-cant-ask-for/cover-the-consent-you-cant-ask-for.png" alt="Featured image of post The consent you can't ask for" /&gt;&lt;p&gt;There&amp;rsquo;s a comfortable story going round about telemetry, and it goes like this.
There are two kinds. There&amp;rsquo;s the creepy kind, the usage data a vendor harvests to
work out who you are and what you do, and that kind needs your permission. And
there&amp;rsquo;s the innocent kind, the operational data a service emits so the people
running it can keep it up, and that kind is just plumbing, nobody&amp;rsquo;s business, no
permission required. Two neat boxes, and only one of them has a lock on it.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t think the boxes are that neat. And I think a fair few of the people
drawing them that way know it.&lt;/p&gt;
&lt;p&gt;Because there&amp;rsquo;s no clean line where operational data stops being personal. A web
service&amp;rsquo;s logs carry IP addresses. Its traces carry the path you walked through
the system, the ids of the things you touched, sometimes the very fields you sent.
Point at almost any of it and a GDPR lawyer will cheerfully tell you it can be
personal data, and that the law doesn&amp;rsquo;t much care whether you filed it under
&amp;ldquo;analytics&amp;rdquo; or &amp;ldquo;observability&amp;rdquo;. The word you picked to describe the data was never
the thing that decided whether it was personal. The data decided that, and a lot
of operational data is personal.&lt;/p&gt;
&lt;p&gt;So if you can&amp;rsquo;t hide behind the box marked &amp;ldquo;just plumbing&amp;rdquo;, what do you actually
do?&lt;/p&gt;
&lt;h2 id="where-im-coming-from"&gt;Where I&amp;rsquo;m coming from
&lt;/h2&gt;&lt;p&gt;I should say up front that I haven&amp;rsquo;t always been this relaxed about it. I spent a
good few years in righteous fury at every tool that phoned home, every &amp;ldquo;we collect
anonymous telemetry to improve the product&amp;rdquo; I never agreed to. Then I started
building the tools, and I needed the data myself: the kind that tells you which
features people actually use and which command falls over on first run, the kind
that lets you make the next decision with something better than a hunch. And it
softened me. Not into thinking it&amp;rsquo;s fine to take it without asking. Into
understanding why everyone wants to.&lt;/p&gt;
&lt;p&gt;What the fury left me with, the one thing I&amp;rsquo;ve never talked myself out of, is
being pro-choice. Not pro-collection, not anti. Pro-choice. Any tool I ask another
person to run will never quietly opt them into sending me a thing. It asks. On
first run it &lt;a class="link" href="https://blog-570662.gitlab.io/telemetry-that-asks-first/" &gt;makes its case&lt;/a&gt;,
says what it wants and why, and lets them say no and mean it. I&amp;rsquo;ll try hard to win the yes, because the data is genuinely useful and a
tool gets better when people share it. But I won&amp;rsquo;t presume it. The choice is
theirs, and the prompt exists so they actually get to make it.&lt;/p&gt;
&lt;h2 id="the-trouble-with-a-service"&gt;The trouble with a service
&lt;/h2&gt;&lt;p&gt;Which is a lovely principle right up until you build a web service. Because who,
exactly, do you prompt? An API doesn&amp;rsquo;t have a first run. It has a thousand callers
a second, none of them sat at a terminal waiting to tick a box. You can&amp;rsquo;t show a
consent dialog to a webhook. The answer the industry reaches for is &amp;ldquo;consent is
implied by use&amp;rdquo;, and&amp;hellip; maybe. It&amp;rsquo;s a grey area, full stop. Implied consent is the
same hand-wave that gave us the cookie banner, the thing we all click through
without reading. I&amp;rsquo;m not going to stand here and call it clean.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s a version of the principle that survives the grey, and it&amp;rsquo;s the one I
&lt;a class="link" href="https://blog-570662.gitlab.io/telemetry-that-asks-and-telemetry-that-doesnt/" &gt;built the framework around&lt;/a&gt;. Consent belongs to whoever can actually give it. For a
command-line tool, that&amp;rsquo;s the person running it, so you ask them. For a web
service, the person who can give it was never the end user at all, because you
can&amp;rsquo;t reach them. It&amp;rsquo;s the engineer who deploys the thing. They know what their
service collects, who its users are, which law they sit under, whether they owe
anyone a privacy notice. They are the one party in the whole chain who can make
the call with any of the facts in front of them. So that&amp;rsquo;s where the choice goes.&lt;/p&gt;
&lt;p&gt;Which is why, in go-tool-base, the web-service telemetry is a switch. On or off,
the engineer&amp;rsquo;s hand on it, collecting only what you need to keep the lights on by
default. There&amp;rsquo;s no consent prompt, not because consent stopped mattering, but
because there&amp;rsquo;s nobody in the loop I could ask. The accountability sits with the
person who can hold it.&lt;/p&gt;
&lt;h2 id="the-part-ill-own"&gt;The part I&amp;rsquo;ll own
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m pro-choice on telemetry, which is exactly why I built a way to switch it off
and a way to force it on. Because for a web service the person holding the choice
was never the end user, it&amp;rsquo;s the engineer who ships it, and &amp;ldquo;pro-choice&amp;rdquo; has to
mean putting the switch in their hand, not pretending a popup would have meant
anything.&lt;/p&gt;
&lt;p&gt;That force-it-on part is the bit I&amp;rsquo;ll answer for. I built a way for a tool author
to bypass the first-run prompt entirely and bake the consent in. There&amp;rsquo;s a real
use case behind it, the enterprise tool deployed under a policy where collection
is contractual rather than optional. But I also know I&amp;rsquo;ve handed someone a way to
take the choice away, and I did it deliberately. Rightly or wrongly, I made the
framework flexible enough to do the wrong thing, and the line I care about is now
only as safe as the judgement of whoever picks it up.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the uncomfortable place this lands, and I&amp;rsquo;ve come to think it&amp;rsquo;s the true
one. A framework can put the choice in the right hands. It cannot make the right
choice. I can build the prompt, build the switch, set the defaults to the modest
thing, and after that I have to trust the engineer on the other side to use it
justly and with some wisdom, because there is nothing further down the stack that
makes them. When the blame gets shared out, and it&amp;rsquo;s always shared, a piece of it
has my name on it, for every escape hatch I left in.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m at peace with that, mostly. Not because the grey went away, but because the
alternative, pretending there&amp;rsquo;s a clean line and that &amp;ldquo;operational&amp;rdquo; means &amp;ldquo;not
your problem&amp;rdquo;, is the real dodge. I&amp;rsquo;d rather say it plainly: this data can be
personal, the consent is real even when there&amp;rsquo;s nobody to ask, and the most a tool
can do is hand the decision to the person who can make it, and trust them with it.&lt;/p&gt;</description></item><item><title>Three traps release-plz sets for a Rust workspace</title><link>https://blog-570662.gitlab.io/three-traps-release-plz-workspace/</link><pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/three-traps-release-plz-workspace/</guid><description>&lt;img src="https://blog-570662.gitlab.io/three-traps-release-plz-workspace/cover-three-traps-release-plz-workspace.png" alt="Featured image of post Three traps release-plz sets for a Rust workspace" /&gt;&lt;p&gt;I wrote up the two days I lost releasing a seventeen-crate workspace to crates.io
as &lt;a class="link" href="https://blog-570662.gitlab.io/same-config-two-answers/" &gt;a war story&lt;/a&gt;, wrong
turns and all. This is the other half: the field guide, so you don&amp;rsquo;t have to lose
the same two days.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://release-plz.dev" target="_blank" rel="noopener"
 &gt;release-plz&lt;/a&gt; is a genuinely good tool, and none of what
follows is a bug. It&amp;rsquo;s three behaviours that are entirely within its design and
will still ambush you the moment you point it at a Cargo &lt;em&gt;workspace&lt;/em&gt; rather than a
single crate. Mildest first, because the third is the one that actually ate my
release.&lt;/p&gt;
&lt;h2 id="first-what-release-plz-is-doing"&gt;First, what release-plz is doing
&lt;/h2&gt;&lt;p&gt;In one line: it&amp;rsquo;s release-please for cargo. It keeps a Release MR open, bumps your
versions and per-crate changelogs from your Conventional Commits, and when that MR
merges it publishes every crate to crates.io and tags the release. On a workspace
where N crates all share one version, &amp;ldquo;the release&amp;rdquo; is N publishes and N tag
operations. Hold on to that N. It&amp;rsquo;s hiding behind all three traps.&lt;/p&gt;
&lt;h2 id="trap-1-the-default-tag-template-is-built-for-one-crate-not-a-workspace"&gt;Trap 1: the default tag template is built for one crate, not a workspace
&lt;/h2&gt;&lt;p&gt;You will reach for one tag per version, and for me it was more than tidiness. I
wanted to ship the whole framework as a single release: one &lt;code&gt;v0.5.1&lt;/code&gt; covering all
seventeen crates, because that was the compatibility promise I wanted to make.
Use the crates that share a version and they&amp;rsquo;re guaranteed to work together. A
single tag felt like the natural way to say &amp;ldquo;this is one coherent release of the
whole thing&amp;rdquo; (and it didn&amp;rsquo;t hurt that the repo already had a &lt;code&gt;v0.5.0&lt;/code&gt; tag from
before release-plz, so one unified tag also looked like continuity). So you either
set this, or, worse, you leave &lt;code&gt;git_tag_name&lt;/code&gt; unset assuming the default does
something workspace-aware:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_tag_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s the catch. release-plz&amp;rsquo;s default &lt;code&gt;git_tag_name&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; &lt;code&gt;v{{ version }}&lt;/code&gt;, and
release-plz tags &lt;strong&gt;per crate&lt;/strong&gt;. So the first crate publishes and creates the tag
&lt;code&gt;v0.5.1&lt;/code&gt;. The second crate publishes and tries to create &lt;code&gt;v0.5.1&lt;/code&gt; again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR failed to create git tag &amp;#39;v0.5.1&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;message&amp;#34;: &amp;#34;Tag v0.5.1 already exists&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By the time you read that error, the first crate (and on a retry, the next, and
the next) is already live on crates.io, and crates.io publishes are forever.
Leaving the line out doesn&amp;rsquo;t save you, because the default is the same
single-crate-shaped template. This is the trap I walked straight into on the
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/f6de975/release-plz.toml#L20-L21" target="_blank" rel="noopener"
 &gt;release commit&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="trap-2-one-release-for-the-whole-workspace-isnt-a-setting-its-a-category-error"&gt;Trap 2: &amp;ldquo;one release for the whole workspace&amp;rdquo; isn&amp;rsquo;t a setting, it&amp;rsquo;s a category error
&lt;/h2&gt;&lt;p&gt;The natural next thought is &amp;ldquo;fine, I&amp;rsquo;ll keep one tag but configure release-plz to
roll the crates into a single release.&amp;rdquo; There&amp;rsquo;s no knob for that, and chasing one
is a waste of an afternoon. release-plz&amp;rsquo;s model is per-crate all the way down:
per-crate tags, per-crate GitLab/GitHub releases, per-crate changelogs. &amp;ldquo;One
unified release for the whole workspace&amp;rdquo; isn&amp;rsquo;t an option it withholds, it&amp;rsquo;s a
shape it doesn&amp;rsquo;t have.&lt;/p&gt;
&lt;p&gt;So you stop fighting it and
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/7afc42e/release-plz.toml#L21-L22" target="_blank" rel="noopener"
 &gt;set the per-crate templates explicitly&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_tag_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{ package }}-v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_release_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{ package }} v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now each crate gets its own tag (&lt;code&gt;rtb-assets-v0.5.1&lt;/code&gt;, &lt;code&gt;rtb-config-v0.5.1&lt;/code&gt;, and so
on) and its own release. It&amp;rsquo;s more objects per version than you wanted, but it&amp;rsquo;s
the grain the tool works in, and once you accept that the collisions stop.&lt;/p&gt;
&lt;p&gt;This is where I had to pull apart two things I&amp;rsquo;d quietly merged in my head: the
version and the tag. The compatibility promise I cared about, that crates sharing
a version work together, is carried by the &lt;em&gt;version&lt;/em&gt;, and release-plz keeps every
crate on the one workspace version no matter how it tags them. The tag is just a
label pointing at a commit. I&amp;rsquo;d wanted a single tag to mean &amp;ldquo;one coherent
framework release&amp;rdquo;, but the coherence was always in the shared version number, not
in the tag. Once that landed, seventeen tags stopped feeling like seventeen
releases of seventeen different things and started looking like what they are:
seventeen labels on one versioned release. The version is not the tag. If you still want
one human-facing narrative for the whole thing, keep a hand-written root
&lt;code&gt;CHANGELOG.md&lt;/code&gt; alongside the generated per-crate ones, rather than trying to make
release-plz aggregate.&lt;/p&gt;
&lt;h2 id="trap-3-a-release-reads-its-config-from-the-release-commit-not-head"&gt;Trap 3: a release reads its config from the release commit, not HEAD
&lt;/h2&gt;&lt;p&gt;This is the small one, and the one that cost me the most, because it makes the fix
for Trap 1 look like it isn&amp;rsquo;t working.&lt;/p&gt;
&lt;p&gt;When release-plz runs a &lt;code&gt;release&lt;/code&gt;, it does not read &lt;code&gt;release-plz.toml&lt;/code&gt; from your
working tree. It reads it from the &lt;strong&gt;release commit&lt;/strong&gt;, the commit that first
introduced the version it&amp;rsquo;s releasing. So picture the obvious recovery: you hit
the tag collision, you realise your template is wrong, you fix it in a follow-up
commit and push to main. Your fix is real. It&amp;rsquo;s committed. It&amp;rsquo;s on the default
branch. And it is completely ignored, because the version hasn&amp;rsquo;t changed, so the
release commit release-plz reads from is still the old one with the old template.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t take this on faith. With the corrected per-crate template sitting on
&lt;code&gt;HEAD&lt;/code&gt;, the CI release job still tried to create the unified tag, pinned to the
&lt;em&gt;old&lt;/em&gt; commit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR failed to create git tag &amp;#39;v0.5.1&amp;#39; with ref &amp;#39;f6de975...&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#34;message&amp;#34;: &amp;#34;Tag v0.5.1 already exists&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;ref&lt;/code&gt; is the release commit, not the HEAD that held my fix. And the cruel
part: &lt;code&gt;release-plz release --dry-run&lt;/code&gt; on your laptop reads your &lt;em&gt;working-directory&lt;/em&gt;
config, so it renders the shiny new per-crate tags and tells you you&amp;rsquo;re sorted. CI
runs the real thing against the release commit and does something else entirely.
Same config file, two different answers depending on who&amp;rsquo;s asking, which is why
the war story has &lt;a class="link" href="https://blog-570662.gitlab.io/same-config-two-answers/" &gt;the title it does&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The operational rule that falls out of this: &lt;strong&gt;any release-plz config change that
affects how a release behaves has to ride along with a version bump, or it does
not apply.&lt;/strong&gt; A &amp;ldquo;fix-up&amp;rdquo; commit on its own is a no-op.&lt;/p&gt;
&lt;h2 id="if-you-set-one-thing"&gt;If you set one thing
&lt;/h2&gt;&lt;p&gt;If you run release-plz on a multi-crate workspace and you change a single line
from the defaults, make it the tag template:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_tag_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{ package }}-v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And set it &lt;em&gt;before&lt;/em&gt; your first release, not during it, so it&amp;rsquo;s already in the
commit that introduces the version, because that&amp;rsquo;s the only commit a release will
ever read it from. Everything else here follows from two facts: the grain is
per-crate, and CI reads history while your laptop reads your working tree. Trust
the history.&lt;/p&gt;
&lt;p&gt;None of this is release-plz misbehaving. Every bit of it is documented and
deliberate. It just isn&amp;rsquo;t where you&amp;rsquo;ll think to look until it has published six
crates you can&amp;rsquo;t take back, which is roughly how I came to know it so well.&lt;/p&gt;</description></item><item><title>Telemetry that asks, and telemetry that doesn't</title><link>https://blog-570662.gitlab.io/telemetry-that-asks-and-telemetry-that-doesnt/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/telemetry-that-asks-and-telemetry-that-doesnt/</guid><description>&lt;img src="https://blog-570662.gitlab.io/telemetry-that-asks-and-telemetry-that-doesnt/cover-telemetry-that-asks-and-telemetry-that-doesnt.png" alt="Featured image of post Telemetry that asks, and telemetry that doesn't" /&gt;&lt;p&gt;go-tool-base has had a thing called telemetry for a long while now. It&amp;rsquo;s the
opt-in kind: the &lt;a class="link" href="https://blog-570662.gitlab.io/telemetry-that-asks-first/" &gt;product analytics&lt;/a&gt;
that asks a user&amp;rsquo;s permission before it phones a single byte home, sits there as
a no-op until they say yes, and can be wiped on request. The whole package is
built around consent.&lt;/p&gt;
&lt;p&gt;Then the &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/" &gt;web-service series&lt;/a&gt;
went and needed telemetry too. Not that telemetry. The other one, the one the
rest of the industry means when it says the word: traces, metrics and logs of a
running service. And the awkward thing about those two is that they share a name,
they want to share a package, and they pull in exactly opposite directions on the
one question that matters most.&lt;/p&gt;
&lt;p&gt;This is the story of how 0.7.x grew a second telemetry without breaking the
first, and where the line between them ended up getting drawn.&lt;/p&gt;
&lt;h2 id="why-bother-putting-it-in-the-framework-at-all"&gt;Why bother putting it in the framework at all
&lt;/h2&gt;&lt;p&gt;The starting point is that I could have left observability out. A reader could
wire up OpenTelemetry in their own service and go about their day. But the six
parts of the web-service series spent a lot of effort making the transports
first-class: a gRPC server, an HTTP server, a gateway, TLS across all of them,
each one a &lt;code&gt;Register&lt;/code&gt; call against the controller. Turning a CLI into a real
long-running service and then shrugging &amp;ldquo;observability is your problem&amp;rdquo; would
have left a hole exactly where it hurts.&lt;/p&gt;
&lt;p&gt;Because a service you can&amp;rsquo;t see into is a liability the moment it leaves your
laptop. The series ended with a macguffin service that was typed, fast and served
over TLS, and was also a black box: when it got slow, you had nowhere to look.
Metrics and traces are how you get the lights on, and they deserved the same
first-class treatment as the things they observe.&lt;/p&gt;
&lt;p&gt;The other half of the reason is that the framework already had a foot in this
world. The analytics package&amp;rsquo;s preferred backend speaks OTLP, the OpenTelemetry
wire protocol. So OpenTelemetry was already in the building. Doing observability
any other way would have meant two standards where one would do.&lt;/p&gt;
&lt;h2 id="the-catch-two-telemetries-opposite-instincts"&gt;The catch: two telemetries, opposite instincts
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where it gets interesting, and it&amp;rsquo;s the part worth slowing down on.&lt;/p&gt;
&lt;p&gt;The analytics telemetry is about a user. It collects usage data, hashed machine
id, which command ran, exit code, and the entire design assumes you have to ask
first. It is off by default. The collector you get when it&amp;rsquo;s disabled is a no-op,
so nothing is recorded until the user opts in, and there&amp;rsquo;s a deletion path for
when they change their mind. That&amp;rsquo;s not an add-on, that&amp;rsquo;s by design.&lt;/p&gt;
&lt;p&gt;The observability telemetry is about a service. It emits operational data, how
long a request took, which span was slow, how many errored, to a collector the
operator runs. And there is no user in the loop to ask. The operator deploys the
service, points it at their collector, and that act is itself the consent. Asking
would be nonsensical: whose permission, for data about their own service, on
their own infrastructure?&lt;/p&gt;
&lt;p&gt;So you have two things called telemetry, wanting to live in one package, with the
opposite default on consent. One is off until someone says yes; the other is on
the moment it&amp;rsquo;s configured. Get that wiring wrong and you fail in one of two ugly
ways. Gate the operational telemetry behind the user&amp;rsquo;s analytics opt-in, and a
service&amp;rsquo;s tracing silently does nothing because nobody ticked a box meant for
something else. Or loosen the analytics gate to make observability flow, and you
start leaking usage data the user never agreed to share. Neither is acceptable,
and &amp;ldquo;just use two packages&amp;rdquo; throws away everything the two genuinely have in
common.&lt;/p&gt;
&lt;h2 id="what-they-actually-share"&gt;What they actually share
&lt;/h2&gt;&lt;p&gt;Quite a lot, as it turns out, and all of it below the consent line.&lt;/p&gt;
&lt;p&gt;Both ship their data over OTLP to a collector. Both need to describe who is
emitting, the service name and version, the resource in OpenTelemetry&amp;rsquo;s terms.
Both parse an endpoint, attach headers, decide whether the connection is
plaintext. None of that has the faintest thing to do with consent. It&amp;rsquo;s just the
plumbing of getting bytes to a collector, and the analytics backend already had
all of it, written inline.&lt;/p&gt;
&lt;p&gt;So the shape of the solution fell out of the problem. Lift the shared plumbing
into one place, let both telemetries stand on it, and keep the consent decision
firmly out of that shared layer. The structure under &lt;code&gt;pkg/telemetry&lt;/code&gt; ended up
like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg/telemetry/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; telemetry.go the analytics Collector (consent-gated)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; backend_otel.go its OTLP backend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; posthog/ datadog/ vendor analytics backends
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; otelcore/ shared: OTLP endpoint, resource, telemetry.* config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tracing/ observability signal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; metrics/ observability signal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; logs/ observability signal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; observability.go Setup: builds the enabled signals (implied consent)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The new &lt;code&gt;otelcore&lt;/code&gt; is the keystone. It holds the three things both sides need and
nothing they don&amp;rsquo;t:
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/otelcore/endpoint.go#L22" target="_blank" rel="noopener"
 &gt;&lt;code&gt;ParseEndpoint&lt;/code&gt;&lt;/a&gt;
for the OTLP URL,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/otelcore/resource.go#L11" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Resource&lt;/code&gt;&lt;/a&gt;
for the service identity, and
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/otelcore/config.go#L33" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Resolve&lt;/code&gt;&lt;/a&gt;
for reading the shared &lt;code&gt;telemetry.*&lt;/code&gt; config (a base endpoint, plus per-signal
overrides, in the same cascade as the TLS config). It imports no signal exporter
and knows nothing about traces, metrics, logs or analytics. It is deliberately
dumb plumbing.&lt;/p&gt;
&lt;h2 id="the-refactor-making-the-old-telemetry-stand-on-the-new-core"&gt;The refactor: making the old telemetry stand on the new core
&lt;/h2&gt;&lt;p&gt;This next part is where the old telemetry and the new one become a single thing.
The analytics OTLP backend was the first user of OTLP in the framework, and it had
grown its own copy of all that
plumbing: a function that parsed the endpoint URL, split out the host and path,
worked out the insecure flag, and built the resource from a service name. Exactly
the code the three new signals were about to need.&lt;/p&gt;
&lt;p&gt;So rather than write it a second time and let the two drift, the analytics
backend was refactored onto &lt;code&gt;otelcore&lt;/code&gt;. Its exporter builder,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/backend_otel.go#L134" target="_blank" rel="noopener"
 &gt;&lt;code&gt;buildOTelExporterOpts&lt;/code&gt;&lt;/a&gt;,
now calls &lt;code&gt;otelcore.ParseEndpoint&lt;/code&gt;, the same function &lt;code&gt;tracing&lt;/code&gt;, &lt;code&gt;metrics&lt;/code&gt; and
&lt;code&gt;logs&lt;/code&gt; call, and the resource comes from &lt;code&gt;otelcore.Resource&lt;/code&gt;, the same one they
use. One implementation of &amp;ldquo;talk OTLP to a collector&amp;rdquo;, four callers: the
analytics backend and the three observability signals. Change how the framework
forms an OTLP endpoint, and every signal moves together.&lt;/p&gt;
&lt;p&gt;The reassuring part was that the analytics tests didn&amp;rsquo;t budge. The refactor moved
code without changing behaviour, and the consent machinery, the opt-in, the
no-op-when-disabled, the deletion path, never came near &lt;code&gt;otelcore&lt;/code&gt;. Which is
exactly the point.&lt;/p&gt;
&lt;h2 id="where-the-line-is"&gt;Where the line is
&lt;/h2&gt;&lt;p&gt;Because the shared core is the easy half. The half that earns its keep is the bit
that isn&amp;rsquo;t shared, and it&amp;rsquo;s a single, deliberate line.&lt;/p&gt;
&lt;p&gt;The analytics collector keeps its gate. The constructor,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/telemetry.go#L84" target="_blank" rel="noopener"
 &gt;&lt;code&gt;NewCollector&lt;/code&gt;&lt;/a&gt;,
still returns a no-op the moment telemetry is disabled, so a user who hasn&amp;rsquo;t opted
in gets a collector that silently discards everything. Informed consent, untouched.&lt;/p&gt;
&lt;p&gt;Observability gets a different door entirely.
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/observability.go#L47" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Setup&lt;/code&gt;&lt;/a&gt;
builds whichever signals the operator has switched on, and it is gated only by
&lt;code&gt;telemetry.tracing.enabled&lt;/code&gt; and its siblings, which the operator sets. It never
consults the analytics opt-in. Turning on tracing doesn&amp;rsquo;t turn on analytics;
disabling analytics doesn&amp;rsquo;t silence tracing. The two enable flags live under the
same &lt;code&gt;telemetry.*&lt;/code&gt; config root, sit next to each other, and never read each
other.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s the whole architecture in a sentence: one package, one OTLP export core,
two consent models that share everything except the answer to &amp;ldquo;do we need to
ask&amp;rdquo;. The principle underneath, the one that decided every one of these calls, is
that the &lt;em&gt;kind of data&lt;/em&gt; sets the consent model. Usage data about a person needs
informed consent. Operational data about a service runs on implied consent. The
CLI and the web service are just where each kind tends to live.&lt;/p&gt;
&lt;h2 id="where-this-leaves-the-framework"&gt;Where this leaves the framework
&lt;/h2&gt;&lt;p&gt;0.7.x came out the other side with both telemetries: the one that asks first,
exactly as it was, and a new one that doesn&amp;rsquo;t, because it has nobody to ask. They
share an export core, a config root and a name, and they part company on the only
thing they were ever going to disagree about.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been careful here to describe how the two consent models are kept apart, not
to argue why they have to be. That argument, that &amp;ldquo;the kind of data decides
the consent model&amp;rdquo; is a line worth holding rather than a convenient bit of
engineering, is a piece of its own, and it&amp;rsquo;s the one I&amp;rsquo;m writing next.&lt;/p&gt;</description></item><item><title>Same config, two answers</title><link>https://blog-570662.gitlab.io/same-config-two-answers/</link><pubDate>Wed, 03 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/same-config-two-answers/</guid><description>&lt;img src="https://blog-570662.gitlab.io/same-config-two-answers/cover-same-config-two-answers.png" alt="Featured image of post Same config, two answers" /&gt;&lt;p&gt;Let me confess a small heresy first, because it&amp;rsquo;s the reason any of this
happened. After a career spent as a branching man, gitflow, gitlabflow, a
tidy &lt;code&gt;develop&lt;/code&gt; branch and a careful dance of merges, I&amp;rsquo;ve come round to
trunk-based development. I resisted it for years. It felt like working without
a net.&lt;/p&gt;
&lt;p&gt;What changed my mind was working solo with an AI pair. The branch ceremony that
earns its keep on a team of eight is just drag when it&amp;rsquo;s me and a model at
two in the morning. So I&amp;rsquo;ve softened on &amp;ldquo;main is always deployable&amp;rdquo; and let the
trunk act as the develop branch, with tagged releases as the actual source of
truth. For compiled languages, where the artefact you ship is a built, tagged
thing and not whatever&amp;rsquo;s on a server right now, that finally clicks.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d already rolled this out on my Go and Terraform projects with
&lt;a class="link" href="https://github.com/apricote/releaser-pleaser" target="_blank" rel="noopener"
 &gt;releaser-pleaser&lt;/a&gt;, a GitLab-native
take on release-please: a bot keeps a Release MR open, and merging it cuts the
tag. It&amp;rsquo;s the same model I wrote about when
&lt;a class="link" href="https://blog-570662.gitlab.io/reviewed-then-applied/" &gt;the infra repo moved to plan-on-merge, apply-on-tag&lt;/a&gt;.
Lovely. Then I came to do the same for rust-tool-base, and Rust, being Rust,
&lt;a class="link" href="https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/" &gt;had opinions&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="rust-brings-its-own-toolchain"&gt;Rust brings its own toolchain
&lt;/h2&gt;&lt;p&gt;releaser-pleaser is happy to tag a repo and write a release. What it does not do
is &lt;code&gt;cargo publish&lt;/code&gt; seventeen crates to crates.io in dependency order. Rust&amp;rsquo;s
release story isn&amp;rsquo;t &amp;ldquo;push a tag and let a runner build a binary&amp;rdquo;, it&amp;rsquo;s a whole
publishing pipeline with a public registry at the end of it, and that registry
has rules of its own. So for the Rust workspace I reached for the tool built for
exactly that job: &lt;a class="link" href="https://release-plz.dev" target="_blank" rel="noopener"
 &gt;release-plz&lt;/a&gt;. Same Release-MR shape,
but it understands cargo, versions every crate, and publishes the lot.&lt;/p&gt;
&lt;p&gt;That was the right call. Getting it to actually do it was where I spent two days
I&amp;rsquo;d quite like back.&lt;/p&gt;
&lt;h2 id="the-gauntlet-before-the-gun"&gt;The gauntlet before the gun
&lt;/h2&gt;&lt;p&gt;Before I got anywhere near the interesting failure, there was a run of CI
papercuts, the sort where every fix politely reveals the next one. GitLab checks
out a detached HEAD, and release-plz wants to be on a branch (&amp;ldquo;HEAD does not
point to a branch&amp;rdquo;), so you re-attach. Then the default &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; can&amp;rsquo;t push
to a protected repo, so you point the remote at a real token. Then release-plz
assumes you&amp;rsquo;re on GitHub and errors that the repo &amp;ldquo;is not hosted in GitHub&amp;rdquo;, so
you tell it &lt;code&gt;--forge gitlab&lt;/code&gt;. Then it refuses to run at all because the &lt;code&gt;pages&lt;/code&gt;
job left a &lt;code&gt;public/&lt;/code&gt; directory lying about and the working tree is &amp;ldquo;dirty&amp;rdquo;, so
you stop pulling artefacts into the job.&lt;/p&gt;
&lt;p&gt;Five merge requests before the thing would even &lt;em&gt;start&lt;/em&gt; doing its actual job.
You can read the
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/7afc42e/.gitlab-ci.yml#L379-L409" target="_blank" rel="noopener"
 &gt;scar tissue in the &lt;code&gt;before_script&lt;/code&gt;&lt;/a&gt;;
every line in it is a fix for something on that list. None of it was hard.
It was just death by a thousand cuts, and I was feeling quite smug by the time
it finally reached the publish step.&lt;/p&gt;
&lt;p&gt;I should not have been.&lt;/p&gt;
&lt;h2 id="tag-v051-already-exists"&gt;&amp;ldquo;Tag v0.5.1 already exists&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;My &lt;code&gt;release-plz.toml&lt;/code&gt; asked for
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/f6de975/release-plz.toml#L20-L21" target="_blank" rel="noopener"
 &gt;one tag per release&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_tag_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_release_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That felt obviously right. It matched the repo&amp;rsquo;s existing &lt;code&gt;v0.5.0&lt;/code&gt; tag, it&amp;rsquo;s how
a single-crate project tags, and the crates all share one workspace version
anyway. One version, one tag. What&amp;rsquo;s to argue with?&lt;/p&gt;
&lt;p&gt;release-plz, that&amp;rsquo;s what. It tags &lt;em&gt;per crate&lt;/em&gt;. So it publishes a crate, creates
the tag, publishes the next crate, and tries to create the same tag again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO published rtb-assets 0.5.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERROR failed to release package
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Caused by:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 0: failed to create git tag &amp;#39;v0.5.1&amp;#39; with ref &amp;#39;f6de975a75...&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1: Response body: { &amp;#34;message&amp;#34;: &amp;#34;Tag v0.5.1 already exists&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 2: HTTP status client error (400 Bad Request) ... /repository/tags
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The collision is annoying. What makes it a proper trap is the half-second before
it: &lt;code&gt;published rtb-assets 0.5.1&lt;/code&gt;. That happened. On crates.io. For keeps. A
crates.io publish is forever, there is no unpublish, only a yank that still
leaves the name and version burned. So every time my flaky pipeline limped one
crate further and then fell over on the tag, it left another crate live on the
public registry that I could never take back. By the time the dust settled, six
of the seventeen were out there: &lt;code&gt;rtb-assets&lt;/code&gt; and &lt;code&gt;rtb-config&lt;/code&gt;, then on a later
retry &lt;code&gt;rtb-credentials&lt;/code&gt; and &lt;code&gt;rtb-error&lt;/code&gt;, then &lt;code&gt;rtb-app&lt;/code&gt; and &lt;code&gt;rtb-redact&lt;/code&gt;. Two
more permanent crates per failed run.&lt;/p&gt;
&lt;h2 id="i-assumed-the-default"&gt;I assumed the default
&lt;/h2&gt;&lt;p&gt;My first fix was the clever one, and it deserves to be on display because it&amp;rsquo;s
the whole lesson in miniature. I deleted the &lt;code&gt;git_tag_name&lt;/code&gt; line. My reasoning:
per-crate tags are release-plz&amp;rsquo;s native model, so surely its &lt;em&gt;default&lt;/em&gt; does the
right thing without me spelling it out. I was confident enough to write it into
the commit message: &amp;ldquo;per-crate tags/releases (release-plz defaults).&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The next run collided on &lt;code&gt;v0.5.1&lt;/code&gt;, exactly as before.&lt;/p&gt;
&lt;p&gt;Because release-plz&amp;rsquo;s default &lt;code&gt;git_tag_name&lt;/code&gt; is not per-crate. It&amp;rsquo;s the unified
&lt;code&gt;v{{ version }}&lt;/code&gt;. I had deleted a line that said the wrong thing and replaced it
with a default that said the &lt;em&gt;same&lt;/em&gt; wrong thing, then congratulated myself for
tidiness. If I&amp;rsquo;d spent thirty seconds on the configuration reference instead of
thirty seconds being clever, I&amp;rsquo;d have read that in black and white.&lt;/p&gt;
&lt;h2 id="same-config-two-answers"&gt;Same config, two answers
&lt;/h2&gt;&lt;p&gt;So I read the manual, and set it explicitly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_tag_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{ package }}-v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;git_release_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{ package }} v{{ version }}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On my laptop, a dry run rendered exactly the per-crate tags I wanted. In CI, the
very next run published another crate and then created the tag &lt;code&gt;v0.5.1&lt;/code&gt;. The
unified one. The wrong one. The one I had just, demonstrably, on main,
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/7afc42e/release-plz.toml#L21-L22" target="_blank" rel="noopener"
 &gt;replaced&lt;/a&gt;.
Same &lt;code&gt;release-plz.toml&lt;/code&gt;, two completely different answers depending on who was
asking.&lt;/p&gt;
&lt;p&gt;That one took me an embarrassingly long time to see. release-plz does not read
your config from the working tree when it runs a &lt;em&gt;release&lt;/em&gt;. It reads it from the
&lt;strong&gt;release commit&lt;/strong&gt;, the commit that introduced the version it&amp;rsquo;s releasing. My
version was still &lt;code&gt;0.5.1&lt;/code&gt;, set days earlier on a commit that still carried the
unified template. You can see it in the failure: the tag it tries to create is
pinned to &lt;code&gt;ref 'f6de975...'&lt;/code&gt;, an old commit, not the HEAD that held my fix.
Every edit I made at the tip of main was real, committed, and utterly invisible
to the release of 0.5.1, because no version bump had created a fresh release
commit for it to read. My fix was correct and inert at the same time. The
dry run read my working directory and looked perfect; CI read history and did
something else.&lt;/p&gt;
&lt;p&gt;There is no config change that rescues an in-flight release. The version was
already out, half-published, tagged wrong, and pointed at a commit I couldn&amp;rsquo;t
edit without bumping the version, which I couldn&amp;rsquo;t cleanly do with six crates
already live.&lt;/p&gt;
&lt;h2 id="doing-it-the-way-id-have-done-it-a-year-ago"&gt;Doing it the way I&amp;rsquo;d have done it a year ago
&lt;/h2&gt;&lt;p&gt;So I stopped. Three retries deep, each one a seventy-minute CI cycle thrown at an
opaque mismatch, six crates already immovable on crates.io, and a tooling problem
I now understood well enough to know the tool was never going to dig me out of
&lt;em&gt;this particular&lt;/em&gt; hole. The question quietly changed from &amp;ldquo;why is it doing this?&amp;rdquo;
to &amp;ldquo;am I going to keep grinding, or finish this the way I would have before I had
clever tooling?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I went manual. &lt;code&gt;cargo publish&lt;/code&gt;, the remaining eleven, by hand, in dependency
order: the leaf crates first and the &lt;code&gt;rust-tool-base&lt;/code&gt; umbrella dead last,
because it depends on all of them. crates.io rate-limits new crate names, so
after a burst it simply made me wait, a roughly half-hour pause in the middle
while the registry caught its breath and I caught mine. Then one &lt;code&gt;v0.5.1&lt;/code&gt; tag,
cut by hand, and one GitLab release to match the convention. The next CI run came
up green, for the gloriously dull reason that there was nothing left to do:
every crate published, the tag already there.&lt;/p&gt;
&lt;h2 id="stop-being-clever-and-rtfm"&gt;Stop being clever and RTFM
&lt;/h2&gt;&lt;p&gt;The tool was never broken. Every single thing it did was documented behaviour I
hadn&amp;rsquo;t bothered to read: that the default tag template is unified, that the model
is per-crate, that a release reads its config from the release commit and not
from HEAD. I assumed my way past the manual three times in a row, and each
assumption cost me real, permanent state on a public registry that doesn&amp;rsquo;t take
returns.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s the part that actually stung, because I should have known better than
most. I wasn&amp;rsquo;t a beginner here. I knew the Release-MR pattern cold, I&amp;rsquo;d shipped it
half a dozen times with releaser-pleaser on my Go and Terraform repos. That
familiarity &lt;em&gt;was&lt;/em&gt; the trap. I trusted the pattern and skipped the tool, on the
lazy assumption that something I understood well in one tool would behave the same
in the next. release-plz carries the same design, but it&amp;rsquo;s a different tool, with
its own defaults and its own idea of where the config lives. The pattern came
across fine. The mechanics didn&amp;rsquo;t, and I never thought to check.&lt;/p&gt;
&lt;p&gt;So here&amp;rsquo;s the lesson, written down in the hope it sticks this time: no matter how
familiar I am with a pattern or a design, the moment I switch the tool that
implements it, reading the manual is paramount. The familiarity is exactly what
tempts you to skip it, and exactly why you can&amp;rsquo;t. (The narrower, more practical
one, while I&amp;rsquo;m here: a config change that affects how a release behaves has to
travel &lt;em&gt;with&lt;/em&gt; a version bump, or it sits there looking applied and doing nothing.)&lt;/p&gt;
&lt;p&gt;release-plz is genuinely good, and every release since has gone out clean on the
first try, the way &lt;a class="link" href="https://blog-570662.gitlab.io/from-allow-failure-to-blocking/" &gt;the rest of the CI now does&lt;/a&gt;.
I just had to stop being clever long enough to read how it actually works. RTFM.
I&amp;rsquo;ll get it tattooed eventually.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 6: seeing what your service is doing</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/cover-building-a-web-service-with-go-tool-base-part-6.png" alt="Featured image of post Building a web service with go-tool-base, part 6: seeing what your service is doing" /&gt;&lt;p&gt;On paper the macguffin service is finished. &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/" &gt;Part
5&lt;/a&gt;
left it typed, fast, documented and served over TLS. So you deploy it, traffic
starts flowing, and a week later someone wanders over to say &amp;ldquo;it&amp;rsquo;s slow&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Slow how? Slow where? Which endpoint, the gateway or the gRPC behind it, the
store or the network in between? You open the logs and there&amp;rsquo;s a wall of &amp;ldquo;request
completed&amp;rdquo; lines, no way to tell which ones belong to the same request, and not a
word about the half-second one of them spent somewhere inside. The service is a
black box that happens to be on fire.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the gap this part closes. A long-running service needs to be observable:
you need to see what it&amp;rsquo;s doing, how often, how fast, and where the time actually
goes. And we get there without bolting on a separate stack, because go-tool-base
speaks OpenTelemetry, and three signals fall out of the same wiring.&lt;/p&gt;
&lt;p&gt;This part is written against &lt;strong&gt;go-tool-base v0.7.1&lt;/strong&gt;, the release that brings the
observability components in.&lt;/p&gt;
&lt;h2 id="the-three-things-you-actually-want-to-see"&gt;The three things you actually want to see
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;Observability&amp;rdquo; is a grand word for three fairly down-to-earth questions. Logs
answer &lt;em&gt;what happened&lt;/em&gt; (you&amp;rsquo;ve had those since part 1). Metrics answer &lt;em&gt;how often,
and how fast&lt;/em&gt;: request rates, error counts, the spread of latencies. And traces
answer &lt;em&gt;where did the time go&lt;/em&gt;, by following a single request as it crosses from
one piece of your system to the next.&lt;/p&gt;
&lt;p&gt;For a service shaped like ours, that last one is the prize. A REST call hits the
gateway, turns into a gRPC call, runs through the store, and comes back. A trace
stitches that whole journey into one timeline, so &amp;ldquo;it&amp;rsquo;s slow&amp;rdquo; stops being a shrug
and becomes &amp;ldquo;it&amp;rsquo;s slow in CreateMacguffin, in the store, for 400ms&amp;rdquo;. You can&amp;rsquo;t
get there from a pile of disconnected log lines.&lt;/p&gt;
&lt;p&gt;OpenTelemetry (OTel, if you&amp;rsquo;ve seen it bandied about) is the vendor-neutral
standard for all three. You instrument once and ship the data to whatever backend
you fancy (Jaeger, Grafana, Honeycomb, a plain collector) over one wire protocol,
OTLP. go-tool-base does the instrumenting; you point it at a collector and pick
your poison.&lt;/p&gt;
&lt;h2 id="turn-it-on"&gt;Turn it on
&lt;/h2&gt;&lt;p&gt;One call wires the lot. In the &lt;code&gt;serve&lt;/code&gt; command from &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-4/" &gt;part
4&lt;/a&gt;,
right after the controller exists:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/telemetry&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Build whichever signals are enabled, install them, flush them on shutdown.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/observability.go#L47" target="_blank" rel="noopener"
 &gt;&lt;code&gt;telemetry.Setup&lt;/code&gt;&lt;/a&gt;
reads your config, builds whichever of the three signals you&amp;rsquo;ve switched on, and
installs them as the process-wide OTel providers. It also hangs their shutdown on
the controller, so when a SIGTERM lands the buffered spans and metrics flush
before the process exits. That&amp;rsquo;s the same graceful-shutdown promise from part 1,
now extended to your telemetry: a clean stop drops nothing, not even the trace
that was mid-flight.&lt;/p&gt;
&lt;p&gt;On its own that line shows you nothing. A signal you haven&amp;rsquo;t enabled is skipped,
so an unconfigured service pays exactly nothing. You turn each on in config, which
we&amp;rsquo;ll get to. First, the instrumentation.&lt;/p&gt;
&lt;h2 id="wire-it-onto-the-transports"&gt;Wire it onto the transports
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Setup&lt;/code&gt; builds the pipeline; two short additions feed it. The gRPC server takes a
stats handler:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;grpc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OTelStatsHandler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the HTTP server takes a middleware. This is also where we finally meet
go-tool-base&amp;rsquo;s middleware chain, which we&amp;rsquo;ve managed without until now. A chain is
just an ordered stack of wrappers around your handler. We put the OTel one first
so it opens the span, and the request logger second, so it runs inside that span
and can see it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OTelMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoggingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the whole code change. Both helpers,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/http/otel.go#L20" target="_blank" rel="noopener"
 &gt;&lt;code&gt;OTelMiddleware&lt;/code&gt;&lt;/a&gt;
and
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/grpc/otel.go#L19" target="_blank" rel="noopener"
 &gt;&lt;code&gt;OTelStatsHandler&lt;/code&gt;&lt;/a&gt;,
lean on the standard OTel instrumentation libraries underneath (&lt;code&gt;otelhttp&lt;/code&gt; and
&lt;code&gt;otelgrpc&lt;/code&gt;), so the spans and metrics they emit follow the conventions every
backend already knows how to read. Every request now opens a span and records its
own duration, on both transports, and you wrote none of it per handler.&lt;/p&gt;
&lt;h2 id="go-deeper-than-the-plumbing"&gt;Go deeper than the plumbing
&lt;/h2&gt;&lt;p&gt;The transport spans show the request crossing HTTP and gRPC, but they stop at the
edge of your handler. To see where the time goes inside it, you open a span
yourself, and that needs no go-tool-base API at all, just OpenTelemetry. Here&amp;rsquo;s
the store doing it, opening a span around the work and tagging it with what it&amp;rsquo;s
handling:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/macguffin/store.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;go.opentelemetry.io/otel&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;go.opentelemetry.io/otel/attribute&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;otel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffinsvc/store&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Store.Create&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin.name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin.quantity&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// ... store the macguffin, generate its id ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin.id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The span starts as a child of whatever is already in the context, which is the
gRPC server span the handler is running under, so it slots into the trace on its
own. The one thing to get right is the &lt;code&gt;context&lt;/code&gt;: give the store method a
&lt;code&gt;ctx context.Context&lt;/code&gt; first parameter and thread it down from the handler, and the
span finds its parent. Tag it with the things you&amp;rsquo;d want when something&amp;rsquo;s wrong,
the macguffin&amp;rsquo;s name and its id, and they show up on the span in the UI, right
next to the timing.&lt;/p&gt;
&lt;h2 id="point-it-at-a-collector"&gt;Point it at a collector
&lt;/h2&gt;&lt;p&gt;The wiring&amp;rsquo;s on; now tell it where to send. The config lives under one root:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# config.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;http://localhost:4318&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;insecure: true # plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;a local collector only&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sampling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# every trace while you&amp;#39;re looking&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;endpoint&lt;/code&gt; is your OTLP collector. Sampling defaults to a level-headed &lt;code&gt;0.1&lt;/code&gt; in
production (you rarely want every trace under real load), so turn it up to &lt;code&gt;1.0&lt;/code&gt;
while you&amp;rsquo;re developing or you&amp;rsquo;ll wonder where yours went. Stand up a collector (a
local Jaeger handles traces and gives you a UI to poke at), start the service, and
push some traffic through the REST side:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/v1/macguffins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -X POST https://localhost:8443/v1/macguffins -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;the-grail&amp;#34;,&amp;#34;quantity&amp;#34;:1}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="one-request-the-whole-way-through"&gt;One request, the whole way through
&lt;/h2&gt;&lt;p&gt;Open the collector and there it is, the thing that justifies the exercise: a
single trace for that POST, four spans deep. The HTTP server span on top, the
gRPC call nested inside it, the gRPC handler inside that, and your &lt;code&gt;Store.Create&lt;/code&gt;
span at the bottom, with the macguffin&amp;rsquo;s name and id sat right on it. The REST
request became a gRPC call, crossed the gateway we built in part 4, ran the
handler and went into the store, and OTel followed it the entire way, because the
trace context travels with the request and everything reads from the same
providers. &amp;ldquo;It&amp;rsquo;s slow&amp;rdquo; finally has somewhere to point, and &amp;ldquo;slow where&amp;rdquo; has an
answer.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A single trace in the collector UI: the HTTP span on top, the gRPC call nested inside it, the gRPC handler inside that, and the Store.Create span at the bottom carrying the macguffin’s name and id" class="gallery-image" data-flex-basis="506px" data-flex-grow="210" height="944" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger.jpg" srcset="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_1dc768fdbfd99c32.jpg 800w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_baf38f0fcdf6f9ed.jpg 1600w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger.jpg 1991w" width="1991"&gt;&lt;/p&gt;
&lt;p&gt;The metrics ride the same pipeline: request counts, error counts, a latency
histogram broken down by route and method. And your logs get an upgrade you didn&amp;rsquo;t
ask for. Every &amp;ldquo;request completed&amp;rdquo; line now carries the &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;
of the request it describes, so a slow trace in the UI and its log line in your
terminal share an id, and you can jump straight from one to the other. That
correlation lands in your stderr logs and in the records shipped to the collector,
both.&lt;/p&gt;
&lt;h2 id="a-word-on-the-other-telemetry"&gt;A word on the other telemetry
&lt;/h2&gt;&lt;p&gt;If you read &lt;a class="link" href="https://blog-570662.gitlab.io/telemetry-that-asks-first/" &gt;Telemetry that asks
first&lt;/a&gt;, you&amp;rsquo;ll know
go-tool-base has a telemetry package for product analytics too: the opt-in kind
that asks a user&amp;rsquo;s permission before it phones anything home. This is not that,
and the difference is the whole point.&lt;/p&gt;
&lt;p&gt;That analytics is about a user, and it&amp;rsquo;s gated behind informed consent: off until
they say yes. This is about your service, emitted by you, to a collector you run,
and it works on implied consent. You switched it on; there&amp;rsquo;s no end user in the
loop to ask. Same package, deliberately separate gates, and never the twain shall
meet. There&amp;rsquo;s a fuller post in that one distinction, and it&amp;rsquo;s the next thing I&amp;rsquo;m
writing.&lt;/p&gt;
&lt;h2 id="where-that-leaves-the-service"&gt;Where that leaves the service
&lt;/h2&gt;&lt;p&gt;Six parts in, the macguffin service starts cleanly, reports its health, serves
gRPC and REST and interactive docs over TLS, shuts down without dropping a
request, and now tells you what it&amp;rsquo;s doing while it runs, all from one annotated
proto and a controller it registers against. Observability was the last missing
sense. The thing was fast and correct before, but you couldn&amp;rsquo;t see inside it.
Now you can, and it cost you about a dozen lines.&lt;/p&gt;</description></item><item><title>The security service I had to switch off</title><link>https://blog-570662.gitlab.io/the-security-service-i-had-to-switch-off/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-security-service-i-had-to-switch-off/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-security-service-i-had-to-switch-off/cover-the-security-service-i-had-to-switch-off.png" alt="Featured image of post The security service I had to switch off" /&gt;&lt;p&gt;A while back I wrote about &lt;a class="link" href="https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/" &gt;hardening the account that would hold the signing
key&lt;/a&gt;,
and one line in it has aged badly. &amp;ldquo;GuardDuty is already looking,&amp;rdquo; I wrote: the
account watched from day one, threat detection on before the key even arrives.
Then I went to apply that baseline to a brand-new account, and GuardDuty wasn&amp;rsquo;t
looking at all, because the account wouldn&amp;rsquo;t let it.&lt;/p&gt;
&lt;h2 id="what-the-baseline-switches-on"&gt;What the baseline switches on
&lt;/h2&gt;&lt;p&gt;The baseline runs a &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/blob/v0.2.0/modules/threat-detection/main.tf#L24-66" target="_blank" rel="noopener"
 &gt;threat-detection module&lt;/a&gt;
that turns on three AWS services: GuardDuty, Security Hub, and IAM Access
Analyzer. Each sits behind an &lt;code&gt;enable_*&lt;/code&gt; toggle, all defaulting to on, which is
the right default. An account about to hold something sensitive should be
watched, flagged and analysed from the start. The hardening post&amp;rsquo;s whole
argument was that you fit the locks before you move the valuables in, and I
stand by it. The gap I hadn&amp;rsquo;t accounted for is the one between a posture you can
&lt;em&gt;design&lt;/em&gt; and a posture a fresh account will actually &lt;em&gt;run&lt;/em&gt; on its first day.&lt;/p&gt;
&lt;h2 id="subscriptionrequiredexception"&gt;SubscriptionRequiredException
&lt;/h2&gt;&lt;p&gt;GuardDuty and Security Hub are first-party AWS services, but they are not &lt;em&gt;on&lt;/em&gt;
by default. They have to be activated for the account before anything can
configure them. Point OpenTofu at a brand-new account that has never had them
switched on and you don&amp;rsquo;t get a security baseline, you get a
&lt;code&gt;SubscriptionRequiredException&lt;/code&gt; and a failed apply. The locks you carefully
specced won&amp;rsquo;t fit, because the door hasn&amp;rsquo;t been registered as a door yet.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d actually met this exact wall from another direction: an aws-nuke dry-run on
a fresh account &lt;a class="link" href="https://blog-570662.gitlab.io/the-cleanup-tool-that-almost-deleted-its-own-hands/" &gt;throws screenfuls of the same exception&lt;/a&gt;
for every service you&amp;rsquo;ve never enabled. Same root cause, different tool. A new
AWS account is a quieter, emptier place than the console makes it look.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a second, duller reason too, and it belongs in the open: GuardDuty and
Security Hub both cost money once their free trial lapses, and the budget for
this account wasn&amp;rsquo;t in place yet. Enabling a service you can&amp;rsquo;t yet afford to
keep on is its own small mistake.&lt;/p&gt;
&lt;h2 id="switching-two-watchers-back-off-on-purpose"&gt;Switching two watchers back off, on purpose
&lt;/h2&gt;&lt;p&gt;So I did the thing that felt wrong and was right. I switched two of them off,
and wrote the reason straight into the module call:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# GuardDuty + Security Hub are deferred. The account&amp;#39;s first-time
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# service activation is pending (it returns SubscriptionRequiredException)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# and the services carry an ongoing cost beyond their free trial.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Re-enable by flipping both to true (or deleting these lines) once
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# activation clears and the budget is in place.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;enable_guardduty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;enable_securityhub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two things make that a deferral rather than a retreat. First, it&amp;rsquo;s scoped:
Access Analyzer needs no subscription and costs nothing, so it stays on. The
account isn&amp;rsquo;t unwatched, it&amp;rsquo;s watched by the part that &lt;em&gt;can&lt;/em&gt; watch it right now.
Second, it carries its own undo. The comment is the re-enable instructions, the
toggles sit right there, and flipping them back is a one-line change the day
activation clears and the budget lands. A disabled control with a written path
back is a deferral. A disabled control with no note is a hole someone finds in a
year.&lt;/p&gt;
&lt;h2 id="why-this-lives-in-the-module-not-in-a-hack"&gt;Why this lives in the module, not in a hack
&lt;/h2&gt;&lt;p&gt;I could only do this cleanly because the module
&lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/blob/v0.2.0/variables.tf#L68-86" target="_blank" rel="noopener"
 &gt;exposes each service as its own toggle&lt;/a&gt;,
and it only gained that recently; the previous version was all-or-nothing on
threat detection. Granular &lt;code&gt;enable_guardduty&lt;/code&gt; and &lt;code&gt;enable_securityhub&lt;/code&gt; flags are
exactly what let you say &amp;ldquo;this account, these two, not yet&amp;rdquo; without forking the
module or commenting resources out. It&amp;rsquo;s the difference between a baseline you
adapt per account and one you fight.&lt;/p&gt;
&lt;h2 id="the-honest-version-of-secure-by-default"&gt;The honest version of &amp;ldquo;secure by default&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;The hardening post wasn&amp;rsquo;t wrong. It was idealised. Secure-by-default is the
right aim, and on a mature account the baseline goes on clean. On a brand-new
one, reality intrudes: services that must be activated before they can be
configured, costs that need a budget before they can be incurred. The honest
response isn&amp;rsquo;t to pretend the posture is fully live when it isn&amp;rsquo;t. It&amp;rsquo;s to
enable what the account will take, defer what it won&amp;rsquo;t, write down precisely why
and how to finish, and leave Access Analyzer watching in the meantime. GuardDuty
will be looking soon enough. It just wasn&amp;rsquo;t looking on day one, and saying so is
better than a comment-free &lt;code&gt;enable = true&lt;/code&gt; that quietly errored on every apply.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 5: docs that write themselves</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/</link><pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/cover-building-a-web-service-with-go-tool-base-part-5.png" alt="Featured image of post Building a web service with go-tool-base, part 5: docs that write themselves" /&gt;&lt;p&gt;The &lt;code&gt;google.api.http&lt;/code&gt; annotations we added in &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-4/" &gt;part
4&lt;/a&gt;
have done one job so far: they told the gateway which REST calls map to which
RPCs. But they describe the API precisely, the paths, the verbs, the request and
response shapes, and a precise description of an API is most of an OpenAPI
document. Feed that document to a viewer and you have an interactive docs site:
every endpoint listed, every field typed, and a &amp;ldquo;try it&amp;rdquo; button that calls the
real service.&lt;/p&gt;
&lt;p&gt;So from one &lt;code&gt;.proto&lt;/code&gt; we&amp;rsquo;ve had gRPC, then REST, and now documentation, none of
it a separate thing to write or keep in sync.&lt;/p&gt;
&lt;h2 id="the-annotations-a-third-time"&gt;The annotations, a third time
&lt;/h2&gt;&lt;p&gt;The pattern of this whole series is one source of truth and several outputs
generated from it. The proto defined the gRPC service; the annotations on it
generated the REST gateway; and those same annotations generate an OpenAPI
document. Add a field to a message, and it shows up in the gRPC API, the REST
API, and the docs, all at once, because all three are read from the proto.&lt;/p&gt;
&lt;h2 id="generate-the-openapi-document"&gt;Generate the OpenAPI document
&lt;/h2&gt;&lt;p&gt;This is one more buf plugin. A small wrinkle to know up front: grpc-gateway ships
its own OpenAPI generator, but it emits OpenAPI v2 (the old Swagger format). For
a v3 document we use &lt;a class="link" href="https://github.com/kollalabs/protoc-gen-openapi" target="_blank" rel="noopener"
 &gt;&lt;code&gt;kollalabs/protoc-gen-openapi&lt;/code&gt;&lt;/a&gt;,
which reads the very same &lt;code&gt;google.api.http&lt;/code&gt; annotations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go install github.com/kollalabs/protoc-gen-openapi@latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# buf.gen.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ... the go, go-grpc and grpc-gateway plugins from before ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;protoc-gen-openapi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/docs/assets&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;title=Macguffin API&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;default_response=false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;go generate ./...&lt;/code&gt; now also writes an &lt;code&gt;openapi.yaml&lt;/code&gt;. It&amp;rsquo;s the REST API described
in full, drawn straight from the annotations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# internal/docs/assets/openapi.yaml (generated, trimmed)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;openapi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3.0.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Macguffin API&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.0.1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;/v1/macguffins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ListMacguffins&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;MacguffinService_ListMacguffins&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pageSize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;query&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;integer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;int32&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;200&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;OK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;application/json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;$ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#/components/schemas/ListMacguffinsResponse&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;GET /v1/macguffins&lt;/code&gt; operation is the &lt;code&gt;get: &amp;quot;/v1/macguffins&amp;quot;&lt;/code&gt; rule from the
proto, turned into OpenAPI. You wrote the annotation once; it now feeds three
generators.&lt;/p&gt;
&lt;h2 id="serve-it-viewer-and-all"&gt;Serve it, viewer and all
&lt;/h2&gt;&lt;p&gt;A spec on disk is a means, not an end; people want to read it. The usual move is
to bolt on a docs UI like &lt;a class="link" href="https://stoplight.io/open-source/elements" target="_blank" rel="noopener"
 &gt;Stoplight
Elements&lt;/a&gt; or Swagger UI, which means
vendoring a couple of megabytes of JavaScript into every project. go-tool-base&amp;rsquo;s
&lt;code&gt;openapi&lt;/code&gt; package does that part for you: the Stoplight Elements UI is embedded in
the framework, so your project ships only its generated spec.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;openapi.Register&lt;/code&gt; mounts both the document and the docs site onto a mux:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/docs/docs.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;embed&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/openapi&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:embed assets/openapi.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Register mounts /openapi.yaml and the Stoplight docs site (/docs/) onto mux.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServeMux&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;openapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;openapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Macguffin API&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;//go:embed&lt;/code&gt; bakes the generated spec into the binary (the same trick as part
3.5), and &lt;code&gt;openapi.Register&lt;/code&gt; serves it at &lt;code&gt;/openapi.yaml&lt;/code&gt; and the Stoplight site
at &lt;code&gt;/docs/&lt;/code&gt;. Wiring it into &lt;code&gt;serve&lt;/code&gt; is one line, on the same mux the gateway is
mounted on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go (the existing HTTP wiring)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stdhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/v1/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// REST, from part 4&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="why-it-goes-on-the-same-server"&gt;Why it goes on the same server
&lt;/h2&gt;&lt;p&gt;That last point is doing more work than it looks. The docs, the spec, and the
live REST API are all on the one HTTP server, so they share an origin. That&amp;rsquo;s what
makes the &amp;ldquo;try it&amp;rdquo; console actually work: when you fill in a request in the docs
and hit send, the browser calls &lt;code&gt;/v1/macguffins&lt;/code&gt; on the same host it loaded the
page from, with no cross-origin dance to configure. And because the certificate is
your mkcert one, the page and its requests are all clean HTTPS, no warnings to
click past, which is exactly why we set the local CA up back in part 2.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/openapi.yaml &lt;span class="p"&gt;|&lt;/span&gt; head -4
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Generated with protoc-gen-openapi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;openapi: 3.0.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;info:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; title: Macguffin API
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Open &lt;code&gt;https://localhost:8443/docs/&lt;/code&gt; in a browser and there&amp;rsquo;s the service:
every endpoint, every field, and a working console that calls the real thing.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The Macguffin API in a Stoplight Elements docs site, served by the same server, with a working “try it” console" class="gallery-image" data-flex-basis="473px" data-flex-grow="197" height="1499" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/docs-in-browser.jpg" srcset="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/docs-in-browser_hu_39b2100829772a5d.jpg 800w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/docs-in-browser_hu_94d369d7ca269652.jpg 1600w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/docs-in-browser_hu_aefc6f5409a27fc3.jpg 2400w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-5/docs-in-browser.jpg 2960w" width="2960"&gt;&lt;/p&gt;
&lt;h2 id="one-proto-three-faces"&gt;One proto, three faces
&lt;/h2&gt;&lt;p&gt;Step back and look at what a single annotated &lt;code&gt;.proto&lt;/code&gt; is now producing: a gRPC
service for the things that speak it, a REST API for the things that don&amp;rsquo;t, and an
interactive docs site for the people who have to consume either. One source, three
faces, and nothing hand-maintained between them. That&amp;rsquo;s the whole argument for
building it this way, and it&amp;rsquo;s why the annotations were worth the small ceremony.&lt;/p&gt;
&lt;p&gt;The service is, by any reasonable measure, done: typed, fast, documented, and
served over TLS. The last part is about what happens once it&amp;rsquo;s out there and
taking traffic. In part 6 we add telemetry and logging, so you can see how it&amp;rsquo;s
being used and why it&amp;rsquo;s slow, without bolting on a separate observability stack.&lt;/p&gt;</description></item><item><title>From allow_failure to blocking</title><link>https://blog-570662.gitlab.io/from-allow-failure-to-blocking/</link><pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/from-allow-failure-to-blocking/</guid><description>&lt;img src="https://blog-570662.gitlab.io/from-allow-failure-to-blocking/cover-from-allow-failure-to-blocking.png" alt="Featured image of post From allow_failure to blocking" /&gt;&lt;p&gt;There&amp;rsquo;s a special kind of CI job that everyone on a team quietly learns to
ignore: the one marked &lt;code&gt;allow_failure: true&lt;/code&gt;. It runs, it goes red, the
pipeline goes green anyway, and after the third time you stop looking at it. I
inherited six of those when I moved rust-tool-base&amp;rsquo;s CI to GitLab. Over a few
days I turned three of them into real gates, and the interesting part was never
the YAML. It was working out which ones had earned the right to block, and
which hadn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="what-allow_failure-actually-buys-you"&gt;What allow_failure actually buys you
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;allow_failure: true&lt;/code&gt; is genuinely useful, and quietly corrosive. It lets a job
report a problem without stopping the pipeline, which is exactly right for a
check that&amp;rsquo;s noisy, not yet stable, or guarding against something you can&amp;rsquo;t fix
this minute. The trouble is that a warning nobody is forced to act on is a
warning nobody acts on. Leave a job advisory long enough and it becomes
scenery: red, ignored, pointless. So an advisory check is really a promise,
&amp;ldquo;I&amp;rsquo;ll make this blocking once it&amp;rsquo;s trustworthy&amp;rdquo;, and a promise you only ever
mean to keep is just a lie you haven&amp;rsquo;t noticed yet.&lt;/p&gt;
&lt;p&gt;When I &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/2213f8e/.gitlab-ci.yml" target="_blank" rel="noopener"
 &gt;migrated rust-tool-base from GitHub Actions to GitLab CI&lt;/a&gt;,
the move landed six jobs as &lt;code&gt;allow_failure: true&lt;/code&gt;: the macOS and Windows tests,
the integration tests, &lt;code&gt;cargo-audit&lt;/code&gt;, &lt;code&gt;trivy&lt;/code&gt;, and coverage. That wasn&amp;rsquo;t
laziness. A migration is the wrong moment to also be fighting flaky gates. But
it left me holding six promises to either keep or admit I wasn&amp;rsquo;t going to.&lt;/p&gt;
&lt;h2 id="a-check-earns-the-right-to-block"&gt;A check earns the right to block
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the rule I settled on. A check earns the right to fail your build when
two things are true: it&amp;rsquo;s &lt;em&gt;meaningful&lt;/em&gt; (a red result is a real problem, not
noise) and it&amp;rsquo;s &lt;em&gt;reliable&lt;/em&gt; (it goes red only when there genuinely is a problem,
and it can actually run to completion). Flip a check to blocking before both
hold and you haven&amp;rsquo;t raised the bar, you&amp;rsquo;ve taught the team to force-merge past
red, which is worse than no gate at all, because now the red means nothing.&lt;/p&gt;
&lt;p&gt;Three of my six crossed that line within a few days. Three deliberately didn&amp;rsquo;t.
The reasons are the whole story.&lt;/p&gt;
&lt;h2 id="trivy-blocked-once-there-was-nothing-to-block-on"&gt;trivy: blocked once there was nothing to block on
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/f9cab20/.gitlab-ci.yml#L247-256" target="_blank" rel="noopener"
 &gt;&lt;code&gt;trivy&lt;/code&gt;&lt;/a&gt;
scans the dependency tree for HIGH and CRITICAL advisories. It went across as
advisory for an honest reason: the &lt;code&gt;Cargo.lock&lt;/code&gt; at migration time already
carried two known HIGH/CRITICAL advisories I hadn&amp;rsquo;t cleared yet, a
path-traversal in &lt;code&gt;gix-validate&lt;/code&gt; and a DNS-rebinding issue in &lt;code&gt;rmcp&lt;/code&gt;. Make
trivy blocking with those sitting there and the pipeline is red from day one,
over problems I already knew about and was already fixing. So it stayed
advisory until the dependency bumps cleared both, and then the &lt;code&gt;allow_failure&lt;/code&gt;
line came out. The gate never changed. The tree underneath it got clean enough
to stand on.&lt;/p&gt;
&lt;h2 id="integration-tests-blocked-once-it-could-actually-run"&gt;integration-tests: blocked once it could actually run
&lt;/h2&gt;&lt;p&gt;The &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/193f380/.gitlab-ci.yml#L200-226" target="_blank" rel="noopener"
 &gt;integration tests&lt;/a&gt;
stand up a real Gitea in a Docker-in-Docker service and talk to it. They were
advisory for a different reason: they couldn&amp;rsquo;t reliably run. dind needs a
privileged runner, and the suite was resolving the container host with a
hardcoded &lt;code&gt;127.0.0.1&lt;/code&gt; that didn&amp;rsquo;t hold everywhere. Blocking a job that fails
for infrastructure reasons rather than code reasons is the fastest way to make
people distrust the entire pipeline. So the fix wasn&amp;rsquo;t in the YAML, it was
making the thing dependable: &lt;code&gt;privileged&lt;/code&gt; set on the runner, and the host
resolved through the test library&amp;rsquo;s own &lt;code&gt;get_host()&lt;/code&gt; instead of a hardcoded
address. Once it ran the same way every time, it earned the gate.&lt;/p&gt;
&lt;h2 id="coverage-blocked-once-it-could-run-at-all-then-once-it-cleared-the-bar"&gt;coverage: blocked once it could run at all, then once it cleared the bar
&lt;/h2&gt;&lt;p&gt;Coverage is the two-step one, and my favourite, because it nearly didn&amp;rsquo;t make
it for a thoroughly undramatic reason: it ran out of memory. &lt;code&gt;cargo llvm-cov&lt;/code&gt;
instruments every test binary, and linking hundreds of instrumented object
files needs more RAM than the shared medium runner had, so the job bus-errored
on the link. I tagged it onto a larger runner, and then the shared SaaS runners
were switched off entirely, so the tag matched nothing and the job sat pending
forever.&lt;/p&gt;
&lt;p&gt;The fix was a &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/193f380/.gitlab-ci.yml#L200-226" target="_blank" rel="noopener"
 &gt;self-hosted homelab runner&lt;/a&gt;
with the RAM the instrumented link actually needs. I moved coverage there but
kept it advisory &lt;em&gt;for one run&lt;/em&gt;, to confirm the box could finish the build
before I trusted it. It did, at
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/1c9e589/.gitlab-ci.yml#L286-320" target="_blank" rel="noopener"
 &gt;73.22% line coverage&lt;/a&gt;,
so I set the gate to fail under 70% and made it blocking. Three points of
headroom: enough that ordinary churn won&amp;rsquo;t trip it, tight enough that a real
drop will. A coverage gate pinned to the current number is a tripwire that
fires on the very next commit; set it a touch below and it catches regressions
instead of normal life.&lt;/p&gt;
&lt;h2 id="the-three-i-left-advisory-on-purpose"&gt;The three I left advisory, on purpose
&lt;/h2&gt;&lt;p&gt;The point was never &amp;ldquo;block everything&amp;rdquo;. Three jobs are still &lt;code&gt;allow_failure&lt;/code&gt; in
&lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/d3c23fc/.gitlab-ci.yml" target="_blank" rel="noopener"
 &gt;the current pipeline&lt;/a&gt;,
deliberately. The macOS and Windows tests run on SaaS runners that bill by the
minute; they&amp;rsquo;re worth running, not worth blocking every merge of a Linux-first
project over a quota I&amp;rsquo;m choosing to ration. And &lt;code&gt;cargo-audit&lt;/code&gt; stays advisory
because &lt;code&gt;cargo-deny&lt;/code&gt; already does the blocking advisory check: cargo-audit is a
second opinion from a different database, and a second opinion that can veto
isn&amp;rsquo;t a second opinion, it&amp;rsquo;s a duplicate gate that will eventually disagree with
the first and block you on the difference.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the same rule from the other side. Those three haven&amp;rsquo;t earned the right
to block, because blocking them would cost more than it ever caught.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;allow_failure: true&lt;/code&gt; is fine as a waiting room and corrosive as a destination.
Every advisory check is a promise to make it blocking once it&amp;rsquo;s both meaningful
and reliable, and the job is to keep the promise or admit you won&amp;rsquo;t. trivy
earned its gate when the advisories cleared, the integration tests when they
ran the same way every time, coverage when it had a runner with enough memory
and a threshold set just below the current mark. The three I left advisory
earned that standing too, by costing more to block than they&amp;rsquo;d catch. The YAML
is one deleted line per job. Knowing which line to delete, and when, is the
whole skill.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 4: REST for free, with the gateway</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-4/</link><pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-4/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-4/cover-building-a-web-service-with-go-tool-base-part-4.png" alt="Featured image of post Building a web service with go-tool-base, part 4: REST for free, with the gateway" /&gt;&lt;p&gt;A quick tally of where &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-3/" &gt;part
3&lt;/a&gt;
left us. One domain, the &lt;code&gt;Store&lt;/code&gt;. One gRPC service over it, mapping the domain to
proto with &lt;code&gt;toProto&lt;/code&gt;. And then a whole second transport, the REST layer, with its
own routing and its own &lt;code&gt;toDTO&lt;/code&gt; mapping the very same domain into the very same
shape, by hand. Two encodings of one thing, drifting apart the moment anyone adds
a field and forgets the other side.&lt;/p&gt;
&lt;p&gt;I promised that doubling would go away. This is the part where it does, and the
thing that does it is the grpc-gateway.&lt;/p&gt;
&lt;h2 id="what-the-gateway-actually-is"&gt;What the gateway actually is
&lt;/h2&gt;&lt;p&gt;The grpc-gateway is a reverse proxy, generated from your &lt;code&gt;.proto&lt;/code&gt;, that speaks
REST on the front and gRPC on the back. A JSON request comes in, the gateway
turns it into the matching gRPC call, hands it to your gRPC server, and turns the
gRPC response back into JSON on the way out.&lt;/p&gt;
&lt;p&gt;Read that again with part 3 in mind, because it&amp;rsquo;s the whole point. The gateway
does the JSON-to-proto-and-back encoding for you, using the proto types your
gRPC server already produces. You wrote &lt;code&gt;domain → proto&lt;/code&gt; once, in the gRPC
adapter. The gateway supplies &lt;code&gt;proto → JSON&lt;/code&gt;. There is no second hand-written
encoding to keep in step, because there is no second implementation: REST becomes
a generated front door onto the gRPC service you already have.&lt;/p&gt;
&lt;p&gt;So the plan is short. Tell the proto which HTTP calls map to which RPCs,
regenerate, wire the gateway in, and delete the part-3 REST layer entirely.&lt;/p&gt;
&lt;h2 id="map-http-onto-the-proto"&gt;Map HTTP onto the proto
&lt;/h2&gt;&lt;p&gt;gRPC has no opinion about URLs and verbs; REST is all URLs and verbs. The bridge
is an annotation, &lt;code&gt;google.api.http&lt;/code&gt;, that you attach to each RPC to say &amp;ldquo;this one
is &lt;code&gt;GET /v1/macguffins/{id}&lt;/code&gt;&amp;rdquo;. Here&amp;rsquo;s the service with those rules added:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// proto/macguffin/v1/macguffin.proto
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;google/api/annotations.proto&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;service&lt;/span&gt; &lt;span class="n"&gt;MacguffinService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;GetMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google.api.http&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;/v1/macguffins/{id}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;ListMacguffins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ListMacguffinsRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ListMacguffinsResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google.api.http&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;/v1/macguffins&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;CreateMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google.api.http&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;/v1/macguffins&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each rule is small but exact. &lt;code&gt;{id}&lt;/code&gt; in the path binds to the &lt;code&gt;id&lt;/code&gt; field of the
request message. &lt;code&gt;body: &amp;quot;*&amp;quot;&lt;/code&gt; on the create says the whole JSON body maps onto the
request. The list takes no body and no path parameter, just the verb and path.
This is the same information part 3&amp;rsquo;s hand-written routes carried, except now it
lives next to the RPC it describes, and a generator reads it instead of you.&lt;/p&gt;
&lt;p&gt;These rules go a good deal further than the three cases we need: query-string
parameters, several URL bindings for a single RPC, custom verbs, choosing which
field becomes the response body. When you reach for those, the &lt;a class="link" href="https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/" target="_blank" rel="noopener"
 &gt;grpc-gateway
docs&lt;/a&gt; walk through
the mapping, and the canonical reference is the &lt;a class="link" href="https://github.com/googleapis/googleapis/blob/master/google/api/http.proto" target="_blank" rel="noopener"
 &gt;&lt;code&gt;HttpRule&lt;/code&gt;
message&lt;/a&gt;
that &lt;code&gt;google.api.http&lt;/code&gt; comes from, its comments document every option.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;annotations.proto&lt;/code&gt; import comes from Google&amp;rsquo;s common protos, so tell buf
where to find them by adding a dependency, then fetch it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# buf.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;buf.build/googleapis/googleapis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;buf dep update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="generate-the-gateway"&gt;Generate the gateway
&lt;/h2&gt;&lt;p&gt;This is another buf plugin, exactly like part 2&amp;rsquo;s. Install it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and add it to the generators:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# buf.gen.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;protoc-gen-go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/gen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;paths=source_relative&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;protoc-gen-go-grpc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/gen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;paths=source_relative&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;protoc-gen-grpc-gateway&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/gen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;paths=source_relative&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;go generate ./...&lt;/code&gt; now also writes &lt;code&gt;macguffin.pb.gw.go&lt;/code&gt;, the gateway: a
&lt;code&gt;RegisterMacguffinServiceHandler&lt;/code&gt; function that, given a connection to your gRPC
server, mounts the REST routes the annotations described.&lt;/p&gt;
&lt;h2 id="wire-it-on"&gt;Wire it on
&lt;/h2&gt;&lt;p&gt;The gateway needs to call your gRPC server, which means dialling it like any
other client, over the same TLS, with credentials that trust its certificate.
That&amp;rsquo;s fiddly to get right by hand, so go-tool-base&amp;rsquo;s &lt;code&gt;gateway&lt;/code&gt; package does it
for you. &lt;code&gt;gateway.New&lt;/code&gt; opens the connection (matching your server&amp;rsquo;s transport
security) and hands you a mux to register the generated handlers on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;serve&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;context&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;stdhttp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;github.com/grpc-ecosystem/grpc-gateway/v2/runtime&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;google.golang.org/grpc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/controls&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/gateway&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/grpc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/props&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/gen/macguffin/v1&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/grpcsvc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ServeOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// gRPC: the one implementation, mapping the domain to proto.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;grpc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMacguffinServiceServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grpcsvc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// REST, for free: the gateway proxies JSON/HTTP to the gRPC server above.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServeMux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ClientConn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMacguffinServiceHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stdhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/v1/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only macguffin-specific line is the one inside the callback,
&lt;code&gt;RegisterMacguffinServiceHandler&lt;/code&gt;. Everything around it, the dial, the
credentials, the mux, is the framework&amp;rsquo;s. Mount the result under &lt;code&gt;/v1/&lt;/code&gt;, register
it on the same controller and HTTP server as before, and you&amp;rsquo;re done.&lt;/p&gt;
&lt;h2 id="delete-the-duplication"&gt;Delete the duplication
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the satisfying bit. The hand-written REST adapter from part 3, the
&lt;code&gt;resthand&lt;/code&gt; package, the routes, the &lt;code&gt;toDTO&lt;/code&gt;, all of it, comes out. You don&amp;rsquo;t need
it: the gateway serves the same REST surface, backed by the gRPC service, from
the proto. The &lt;code&gt;serve&lt;/code&gt; command shrinks to one gRPC server and one gateway, and
your codebase now has a single place where a macguffin becomes JSON.&lt;/p&gt;
&lt;h2 id="see-it-work"&gt;See it work
&lt;/h2&gt;&lt;p&gt;The gateway answers REST, and it&amp;rsquo;s the same store the gRPC service uses:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/v1/macguffins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;macguffins&amp;#34;&lt;/span&gt;:&lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;m-1&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;maltese-falcon&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;:1&lt;span class="o"&gt;}]}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -X POST https://localhost:8443/v1/macguffins -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;the-grail&amp;#34;,&amp;#34;quantity&amp;#34;:1}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;m-2&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;the-grail&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;:1&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create over REST, and the macguffin is there over gRPC a moment later, because
both are the same implementation over the same &lt;code&gt;Store&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ grpcurl localhost:50051 macguffin.v1.MacguffinService/ListMacguffins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;macguffins&amp;#34;&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;m-1&amp;#34;&lt;/span&gt;, ... &lt;span class="o"&gt;}&lt;/span&gt;, &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;m-2&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;the-grail&amp;#34;&lt;/span&gt;, ... &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="errors-and-changing-them"&gt;Errors, and changing them
&lt;/h2&gt;&lt;p&gt;Error handling comes across too. When a gRPC handler returns a status code, the
gateway maps it to the matching HTTP status. The &lt;code&gt;codes.NotFound&lt;/code&gt; we returned back
in part 2 arrives as a 404, with a JSON error body, and we wrote none of it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -s -o /dev/null -w &lt;span class="s1"&gt;&amp;#39;%{http_code}\n&amp;#39;&lt;/span&gt; https://localhost:8443/v1/macguffins/nope
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="m"&gt;404&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That default mapping is the sensible one you&amp;rsquo;d reach for anyway. A few of the
common codes:&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;gRPC code&lt;/th&gt;
 &lt;th&gt;HTTP&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;InvalidArgument&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;400&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Unauthenticated&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;401&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;PermissionDenied&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;403&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;NotFound&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;404&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;AlreadyExists&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;409&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Internal&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;500&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Unavailable&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;503&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So the rule of thumb is simply to return the right &lt;code&gt;codes.*&lt;/code&gt; from your gRPC
handlers, and the REST side gets the right status for free.&lt;/p&gt;
&lt;p&gt;When the default shape isn&amp;rsquo;t what your clients expect, a &lt;code&gt;{&amp;quot;error&amp;quot;: {…}}&lt;/code&gt;
envelope, a trace id header, a tweak to one particular status, you supply your own
error handler. The grpc-gateway takes one as a &lt;code&gt;runtime.ServeMuxOption&lt;/code&gt;, and
&lt;code&gt;gateway.New&lt;/code&gt; passes those straight through:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;register&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMuxOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithErrorHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myErrorHandler&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;myErrorHandler&lt;/code&gt; receives the error and the &lt;code&gt;http.ResponseWriter&lt;/code&gt; and writes
whatever your API promises. That same &lt;code&gt;WithMuxOptions&lt;/code&gt; hatch takes the gateway&amp;rsquo;s
other knobs, header matchers, custom marshalers, and the rest; they&amp;rsquo;re all in the
&lt;a class="link" href="https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/" target="_blank" rel="noopener"
 &gt;grpc-gateway docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;This is the shape the series was building towards. One domain, one gRPC
implementation, one &lt;code&gt;domain → proto&lt;/code&gt; mapping you wrote by hand, and a REST API
generated from the same proto that needed no second implementation and no second
encoding. The things that speak gRPC get gRPC; the browser, the webhook and the
&lt;code&gt;curl&lt;/code&gt; get JSON; and there&amp;rsquo;s exactly one place to change when a macguffin grows a
field.&lt;/p&gt;
&lt;p&gt;Those &lt;code&gt;google.api.http&lt;/code&gt; annotations have one more trick in them. They describe
your REST API precisely enough to generate an OpenAPI document, and in part 5 we
serve that as a live, clickable docs site, from the very same proto.&lt;/p&gt;</description></item><item><title>Nobody's coming to clean your supply chain</title><link>https://blog-570662.gitlab.io/nobody-is-coming-to-clean-your-supply-chain/</link><pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/nobody-is-coming-to-clean-your-supply-chain/</guid><description>&lt;img src="https://blog-570662.gitlab.io/nobody-is-coming-to-clean-your-supply-chain/cover-nobody-is-coming-to-clean-your-supply-chain.png" alt="Featured image of post Nobody's coming to clean your supply chain" /&gt;&lt;p&gt;Pick a week in May 2026 and there&amp;rsquo;s a supply-chain attack in it. On the 11th
someone owned TanStack&amp;rsquo;s CI and pushed
&lt;a class="link" href="https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised" target="_blank" rel="noopener"
 &gt;84 poisoned package versions in six minutes&lt;/a&gt;.
On the 14th, three malicious versions of
&lt;a class="link" href="https://www.stepsecurity.io/blog/node-ipc-npm-supply-chain-attack" target="_blank" rel="noopener"
 &gt;node-ipc&lt;/a&gt;,
a library with ten million weekly downloads, shipped an identical
credential-stealer. Days later it was
&lt;a class="link" href="https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/" target="_blank" rel="noopener"
 &gt;@antv&lt;/a&gt;,
cascading down into a charting library a million projects depend on. Each one
runs its payload the moment you install it, then quietly tries to publish
itself from your machine.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ve heard this story so many times the outrage has worn smooth. So let me
point at the one detail that should still make you sit up: the TanStack
packages carried &lt;em&gt;valid signing provenance&lt;/em&gt;. Real attestation, pointing at the
real pipeline. The seal was genuine. The contents were poison.&lt;/p&gt;
&lt;h2 id="a-signature-proves-the-sender-not-the-contents"&gt;A signature proves the sender, not the contents
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve spent a fair while &lt;a class="link" href="https://blog-570662.gitlab.io/verifying-your-own-downloads/" &gt;building integrity and signing into my own
tools&lt;/a&gt;, so this one
stings a little. Signing is a trust mechanism, and a good one. It&amp;rsquo;s how I prove
a binary you downloaded was built and published by me and nobody else, and in a
world with this many ways to be impersonated, that matters more than ever.&lt;/p&gt;
&lt;p&gt;But TanStack shows the limit in neon. If the pipeline doing the signing is
itself compromised, the signature is still perfectly valid. It just now
certifies a lie. Provenance answers &amp;ldquo;did this come from where it claims?&amp;rdquo; It
does not answer &amp;ldquo;is what&amp;rsquo;s inside safe?&amp;rdquo;, and we have spent a few years quietly
letting people treat those as the same question.&lt;/p&gt;
&lt;p&gt;They aren&amp;rsquo;t. A signature is a promise about the &lt;em&gt;sender&lt;/em&gt;. The thing we actually
need is a promise about the &lt;em&gt;contents&lt;/em&gt;: that whoever signed has done the
diligence, the testing, the vetting, to vouch for what&amp;rsquo;s in the tin. A
signature without that behind it isn&amp;rsquo;t a safety certificate. It&amp;rsquo;s a
tamper-proof seal on a poisoned jar.&lt;/p&gt;
&lt;h2 id="it-was-never-just-npm"&gt;It was never just npm
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s tempting to file all this under &amp;ldquo;npm being npm&amp;rdquo;. Resist it, because it&amp;rsquo;s a
category error. The thing that makes these attacks work, a stranger&amp;rsquo;s code
running on your machine as a side effect of installing or building, is not an
npm bug. It&amp;rsquo;s a near-universal design choice.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Ecosystem&lt;/th&gt;
 &lt;th&gt;Untrusted code on install/build?&lt;/th&gt;
 &lt;th&gt;Mechanism&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;npm&lt;/strong&gt; (JS)&lt;/td&gt;
 &lt;td&gt;Yes, at install (dependencies too)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;pre&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; scripts&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;PyPI&lt;/strong&gt; (Python)&lt;/td&gt;
 &lt;td&gt;sdist yes, wheel no&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;setup.py&lt;/code&gt;; wheels forbid hooks&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;RubyGems&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;Yes, at install&lt;/td&gt;
 &lt;td&gt;native-extension build (&lt;code&gt;extconf.rb&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;cargo&lt;/strong&gt; (Rust)&lt;/td&gt;
 &lt;td&gt;Yes, at build&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;build.rs&lt;/code&gt; and proc-macros&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Composer&lt;/strong&gt; (PHP)&lt;/td&gt;
 &lt;td&gt;Dependencies: no&lt;/td&gt;
 &lt;td&gt;only the &lt;em&gt;root&lt;/em&gt; project&amp;rsquo;s scripts run, by design&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Maven/Gradle&lt;/strong&gt; (JVM)&lt;/td&gt;
 &lt;td&gt;Yes, at build&lt;/td&gt;
 &lt;td&gt;build scripts and plugins&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;NuGet&lt;/strong&gt; (.NET)&lt;/td&gt;
 &lt;td&gt;Modern: no&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;install.ps1&lt;/code&gt;, legacy format only&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt; (modules)&lt;/td&gt;
 &lt;td&gt;No&lt;/td&gt;
 &lt;td&gt;no install or build hooks&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(Lifecycle hooks across ecosystems are catalogued at
&lt;a class="link" href="https://github.com/ecosyste-ms/package-manager-hooks" target="_blank" rel="noopener"
 &gt;ecosyste.ms&lt;/a&gt; if you want
the receipts.)&lt;/p&gt;
&lt;p&gt;Read that and the lesson isn&amp;rsquo;t &amp;ldquo;npm is uniquely bad&amp;rdquo;, it&amp;rsquo;s &amp;ldquo;this was a choice,
and several ecosystems chose differently&amp;rdquo;. Go runs no install or build hooks at
all. PHP&amp;rsquo;s Composer flatly refuses to run a dependency&amp;rsquo;s scripts, only your own
project&amp;rsquo;s. Python&amp;rsquo;s wheel format forbids install hooks. The hook was never
inevitable.&lt;/p&gt;
&lt;p&gt;And yes, that includes my own back yard. cargo&amp;rsquo;s &lt;code&gt;build.rs&lt;/code&gt; is the same gun
fired at build time instead of install time, and the
&lt;a class="link" href="https://socket.dev/blog/trapdoor-crypto-stealer-npm-pypi-crates" target="_blank" rel="noopener"
 &gt;TrapDoor campaign&lt;/a&gt;
used exactly that to rifle through keystores on crates.io this year. Rust isn&amp;rsquo;t
safe here. It&amp;rsquo;s a smaller, better-policed target, which is a different thing,
and I&amp;rsquo;d rather say so than pretend one of my favourite languages is above it.&lt;/p&gt;
&lt;h2 id="no-registry-can-hand-you-a-clean-package"&gt;No registry can hand you a clean package
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the uncomfortable core. Not one of these registries can guarantee the
package you pull is clean. They can sign it, scan it, attest its origin and
mandate 2FA on maintainers, and they should do all of that. But none of it is a
guarantee, because the failure modes are endless and attackers keep finding new
ones. A maintainer account gets phished. A CI token leaks. A trusted
contributor turns. A dependency four levels down quietly changes hands.&lt;/p&gt;
&lt;p&gt;So the onus lands, and will keep landing for a good while yet, on the consuming
engineer. That isn&amp;rsquo;t a comfortable answer or a clever one. It&amp;rsquo;s the true one.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s a genuinely rotten spot to stand in, because the advice contradicts
itself. Patch slowly and you&amp;rsquo;re scolded for running known-vulnerable
dependencies. Patch the instant a release drops and you&amp;rsquo;ve skipped the
bedding-in that might have caught a poisoned one. There&amp;rsquo;s no setting on that
dial that&amp;rsquo;s safe, only trade-offs you have to actually think about. Add CI that
leaks credentials it never needed, and a dependency tree thousands of strangers
deep, and you can see why there&amp;rsquo;s no single villain to blame and no single
switch to flip.&lt;/p&gt;
&lt;h2 id="the-boring-discipline-that-actually-helps"&gt;The boring discipline that actually helps
&lt;/h2&gt;&lt;p&gt;What&amp;rsquo;s left isn&amp;rsquo;t heroic, it&amp;rsquo;s hygiene, and it&amp;rsquo;s the unglamorous stuff I keep
banging on about.
&lt;a class="link" href="https://blog-570662.gitlab.io/openssf-scorecard-graded-my-supply-chain/" &gt;Pin your CI actions to commit SHAs&lt;/a&gt;
so a moved tag can&amp;rsquo;t swap code under you. Commit your lockfiles. Run the
auditors, &lt;code&gt;cargo-audit&lt;/code&gt;, &lt;code&gt;pip-audit&lt;/code&gt;, &lt;code&gt;govulncheck&lt;/code&gt;, &lt;code&gt;npm audit&lt;/code&gt;, or Google&amp;rsquo;s
cross-ecosystem &lt;a class="link" href="https://github.com/google/osv-scanner" target="_blank" rel="noopener"
 &gt;OSV-Scanner&lt;/a&gt;, on every
build. Gate the dependency tree and
&lt;a class="link" href="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/" &gt;give every exception an expiry date&lt;/a&gt;
so &amp;ldquo;we&amp;rsquo;ll deal with it later&amp;rdquo; can&amp;rsquo;t quietly become &amp;ldquo;never&amp;rdquo;. Keep the tree
small: every crate you don&amp;rsquo;t add is a stranger you don&amp;rsquo;t have to trust.&lt;/p&gt;
&lt;p&gt;None of that is a solution. All of it is diligence, and diligence is the only
thing that was ever going to stand behind the signature. When I sign a release,
the cryptography is the easy part. The promise underneath it, that I pinned,
locked, audited, vetted and tested before I put my name on it, is the part
worth anything. That&amp;rsquo;s the contract. The signature is just how I countersign
it.&lt;/p&gt;
&lt;p&gt;The encouraging note is that the structural defences exist and they work. Go&amp;rsquo;s
checksum database and its refusal to run hooks. Composer declining to trust a
dependency&amp;rsquo;s scripts. Python&amp;rsquo;s wheels. &lt;code&gt;cargo-vet&lt;/code&gt; and &lt;code&gt;cargo-deny&lt;/code&gt; giving you
somewhere to record human judgement at scale. More ecosystems should steal
these shamelessly, because a registry that makes the safe path the &lt;em&gt;default&lt;/em&gt;
does the working engineer a far bigger favour than one that leaves it all to
discipline.&lt;/p&gt;
&lt;h2 id="the-same-shape-a-third-time"&gt;The same shape, a third time
&lt;/h2&gt;&lt;p&gt;If this feels familiar, it should. I wrote recently about
&lt;a class="link" href="https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/" &gt;a bug bounty that collapsed because the cost of slop was deferred&lt;/a&gt;,
and about &lt;a class="link" href="https://blog-570662.gitlab.io/the-greybeards-edge-was-never-typing/" &gt;a junior pipeline being cut because the bill lands years
later&lt;/a&gt;.
Supply-chain security is the same shape a third time. The convenience is now,
the catastrophe is later, and the only thing standing in the gap is an engineer
paying attention, doing the dull work, refusing to be rushed into trusting
something they haven&amp;rsquo;t checked.&lt;/p&gt;
&lt;p&gt;There is no clean package waiting to be found, no registry about to solve this
for us, no signature that means &amp;ldquo;safe&amp;rdquo; all on its own. There&amp;rsquo;s the diligence
you do before you put your name to something, and the judgement to know when an
install is asking you to trust more than you should. For a good while yet, that
is the whole job. Boring, unfashionable, and the only thing that works.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 3.5: the same server, now a website</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/</link><pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/cover-building-a-web-service-with-go-tool-base-serving-content.png" alt="Featured image of post Building a web service with go-tool-base, part 3.5: the same server, now a website" /&gt;&lt;p&gt;The HTTP server from &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-3/" &gt;part
3&lt;/a&gt;
serves JSON. But &lt;code&gt;net/http&lt;/code&gt; doesn&amp;rsquo;t care what you hand it: HTML, an image, a
stylesheet, a whole little site, it&amp;rsquo;s all just bytes with a content type. So
before we get back to the API in part 4, a short detour to prove the point and
pick up a couple of genuinely useful tools: we&amp;rsquo;ll turn the macguffin service into
a tiny website.&lt;/p&gt;
&lt;p&gt;This is a bonus, off to the side of the API arc, but it earns its place. Real
services nearly always grow a bit of HTML eventually: a status page, a landing
page, a small admin view, an embedded docs site (we&amp;rsquo;ll do exactly that in part
5). The mechanics are the same every time, and worth having in hand.&lt;/p&gt;
&lt;h2 id="a-page-from-htmltemplate"&gt;A page from &lt;code&gt;html/template&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;html/template&lt;/code&gt; renders HTML from a template and your data, and it escapes
that data on the way out, so a macguffin called &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; becomes text rather
than a problem. Here&amp;rsquo;s a page that lists the catalogue:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;&amp;lt;!-- internal/site/templates/index.html --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;meta&lt;/span&gt; &lt;span class="na"&gt;charset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Macguffins&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;title&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/static/style.css&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;The macguffin catalogue&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{range .}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;{{.Name}} &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;qty&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;times;&lt;/span&gt;{{.Quantity}}&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; {{end}}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;{{range .}}&lt;/code&gt; walks the slice we pass in, and &lt;code&gt;{{.Name}}&lt;/code&gt; / &lt;code&gt;{{.Quantity}}&lt;/code&gt; read
each macguffin&amp;rsquo;s fields. The data is the same &lt;code&gt;Store&lt;/code&gt; from part 2, so the page is
a view onto the very same domain the gRPC and JSON APIs serve.&lt;/p&gt;
&lt;h2 id="shipping-the-files-inside-the-binary"&gt;Shipping the files inside the binary
&lt;/h2&gt;&lt;p&gt;A template and a stylesheet are files, and you do not want to deploy a folder of
loose assets next to your binary and hope they line up. Go&amp;rsquo;s &lt;code&gt;embed&lt;/code&gt; package
bakes them into the binary at build time, so the whole thing ships as one file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/site/site.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;embed&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;html/template&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;io/fs&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:embed templates static&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Must&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParseFS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;templates/index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Site&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Site&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;Site&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServeMux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;static&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;static&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// the embedded path is a compile-time constant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;GET /{$}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;GET /static/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StripPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/static/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FileServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;static&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tmpl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;List&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three things are doing the work. &lt;code&gt;//go:embed templates static&lt;/code&gt; pulls both folders
into the &lt;code&gt;content&lt;/code&gt; filesystem. &lt;code&gt;template.ParseFS&lt;/code&gt; parses the page from it once at
startup. And &lt;code&gt;http.FileServer(http.FS(static))&lt;/code&gt; serves the stylesheet (and
anything else under &lt;code&gt;static/&lt;/code&gt;) straight from the embedded files, with content
types set for you, so &lt;code&gt;GET /static/style.css&lt;/code&gt; comes back as &lt;code&gt;text/css&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;GET /{$}&lt;/code&gt; pattern is worth a note: the &lt;code&gt;{$}&lt;/code&gt; anchors it to the exact root
path, so &lt;code&gt;/&lt;/code&gt; renders the page but &lt;code&gt;/anything-else&lt;/code&gt; doesn&amp;rsquo;t accidentally match it.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d rather edit templates without rebuilding during development, swap the
embedded filesystem for the real one: &lt;code&gt;http.FileServer(http.Dir(&amp;quot;static&amp;quot;))&lt;/code&gt;, and
&lt;code&gt;template.ParseGlob&lt;/code&gt; instead of &lt;code&gt;ParseFS&lt;/code&gt;. Embed for release, disk for the
edit-refresh loop; the handlers don&amp;rsquo;t change.&lt;/p&gt;
&lt;h2 id="on-the-same-server"&gt;On the same server
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Routes()&lt;/code&gt; hands back a &lt;code&gt;*http.ServeMux&lt;/code&gt;, which is an &lt;code&gt;http.Handler&lt;/code&gt;, so it
registers exactly like the JSON API did, on the same controller, with the same
TLS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go (or a dedicated command)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;site&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;site&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because the certificate is the mkcert one from part 2, opening
&lt;code&gt;https://localhost:8443/&lt;/code&gt; renders the page, stylesheet and all, with a clean
padlock on any machine that trusts your local CA (where you ran &lt;code&gt;mkcert -install&lt;/code&gt;); anywhere else, the browser flags the cert, exactly as it should.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The macguffin catalogue rendered in a browser, served over HTTPS by the same server as the API" class="gallery-image" data-flex-basis="614px" data-flex-grow="255" height="714" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/site-in-browser.jpg" srcset="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/site-in-browser_hu_d724581ca983d743.jpg 800w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/site-in-browser_hu_65e8a6549a61f1fa.jpg 1600w, https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-serving-content/site-in-browser.jpg 1827w" width="1827"&gt;&lt;/p&gt;
&lt;p&gt;The same hardened server, the same graceful shutdown, the same &lt;code&gt;/healthz&lt;/code&gt;, now
serving a website instead of (or alongside) JSON.&lt;/p&gt;
&lt;h2 id="back-to-the-api"&gt;Back to the API
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the whole trick: the HTTP server is just &lt;code&gt;net/http&lt;/code&gt;, and it will serve
whatever you point it at, escaped and content-typed properly, shipped inside the
binary. We&amp;rsquo;ll use exactly this in part 5 to serve interactive API docs.&lt;/p&gt;
&lt;p&gt;Detour over. In part 4 we get back to the API and finally deal with that
duplicated REST layer, the one we wrote twice and promised to delete.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 3: a REST service, by hand and by spec</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-3/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-3/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-3/cover-building-a-web-service-with-go-tool-base-part-3.png" alt="Featured image of post Building a web service with go-tool-base, part 3: a REST service, by hand and by spec" /&gt;&lt;p&gt;The gRPC service from &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-2/" &gt;part
2&lt;/a&gt;
is the right core for service-to-service traffic. It is also useless to a great
many of the things that might want to call it: a browser, a webhook from some
SaaS, a partner who will not touch protobuf, a &lt;code&gt;curl&lt;/code&gt; in a 2am runbook. They all
want the same thing, plain JSON over HTTP.&lt;/p&gt;
&lt;p&gt;So we need a REST face as well. The only way we know how so far is to build one,
a second implementation of the macguffin service, this time over HTTP. We&amp;rsquo;ll do
it two ways, by hand and from a spec, and wire it onto the very same controller
from part 1. And yes, we&amp;rsquo;ll feel the duplication, because the next part is about
making it disappear.&lt;/p&gt;
&lt;h2 id="why-not-just-nethttp"&gt;Why not just &lt;code&gt;net/http&lt;/code&gt;?
&lt;/h2&gt;&lt;p&gt;A fair question before we register anything: go-tool-base&amp;rsquo;s HTTP server is
&lt;code&gt;net/http&lt;/code&gt; underneath, so why not stand up a &lt;code&gt;http.Server{}&lt;/code&gt; yourself? You can,
and nothing here hides the standard library from you. What &lt;code&gt;gtbhttp.Register&lt;/code&gt;
adds is the dull, easy-to-botch scaffolding that goes around the handler:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s still net/http.&lt;/strong&gt; You write ordinary &lt;code&gt;http.Handler&lt;/code&gt;s. There&amp;rsquo;s no
bespoke router and no framework to learn underneath, just the standard library
doing the serving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timeouts and limits you&amp;rsquo;d otherwise forget.&lt;/strong&gt; A bare &lt;code&gt;http.Server{}&lt;/code&gt; has no
&lt;code&gt;ReadTimeout&lt;/code&gt; or &lt;code&gt;WriteTimeout&lt;/code&gt;, which is a Slowloris waiting to happen. You
get sane read/write/idle timeouts and caps on header and body sizes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The hardened TLS from part 2,&lt;/strong&gt; the same shared certificate and the same
1.2-minimum AEAD config, with no extra setup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lifecycle for free.&lt;/strong&gt; It registers on the same controller as your gRPC
server, so a SIGTERM drains in-flight requests and stops both together,
instead of you re-writing the signal-and-&lt;code&gt;Shutdown&lt;/code&gt; dance for every service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health endpoints,&lt;/strong&gt; &lt;code&gt;/healthz&lt;/code&gt;, &lt;code&gt;/livez&lt;/code&gt; and &lt;code&gt;/readyz&lt;/code&gt;, backed by the
controller and ready for an orchestrator to probe, with nothing to write.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You write the handlers; it owns the lifecycle and the hardening. With that
settled, let&amp;rsquo;s write some handlers.&lt;/p&gt;
&lt;h2 id="by-hand-on-the-standard-library"&gt;By hand, on the standard library
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;net/http&lt;/code&gt;&amp;rsquo;s &lt;code&gt;ServeMux&lt;/code&gt; acquired method and path patterns in Go 1.22, so a small
REST surface needs no router and no dependencies at all. And the hard part is
already behind us: the &lt;code&gt;Store&lt;/code&gt; from part 2 is our domain, and the HTTP handlers
are just another thin adapter over it, exactly as the gRPC server was. A small
type holds the store, and the routes hang off it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/resthand/server.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resthand&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;encoding/json&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServeMux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;GET /macguffins&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;GET /macguffins/{id}&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;POST /macguffins&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each pattern names a method and a path, and &lt;code&gt;{id}&lt;/code&gt; is a named wildcard a handler
reads back with &lt;code&gt;r.PathValue(&amp;quot;id&amp;quot;)&lt;/code&gt;, no third-party router required. And because
the domain &lt;code&gt;Macguffin&lt;/code&gt; already carries JSON tags, this adapter can encode it
straight to the response, with no separate type to map to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/resthand/server.go (same file)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PathValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin not found&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;writeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;list&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt; are the same shape: ask the store, encode the result with a
small &lt;code&gt;writeJSON&lt;/code&gt; helper. &lt;code&gt;Routes()&lt;/code&gt; hands back a &lt;code&gt;*http.ServeMux&lt;/code&gt;, which is
itself an &lt;code&gt;http.Handler&lt;/code&gt;, so it slots straight into the controller in a moment.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s nothing clever in any of it, and that&amp;rsquo;s the appeal: total control, no
tooling, and because we serve the domain type directly, nothing to map. The cost
is the kind that creeps up on you. Every route, every bit of marshalling, every
status code is yours to write and keep correct, and as the surface grows, so does
the area for small mistakes.&lt;/p&gt;
&lt;h2 id="by-spec-with-oapi-codegen"&gt;By spec, with oapi-codegen
&lt;/h2&gt;&lt;p&gt;The other road is the OpenAPI mirror of part 2&amp;rsquo;s proto. You describe the API in
an OpenAPI document, and a generator turns it into a Go interface for you to
implement. The tool is
&lt;a class="link" href="https://github.com/oapi-codegen/oapi-codegen" target="_blank" rel="noopener"
 &gt;oapi-codegen&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First the contract, &lt;code&gt;api/macguffin.openapi.yaml&lt;/code&gt; (trimmed to one path here):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# api/macguffin.openapi.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/macguffins/{id}:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;getMacguffin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;string }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;200&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;OK&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;application/json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;$ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;#/components/schemas/Macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;404&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Not found&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;schemas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;object&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;id, name, quantity]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;string }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;string }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;{&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type: integer, format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;int32 }&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Install the tool and tell it what to emit with a small config. We want the
standard-library server, so &lt;code&gt;std-http-server&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# api/oapi-codegen.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;restapi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/restapi/macguffin.gen.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;std-http-server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As in part 2, wire it into &lt;code&gt;go generate&lt;/code&gt; so it can&amp;rsquo;t drift, this time sitting
right alongside the &lt;code&gt;buf&lt;/code&gt; directive:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// gen.go (at your module root): add the second line alongside part 2&amp;#39;s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:generate buf generate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:generate oapi-codegen -config api/oapi-codegen.yaml api/macguffin.openapi.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;go generate ./...&lt;/code&gt; now regenerates both the gRPC and the REST code in one go.
What oapi-codegen writes is the message models and, the important part, a
&lt;code&gt;ServerInterface&lt;/code&gt;, one method per operation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/restapi/macguffin.gen.go (generated)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ServerInterface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;ListMacguffins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;CreateMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;GetMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If that feels familiar, it should: it&amp;rsquo;s the exact shape from part 2. The
generator hands you an interface and your job is a type that honours it, an
adapter over the same &lt;code&gt;Store&lt;/code&gt;. But here&amp;rsquo;s the wrinkle that earns this part its
keep. oapi-codegen generated its own &lt;code&gt;Macguffin&lt;/code&gt; type, a DTO, so the adapter has
to map the domain into it. (The path parameter, at least, arrives already typed
as &lt;code&gt;id string&lt;/code&gt; rather than fished out by hand.)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/restapi/server.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restapi&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;net/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;GetMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin not found&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;writeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;toDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// toDTO maps the domain type to oapi-codegen&amp;#39;s generated Macguffin DTO.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;toDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;toDTO&lt;/code&gt; is the second mapping of this kind we&amp;rsquo;ve written. Part 2&amp;rsquo;s gRPC
adapter had &lt;code&gt;toProto&lt;/code&gt;; this one has &lt;code&gt;toDTO&lt;/code&gt;. The same domain data, encoded twice,
into two generated shapes, kept in step by hand. Hold that thought: part 4 is
where that second mapping stops being something you write.&lt;/p&gt;
&lt;p&gt;A generated helper turns your implementation into an &lt;code&gt;http.Handler&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;restapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HandlerFromMux&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;restapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So which road? By hand when it&amp;rsquo;s a handful of endpoints you fully control and
you&amp;rsquo;d rather not own a generator. Spec-first when the contract carries weight,
several teams consume the API, the shape changes often, or you simply want the
OpenAPI document to exist, which (spoiler) we&amp;rsquo;ll be serving as live, clickable
docs in part 5.&lt;/p&gt;
&lt;h2 id="wire-it-onto-the-controller"&gt;Wire it onto the controller
&lt;/h2&gt;&lt;p&gt;Whichever road you took, you&amp;rsquo;re holding an &lt;code&gt;http.Handler&lt;/code&gt;. We don&amp;rsquo;t replace the
gRPC server from part 2; we add the HTTP one beside it, on the same controller,
both reading from one shared &lt;code&gt;store&lt;/code&gt;. This is the controller from part 1 doing
exactly what it exists for: two transports, one lifecycle.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;serve&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;context&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/controls&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/grpc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/http&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/props&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/gen/macguffin/v1&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/grpcsvc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/resthand&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ServeOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// One shared domain behind both transports.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// gRPC, from part 2.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;grpc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMacguffinServiceServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grpcsvc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// HTTP, new this part. (Or the oapi-codegen handler; either is an http.Handler.)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resthand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The HTTP &lt;code&gt;Register&lt;/code&gt; is the counterpart of part 2&amp;rsquo;s gRPC one: same controller, so
a single SIGTERM drains and stops both together and &lt;code&gt;/healthz&lt;/code&gt; reports on both.
The HTTP server reads &lt;code&gt;server.http.port&lt;/code&gt;, the gRPC server &lt;code&gt;server.grpc.port&lt;/code&gt; from
part 2, and both take their TLS from the shared &lt;code&gt;server.tls&lt;/code&gt; block, the one
certificate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50051&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8443&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;./localhost+2.pem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;./localhost+2-key.pem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="a-browser-away"&gt;A browser away
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where the mkcert groundwork from part 2 pays off. Because that
certificate is signed by a CA your machine already trusts, the service answers
over HTTPS with no &lt;code&gt;--cacert&lt;/code&gt; flag and, more to the point, no browser warning:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/macguffins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;m-1&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;maltese-falcon&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;:1&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/macguffins/m-1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;m-1&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;maltese-falcon&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;:1&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And &lt;code&gt;/healthz&lt;/code&gt; now reports both transports, since they registered against the one
controller:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/healthz
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;overall_healthy&amp;#34;&lt;/span&gt;:true,&lt;span class="s2"&gt;&amp;#34;services&amp;#34;&lt;/span&gt;:&lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;grpc&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;OK&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;http&amp;#34;&lt;/span&gt;,&lt;span class="s2"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;:&lt;span class="s2"&gt;&amp;#34;OK&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;}]}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Open &lt;code&gt;https://localhost:8443/macguffins&lt;/code&gt; in an actual browser and it just loads,
green padlock and all. That matters more than it sounds, and it&amp;rsquo;s the reason we
set the local CA up early: in part 5 the API docs are a web page, and a docs page
behind a cert warning is a docs page nobody trusts.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;Step back and count what we actually built. There&amp;rsquo;s one domain, the &lt;code&gt;Store&lt;/code&gt;, and
it didn&amp;rsquo;t change at all this part. What we added was a second delivery adapter
over it: routing, marshalling, and, on the spec-first road, a &lt;code&gt;toDTO&lt;/code&gt; mapping
sitting beside part 2&amp;rsquo;s &lt;code&gt;toProto&lt;/code&gt;. The same data, encoded into two generated
shapes, kept in step by hand. That&amp;rsquo;s the real duplication, not the logic, but the
transport scaffolding wrapped around it.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s the itch the next part scratches. We built a second transport and a
second encoding of the same domain, and kept them in step by hand. Part 4 is
where that whole second layer, adapter and encoding both, stops being something
you write at all. We felt the cost first on purpose; now we get to remove it.&lt;/p&gt;</description></item><item><title>The greybeards' edge was never typing</title><link>https://blog-570662.gitlab.io/the-greybeards-edge-was-never-typing/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-greybeards-edge-was-never-typing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-greybeards-edge-was-never-typing/cover-the-greybeards-edge-was-never-typing.png" alt="Featured image of post The greybeards' edge was never typing" /&gt;&lt;p&gt;I have a retirement plan, and it is gloriously low-tech. A cabin, some trees, a
woodstove, and a firm rule that no wifi symbol ever appears within a mile of me
again. I think about it more than is probably healthy.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a snag, though, and it&amp;rsquo;s the same one the whole industry is currently
pretending it can&amp;rsquo;t see. For me to vanish into the woods, somebody has to be
able to do my job after I&amp;rsquo;ve gone. And right now, collectively, we are working
very hard to make sure nobody can.&lt;/p&gt;
&lt;h2 id="the-boost-and-the-drag"&gt;The boost, and the drag
&lt;/h2&gt;&lt;p&gt;I wrote the other day about how AI made &lt;a class="link" href="https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/" &gt;&lt;em&gt;producing&lt;/em&gt; plausible work nearly free
while &lt;em&gt;verifying&lt;/em&gt; it stays expensive and human&lt;/a&gt;.
Point that same lens at a team and something uncomfortable falls out. It isn&amp;rsquo;t
mine; it belongs to Mark Russinovich and Scott Hanselman of Microsoft, who
&lt;a class="link" href="https://dl.acm.org/doi/10.1145/3779312" target="_blank" rel="noopener"
 &gt;laid it out in Communications of the ACM&lt;/a&gt;:
agentic coding tools give a senior engineer an &lt;em&gt;AI boost&lt;/em&gt;, multiplying what
they ship, because a senior has the judgement to steer and verify the output.
The same tools give an early-career engineer an &lt;em&gt;AI drag&lt;/em&gt;, because they don&amp;rsquo;t
have that judgement yet, and the machine hands them far more rope than they can
hold.&lt;/p&gt;
&lt;p&gt;The cold incentive writes itself, and they name it: hire seniors, automate
juniors. It isn&amp;rsquo;t hypothetical, either. Meta
&lt;a class="link" href="https://www.nytimes.com/2026/05/19/technology/meta-layoffs-ai.html" target="_blank" rel="noopener"
 &gt;cut 8,000 roles last week&lt;/a&gt;,
in a round the Times filed under mounting AI casualties. For any single quarter
you care to look at, the maths is impeccable.&lt;/p&gt;
&lt;h2 id="the-bill-is-just-deferred"&gt;The bill is just deferred
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the line the spreadsheet leaves off. The grindy, unglamorous work a
junior used to cut their teeth on, the small fixes, the boring migrations, the
read-the-stack-trace-and-figure-it-out, is exactly the work AI now does. So the
proving ground is gone. And the entry-level seats where they&amp;rsquo;d have stood on it
are the ones being cut. Squeezed from both ends at once: no reps, and nowhere
to take them.&lt;/p&gt;
&lt;p&gt;Russinovich and Hanselman put the consequence plainly. Without early-career
hiring the talent pipeline collapses, and you arrive at a future with no next
generation of experienced engineers. The seniors you&amp;rsquo;ll be desperate for in
2032 are the juniors you declined to train in 2026. The bill doesn&amp;rsquo;t vanish. It
just falls due long after the people who cut the cheque have moved on.&lt;/p&gt;
&lt;h2 id="how-to-manufacture-a-world-of-ai-slop"&gt;How to manufacture a world of AI slop
&lt;/h2&gt;&lt;p&gt;I named the last piece for its villain; let me name this one&amp;rsquo;s too. Raise a
generation that can &lt;em&gt;produce&lt;/em&gt; with AI but was never taught to &lt;em&gt;validate&lt;/em&gt;, and
here is what you get: people shipping machine-built products at speed with no
instinct for where the output is quietly wrong, because they never had to be
wrong the slow way first. Software nobody genuinely understands, human-written
and AI-written alike, and a steady leak of trust out of all of it.&lt;/p&gt;
&lt;p&gt;That isn&amp;rsquo;t a productivity problem. That&amp;rsquo;s a world of
&lt;a class="link" href="https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/" &gt;AI slop&lt;/a&gt;, and not
in one project&amp;rsquo;s inbox this time but everywhere at once. We&amp;rsquo;d have automated our
way clean out of the one job AI cannot do for us: knowing when not to trust the
machine.&lt;/p&gt;
&lt;h2 id="its-a-choice-and-its-yours"&gt;It&amp;rsquo;s a choice, and it&amp;rsquo;s yours
&lt;/h2&gt;&lt;p&gt;Andrew Murphy put it with more bite than I&amp;rsquo;d quite dare:
&lt;a class="link" href="https://andrewmurphy.io/blog/ai-didnt-kill-your-junior-pipeline-you-did" target="_blank" rel="noopener"
 &gt;AI didn&amp;rsquo;t kill your junior pipeline, you did&lt;/a&gt;.
He&amp;rsquo;s right. This isn&amp;rsquo;t weather. Nobody is making you do it. It&amp;rsquo;s a decision,
taken quarter by quarter, and a decision is a thing you can take differently.&lt;/p&gt;
&lt;p&gt;The fix isn&amp;rsquo;t complicated, it&amp;rsquo;s just unfashionable. Keep hiring early-career
engineers. Say out loud that they cost you capacity at first, and treat their
growth as an actual goal rather than something meant to happen by osmosis.
Russinovich and Hanselman call it preceptorship at scale: senior mentorship,
deliberately structured, turning the ordinary day&amp;rsquo;s work into teachable
moments.&lt;/p&gt;
&lt;p&gt;And the proving ground can be rebuilt, just not where it stood. If AI does the
writing now, the apprenticeship moves to the reviewing. Put juniors in the loop
on the machine&amp;rsquo;s output and have them hunt for the subtle wrongness, the way
&lt;a class="link" href="https://blog-570662.gitlab.io/the-security-finding-you-must-not-fix/" &gt;a scanner is an argument, not an order&lt;/a&gt;.
That&amp;rsquo;s how judgement gets built now: not by grinding out the work, but by
verifying it. Which, as luck would have it, is the single most valuable thing
anyone on your team can learn to do.&lt;/p&gt;
&lt;h2 id="the-part-thats-on-the-greybeards"&gt;The part that&amp;rsquo;s on the greybeards
&lt;/h2&gt;&lt;p&gt;This is where I stop letting the companies wear all the blame, because some of
it is mine, and yours. Verification is a craft, and crafts pass from person to
person or not at all. I know where every one of my own AI misfires comes from:
I gave it too little context, or too much rope, and didn&amp;rsquo;t check the result
closely enough. The tool rarely went rogue. The gap was always my diligence.
That&amp;rsquo;s not a confession, it&amp;rsquo;s the curriculum, and it&amp;rsquo;s precisely the judgement
a junior can only earn by sitting in the loop beside someone who has already
made those mistakes.&lt;/p&gt;
&lt;p&gt;So the senior engineer&amp;rsquo;s job has quietly changed underneath us. It was never
really the typing. It was knowing when something is off, and what the customer
actually needs, and now it is also &lt;em&gt;handing that on&lt;/em&gt;, deliberately, while
there&amp;rsquo;s still time to. Mentor and guardian first; fastest prompt in the room a
distant second.&lt;/p&gt;
&lt;h2 id="the-ladder-youre-standing-on"&gt;The ladder you&amp;rsquo;re standing on
&lt;/h2&gt;&lt;p&gt;There will always be something AI can&amp;rsquo;t do well enough, and for a good while
yet it&amp;rsquo;s the thing that matters most: being the accountable human who genuinely
understands what&amp;rsquo;s needed and can be held to it when it goes wrong. A simulation
can be enormously convincing. It cannot be &lt;em&gt;responsible&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Which brings me back to my cabin. I do want it one day, the trees and the
woodstove and the blissful disconnection. But I only get to go if the work
outlives me, and the work only outlives me if the people do. So the last useful
thing my generation does, before we shuffle off to find our trees, isn&amp;rsquo;t
shipping a little more code. It&amp;rsquo;s making sure there&amp;rsquo;s somebody left who can tell
when the machine is wrong. Pull the ladder up behind us and there&amp;rsquo;ll be nobody
to notice the rot, and no cabin quiet enough to make that sit right.&lt;/p&gt;</description></item><item><title>AI didn't kill curl's bug bounty. The bounty did.</title><link>https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/</guid><description>&lt;img src="https://blog-570662.gitlab.io/ai-didnt-kill-curls-bug-bounty/cover-ai-didnt-kill-curls-bug-bounty.png" alt="Featured image of post AI didn't kill curl's bug bounty. The bounty did." /&gt;&lt;p&gt;In January, Daniel Stenberg shut down curl&amp;rsquo;s bug bounty. The headlines wrote
themselves, and they all said the same thing: AI killed it. A flood of
machine-generated slop drowned the maintainers, so they pulled the plug.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s true, as far as it goes. It&amp;rsquo;s also the wrong lesson, and the right one
is sitting in plain sight in the same project, in the same few months.&lt;/p&gt;
&lt;h2 id="volume-without-validation-is-the-attack"&gt;Volume without validation is the attack
&lt;/h2&gt;&lt;p&gt;curl had run its bounty since April 2019. Over its life it paid out
&lt;a class="link" href="https://daniel.haxx.se/blog/2026/01/26/the-end-of-the-curl-bug-bounty/" target="_blank" rel="noopener"
 &gt;more than $100,000 for 87 genuine vulnerabilities&lt;/a&gt;,
a thoroughly good return for one of the most depended-on pieces of software on
the planet. Then the reports stopped being reports. The confirmation rate, the
share of submissions that turned out to be a real bug, had historically sat
north of 15%. By 2025 it was below 5%. Fewer than one in twenty submissions
were worth anything, and the rest still had to be read.&lt;/p&gt;
&lt;p&gt;That last part is the whole problem. A bogus report doesn&amp;rsquo;t announce itself.
Someone has to open it, take it seriously, try to reproduce it, and work out
that it&amp;rsquo;s nonsense, and that someone is a human being with a finite number of
hours and a project to run. Stenberg put it plainly: the slop &amp;ldquo;take[s] a
serious mental toll to manage and sometimes also a long time to debunk.&amp;rdquo; The
submitter spends seconds. The maintainer spends an afternoon. Do that at volume
and it stops being noise and becomes an attack, a denial-of-service aimed not
at curl&amp;rsquo;s servers but at its maintainers&amp;rsquo; attention. No exploit required. Just
plausibility, in bulk.&lt;/p&gt;
&lt;h2 id="the-bounty-was-the-accelerant-not-the-ai"&gt;The bounty was the accelerant, not the AI
&lt;/h2&gt;&lt;p&gt;So far this is the story everyone tells. Here&amp;rsquo;s where I get off the bus.&lt;/p&gt;
&lt;p&gt;The instinct is to blame the AI for the slop. But look at what a bounty actually
is. It&amp;rsquo;s a cash prize, and curl&amp;rsquo;s was priced for the thing it wanted: the hours
and the judgement a skilled human pours into finding a real flaw. That pricing
made complete sense right up until the cost of producing something that &lt;em&gt;looked
like&lt;/em&gt; a finding collapsed to nearly nothing.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what AI changed. Not the supply of bugs. The supply of plausible-looking
bug reports. Put a cash prize on &amp;ldquo;looks like a finding&amp;rdquo;, then make &amp;ldquo;looks like a
finding&amp;rdquo; free to generate, and you haven&amp;rsquo;t got a bug bounty any more. You&amp;rsquo;ve got
a slot machine. Stenberg said he&amp;rsquo;d started to sense &amp;ldquo;a bad faith attitude&amp;rdquo; in
the reports, and of course he had. The incentive was openly inviting it.&lt;/p&gt;
&lt;p&gt;So the death spiral was structural, not bad luck. The moment generating
plausible reports went free, any cash bounty became a magnet for spray-and-pray,
and the only open questions were how fast it would rot and whether you&amp;rsquo;d close
the programme or just let the rewards quietly wither. The AI was the match. The
bounty was the petrol. We have been pointing at the wrong one.&lt;/p&gt;
&lt;h2 id="the-proof-curl-turned-around-and-hired-the-ai"&gt;The proof: curl turned around and hired the AI
&lt;/h2&gt;&lt;p&gt;If AI were really the villain here, you&amp;rsquo;d expect curl to have slammed the door
on it. It did the opposite.&lt;/p&gt;
&lt;p&gt;In the same stretch, &lt;a class="link" href="https://aisle.com/blog/curl-adopts-aisle-after-its-ai-agents-discovered-5-cves" target="_blank" rel="noopener"
 &gt;by AISLE&amp;rsquo;s own account&lt;/a&gt;,
an AI security platform contributed 24 pull requests to curl, five of which
earned CVEs, and the project now runs it internally for continuous review. The
same tooling reportedly found &lt;a class="link" href="https://www.lesswrong.com/posts/7aJwgbMEiKq5egQbd/" target="_blank" rel="noopener"
 &gt;all twelve zero-days&lt;/a&gt;
in an OpenSSL release in late January. (Both of those are the tool-makers&amp;rsquo; and a
third party&amp;rsquo;s numbers rather than curl&amp;rsquo;s audited figures, so weigh them as such.
But curl adopting the thing isn&amp;rsquo;t a claim. It&amp;rsquo;s a decision.)&lt;/p&gt;
&lt;p&gt;Sit with the shape of that. curl shut down strangers being paid for AI-shaped
noise, and in the same breath put AI to work as a tool its own maintainers
drive. The two moves look contradictory only if you think &amp;ldquo;AI&amp;rdquo; is a single thing
with a single verdict attached. It isn&amp;rsquo;t. Pointed at the problem by people
accountable for the result, with no prize to farm, it found real bugs. Dangled
in front of anonymous strangers chasing a payout, it produced sand.&lt;/p&gt;
&lt;h2 id="the-tell-is-which-ai-curl-kept-and-which-it-mocked"&gt;The tell is which AI curl kept, and which it mocked
&lt;/h2&gt;&lt;p&gt;Stenberg drew that line about as sharply as a person can. When Anthropic put its
security model, Mythos, in front of curl this spring, it
&lt;a class="link" href="https://daniel.haxx.se/blog/2026/05/11/mythos-finds-a-curl-vulnerability/" target="_blank" rel="noopener"
 &gt;scanned 176,000 lines of C and surfaced a single flaw&lt;/a&gt;,
and Stenberg called the surrounding fanfare
&lt;a class="link" href="https://www.theregister.com/security/2026/05/11/anthropics-bug-hunting-mythos-was-greatest-marketing-stunt-ever-says-curl-creator/5238111" target="_blank" rel="noopener"
 &gt;the greatest marketing stunt he&amp;rsquo;d seen&lt;/a&gt;.
Same maintainer. Adopts one AI, rubbishes another.&lt;/p&gt;
&lt;p&gt;The deciding factor was never whether the thing was AI. Both were. It was
whether the output survived a human checking it, and whether you could check it
at all. AISLE handed over pull requests and CVEs you could read and merge.
Mythos arrived as a closed model and a press release, which is to say a claim
the community has no way to independently test.&lt;/p&gt;
&lt;p&gt;My bias, up front, because it runs the opposite way to what you&amp;rsquo;d expect from
someone writing this: I&amp;rsquo;m a paying Claude subscriber and I lean on Anthropic&amp;rsquo;s
models every working day, the one behind the spadework for this post included.
I&amp;rsquo;m an advocate, not a sceptic, and AI genuinely has its place. That is
&lt;em&gt;exactly&lt;/em&gt; why the Mythos fanfare grates. Overselling a closed model to get out
ahead of the competition, when the one test the public got to see turned up a
single bug, is the sort of thing that chips away at trust in all of it. A result
you can&amp;rsquo;t verify is marketing until proven otherwise, whoever&amp;rsquo;s logo is on the
slide, and I&amp;rsquo;d rather the tools I depend on didn&amp;rsquo;t stoop to it.&lt;/p&gt;
&lt;h2 id="the-cheap-half-and-the-expensive-half"&gt;The cheap half and the expensive half
&lt;/h2&gt;&lt;p&gt;Pull back from curl for a moment, because the lesson isn&amp;rsquo;t really about bounties
at all. Anyone who works with these tools every day knows the same thing: when
they go wrong, it&amp;rsquo;s rarely the model running off on its own. It&amp;rsquo;s the context it
wasn&amp;rsquo;t given, the rope it was handed, the output nobody checked closely enough.
The failure sits on the human side of the keyboard, at the one step that&amp;rsquo;s
easiest to skip, which is verification.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the pattern curl hit at the scale of an ecosystem. AI made one thing
nearly free: producing work that looks right. It did not make the other thing a
penny cheaper: confirming the work &lt;em&gt;is&lt;/em&gt; right. That cost still falls, in full,
on a person. (A scanner, &lt;a class="link" href="https://blog-570662.gitlab.io/the-security-finding-you-must-not-fix/" &gt;I&amp;rsquo;ve argued before&lt;/a&gt;,
is an argument, not an order; the same goes double for a model.) The bounty&amp;rsquo;s
fatal mistake was paying for the cheap half and quietly assuming it had bought
the expensive one. The same trap waits in code review, in hiring, in CVs read by
machines, but that&amp;rsquo;s a bigger argument for another post.&lt;/p&gt;
&lt;h2 id="pouring-sand-into-the-machine"&gt;Pouring sand into the machine
&lt;/h2&gt;&lt;p&gt;curl didn&amp;rsquo;t capitulate to AI, whatever the headlines decided. It stopped paying
for the worthless half and started using the valuable half, and it had the
discernment to tell a useful tool from a press release while it did so.&lt;/p&gt;
&lt;p&gt;The bounty wasn&amp;rsquo;t a casualty of artificial intelligence. It was a structure
that, the instant plausible output became free, could only fill with sand.
Stenberg said he hopes closing it stops &amp;ldquo;more people pouring sand into the
machine.&amp;rdquo; Reading the last year of his inbox, I think he&amp;rsquo;ll get his wish. The
sand was only ever there because somebody left a bucket of money beside the
funnel.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 2: a gRPC service, with TLS</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-2/</link><pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-2/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-2/cover-building-a-web-service-with-go-tool-base-part-2.png" alt="Featured image of post Building a web service with go-tool-base, part 2: a gRPC service, with TLS" /&gt;&lt;p&gt;The heartbeat from &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-1/" &gt;part
1&lt;/a&gt;
runs, ticks along, and shuts down politely when you ask it to. It also talks to
absolutely no one. A service people can actually call needs an API, and for a
typed, fast, streaming-capable one, gRPC is the obvious first move.&lt;/p&gt;
&lt;p&gt;The catch is that a production-grade gRPC server is rather more than
&lt;code&gt;grpc.NewServer()&lt;/code&gt;. You want health checks an orchestrator understands,
reflection so you can poke at it without the &lt;code&gt;.proto&lt;/code&gt; file in hand, a graceful
shutdown that doesn&amp;rsquo;t guillotine calls that are still in flight, and TLS, which
is where most people&amp;rsquo;s first attempt quietly goes wrong. The good news: part 1
already gave us the thing that carries all of it. A gRPC server is just another
service to register on the controller.&lt;/p&gt;
&lt;h2 id="why-grpc-and-not-just-rest"&gt;Why gRPC, and not just REST
&lt;/h2&gt;&lt;p&gt;Worth a moment on why we&amp;rsquo;re reaching for gRPC at all, because for plenty of
services a plain JSON-over-HTTP API is the right call and less faff. gRPC earns
its place when a few of these matter:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A contract that&amp;rsquo;s enforced, not hoped for.&lt;/strong&gt; The &lt;code&gt;.proto&lt;/code&gt; is the single
source of truth, and both ends are generated from it. You don&amp;rsquo;t hand-write
JSON marshalling, and you don&amp;rsquo;t find out at runtime that the client and server
disagree about a field&amp;rsquo;s type. Evolve the schema carefully (add fields by
number) and old clients keep working.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clients in any language, for free.&lt;/strong&gt; The same &lt;code&gt;.proto&lt;/code&gt; generates a Go server
and a Python, TypeScript, Rust or Java client with nobody writing an SDK by
hand. For an internal service that several teams call, that one point can
decide it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s built for service-to-service traffic.&lt;/strong&gt; Binary protobuf is smaller and
quicker to encode than JSON, calls multiplex down a single HTTP/2 connection,
and streaming (from the client, the server, or both at once) is a first-class
thing rather than something you bolt onto REST with websockets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deadlines, cancellation and a health protocol&lt;/strong&gt; come built in, rather than
conventions you reinvent for every service.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trade-offs are real. A browser doesn&amp;rsquo;t speak gRPC natively, and a binary
protocol is fiddlier to poke at than a JSON endpoint you can curl (which is
exactly why reflection and &lt;code&gt;grpcurl&lt;/code&gt; exist). That&amp;rsquo;s not a reason to avoid it;
it&amp;rsquo;s the reason this series doesn&amp;rsquo;t &lt;em&gt;stop&lt;/em&gt; at gRPC. In part 4 we put a REST/JSON
face on this very service, so the things that call it get the typed, fast core
and the things that can&amp;rsquo;t speak gRPC still get a friendly HTTP surface. You don&amp;rsquo;t
have to pick a side.&lt;/p&gt;
&lt;h2 id="define-the-contract"&gt;Define the contract
&lt;/h2&gt;&lt;p&gt;gRPC starts with a schema. Here&amp;rsquo;s a small macguffin service, &lt;code&gt;macguffin.proto&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// proto/macguffin/v1/macguffin.proto
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;syntax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;proto3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;macguffin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="n"&gt;go_package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/gen/macguffin/v1;macguffinv1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;service&lt;/span&gt; &lt;span class="n"&gt;MacguffinService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;GetMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;ListMacguffins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ListMacguffinsRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ListMacguffinsResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;rpc&lt;/span&gt; &lt;span class="n"&gt;CreateMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;returns&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Macguffin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;GetMacguffinRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;ListMacguffinsRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;page_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;ListMacguffinsResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;Macguffin&lt;/span&gt; &lt;span class="n"&gt;macguffins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;CreateMacguffinRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="from-proto-to-go"&gt;From proto to Go
&lt;/h3&gt;&lt;p&gt;If gRPC in Go is new to you, this is the part that catches people out: you don&amp;rsquo;t
write the Go for those messages and that service interface, you &lt;em&gt;generate&lt;/em&gt; it
from the &lt;code&gt;.proto&lt;/code&gt;. The proto is the source of truth; a compiler turns it into Go
you import and build against. Same goes for a client in any other language, all
from the same file.&lt;/p&gt;
&lt;p&gt;That compiler is &lt;code&gt;protoc&lt;/code&gt;, and on its own it&amp;rsquo;s a faff. You install it, then a
separate plugin for each output you want (&lt;code&gt;protoc-gen-go&lt;/code&gt; for the message types,
&lt;code&gt;protoc-gen-go-grpc&lt;/code&gt; for the client and server stubs), keep their versions in
step, and drive the lot with a command line of &lt;code&gt;-I&lt;/code&gt; include paths and &lt;code&gt;--*_out&lt;/code&gt;
flags that&amp;rsquo;s easy to get subtly wrong.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://buf.build/docs" target="_blank" rel="noopener"
 &gt;buf&lt;/a&gt; is the friendlier way to run exactly that. It wraps
protoc and its plugins behind a couple of small config files, handles the plugin
versions, and turns that gnarly invocation into a single word. It&amp;rsquo;s become the
usual way to work with protobuf in Go, and it&amp;rsquo;s what we&amp;rsquo;ll use here.&lt;/p&gt;
&lt;p&gt;At a minimum you need three binaries on your &lt;code&gt;PATH&lt;/code&gt;: buf itself, and the two
plugins it drives. &lt;code&gt;go install&lt;/code&gt; is the quickest way to get them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go install github.com/bufbuild/buf/cmd/buf@latest
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those land in &lt;code&gt;$(go env GOPATH)/bin&lt;/code&gt;, so make sure that&amp;rsquo;s on your &lt;code&gt;$PATH&lt;/code&gt;. Then
describe what you want generated in a &lt;code&gt;buf.gen.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# buf.gen.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;protoc-gen-go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/gen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;paths=source_relative&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;protoc-gen-go-grpc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal/gen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;paths=source_relative&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each part of that earns its place. &lt;code&gt;version: v2&lt;/code&gt; is buf&amp;rsquo;s config format. The
&lt;code&gt;plugins&lt;/code&gt; list names the generators to run, and we run two, because gRPC in Go
arrives in two halves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;protoc-gen-go&lt;/code&gt;&lt;/strong&gt; turns the messages into Go structs, the &lt;code&gt;Macguffin&lt;/code&gt; type
and the request and response types, in a &lt;code&gt;macguffin.pb.go&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;protoc-gen-go-grpc&lt;/code&gt;&lt;/strong&gt; turns the &lt;code&gt;service&lt;/code&gt; into the client and server
scaffolding, in a &lt;code&gt;macguffin_grpc.pb.go&lt;/code&gt; file, including the
&lt;code&gt;MacguffinServiceServer&lt;/code&gt; interface you&amp;rsquo;re about to implement.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;out: internal/gen&lt;/code&gt; is where the files land, and &lt;code&gt;paths=source_relative&lt;/code&gt; lays
them out mirroring the proto&amp;rsquo;s own folders (so &lt;code&gt;proto/macguffin/v1/...&lt;/code&gt; becomes
&lt;code&gt;internal/gen/macguffin/v1/...&lt;/code&gt;) rather than deriving the path from the
&lt;code&gt;go_package&lt;/code&gt; line. Then run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;buf generate
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both files appear under &lt;code&gt;internal/gen/macguffin/v1&lt;/code&gt;, and we&amp;rsquo;re ready to write the
implementation.&lt;/p&gt;
&lt;p&gt;Running that by hand once is fine; remembering to run it every time the &lt;code&gt;.proto&lt;/code&gt;
changes is where it goes wrong, and the generated code quietly drifts out of
step, usually right before a demo. Wire it into &lt;code&gt;go generate&lt;/code&gt; instead. Drop a
one-line directive in a file at your module root, say &lt;code&gt;gen.go&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// gen.go (at your module root)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffinsvc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:generate buf generate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now &lt;code&gt;go generate ./...&lt;/code&gt; regenerates everything from the proto, and it&amp;rsquo;s the same
one command for any other generator you add later. Run it whenever the schema
changes, and in CI if you want to catch a stale checkout.&lt;/p&gt;
&lt;h3 id="if-openapi-is-your-map-of-the-territory"&gt;If OpenAPI is your map of the territory
&lt;/h3&gt;&lt;p&gt;If your mental model of an API contract is an OpenAPI (Swagger) document, a
&lt;code&gt;.proto&lt;/code&gt; is the same idea wearing fewer clothes: a typed, language-neutral
description of a service that both ends generate from. The thing you notice
first is how much less of it there is. Here&amp;rsquo;s that &lt;code&gt;Macguffin&lt;/code&gt; message again, and
the same shape written as an OpenAPI schema:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-protobuf" data-lang="protobuf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Macguffin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;object&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;integer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;int32&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that pattern holds across the whole service. The proto above, three calls
and five messages, is about twenty lines. Describe the same surface in OpenAPI
and you&amp;rsquo;re closer to a hundred, because OpenAPI also pins down the HTTP verbs,
paths, status codes and content types: the transport details a proto leaves
out on purpose. That isn&amp;rsquo;t OpenAPI being bloated; it&amp;rsquo;s describing more. But when
the contract is the thing you care about, the proto says it with less
ceremony, and it doesn&amp;rsquo;t wed your API to HTTP, which is exactly what lets us
serve this same service over gRPC now and REST later. (We&amp;rsquo;ll generate a real
OpenAPI document from this proto in part 5, for the readers who still want one.)&lt;/p&gt;
&lt;h2 id="implement-it"&gt;Implement it
&lt;/h2&gt;&lt;p&gt;Generating the code gave you the message types and, more to the point, an
interface to satisfy. Open &lt;code&gt;macguffin_grpc.pb.go&lt;/code&gt; and you&amp;rsquo;ll find
&lt;code&gt;MacguffinServiceServer&lt;/code&gt;, one method per RPC in the proto:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/gen/macguffin/v1/macguffin_grpc.pb.go (generated)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MacguffinServiceServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;GetMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GetMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;ListMacguffins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ListMacguffinsRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ListMacguffinsResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;CreateMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;CreateMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nf"&gt;mustEmbedUnimplementedMacguffinServiceServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That interface is the server-side contract. Each method takes the request
message you defined and hands back the response message, plus an error. Writing
the type that honours it, the actual logic behind each call, is the part that&amp;rsquo;s
yours: the proto pins down the shape of the conversation, and this is what the
service actually does when one happens.&lt;/p&gt;
&lt;p&gt;The one curious line is &lt;code&gt;mustEmbedUnimplementedMacguffinServiceServer()&lt;/code&gt;.
Alongside the interface, buf generated an &lt;code&gt;UnimplementedMacguffinServiceServer&lt;/code&gt;
struct with a do-nothing stub for every method, and you embed it in your own
type. It earns its keep twice over. It satisfies that unexported method, so your
type counts as a real implementation. And it future-proofs you: add a new RPC to
the proto later and your existing server still compiles, falling back to the
stub (which returns a clean &amp;ldquo;unimplemented&amp;rdquo; error) until you write the real
method.&lt;/p&gt;
&lt;p&gt;Before we satisfy that interface, one separation worth making up front. The gRPC
server is a &lt;em&gt;delivery mechanism&lt;/em&gt;, not the place the data lives. If we stuff the
map of macguffins straight inside it and then build an HTTP server next part,
we&amp;rsquo;d have two servers each hoarding their own copy of the same data. So keep the
domain, the macguffins and what you can do with them, in its own type, and let
each transport be a thin layer over it.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s that domain: an in-memory store standing in for the repository a real
service would have. Nothing in it knows about gRPC, HTTP, or any wire format.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/macguffin/store.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sync&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Macguffin is the domain type. The JSON tags will let a hand-written HTTP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// handler serve it directly in part 3.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;id&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;name&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;quantity&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;mu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mutex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;seq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;List&lt;/code&gt; and &lt;code&gt;Create&lt;/code&gt; are the same shape, and &lt;code&gt;NewStore&lt;/code&gt; seeds it with a single
&lt;code&gt;maltese-falcon&lt;/code&gt;. Now the gRPC server is thin: it embeds the stub, holds a store,
and each method calls the store and translates the result into the generated
protobuf type.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/grpcsvc/server.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grpcsvc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;context&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;google.golang.org/grpc/codes&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;google.golang.org/grpc/status&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/gen/macguffin/v1&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UnimplementedMacguffinServiceServer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;GetMacguffin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GetMacguffinRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin %q not found&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetId&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;toProto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// toProto maps the domain type to the generated protobuf DTO.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;toProto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Quantity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ListMacguffins&lt;/code&gt; and &lt;code&gt;CreateMacguffin&lt;/code&gt; are the same: call the store, map the
result. The one habit worth keeping is to return real gRPC status codes
(&lt;code&gt;codes.NotFound&lt;/code&gt; here) rather than bare errors, so callers get something they
can branch on.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;toProto&lt;/code&gt; step is worth a second look, because it comes back round later.
The domain has one shape, the proto has its own generated &lt;code&gt;Macguffin&lt;/code&gt;, so the
adapter maps between them. It&amp;rsquo;s a small price for a single transport. In part 3
we add a second transport with its own generated type and pay that price again,
and part 4 is where we stop paying it twice.&lt;/p&gt;
&lt;h2 id="wire-it-onto-the-controller"&gt;Wire it onto the controller
&lt;/h2&gt;&lt;p&gt;This is the part that earns its keep. First, generate a &lt;code&gt;serve&lt;/code&gt; command, the
same way the &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/" &gt;CLI
series&lt;/a&gt;
generated its commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --name serve &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --short &lt;span class="s2"&gt;&amp;#34;Run the macguffin service&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That scaffolds two files: &lt;code&gt;pkg/cmd/serve/cmd.go&lt;/code&gt; (generated, and wired into your
command tree for you) and &lt;code&gt;pkg/cmd/serve/main.go&lt;/code&gt;, which holds a &lt;code&gt;RunServe&lt;/code&gt;
function for your logic. Fill it in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;serve&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;context&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/controls&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/grpc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/props&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/gen/macguffin/v1&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/grpcsvc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/myorg/macguffinsvc/internal/macguffin&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;ServeOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;macguffin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;grpc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;macguffinv1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterMacguffinServiceServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grpcsvc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the whole server. &lt;code&gt;gtbgrpc.Register&lt;/code&gt; does four things in one call: it
builds a &lt;code&gt;*grpc.Server&lt;/code&gt;, wires the standard gRPC health service to the
controller&amp;rsquo;s health reports (the ones we met in part 1), registers &lt;code&gt;Start&lt;/code&gt;,
&lt;code&gt;Stop&lt;/code&gt; and &lt;code&gt;Status&lt;/code&gt; against the controller so the lifecycle is handled, and
hands you back the server to register your own service on, which is the
&lt;code&gt;RegisterMacguffinServiceServer&lt;/code&gt; line. After that it&amp;rsquo;s the same
&lt;code&gt;Start()&lt;/code&gt; / &lt;code&gt;Wait()&lt;/code&gt; we used for the heartbeat.&lt;/p&gt;
&lt;p&gt;It reads its port from config (&lt;code&gt;server.grpc.port&lt;/code&gt;, falling back to
&lt;code&gt;server.port&lt;/code&gt;), so a minimal config is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50051&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="poke-it"&gt;Poke it
&lt;/h2&gt;&lt;p&gt;Build, run &lt;code&gt;mytool serve&lt;/code&gt;, and reach for &lt;code&gt;grpcurl&lt;/code&gt;. Reflection is on, so you
don&amp;rsquo;t need the &lt;code&gt;.proto&lt;/code&gt; to hand:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ grpcurl -plaintext localhost:50051 list
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grpc.health.v1.Health
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grpc.reflection.v1.ServerReflection
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grpc.reflection.v1alpha.ServerReflection
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;macguffin.v1.MacguffinService
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ grpcurl -plaintext localhost:50051 macguffin.v1.MacguffinService/ListMacguffins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;macguffins&amp;#34;&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;m-1&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;maltese-falcon&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;: &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the health service is already answering, wired straight to the controller,
without you registering a thing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;SERVING&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the lifecycle work from part 1 paying out: the controller&amp;rsquo;s health is the
gRPC health, and a SIGTERM still drains and stops the server cleanly.&lt;/p&gt;
&lt;h2 id="now-turn-on-tls"&gt;Now turn on TLS
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the bit people brace for. Plaintext gRPC is fine on a laptop and
unacceptable the moment it leaves one. With go-tool-base it&amp;rsquo;s a config change,
not a code change.&lt;/p&gt;
&lt;p&gt;The fiddly part of local TLS is usually the certificate. A hand-rolled
self-signed one means passing a &lt;code&gt;-cacert&lt;/code&gt; to every client and clicking past
browser warnings. &lt;a class="link" href="https://github.com/FiloSottile/mkcert" target="_blank" rel="noopener"
 &gt;mkcert&lt;/a&gt; makes that go
away: it creates a local certificate authority and installs it into your
system&amp;rsquo;s (and your browser&amp;rsquo;s) trust stores, so anything it signs is simply
trusted. Set the CA up once:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkcert -install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then mint a certificate for the names the service answers on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkcert localhost 127.0.0.1 ::1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That writes &lt;code&gt;localhost+2.pem&lt;/code&gt; (the certificate) and &lt;code&gt;localhost+2-key.pem&lt;/code&gt; (the
key), signed by your now-trusted local CA. Doing this properly now pays off
later: in part 3 the HTTP server, and in part 5 the API docs in a browser, both
lean on that certificate being trusted with no warning.&lt;/p&gt;
&lt;p&gt;Point the tool&amp;rsquo;s config at the pair, under the shared &lt;code&gt;server.tls&lt;/code&gt; block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;grpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50051&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;reflection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;./localhost+2.pem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;./localhost+2-key.pem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No code changes. Run &lt;code&gt;mytool serve&lt;/code&gt; again and it comes up over TLS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO starting gRPC server tls=true addr=:50051
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because the certificate is signed by a CA your machine already trusts, the
client needs no extra flags:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ grpcurl localhost:50051 macguffin.v1.MacguffinService/GetMacguffin -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;id&amp;#34;:&amp;#34;m-1&amp;#34;}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;m-1&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span class="s2"&gt;&amp;#34;maltese-falcon&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;: &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A plaintext client is now refused, as it should be. (In production you&amp;rsquo;d point
those same two config keys at whatever your real CA issues; the wiring doesn&amp;rsquo;t
change.) Two details are worth knowing about what just happened, because both
are easy to get wrong by hand.
The server uses a hardened TLS config (1.2 minimum, AEAD cipher suites, X25519),
so you&amp;rsquo;re not accidentally shipping a weak handshake. And the listener
advertises HTTP/2 over ALPN, the &lt;code&gt;h2&lt;/code&gt; protocol gRPC rides on, which sounds like
a footnote until you discover that recent gRPC clients refuse a TLS connection
that doesn&amp;rsquo;t offer it. The framework sets it for you; it&amp;rsquo;s the single most
common reason a hand-rolled gRPC-over-TLS server works with old tooling and
mysteriously rejects a current client. All of that lives in the shared
&lt;a class="link" href="https://gtb.phpboyscout.uk/components/tls/" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/tls&lt;/code&gt;&lt;/a&gt; package.&lt;/p&gt;
&lt;p&gt;I put the certificate under &lt;code&gt;server.tls&lt;/code&gt; rather than &lt;code&gt;server.grpc.tls&lt;/code&gt;
deliberately. That shared block is the cert every transport falls back to, so
the HTTP server in the next part and the transports after it can use the same
one, with a per-transport override only where you actually need it.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;A few files in, you have a real gRPC API: a typed contract, an implementation,
health an orchestrator understands, reflection for poking, a clean shutdown, and
TLS, and the only part that was actually yours to write was the service logic.
The reference for the server helpers is the &lt;a class="link" href="https://gtb.phpboyscout.uk/components/grpc/" target="_blank" rel="noopener"
 &gt;gRPC
component&lt;/a&gt; doc, and the
&lt;a class="link" href="https://gtb.phpboyscout.uk/how-to/add-grpc-service/" target="_blank" rel="noopener"
 &gt;add-a-gRPC-service how-to&lt;/a&gt;
has the manual-wiring path if you ever want it.&lt;/p&gt;
&lt;p&gt;Next part puts an HTTP face on the very same controller, REST handlers and the
same health endpoints an orchestrator probes, sharing that one certificate.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 1: lifecycle and graceful shutdown</title><link>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-1/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-1/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-web-service-with-go-tool-base-part-1/cover-building-a-web-service-with-go-tool-base-part-1.png" alt="Featured image of post Building a web service with go-tool-base, part 1: lifecycle and graceful shutdown" /&gt;&lt;p&gt;A CLI does its job and gets out of the way. You run it, it prints something or
writes a file, the process exits, done. Then one day you want the opposite: a
thing that &lt;em&gt;stays&lt;/em&gt; running. A server answering requests, a worker chewing
through a queue, something that sits there until you tell it to stop. And the
moment a process is long-lived, a pile of unglamorous problems lands on your
desk that a short-lived command never had to think about.&lt;/p&gt;
&lt;p&gt;How does it shut down when Kubernetes sends it a SIGTERM, without dropping the
requests it&amp;rsquo;s halfway through? How does anything outside it know it&amp;rsquo;s alive, or
ready for traffic? When one part falls over at 3am, does the whole thing come
down, or pick itself back up? None of that is your actual service. It&amp;rsquo;s the
plumbing around it, and it&amp;rsquo;s the sort of plumbing that&amp;rsquo;s easy to write &lt;em&gt;almost&lt;/em&gt;
right and only notice the gap in during an incident.&lt;/p&gt;
&lt;p&gt;This is a new series, a companion to the &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/" &gt;one on building a
CLI&lt;/a&gt;.
That series gets you a working command-line tool; this one turns it into a web
service, a piece at a time: gRPC, HTTP, a gateway that bridges the two, TLS
across all of them, and live API docs. But every one of those is a long-running
process, so we start with the part they all stand on.&lt;/p&gt;
&lt;p&gt;Same shape as before, each part stands alone. By the end of this one you&amp;rsquo;ll have
a process that starts cleanly, reports its own health, and shuts down without
dropping anything, and you won&amp;rsquo;t have written the lifecycle code yourself. The
series is written against &lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt;, the release that brings the
web-service components in.&lt;/p&gt;
&lt;h2 id="what-every-long-running-process-needs"&gt;What every long-running process needs
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s answer to all of the above is the &lt;code&gt;controls&lt;/code&gt; package, and its
centrepiece is the &lt;code&gt;Controller&lt;/code&gt;. You hand it a set of &lt;em&gt;services&lt;/em&gt;, things with a
way to start, a way to stop, and a way to report health, and it owns their
lifecycle. It starts them, watches for the operating system asking the process
to quit, drives a graceful shutdown in the right order, and keeps a running
picture of whether everything is alright.&lt;/p&gt;
&lt;p&gt;A &amp;ldquo;service&amp;rdquo; here is deliberately loose. An HTTP server is one. A gRPC server is
one. So is a background worker that wakes every few seconds, or a queue
consumer. The controller doesn&amp;rsquo;t care what&amp;rsquo;s inside; it cares that it can start
it, stop it, and ask after its health. That&amp;rsquo;s the whole trick: get those three
verbs right once, in one place, and everything you bolt on later inherits them.&lt;/p&gt;
&lt;h2 id="a-service-in-thirty-lines"&gt;A service in thirty lines
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s build the smallest useful one: a heartbeat that logs a tick every second.
It isn&amp;rsquo;t exciting, but it&amp;rsquo;s a real long-running service, and it shows every
moving part without a transport getting in the way.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;context&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;os&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sync/atomic&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/controls&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/logger&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewCharm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// The controller owns the process lifecycle: it starts registered services,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// watches for SIGINT/SIGTERM, and drives a graceful shutdown.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;beats&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;atomic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Int64&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;heartbeat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// Start launches the work. The context is cancelled when the controller&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// shuts down, so the goroutine just watches ctx.Done().&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewTicker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;					&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;					&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;						&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;					&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;						&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;beat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;count&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;beats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;					&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// Stop runs during shutdown for any explicit cleanup.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithStop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;heartbeat stopping&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;total_beats&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;beats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// Status reports health. Here we&amp;#39;re healthy as long as we&amp;#39;re ticking.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A few things are earning their keep there. &lt;code&gt;NewController&lt;/code&gt; takes a context and
some options, here just a logger. &lt;code&gt;Register&lt;/code&gt; names a service and gives it its
three verbs through functional options: &lt;code&gt;WithStart&lt;/code&gt; launches it (and is handed a
context that gets cancelled when the controller shuts down, which is the hook
the goroutine watches), &lt;code&gt;WithStop&lt;/code&gt; is called during shutdown for cleanup, and
&lt;code&gt;WithStatus&lt;/code&gt; answers &amp;ldquo;are you alright?&amp;rdquo;. Then &lt;code&gt;controller.Start()&lt;/code&gt; launches
everything and &lt;code&gt;controller.Wait()&lt;/code&gt; blocks until the whole thing has stopped.
The three options are all in
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/7d65f86/pkg/controls/controls.go#L103-L139" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/controls&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Build it and run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;go run .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO beat count=1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO beat count=2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO beat count=3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A service, running. Now for the half that&amp;rsquo;s easy to get wrong.&lt;/p&gt;
&lt;h2 id="shutting-down-on-purpose"&gt;Shutting down on purpose
&lt;/h2&gt;&lt;p&gt;Press Ctrl-C, or send the process a SIGTERM the way an orchestrator would, and
watch what it does:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WARN [Controller] : received signal signal=terminated
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WARN [Controller] : Stopping Services
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO heartbeat stopping total_beats=3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO [Controller] : Stopped
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing in our thirty lines handled a signal. The controller registered its own
handlers for SIGINT and SIGTERM, and when one arrived it cancelled the context
that every service&amp;rsquo;s &lt;code&gt;Start&lt;/code&gt; is watching, gave them a window to finish, ran each
&lt;code&gt;Stop&lt;/code&gt;, and exited cleanly. That cancel-the-context-then-&lt;code&gt;Stop&lt;/code&gt; order is the
thing: it&amp;rsquo;s exactly what stops an HTTP server from dropping requests it&amp;rsquo;s
mid-way through when the pod rolls. We&amp;rsquo;ve got it here for a heartbeat that does
nothing on the way out, and we&amp;rsquo;ll get the same order for free for every real
transport we add later.&lt;/p&gt;
&lt;p&gt;You can tune the window with &lt;code&gt;WithShutdownTimeout&lt;/code&gt;, and turn the signal handling
off entirely (handy in tests) with &lt;code&gt;WithoutSignals&lt;/code&gt;. The defaults are the right
ones for a service in a container.&lt;/p&gt;
&lt;h2 id="health-before-anythings-asking"&gt;Health, before anything&amp;rsquo;s asking
&lt;/h2&gt;&lt;p&gt;That third verb, &lt;code&gt;WithStatus&lt;/code&gt;, is the start of the health story, and it&amp;rsquo;s worth
seeing now even though nothing&amp;rsquo;s calling it yet. The controller can report three
separate things: overall status, liveness, and readiness. Those aren&amp;rsquo;t the same
question. Liveness is &amp;ldquo;is this process wedged and in need of a restart&amp;rdquo;;
readiness is &amp;ldquo;should traffic come here yet&amp;rdquo;. An orchestrator uses them
differently, which is why they&amp;rsquo;re kept apart, and a service can answer them
separately by adding &lt;code&gt;WithLiveness&lt;/code&gt; and &lt;code&gt;WithReadiness&lt;/code&gt; alongside &lt;code&gt;WithStatus&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Right now nothing asks, because we&amp;rsquo;ve no transport. But this is the quiet payoff
of putting lifecycle first: when we add an HTTP server in part 3, these reports
are what back its &lt;code&gt;/healthz&lt;/code&gt;, &lt;code&gt;/livez&lt;/code&gt; and &lt;code&gt;/readyz&lt;/code&gt; endpoints, and when we add
gRPC in part 2 they back the standard gRPC health service, with no re-plumbing on
our side. The controller also carries a restart policy for services that should
pick themselves back up, and standalone health checks for things like &amp;ldquo;can I
still reach the database&amp;rdquo;, but those earn their place once we&amp;rsquo;ve something worth
checking.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;A few lines in, we&amp;rsquo;ve a process that starts, ticks along, answers for its own
health, and stops cleanly when the platform asks it to, on a controller that the
real transports will register against unchanged. The heartbeat is a stand-in.
Next part we swap it for a proper gRPC service, give it TLS, and the controller
barely notices the difference, which is the entire point of it.&lt;/p&gt;
&lt;p&gt;If you want to read ahead, the &lt;a class="link" href="https://gtb.phpboyscout.uk/components/controls/" target="_blank" rel="noopener"
 &gt;controls
component&lt;/a&gt; has the full
interface, and the &lt;a class="link" href="https://gtb.phpboyscout.uk/concepts/service-orchestration/" target="_blank" rel="noopener"
 &gt;service-orchestration
deep-dive&lt;/a&gt; covers how
the startup ordering and shutdown actually work underneath.&lt;/p&gt;</description></item><item><title>Building a CLI with go-tool-base, part 5: a CLI that updates itself</title><link>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-5/</link><pubDate>Sun, 24 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-5/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-5/cover-building-a-cli-with-go-tool-base-part-5.png" alt="Featured image of post Building a CLI with go-tool-base, part 5: a CLI that updates itself" /&gt;&lt;p&gt;You ship version one. A week later someone finds a bug, you fix it, you cut version
two. Now for the awkward part: how does the person who installed version one ever
get version two? Email them? Hope they wander back to the install page? For a CLI
that lives on people&amp;rsquo;s machines, &amp;ldquo;go and re-download it&amp;rdquo; is the answer that quietly
strands half your users on old, broken builds. This part closes that gap, and like
most of this series, the work is already done for you: your tool has shipped with an
&lt;code&gt;update&lt;/code&gt; command since &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/" &gt;part 1&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As before, this is written against &lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt; (&lt;code&gt;gtb version&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="the-command-is-already-there"&gt;The command is already there
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;update&lt;/code&gt; is one of the default features, so it&amp;rsquo;s been in your binary all along.
Your users run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the tool fetches the newest release, checks it, and replaces itself in place. No
package manager, no re-download, no instructions. The rest of this part is about
what that one command actually does, and how to make sure the binary it pulls down
is the one you shipped.&lt;/p&gt;
&lt;h2 id="where-it-looks-for-releases"&gt;Where it looks for releases
&lt;/h2&gt;&lt;p&gt;A tool can&amp;rsquo;t update itself without knowing where its releases live. That&amp;rsquo;s the
&lt;code&gt;--repo&lt;/code&gt; you passed back in part 1: it filled in your tool&amp;rsquo;s release source, the
platform, owner and repository it checks. For &lt;code&gt;--repo myorg/mytool&lt;/code&gt; that&amp;rsquo;s
&lt;code&gt;github.com/myorg/mytool&lt;/code&gt;, and &lt;code&gt;mytool update&lt;/code&gt; looks at that project&amp;rsquo;s releases.&lt;/p&gt;
&lt;p&gt;go-tool-base speaks more than one platform here, GitHub, GitLab, Gitea, Codeberg,
Bitbucket, or a plain HTTP server, so the same command works whether you publish on
github.com or your own GitLab. If you ever need to point somewhere else (a mirror, a
private host), the
&lt;a class="link" href="https://gtb.phpboyscout.uk/how-to/custom-release-source/" target="_blank" rel="noopener"
 &gt;custom release source how-to&lt;/a&gt;
covers it; for a private repository it reads a token the same way the rest of the
tool does.&lt;/p&gt;
&lt;h2 id="what-update-does-step-by-step"&gt;What &lt;code&gt;update&lt;/code&gt; does, step by step
&lt;/h2&gt;&lt;p&gt;When a user runs it, the command walks a short, careful path:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Resolve the latest release&lt;/strong&gt; from your release source.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare versions.&lt;/strong&gt; It reads the version baked into the running binary and
compares it, as semver, against the latest. If you&amp;rsquo;re already current, it says
so and stops: &lt;code&gt;already running latest version, v1.2.0&lt;/code&gt;. (If your build somehow
reports a version ahead of the latest published, it tells you off in character:
&lt;code&gt;your tardis travelled too far into the future...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Download&lt;/strong&gt; the right archive for the user&amp;rsquo;s OS and architecture.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify it&lt;/strong&gt; before trusting it (the next section).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace the running binary&lt;/strong&gt; with the new one, in place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bring the config along.&lt;/strong&gt; If your tool has the &lt;code&gt;init&lt;/code&gt; feature (it does by
default), the update then runs the &lt;em&gt;new&lt;/em&gt; binary&amp;rsquo;s &lt;code&gt;init&lt;/code&gt; over the user&amp;rsquo;s config
directory to fold in anything the release added.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last step is easy to miss and matters more than it looks. A new version often
ships new config: a key for a feature you just added, a changed default. Rather than
leave the user a version behind, with code that expects settings their config file
has never heard of, &lt;code&gt;update&lt;/code&gt; re-runs &lt;code&gt;init&lt;/code&gt; against their existing config once the
swap is done, non-interactively (it passes &lt;code&gt;--skip-login --skip-key&lt;/code&gt;, so nobody gets
re-prompted for a token). It&amp;rsquo;s the same initialiser system from
&lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/" &gt;part 2&lt;/a&gt;,
reused: the merge keeps what the user set and adds what the new version introduced,
so the binary and its config move forward together. Turn the &lt;code&gt;init&lt;/code&gt; feature off and
this step is simply skipped, there&amp;rsquo;s no config to keep in step with.&lt;/p&gt;
&lt;p&gt;There are two flags worth knowing. &lt;code&gt;--version v1.3.0&lt;/code&gt; targets a specific release
instead of the latest, handy for pinning or rolling back. And &lt;code&gt;--force&lt;/code&gt; updates even
when the version check thinks you don&amp;rsquo;t need to. Most of the time, a bare &lt;code&gt;mytool update&lt;/code&gt; is the whole story.&lt;/p&gt;
&lt;h2 id="downloaded-isnt-the-same-as-trusted"&gt;Downloaded isn&amp;rsquo;t the same as trusted
&lt;/h2&gt;&lt;p&gt;A binary that arrives over the network is a binary you didn&amp;rsquo;t build on the machine
it&amp;rsquo;s running on, and a self-updater that swaps itself for whatever the server sent
is a lovely way to ship a corrupted or tampered build straight into your users'
hands. So before the swap, &lt;code&gt;update&lt;/code&gt; verifies what it downloaded against a checksum
manifest, the &lt;code&gt;checksums.txt&lt;/code&gt; GoReleaser produces alongside your binaries. If the
hash of the downloaded archive doesn&amp;rsquo;t match the one in the manifest, the update
aborts and nothing gets replaced.&lt;/p&gt;
&lt;p&gt;By default this is best-effort: a release that ships a &lt;code&gt;checksums.txt&lt;/code&gt; is verified,
but a release without one is updated with a warning rather than a hard stop. When you
want the guarantee, make it mandatory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# in your tool&amp;#39;s config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;require_checksum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now a missing or mismatched checksum is a refusal, not a shrug. I wrote up why this
matters, and exactly what it does and doesn&amp;rsquo;t buy you, in
&lt;a class="link" href="https://blog-570662.gitlab.io/verifying-your-own-downloads/" &gt;verifying your own downloads&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The limit is worth stating plainly, because it&amp;rsquo;s the whole reason there&amp;rsquo;s a
&amp;ldquo;part two&amp;rdquo; to this story. A checksum proves the binary matches the manifest &lt;em&gt;on the
same release page&lt;/em&gt;. It catches a corrupted download or a botched upload cold. What it
cannot catch is an attacker who owns the release platform and swaps both the binary
and its checksum in the same breath, because then the two still agree. Closing that
gap needs a signature whose trust root the release host can&amp;rsquo;t reach, which is a
different piece of machinery (and
&lt;a class="link" href="https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/" &gt;a post of its own&lt;/a&gt;).
go-tool-base now does exactly that: self-update signature verification has shipped, the
binary checking a detached signature against a key it both embeds and fetches over WKD
(&lt;a class="link" href="https://blog-570662.gitlab.io/a-signature-the-platform-cant-forge/" &gt;how it works&lt;/a&gt;).
Until you turn signing on for your own tool, checksums are the floor, and a worthwhile
one.&lt;/p&gt;
&lt;h2 id="seeing-it-work-without-publishing-anything"&gt;Seeing it work without publishing anything
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the catch with writing about self-update: you can&amp;rsquo;t update from a release you
haven&amp;rsquo;t published, and your tutorial tool isn&amp;rsquo;t on anyone&amp;rsquo;s GitHub. There&amp;rsquo;s a flag
for exactly this, meant for offline and air-gapped installs but perfect for a look
under the hood: &lt;code&gt;--from-file&lt;/code&gt; installs from a local release archive instead of the
network.&lt;/p&gt;
&lt;p&gt;Build a snapshot of your tool the way your release pipeline would (GoReleaser&amp;rsquo;s
&lt;code&gt;--snapshot&lt;/code&gt; builds the archives without publishing), then point &lt;code&gt;update&lt;/code&gt; at one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;goreleaser release --snapshot --clean
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool update --from-file ./dist/mytool_Linux_x86_64.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll watch the same extract-and-swap the network path uses, with nothing published
and no release source involved. It&amp;rsquo;s also genuinely useful in its own right, for
shipping into environments that can&amp;rsquo;t reach the internet.&lt;/p&gt;
&lt;h2 id="the-real-loop"&gt;The real loop
&lt;/h2&gt;&lt;p&gt;In production the cycle is the one part 1 already set you up for. The project gtb
scaffolds ships a GoReleaser config and a release pipeline, so the flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tag a version and push the tag.&lt;/li&gt;
&lt;li&gt;CI builds the binaries for every OS and architecture, generates &lt;code&gt;checksums.txt&lt;/code&gt;,
and publishes them as a release on your source.&lt;/li&gt;
&lt;li&gt;Your users run &lt;code&gt;mytool update&lt;/code&gt; and get it, verified.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You write &lt;code&gt;git tag v1.3.0 &amp;amp;&amp;amp; git push --tags&lt;/code&gt;; everyone who installed v1.2.0 is one
command away from the fix. That&amp;rsquo;s the whole point of putting the update channel
inside the tool: shipping a fix becomes tagging a release, and nothing else.&lt;/p&gt;
&lt;h2 id="what-this-buys-you"&gt;What this buys you
&lt;/h2&gt;&lt;p&gt;A tool that updates itself turns &amp;ldquo;please go and reinstall&amp;rdquo; into &lt;code&gt;mytool update&lt;/code&gt;, and
a tool that verifies what it updates to turns &amp;ldquo;I hope that download was clean&amp;rdquo; into a
checked guarantee. Both came with the scaffold; the only work was understanding them.
The full reference, including the config keys and the per-platform release sources,
is in the
&lt;a class="link" href="https://gtb.phpboyscout.uk/components/commands/update/" target="_blank" rel="noopener"
 &gt;update command docs&lt;/a&gt; and the
&lt;a class="link" href="https://gtb.phpboyscout.uk/concepts/auto-update/" target="_blank" rel="noopener"
 &gt;auto-update concepts page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next part is the last one, and it&amp;rsquo;s about what happens after your tool is out there
doing its job: telemetry and logging, so you can see how it&amp;rsquo;s actually being used
without spying on the people using it. Until then, tag a release and watch your tool
catch up to itself.&lt;/p&gt;</description></item><item><title>Building a CLI with go-tool-base, part 4: an AI dungeon master</title><link>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-4/</link><pubDate>Sat, 23 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-4/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-4/cover-building-a-cli-with-go-tool-base-part-4.png" alt="Featured image of post Building a CLI with go-tool-base, part 4: an AI dungeon master" /&gt;&lt;p&gt;I run a Dungeons &amp;amp; Dragons game on the odd weekend, so when I sat down to put an
AI feature inside a CLI, my first instinct wasn&amp;rsquo;t a chatbot. It was: could the
tool run a little adventure, with an AI as the dungeon master? It turns out that&amp;rsquo;s
a near-perfect way to learn the chat client, because the thing that makes a game
trustworthy, rules the players can&amp;rsquo;t break, is exactly the thing that makes any AI
feature trustworthy. So this part builds &lt;code&gt;mytool adventure&lt;/code&gt;: a tiny dungeon you
play in your terminal, narrated by an AI that is firmly on a leash.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/" &gt;Part 3&lt;/a&gt;
pointed AI at your CLI from the outside (an agent driving your commands over MCP).
This part goes the other way: AI inside your tool, as a feature you write. The
worry everyone has about that is fair, AI output is unpredictable, and a CLI is
meant to be dependable. The whole lesson here is how you square those two: you
don&amp;rsquo;t hope the model behaves, you box it in with rules it can&amp;rsquo;t escape and
mechanics it doesn&amp;rsquo;t get to invent.&lt;/p&gt;
&lt;p&gt;As before, this is written against &lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt; (&lt;code&gt;gtb version&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="behind-the-dm-screen"&gt;Behind the DM screen
&lt;/h2&gt;&lt;p&gt;A turn of our game looks like this: the player types what they want to do, the AI
dungeon master narrates what happens and offers a few choices, and round it goes
until the adventure reaches an end. The trick is where the truth lives. The model&amp;rsquo;s
job is the prose, and only the prose. Everything else is yours:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The rules&lt;/strong&gt; live in the system prompt: what the DM may and may not do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The mechanics&lt;/strong&gt; live in Go functions the model calls as tools (dice, combat).
It never makes a number up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The state&lt;/strong&gt; lives in a Go struct you hand the model fresh every turn, so it
never has to remember, and can&amp;rsquo;t quietly rewrite history.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The shape of each turn&lt;/strong&gt; is a typed Go struct the model fills in, so your code
always gets back something it can render, never a wall of prose to parse.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two go-tool-base capabilities do the heavy lifting: the AI
&lt;a class="link" href="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/" &gt;calling your Go functions&lt;/a&gt;,
and the AI
&lt;a class="link" href="https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/" &gt;handing back a typed struct&lt;/a&gt;
instead of text you have to regex. The game is just a fun excuse to use both at
once.&lt;/p&gt;
&lt;h2 id="wiring-a-provider"&gt;Wiring a provider
&lt;/h2&gt;&lt;p&gt;The chat client (&lt;code&gt;pkg/chat&lt;/code&gt;) is a library you import; you don&amp;rsquo;t need any special
feature flag for it. It does need an API key, and it&amp;rsquo;ll find one from a few places.
The simplest, for now, is the well-known environment variable for your provider:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;sk-ant-...&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# or GEMINI_API_KEY, OPENAI_API_KEY&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the bottom of the client&amp;rsquo;s lookup chain, which is fine for playing locally.
For a tool you actually ship, go-tool-base has the &lt;code&gt;ai&lt;/code&gt; feature and its &lt;code&gt;mytool init&lt;/code&gt; wizard (the same initialiser system from
&lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/" &gt;part 2&lt;/a&gt;)
to store the key properly, and there&amp;rsquo;s a whole post on
&lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;where a CLI should keep your keys&lt;/a&gt;.
For learning the client, an env var is plenty.&lt;/p&gt;
&lt;h2 id="scaffold-the-command"&gt;Scaffold the command
&lt;/h2&gt;&lt;p&gt;You know this step from part 1:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; --name adventure --short &lt;span class="s2"&gt;&amp;#34;Play a dungeon adventure&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Everything below goes in the &lt;code&gt;RunAdventure&lt;/code&gt; function the generator left you in
&lt;code&gt;pkg/cmd/adventure/main.go&lt;/code&gt;, plus a couple of types and helpers in the same
package.&lt;/p&gt;
&lt;h2 id="the-state-is-yours-not-the-models"&gt;The state is yours, not the model&amp;rsquo;s
&lt;/h2&gt;&lt;p&gt;Start with the truth. The game state is a plain Go struct that you own. The model
never holds it; instead you hand it the current state at the top of every turn
(more on that in the loop). This is the part to grow: start small, then add rooms,
items, NPCs, quest flags, whatever your adventure needs. Nothing else in the design
has to change when you do.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// GameState is the single source of truth for the game. Extend it freely.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GameState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;PlayerHP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Inventory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Foes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// foe name -&amp;gt; remaining hit points&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// summary renders the state into a line the model is given each turn.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GameState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;foes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Foes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Foes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;foes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;foes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;%s (%d HP)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;You have %d HP, at %s, carrying %s. Foes: %s.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlayerHP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;, &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;foes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;, &amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the shape of a turn, the thing the model has to produce:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Narration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;narration&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;choices&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;GameOver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;game_over&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-dungeon-masters-tools"&gt;The dungeon master&amp;rsquo;s tools
&lt;/h2&gt;&lt;p&gt;A tool in &lt;code&gt;pkg/chat&lt;/code&gt; is a &lt;code&gt;chat.Tool&lt;/code&gt;: a name, a description the model reads to
decide when to use it, a parameter schema, and a handler. The handler gets the
model&amp;rsquo;s arguments as raw JSON and returns any value (which the framework JSON-encodes
back to the model) or an error.&lt;/p&gt;
&lt;p&gt;The simplest possible one is a die roll. This is the canonical &amp;ldquo;give the model
something it&amp;rsquo;s bad at&amp;rdquo; tool, because language models cannot be trusted to roll
fairly or even add up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rollTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;roll&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Roll a die with the given number of sides; returns 1..sides.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// Use an anonymous struct so the schema&amp;#39;s properties sit at the top level,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// which is where SetTools looks. A named type would hide them behind a $ref.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jsonschema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Reflect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;Sides&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;sides&amp;#34; jsonschema:&amp;#34;description=number of sides on the die&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}{}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RawMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nx"&gt;Sides&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;sides&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sides&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sides&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Sides&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That comment about the anonymous struct matters, by the way. Reflect a named
type and &lt;code&gt;jsonschema&lt;/code&gt; emits a top-level reference with the real fields tucked
inside, and the tool ships with no parameters at all. An anonymous struct inlines
them where the framework expects. It&amp;rsquo;s the one sharp edge in the whole exercise.&lt;/p&gt;
&lt;p&gt;Combat is where state actually changes, so combat is a tool too. Note it takes the
foe by name and looks it up in &lt;code&gt;Foes&lt;/code&gt;, so it works for the goblin and for any
creature you add later, without touching this function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;attackTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GameState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;attack&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Resolve the player&amp;#39;s attack on a named foe. Rolls to hit, applies damage.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jsonschema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Reflect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;target&amp;#34; jsonschema:&amp;#34;description=the name of the foe being attacked&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}{}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RawMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;target&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Foes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;no such foe: &amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;foe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;dmg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Intn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dmg&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Foes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;foe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;damage&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dmg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;foe_hp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;defeated&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A bad target comes back as a plain error string, which the framework hands to the
model so it can recover (apologise, pick a real foe) rather than crash.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the whole tool set, and there&amp;rsquo;s deliberately nothing here for reading the
state. The model never fetches it. Instead the loop hands it the current state at
the top of every turn, which we wire up shortly. A language model has no memory you
can rely on, so rather than trust it to remember the fight, you give it the truth
each time.&lt;/p&gt;
&lt;h2 id="the-turn-is-a-tool-too"&gt;The turn is a tool too
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the neat part. The chat client won&amp;rsquo;t let a single call both run tools and
return a typed struct, they&amp;rsquo;re separate modes. So instead of asking for the struct
afterwards, we make submitting the turn into a tool of its own. The dungeon master ends its
turn by calling &lt;code&gt;submit_turn&lt;/code&gt;, and its handler captures the typed &lt;code&gt;Turn&lt;/code&gt; into a
variable we hold:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submitTurnTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Turn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;submit_turn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;End your turn. Call this exactly once, last, with the turn&amp;#39;s outcome.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;jsonschema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Reflect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;Narration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;narration&amp;#34; jsonschema:&amp;#34;description=two-sentence narration of what just happened&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;Choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;choices&amp;#34; jsonschema:&amp;#34;description=the actions the player may take next&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;GameOver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;game_over&amp;#34; jsonschema:&amp;#34;description=true only if the game has ended&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}{}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RawMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;turn recorded&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So the turn&amp;rsquo;s structure is enforced by a schema, same as any other tool&amp;rsquo;s
parameters. Your loop gets a populated &lt;code&gt;Turn&lt;/code&gt; every round, never prose.&lt;/p&gt;
&lt;h2 id="the-rules"&gt;The rules
&lt;/h2&gt;&lt;p&gt;This is where you bound the model. The system prompt is the rulebook, and it leans
hard on the tools so the DM has no room to freelance the mechanics:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dmRules&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`You are the dungeon master of a short fantasy adventure. Each turn
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;you are given the current game state and the player&amp;#39;s action.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Resolve the action and end the turn:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;- If the player attacks, you MUST call the attack tool with the foe&amp;#39;s name to
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; resolve it. Do not decide the hit or the damage yourself.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;- For any other chance event, call the roll tool and use its result.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;- For simple actions, just narrate them.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;- Then call submit_turn exactly once: a two-sentence narration, two or three
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; choices, and game_over.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Trust the state you are given; never contradict it. A foe at 0 hit points is dead
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;and stays dead. The game ends when the player&amp;#39;s hit points reach 0 (they lose), or
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;when the player reaches a satisfying ending. When it ends, set game_over and narrate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;the finish.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;Keep the tone light and quick.`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two of those lines carry the weight. Trusting the state you are given, and never
contradicting it, is what keeps the world consistent: the state is handed in fresh
every turn (the next section), so the model works from the truth instead of from a
memory it does not reliably have. And &lt;code&gt;you MUST call the attack tool&lt;/code&gt; is what stops
it quietly deciding hits and damage itself when it would rather just narrate. Those
two are the difference between a game with rules and a model telling a story.&lt;/p&gt;
&lt;h2 id="the-loop"&gt;The loop
&lt;/h2&gt;&lt;p&gt;Now stitch it together. Create the client with the rules as its system prompt,
register the tools once, and run a turn each time the player acts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunAdventure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;AdventureOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;GameState&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;PlayerHP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;the mouth of a damp cave&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a short sword&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a guttering torch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;Foes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;goblin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Turn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dmRules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;MaxSteps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// roll/attack, then submit_turn&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetTools&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nf"&gt;rollTool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nf"&gt;attackTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nf"&gt;submitTurnTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;I step into the cave.&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Turn&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// Hand the model the current truth, then the player&amp;#39;s action.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;State: %s\nThe player: %s&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Narration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GameOver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;chooseAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The same &lt;code&gt;client&lt;/code&gt; runs every turn, so the conversation and the tools carry through
the whole game; and the &lt;code&gt;State:&lt;/code&gt; line you prepend is always current, because the
&lt;code&gt;attack&lt;/code&gt; tool mutated &lt;code&gt;game&lt;/code&gt; last turn. The model is never trusted to remember,
only to narrate.&lt;/p&gt;
&lt;h2 id="let-the-player-off-the-menu"&gt;Let the player off the menu
&lt;/h2&gt;&lt;p&gt;The one helper I glossed is &lt;code&gt;chooseAction&lt;/code&gt;. A bare &lt;code&gt;fmt.Scanln&lt;/code&gt; would do, but we can
do much better with almost no effort, and make a point while we&amp;rsquo;re at it. The
framework already leans on Charm&amp;rsquo;s &lt;a class="link" href="https://github.com/charmbracelet/huh" target="_blank" rel="noopener"
 &gt;huh&lt;/a&gt; for
its &lt;code&gt;init&lt;/code&gt; wizard, you met it in
&lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/" &gt;part 2&lt;/a&gt;,
so we&amp;rsquo;ll use the same library for a proper menu, with one deliberate addition:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;chooseAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;__other__&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Option&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Something else...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NewSelect&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]().&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;What do you do?&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="c1"&gt;// A second step that only appears when the player chose &amp;#34;Something else&amp;#34;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewInput&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Describe your action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;WithHideFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The select gives the player a tidy arrow-key menu instead of typing a number, but
the addition that earns its keep is the last option. &amp;ldquo;Something else&amp;hellip;&amp;rdquo; is always
there, and choosing it unfolds a second step (huh shows or hides a group with
&lt;code&gt;WithHideFunc&lt;/code&gt;) where the player types whatever they actually want to do. That free
text goes straight to the dungeon master as the next turn&amp;rsquo;s input, and because the
DM is an AI bound by the rules rather than a switch statement over three fixed
choices, it just copes. Bargain with the goblin, search your pockets, set the cave
alight: the model narrates it within the rules you gave it, rolling and applying
damage through the same tools. That is the agency a scripted game can&amp;rsquo;t offer, and
it&amp;rsquo;s the natural place to start building your own richer interactivity on top.&lt;/p&gt;
&lt;h2 id="play-it"&gt;Play it
&lt;/h2&gt;&lt;p&gt;Set your key, build, and go:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;sk-ant-...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;just build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool adventure
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A turn looks like this (your wording will differ every time; the mechanics won&amp;rsquo;t):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;You swing your short sword at the goblin, the blade whistling through the damp cave
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;air. The creature snarls as it tries to dodge your blow.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;What do you do?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; Attack the goblin again
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Try to push deeper into the cave
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Retreat to the entrance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Something else...
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Your blade whistles through the air, but the nimble goblin dances back just in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;time. It lunges forward with a rusty dagger in return, yet its clumsy strike only
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;finds empty air.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;What do you do?
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;gt; Swing your sword again!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Try to intimidate the creature
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Retreat from the cave
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Something else...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Behind that, the dungeon master called &lt;code&gt;attack&lt;/code&gt; each turn (a hit, then a miss), the
goblin&amp;rsquo;s hit points changed in the &lt;code&gt;GameState&lt;/code&gt; you own, and the next turn handed
that updated state straight back to the model. The prose is the model&amp;rsquo;s; every
number is yours.&lt;/p&gt;
&lt;h2 id="the-pattern-under-the-game"&gt;The pattern under the game
&lt;/h2&gt;&lt;p&gt;Strip the dungeon away and you&amp;rsquo;re left with the thing worth keeping. An AI feature
you can ship is one where you&amp;rsquo;ve kept the model away from everything that has to be
right: the &lt;strong&gt;rules&lt;/strong&gt; live in the system prompt, the &lt;strong&gt;mechanics&lt;/strong&gt; in typed Go tools
the model must call, the &lt;strong&gt;state&lt;/strong&gt; in a struct you hand it fresh each turn rather
than trust it to remember, and the &lt;strong&gt;output&lt;/strong&gt; in a struct it fills in rather than
free text. Do that and the model&amp;rsquo;s unpredictability is confined to exactly where you
want it, the wording, and walled out of everywhere you don&amp;rsquo;t, the maths, the state,
the shape of the result.&lt;/p&gt;
&lt;p&gt;Two honest limits worth knowing. There&amp;rsquo;s no
&lt;a class="link" href="https://platform.claude.com/docs/en/about-claude/glossary#temperature" target="_blank" rel="noopener"
 &gt;temperature&lt;/a&gt;
dial on the client (the setting that would let you turn the model&amp;rsquo;s randomness
down), so you can&amp;rsquo;t make the prose reproducible; you make the mechanics
reproducible instead, which for most features is what you actually needed. And a tool calling loop is
several round-trips to the model per turn, so it&amp;rsquo;s not free, keep &lt;code&gt;MaxSteps&lt;/code&gt; tight
for anything interactive.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the foundation, and the state struct is already sized for more than one
fight: it carries a location, an inventory and a map of foes you&amp;rsquo;ve barely touched.
Add a &lt;code&gt;move&lt;/code&gt; tool that updates &lt;code&gt;Location&lt;/code&gt;, a &lt;code&gt;use_item&lt;/code&gt; tool that reaches into
&lt;code&gt;Inventory&lt;/code&gt;, a second creature in &lt;code&gt;Foes&lt;/code&gt;, even a &lt;code&gt;give_quest&lt;/code&gt; flag, and the
adventure grows without the architecture changing. The model just gets more tools
to call and more truth to read. Saved games come nearly free, too: the client can
snapshot and resume a conversation. Next part leaves AI behind and gets the tool
ready to look after itself: shipping signed self-updates, so a new release reaches
your users safely. Until then, go explore the cave.&lt;/p&gt;</description></item><item><title>Building a CLI with go-tool-base, part 3: expose your CLI to AI agents</title><link>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/cover-building-a-cli-with-go-tool-base-part-3.png" alt="Featured image of post Building a CLI with go-tool-base, part 3: expose your CLI to AI agents" /&gt;&lt;p&gt;&amp;ldquo;Make it work with AI&amp;rdquo; is the request that lands on your desk with a thud and no
further detail. The first time it landed on mine I braced for a treadmill of
integration work: an adapter for this assistant, a wrapper for that one, one per
client, forever. Then I looked at the &lt;code&gt;hello&lt;/code&gt; command we built back in
&lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/" &gt;part 1&lt;/a&gt;.
It has a name, a one-line description, and (once you give it some) typed,
documented flags. That is exactly the shape an AI agent needs to call a tool.
You already did the hard part.&lt;/p&gt;
&lt;p&gt;This part wires that up: turning the CLI you&amp;rsquo;ve been building into something an
AI assistant can drive, with no AI code of your own. The how-it-works behind it
is in &lt;a class="link" href="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/" &gt;your CLI is already an AI tool&lt;/a&gt;;
here we just use it.&lt;/p&gt;
&lt;p&gt;A version note, as in the earlier parts: this is written against
&lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt; (&lt;code&gt;gtb version&lt;/code&gt;). The tool is young and moving, so if
you&amp;rsquo;re on a newer release and a command or its output has shifted, check there
first.&lt;/p&gt;
&lt;h2 id="the-translator-you-already-have"&gt;The translator you already have
&lt;/h2&gt;&lt;p&gt;An AI agent that wants to call local tools needs three things: a list of named
operations, a description of each so it knows when to reach for them, and a typed
parameter schema for each so it knows how to call them. A good CLI is already all
three. The only missing piece is a translator between &amp;ldquo;this is a CLI&amp;rdquo; and &amp;ldquo;this
is a set of tools an AI can call&amp;rdquo;, and that translator is the
&lt;a class="link" href="https://modelcontextprotocol.io/" target="_blank" rel="noopener"
 &gt;Model Context Protocol&lt;/a&gt; (MCP), an open standard
every serious assistant now speaks.&lt;/p&gt;
&lt;p&gt;Your tool already ships it. &lt;code&gt;mcp&lt;/code&gt; is one of the default features, so it&amp;rsquo;s been in
your binary since you scaffolded in part 1, no flag required. Check:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool mcp --help
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see subcommands you never wrote. The rest of this part is just three of
them.&lt;/p&gt;
&lt;h2 id="see-what-the-agent-sees"&gt;See what the agent sees
&lt;/h2&gt;&lt;p&gt;Before you connect anything, look at what your tool would expose. This writes the
tool definitions to a file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool mcp tools
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mytool_hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Say hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;inputSchema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;object&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s your &lt;code&gt;hello&lt;/code&gt; command, seen from an agent&amp;rsquo;s side of the glass. The name is
your tool&amp;rsquo;s name and the command path joined with an underscore; the description
is the &lt;code&gt;Short&lt;/code&gt; you gave it in part 1; the &lt;code&gt;inputSchema&lt;/code&gt; is empty because &lt;code&gt;hello&lt;/code&gt;
has no flags yet. Add a flag and it shows up here as a property, with the type and
help text you already wrote. There&amp;rsquo;s no second schema to keep in sync, because the
command tree is the schema.&lt;/p&gt;
&lt;p&gt;A few things are deliberately left off this list: hidden and deprecated commands,
pure command groups that don&amp;rsquo;t do anything themselves, and the &lt;code&gt;mcp&lt;/code&gt;, &lt;code&gt;help&lt;/code&gt; and
&lt;code&gt;completion&lt;/code&gt; plumbing. So &lt;code&gt;mcp tools&lt;/code&gt; doubles as an audit: it&amp;rsquo;s exactly what an
agent can reach, and nothing else.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Exporting the tool definitions with mcp tools" class="gallery-image" data-flex-basis="450px" data-flex-grow="187" height="640" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/demo-mcp-tools.gif" srcset="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/demo-mcp-tools_hu_1f2f7bbe742d14fb.gif 800w, https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/demo-mcp-tools.gif 1200w" width="1200"&gt;&lt;/p&gt;
&lt;h2 id="run-the-server"&gt;Run the server
&lt;/h2&gt;&lt;p&gt;One command turns the whole thing on:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool mcp start
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It doesn&amp;rsquo;t print a banner and march off doing things. It sits quietly, speaking
MCP as JSON-RPC over standard input and output, waiting for an assistant to talk
to it. You won&amp;rsquo;t run this by hand much; the assistant launches it for you. But
it&amp;rsquo;s worth knowing what happens when the agent calls one of your tools: the server
re-runs your own binary with the arguments the agent supplied, captures the output,
and hands it back. The agent isn&amp;rsquo;t poking at your internals. It&amp;rsquo;s running
&lt;code&gt;mytool hello&lt;/code&gt;, the same command a human would, and getting the same result.&lt;/p&gt;
&lt;h2 id="point-an-assistant-at-it"&gt;Point an assistant at it
&lt;/h2&gt;&lt;p&gt;The quickest way is to let the tool write the client config for you. For Claude
Desktop:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool mcp claude &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There are &lt;code&gt;cursor&lt;/code&gt; and &lt;code&gt;vscode&lt;/code&gt; variants too. Restart the assistant and your CLI
is in its toolbox.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d rather wire it by hand (or your client isn&amp;rsquo;t one of those three), the
config is small. Point the client at your binary with &lt;code&gt;mcp start&lt;/code&gt; as its
arguments, using the absolute path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mcpServers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/absolute/path/to/bin/mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Claude Desktop keeps that in &lt;code&gt;claude_desktop_config.json&lt;/code&gt; (under
&lt;code&gt;~/Library/Application Support/Claude/&lt;/code&gt; on macOS, &lt;code&gt;%APPDATA%\Claude\&lt;/code&gt; on Windows);
Cursor uses &lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;; VS Code&amp;rsquo;s Copilot reads
&lt;code&gt;github.copilot.mcpServers&lt;/code&gt; in your settings. The shape is the same everywhere.
From here, ask the assistant to say hello and watch it call &lt;code&gt;mytool_hello&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Wiring the tool into an assistant with mcp claude enable" class="gallery-image" data-flex-basis="411px" data-flex-grow="171" height="700" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/demo-mcp-enable.gif" srcset="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/demo-mcp-enable_hu_c5b32b38db20d0d6.gif 800w, https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-3/demo-mcp-enable.gif 1200w" width="1200"&gt;&lt;/p&gt;
&lt;h2 id="the-agents-reach-is-exactly-your-clis-reach"&gt;The agent&amp;rsquo;s reach is exactly your CLI&amp;rsquo;s reach
&lt;/h2&gt;&lt;p&gt;This is the part worth being calm about. Exposing your CLI over MCP doesn&amp;rsquo;t widen
its surface by an inch. The agent can call the commands you shipped, with the
parameters you defined, and nothing else. It can&amp;rsquo;t invent a command or pass a flag
you never wrote. The boundary of what it can do is the boundary you drew when you
built the tool, and &lt;code&gt;mcp tools&lt;/code&gt; shows you that boundary in full. If there&amp;rsquo;s a
command you don&amp;rsquo;t want an agent touching, mark it hidden and it drops off the list.&lt;/p&gt;
&lt;p&gt;For a long-running or remote setup there&amp;rsquo;s also &lt;code&gt;./bin/mytool mcp stream&lt;/code&gt;, which
serves the same tools over HTTP instead of stdio; the
&lt;a class="link" href="https://gtb.phpboyscout.uk/cli/mcp/" target="_blank" rel="noopener"
 &gt;mcp reference&lt;/a&gt; has the details. For most
desktop assistants, &lt;code&gt;start&lt;/code&gt; over stdio is all you need.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;You turned the CLI you&amp;rsquo;ve been building into an agent-callable tool with one
command and zero lines of AI code, because the real work, naming your operations
and documenting their inputs, you finished the moment your &lt;code&gt;--help&lt;/code&gt; was any good.
Every command you add from here is a new tool the agent gets for free.&lt;/p&gt;
&lt;p&gt;Next part goes the other way: instead of letting an assistant drive your tool from
outside, we put AI inside it, wiring up a provider and building a feature against
go-tool-base&amp;rsquo;s chat SDK. Until then, add a command or two and watch them appear in
&lt;code&gt;mcp tools&lt;/code&gt;. The agent&amp;rsquo;s toolbox grows as your CLI does.&lt;/p&gt;</description></item><item><title>Technical CV writing is still hard, and now a robot reads it first</title><link>https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/</guid><description>&lt;img src="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cover-technical-cv-writing-and-the-ai-filter.png" alt="Featured image of post Technical CV writing is still hard, and now a robot reads it first" /&gt;&lt;p&gt;Seven years ago I wrote a post called &lt;a class="link" href="https://blog-570662.gitlab.io/technical-cv-writing/" &gt;Technical CV writing is hard&lt;/a&gt;, pulled my own CV apart, and explained every choice in it. I even bragged that it converted to a first interview about eighty per cent of the time, then added &amp;ldquo;watch me now jinx myself for the future&amp;rdquo;. Reader, I jinxed myself. I&amp;rsquo;m back on the market, the same CV that served me for two decades went out into the world, and what came back was a sort of stunned silence. Not even rejections. Just nothing.&lt;/p&gt;
&lt;h2 id="the-cv-that-suddenly-stopped-working"&gt;The CV that suddenly stopped working
&lt;/h2&gt;&lt;p&gt;The thing about that silence is how &lt;em&gt;specific&lt;/em&gt; it was. Some applications behaved exactly as they always had: a human read the CV, liked it or didn&amp;rsquo;t, and replied like a person. Others went into a void. And the void had a pattern to it. It was the bigger, more process-heavy outfits, the ones you&amp;rsquo;d bet good money have an Applicant Tracking System and an &amp;ldquo;AI-assisted screening&amp;rdquo; line item in some HR budget.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s when the penny dropped. My CV wasn&amp;rsquo;t failing to impress anyone. It wasn&amp;rsquo;t reaching anyone. The first thing reading it wasn&amp;rsquo;t a person at all.&lt;/p&gt;
&lt;h2 id="the-reader-changed-and-i-hadnt-noticed"&gt;The reader changed, and I hadn&amp;rsquo;t noticed
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve made this exact point on this blog before, only about software: &lt;a class="link" href="https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/" &gt;half your users don&amp;rsquo;t have eyes&lt;/a&gt;. A CLI tool&amp;rsquo;s output has two audiences, the human at the terminal and the script parsing the output, and they want completely different things. It turns out a CV is now precisely the same. It has two readers, and the first one is a machine.&lt;/p&gt;
&lt;p&gt;A human recruiter reads a CV the way I designed mine to be read: narrative, personality, a sense of the person. An ATS or an AI screen does nothing of the sort. It parses for structure, for keyword density, for recency, for numbers it can latch onto. My CV was a beautifully tailored sales pitch aimed squarely at a human who, increasingly, never gets to see it, because a parser in front of them scored it and quietly binned it first.&lt;/p&gt;
&lt;p&gt;Everything that made it a good &lt;em&gt;human&lt;/em&gt; document was, to the machine, either invisible or actively confusing.&lt;/p&gt;
&lt;h2 id="so-i-asked-an-ai-what-the-ai-hated"&gt;So I asked an AI what the AI hated
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s an irony here I&amp;rsquo;m choosing to enjoy rather than resent. The way I worked out what the filters object to was to sit down with Gemini, hand it my CV, and ask it to read the thing the way a recruitment AI would and tell me where it tripped. Using one AI to get past another. Fight fire with fire.&lt;/p&gt;
&lt;p&gt;The one instruction I was firm about, and I&amp;rsquo;ll come back to it, was that the CV had to stay recognisably &lt;em&gt;me&lt;/em&gt;. I wasn&amp;rsquo;t asking Gemini to launder my career into something generic and machine-shaped. I was asking it to help me keep as much of my own voice and judgement as possible, while making the thing easier for an AI to approve and a human to enjoy. There&amp;rsquo;s a practical edge to that, too: the screening tools are increasingly tuned to spot the patterns of generated text and weight them down, so a CV that reads as though a model wrote it can trip the very filter you were trying to please, quite apart from leaving the human at the end of it cold.&lt;/p&gt;
&lt;p&gt;With that ground rule set, the hurdles it surfaced were genuinely illuminating, and a bit humbling given I&amp;rsquo;d written a whole confident blog post about how to do this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The skills tables are worse than useless.&lt;/strong&gt; My CV led with two lovely tables: Management Skills and Technical Skills, each with a level and years of experience. Clean and scannable for a human. To a lot of parsers, a table is a trap: they flatten it into a jumble and lose the structure entirely. Worse, listing &amp;ldquo;20+ years&amp;rdquo; against nearly everything triggers what I can only call the recency trap. Modern screening looks for skills that show up &lt;em&gt;in your recent job descriptions&lt;/em&gt;, not in a header table. A language sitting in my skills table but not in my last two roles reads as stale or unverified, no matter how many years I claimed next to it. Gemini put it plainly: &amp;ldquo;if a tool sees Golang in a top table but doesn&amp;rsquo;t see it explicitly mentioned in your last two job descriptions, it assumes the skill is stale or unverified.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;img alt="My skills laid out as tables of skill, level and commercial experience. Lovely for a human to scan, a jumble the moment a parser flattens the formatting. This is the long-standing shape, here in its original 2019 form." class="gallery-image" data-flex-basis="251px" data-flex-grow="104" height="792" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-skills-before.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-skills-before_hu_d839434784c23aee.png 800w, https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-skills-before.png 831w" width="831"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;I have a passion for what I do&amp;rdquo; is noise.&lt;/strong&gt; My opening profile statement, which I was rather proud of, is exactly the sort of thing a screening tool discards wholesale. As Gemini noted, these tools &amp;ldquo;completely ignore subjective self-assessments &amp;hellip; because they cannot be measured or verified.&amp;rdquo; It wants a dense, factual summary full of the nouns it&amp;rsquo;s searching for, right at the top.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The old opening: my name, my contact details, and a warm but entirely unmeasurable “I have a passion for what I do” profile statement." class="gallery-image" data-flex-basis="1055px" data-flex-grow="439" height="188" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-profile-before.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-profile-before_hu_6f720dcded107b1.png 800w, https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-profile-before.png 827w" width="827"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My numbers thin out the further back you go.&lt;/strong&gt; My recent roles are full of the data these tools love: a 75% reduction in deployment times, three thousand-odd Kubernetes clusters, a GitLab instance with four hundred thousand repositories. My older roles, written years ago in a more narrative style, are all &amp;ldquo;oversaw the delivery of solutions&amp;rdquo; with not a metric in sight. The machine reads that as a career that got vaguer over time, which is the opposite of true.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Four pages is at least two too many.&lt;/strong&gt; Parsers weight the first page or two most heavily. My education and the foundational stuff sat on pages three and four, where the algorithm barely bothers to look.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It couldn&amp;rsquo;t work out what I am.&lt;/strong&gt; This was the sharp one. With &amp;ldquo;pre-sales&amp;rdquo;, &amp;ldquo;client management&amp;rdquo; and &amp;ldquo;Managing Director&amp;rdquo; sitting next to deep technical keywords, the classifier genuinely can&amp;rsquo;t decide whether I&amp;rsquo;m a commercial manager who used to code or a hands-on engineer who drifted into management. As Gemini described it: &amp;ldquo;the algorithm gets confused &amp;hellip; It struggles to classify you: Are you a commercial manager who used to code, or a hands-on techie who got pushed into management?&amp;rdquo; So it does the safe thing and matches me to neither.&lt;/p&gt;
&lt;h2 id="what-im-actually-changing"&gt;What I&amp;rsquo;m actually changing
&lt;/h2&gt;&lt;p&gt;Knowing the hurdles, here&amp;rsquo;s what the rebuild looks like. This is the part I want to be useful, so it&amp;rsquo;s concrete.&lt;/p&gt;
&lt;p&gt;The tables are gone. In their place is a &amp;ldquo;Core Expertise&amp;rdquo; section, plain text the parser can read, grouped so my leadership sits next to my technical stack. And I&amp;rsquo;ve done the thing 2019-me was too much of a show-off to do: tiered it &lt;em&gt;honestly&lt;/em&gt;. Instead of &amp;ldquo;Expert+&amp;rdquo; against everything, there&amp;rsquo;s a primary tier of what I actually do day to day, a proficient tier I can deploy without blinking, and a frank &amp;ldquo;familiar, not current&amp;rdquo; tier for the languages I last touched in anger a decade ago. That honesty isn&amp;rsquo;t just decency. A wall of &amp;ldquo;expert at everything&amp;rdquo; reads as noise to a machine and as bluster to a human, and I&amp;rsquo;d been doing both.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The replacement: a plain-text Core Expertise list a parser can read straight through, tiered honestly into what I do day to day, what I’m proficient in, and what I’m only still familiar with." class="gallery-image" data-flex-basis="288px" data-flex-grow="120" height="1149" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-skills-after.jpg" srcset="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-skills-after_hu_89689e72a4badcba.jpg 800w, https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-skills-after.jpg 1383w" width="1383"&gt;&lt;/p&gt;
&lt;p&gt;The subjective profile is replaced with a keyword-rich professional summary that says, in the first two lines, exactly what I am and at what scale.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The replacement: a Professional Summary that leads with the role and the scale, in the nouns a parser is actually hunting for, with the person still audible underneath." class="gallery-image" data-flex-basis="623px" data-flex-grow="259" height="537" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-profile-after.jpg" srcset="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-profile-after_hu_79f08a915a901057.jpg 800w, https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-profile-after.jpg 1394w" width="1394"&gt; The keywords that mattered have been woven down &lt;em&gt;into&lt;/em&gt; the recent role bullets, so the parser sees them where it trusts them. And I&amp;rsquo;ve reframed the people-management and pre-sales language toward technical enablement and architectural advisory, because what I&amp;rsquo;m actually chasing is the technical-leader sweet spot: the person who owns the architecture and mentors the engineers, without the HR admin and the sales pitches. The CV now points at that, deliberately, so the classifier stops dithering.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a more personal beat in here. A previous employer handed me a role with a &amp;ldquo;VP&amp;rdquo; title, sold to me as exactly the technical-leadership job I&amp;rsquo;d been chasing. It wasn&amp;rsquo;t. The title turned out to be a pay-grade bracket rather than a description of the work, the work itself was hands-on firefighting with little of the leadership or empowerment I&amp;rsquo;d been promised, and I moved on within a few months. To a screening AI, that pairing is doubly awkward. A &amp;ldquo;VP&amp;rdquo; title files me as a meeting-heavy executive and rules me out of the hands-on Principal and Lead roles I actually want, and a sub-six-month stint trips the flight-risk flag that some trackers quietly score you down for. So the fix is to stop letting the inflated label do the talking: describe the functional reality of the work, retitle it to the technical track it actually was, and let the scale of what I wrestled with speak instead of the job title. Titles, it turns out, are for the pay band. The bullets are for the truth.&lt;/p&gt;
&lt;p&gt;&lt;img alt="My recent roles on the new CV: each leads with the work and the numbers, in technical-track titles a parser weights and a human believes." class="gallery-image" data-flex-basis="282px" data-flex-grow="117" height="1167" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-experience.jpg" srcset="https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-experience_hu_eb79c05fe00004f.jpg 800w, https://blog-570662.gitlab.io/technical-cv-writing-and-the-ai-filter/cv-experience.jpg 1374w" width="1374"&gt;&lt;/p&gt;
&lt;h2 id="keeping-myself-in-it"&gt;Keeping myself in it
&lt;/h2&gt;&lt;p&gt;Back to that ground rule. Every one of these changes is in service of getting past the machine to the human behind it, and neither reader is well served by a CV with the person scrubbed out of it. The screen, increasingly, is trained to notice generic generated phrasing and mark it down; the human, always, would rather read something with a pulse. So the keywords go in, the structure gets fixed, the metrics come forward, and the &lt;em&gt;voice stays mine&lt;/em&gt;. No &amp;ldquo;results-driven synergistic leveraging of cross-functional paradigms&amp;rdquo; that nobody would ever say out loud. That was the whole point of doing it this way: let the AI help reshape the &lt;em&gt;structure&lt;/em&gt; a parser cares about, while the &lt;em&gt;words&lt;/em&gt; stay mine, so what comes out is easier for a machine to approve, easier for a human to enjoy, and still unmistakably written by me. Optimising for the filter and sounding like myself turned out not to be in conflict at all.&lt;/p&gt;
&lt;h2 id="i-genuinely-dont-know-if-this-works-yet"&gt;I genuinely don&amp;rsquo;t know if this works yet
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part that makes this a post and not a victory lap. I don&amp;rsquo;t know if any of this lands. The old CV converted at around eighty per cent, on my own possibly-generous reckoning, right up until it abruptly didn&amp;rsquo;t. The new one is going out now, into the same market and the same filters that were stonewalling me a fortnight ago.&lt;/p&gt;
&lt;p&gt;So this is a promise as much as a post. I&amp;rsquo;m going to keep count, the way I should have all along, and come back with the actual numbers: did reshaping my CV for a reader with no eyes genuinely move the needle, or did I just make it uglier and learn nothing? Either way you&amp;rsquo;ll get the truth, because a follow-up that only reports good news isn&amp;rsquo;t worth writing. Watch this space, and if you&amp;rsquo;re sending CVs into the same silence, maybe try reading yours the way a machine would first. It&amp;rsquo;s a deeply odd exercise, and I suspect it&amp;rsquo;s now an essential one.&lt;/p&gt;</description></item><item><title>Building a CLI with go-tool-base, part 2: configuration your tool can trust</title><link>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-2/cover-building-a-cli-with-go-tool-base-part-2.png" alt="Featured image of post Building a CLI with go-tool-base, part 2: configuration your tool can trust" /&gt;&lt;p&gt;In &lt;a class="link" href="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/" &gt;part 1&lt;/a&gt;
you scaffolded a tool and gave it a &lt;code&gt;hello&lt;/code&gt; command. It says the same thing
every time, which is fine for a first command and useless for a real one. The
moment a tool does anything worth doing it needs settings: an endpoint, a
default, a token, a log level. And the moment you have settings, you have the
problem nobody warns you about. You set one in a file, the tool ignores it, the
code that reads it looks perfectly correct, and an hour later you find you&amp;rsquo;d
typed &lt;code&gt;tiemout&lt;/code&gt;. Nothing in the whole stack thought that worth a word.&lt;/p&gt;
&lt;p&gt;The good news is you don&amp;rsquo;t have to build any of this. Your scaffold already
wired up a config system in part 1, the same one the rest of go-tool-base uses.
This part puts it to work: where a setting&amp;rsquo;s value actually comes from, how to
ship sensible defaults alongside the command they belong to, how to layer files
so a team and a laptop can disagree politely, and how to turn a fat-fingered key
from a silent shrug into an error that tells you exactly what you got wrong.&lt;/p&gt;
&lt;p&gt;The same version note as part 1, since each of these stands on its own:
everything here is written against &lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt; (&lt;code&gt;gtb version&lt;/code&gt; will
tell you what you&amp;rsquo;re on). The tool is young and still changing shape, so if you&amp;rsquo;re
on a newer release and a detail has drifted, that&amp;rsquo;s the first thing to check. I&amp;rsquo;ll
flag anything that breaks across versions as it comes up.&lt;/p&gt;
&lt;h2 id="you-already-have-a-config-system"&gt;You already have a config system
&lt;/h2&gt;&lt;p&gt;The root command loads configuration for you before any of your command code
runs, merges every source together, and hands the result to each command through
&lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;&lt;code&gt;Props&lt;/code&gt;&lt;/a&gt;.
By the time your &lt;code&gt;RunHello&lt;/code&gt; runs, &lt;code&gt;props.Config&lt;/code&gt; is populated and ready.&lt;/p&gt;
&lt;p&gt;A value can arrive from several places at once, so there&amp;rsquo;s an order. Highest
wins:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Command-line flags&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Environment variables&lt;/strong&gt; (your tool&amp;rsquo;s prefix plus the key, so
&lt;code&gt;hello.greeting&lt;/code&gt; reads &lt;code&gt;MYTOOL_HELLO_GREETING&lt;/code&gt;, with the dots turned into
underscores)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Config files&lt;/strong&gt; (on disk, in the order they were loaded)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That ladder is the mental model for what beats what: a flag beats an env var, an
env var beats a file. The files are worth pinning down, though, because there&amp;rsquo;s
more than one and they don&amp;rsquo;t all come from the same place. This is the bit that&amp;rsquo;s
easy to trip over:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Embedded defaults&lt;/strong&gt; are baked into the binary, one slice per command. You
don&amp;rsquo;t read these at runtime directly. The &lt;code&gt;init&lt;/code&gt; command (coming up) bakes them
into your config file for you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The file &lt;code&gt;init&lt;/code&gt; writes&lt;/strong&gt;, &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;, is the default the tool
reads, along with a machine-wide &lt;code&gt;/etc/mytool/config.yaml&lt;/code&gt; if one exists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Files passed with &lt;code&gt;--config&lt;/code&gt;&lt;/strong&gt; replace those defaults for that run rather than
adding to them. Name one or more and the tool reads exactly those.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We&amp;rsquo;ll set each of these up in turn. The full reference lives in the
&lt;a class="link" href="https://gtb.phpboyscout.uk/components/config/" target="_blank" rel="noopener"
 &gt;config docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Reading a value is one call, and it&amp;rsquo;s typed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.greeting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;server.timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;debug&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;verbose&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="give-a-command-a-setting"&gt;Give a command a setting
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s make &lt;code&gt;hello&lt;/code&gt; configurable. Open &lt;code&gt;pkg/cmd/hello/main.go&lt;/code&gt; (your file, the one
the generator leaves alone) and read the greeting from config instead of
hard-coding it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunHello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;HelloOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.greeting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Build and run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;just build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERRO failed to load config: no configuration files found
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;please run init, or provide a config file using the --config flag
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Not what you expected, maybe, but it&amp;rsquo;s the right instinct from the tool. It has
no configuration to read yet, and rather than guess, it stops and says so. Which
brings us neatly to where settings actually come from.&lt;/p&gt;
&lt;h2 id="defaults-belong-to-the-command"&gt;Defaults belong to the command
&lt;/h2&gt;&lt;p&gt;You could drop a default into the project&amp;rsquo;s central config, and for something
truly global like the log level that&amp;rsquo;s the right home. But a setting that belongs
to &lt;code&gt;hello&lt;/code&gt; should live with &lt;code&gt;hello&lt;/code&gt;, not in a shared file you have to remember to
edit every time you add a command. The generator does this for you, you just have
to ask. Back in part 1 you generated &lt;code&gt;hello&lt;/code&gt; without config support, so run the
same command again with &lt;code&gt;--assets&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; --name hello --short &lt;span class="s2"&gt;&amp;#34;Say hello&amp;#34;&lt;/span&gt; --assets
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is safe to re-run. The generator honours the code you&amp;rsquo;ve already written:
it refreshes the boilerplate &lt;code&gt;cmd.go&lt;/code&gt;, adds the asset scaffolding, and leaves
your &lt;code&gt;main.go&lt;/code&gt;, and the &lt;code&gt;RunHello&lt;/code&gt; you&amp;rsquo;ve been editing, completely alone. One
thing to hold off on here: don&amp;rsquo;t reach for &lt;code&gt;--force&lt;/code&gt;. Force rewrites everything,
including that &lt;code&gt;main.go&lt;/code&gt;, which is exactly the work you want to keep.&lt;/p&gt;
&lt;p&gt;You now have &lt;code&gt;pkg/cmd/hello/assets/init/config.yaml&lt;/code&gt;, and the generator has
already opened it under the command&amp;rsquo;s own namespace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fill in your defaults under it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Hello&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;plain&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those values are embedded into the binary as an asset, and the generated &lt;code&gt;cmd.go&lt;/code&gt;
registers them with &lt;code&gt;Props&lt;/code&gt; for you (&lt;code&gt;props.Assets.Register(&amp;quot;hello&amp;quot;, &amp;amp;assets)&lt;/code&gt;),
so the config system knows where your command&amp;rsquo;s defaults live. A quick word on
&lt;code&gt;style&lt;/code&gt;, since we&amp;rsquo;ll lean on it shortly: it&amp;rsquo;s a second setting I&amp;rsquo;m giving a
default now so it&amp;rsquo;s ready when we need it. Plain says the greeting as written;
loud will shout it.&lt;/p&gt;
&lt;p&gt;That per-command home comes with one rule worth taking seriously: namespace your
keys. Notice the generator opened the file under a &lt;code&gt;hello:&lt;/code&gt; key rather than at
the top level. Copy that. Every command ships its defaults in its own embedded
file, and those files are all merged together to build the config, but the order
they merge in is not guaranteed. If two commands both defined a top-level
&lt;code&gt;timeout&lt;/code&gt;, which one won would be a toss-up that could flip between builds. Keep
each command&amp;rsquo;s settings under its own name (&lt;code&gt;hello.greeting&lt;/code&gt;, &lt;code&gt;report.timeout&lt;/code&gt;)
and the clash can&amp;rsquo;t happen in the first place. The generator namespacing the file
for you is a hint worth taking.&lt;/p&gt;
&lt;p&gt;One thing the defaults file does not do is set values through struct tags. If you
later add a &lt;code&gt;default:&amp;quot;info&amp;quot;&lt;/code&gt; tag to a config field, that&amp;rsquo;s documentation for the
error messages, nothing more. Real defaults live here, in the embedded YAML. It&amp;rsquo;s
an easy thing to assume otherwise and then wonder why your default never applied.&lt;/p&gt;
&lt;h2 id="first-run-init"&gt;First run: init
&lt;/h2&gt;&lt;p&gt;So your defaults are baked into the binary. The tool still needs an actual config
file to read, and that&amp;rsquo;s what &lt;code&gt;init&lt;/code&gt; is for. It&amp;rsquo;s one of the features your tool
shipped with, so it&amp;rsquo;s already there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Configuration initialised in /home/you/.mytool/config.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Open that file and you&amp;rsquo;ll find your command&amp;rsquo;s defaults waiting in it, merged with
the framework&amp;rsquo;s own:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Hello&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;plain&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;info&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the missing piece. &lt;code&gt;init&lt;/code&gt; gathers every command&amp;rsquo;s embedded defaults
through the &lt;code&gt;Assets&lt;/code&gt; layer, writes them to &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;, locks the
file down to &lt;code&gt;0600&lt;/code&gt; (it may hold secrets later), and drops in a &lt;code&gt;.gitignore&lt;/code&gt; so
nobody commits it by accident. Now &lt;code&gt;hello&lt;/code&gt; has something to read:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Prefer no init step?&lt;/strong&gt; &lt;code&gt;init&lt;/code&gt; is a feature, and you can leave it out of your
tool&amp;rsquo;s feature set. With it off, the tool loads its embedded defaults directly
and runs with no config file at all, you&amp;rsquo;d only add one to override something.
That suits a small, self-contained tool. This tutorial keeps &lt;code&gt;init&lt;/code&gt; on, which
is the default and the right call while a tool is finding its feet, so the rest
of the article assumes it.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="setup-that-needs-a-human-initialisers"&gt;Setup that needs a human: initialisers
&lt;/h2&gt;&lt;p&gt;Static defaults cover the values you can decide for the user. Some you can&amp;rsquo;t: a
token, an API key, an endpoint that differs per person. Writing a blank or
guessed value for those is worse than useless. This is where go-tool-base does
something I&amp;rsquo;ve not seen many CLI frameworks bother with: it lets a command bring
its own first-run setup, and wires it in for you. It&amp;rsquo;s one of the genuine reasons
to build on the framework rather than roll your own, so it&amp;rsquo;s worth a proper look.&lt;/p&gt;
&lt;p&gt;Generate a command with &lt;code&gt;--with-initializer&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; --name greet --short &lt;span class="s2"&gt;&amp;#34;Greet someone&amp;#34;&lt;/span&gt; --with-initializer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alongside the usual files you get an &lt;code&gt;init.go&lt;/code&gt;. It&amp;rsquo;s generated and marked &lt;code&gt;DO NOT EDIT&lt;/code&gt;, and it does all the wiring. Here&amp;rsquo;s the heart of it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Code generated by gtb. DO NOT EDIT.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FeatureCmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InitialiserProvider&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Initialiser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;skipGreet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SubcommandProvider&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;NewCmdInitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FeatureFlag&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;BoolVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;skipGreet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;skip-greet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;skip initializing greet configuration&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;IsConfigured&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;GreetInitialiser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;InitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That package &lt;code&gt;init()&lt;/code&gt; registers three things with the framework the moment your
command is imported, with no central setup file for you to edit: the initialiser
itself, an &lt;code&gt;init greet&lt;/code&gt; subcommand so the user can reconfigure just this command
later, and a &lt;code&gt;--skip-greet&lt;/code&gt; flag on the main &lt;code&gt;init&lt;/code&gt;. &lt;code&gt;IsConfigured&lt;/code&gt; is how the
framework avoids nagging: if the &lt;code&gt;greet&lt;/code&gt; key is already in the config, &lt;code&gt;init&lt;/code&gt;
leaves it be and moves on.&lt;/p&gt;
&lt;p&gt;All of that is generated for you. The one piece that&amp;rsquo;s yours is the &lt;code&gt;InitGreet&lt;/code&gt;
function in &lt;code&gt;main.go&lt;/code&gt;, which starts as a stub:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;InitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// TODO: Implement custom initialization logic for greet&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Fill it in with whatever the setup needs. go-tool-base leans on
&lt;a class="link" href="https://github.com/charmbracelet/huh" target="_blank" rel="noopener"
 &gt;huh&lt;/a&gt; for prompts, the same library its own
GitHub and AI setup use, so a one-question form looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;InitGreet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;			&lt;/span&gt;&lt;span class="nx"&gt;huh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewInput&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;What greeting should greet use?&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;				&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;greet.greeting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set the value on &lt;code&gt;cfg&lt;/code&gt; and you&amp;rsquo;re done. After the initialisers run, &lt;code&gt;init&lt;/code&gt; writes
the whole config out to disk, so the answer persists into &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;
with everything else. Run &lt;code&gt;mytool init&lt;/code&gt; on a fresh machine now and it stops to ask
for the greeting; run it again and it sails past, because &lt;code&gt;IsConfigured&lt;/code&gt; sees the
key is already there. Need to redo just this one command&amp;rsquo;s setup? &lt;code&gt;mytool init greet&lt;/code&gt;. The framework hands each command its own setup step, its own subcommand
and its own skip flag, and asks you for a single function in return. That&amp;rsquo;s the
trade worth making: static defaults in your embedded YAML, anything that needs a
human in an initialiser.&lt;/p&gt;
&lt;h2 id="overriding-the-environment-and-layered-files"&gt;Overriding: the environment and layered files
&lt;/h2&gt;&lt;p&gt;With a config file in place, the other sources come into their own. The quickest
override is an environment variable. Remember the prefix you set when scaffolding
in part 1: &lt;code&gt;hello.greeting&lt;/code&gt; maps to &lt;code&gt;MYTOOL_HELLO_GREETING&lt;/code&gt;, the prefix and key
joined up, uppercased, dots turned to underscores:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;MYTOOL_HELLO_GREETING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Hello from mytool&amp;#34;&lt;/span&gt; ./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;INFO Hello from mytool
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You didn&amp;rsquo;t register that variable anywhere; the config system binds it for you.
The prefix is what keeps it from colliding with some other tool&amp;rsquo;s &lt;code&gt;LOG_LEVEL&lt;/code&gt; on
the same machine, which is exactly why it&amp;rsquo;s worth having.&lt;/p&gt;
&lt;p&gt;Files are the other half, and they&amp;rsquo;re where that precedence list earns a closer
look. A single config file is fine until two people, or two machines, want
slightly different settings, and then you&amp;rsquo;re copying files around by hand. The
&lt;code&gt;--config&lt;/code&gt; flag fixes that: pass it more than once and the tool merges the files
in order.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --config ./config.yaml &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --config ./config.local.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Between the files you name, the rule is &lt;strong&gt;later wins on a clash, and every key
that doesn&amp;rsquo;t clash is kept.&lt;/strong&gt; If &lt;code&gt;config.yaml&lt;/code&gt; sets &lt;code&gt;hello.greeting: Hello&lt;/code&gt; and
&lt;code&gt;config.local.yaml&lt;/code&gt; sets &lt;code&gt;hello.greeting: Oi&lt;/code&gt;, you get &lt;code&gt;Oi&lt;/code&gt;, but keys that appear
in only one file survive untouched. It&amp;rsquo;s a merge between them, not a replacement.&lt;/p&gt;
&lt;p&gt;The edge to remember is what &lt;code&gt;--config&lt;/code&gt; does to the default locations: it replaces them.
The moment you name a file, &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt; drops out of the picture
unless you name it too. So you pass the whole stack you want, a shared base and a
local override together, and let precedence settle it. Commit a &lt;code&gt;config.yaml&lt;/code&gt; with
the team&amp;rsquo;s settings, keep an untracked &lt;code&gt;config.local.yaml&lt;/code&gt; for your own, run with
both, and your local tweaks win without anyone editing a shared file. Leave
&lt;code&gt;--config&lt;/code&gt; off and you&amp;rsquo;re back on the defaults &lt;code&gt;init&lt;/code&gt; wrote: &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt;
plus that machine-wide &lt;code&gt;/etc/mytool/config.yaml&lt;/code&gt; if it&amp;rsquo;s there. Whichever set of
files you land on, environment variables and flags still sit on top.&lt;/p&gt;
&lt;h2 id="the-typo-that-does-nothing"&gt;The typo that does nothing
&lt;/h2&gt;&lt;p&gt;Now for the failure I keep circling. Say you want to change the greeting. Open
your config, but fat-finger the key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;greting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Oi &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# meant to be greeting&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run it, and you get a blank line. The greeting you set never applied: the
misspelled key was read, matched nothing, and was silently dropped, and the real
&lt;code&gt;greeting&lt;/code&gt; is now nowhere to be found. Nothing said a word. For a greeting it&amp;rsquo;s a
shrug. For a timeout or a retry count it&amp;rsquo;s the bug you chase at 2am, and I wrote
up the why of it in
&lt;a class="link" href="https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/" &gt;the config key that quietly did nothing&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;go-tool-base won&amp;rsquo;t catch this for you by default, and that&amp;rsquo;s a choice rather than
an oversight. There&amp;rsquo;s no central schema that knows every key your tool could ever
take, because keys belong to the commands that use them. What you get instead is
a way to opt a command in, so it validates its own slice and nobody else&amp;rsquo;s.&lt;/p&gt;
&lt;h2 id="making-mistakes-loud"&gt;Making mistakes loud
&lt;/h2&gt;&lt;p&gt;Tell the generator you want validation for a command and it scaffolds exactly
this (&lt;code&gt;gtb generate command --name hello --with-config-validation&lt;/code&gt;). Since
&lt;code&gt;hello&lt;/code&gt; already exists, it&amp;rsquo;s a small file to add by hand. Create
&lt;code&gt;pkg/cmd/hello/config.go&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hello&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/config&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// HelloConfig describes the config keys the hello command consumes.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HelloConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;hello.greeting&amp;#34; validate:&amp;#34;required&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;Style&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;hello.style&amp;#34; enum:&amp;#34;plain,loud&amp;#34; default:&amp;#34;plain&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ValidateHelloConfig checks the hello config against its schema.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ValidateHelloConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Containable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ValidateStruct&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HelloConfig&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The tags carry the rules. &lt;code&gt;validate:&amp;quot;required&amp;quot;&lt;/code&gt; means the key has to be present
and non-empty. &lt;code&gt;enum:&amp;quot;plain,loud&amp;quot;&lt;/code&gt; means &lt;code&gt;style&lt;/code&gt; has to be one of those two words.
&lt;code&gt;config.ValidateStruct[HelloConfig]&lt;/code&gt; does the rest: it derives a schema from those
tags and checks the config against it, returning a readable error if anything is
off. It takes &lt;code&gt;props.Config&lt;/code&gt; as it is, the &lt;code&gt;Containable&lt;/code&gt; interface, so there&amp;rsquo;s no
casting to a concrete type. Call it at the top of &lt;code&gt;RunHello&lt;/code&gt;, before you trust any
of the values, and use the style while you&amp;rsquo;re there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunHello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;HelloOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ValidateHelloConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.greeting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello.style&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;loud&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;strings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(You&amp;rsquo;ll add &lt;code&gt;strings&lt;/code&gt; to the imports at the top of &lt;code&gt;main.go&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;Now make a real mistake. Set the style to something that isn&amp;rsquo;t allowed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Hello&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;shout&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ERRO config validation failed:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; hello.style: value &amp;#34;shout&amp;#34; is not allowed (hint: allowed values: plain, loud)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the difference. The command stops and tells you the key, the bad value,
and what it would have accepted. The same check catches a misspelled
&lt;code&gt;greeting&lt;/code&gt;: the moment the real key goes missing, &lt;code&gt;required&lt;/code&gt; fails with
&lt;code&gt;hello.greeting: required field is missing&lt;/code&gt; instead of quietly running on
nothing. Set &lt;code&gt;style: loud&lt;/code&gt; and you get &lt;code&gt;HELLO&lt;/code&gt;, because the value finally passes
and the code downstream can trust it.&lt;/p&gt;
&lt;p&gt;If you switch on the optional &lt;code&gt;config&lt;/code&gt; feature (it isn&amp;rsquo;t in the default set, so
you opt into it), you also get a ready-made &lt;code&gt;mytool config validate&lt;/code&gt; command that
runs these checks without you wiring anything into a command at all. Either way,
the principle holds: the program already knows what good config looks like, so
make it say so when the config is bad.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;Your &lt;code&gt;hello&lt;/code&gt; command now reads a real setting, ships a sensible default that
&lt;code&gt;init&lt;/code&gt; writes into place, honours overrides from the environment and from layered
files in a predictable order, and refuses to run on a value it doesn&amp;rsquo;t understand.
That&amp;rsquo;s most of what configuration ever needs to be, and you wrote almost none of
the machinery.&lt;/p&gt;
&lt;p&gt;One thing I&amp;rsquo;ve skipped: config can also reload while the tool is running, so a
long-lived process picks up a changed file without a restart. That&amp;rsquo;s its own
capability with its own moving parts, and I pulled it apart in
&lt;a class="link" href="https://blog-570662.gitlab.io/reloading-config-without-a-restart/" &gt;reloading config without a restart&lt;/a&gt;
if you need it.&lt;/p&gt;
&lt;p&gt;Next part, we give the tool something to do with all this config: we turn it into
an AI tool, with a chat command and an MCP server. Until then, go add a couple of
validated settings to your own commands. You&amp;rsquo;ve got the shape of it now.&lt;/p&gt;</description></item><item><title>Moving this blog off Jekyll</title><link>https://blog-570662.gitlab.io/moving-this-blog-off-jekyll/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/moving-this-blog-off-jekyll/</guid><description>&lt;img src="https://blog-570662.gitlab.io/moving-this-blog-off-jekyll/cover-moving-this-blog-off-jekyll.png" alt="Featured image of post Moving this blog off Jekyll" /&gt;&lt;p&gt;The blog you&amp;rsquo;re reading used to be a Jekyll site on GitHub Pages, built on the lovely &lt;a class="link" href="https://github.com/daattali/beautiful-jekyll" target="_blank" rel="noopener"
 &gt;beautiful-jekyll&lt;/a&gt; theme. It isn&amp;rsquo;t any more: it&amp;rsquo;s Hugo now, published to GitLab Pages. The hosting move rode along with go-tool-base &lt;a class="link" href="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/" &gt;leaving GitHub for GitLab&lt;/a&gt;, but dropping Jekyll for Hugo was its own decision, and the more interesting one. Most of the migration was painless. Two bits were not, and they&amp;rsquo;re the two bits worth writing down.&lt;/p&gt;
&lt;h2 id="why-leave-jekyll"&gt;Why leave Jekyll
&lt;/h2&gt;&lt;p&gt;Jekyll hadn&amp;rsquo;t done anything wrong, exactly. beautiful-jekyll is a genuinely nice theme and the site worked fine for years. But it had started to show its age. It hadn&amp;rsquo;t seen much improvement in a long while, and keeping it building meant staying on older versions of Ruby. Ruby is a perfectly good language, just never one I&amp;rsquo;ve much enjoyed living in, and I could feel one of those fork-it-and-drag-it-up-to-date afternoons coming, the kind I&amp;rsquo;d done before and didn&amp;rsquo;t fancy repeating.&lt;/p&gt;
&lt;p&gt;So rather than patch up what I had, I asked the more interesting question: what else is out there? It came down to a shortlist of two, Astro and Hugo. Hugo won, fairly narrowly. Partly I just liked more of its out-of-the-box themes. And partly because it&amp;rsquo;s written in Go: one portable binary, no toolchain to wrangle, the sort of thing an engineer can drop onto any machine and run without a second thought.&lt;/p&gt;
&lt;h2 id="the-day-every-image-on-the-blog-tripled"&gt;The day every image on the blog tripled
&lt;/h2&gt;&lt;p&gt;The first proper snag was about where images live. Hugo would happily have let me keep Jekyll&amp;rsquo;s arrangement, one big &lt;code&gt;/assets/images/&lt;/code&gt; folder with every post linking into it by absolute path. But I&amp;rsquo;d picked the Stack theme, and Stack leans towards &lt;em&gt;page bundles&lt;/em&gt;: each post is a directory, and the post&amp;rsquo;s own images sit right next to its &lt;code&gt;index.md&lt;/code&gt;, referenced by plain relative name. The cover image becomes a resource of the post rather than a file in a shared bucket.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a better model, and I decided to commit to it. Getting there, I managed to make a proper mess. The migration copied the old &lt;code&gt;assets/images&lt;/code&gt; across, and Hugo&amp;rsquo;s &lt;code&gt;static/&lt;/code&gt; directory wanted a copy too, and then I started moving covers into the bundles, and at one humbling point a count turned up &lt;em&gt;every image existing three times&lt;/em&gt;: once in &lt;code&gt;assets/images&lt;/code&gt;, once under &lt;code&gt;static/&lt;/code&gt;, and once in a bundle. A blog with a hundred-odd images had become a blog with three hundred-odd, most of them duplicates nobody referenced.&lt;/p&gt;
&lt;p&gt;The fix was to go all the way to the bundle model: move each post&amp;rsquo;s images into its own directory, rewrite the references from absolute &lt;code&gt;/assets/images/x.png&lt;/code&gt; paths to bare &lt;code&gt;x.png&lt;/code&gt;, and delete the two shared piles entirely. Once the images lived &lt;em&gt;with&lt;/em&gt; the posts, there was exactly one copy of each and the path was obvious. But for an afternoon the repository was a hall of mirrors, and the lesson was to pick the new tool&amp;rsquo;s model and go all the way to it, rather than carrying the old one alongside it and ending up with both.&lt;/p&gt;
&lt;h2 id="the-extended-image-that-wasnt-new-enough"&gt;The &amp;ldquo;extended&amp;rdquo; image that wasn&amp;rsquo;t new enough
&lt;/h2&gt;&lt;p&gt;The second one cost me the most time, and it&amp;rsquo;s the most transferable, so it gets the most words.&lt;/p&gt;
&lt;p&gt;Hugo comes in two flavours, ordinary and &lt;em&gt;extended&lt;/em&gt;, and the Stack theme needs extended because it compiles SCSS. So I reached for an off-the-shelf extended Hugo container image, wired it into the pipeline, and watched the build fail with an error about a template function the theme was calling that simply didn&amp;rsquo;t seem to exist.&lt;/p&gt;
&lt;p&gt;I spent far too long suspecting the theme, my config, my content. The actual culprit was a version. The image I&amp;rsquo;d grabbed was a couple of minor releases behind, and Stack v4 uses &lt;code&gt;.Site.Language.Locale&lt;/code&gt;, a Hugo feature that only landed in 0.157. The image was older than that, so the function genuinely wasn&amp;rsquo;t there, and the error was telling me the literal truth in a way I wasn&amp;rsquo;t ready to hear. &amp;ldquo;Extended&amp;rdquo; had told me the &lt;em&gt;flavour&lt;/em&gt; was right and lulled me into not checking the &lt;em&gt;version&lt;/em&gt;, which was the thing that actually mattered.&lt;/p&gt;
&lt;p&gt;The fix was to pin a specific, recent extended image rather than trusting a floating &amp;ldquo;extended&amp;rdquo; tag to be new enough. The pipeline now runs on a pinned &lt;code&gt;hugomods/hugo:debian-git-0.161.1&lt;/code&gt;, comfortably past the 0.157 the theme needs, and the build that had been failing on a missing function went green the moment the version was right. A theme has a minimum Hugo version the same way any dependency has a minimum, and &amp;ldquo;extended&amp;rdquo; is a feature flag, not a version number.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;Moving this blog from Jekyll to Hugo, and from GitHub Pages to GitLab Pages on the way, was mostly a pleasant afternoon, with two frustrations worth sharing. Commit fully to your theme&amp;rsquo;s page-bundle model rather than dragging Jekyll&amp;rsquo;s shared-assets layout along beside it, or you&amp;rsquo;ll briefly own three copies of every image. And pin your Hugo version explicitly, because a theme needs a &lt;em&gt;recent enough&lt;/em&gt; Hugo, and the &amp;ldquo;extended&amp;rdquo; label tells you nothing at all about whether yours is.&lt;/p&gt;
&lt;p&gt;If the site renders for you now, both got sorted. If it doesn&amp;rsquo;t, well, you&amp;rsquo;re reading this in a text editor, and I&amp;rsquo;ve some more debugging to do.&lt;/p&gt;</description></item><item><title>Building a CLI with go-tool-base, part 1: scaffold and your first command</title><link>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/</guid><description>&lt;img src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/cover-building-a-cli-with-go-tool-base-part-1.png" alt="Featured image of post Building a CLI with go-tool-base, part 1: scaffold and your first command" /&gt;&lt;p&gt;Every time I start a new Go CLI, the first hour goes the same way, and none of
it is the actual tool. Config loading. A logger. An update command. An error
path that prints something a human can act on. A help system. I built
go-tool-base so I&amp;rsquo;d never write that hour again, and I&amp;rsquo;ve spent a good few posts
explaining how the pieces work inside. This series is the other half: how &lt;em&gt;you&lt;/em&gt;
use it. By the end you&amp;rsquo;ll have a real CLI with all that wiring for free. This
part scaffolds one and gives it its first command.&lt;/p&gt;
&lt;p&gt;One note on shape before we start: each part stands on its own. Finish this one
and you&amp;rsquo;ve got a working, buildable tool. Later parts add configuration, AI,
self-update and telemetry, one at a time. Where you want to know how a piece
works underneath, I&amp;rsquo;ll link the deep-dive as we go.&lt;/p&gt;
&lt;h2 id="install-the-gtb-cli"&gt;Install the gtb CLI
&lt;/h2&gt;&lt;p&gt;go-tool-base ships an automation CLI called &lt;code&gt;gtb&lt;/code&gt;. Install it with the script
from the &lt;a class="link" href="https://gtb.phpboyscout.uk/installation/" target="_blank" rel="noopener"
 &gt;installation docs&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sSL &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;https://gitlab.com/phpboyscout/go-tool-base/-/raw/main/install.sh&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; bash
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That fetches a pre-built release, embedded docs and all, and drops &lt;code&gt;gtb&lt;/code&gt; in
&lt;code&gt;~/.local/bin&lt;/code&gt;, so make sure that&amp;rsquo;s on your &lt;code&gt;$PATH&lt;/code&gt;. Then check it&amp;rsquo;s there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb version
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One thing to get out of the way before we build: versions. As I write this,
&lt;code&gt;gtb version&lt;/code&gt; prints &lt;strong&gt;go-tool-base v0.6.0&lt;/strong&gt;, and that&amp;rsquo;s what every command and
snippet in this series is verified against. It&amp;rsquo;s a young tool that&amp;rsquo;s still moving
quickly, and the install script always pulls the latest release, so if you&amp;rsquo;re
reading this later and something doesn&amp;rsquo;t line up, a newer version is the likeliest
reason. When a release changes something that matters to this series, I&amp;rsquo;ll cover
it in a follow-up.&lt;/p&gt;
&lt;h2 id="scaffold-a-project"&gt;Scaffold a project
&lt;/h2&gt;&lt;p&gt;One command stands up a whole project, and &lt;code&gt;gtb&lt;/code&gt; gives you two ways to drive it.&lt;/p&gt;
&lt;p&gt;The direct way, with flags, is good for scripting and for repeating a setup
exactly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate project &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --name mytool &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --repo myorg/mytool &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --description &lt;span class="s2"&gt;&amp;#34;My CLI tool&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --env-prefix MYTOOL &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --path ./mytool
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--env-prefix&lt;/code&gt; is worth setting now: it&amp;rsquo;s the prefix for the environment
variables that can override your config later (so &lt;code&gt;MYTOOL_LOG_LEVEL&lt;/code&gt; rather than
a bare &lt;code&gt;LOG_LEVEL&lt;/code&gt; that would clash with every other tool on the box). The
wizard defaults it to your tool&amp;rsquo;s name in capitals; with flags it&amp;rsquo;s worth being
explicit. We&amp;rsquo;ll lean on it in part 2.&lt;/p&gt;
&lt;p&gt;(&lt;code&gt;gtb generate cli&lt;/code&gt; is the same command, if you prefer that name.) Or leave the
flags off and &lt;code&gt;gtb&lt;/code&gt; walks you through an interactive prompt instead, which is
the gentler way the first time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate project
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img alt="Scaffolding a new project with the interactive gtb generate wizard" class="gallery-image" data-flex-basis="360px" data-flex-grow="150" height="800" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-generate.gif" srcset="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-generate_hu_6ec09a7d04db0750.gif 800w, https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-generate.gif 1200w" width="1200"&gt;&lt;/p&gt;
&lt;p&gt;Either way, one of the choices is worth calling out now, because it explains
something you&amp;rsquo;ll see in a minute: &lt;strong&gt;features&lt;/strong&gt;. go-tool-base bundles a set of
ready-made commands, self-update, embedded docs, a &lt;code&gt;doctor&lt;/code&gt; health check, an
MCP server, a changelog, OS-keychain storage, and you choose which ones your
tool ships with at generation time, either through the &lt;code&gt;--features&lt;/code&gt; flag or a
checklist in the wizard. The default set is a sensible starting point, and you
can add or drop features later. That is why, a moment from now, your brand-new
tool already answers &lt;code&gt;--help&lt;/code&gt; with commands you never wrote. The full flag list
is in the &lt;a class="link" href="https://gtb.phpboyscout.uk/cli/skeleton/" target="_blank" rel="noopener"
 &gt;generate reference&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-you-just-got"&gt;What you just got
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cd mytool&lt;/code&gt; and look around. It&amp;rsquo;s a complete, releasable project, not a
hello-world:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── cmd/mytool/main.go # entry point
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── pkg/cmd/root/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ ├── cmd.go # builds Props, wires the root command
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └── assets/init/config.yaml # embedded default config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── internal/version/version.go # version info, stamped at release
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .gtb/manifest.yaml # the generator&amp;#39;s record of your command tree
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── .github/workflows/ # lint, test, docs, release pipelines
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── justfile # build / test / lint / docs tasks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── go.mod # with `go tool` deps pinned
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── ... # .golangci.yaml, .goreleaser.yaml, README, CHANGELOG
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One file there is worth understanding before anything else: &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;.
It is the generator&amp;rsquo;s source of truth, a record of every command your tool has,
how they nest, and a content hash of each generated file. You won&amp;rsquo;t edit it by
hand, but &lt;code&gt;gtb&lt;/code&gt; reads and rewrites it constantly. It is how the generator knows
what your command tree looks like, and how it can tell whether you have changed
a file it owns. Think of it as the map the generator builds from: it&amp;rsquo;s committed
to git for you, and as long as it&amp;rsquo;s there, your tool&amp;rsquo;s structure stays
reproducible. We&amp;rsquo;ll see it earn its keep when we regenerate.&lt;/p&gt;
&lt;p&gt;The entry point, by contrast, is tiny, because the framework does the lifting.
Here&amp;rsquo;s the generated &lt;code&gt;cmd/mytool/main.go&lt;/code&gt; in full:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Code generated by gtb. DO NOT EDIT.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool/internal/version&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/cmd/root&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool/pkg/cmd/root&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;rootCmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewCmdRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbRoot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootCmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two lines of body. &lt;code&gt;root.NewCmdRoot&lt;/code&gt; (in your &lt;code&gt;pkg/cmd/root/cmd.go&lt;/code&gt;) builds a
&lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;&lt;code&gt;Props&lt;/code&gt;&lt;/a&gt;,
the container that carries the logger, config, filesystem and version to every
command. &lt;code&gt;gtbRoot.Execute&lt;/code&gt; runs it and routes any failure through one
&lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;consistent error handler&lt;/a&gt;,
so there&amp;rsquo;s no &lt;code&gt;os.Exit&lt;/code&gt; scattered about. Note the &lt;code&gt;DO NOT EDIT&lt;/code&gt; header: &lt;code&gt;main.go&lt;/code&gt;
and the root &lt;code&gt;cmd.go&lt;/code&gt; belong to the generator. Your code goes elsewhere, which
matters in a minute.&lt;/p&gt;
&lt;p&gt;Build it and you already have a working CLI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;just build &lt;span class="c1"&gt;# or: go build -o bin/mytool ./cmd/mytool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool --help
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll see the built-in commands from the features you picked, update, docs,
doctor and the rest, with not a line written by you.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s one step before those commands will actually run. Try one, say
&lt;code&gt;./bin/mytool docs&lt;/code&gt;, and the tool stops with &lt;code&gt;please run init&lt;/code&gt;: it has no
configuration yet and won&amp;rsquo;t guess at one. So give it some:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That writes &lt;code&gt;~/.mytool/config.yaml&lt;/code&gt; from the defaults your tool ships with, and
now its commands run. (&lt;code&gt;init&lt;/code&gt; is itself one of the features. You can switch it
off for a tool that should run straight from its built-in defaults with no file
at all, but leave it on for now.) Part 2 takes configuration apart properly; for
now, &lt;code&gt;init&lt;/code&gt; once and carry on.&lt;/p&gt;
&lt;h2 id="add-your-first-command"&gt;Add your first command
&lt;/h2&gt;&lt;p&gt;Don&amp;rsquo;t hand-roll a command file. &lt;code&gt;gtb&lt;/code&gt; generates the boilerplate and leaves you
the logic:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb generate &lt;span class="nb"&gt;command&lt;/span&gt; --name hello --short &lt;span class="s2"&gt;&amp;#34;Say hello&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img alt="Generating a command and running it" class="gallery-image" data-flex-basis="450px" data-flex-grow="187" height="640" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-command.gif" srcset="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-command_hu_907301427689d5cc.gif 800w, https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-command.gif 1200w" width="1200"&gt;&lt;/p&gt;
&lt;p&gt;That creates two files (see the
&lt;a class="link" href="https://gtb.phpboyscout.uk/cli/command/" target="_blank" rel="noopener"
 &gt;command reference&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pkg/cmd/hello/cmd.go&lt;/code&gt; (generated, &lt;code&gt;DO NOT EDIT&lt;/code&gt;): the options struct, flag
wiring, and the &lt;code&gt;NewCmdHello(props *props.Props)&lt;/code&gt; constructor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pkg/cmd/hello/main.go&lt;/code&gt; (yours): a &lt;code&gt;RunHello&lt;/code&gt; function, where all your real
business logic goes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The split is the whole point. Open &lt;code&gt;pkg/cmd/hello/main.go&lt;/code&gt; and write what the
command does:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunHello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;HelloOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;hello from mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Rebuild, and the command is wired into the tree:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;just build
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./bin/mytool hello
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You never touched the root command to register it. &lt;code&gt;gtb&lt;/code&gt; recorded &lt;code&gt;hello&lt;/code&gt; in
that &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt; and wired it in for you. (If you&amp;rsquo;d rather wire commands
by hand against the library directly, the
&lt;a class="link" href="https://gtb.phpboyscout.uk/how-to/custom-commands/" target="_blank" rel="noopener"
 &gt;custom-commands how-to&lt;/a&gt;
shows that path; the generated route is the one this series follows.)&lt;/p&gt;
&lt;h2 id="regenerate-without-losing-your-work"&gt;Regenerate without losing your work
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the bit people are right to be wary of. If the generator owns &lt;code&gt;cmd.go&lt;/code&gt;
and the root wiring, what happens when it runs again, after you&amp;rsquo;ve made changes?
And it runs often: every &lt;code&gt;gtb generate command&lt;/code&gt; rebuilds the wiring.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gtb regenerate project
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Your edits survive, and not by luck. Three separate things protect them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Your logic sits in a file the generator never rewrites.&lt;/strong&gt; Command &lt;em&gt;logic&lt;/em&gt;
lives in &lt;code&gt;main.go&lt;/code&gt;; only the boilerplate &lt;code&gt;cmd.go&lt;/code&gt; is regenerated. The split
isn&amp;rsquo;t cosmetic, it&amp;rsquo;s the contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It notices if you edited a generated file.&lt;/strong&gt; That manifest stores a content
hash of every generated file, so if you&amp;rsquo;ve changed one, regeneration stops
and asks before overwriting rather than silently stamping over you.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can fence files off entirely.&lt;/strong&gt; A gitignore-style &lt;code&gt;.gtb/ignore&lt;/code&gt; tells
the generator to leave specific paths alone, even under &lt;code&gt;--force&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I wrote up
&lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;how that edit-preserving diff actually works&lt;/a&gt;
if you want the mechanism; the
&lt;a class="link" href="https://gtb.phpboyscout.uk/cli/regenerate/" target="_blank" rel="noopener"
 &gt;regenerate reference&lt;/a&gt; has the
flags. For now, the thing to trust: scaffolding here is not a one-way door. You
keep regenerating as the tool grows, and your edits stay put.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Editing a command, regenerating, and the edit surviving" class="gallery-image" data-flex-basis="450px" data-flex-grow="187" height="640" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-regenerate.gif" srcset="https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-regenerate_hu_a81df1da404dd4f8.gif 800w, https://blog-570662.gitlab.io/building-a-cli-with-go-tool-base-part-1/demo-regenerate.gif 1200w" width="1200"&gt;&lt;/p&gt;
&lt;h2 id="where-this-leaves-you"&gt;Where this leaves you
&lt;/h2&gt;&lt;p&gt;A few minutes in, you have a real CLI: config, logging, a consistent error
path, self-update, embedded docs and a release pipeline, none of it written by
you, plus your own &lt;code&gt;hello&lt;/code&gt; command and the confidence to regenerate without
fear. That&amp;rsquo;s the head start go-tool-base exists to give.&lt;/p&gt;
&lt;p&gt;Next part: configuration. Typed settings, defaults the tool ships with, and how
to turn a misspelled config key from a silent shrug into an error that tells you
what you got wrong rather than a mystery you debug at 2am. Until then, go add a
few more commands. You&amp;rsquo;ve got the pattern now.&lt;/p&gt;</description></item><item><title>Two bugs that taught me the rules</title><link>https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/cover-two-bugs-that-taught-me-the-rules.png" alt="Featured image of post Two bugs that taught me the rules" /&gt;&lt;p&gt;Some bugs are interesting because they&amp;rsquo;re subtle. These two were interesting because they were the exact opposite&amp;hellip; in each case the tool had a hard rule I simply didn&amp;rsquo;t know about, and its error message couldn&amp;rsquo;t be bothered to tell me what that rule was. Both came out of building the infrastructure toolchain, both cost me a good deal more time than they had any right to, and both are the sort of thing that looks blindingly obvious the moment you know it and utterly baffling until you do.&lt;/p&gt;
&lt;p&gt;So here they are, written down, partly to save you the bother and partly so I don&amp;rsquo;t go and forget them myself.&lt;/p&gt;
&lt;h2 id="bug-one-the-rule-less-job-that-skips-your-merge-requests"&gt;Bug one: the rule-less job that skips your merge requests
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;cicd&lt;/code&gt; gate components, in their first cut, shipped with no &lt;code&gt;rules:&lt;/code&gt; block. They were dead simple jobs: lint, scan, validate. No conditions, because they should just always run. Obviously.&lt;/p&gt;
&lt;p&gt;They ran on branch pipelines. On merge requests, they didn&amp;rsquo;t run at all! The gates that were the entire point of the components were simply absent from the one place you&amp;rsquo;d most want to see them&amp;hellip; the merge request.&lt;/p&gt;
&lt;p&gt;The cause is a GitLab CI rule that&amp;rsquo;s remarkably easy to go years without ever learning: a job with no &lt;code&gt;rules:&lt;/code&gt; block runs only on branch and tag pipelines. It does not run on merge-request pipelines. So &amp;ldquo;no conditions&amp;rdquo; doesn&amp;rsquo;t mean &amp;ldquo;runs everywhere&amp;rdquo; at all. It means &amp;ldquo;runs everywhere except a merge request&amp;rdquo;, which is about the least intuitive default I can think of.&lt;/p&gt;
&lt;p&gt;The fix is faintly absurd, and that&amp;rsquo;s exactly what makes it stick. You add an &lt;em&gt;unconditional&lt;/em&gt; rule: &lt;a class="link" href="https://gitlab.com/phpboyscout/cicd/-/blob/v0.5.0/templates/tofu-lint.yml#L38" target="_blank" rel="noopener"
 &gt;&lt;code&gt;rules: [{ when: on_success }]&lt;/code&gt;&lt;/a&gt;. The content of that rule does precisely nothing. It always matches. What actually matters is that the job now &lt;em&gt;has&lt;/em&gt; a &lt;code&gt;rules:&lt;/code&gt; block at all, because merely having one is what makes a job eligible for merge-request pipelines. A rule whose content is meaningless, added solely so the block exists. That&amp;rsquo;s the fix. I&amp;rsquo;ll admit I stared at it for a moment.&lt;/p&gt;
&lt;h2 id="bug-two-the-import-block-that-only-works-at-the-root"&gt;Bug two: the import block that only works at the root
&lt;/h2&gt;&lt;p&gt;The second one came from &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt;. The &lt;code&gt;account-hardening&lt;/code&gt; module needed to adopt a resource that already existed in the account, which is exactly what OpenTofu&amp;rsquo;s &lt;code&gt;import {}&lt;/code&gt; block is for. So an &lt;code&gt;import&lt;/code&gt; block went into the &lt;code&gt;account-hardening&lt;/code&gt; module, right next to the resource it was adopting. The natural home for it, surely.&lt;/p&gt;
&lt;p&gt;OpenTofu disagreed, and rejected it outright. The rule: an &lt;code&gt;import&lt;/code&gt; block is only allowed in the &lt;em&gt;root&lt;/em&gt; module. It can&amp;rsquo;t live inside a child module. A module that wants one of its own resources imported can&amp;rsquo;t declare that import itself&amp;hellip; the import has to be declared up at the root, and the root caller does the adopting.&lt;/p&gt;
&lt;p&gt;The fix was to take the &lt;code&gt;import&lt;/code&gt; block out of the module and document caller-side adoption instead. The module describes the resource, and the root configuration that calls the module is where the &lt;code&gt;import&lt;/code&gt; actually lives.&lt;/p&gt;
&lt;h2 id="the-shape-they-share"&gt;The shape they share
&lt;/h2&gt;&lt;p&gt;Two unrelated bugs, in two completely different tools, and the same shape sitting underneath both of them.&lt;/p&gt;
&lt;p&gt;In each case the tool has a hard structural rule. Where a block is allowed to live. What makes a job eligible for a particular kind of pipeline. And in each case the error told me the tool was unhappy without telling me &lt;em&gt;which&lt;/em&gt; rule I&amp;rsquo;d broken, so the obvious next move (debugging my own logic) was the wrong move entirely. There was nothing wrong with the logic. The thing was simply in a place the tool doesn&amp;rsquo;t allow, or missing a block the tool quietly insists on.&lt;/p&gt;
&lt;p&gt;The lasting lesson here isn&amp;rsquo;t the two specific rules, useful as they are to know. It&amp;rsquo;s the reflex. When something that should obviously work just doesn&amp;rsquo;t, and the error is unhelpful, stop debugging your logic and start suspecting a structural rule about &lt;em&gt;where&lt;/em&gt; something is allowed to be, or &lt;em&gt;whether&lt;/em&gt; a thing is eligible in the first place. GitLab CI and OpenTofu both have a handful of these, and you mostly learn them the hard way, by tripping over them. Knowing the shape of the category at least means the next one costs you an hour instead of a whole afternoon.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;Two bugs from building the toolchain, one shape. A GitLab CI job with no &lt;code&gt;rules:&lt;/code&gt; block runs on branches and tags but silently not on merge requests, and the fix is an unconditional &lt;code&gt;rules:&lt;/code&gt; block whose content does nothing and whose mere existence is the entire point. An OpenTofu &lt;code&gt;import&lt;/code&gt; block gets rejected inside a child module, because imports are only legal at the root, so the caller adopts and the module just describes.&lt;/p&gt;
&lt;p&gt;Neither error named the rule it was enforcing, and that&amp;rsquo;s the category to watch for. When sound logic fails against an unhelpful error, suspect a structural rule about where a thing may live or whether it&amp;rsquo;s even eligible&amp;hellip; not a bug in what you actually wrote. It&amp;rsquo;ll save you an afternoon. It certainly cost me a couple.&lt;/p&gt;</description></item><item><title>Reviewed, then applied</title><link>https://blog-570662.gitlab.io/reviewed-then-applied/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/reviewed-then-applied/</guid><description>&lt;img src="https://blog-570662.gitlab.io/reviewed-then-applied/cover-reviewed-then-applied.png" alt="Featured image of post Reviewed, then applied" /&gt;&lt;p&gt;The genuinely dangerous moment in infrastructure-as-code isn&amp;rsquo;t the apply. It&amp;rsquo;s the gap between the plan a human read and approved, and the change that actually runs a moment later. If those two are different computations (and by default they are) then nobody really reviewed the thing that touched your account. The &lt;code&gt;infra&lt;/code&gt; repo closes that gap from both ends.&lt;/p&gt;
&lt;h2 id="the-gap-between-reviewed-and-ran"&gt;The gap between &amp;ldquo;reviewed&amp;rdquo; and &amp;ldquo;ran&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the moment in infrastructure-as-code where things go wrong.&lt;/p&gt;
&lt;p&gt;Someone opens a merge request. CI runs &lt;code&gt;tofu plan&lt;/code&gt; and the output is there to review: these three resources change, this one is destroyed. A human reads it, decides it&amp;rsquo;s correct, approves, merges. Then &lt;code&gt;apply&lt;/code&gt; runs.&lt;/p&gt;
&lt;p&gt;The trap is in what &lt;code&gt;apply&lt;/code&gt; actually applies. If &lt;code&gt;apply&lt;/code&gt; does its own fresh &lt;code&gt;tofu plan&lt;/code&gt; and then applies &lt;em&gt;that&lt;/em&gt;, the change that runs is not necessarily the change that was reviewed. State can have moved. A provider can have drifted. Someone else can have applied something in between. The reviewed plan and the applied change are two separate computations done at two different moments, and every difference between those moments is a change nobody looked at.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; closes that gap from both ends.&lt;/p&gt;
&lt;h2 id="plan-as-an-artifact"&gt;Plan as an artifact
&lt;/h2&gt;&lt;p&gt;The first end is making the reviewed plan and the applied plan the &lt;em&gt;same object&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a class="link" href="https://gitlab.com/phpboyscout/cicd/-/blob/v0.5.0/templates/tofu-plan.yml" target="_blank" rel="noopener"
 &gt;&lt;code&gt;tofu-plan&lt;/code&gt; component&lt;/a&gt; runs the plan and saves it. It writes &lt;code&gt;tfplan.cache&lt;/code&gt;, OpenTofu&amp;rsquo;s binary plan file, as a CI artifact. It also writes &lt;code&gt;tfplan.json&lt;/code&gt;, which GitLab renders as a plan widget right in the merge request: the add, change and destroy summary, there to review without leaving the MR.&lt;/p&gt;
&lt;p&gt;The &lt;a class="link" href="https://gitlab.com/phpboyscout/cicd/-/blob/v0.5.0/templates/tofu-apply.yml" target="_blank" rel="noopener"
 &gt;&lt;code&gt;tofu-apply&lt;/code&gt; component&lt;/a&gt; then does &lt;em&gt;not&lt;/em&gt; re-plan. It applies that saved &lt;code&gt;tfplan.cache&lt;/code&gt;. And OpenTofu itself enforces the safety net: applying a stale plan file, one captured against a state that has since moved, is rejected by the tool. So what reaches the account is provably the plan that was reviewed, or it&amp;rsquo;s nothing at all. There&amp;rsquo;s no third option where something unreviewed slips through.&lt;/p&gt;
&lt;h2 id="applying-is-a-human-decision"&gt;Applying is a human decision
&lt;/h2&gt;&lt;p&gt;The second end is &lt;em&gt;when&lt;/em&gt; apply runs.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; is trunk-based: it dropped the &lt;code&gt;develop&lt;/code&gt; branch and works on &lt;code&gt;main&lt;/code&gt;. But a naive trunk setup auto-applies every push to &lt;code&gt;main&lt;/code&gt;, which means there&amp;rsquo;s no human gate at all, just whatever the last merge happened to contain.&lt;/p&gt;
&lt;p&gt;So the gate is built explicitly. &lt;code&gt;releaser-pleaser&lt;/code&gt; keeps a release merge request open against &lt;code&gt;main&lt;/code&gt;. Ordinary merges to &lt;code&gt;main&lt;/code&gt; run plans but apply nothing. The apply happens only when a person &lt;em&gt;merges the release MR&lt;/em&gt;. Merging it cuts a release tag, and the tag pipeline is what runs &lt;code&gt;tofu-apply&lt;/code&gt;, against the plan banked by the latest &lt;code&gt;main&lt;/code&gt; pipeline.&lt;/p&gt;
&lt;p&gt;The effect is that the act of applying to the account is the deliberate, visible act of merging the release request. Nothing reaches the account because a commit landed. It reaches the account because a person decided a release should go out and merged it. (Which, after the &lt;a class="link" href="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/" &gt;accidental &lt;code&gt;v2.0.0&lt;/code&gt;&lt;/a&gt; that kicked off the whole GitLab move, is a discipline I&amp;rsquo;d freshly relearned the value of.)&lt;/p&gt;
&lt;h2 id="the-guard-on-the-gate"&gt;The guard on the gate
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one more piece, because a gate is only as good as its precondition.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;verify-main-plan&lt;/code&gt; job blocks the release MR from being mergeable unless the latest &lt;code&gt;main&lt;/code&gt; pipeline is green. You can&amp;rsquo;t cut a release, and therefore can&amp;rsquo;t apply, on top of a &lt;code&gt;main&lt;/code&gt; whose plan didn&amp;rsquo;t even succeed. The human gate has its own gate: the thing you&amp;rsquo;re about to merge has to be standing on a known-good plan before you&amp;rsquo;re allowed to merge it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;The risk in infrastructure-as-code is the gap between the plan a human reviewed and the change that runs, because a re-plan at apply time is a different computation from the one that was approved.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; closes it twice over. &lt;code&gt;tofu-plan&lt;/code&gt; saves the plan as a &lt;code&gt;tfplan.cache&lt;/code&gt; artifact and renders it as a merge-request widget; &lt;code&gt;tofu-apply&lt;/code&gt; applies that exact artifact, and OpenTofu rejects it outright if the state has moved underneath it. And applying is gated on a human merging a &lt;code&gt;releaser-pleaser&lt;/code&gt; release request, not on a push, with a &lt;code&gt;verify-main-plan&lt;/code&gt; check making sure that request can only be merged on top of a green plan. What gets applied is what was reviewed, when a person decided it should be.&lt;/p&gt;</description></item><item><title>One graph, not micro-stacks</title><link>https://blog-570662.gitlab.io/one-graph-not-micro-stacks/</link><pubDate>Sun, 17 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/one-graph-not-micro-stacks/</guid><description>&lt;img src="https://blog-570662.gitlab.io/one-graph-not-micro-stacks/cover-one-graph-not-micro-stacks.png" alt="Featured image of post One graph, not micro-stacks" /&gt;&lt;p&gt;Once an infrastructure repo has a few concerns in it (account hardening, the security baseline, the signing stack still to come) there&amp;rsquo;s a steady pressure to split them into separate stacks with separate state, and Terragrunt is right there to help you do it. The &lt;code&gt;infra&lt;/code&gt; repo keeps everything in one OpenTofu graph instead. The reason comes down to who enforces your dependency ordering: the engine, or you.&lt;/p&gt;
&lt;h2 id="the-pressure-to-split"&gt;The pressure to split
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;infra&lt;/code&gt; repo&amp;rsquo;s &lt;code&gt;src/&lt;/code&gt; has several concerns in it, and more coming, the signing stack among them. Once a repo reaches that point, there&amp;rsquo;s a steady pressure to split: one stack per concern, each with its own state file.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s an appealing pressure. Separate stacks feel modular. Each &lt;code&gt;apply&lt;/code&gt; touches less, so the blast radius of any one run is smaller. And Terragrunt exists, popular and well-regarded, precisely to orchestrate a fleet of separate stacks. The path is well trodden.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; didn&amp;rsquo;t take it. &lt;code&gt;src/&lt;/code&gt; is a single OpenTofu root stack: each concern is a &lt;code&gt;module&lt;/code&gt; block, in its own &lt;code&gt;main.&amp;lt;concern&amp;gt;.tf&lt;/code&gt; file, all sharing one state and one graph.&lt;/p&gt;
&lt;h2 id="what-one-graph-gives-you"&gt;What one graph gives you
&lt;/h2&gt;&lt;p&gt;The thing a single graph gives you is engine-enforced truth about ordering and data.&lt;/p&gt;
&lt;p&gt;Inside one OpenTofu graph, the tool builds the full dependency DAG itself. When the signing stack needs a value the security baseline produced, you reference it directly, &lt;code&gt;module.baseline.something&lt;/code&gt;, and OpenTofu &lt;em&gt;guarantees&lt;/em&gt; two things: the baseline is created before the thing that depends on it, and the value handed across is the current one from this same apply. Ordering and data-passing aren&amp;rsquo;t things you arranged. They&amp;rsquo;re facts the engine checks and enforces, every plan, every apply.&lt;/p&gt;
&lt;h2 id="what-splitting-costs"&gt;What splitting costs
&lt;/h2&gt;&lt;p&gt;Split &lt;code&gt;src/&lt;/code&gt; into per-concern stacks with separate state, and that guarantee is the thing you spend.&lt;/p&gt;
&lt;p&gt;Now one stack reads another&amp;rsquo;s outputs through &lt;code&gt;terraform_remote_state&lt;/code&gt;. That&amp;rsquo;s a lookup of a &lt;em&gt;snapshot&lt;/em&gt;: the other stack&amp;rsquo;s last applied state, whatever it was, whenever that was. It&amp;rsquo;s not a live edge in a graph. Ordering is no longer enforced by the engine either; it becomes something you arrange yourself, in CI stage sequencing or in Terragrunt&amp;rsquo;s own dependency blocks.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the trade, stated plainly. You give up a strong, engine-checked guarantee, and you buy back a weaker, hand-arranged imitation of it. Terragrunt is a good tool for managing that weaker world tidily. But the question worth asking first is whether you should be in the weaker world at all.&lt;/p&gt;
&lt;h2 id="when-splitting-is-genuinely-right"&gt;When splitting is genuinely right
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t an argument that splitting is always wrong. Separate states genuinely earn their place when concerns have different change cadences, different access boundaries, or different teams owning them: when you actively &lt;em&gt;want&lt;/em&gt; an apply of one to be unable to touch another, and you want different people holding different state.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;infra&lt;/code&gt; has none of those. It&amp;rsquo;s a single account, a single operator, one cohesive set of concerns. The only thing splitting would buy here is a smaller per-apply blast radius, and that&amp;rsquo;s better handled by reviewing the plan before it applies, which &lt;a class="link" href="https://blog-570662.gitlab.io/reviewed-then-applied/" &gt;the next post&lt;/a&gt; is about, than by fragmenting the dependency graph. So &lt;code&gt;src/&lt;/code&gt; stays one graph, and Terragrunt was considered and deliberately not adopted.&lt;/p&gt;
&lt;h2 id="if-ordering-between-graphs-is-ever-needed"&gt;If ordering between graphs is ever needed
&lt;/h2&gt;&lt;p&gt;If &lt;code&gt;infra&lt;/code&gt; ever does genuinely need more than one stack, the plan isn&amp;rsquo;t Terragrunt. It&amp;rsquo;s to keep each stack a single strong graph internally, and to sequence the stacks with CI stages. Keep the engine-enforced guarantee where it&amp;rsquo;s strongest, inside each graph, and reach for hand-arranged ordering only at the one seam where it&amp;rsquo;s unavoidable.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;A multi-concern infrastructure repo feels like it should be split into per-concern stacks, and Terragrunt is right there to manage the result. &lt;code&gt;infra&lt;/code&gt; keeps &lt;code&gt;src/&lt;/code&gt; as one OpenTofu graph instead.&lt;/p&gt;
&lt;p&gt;Inside one graph, OpenTofu enforces dependency ordering and passes current values across module boundaries as checked facts. Split into separate states and that becomes a &lt;code&gt;terraform_remote_state&lt;/code&gt; snapshot lookup plus ordering you arrange by hand: a weaker version of what you gave up. Splitting is right when concerns have different cadences, boundaries or owners; for a single-account, single-operator repo none of that applies, so the strong guarantee is worth keeping, and Terragrunt is the tool for a problem &lt;code&gt;infra&lt;/code&gt; chose not to have.&lt;/p&gt;</description></item><item><title>CI you include, not copy</title><link>https://blog-570662.gitlab.io/ci-you-include-not-copy/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/ci-you-include-not-copy/</guid><description>&lt;img src="https://blog-570662.gitlab.io/ci-you-include-not-copy/cover-ci-you-include-not-copy.png" alt="Featured image of post CI you include, not copy" /&gt;&lt;p&gt;Every infrastructure repo runs the same CI: lint the OpenTofu, scan it, validate it, plan, apply. The first repo, you write that &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; by hand. The second, you copy it. By the third, you&amp;rsquo;ve got three copies of the same pipeline quietly drifting apart, which is the exact problem you&amp;rsquo;d never tolerate in application code. The &lt;code&gt;cicd&lt;/code&gt; repo is the fix, and it&amp;rsquo;s just the library-first instinct pointed at the pipeline.&lt;/p&gt;
&lt;h2 id="the-gitlab-ciyml-you-keep-copying"&gt;The &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; you keep copying
&lt;/h2&gt;&lt;p&gt;The infrastructure repos in this series all run the same CI gate jobs: format and validate the OpenTofu, lint it, scan it for security issues and secrets, and on the deploy side, plan and apply.&lt;/p&gt;
&lt;p&gt;The first repo, you write that &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; by hand. The second repo needs the same jobs, so you copy it. The third repo, you copy it again. Now there are three copies of the same pipeline, and they do what copies always do. They drift. A fix you make in one repo&amp;rsquo;s CI doesn&amp;rsquo;t reach the other two. A tightened scan rule lands in the repo you were working in and nowhere else. It&amp;rsquo;s the copy-paste problem, exactly as it shows up in application code, just written in YAML and therefore that bit easier to pretend isn&amp;rsquo;t code.&lt;/p&gt;
&lt;h2 id="gitlab-has-a-feature-for-exactly-this"&gt;GitLab has a feature for exactly this
&lt;/h2&gt;&lt;p&gt;GitLab CI/CD Components are the answer to that problem. A component is a reusable, versioned piece of pipeline that you publish, and other projects pull in with an &lt;code&gt;include:&lt;/code&gt; pinned to a version:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;gitlab.com/phpboyscout/cicd/tofu-lint@v0.5.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s a library import, for pipeline. The component has a defined interface, a version, and a home in GitLab&amp;rsquo;s CI/CD Catalog. A consuming repo includes it instead of carrying its own copy, and when the component improves, the consumer moves a version pin rather than re-copying YAML.&lt;/p&gt;
&lt;h2 id="why-a-monorepo-of-components"&gt;Why a monorepo of components
&lt;/h2&gt;&lt;p&gt;The &lt;a class="link" href="https://gitlab.com/phpboyscout/cicd/-/tree/v0.5.0/templates" target="_blank" rel="noopener"
 &gt;&lt;code&gt;cicd&lt;/code&gt; repo&lt;/a&gt; holds all of the components together: &lt;code&gt;tofu-lint&lt;/code&gt;, &lt;code&gt;tofu-security&lt;/code&gt;, &lt;code&gt;tofu-validate&lt;/code&gt;, &lt;code&gt;tofu-plan&lt;/code&gt;, &lt;code&gt;tofu-apply&lt;/code&gt;, and more. One project, not one project per component.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a deliberate call, and the reason is how GitLab versions things. A version is a tag, and a tag belongs to a &lt;em&gt;project&lt;/em&gt;. A component&amp;rsquo;s version is its project&amp;rsquo;s tag. So a monorepo of components, versioned together as one tag stream, is the natural unit: a consumer pins &lt;code&gt;@v0.5.0&lt;/code&gt; and gets a known-good &lt;em&gt;set&lt;/em&gt; of components that were tested together, rather than juggling a separate version for each one.&lt;/p&gt;
&lt;h2 id="authoring-discipline"&gt;Authoring discipline
&lt;/h2&gt;&lt;p&gt;A component is a file under &lt;code&gt;templates/&lt;/code&gt;, and it opens with a &lt;code&gt;spec: inputs:&lt;/code&gt; block: the typed inputs, their defaults, the component&amp;rsquo;s public interface.&lt;/p&gt;
&lt;p&gt;The discipline that keeps the library usable is that a component must be consumer-agnostic. It never hardcodes a token, and it never names a particular consumer&amp;rsquo;s variable. Inputs have sensible defaults, and a consuming repo overrides them. A component that reaches out and assumes something about the repo including it is a component that works in one repo and surprises the next. An authoring guide in the repo keeps that consistent across everyone who adds a component.&lt;/p&gt;
&lt;h2 id="the-self-test-you-cannot-fully-write"&gt;The self-test you cannot fully write
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;cicd&lt;/code&gt; repo tests its own components with a self-test pipeline. It&amp;rsquo;s worth knowing where that self-test stops.&lt;/p&gt;
&lt;p&gt;When a repo tests its own components by running them in child pipelines, GitLab masks &lt;code&gt;$CI_PIPELINE_SOURCE&lt;/code&gt; as &lt;code&gt;parent_pipeline&lt;/code&gt;. A component&amp;rsquo;s &lt;code&gt;rules:&lt;/code&gt;, which often branch on the pipeline source to behave differently for a merge request than for a branch or a tag, therefore can&amp;rsquo;t be exercised honestly by the self-test: the source they&amp;rsquo;d branch on has been flattened. The self-test covers what it can, and the component &lt;code&gt;rules:&lt;/code&gt; are, in the end, validated by real consumers using them for real. That&amp;rsquo;s a genuine limit, and naming it is better than pretending the self-test proves more than it does. (It&amp;rsquo;s also, not coincidentally, the exact &lt;code&gt;rules:&lt;/code&gt; quirk that bit me in &lt;a class="link" href="https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/" &gt;one of the two bugs&lt;/a&gt; I closed the series with.)&lt;/p&gt;
&lt;h2 id="the-same-instinct-again"&gt;The same instinct, again
&lt;/h2&gt;&lt;p&gt;This blog keeps circling the same instinct. go-tool-base exists because the same CLI scaffolding kept getting rewritten, so it was &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;extracted into a library&lt;/a&gt;. &lt;code&gt;cicd&lt;/code&gt; is that instinct pointed at the pipeline: the same gate jobs kept getting copied between repos, so they were extracted into a versioned, included library.&lt;/p&gt;
&lt;p&gt;Stop copy-pasting. Publish, version, include. It&amp;rsquo;s true for CLI code, and it turns out to be just as true for the YAML that builds and ships it.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;Every infrastructure repo needs the same CI, and copying the &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; between them produces copies that drift apart. GitLab CI/CD Components fix it: reusable, versioned pipeline that a repo &lt;code&gt;include:&lt;/code&gt;s and pins, instead of carrying its own copy.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cicd&lt;/code&gt; is a monorepo of those components, versioned together as one tag stream, because GitLab tags a project and a component&amp;rsquo;s version is its project&amp;rsquo;s tag. Components are authored consumer-agnostic, with typed &lt;code&gt;spec: inputs:&lt;/code&gt; and no hardcoded assumptions, and their &lt;code&gt;rules:&lt;/code&gt; are validated by real use because the self-test can&amp;rsquo;t see the pipeline source. It&amp;rsquo;s the library-first instinct, applied to CI: publish it once, include it everywhere, fix it in one place.&lt;/p&gt;</description></item><item><title>One image for the whole toolchain</title><link>https://blog-570662.gitlab.io/one-image-for-the-whole-toolchain/</link><pubDate>Fri, 15 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/one-image-for-the-whole-toolchain/</guid><description>&lt;img src="https://blog-570662.gitlab.io/one-image-for-the-whole-toolchain/cover-one-image-for-the-whole-toolchain.png" alt="Featured image of post One image for the whole toolchain" /&gt;&lt;p&gt;Every CI gate job across the infrastructure repos reaches for the same pile of tools: OpenTofu, tflint, trivy, checkov, gitleaks, terraform-docs, the AWS CLI. Installing that pile per job is both slow and quietly dangerous, because nothing pins it consistently. &lt;code&gt;infra-tools&lt;/code&gt; is the obvious fix (one image, one source of truth for versions), but two of its build decisions are less obvious and worth a look: it publishes with &lt;code&gt;crane&lt;/code&gt; instead of a second build, and it deliberately lets its own vulnerability scan fail.&lt;/p&gt;
&lt;h2 id="the-same-pile-of-tools-in-every-repo"&gt;The same pile of tools, in every repo
&lt;/h2&gt;&lt;p&gt;Every infrastructure repo in this series runs the same CI gate jobs: format and validate the OpenTofu, lint it, scan it for security problems and secrets, check the docs. Those jobs need a specific set of tools, and it&amp;rsquo;s the same set in every repo.&lt;/p&gt;
&lt;p&gt;Install them per job and you pay twice. You pay in time, because every pipeline downloads and installs the whole set again. And you pay in drift, because unless every repo pins every tool identically, the repos slowly diverge on which version of trivy or tflint they actually run, and a check that passes in one repo fails in another for no reason anyone can see.&lt;/p&gt;
&lt;h2 id="one-image-one-source-of-truth"&gt;One image, one source of truth
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;infra-tools&lt;/code&gt; is the answer: a single Debian-based container image with the whole toolchain baked in. Every CI job in every repo uses it with one &lt;code&gt;image:&lt;/code&gt; line.&lt;/p&gt;
&lt;p&gt;The real value isn&amp;rsquo;t the convenience. It&amp;rsquo;s that the image is the &lt;em&gt;one place&lt;/em&gt; tool versions are pinned. The Go-based tools are pinned in a &lt;code&gt;mise.toml&lt;/code&gt;. &lt;code&gt;checkov&lt;/code&gt;, which has no mise plugin, is pinned in a requirements file installed with pipx. The AWS CLI is pinned by a build argument. Three mechanisms, because the tools come from three kinds of source, but one image, and every pin wired to Renovate so a version bump arrives as a reviewable pull request. There&amp;rsquo;s exactly one answer to &amp;ldquo;what version of trivy does the toolchain use&amp;rdquo;, and it lives here.&lt;/p&gt;
&lt;h2 id="publishing-with-crane-not-a-second-build"&gt;Publishing with crane, not a second build
&lt;/h2&gt;&lt;p&gt;A build-pipeline detail that took a real bug to discover.&lt;/p&gt;
&lt;p&gt;The pipeline builds the image with kaniko, which builds images without a privileged Docker daemon, something that matters a great deal on shared CI runners. Then it scans the image, then it publishes it.&lt;/p&gt;
&lt;p&gt;The obvious way to write the publish stage is &amp;ldquo;build the image and push it&amp;rdquo;. But kaniko has no mode for &amp;ldquo;just push this tarball I already built&amp;rdquo;. A second kaniko invocation re-executes the entire Dockerfile from the top, including a second &lt;code&gt;mise install&lt;/code&gt;, which makes a fresh round of calls to GitHub&amp;rsquo;s API to fetch tools. GitHub&amp;rsquo;s anonymous API limit is low and shared by IP, so on a CI runner that second install reliably trips a &lt;code&gt;403&lt;/code&gt; rate-limit. (Yes, another &lt;code&gt;403&lt;/code&gt;. They do get everywhere.)&lt;/p&gt;
&lt;p&gt;So the publish stage doesn&amp;rsquo;t rebuild. It &lt;a class="link" href="https://gitlab.com/phpboyscout/images/infra-tools/-/blob/v0.2.0/.gitlab-ci.yml#L115" target="_blank" rel="noopener"
 &gt;uses &lt;code&gt;crane&lt;/code&gt;&lt;/a&gt; to push the exact image tarball the build stage already produced. The image is built once. And because the published bytes are the same bytes the scan stage scanned, there&amp;rsquo;s no gap between &amp;ldquo;the image we checked&amp;rdquo; and &amp;ldquo;the image we shipped&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="soft-failing-the-scanner-on-purpose"&gt;Soft-failing the scanner on purpose
&lt;/h2&gt;&lt;p&gt;The decision that looks wrong until you see the reasoning: the pipeline scans the image with trivy, and trivy is allowed to fail without failing the pipeline.&lt;/p&gt;
&lt;p&gt;A vulnerability scanner that doesn&amp;rsquo;t gate the build sounds like a scanner switched off. It isn&amp;rsquo;t. It&amp;rsquo;s a scanner pointed at something it can&amp;rsquo;t helpfully gate.&lt;/p&gt;
&lt;p&gt;The tools in the image are prebuilt Go binaries. trivy inspects them, reads the version of the Go runtime each was compiled with, and reports every known CVE in that Go runtime. Those findings are real, but they aren&amp;rsquo;t &lt;em&gt;mine&lt;/em&gt; to fix. The only fix is the upstream tool rebuilding itself against a patched Go. With seven such tools in the image, at any given moment one of them is usually a little behind on its Go version.&lt;/p&gt;
&lt;p&gt;A hard gate would mean the image becomes unpublishable whenever any single upstream lags, over a CVE in code I don&amp;rsquo;t own and can&amp;rsquo;t patch. That&amp;rsquo;s not a security control; it&amp;rsquo;s a way to be unable to ship. So the scan is &lt;code&gt;allow_failure&lt;/code&gt;. The findings stay fully visible, and the residual count is genuinely useful as a &lt;em&gt;metric&lt;/em&gt; for how far behind upstream the toolchain has drifted. It just doesn&amp;rsquo;t block shipping an image whose only &amp;ldquo;vulnerabilities&amp;rdquo; are other people&amp;rsquo;s build timelines.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;The infrastructure repos all run the same CI gate jobs, needing the same tools, so &lt;code&gt;infra-tools&lt;/code&gt; bakes the whole toolchain into one image and pins every version in one place, wired to Renovate.&lt;/p&gt;
&lt;p&gt;Two build choices are worth copying. The publish stage uses &lt;code&gt;crane&lt;/code&gt; to push the already-built, already-scanned tarball, because a second kaniko build would re-run &lt;code&gt;mise install&lt;/code&gt; and hit GitHub&amp;rsquo;s anonymous rate limit, and because pushing the scanned bytes means shipping exactly what was checked. And the trivy scan is deliberately &lt;code&gt;allow_failure&lt;/code&gt;, because it reports Go-runtime CVEs in prebuilt upstream binaries that no change to this repo can fix, so a hard gate would only make the image unshippable over someone else&amp;rsquo;s lag.&lt;/p&gt;</description></item><item><title>A 403 you can't fix in IAM</title><link>https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/cover-a-403-you-cant-fix-in-iam.png" alt="Featured image of post A 403 you can't fix in IAM" /&gt;&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/no-access-keys-in-ci/" &gt;The OIDC post&lt;/a&gt; explained the handshake that lets a GitLab pipeline deploy to AWS with no stored key. This is the story of the first time I got it wrong, and spent an afternoon fixing the wrong thing. The error was a flat 403 from AWS, and the maddening part is that no amount of editing the IAM policy was ever going to fix it.&lt;/p&gt;
&lt;h2 id="a-403-on-the-first-real-run"&gt;A 403 on the first real run
&lt;/h2&gt;&lt;p&gt;The OIDC post covered the handshake: GitLab CI mints a signed token, AWS exchanges it for short-lived credentials against a role whose trust policy names the pipeline. During the GitLab migration I wired exactly that up for the &lt;code&gt;infra&lt;/code&gt; repo, including a trust policy condition meant to let merge-request pipelines run a plan.&lt;/p&gt;
&lt;p&gt;The first merge request that should have triggered &lt;code&gt;tofu-plan&lt;/code&gt; didn&amp;rsquo;t run it. The job failed, and the error from AWS was a flat &lt;code&gt;AccessDenied&lt;/code&gt;. A 403.&lt;/p&gt;
&lt;h2 id="the-instinct-and-why-it-wastes-an-afternoon"&gt;The instinct, and why it wastes an afternoon
&lt;/h2&gt;&lt;p&gt;The instinct on an IAM 403 is immediate and almost always right: the policy&amp;rsquo;s wrong, so go and edit the policy. Tighten the condition. Loosen the condition. Check the wildcard. Re-read the &lt;code&gt;sub&lt;/code&gt; pattern character by character.&lt;/p&gt;
&lt;p&gt;All of that was wasted, and it was wasted for a reason that took me far too long to see. The trust policy wasn&amp;rsquo;t matching the &lt;em&gt;wrong&lt;/em&gt; value. It was matching a value that &lt;em&gt;does not exist&lt;/em&gt;. No amount of editing a condition makes it match a thing that&amp;rsquo;s never present.&lt;/p&gt;
&lt;h2 id="what-is-actually-in-the-token"&gt;What is actually in the token
&lt;/h2&gt;&lt;p&gt;GitLab&amp;rsquo;s OIDC token has a &lt;code&gt;sub&lt;/code&gt; claim that encodes the pipeline&amp;rsquo;s context, and part of that encoding is a &lt;code&gt;ref_type&lt;/code&gt;. I&amp;rsquo;d assumed &lt;code&gt;ref_type&lt;/code&gt; could be &lt;code&gt;branch&lt;/code&gt;, &lt;code&gt;tag&lt;/code&gt;, or &lt;code&gt;mr&lt;/code&gt;, because a pipeline can certainly be a branch pipeline, a tag pipeline, or a merge-request pipeline. So the trust policy, for the plan job, matched a &lt;code&gt;sub&lt;/code&gt; containing &lt;code&gt;ref_type:mr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That assumption was wrong. GitLab&amp;rsquo;s &lt;code&gt;ref_type&lt;/code&gt; is &lt;code&gt;branch&lt;/code&gt; or &lt;code&gt;tag&lt;/code&gt;. That&amp;rsquo;s the entire set. There is no &lt;code&gt;mr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A merge-request pipeline doesn&amp;rsquo;t run against a merge-request ref. It runs against the source &lt;em&gt;branch&lt;/em&gt;. So its token&amp;rsquo;s &lt;code&gt;sub&lt;/code&gt; carries &lt;code&gt;ref_type:branch&lt;/code&gt;, like any other branch pipeline. The trust policy condition asked for &lt;code&gt;ref_type:mr&lt;/code&gt;, GitLab never puts &lt;code&gt;mr&lt;/code&gt; in a token, the condition was therefore never true, and every merge-request pipeline got a 403. Forever, until the policy stopped asking for a claim that isn&amp;rsquo;t real.&lt;/p&gt;
&lt;h2 id="the-fix-and-the-lesson-worth-more-than-the-fix"&gt;The fix, and the lesson worth more than the fix
&lt;/h2&gt;&lt;p&gt;The fix is small once it&amp;rsquo;s visible: match &lt;code&gt;ref_type:branch&lt;/code&gt; and narrow it down by branch name or project path instead. An afternoon of policy edits, and the actual change is one word.&lt;/p&gt;
&lt;p&gt;The lesson is the part worth keeping. When an OIDC trust fails, the useful question is never &amp;ldquo;is my policy clever enough&amp;rdquo;. It&amp;rsquo;s &amp;ldquo;what&amp;rsquo;s &lt;em&gt;actually in the token&lt;/em&gt;&amp;rdquo;. An OIDC trust policy can only ever match the claims the identity provider genuinely asserts, and the gap between what a provider asserts and what you &lt;em&gt;assumed&lt;/em&gt; it asserts is precisely where this class of bug lives.&lt;/p&gt;
&lt;p&gt;So the move, when an OIDC handshake 403s, is to get hold of a real token and decode it. Look at the actual &lt;code&gt;sub&lt;/code&gt;, the actual claims, the actual values. Match what&amp;rsquo;s there. A 403 that survives every sensible edit to the policy is usually not a policy that&amp;rsquo;s too loose or too strict. It&amp;rsquo;s a policy matching a claim that was never going to be in the token.&lt;/p&gt;
&lt;h2 id="the-habit-it-left-behind"&gt;The habit it left behind
&lt;/h2&gt;&lt;p&gt;I wired an OIDC trust policy to let merge-request pipelines plan, by matching a &lt;code&gt;sub&lt;/code&gt; claim with &lt;code&gt;ref_type:mr&lt;/code&gt;. The first real merge request got a 403, and no edit to the policy fixed it, because GitLab&amp;rsquo;s &lt;code&gt;ref_type&lt;/code&gt; is only ever &lt;code&gt;branch&lt;/code&gt; or &lt;code&gt;tag&lt;/code&gt;. A merge-request pipeline runs on a branch ref, so the &lt;code&gt;mr&lt;/code&gt; value the policy demanded was never in any token.&lt;/p&gt;
&lt;p&gt;The fix was one word. The habit it left behind is the valuable bit: when an OIDC trust fails, stop editing the policy and go and read a real token. A trust policy can only match what the provider actually asserts, and &amp;ldquo;what I assumed it asserts&amp;rdquo; is where the 403 was hiding the whole time. (If this shape of bug feels familiar by the end of the series, that&amp;rsquo;s not an accident: I &lt;a class="link" href="https://blog-570662.gitlab.io/two-bugs-that-taught-me-the-rules/" &gt;come back to it&lt;/a&gt; with two more from exactly the same family.)&lt;/p&gt;</description></item><item><title>Pure-Rust Git, no git binary</title><link>https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/</guid><description>&lt;img src="https://blog-570662.gitlab.io/pure-rust-git-no-git-binary/cover-pure-rust-git-no-git-binary.png" alt="Featured image of post Pure-Rust Git, no git binary" /&gt;&lt;p&gt;go-tool-base&amp;rsquo;s VCS support has two halves that get confused for one. One half talks to forge APIs (GitHub, GitLab) for releases and pull requests. The other talks to the &lt;code&gt;.git&lt;/code&gt; directory on disk: clone, history, diff, status. This post is mostly about the second half, and specifically about a question that turns out to have three answers in Rust, only one of which I&amp;rsquo;d recommend: how do you actually &lt;em&gt;do&lt;/em&gt; Git from inside a program?&lt;/p&gt;
&lt;h2 id="a-vcs-subsystem-with-two-halves"&gt;A VCS subsystem with two halves
&lt;/h2&gt;&lt;p&gt;go-tool-base has a VCS subsystem, and it does two distinct jobs.&lt;/p&gt;
&lt;p&gt;The first is forge APIs. GitHub and GitLab, Enterprise and nested group paths included. It authenticates, lists releases, fetches release assets, manages pull requests. The self-update machinery sits on this half, and it&amp;rsquo;s what a tool uses to ask &amp;ldquo;what&amp;rsquo;s the latest release?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The second is local Git. go-tool-base also carries a &lt;code&gt;RepoLike&lt;/code&gt; object, an abstraction over an actual Git repository on disk: clone it, read its commit history, diff two trees, check its status. This half doesn&amp;rsquo;t talk to a hosting service at all. It talks to the &lt;code&gt;.git&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;It would be easy to assume the second half grew out of the first. It didn&amp;rsquo;t, and where it actually came from is the part worth telling.&lt;/p&gt;
&lt;h2 id="a-capability-ahead-of-its-consumer"&gt;A capability ahead of its consumer
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;RepoLike&lt;/code&gt; object wasn&amp;rsquo;t built for go-tool-base. It came from another project, where it had already proved itself, and it was pulled into go-tool-base on purpose, with a specific future consumer in mind: the code generator.&lt;/p&gt;
&lt;p&gt;The plan is for the generator to use Git directly. When it scaffolds a new tool, that tool should start life as a Git repository, with a &lt;code&gt;git init&lt;/code&gt; and an initial commit. When you later regenerate, the generator should diff the regenerated template output against your working tree to detect drift, the same idea as &lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;respecting your edits&lt;/a&gt;. Both of those are local Git operations, not API calls, so the generator needs a repository abstraction to call into.&lt;/p&gt;
&lt;p&gt;That wiring isn&amp;rsquo;t finished yet. The generator doesn&amp;rsquo;t drive &lt;code&gt;RepoLike&lt;/code&gt; today. But the capability is in place, deliberately, ahead of the consumer that will use it, because the alternative is bolting Git support on later under deadline pressure, and that&amp;rsquo;s how you end up with the wrong abstraction.&lt;/p&gt;
&lt;p&gt;So when rust-tool-base was built, a repository abstraction was never in question. The Rust port carries the same capability for the same reason: a &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-vcs/src/git/mod.rs#L69" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Repo&lt;/code&gt; type&lt;/a&gt; with &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;clone&lt;/code&gt;, &lt;code&gt;walk&lt;/code&gt;, &lt;code&gt;diff&lt;/code&gt;, &lt;code&gt;blame&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;checkout&lt;/code&gt;, present and ready for the generator to wire into. The open question was never &lt;em&gt;whether&lt;/em&gt; to have it. It was how to &lt;em&gt;do&lt;/em&gt; Git from inside a Rust program, and there are three answers, only one of which is any good.&lt;/p&gt;
&lt;h2 id="three-ways-to-do-git-and-the-one-worth-picking"&gt;Three ways to do Git, and the one worth picking
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Shell out to &lt;code&gt;git&lt;/code&gt;.&lt;/strong&gt; Run the &lt;code&gt;git&lt;/code&gt; binary as a subprocess and parse its output. It works until it doesn&amp;rsquo;t. The binary might not be installed. It might be a different version with different output. Its output is formatted for humans and changes between releases, so parsing it is a standing liability. You&amp;rsquo;ve made an undeclared dependency on a program you don&amp;rsquo;t ship.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Link libgit2.&lt;/strong&gt; libgit2 is the C library that reimplements Git as something you can call from code, and &lt;code&gt;git2&lt;/code&gt; is the Rust binding to it. It&amp;rsquo;s solid and widely used. But it&amp;rsquo;s a C dependency, which means a C toolchain in the build, and it&amp;rsquo;s consistently the single biggest source of cross-compilation pain in the Rust Git ecosystem. The musl builds, the Windows builds, the static linking: libgit2 is where they tend to break.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;gix&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;gix&lt;/code&gt; is a reimplementation of Git in pure Rust. No C library, no subprocess. It&amp;rsquo;s just Rust code, and it compiles and cross-compiles like any other crate, because that&amp;rsquo;s all it is. It&amp;rsquo;s also generally faster, and being pure Rust it fits the &lt;a class="link" href="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/" &gt;no-&lt;code&gt;unsafe&lt;/code&gt;-in-first-party-code&lt;/a&gt; story far more comfortably than dragging a C library along.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-vcs&lt;/code&gt; is &lt;code&gt;gix&lt;/code&gt;-first. The &lt;code&gt;Repo&lt;/code&gt; type is built on it. There&amp;rsquo;s no &lt;code&gt;git&lt;/code&gt; binary dependency, and there&amp;rsquo;s no libgit2 in a default build.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gix&lt;/code&gt; is still maturing, and a few write paths, &lt;code&gt;push&lt;/code&gt; in particular, aren&amp;rsquo;t ready in it yet. For those, &lt;code&gt;git2&lt;/code&gt; (the libgit2 binding) is held in reserve as a documented fallback, to be wired behind an opt-in Cargo feature if and when a write path actually needs it. Until then a default build carries no libgit2 at all, and the common case, a tool that clones, reads history, diffs and commits, never pays its cross-compile cost. (The &lt;code&gt;gix&lt;/code&gt; backend itself sits behind an opt-in &lt;code&gt;git&lt;/code&gt; feature, which is &lt;a class="link" href="https://blog-570662.gitlab.io/two-kinds-of-feature-flag/" &gt;exactly the feature-flag story&lt;/a&gt; from a couple of weeks back, doing real work.)&lt;/p&gt;
&lt;h2 id="repo-is-a-foundation-not-a-façade"&gt;&lt;code&gt;Repo&lt;/code&gt; is a foundation, not a façade
&lt;/h2&gt;&lt;p&gt;One design decision is worth calling out, because it came straight from a go-tool-base lesson.&lt;/p&gt;
&lt;p&gt;It would have been easy to build &lt;code&gt;Repo&lt;/code&gt; as a narrow façade exposing exactly what the scaffolder and the release-notes feature need today, and nothing else. That was rejected on purpose. go-tool-base&amp;rsquo;s &lt;code&gt;RepoLike&lt;/code&gt; is itself the cautionary tale: it arrived from another project, settled into a sensible abstraction, and is already lined up to carry a consumer, the generator, that wasn&amp;rsquo;t driving its design when it was first written. A repository abstraction gets used by code that doesn&amp;rsquo;t exist yet. Build one as a narrow façade around today&amp;rsquo;s needs and you&amp;rsquo;ve guaranteed a rewrite the first time a downstream tool wants something slightly different.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;rtb-vcs&lt;/code&gt;&amp;rsquo;s &lt;code&gt;Repo&lt;/code&gt; is built as a foundation: a sensible, reasonably complete vocabulary of Git operations that a tool author can compose richer behaviour on, without re-importing &lt;code&gt;gix&lt;/code&gt; directly and re-deriving the framework&amp;rsquo;s auth and concurrency conventions. The errors back this up. &lt;code&gt;gix&lt;/code&gt;&amp;rsquo;s error types aren&amp;rsquo;t leaked through the public API; they&amp;rsquo;re wrapped in semantic &lt;code&gt;RepoError&lt;/code&gt; variants, so the backend could be swapped, &lt;code&gt;gix&lt;/code&gt; to &lt;code&gt;git2&lt;/code&gt;, or to something else entirely, without breaking a single downstream caller.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s VCS support has two halves: forge-API calls for releases and pull requests, and a &lt;code&gt;RepoLike&lt;/code&gt; object for local Git operations. The repo half arrived from another project and is wired in ahead of its intended consumer, the code generator, which will use it to initialise repositories for scaffolded tools and to diff regenerated output for drift.&lt;/p&gt;
&lt;p&gt;rust-tool-base carries the same capability on purpose. Its &lt;code&gt;Repo&lt;/code&gt; type is built on &lt;code&gt;gix&lt;/code&gt;, a pure-Rust Git implementation, so there&amp;rsquo;s no dependency on an installed &lt;code&gt;git&lt;/code&gt; binary and no libgit2 C library in a default build, which keeps cross-compilation clean. &lt;code&gt;git2&lt;/code&gt; stays an opt-in fallback for the few write paths &lt;code&gt;gix&lt;/code&gt; can&amp;rsquo;t do yet. And &lt;code&gt;Repo&lt;/code&gt; is built as a foundation for downstream tools, with the backend wrapped behind its own error type so it can be replaced without breaking callers.&lt;/p&gt;</description></item><item><title>Routing security findings without the noise</title><link>https://blog-570662.gitlab.io/routing-security-findings-without-the-noise/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/routing-security-findings-without-the-noise/</guid><description>&lt;img src="https://blog-570662.gitlab.io/routing-security-findings-without-the-noise/cover-routing-security-findings-without-the-noise.png" alt="Featured image of post Routing security findings without the noise" /&gt;&lt;p&gt;Turning on GuardDuty and Security Hub gives you threat detection. It also gives you a firehose. And an alert system that dutifully forwards everything in that firehose isn&amp;rsquo;t monitoring, it&amp;rsquo;s a very efficient way of training your team to ignore alerts. So the &lt;code&gt;alerts&lt;/code&gt; module&amp;rsquo;s real job isn&amp;rsquo;t detection at all. It&amp;rsquo;s deciding what&amp;rsquo;s actually worth interrupting a human for, and the interesting part is everything it deliberately throws away.&lt;/p&gt;
&lt;h2 id="detection-is-the-easy-half"&gt;Detection is the easy half
&lt;/h2&gt;&lt;p&gt;Switching on threat detection in an AWS account is a few resources. GuardDuty, Security Hub with its standards, IAM Access Analyzer: &lt;a class="link" href="https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/" &gt;the security baseline&lt;/a&gt; does exactly that. From then on, the account is generating findings.&lt;/p&gt;
&lt;p&gt;And it generates a lot of them. Plenty are low-severity, informational, or simply the normal texture of a cloud account. If you wire every finding to an email or a pager, you haven&amp;rsquo;t built monitoring. You&amp;rsquo;ve built noise. And noise has a specific failure mode: people stop reading it, and the one finding that genuinely mattered scrolls past unread alongside two hundred that didn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;So the valuable work isn&amp;rsquo;t detection. It&amp;rsquo;s &lt;em&gt;routing&lt;/em&gt;: deciding what&amp;rsquo;s worth interrupting a human for, and letting the rest sit quietly in a console for whenever someone reviews it.&lt;/p&gt;
&lt;h2 id="forward-the-severe-leave-the-rest"&gt;Forward the severe, leave the rest
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;alerts&lt;/code&gt; module routes findings with EventBridge rules into an SNS topic that emails out. The rules are deliberately picky. &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/blob/v0.2.0/modules/alerts/main.tf#L146" target="_blank" rel="noopener"
 &gt;GuardDuty findings are forwarded only at severity 7 and above&lt;/a&gt;. Security Hub findings are forwarded only at HIGH and CRITICAL.&lt;/p&gt;
&lt;p&gt;Everything below those thresholds isn&amp;rsquo;t discarded. It&amp;rsquo;s still in GuardDuty and Security Hub, where someone doing a review will see it. It just doesn&amp;rsquo;t get to interrupt anyone&amp;rsquo;s day. The threshold is the line between &amp;ldquo;look at this now&amp;rdquo; and &amp;ldquo;look at this sometime&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="the-duplicate-you-would-otherwise-send-twice"&gt;The duplicate you would otherwise send twice
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the subtle one, and it&amp;rsquo;s the kind of thing you only find by looking closely at where findings come from.&lt;/p&gt;
&lt;p&gt;Security Hub is an aggregator. It pulls findings &lt;em&gt;in&lt;/em&gt; from other services, GuardDuty among them. So a single GuardDuty finding can show up in two places: in GuardDuty itself, and again in Security Hub as an aggregated copy.&lt;/p&gt;
&lt;p&gt;A rule on GuardDuty findings and a rule on Security Hub HIGH/CRITICAL findings would therefore both fire for the same underlying GuardDuty finding. One event, two emails. Do that across an account and a meaningful fraction of your alert volume is just the same findings counted twice, which is its own kind of noise.&lt;/p&gt;
&lt;p&gt;So the Security Hub rule explicitly &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/blob/v0.2.0/modules/alerts/main.tf#L178" target="_blank" rel="noopener"
 &gt;excludes findings whose &lt;code&gt;ProductName&lt;/code&gt; is GuardDuty, with an &lt;code&gt;anything-but&lt;/code&gt; match&lt;/a&gt;. GuardDuty findings come through the GuardDuty rule. The Security Hub rule handles everything Security Hub adds that GuardDuty didn&amp;rsquo;t already report. One finding, one alert, regardless of how many services it passed through.&lt;/p&gt;
&lt;h2 id="two-tripwires-on-the-root-account"&gt;Two tripwires on the root account
&lt;/h2&gt;&lt;p&gt;Findings are about threats the detectors recognise. The module adds two alarms about something simpler: the root account doing anything at all.&lt;/p&gt;
&lt;p&gt;One CloudWatch alarm fires on a root console sign-in. The other fires on any root API call that isn&amp;rsquo;t a console login. In a well-run AWS account, the root user does almost nothing after initial setup: day-to-day work happens through roles. So root activity isn&amp;rsquo;t a &amp;ldquo;finding&amp;rdquo; to be assessed for severity. It&amp;rsquo;s a tripwire. Any of it, in an account that should be silent, is worth an immediate look, and the two alarms say so directly.&lt;/p&gt;
&lt;h2 id="why-a-quiet-alert-stream-matters-here"&gt;Why a quiet alert stream matters here
&lt;/h2&gt;&lt;p&gt;This is monitoring for the account that&amp;rsquo;s going to hold the release-signing key, and that raises the stakes on getting the routing right.&lt;/p&gt;
&lt;p&gt;If a key-bearing account ever does come under attack, the alert that says so has to be &lt;em&gt;seen&lt;/em&gt;. An alert stream that&amp;rsquo;s mostly noise and duplicates is, functionally, no alerting at all, because the people who&amp;rsquo;d act on it have long since tuned it out. Routing the stream down to &amp;ldquo;severe, deduplicated, plus root tripwires&amp;rdquo; is what keeps it something a human will still read on the day it finally matters.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;GuardDuty and Security Hub make detection easy. The hard, valuable part is routing: forwarding what deserves to interrupt someone and leaving the rest in a console.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;alerts&lt;/code&gt; module forwards GuardDuty at severity 7-plus and Security Hub at HIGH/CRITICAL, and it drops the duplicate that aggregation creates by excluding GuardDuty-sourced findings from the Security Hub rule, so one finding is one alert. Two CloudWatch alarms act as tripwires on root-account activity, which should be near-zero. For the account that will hold the signing key, a quiet, trustworthy alert stream isn&amp;rsquo;t a nicety. It&amp;rsquo;s the difference between monitoring and theatre.&lt;/p&gt;</description></item><item><title>Why go-tool-base left GitHub for GitLab</title><link>https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/</guid><description>&lt;img src="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/cover-why-we-left-github-for-gitlab.png" alt="Featured image of post Why go-tool-base left GitHub for GitLab" /&gt;&lt;p&gt;A botched version bump made me stop and actually look at where go-tool-base lived, and I didn&amp;rsquo;t much like what I saw. GitHub had spent months quietly falling over, and when Mitchell Hashimoto (GitHub user #1299, no less) publicly walked Ghostty off the platform, it stopped feeling like just my problem. I&amp;rsquo;ve been a GitLab fan for years, so the move was less a leap and more an overdue nudge. This is the &lt;em&gt;why&lt;/em&gt;, not the &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="it-started-with-a-wrong-number"&gt;It started with a wrong number
&lt;/h2&gt;&lt;p&gt;Every migration has a trigger, and mine was embarrassingly small. A commit landed on &lt;code&gt;main&lt;/code&gt; carrying a &lt;code&gt;BREAKING CHANGE:&lt;/code&gt; footer it didn&amp;rsquo;t really deserve. Semantic-release did exactly what it&amp;rsquo;s told to do with that footer: it cut a major version. go-tool-base lurched from the v1 line straight to v2.0.0, and a chain of things that keyed off the version went sideways with it.&lt;/p&gt;
&lt;p&gt;It was fixable. It wasn&amp;rsquo;t a disaster. But it was the kind of small, stupid breakage that makes you stop and actually &lt;em&gt;look&lt;/em&gt; at your setup instead of just patching it and moving on. And when I looked, the version bump wasn&amp;rsquo;t the thing that bothered me. It was everything around it.&lt;/p&gt;
&lt;h2 id="the-platform-had-been-quietly-failing"&gt;The platform had been quietly failing
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;d been losing time to GitHub for months. Not dramatically. No single outage you&amp;rsquo;d write home about, just a steady drip of Actions queues that wouldn&amp;rsquo;t drain, pull requests that wouldn&amp;rsquo;t merge, the occasional morning where the thing simply wasn&amp;rsquo;t there. You absorb it. You re-run the job. You make a coffee and try again. You tell yourself it&amp;rsquo;s a blip.&lt;/p&gt;
&lt;p&gt;The trouble with a steady drip is that you stop counting it. It becomes weather.&lt;/p&gt;
&lt;h2 id="the-canary-left-the-mine"&gt;The canary left the mine
&lt;/h2&gt;&lt;p&gt;Then, in late April, &lt;a class="link" href="https://mitchellh.com/" target="_blank" rel="noopener"
 &gt;Mitchell Hashimoto&lt;/a&gt; (co-founder of HashiCorp, creator of Vagrant, Terraform and the Ghostty terminal) published &lt;a class="link" href="https://mitchellh.com/writing/ghostty-leaving-github" target="_blank" rel="noopener"
 &gt;&lt;em&gt;Ghostty Is Leaving GitHub&lt;/em&gt;&lt;/a&gt;, and &lt;em&gt;The Register&lt;/em&gt; &lt;a class="link" href="https://www.theregister.com/software/2026/04/29/mitchell-hashimoto-says-github-no-longer-for-serious-work/5227505" target="_blank" rel="noopener"
 &gt;picked it up&lt;/a&gt; a day later under the headline &amp;ldquo;GitHub &amp;rsquo;no longer a place for serious work&amp;rsquo;&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;This is not a man with a casual relationship to GitHub. He&amp;rsquo;s, by his own account, user #1299, joined February 2008. He called it &amp;ldquo;the place that has made me the most happy&amp;rdquo;. And he still wrote this:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;This is no longer a place for serious work if it just blocks you out for hours per day, every day.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The detail that landed hardest for me wasn&amp;rsquo;t a quote, it was a habit. He&amp;rsquo;d kept a journal for a month, marking an &amp;ldquo;X&amp;rdquo; on every day a GitHub outage had cost him working time. &lt;em&gt;Almost every day had an X.&lt;/em&gt; Reading that, I realised I&amp;rsquo;d been having the same month. I&amp;rsquo;d just never been disciplined enough to write it down. He&amp;rsquo;d turned my vague &amp;ldquo;it&amp;rsquo;s been flaky lately&amp;rdquo; into a row of crosses on a calendar.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I want to ship software and it doesn&amp;rsquo;t want me to ship software.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;When the person who&amp;rsquo;s been on the platform for eighteen years and &lt;em&gt;loves&lt;/em&gt; it says that out loud, it stops being your private grumble. It&amp;rsquo;s the canary, and the canary has stopped singing.&lt;/p&gt;
&lt;h2 id="why-gitlab-and-not-just-somewhere-else"&gt;Why GitLab, and not just &amp;ldquo;somewhere else&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;Being annoyed at GitHub is a reason to leave. It is not, on its own, a reason to pick a destination. The destination has to be a positive choice.&lt;/p&gt;
&lt;p&gt;For me GitLab was an easy one, because I&amp;rsquo;ve been a fan for years. Long enough, in fact, to have &lt;em&gt;also&lt;/em&gt; been a reliable grumbler about their pricing tiers, which is how you know it&amp;rsquo;s a real relationship and not a honeymoon. What I&amp;rsquo;ve always rated is the model: GitLab treats source hosting, CI/CD, the package registry, releases and Pages as &lt;em&gt;one integrated product&lt;/em&gt;, not a marketplace of bolted-on parts you assemble yourself.&lt;/p&gt;
&lt;p&gt;That integration is the actual prize. On the old setup, &amp;ldquo;CI&amp;rdquo; meant a folder of separate GitHub Actions workflow files, each pinned, each its own little world. On GitLab it&amp;rsquo;s a single &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/.gitlab-ci.yml#L7" target="_blank" rel="noopener"
 &gt;&lt;code&gt;.gitlab-ci.yml&lt;/code&gt; pipeline&lt;/a&gt; with proper stages (lint, test, security, docs, release) and the release stage talks to the built-in package registry and Pages without me wiring up a single external credential. The CI job that builds the project can authenticate to the things the project needs &lt;em&gt;because they&amp;rsquo;re the same platform&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a second-order benefit too. A migration is a rare licence to fix things you&amp;rsquo;d never otherwise touch. Moving gave me the cover to reset go-tool-base&amp;rsquo;s versioning cleanly (back to a sensible &lt;code&gt;v0.x&lt;/code&gt; line, the accidental &lt;code&gt;v2.0.0&lt;/code&gt; left behind as a cautionary tale) and to move the module path to its new home in one deliberate change rather than a thousand apologetic ones.&lt;/p&gt;
&lt;h2 id="what-im-not-going-to-claim"&gt;What I&amp;rsquo;m not going to claim
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m not going to tell you GitHub is finished, or that GitLab never has a bad day, because it does, everyone does. This isn&amp;rsquo;t a teardown. GitHub gave go-tool-base a perfectly good home for its first year, and the archived mirror is still sitting there, read-only, pointing anyone who finds it at the new place.&lt;/p&gt;
&lt;p&gt;What changed is simpler than a grand verdict. The friction crossed a line, someone I respect said the quiet part loudly enough that I couldn&amp;rsquo;t keep filing it under &amp;ldquo;weather&amp;rdquo;, and the place I&amp;rsquo;d have moved to anyway was sitting right there with a better model. Sometimes the prudent move and the move you secretly wanted turn out to be the same move, and you just need a wrong version number to give you permission.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;go-tool-base moved from GitHub to GitLab in May 2026. The proximate cause was a self-inflicted version-bump mess; the real cause was months of GitHub unreliability that I&amp;rsquo;d stopped consciously noticing until Mitchell Hashimoto&amp;rsquo;s very public departure named it for me. GitLab was a positive pick, not just an escape hatch: its integrated CI/CD, registry, releases and Pages are one product rather than a kit, and that integration is genuinely worth having. The migration also bought a clean versioning restart as a bonus.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been absorbing a steady drip of friction and telling yourself it&amp;rsquo;s normal: try the calendar trick. Mark the X&amp;rsquo;s for a month. The page will tell you something you already half-know.&lt;/p&gt;</description></item><item><title>Why I hand-rolled every module</title><link>https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/</link><pubDate>Sun, 10 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/</guid><description>&lt;img src="https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/cover-why-i-hand-rolled-every-module.png" alt="Featured image of post Why I hand-rolled every module" /&gt;&lt;p&gt;There are well-known community module libraries for AWS: Cloud Posse, the &lt;code&gt;terraform-aws-modules&lt;/code&gt; collection, plenty more. Both &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; and &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/tree/v0.2.0/modules" target="_blank" rel="noopener"
 &gt;&lt;code&gt;terraform-aws-security-baseline&lt;/code&gt;&lt;/a&gt; use almost none of them. Every sub-module is hand-rolled from raw AWS resources, and before you accuse me of not-invented-here syndrome (a perfectly fair first guess), hear me out, because the same evaluation kept landing the same way for a real reason.&lt;/p&gt;
&lt;h2 id="the-promise-of-a-wrapper-module"&gt;The promise of a wrapper module
&lt;/h2&gt;&lt;p&gt;The community module ecosystem makes an appealing offer. Don&amp;rsquo;t write raw &lt;code&gt;aws_s3_bucket&lt;/code&gt; and &lt;code&gt;aws_s3_bucket_policy&lt;/code&gt; and &lt;code&gt;aws_s3_bucket_public_access_block&lt;/code&gt; and the rest. Call a tested, popular module, pass it a handful of inputs, and get a correct, well-configured bucket. Less code in your repo, and the code you don&amp;rsquo;t write has been exercised by thousands of other users.&lt;/p&gt;
&lt;p&gt;For a lot of infrastructure that&amp;rsquo;s a genuinely good deal, and I take it often. For the two infrastructure modules in this series, I took it almost never. Every sub-module is built from raw AWS resources. That wasn&amp;rsquo;t a reflex. It was the same evaluation, made over and over, landing the same way.&lt;/p&gt;
&lt;h2 id="what-kept-going-wrong"&gt;What kept going wrong
&lt;/h2&gt;&lt;p&gt;For each place a wrapper module could have fitted, I looked at the wrapper. And the recurring finding was one of two things. Either using the wrapper &lt;em&gt;correctly&lt;/em&gt;, with all the overrides my posture needed, came to more configuration than the raw resources would have. Or the wrapper&amp;rsquo;s abstraction leaked the instant I needed something it hadn&amp;rsquo;t anticipated, and I was now writing code to fight it.&lt;/p&gt;
&lt;h2 id="the-cloudtrail-bucket-concretely"&gt;The CloudTrail bucket, concretely
&lt;/h2&gt;&lt;p&gt;The clearest example is the bucket that holds CloudTrail logs.&lt;/p&gt;
&lt;p&gt;There are popular modules that set up CloudTrail and bundle an S3 bucket for the logs. Convenient. But that bundled bucket isn&amp;rsquo;t the bucket I want. It doesn&amp;rsquo;t carry &lt;code&gt;lifecycle { prevent_destroy = true }&lt;/code&gt;, and its bucket policy is weaker than the one &lt;a class="link" href="https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/" &gt;the state bucket&lt;/a&gt; taught me to want: TLS-only, SSE-KMS-only, wrong-key-denied.&lt;/p&gt;
&lt;p&gt;So to use the wrapper I had two options. Accept a weaker audit-log bucket than the rest of the account, which rather defeats the point of an audit log. Or fight the wrapper: disable its bucket, create my own, wire it back in. Fighting the wrapper is &lt;em&gt;more&lt;/em&gt; work than simply writing the fifty-odd lines of raw &lt;code&gt;aws_s3_bucket&lt;/code&gt; plus policy that give me exactly the posture I&amp;rsquo;d already designed once. The wrapper didn&amp;rsquo;t save code. It added a negotiation.&lt;/p&gt;
&lt;h2 id="a-wrapper-is-a-deal-and-deals-have-terms"&gt;A wrapper is a deal, and deals have terms
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t an argument that community modules are bad. It&amp;rsquo;s an argument about when the deal is good.&lt;/p&gt;
&lt;p&gt;A wrapper module is a good deal while its abstraction holds: while what it assumes you want matches what you want. The moment you need something it didn&amp;rsquo;t anticipate, the deal inverts. Now you&amp;rsquo;re working &lt;em&gt;against&lt;/em&gt; the abstraction, and an abstraction you&amp;rsquo;re fighting costs more than no abstraction at all. (Regular readers will recognise that line from &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;the LangChain argument&lt;/a&gt;; it&amp;rsquo;s the same principle in a very different language.)&lt;/p&gt;
&lt;p&gt;Infrastructure that holds signing keys is precisely the case where you need to control the specifics: every encryption setting, every lifecycle rule, every line of every bucket policy. That&amp;rsquo;s a domain where wrapper abstractions leak fast, because the whole job &lt;em&gt;is&lt;/em&gt; the details the wrapper smoothed over.&lt;/p&gt;
&lt;h2 id="the-cost-paid-on-purpose"&gt;The cost, paid on purpose
&lt;/h2&gt;&lt;p&gt;Hand-rolling isn&amp;rsquo;t free. It&amp;rsquo;s more lines of HCL in the repo, up front, than a one-line module call.&lt;/p&gt;
&lt;p&gt;What those lines buy is worth the price &lt;em&gt;for this kind of infrastructure&lt;/em&gt;. There&amp;rsquo;s no transitive module-version churn to track. There&amp;rsquo;s no abstraction between me and the resource when something behaves oddly. And every line is one I can read, and defend, in a security review, because I wrote it and it says exactly what it does. For a foundation that will hold the most sensitive key in the system, &amp;ldquo;readable and mine&amp;rdquo; beats &amp;ldquo;short and someone else&amp;rsquo;s&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a deliberate trade, not a universal rule. For an internal tool on a deadline, reach for the wrapper. For the security-critical base of everything else, the raw resources won every time I checked.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;The community module ecosystem offers less code that more people have tested, and for plenty of infrastructure that&amp;rsquo;s the right call. For &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; and &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt; it almost never was, because each wrapper turned out to be more configuration than the raw resources once my posture was accounted for, or it leaked the moment I needed a specific.&lt;/p&gt;
&lt;p&gt;The CloudTrail log bucket is the pattern in miniature: the bundled bucket lacked &lt;code&gt;prevent_destroy&lt;/code&gt; and a strong policy, so using the wrapper meant either a weaker bucket or fighting the module. A wrapper is a good deal while its abstraction holds and a bad one the moment you fight it, and security-critical foundation infrastructure is all specifics. Hand-rolling cost more lines and bought code I can read and defend. For this, that was the trade worth making.&lt;/p&gt;</description></item><item><title>Hardening the account that will hold the keys</title><link>https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/</link><pubDate>Sat, 09 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/</guid><description>&lt;img src="https://blog-570662.gitlab.io/hardening-the-account-that-will-hold-the-keys/cover-hardening-the-account-that-will-hold-the-keys.png" alt="Featured image of post Hardening the account that will hold the keys" /&gt;&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/" &gt;Bootstrapping the account&lt;/a&gt; got it &lt;em&gt;ready&lt;/em&gt;: somewhere to store state, an identity to deploy as, enough for the next &lt;code&gt;tofu apply&lt;/code&gt; to run. Ready is not the same as safe. An account with no audit trail, nothing watching it, and no considered way for a human to get in is fine for experimenting and absolutely not where you put the most sensitive key in the system. So before the signing key goes anywhere near it, the account gets a security baseline.&lt;/p&gt;
&lt;h2 id="ready-is-not-the-same-as-safe"&gt;Ready is not the same as safe
&lt;/h2&gt;&lt;p&gt;The bootstrap post ended with an account that was &lt;em&gt;ready&lt;/em&gt;: it had somewhere to store state and a CI identity to deploy as. The next &lt;code&gt;tofu apply&lt;/code&gt; could run.&lt;/p&gt;
&lt;p&gt;Ready is not safe. That account still has no audit trail, so nobody could tell you afterwards what happened in it. It has no threat detection, so nothing is watching. Its defaults are AWS&amp;rsquo;s defaults, which are not a security posture. There&amp;rsquo;s no considered way for a human to get in. An account in that condition is fine for experimenting. It&amp;rsquo;s not somewhere you put the most sensitive key in the whole system.&lt;/p&gt;
&lt;p&gt;So before the signing key is anywhere near it, the account gets a security baseline.&lt;/p&gt;
&lt;h2 id="the-baseline-in-one-downstream-stack"&gt;The baseline, in one downstream stack
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/tree/v0.2.0/modules" target="_blank" rel="noopener"
 &gt;&lt;code&gt;terraform-aws-security-baseline&lt;/code&gt;&lt;/a&gt; is that baseline, and it&amp;rsquo;s exactly the downstream stack the bootstrap post promised: applied &lt;em&gt;through&lt;/em&gt; the automation role bootstrap created, not bootstrapped specially.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s six sub-modules, each behind an &lt;code&gt;enable_*&lt;/code&gt; toggle: &lt;code&gt;account-hardening&lt;/code&gt; (IAM password policy, account-wide S3 public-access blocking, default EBS encryption), &lt;code&gt;audit-logging&lt;/code&gt; (a multi-region CloudTrail with log-file validation), &lt;code&gt;aws-config&lt;/code&gt;, &lt;code&gt;threat-detection&lt;/code&gt; (GuardDuty, Security Hub, IAM Access Analyzer), &lt;code&gt;alerts&lt;/code&gt;, and &lt;code&gt;operator-role&lt;/code&gt;. Together they turn a bare account into one that records what happens, watches for trouble, and controls who gets in.&lt;/p&gt;
&lt;p&gt;Most of those are the expected baseline. The operator role is the one worth slowing down on, because it&amp;rsquo;s built backwards from how people usually think about an admin role.&lt;/p&gt;
&lt;h2 id="the-operator-role-and-the-inversion"&gt;The operator role, and the inversion
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/tree/v0.2.0/modules/operator-role" target="_blank" rel="noopener"
 &gt;&lt;code&gt;InfraAdmin&lt;/code&gt;&lt;/a&gt; is the human way into the account: the role a person assumes to do operator work. Two things define it.&lt;/p&gt;
&lt;p&gt;The trust policy decides &lt;em&gt;who&lt;/em&gt; may assume it. It trusts only the account root principal, and it requires multi-factor authentication: the assume call must carry &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt;, and &lt;code&gt;aws:MultiFactorAuthAge&lt;/code&gt; bounds how recently that MFA was performed. No MFA, no role. So far this is a careful but ordinary admin role.&lt;/p&gt;
&lt;p&gt;The inversion is a &lt;em&gt;second&lt;/em&gt;, separate inline policy, and it&amp;rsquo;s almost entirely &lt;code&gt;Deny&lt;/code&gt;. It denies, using &lt;code&gt;NotAction&lt;/code&gt;, anything where &lt;code&gt;aws:RequestedRegion&lt;/code&gt; falls outside an allowed set of regions. The role&amp;rsquo;s &lt;em&gt;power&lt;/em&gt; comes from an admin grant. This inline policy &lt;em&gt;fences&lt;/em&gt; that power.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the part worth holding onto. People picture an admin role as a list of what it can do. This one is better understood by what it &lt;em&gt;cannot&lt;/em&gt;: it cannot act outside its permitted regions, full stop. A fat-fingered command, or a compromised session, cannot quietly spin resources up in some region nobody&amp;rsquo;s watching. The fence is as much the point of the role as the grant is.&lt;/p&gt;
&lt;h2 id="the-carve-out-because-honesty"&gt;The carve-out, because honesty
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a fiddly detail, and it&amp;rsquo;s the kind of thing that makes the region fence real rather than theoretical.&lt;/p&gt;
&lt;p&gt;Some AWS services are global. IAM, CloudFront, Route 53 and friends have no region, and they don&amp;rsquo;t honour &lt;code&gt;aws:RequestedRegion&lt;/code&gt;. A naive region-deny would therefore deny calls to IAM, and you&amp;rsquo;d lock yourself out of the very service you manage access with. (A close cousin of the kind of self-inflicted lockout I&amp;rsquo;ll come back to in a &lt;a class="link" href="https://blog-570662.gitlab.io/a-403-you-cant-fix-in-iam/" &gt;later post&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;So the Deny carries explicit carve-outs for the global services. It isn&amp;rsquo;t elegant, and it can&amp;rsquo;t be: the global-versus-regional split is just a fact of AWS, and a correct region fence has to account for it. The carve-out list is the real cost of the control working.&lt;/p&gt;
&lt;h2 id="harden-the-room-then-move-the-keys-in"&gt;Harden the room, then move the keys in
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s an order to all of this, and the order is the argument.&lt;/p&gt;
&lt;p&gt;The account that will hold the signing key has to be audited before the key arrives, so that from day one every call against it is in CloudTrail. It has to be watched before the key arrives, so GuardDuty is already looking. It has to be access-controlled before the key arrives, so the only human path in is MFA-gated and region-fenced.&lt;/p&gt;
&lt;p&gt;You don&amp;rsquo;t move something valuable into a room and then think about locks. You build the room, fit the locks, check they work, and &lt;em&gt;then&lt;/em&gt; move the valuable thing in. The security baseline is fitting the locks. The signing key comes later, into a room already built for it.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;Bootstrapping an account makes it ready for the next deploy. It does not make it safe to hold anything that matters. &lt;code&gt;terraform-aws-security-baseline&lt;/code&gt; is the downstream stack that closes that gap: audit logging, AWS Config, threat detection, account hardening, and an operator role, applied through the CI role bootstrap created.&lt;/p&gt;
&lt;p&gt;The operator role is the piece to study. It&amp;rsquo;s MFA-gated on the way in, and then fenced by a separate, almost-all-&lt;code&gt;Deny&lt;/code&gt; inline policy that confines it to permitted regions, with carve-outs for the global services that have no region. An admin role defined as much by its fence as its grant. Harden the room first; the keys move in afterwards.&lt;/p&gt;</description></item><item><title>No access keys in CI</title><link>https://blog-570662.gitlab.io/no-access-keys-in-ci/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/no-access-keys-in-ci/</guid><description>&lt;img src="https://blog-570662.gitlab.io/no-access-keys-in-ci/cover-no-access-keys-in-ci.png" alt="Featured image of post No access keys in CI" /&gt;&lt;p&gt;A long-lived AWS access key, sitting in a CI system, is just about the single credential I&amp;rsquo;d most like to be rid of. It&amp;rsquo;s powerful, it never expires unless someone remembers to rotate it (nobody remembers to rotate it), and it lives in one of the most attractive targets in the whole supply chain. For infrastructure that&amp;rsquo;s eventually going to hold a release-signing key, it&amp;rsquo;s exactly the wrong place to start. So the &lt;code&gt;phpboyscout&lt;/code&gt; infrastructure has no AWS access key in CI at all. None.&lt;/p&gt;
&lt;h2 id="the-access-key-you-dont-want"&gt;The access key you don&amp;rsquo;t want
&lt;/h2&gt;&lt;p&gt;A CI pipeline that runs &lt;code&gt;tofu apply&lt;/code&gt; against AWS needs AWS credentials. The traditional way to give it some is an IAM user with an access key pair, pasted into the CI system as a masked variable.&lt;/p&gt;
&lt;p&gt;Look at what that key is. It&amp;rsquo;s long-lived: it works until someone remembers to rotate it, and rotating it is a chore, so mostly nobody does. It&amp;rsquo;s powerful: it can apply infrastructure, so it can do nearly anything. And it&amp;rsquo;s sitting in a CI system, which is one of the most attractive targets in your whole supply chain. You&amp;rsquo;ve taken your highest-value credential and stored a permanent copy of it in a place built for running automated jobs.&lt;/p&gt;
&lt;p&gt;For infrastructure that&amp;rsquo;s going to hold a release-signing key, that&amp;rsquo;s precisely the wrong starting point. So the &lt;code&gt;phpboyscout&lt;/code&gt; infrastructure has no AWS access key in CI at all. Not a well-guarded one. None.&lt;/p&gt;
&lt;h2 id="federation-instead-of-a-stored-secret"&gt;Federation instead of a stored secret
&lt;/h2&gt;&lt;p&gt;The replacement is OIDC federation, and the shape of it is worth walking through, because it&amp;rsquo;s genuinely different from &amp;ldquo;a secret, but better&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;A modern CI platform can mint an OIDC token. GitLab does this with an &lt;code&gt;id_tokens:&lt;/code&gt; block: at job time, GitLab issues a short-lived JSON Web Token, signed by GitLab, that asserts a set of facts. This is project X. This is pipeline Y. This is running on ref Z, of this type.&lt;/p&gt;
&lt;p&gt;AWS can consume that. The &lt;code&gt;sts:AssumeRoleWithWebIdentity&lt;/code&gt; call takes such a token and, if it satisfies an IAM role&amp;rsquo;s trust policy, returns short-lived AWS credentials for that role. The trust policy is where the control lives: it names GitLab as a trusted token issuer, and it constrains the token&amp;rsquo;s &lt;code&gt;sub&lt;/code&gt; claim so that only the specific project, and the specific refs, you intend can assume the role.&lt;/p&gt;
&lt;p&gt;Put it together: the pipeline asks GitLab for a token, hands it to AWS, and gets back credentials that last about an hour and are scoped to one role. Nothing long-lived is stored anywhere. The credential exists only for the job that needs it, and it can&amp;rsquo;t be stolen from a CI variable store, because it was never in one.&lt;/p&gt;
&lt;h2 id="two-halves-of-one-handshake"&gt;Two halves of one handshake
&lt;/h2&gt;&lt;p&gt;That handshake is built by two of the repos in this series, each owning one side.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/" &gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&lt;/a&gt; builds the AWS half, in its &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/blob/v0.2.0/modules/automation-iam/main.tf#L99" target="_blank" rel="noopener"
 &gt;&lt;code&gt;automation-iam&lt;/code&gt; module&lt;/a&gt;: it registers GitLab as an OIDC identity provider in the account, and it creates the automation role with the trust policy that decides which pipelines may assume it.&lt;/p&gt;
&lt;p&gt;The CI components build the consuming half: the &lt;code&gt;id_tokens:&lt;/code&gt; block that asks GitLab for the JWT, and then simply letting the AWS provider&amp;rsquo;s own credential chain perform the exchange. The pipeline doesn&amp;rsquo;t call &lt;code&gt;sts&lt;/code&gt; by hand. It presents the token; the SDK does the rest.&lt;/p&gt;
&lt;h2 id="the-gotcha-dont-set-a-profile"&gt;The gotcha: don&amp;rsquo;t set a profile
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one quiet way to break this, and a stack can look completely correct while doing it.&lt;/p&gt;
&lt;p&gt;The AWS SDK finds credentials by walking a chain of sources in order. The web-identity path, the one that uses the OIDC token, is one link in that chain. It triggers off environment variables the CI sets up automatically.&lt;/p&gt;
&lt;p&gt;But if the &lt;code&gt;aws&lt;/code&gt; provider block has a hardcoded &lt;code&gt;profile = &amp;quot;...&amp;quot;&lt;/code&gt;, the SDK takes the &lt;em&gt;profile&lt;/em&gt; link of the chain instead, and never reaches the web-identity link. A &lt;code&gt;profile&lt;/code&gt; line is the sort of thing that ends up in a provider block from someone&amp;rsquo;s local development setup, where it&amp;rsquo;s exactly right. Committed and run in CI, it silently short-circuits the federation. The pipeline either fails to find credentials, or finds the wrong ones.&lt;/p&gt;
&lt;p&gt;The rule is simple once you know it: the provider block that runs in CI must not name a &lt;code&gt;profile&lt;/code&gt;. Leave the chain free to find the web identity. It&amp;rsquo;s the kind of bug that teaches you to be precise about &lt;em&gt;which&lt;/em&gt; link of the credential chain you&amp;rsquo;re actually relying on.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Giving CI an AWS access key means storing your most powerful, longest-lived credential in one of your most exposed systems. OIDC federation removes it entirely. The CI platform mints a short-lived signed token, AWS exchanges it via &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt; for hour-long credentials against a role whose trust policy names the exact pipeline, and nothing permanent is stored.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; builds the AWS side, the identity provider and the trust policy; the CI components build the consuming side, the token request. The one trap is a hardcoded &lt;code&gt;profile&lt;/code&gt; in the provider block, which short-circuits the SDK&amp;rsquo;s credential chain before it reaches the web-identity path. Get that right, and a pipeline deploys to AWS as a verifiable, short-lived identity, with no key to steal.&lt;/p&gt;</description></item><item><title>Two layers of tags, and which one wins</title><link>https://blog-570662.gitlab.io/two-layers-of-tags/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-layers-of-tags/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-layers-of-tags/cover-two-layers-of-tags.png" alt="Featured image of post Two layers of tags, and which one wins" /&gt;&lt;p&gt;Tagging cloud resources is one of those jobs that&amp;rsquo;s trivial to do badly and surprisingly fiddly to do well. Everyone agrees resources should be tagged. The argument nobody quite has out loud is &lt;em&gt;where the tags should come from&lt;/em&gt;, and getting that wrong gives you either a giant copy-pasted tag block on every resource, or a set of tags that quietly disagree with each other across the account.&lt;/p&gt;
&lt;h2 id="tags-answer-two-different-questions"&gt;Tags answer two different questions
&lt;/h2&gt;&lt;p&gt;If you look at what tags are actually &lt;em&gt;for&lt;/em&gt;, they split cleanly into two kinds, and the split is the whole point.&lt;/p&gt;
&lt;p&gt;Some tags are true of every single resource in the account, identically. The environment it belongs to. The fact that OpenTofu manages it. The project or owner it rolls up to for cost reporting. These are invariants: a resource that didn&amp;rsquo;t carry them would be the bug.&lt;/p&gt;
&lt;p&gt;Other tags are specific to a particular piece of infrastructure. Which component this resource belongs to, what subsystem it&amp;rsquo;s part of. The CloudTrail bucket is part of audit logging; the Config recorder is part of &lt;code&gt;aws-config&lt;/code&gt;. That&amp;rsquo;s a fact about the module, not about the account.&lt;/p&gt;
&lt;p&gt;Treat those two kinds the same and you end up repeating the invariants by hand on every resource, which is exactly the copy-paste that drifts. So the &lt;code&gt;infra&lt;/code&gt; setup gives each kind its own home.&lt;/p&gt;
&lt;h2 id="layer-one-declared-once-on-the-provider"&gt;Layer one: declared once, on the provider
&lt;/h2&gt;&lt;p&gt;The invariants live on the AWS provider itself, as &lt;code&gt;default_tags&lt;/code&gt;, set one time in the provider block:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aws&amp;#34;&lt;/span&gt; {&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;default_tags&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; {&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # Environment, project, managed-by: the things true of
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # every resource in this account.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;default_tags&lt;/code&gt; applies those tags to every taggable resource the provider creates, automatically, without a single resource having to mention them. Change the environment label once, here, and it propagates to everything on the next apply. No resource carries a copy; they all inherit the originals. The invariants are stated exactly once, in the one place that&amp;rsquo;s true for all of them.&lt;/p&gt;
&lt;h2 id="layer-two-merged-in-by-the-module"&gt;Layer two: merged in by the module
&lt;/h2&gt;&lt;p&gt;The resource-specific tags live where the resource does: inside the module. Each module merges its own component tag over whatever tags it was handed, which you can see in the public &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-security-baseline/-/blob/v0.2.0/modules/aws-config/main.tf#L10" target="_blank" rel="noopener"
 &gt;&lt;code&gt;terraform-aws-security-baseline&lt;/code&gt;&lt;/a&gt; modules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt; merge({ Component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aws-config&amp;#34;&lt;/span&gt; }&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So the &lt;code&gt;aws-config&lt;/code&gt; module stamps &lt;code&gt;Component = &amp;quot;aws-config&amp;quot;&lt;/code&gt; onto the things it builds, the &lt;code&gt;account-hardening&lt;/code&gt; module stamps its own, and so on. The caller can pass extra tags down through &lt;code&gt;var.tags&lt;/code&gt;, and because they come last in the &lt;code&gt;merge&lt;/code&gt;, the caller can override the module&amp;rsquo;s defaults when it genuinely needs to. Module-specific knowledge stays in the module; per-call adjustments stay with the caller.&lt;/p&gt;
&lt;h2 id="which-layer-wins"&gt;Which layer wins
&lt;/h2&gt;&lt;p&gt;Now the question that actually bites: a resource is getting tags from the provider&amp;rsquo;s &lt;code&gt;default_tags&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; from the module&amp;rsquo;s &lt;code&gt;merge&lt;/code&gt;. What happens when both set the same key?&lt;/p&gt;
&lt;p&gt;The resource-level tags win. AWS&amp;rsquo;s provider treats tags set directly on a resource as an override of &lt;code&gt;default_tags&lt;/code&gt; on a key collision, so the module&amp;rsquo;s merged tags take precedence over the account-wide defaults. That&amp;rsquo;s the right way round: the invariants are a sensible baseline, and a module that has a specific reason to set a key differently can, without having to reach up and edit the provider block that everything else depends on. Most of the time the two layers are simply disjoint, the invariants saying &lt;em&gt;what account this is&lt;/em&gt; and the module tags saying &lt;em&gt;what this resource is for&lt;/em&gt;, and they never collide at all. When they do, local intent beats the global default, which is the precedence you&amp;rsquo;d want.&lt;/p&gt;
&lt;h2 id="why-bother-splitting-it"&gt;Why bother splitting it
&lt;/h2&gt;&lt;p&gt;The payoff is that neither layer has to know about the other. The provider declares the invariants once and never thinks about components. Each module declares its component and never hard-codes the environment. Add a new module and it inherits every account-wide tag for free, while contributing its own. Change an account-wide tag and you touch one block, not two hundred resources. The tags stay consistent not because someone&amp;rsquo;s policing them, but because the place each tag is declared is the one place it &lt;em&gt;can&lt;/em&gt; be declared.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;Resource tags answer two questions, and they want two homes. Account-wide invariants (environment, ownership, managed-by) go on the provider&amp;rsquo;s &lt;code&gt;default_tags&lt;/code&gt;, declared once and inherited by everything. Resource-specific tags go in the module, via &lt;code&gt;merge({ Component = &amp;quot;...&amp;quot; }, var.tags)&lt;/code&gt;, so each module owns its own labels and the caller can still override. On a key conflict the resource-level tag wins, which means the module&amp;rsquo;s intent beats the account default exactly when it should. Two layers, each declared in the one place it belongs, and no copy-pasted tag block anywhere in sight.&lt;/p&gt;</description></item><item><title>clap's global flag, except in a passthrough subtree</title><link>https://blog-570662.gitlab.io/claps-global-flag-except-in-a-passthrough-subtree/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/claps-global-flag-except-in-a-passthrough-subtree/</guid><description>&lt;img src="https://blog-570662.gitlab.io/claps-global-flag-except-in-a-passthrough-subtree/cover-claps-global-flag-except-in-a-passthrough-subtree.png" alt="Featured image of post clap's global flag, except in a passthrough subtree" /&gt;&lt;p&gt;&lt;code&gt;--output json&lt;/code&gt; worked everywhere. On the top-level command, on every ordinary subcommand, wherever the user fancied putting it. Then it stopped working in exactly one place, and of course it was the subcommand I&amp;rsquo;d been clever about.&lt;/p&gt;
&lt;h2 id="how-the-global-flag-is-meant-to-work"&gt;How the global flag is meant to work
&lt;/h2&gt;&lt;p&gt;clap has a lovely feature for this. Define &lt;code&gt;--output text|json&lt;/code&gt; once at the top, mark it &lt;code&gt;global = true&lt;/code&gt;, and it&amp;rsquo;s reachable from every subcommand: &lt;code&gt;mytool --output json widget&lt;/code&gt; and &lt;code&gt;mytool widget --output json&lt;/code&gt; land the same. You stop thinking about it.&lt;/p&gt;
&lt;h2 id="the-one-place-it-goes-missing"&gt;The one place it goes missing
&lt;/h2&gt;&lt;p&gt;One subcommand, &lt;code&gt;credentials&lt;/code&gt;, is a passthrough: it sets &lt;code&gt;subcommand_passthrough = true&lt;/code&gt;, which makes clap capture everything after the subcommand name as &lt;code&gt;trailing_var_arg&lt;/code&gt; and hand it on, the way &lt;code&gt;cargo run -- ...&lt;/code&gt; passes the trailing args to your program rather than to cargo. The handler then re-parses those captured tokens against its own clap definition.&lt;/p&gt;
&lt;p&gt;The trouble is that the captured tokens include &lt;code&gt;--output&lt;/code&gt;. clap&amp;rsquo;s &lt;code&gt;global = true&lt;/code&gt; propagation doesn&amp;rsquo;t reach a passthrough subtree, because the post-name tokens are taken as &lt;code&gt;trailing_var_arg&lt;/code&gt; before the outer parser ever sees them. So in this one subtree the global flag isn&amp;rsquo;t applied, and worse, when the inner parser re-parses the captured args it meets &lt;code&gt;--output&lt;/code&gt;, which it doesn&amp;rsquo;t define, and rejects it as unknown. The code says so where it matters, in &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-cli/src/credentials.rs#L62-69" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-cli/src/credentials.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// clap&amp;#39;s outer `global = true` propagation works for normal
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// subcommands, but `subcommand_passthrough = true` captures
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// post-name tokens as `trailing_var_arg`, so the global
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// never reaches the outer parser for this subtree.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;strip_global_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="parse-it-yourself-then-strip-it"&gt;Parse it yourself, then strip it
&lt;/h2&gt;&lt;p&gt;The fix is two moves. First, parse &lt;code&gt;--output&lt;/code&gt; out of the raw args by hand (there&amp;rsquo;s an &lt;code&gt;OutputMode::from_args_os&lt;/code&gt; for exactly that), so the output mode is still honoured. Then strip &lt;code&gt;--output&lt;/code&gt; out of the args before the inner parser runs, so the inner clap doesn&amp;rsquo;t choke on a flag it doesn&amp;rsquo;t define. &lt;code&gt;strip_global_output&lt;/code&gt; is the second move, from &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-cli/src/render.rs#L95" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-cli/src/render.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;--output=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// inline form: drop just this token
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;--output&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// space-separated form: drop the token and its value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It handles both &lt;code&gt;--output=json&lt;/code&gt; and &lt;code&gt;--output json&lt;/code&gt;, and it&amp;rsquo;s idempotent, so it&amp;rsquo;s safe to call whether or not the flag is actually present.&lt;/p&gt;
&lt;h2 id="the-takeaway"&gt;The takeaway
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;global = true&lt;/code&gt; and &lt;code&gt;trailing_var_arg&lt;/code&gt; are both &amp;ldquo;grab the args&amp;rdquo; features, and in a passthrough subcommand they reach for the same tokens. clap won&amp;rsquo;t arbitrate that overlap, and shouldn&amp;rsquo;t try to guess. So you arbitrate: parse the global out of the raw args yourself, strip it before you re-parse the rest, and the flag that &amp;ldquo;works everywhere&amp;rdquo; actually does.&lt;/p&gt;</description></item><item><title>Secrets that scrub themselves from RAM</title><link>https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/</guid><description>&lt;img src="https://blog-570662.gitlab.io/secrets-that-scrub-themselves-from-ram/cover-secrets-that-scrub-themselves-from-ram.png" alt="Featured image of post Secrets that scrub themselves from RAM" /&gt;&lt;p&gt;A while ago I worked out &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;where a CLI should keep your API key&lt;/a&gt;: env var, OS keychain, or, grudgingly, a literal in the config file. That answers where the secret &lt;em&gt;lives&lt;/em&gt;. It says nothing about what happens to it once it&amp;rsquo;s loaded and sitting in your process memory, which is the half where secrets actually tend to leak. Rust, it turns out, can do something about that half that Go simply can&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-already-settled"&gt;What go-tool-base already settled
&lt;/h2&gt;&lt;p&gt;A while back I wrote about where a CLI should keep your API keys. The answer go-tool-base settled on was three storage modes, in a fixed precedence: an environment variable reference (the recommended default), the OS keychain (opt-in), or a literal value in the config file (legacy, and refused outright when &lt;code&gt;CI=true&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;rust-tool-base keeps that design unchanged. Same three modes, same precedence, same refusal of literal secrets in CI. A tool embeds a &lt;code&gt;CredentialRef&lt;/code&gt; in its typed config, and a &lt;code&gt;Resolver&lt;/code&gt; walks env, then keychain, then literal, then a well-known fallback variable, first hit wins. That part is a straight carry-over, because &lt;em&gt;where&lt;/em&gt; to keep the secret was design, and design survives the port.&lt;/p&gt;
&lt;p&gt;But storage is only half the life of a secret. The other half is what happens to it once it&amp;rsquo;s resolved and sitting in your process memory. That&amp;rsquo;s where Rust can do something Go can&amp;rsquo;t, and rust-tool-base takes the opening.&lt;/p&gt;
&lt;h2 id="the-two-ways-a-secret-leaks-after-youve-loaded-it"&gt;The two ways a secret leaks after you&amp;rsquo;ve loaded it
&lt;/h2&gt;&lt;p&gt;You&amp;rsquo;ve resolved the API key. It&amp;rsquo;s a value in memory now. Two very ordinary things can leak it from there, and neither involves your storage being wrong.&lt;/p&gt;
&lt;p&gt;The first is &lt;strong&gt;the log line&lt;/strong&gt;. Somewhere a developer writes a debug print of a config struct, or an error includes the struct that holds the key, or a panic dumps it. The secret is a string like any other string, so it renders like any other string, straight into a log aggregator that a lot of people can read.&lt;/p&gt;
&lt;p&gt;The second is &lt;strong&gt;the leftover bytes&lt;/strong&gt;. The key sat in a heap allocation. The variable goes out of scope, the allocation is freed, and on most runtimes &amp;ldquo;freed&amp;rdquo; just means &amp;ldquo;returned to the allocator&amp;rdquo;. The bytes are still there until something else writes over them. A core dump taken in that window contains your key. So does the next allocation that happens to land on that memory and gets logged before it&amp;rsquo;s overwritten.&lt;/p&gt;
&lt;p&gt;A Go string can&amp;rsquo;t really defend against either. Go strings are immutable, so you can&amp;rsquo;t zero one in place; the runtime copies them freely, so you can&amp;rsquo;t even track every copy; and there&amp;rsquo;s no compile-time barrier stopping anyone printing one. You can be disciplined, but discipline is all you&amp;rsquo;ve got.&lt;/p&gt;
&lt;h2 id="secretstring-closes-both"&gt;&lt;code&gt;SecretString&lt;/code&gt; closes both
&lt;/h2&gt;&lt;p&gt;rust-tool-base routes every secret through &lt;code&gt;secrecy::SecretString&lt;/code&gt;, and the crate is explicit that taking a plain &lt;code&gt;&amp;amp;str&lt;/code&gt; or &lt;code&gt;String&lt;/code&gt; for a secret is a &lt;em&gt;type error&lt;/em&gt;, not a style preference.&lt;/p&gt;
&lt;p&gt;For the log line, &lt;code&gt;SecretString&lt;/code&gt; has its own &lt;code&gt;Debug&lt;/code&gt; implementation, and it prints &lt;code&gt;[REDACTED]&lt;/code&gt;. Always. A config struct holding a &lt;code&gt;SecretString&lt;/code&gt; can be debug-printed, put in an error, caught in a panic, and the secret field shows up as &lt;code&gt;[REDACTED]&lt;/code&gt; every single time. You don&amp;rsquo;t have to remember not to log it. The type already won&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;For the leftover bytes, &lt;code&gt;SecretString&lt;/code&gt; zeroes its memory when it&amp;rsquo;s dropped. When the value goes out of scope, before the allocation is handed back, the bytes are overwritten. The window where a freed allocation still holds your key is closed. A core dump taken afterwards finds zeroes.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a third leak &lt;code&gt;SecretString&lt;/code&gt; blocks that&amp;rsquo;s easy to miss. It deliberately doesn&amp;rsquo;t implement &lt;code&gt;Serialize&lt;/code&gt;. You cannot serialise a &lt;code&gt;SecretString&lt;/code&gt;. That sounds like an inconvenience until you see what it prevents: a tool that loads config, changes one setting, and writes the whole struct back would, with an ordinary string, faithfully write the resolved secret to disk in plain text. Because &lt;code&gt;SecretString&lt;/code&gt; can&amp;rsquo;t be serialised, &lt;code&gt;CredentialRef&lt;/code&gt; can&amp;rsquo;t be either, and that accident is structurally impossible. Writing a secret back is a deliberate, separate path, never a side effect of saving config.&lt;/p&gt;
&lt;p&gt;When code genuinely needs the raw value, to drop it into an &lt;code&gt;Authorization&lt;/code&gt; header, it calls &lt;code&gt;expose_secret()&lt;/code&gt;. The name is the point. Getting at the plaintext is one explicit, greppable, reviewable call, and everywhere else the secret stays wrapped.&lt;/p&gt;
&lt;h2 id="discipline-versus-the-type-system"&gt;Discipline versus the type system
&lt;/h2&gt;&lt;p&gt;The plain framing is this. None of these leaks are exotic. Logging a struct, a core dump after a free, re-saving a config file: they&amp;rsquo;re all routine, and they&amp;rsquo;re all how real credentials end up somewhere they shouldn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s storage design is good, and rust-tool-base kept it. But in Go, &lt;em&gt;not leaking the secret once it&amp;rsquo;s in memory&lt;/em&gt; comes down to every developer being careful every time. In Rust, &lt;code&gt;SecretString&lt;/code&gt; makes &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;the type system&lt;/a&gt; carry it. The redaction, the zeroing, the un-serialisability aren&amp;rsquo;t things you remember to do. They&amp;rsquo;re things the secret does to itself because of what it is. That&amp;rsquo;s the part Go structurally can&amp;rsquo;t match, and it&amp;rsquo;s why the port didn&amp;rsquo;t just copy the storage modes across, it tightened the handling underneath them.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;go-tool-base settled where a CLI keeps a secret: env var, keychain, or literal, in a fixed precedence. rust-tool-base keeps that design and hardens what happens once the secret is loaded.&lt;/p&gt;
&lt;p&gt;Every secret is a &lt;code&gt;secrecy::SecretString&lt;/code&gt;. It debug-prints as &lt;code&gt;[REDACTED]&lt;/code&gt;, so it can&amp;rsquo;t fall into a log by accident. Its memory is zeroed on drop, so it doesn&amp;rsquo;t survive in freed heap. It isn&amp;rsquo;t serialisable, so it can&amp;rsquo;t be written back to config by a blanket save. Getting the plaintext is one explicit &lt;code&gt;expose_secret()&lt;/code&gt; call. Go can only ask developers to be careful with a secret in memory; Rust lets the type be careful for them.&lt;/p&gt;</description></item><item><title>The chicken-and-egg of remote state</title><link>https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/cover-the-chicken-and-egg-of-remote-state.png" alt="Featured image of post The chicken-and-egg of remote state" /&gt;&lt;p&gt;Here&amp;rsquo;s a puzzle that every infrastructure-as-code setup hits exactly once, right at the very beginning, and then never again. An OpenTofu stack stores its state in a backend. &lt;a class="link" href="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/" &gt;The bootstrap stack&lt;/a&gt; I wrote about last time has a particular job, and part of that job is to &lt;em&gt;create&lt;/em&gt; the backend that remote state lives in. So where does the bootstrap stack store its own state, on the very first run, before it&amp;rsquo;s built the place state is supposed to go?&lt;/p&gt;
&lt;h2 id="where-does-the-state-of-the-thing-that-makes-the-state-store-live"&gt;Where does the state of the thing that makes the state store live?
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the puzzle, and it&amp;rsquo;s a real ordering deadlock rather than a riddle.&lt;/p&gt;
&lt;p&gt;An OpenTofu stack keeps a state file, and for anything shared that state file lives in a remote backend: on AWS, an S3 bucket. Fine. But the bootstrap stack has a particular job, and part of that job is to &lt;em&gt;create the S3 bucket that remote state lives in&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So walk through the first run. Bootstrap has never been applied. The state bucket doesn&amp;rsquo;t exist, because creating it is what bootstrap is for. Bootstrap needs somewhere to store its own state. The only place that would make sense is the bucket it&amp;rsquo;s about to create, which isn&amp;rsquo;t there yet. The thing that builds the state store can&amp;rsquo;t store its state in the state store.&lt;/p&gt;
&lt;h2 id="run-local-then-migrate"&gt;Run local, then migrate
&lt;/h2&gt;&lt;p&gt;The way out is a two-step that OpenTofu supports directly.&lt;/p&gt;
&lt;p&gt;Bootstrap starts configured with a &lt;em&gt;local&lt;/em&gt; backend: &lt;code&gt;backend &amp;quot;local&amp;quot; {}&lt;/code&gt;. State is just a file on the operator&amp;rsquo;s machine. With that in place, the first &lt;code&gt;tofu apply&lt;/code&gt; runs. It creates the S3 bucket and the KMS key, and records all of it in the local state file.&lt;/p&gt;
&lt;p&gt;Now the bucket exists. So the backend configuration is rewritten to point at it: an &lt;code&gt;s3&lt;/code&gt; backend block naming the new bucket. Then &lt;code&gt;tofu init -migrate-state&lt;/code&gt;. OpenTofu sees the backend has changed, picks up the local state file, and copies it into the S3 bucket. From that point on, bootstrap&amp;rsquo;s own state lives in the bucket that bootstrap created. The egg has laid the chicken.&lt;/p&gt;
&lt;p&gt;The local backend was a scaffold. It existed for exactly one apply, to break the ordering deadlock, and then the state moved off it and it was never used again.&lt;/p&gt;
&lt;h2 id="it-happened-twice"&gt;It happened twice
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;infra&lt;/code&gt; repo actually did this migration twice, and the second time is the proof that the pattern is general rather than a one-off trick.&lt;/p&gt;
&lt;p&gt;The first migration was the one above: local to S3, at the very start. The second came later, during the &lt;a class="link" href="https://blog-570662.gitlab.io/why-we-left-github-for-gitlab/" &gt;move from GitHub to GitLab&lt;/a&gt;. GitLab offers a managed HTTP state backend, and &lt;code&gt;infra&lt;/code&gt; chose to use it. So the backend block was rewritten again, this time from &lt;code&gt;s3&lt;/code&gt; to &lt;code&gt;http&lt;/code&gt;, and &lt;code&gt;tofu init -migrate-state&lt;/code&gt; ran again, copying the state from the S3 bucket to GitLab&amp;rsquo;s backend.&lt;/p&gt;
&lt;p&gt;The same move, twice, against three different backends. That&amp;rsquo;s the useful lesson hiding in the chicken-and-egg story. State is portable. The backend is just &lt;em&gt;where you currently keep it&lt;/em&gt;, not a property of the stack itself, and moving it is a routine, supported operation rather than surgery.&lt;/p&gt;
&lt;h2 id="why-this-is-the-honest-answer-not-a-hack"&gt;Why this is the honest answer, not a hack
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s easy to look at &amp;ldquo;apply once with a local backend, then migrate&amp;rdquo; and feel it&amp;rsquo;s a bit of a smell, a workaround for something that should have been cleaner.&lt;/p&gt;
&lt;p&gt;It isn&amp;rsquo;t. It&amp;rsquo;s the honest answer to a real ordering problem, and the alternatives are worse.&lt;/p&gt;
&lt;p&gt;The obvious alternative is to create the state bucket by hand, in the console, before running bootstrap at all. But then the most important bucket in the account is unmanaged. It exists outside every OpenTofu graph, nobody&amp;rsquo;s code describes it, its encryption and policy and &lt;code&gt;prevent_destroy&lt;/code&gt; are whatever someone clicked that day, and it drifts. The local-then-migrate dance avoids exactly that. The bucket is created &lt;em&gt;by bootstrap&lt;/em&gt;, described in code, and tracked in bootstrap&amp;rsquo;s own state from its very first apply. It&amp;rsquo;s managed from birth.&lt;/p&gt;
&lt;p&gt;The chicken-and-egg isn&amp;rsquo;t a flaw to be embarrassed about. It&amp;rsquo;s just the shape of the problem when a stack has to build its own foundations, and OpenTofu&amp;rsquo;s &lt;code&gt;-migrate-state&lt;/code&gt; is the supported tool for exactly that shape.&lt;/p&gt;
&lt;h2 id="pulling-it-together"&gt;Pulling it together
&lt;/h2&gt;&lt;p&gt;Every OpenTofu stack needs a backend to store state, and the bootstrap stack&amp;rsquo;s job is to &lt;em&gt;create&lt;/em&gt; the backend, so on its first run the bucket it needs doesn&amp;rsquo;t yet exist.&lt;/p&gt;
&lt;p&gt;The resolution is to run bootstrap once with a local backend, let that apply create the bucket and key, then rewrite the backend configuration and &lt;code&gt;tofu init -migrate-state&lt;/code&gt; the state into the bucket bootstrap just made. The &lt;code&gt;infra&lt;/code&gt; repo did it twice, local to S3 and later S3 to GitLab, which shows the real point: state is portable, and the backend is just where you keep it. Doing it this way, rather than hand-creating the bucket, is what keeps that critical bucket managed in code from its very first day.&lt;/p&gt;</description></item><item><title>The cleanup tool that almost deleted its own hands</title><link>https://blog-570662.gitlab.io/the-cleanup-tool-that-almost-deleted-its-own-hands/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-cleanup-tool-that-almost-deleted-its-own-hands/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-cleanup-tool-that-almost-deleted-its-own-hands/cover-the-cleanup-tool-that-almost-deleted-its-own-hands.png" alt="Featured image of post The cleanup tool that almost deleted its own hands" /&gt;&lt;p&gt;The first time I pointed aws-nuke at a real account, the dry-run printed hundreds of lines of angry red text and my stomach dropped. Then I read it properly, and two things turned out to be true at once. Almost all of that red was noise. And the one operation I genuinely should have worried about wasn&amp;rsquo;t red at all.&lt;/p&gt;
&lt;h2 id="a-tool-whose-whole-job-is-destruction"&gt;A tool whose whole job is destruction
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/ekristen/aws-nuke" target="_blank" rel="noopener"
 &gt;aws-nuke&lt;/a&gt; deletes everything in an AWS account. That&amp;rsquo;s the point of it: when you spin up a throwaway account to try something, aws-nuke is how you tear it back down to nothing afterwards rather than leaving resources quietly billing forever. go-tool-base&amp;rsquo;s bootstrap renders a scoped aws-nuke config for exactly this, from a &lt;code&gt;nuke-config&lt;/code&gt; module, so the teardown is described in code rather than typed by hand at the worst possible moment.&lt;/p&gt;
&lt;p&gt;A tool that deletes everything is a tool you run in dry-run first, every single time, and read the output before you let it touch anything. So that&amp;rsquo;s what I did. And the output was alarming in a way that turned out to be completely meaningless, and reassuring in a way that turned out to hide the one real hazard.&lt;/p&gt;
&lt;h2 id="the-wall-of-red-that-means-nothing"&gt;The wall of red that means nothing
&lt;/h2&gt;&lt;p&gt;A fresh account threw up screen after screen of &lt;code&gt;SubscriptionRequiredException&lt;/code&gt;. Hundreds of lines, all red, all looking like something had gone badly wrong.&lt;/p&gt;
&lt;p&gt;They hadn&amp;rsquo;t. aws-nuke works by asking every region &amp;ldquo;do you have any of &lt;em&gt;this&lt;/em&gt; kind of resource?&amp;rdquo;, for every kind of resource it knows about. On a brand-new account you&amp;rsquo;ve never enabled most services in most regions, so the API&amp;rsquo;s honest answer is &amp;ldquo;you&amp;rsquo;re not subscribed to that here&amp;rdquo;, which surfaces as an exception, which the tool dutifully logs in red. It isn&amp;rsquo;t a failure. It&amp;rsquo;s the sound of an empty account being asked four hundred questions and answering &amp;ldquo;nothing here&amp;rdquo; to almost all of them.&lt;/p&gt;
&lt;p&gt;The skill, and it is a skill, is learning to read a destructive tool&amp;rsquo;s dry-run and tell the noise from the signal. &lt;code&gt;SubscriptionRequiredException&lt;/code&gt; on a fresh account is noise. Once you know that, the wall of red stops being frightening and becomes scenery.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a related trap in the same neighbourhood, and the &lt;code&gt;nuke-config&lt;/code&gt; module&amp;rsquo;s own &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/blob/v0.2.0/modules/nuke-config/variables.tf#L11" target="_blank" rel="noopener"
 &gt;&lt;code&gt;regions&lt;/code&gt; variable documents it&lt;/a&gt;. aws-nuke has a special &lt;code&gt;global&lt;/code&gt; pseudo-region for things that don&amp;rsquo;t live in one place (IAM, Route 53, CloudFront), and then the actual regions for everything else. It &lt;em&gt;also&lt;/em&gt; accepts &lt;code&gt;all&lt;/code&gt;, meaning every enabled region. Mixing &lt;code&gt;all&lt;/code&gt; with explicit region values scans some regions twice and muddies the output, so the module&amp;rsquo;s guidance is to pick one approach or the other. More scenery you have to learn to read before the genuinely important line will stand out.&lt;/p&gt;
&lt;h2 id="the-line-that-should-have-scared-me-and-didnt-look-like-it"&gt;The line that should have scared me, and didn&amp;rsquo;t look like it
&lt;/h2&gt;&lt;p&gt;Buried in that calm-looking eye of the storm, among the resources aws-nuke intended to delete, were the IAM resources granting the identity &lt;em&gt;running the nuke&lt;/em&gt; its administrative access.&lt;/p&gt;
&lt;p&gt;Sit with that for a second. aws-nuke runs as some principal with enough power to delete everything. To delete EVERYTHING, it has to delete IAM resources too. And if the plan deletes the very grant that gives the running identity its admin &lt;em&gt;before&lt;/em&gt; it&amp;rsquo;s finished, the tool strands itself partway through: no permissions left to complete the teardown, and now you&amp;rsquo;ve got a half-nuked account and a principal that can&amp;rsquo;t act on it. The cleanup tool sawing off the branch it&amp;rsquo;s standing on, calmly, without a single red line to warn you, because from the API&amp;rsquo;s point of view deleting that resource is a perfectly valid request.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the operation that actually mattered, and it was the quietest thing in the output.&lt;/p&gt;
&lt;h2 id="two-ways-to-keep-its-hands-attached"&gt;Two ways to keep its hands attached
&lt;/h2&gt;&lt;p&gt;The fix has two halves, one explicit and one structural.&lt;/p&gt;
&lt;p&gt;The explicit half is to &lt;em&gt;preserve the privilege path&lt;/em&gt;. The &lt;code&gt;nuke-config&lt;/code&gt; module passes a caller-supplied set of &lt;code&gt;filters&lt;/code&gt; straight through into the rendered config, so you tell aws-nuke &amp;ldquo;everything except these&amp;rdquo;. You exclude the identity running the nuke, and the policy and path that grant it admin, from deletion. The tool cleans the account and leaves its own hands alone, because you told it which resources are off-limits.&lt;/p&gt;
&lt;p&gt;The structural half is to not give it a tempting separate thing to delete in the first place. If an identity gets its admin through an IAM &lt;em&gt;group&lt;/em&gt; it belongs to, that group is its own deletable resource, one more thing in the plan, one more way to be stranded. The automation role in &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; instead takes its policies as &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/blob/v0.2.0/modules/automation-iam/main.tf#L119" target="_blank" rel="noopener"
 &gt;direct attachments to the role itself&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aws_iam_role_policy_attachment&amp;#34; &amp;#34;gitlab&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;is_gitlab&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;policy_arns&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; policy_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;value&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No intermediary group sitting there as a separate, deletable object. Flattening the privilege onto the role makes the dependency simpler to reason about and gives the cleanup tool one fewer foot-gun to find. Belt and braces: filter the path out explicitly, &lt;em&gt;and&lt;/em&gt; don&amp;rsquo;t build a structure that invites the problem.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;A destructive tool&amp;rsquo;s dry-run is the most valuable thing it produces, and reading it well is its own competence. On a fresh account, the screenfuls of red &lt;code&gt;SubscriptionRequiredException&lt;/code&gt; are noise, the sound of an empty account answering &amp;ldquo;nothing here&amp;rdquo;, and the &lt;code&gt;all&lt;/code&gt;-versus-&lt;code&gt;global&lt;/code&gt; region wrinkle is more of the same. Learn to see past all of it, because the operation that can actually hurt you is rarely the one shouting. Mine was the calm, unremarkable line proposing to delete the admin grant the nuke needed to finish its own job.&lt;/p&gt;
&lt;p&gt;Keep the cleanup tool&amp;rsquo;s hands attached: filter the privileged path out of the teardown so it&amp;rsquo;s never a candidate for deletion, and attach that privilege directly rather than through a group that&amp;rsquo;s just one more thing to delete. Then let it loose on everything else, which is, after all, what you brought it in to do.&lt;/p&gt;</description></item><item><title>The security finding you must not fix</title><link>https://blog-570662.gitlab.io/the-security-finding-you-must-not-fix/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-security-finding-you-must-not-fix/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-security-finding-you-must-not-fix/cover-the-security-finding-you-must-not-fix.png" alt="Featured image of post The security finding you must not fix" /&gt;&lt;p&gt;A security scanner flagged a finding in my Terraform, and the correct response, the one I had to talk myself into, was to leave it exactly as it was. Not because the finding was wrong about what the code did. It was right. It&amp;rsquo;s that doing what it asked would have quietly bricked the account.&lt;/p&gt;
&lt;h2 id="a-finding-that-looks-open-and-shut"&gt;A finding that looks open and shut
&lt;/h2&gt;&lt;p&gt;I run &lt;code&gt;checkov&lt;/code&gt; over the infrastructure as part of CI, and on the KMS key that protects the Terraform state bucket it raised CKV_AWS_111: a key policy that grants &lt;code&gt;kms:*&lt;/code&gt; is overly permissive. On the face of it, unarguable. The policy says the account root can perform &lt;em&gt;any&lt;/em&gt; KMS action on the key, with a resource of &lt;code&gt;*&lt;/code&gt;. A wildcard action and a wildcard resource is the exact shape a scanner is built to shout about, and ninety-nine times in a hundred it&amp;rsquo;d be right to.&lt;/p&gt;
&lt;p&gt;This was the hundredth time.&lt;/p&gt;
&lt;h2 id="why-narrowing-it-bricks-the-key"&gt;Why narrowing it bricks the key
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the bit that turns the finding on its head. That &lt;code&gt;kms:*&lt;/code&gt;-for-root statement isn&amp;rsquo;t an over-broad grant I left lying around. It&amp;rsquo;s the &lt;em&gt;default&lt;/em&gt; key policy AWS itself applies, and it&amp;rsquo;s load-bearing in a way that&amp;rsquo;s easy to miss.&lt;/p&gt;
&lt;p&gt;A KMS key is administered through its own key policy, and that policy is the &lt;em&gt;only&lt;/em&gt; way in. Unlike most resources, IAM permissions elsewhere can&amp;rsquo;t grant access to a key whose policy doesn&amp;rsquo;t allow it. So if you &amp;ldquo;tighten&amp;rdquo; the key by removing root&amp;rsquo;s full control, and you don&amp;rsquo;t perfectly replace it with some other administrative principal, you can end up with a key that &lt;em&gt;nobody&lt;/em&gt; can administer. Not you, not root, not a future you with a very good reason. KMS will not let you recover it. The key, and anything it encrypts, is stranded.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;kms:*&lt;/code&gt;-for-root statement is what keeps the account&amp;rsquo;s own root able to manage the key as a last resort. It&amp;rsquo;s not the vulnerability. It&amp;rsquo;s the escape hatch, and the scanner was asking me to weld it shut.&lt;/p&gt;
&lt;h2 id="so-the-finding-gets-suppressed-out-loud"&gt;So the finding gets suppressed, out loud
&lt;/h2&gt;&lt;p&gt;The answer isn&amp;rsquo;t to silence the scanner globally, and it isn&amp;rsquo;t to obey it. It&amp;rsquo;s to suppress &lt;em&gt;this finding, on this resource,&lt;/em&gt; with the reasoning written right there next to it, from &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/blob/v0.2.0/modules/state-backend/main.tf#L34" target="_blank" rel="noopener"
 &gt;&lt;code&gt;modules/state-backend/main.tf&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-hcl" data-lang="hcl"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aws_iam_policy_document&amp;#34; &amp;#34;kms&amp;#34;&lt;/span&gt; {&lt;span class="c1"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # checkov:skip=CKV_AWS_111:kms:* on the CMK for the account root is the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # AWS-documented pattern; narrowing it risks an unrecoverable lockout from
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt; # the key. See https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-default.html
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;statement&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;AllowAccountRootAdmin&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Allow&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;principals&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;AWS&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; identifiers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;arn:aws:iam::${var.account_id}:root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;kms:*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt; resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The skip carries a reason and a link to AWS&amp;rsquo;s own documentation of why this is the recommended default. That matters more than it looks. A bare &lt;code&gt;# checkov:skip&lt;/code&gt; with no explanation is indistinguishable from laziness, and the next person to read it (quite possibly me, a year on) has no way to tell whether it was a considered decision or someone making a red mark go away. A skip &lt;em&gt;with&lt;/em&gt; a documented reason is a decision you can audit. The finding is still visible in the sense that the suppression is right there in the code, attached to the thing it&amp;rsquo;s about, defensible out loud.&lt;/p&gt;
&lt;h2 id="a-scanner-is-an-argument-not-an-order"&gt;A scanner is an argument, not an order
&lt;/h2&gt;&lt;p&gt;The wider lesson is the one worth keeping, because it generalises well past this one key. A static-analysis finding is a &lt;em&gt;prompt to think&lt;/em&gt;, not an instruction to comply with. Most of the time thinking leads you straight to &amp;ldquo;yes, fix it&amp;rdquo;, and you should. But a scanner encodes a general rule, and general rules meet specific contexts where they&amp;rsquo;re wrong, or merely irrelevant, or, in the rare and dangerous case, actively harmful to obey. &lt;code&gt;kms:*&lt;/code&gt; for root on a customer-managed key is that last kind: the tool&amp;rsquo;s general rule (&amp;ldquo;wildcards are bad&amp;rdquo;) collides with a hard AWS-specific fact (&amp;ldquo;root must retain control of the key or it&amp;rsquo;s gone&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;The discipline that keeps this honest is the one in the code above. You don&amp;rsquo;t get to ignore a finding. You get to &lt;em&gt;suppress&lt;/em&gt; it, scoped to the exact resource, with a reason a reviewer can weigh. Cheap enough that you&amp;rsquo;ll do it properly, costly enough that you won&amp;rsquo;t paper over a real finding by reflex.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;checkov&lt;/code&gt; was right that the state-bucket key grants the account root &lt;code&gt;kms:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt;. It was wrong that I should narrow it, because that statement is AWS&amp;rsquo;s documented default and the thing that stops the key becoming permanently unadministrable. The fix was a scoped &lt;code&gt;checkov:skip&lt;/code&gt; carrying its reasoning and a link to the AWS docs, so the decision lives next to the code and can be defended rather than merely trusted.&lt;/p&gt;
&lt;p&gt;Treat your scanner as a sharp, tireless colleague who&amp;rsquo;s usually right and occasionally, confidently, about to lock you out of your own key. Read every finding. Obey most of them. And write down, in the open, the rare one you mustn&amp;rsquo;t.&lt;/p&gt;</description></item><item><title>Two telemetry events, one mangled line</title><link>https://blog-570662.gitlab.io/two-events-one-mangled-line/</link><pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-events-one-mangled-line/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-events-one-mangled-line/cover-two-events-one-mangled-line.png" alt="Featured image of post Two telemetry events, one mangled line" /&gt;&lt;p&gt;A line in a log file that no parser would touch. Not a wrong value, not a missing field. Half of one telemetry event spliced into the middle of another, like two people typing into the same text box at once. Which, it turns out, is pretty much exactly what had happened.&lt;/p&gt;
&lt;h2 id="a-format-with-exactly-one-rule"&gt;A format with exactly one rule
&lt;/h2&gt;&lt;p&gt;rust-tool-base writes its telemetry to a file as JSONL: one JSON object per line, newline at the end, next object on the next line. It&amp;rsquo;s a lovely format to work with precisely because it has one rule, and the rule is simple. Every line is a complete object. Honour that and you can &lt;code&gt;tail&lt;/code&gt; it, &lt;code&gt;grep&lt;/code&gt; it, stream it into anything. Break it once and the whole file is suspect, because now a reader can&amp;rsquo;t trust that a line is a line.&lt;/p&gt;
&lt;p&gt;So the one job the file sink has, beyond writing the right bytes, is to never let two events end up sharing a line.&lt;/p&gt;
&lt;h2 id="appending-is-atomic-though-isnt-it"&gt;&amp;ldquo;Appending is atomic, though, isn&amp;rsquo;t it?&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;The mental model I started with, and I suspect I&amp;rsquo;m not alone, was this: open the file with &lt;code&gt;O_APPEND&lt;/code&gt;, write the serialised event, and the operating system tacks it onto the end atomically. Two writers can&amp;rsquo;t tread on each other because each &lt;code&gt;write&lt;/code&gt; goes to wherever the end currently is, no questions asked. I&amp;rsquo;d half-remembered &lt;code&gt;O_APPEND&lt;/code&gt; as the thing that makes concurrent appending safe, full stop.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s half true, and the half that&amp;rsquo;s missing is the half that bit me.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;O_APPEND&lt;/code&gt; does guarantee one thing: the seek-to-end and the write happen as a unit, so you never get the classic lost-update where two writers compute the same offset and clobber each other. Good. What it does &lt;em&gt;not&lt;/em&gt; guarantee, on POSIX, is that a single &lt;code&gt;write()&lt;/code&gt; of arbitrary size is atomic with respect to other writers. That atomicity has a ceiling, and the ceiling is &lt;code&gt;PIPE_BUF&lt;/code&gt;: 4096 bytes on Linux. Under it, a write is all-or-nothing against other writes to the same file. Over it, the kernel is entirely within its rights to split your write into chunks, and another writer&amp;rsquo;s bytes can land in the gap between them.&lt;/p&gt;
&lt;h2 id="the-fat-event-that-went-over-the-edge"&gt;The fat event that went over the edge
&lt;/h2&gt;&lt;p&gt;For a long time nothing went wrong, which is the most dangerous way for a bug like this to behave. A typical event, a command name, a duration, a status, an attribute or two, serialises to a few hundred bytes. Comfortably under four kilobytes, so comfortably inside the atomic window. Hundreds of them a day, never a problem.&lt;/p&gt;
&lt;p&gt;Then an event came along with a lot of attributes on it, and its serialised form sailed past 4 KiB. Two of &lt;em&gt;those&lt;/em&gt; emitted at roughly the same moment, both over the line, and &lt;code&gt;O_APPEND&lt;/code&gt; did the only thing it had ever promised: it put each write at the end. It said nothing about not interleaving the bytes on the way, because past &lt;code&gt;PIPE_BUF&lt;/code&gt; that was never on offer. One spliced line, one file a parser would now choke on.&lt;/p&gt;
&lt;h2 id="the-fix-isnt-a-bigger-write-its-a-smaller-gate"&gt;The fix isn&amp;rsquo;t a bigger write, it&amp;rsquo;s a smaller gate
&lt;/h2&gt;&lt;p&gt;You can&amp;rsquo;t buy your way out of this with a bigger buffer, because there&amp;rsquo;s no buffer size that&amp;rsquo;s reliably atomic above &lt;code&gt;PIPE_BUF&lt;/code&gt;. The fix is to stop relying on the kernel for mutual exclusion you can do yourself: serialise the events through a lock, so only one &lt;code&gt;write&lt;/code&gt; is ever in flight at a time. The &lt;code&gt;FileSink&lt;/code&gt; carries a mutex for exactly that, and the doc comment on it is the whole post in a paragraph, from &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-telemetry/src/sink.rs#L99" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-telemetry/src/sink.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;FileSink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;: &lt;span class="nc"&gt;PathBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Serialises concurrent `emit` calls. Shared across `Clone`s of
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// the same `FileSink` so multiple handles to the same path also
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// serialise correctly.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;: &lt;span class="nc"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;tokio&lt;/span&gt;::&lt;span class="n"&gt;sync&lt;/span&gt;::&lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you don&amp;rsquo;t write Rust day to day (the &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;primer&lt;/a&gt; has the rest of the basics): &lt;code&gt;tokio::sync::Mutex&lt;/code&gt; is an async-aware lock, &lt;code&gt;.await&lt;/code&gt; is where a task waits its turn for that lock without blocking the whole thread, and the &lt;code&gt;Arc&lt;/code&gt; wrapper is shared ownership. That &lt;code&gt;Arc&lt;/code&gt; is the load-bearing bit: it means every clone of the &lt;code&gt;FileSink&lt;/code&gt; points at the &lt;em&gt;same&lt;/em&gt; gate, rather than each getting its own lock that guards nothing.&lt;/p&gt;
&lt;p&gt;The detail I like is &lt;em&gt;where&lt;/em&gt; the lock sits. The event is serialised to a string first, outside the critical section, because turning an event into JSON is the expensive part and there&amp;rsquo;s no reason to hold the gate while you do it. Only then does &lt;code&gt;emit&lt;/code&gt; take the lock, and it holds it across the whole open-write-flush, so no other emit can interleave a single byte:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Serialise the line outside the critical section.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;serde_json&lt;/span&gt;::&lt;span class="n"&gt;to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;redacted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_guard&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...create parent dir, open with append(true)...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;write_all&lt;/code&gt; makes sure the whole line goes out as one logical write from our side, and the gate makes sure ours is the only one happening. The 4 KiB cliff is still there in the kernel. We just never walk near it any more, because we&amp;rsquo;ve serialised the writers ourselves rather than hoping the OS would.&lt;/p&gt;
&lt;h2 id="the-bit-even-the-lock-cant-fix"&gt;The bit even the lock can&amp;rsquo;t fix
&lt;/h2&gt;&lt;p&gt;There is however a genuine limit, and the comment is upfront about it. The mutex lives in the process. Two &lt;code&gt;FileSink&lt;/code&gt;s in two &lt;em&gt;different&lt;/em&gt; processes, both pointed at the same file, are back to relying on &lt;code&gt;O_APPEND&lt;/code&gt; alone, and back under the 4 KiB ceiling. The lock can&amp;rsquo;t reach across a process boundary, so it doesn&amp;rsquo;t pretend to. The guidance there is the older, duller, correct one: give each process its own file and aggregate them somewhere else. Don&amp;rsquo;t have two processes fighting over one log file and expect the filesystem to referee.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;O_APPEND&lt;/code&gt; is a real guarantee, just a much smaller one than its name talks you into. It keeps your write at the end of the file, and it keeps concurrent writes from interleaving only while they stay under &lt;code&gt;PIPE_BUF&lt;/code&gt;, which on Linux is 4096 bytes. A fat JSON event slides straight over that and takes your file&amp;rsquo;s one rule with it.&lt;/p&gt;
&lt;p&gt;The fix was never exotic. Serialise the line, take a mutex, do the write under it, and the interleave can&amp;rsquo;t happen because there&amp;rsquo;s only ever one writer at a time. The POSIX manual had all of this written down long before I went and learned it the interesting way, which is, I&amp;rsquo;m told, how most people meet &lt;code&gt;PIPE_BUF&lt;/code&gt; too.&lt;/p&gt;</description></item><item><title>A state bucket that defends itself</title><link>https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-state-bucket-that-defends-itself/cover-a-state-bucket-that-defends-itself.png" alt="Featured image of post A state bucket that defends itself" /&gt;&lt;p&gt;OpenTofu&amp;rsquo;s remote state file is, quietly, the most sensitive thing in an infrastructure repo. It&amp;rsquo;s a plain JSON document listing every resource you manage, every ID, and, depending on your providers, the odd secret in clear text. So the S3 bucket that holds it can&amp;rsquo;t just be a bucket. It has to actively defend itself, on three separate fronts.&lt;/p&gt;
&lt;h2 id="the-most-sensitive-file-in-the-repo"&gt;The most sensitive file in the repo
&lt;/h2&gt;&lt;p&gt;OpenTofu, like Terraform, keeps a state file: a JSON document recording every resource the stack manages, its real-world ID, and its attributes. It&amp;rsquo;s how the tool knows what already exists. It&amp;rsquo;s also, quietly, the most sensitive file in the whole repo. It can hold resource identifiers an attacker would value, and depending on the providers in play it can hold secret values in clear text.&lt;/p&gt;
&lt;p&gt;Three bad things can happen to it. It can be deleted, and now the tool has forgotten everything it manages. It can be read by someone who shouldn&amp;rsquo;t. It can be corrupted by two runs writing at once. The bucket that holds remote state has to defend against all three, and &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&amp;rsquo;s &lt;code&gt;state-backend&lt;/code&gt; module is built around doing exactly that.&lt;/p&gt;
&lt;h2 id="the-dynamodb-lock-table-is-gone"&gt;The DynamoDB lock table is gone
&lt;/h2&gt;&lt;p&gt;Start with the corruption problem, because the answer changed recently.&lt;/p&gt;
&lt;p&gt;The long-standing pattern for remote state on AWS was an S3 bucket &lt;em&gt;plus&lt;/em&gt; a DynamoDB table. S3 held the state; the DynamoDB table held a lock, so two &lt;code&gt;apply&lt;/code&gt; runs couldn&amp;rsquo;t write at once. Everyone who&amp;rsquo;s done Terraform on AWS has provisioned that table, probably more times than they&amp;rsquo;d care to count.&lt;/p&gt;
&lt;p&gt;OpenTofu 1.10 made it unnecessary. The S3 backend gained &lt;code&gt;use_lockfile&lt;/code&gt;, which does the locking with a small lock &lt;em&gt;object&lt;/em&gt; in the same bucket, using S3&amp;rsquo;s conditional-write support. No separate table. The state backend is now genuinely one bucket and one key, with the lock living beside the state. It&amp;rsquo;s one fewer resource to create, one fewer thing to pay for, and one fewer moving part to reason about. The module takes the new path, and the DynamoDB table simply isn&amp;rsquo;t there.&lt;/p&gt;
&lt;h2 id="a-bucket-you-cant-delete-by-accident"&gt;A bucket you can&amp;rsquo;t delete by accident
&lt;/h2&gt;&lt;p&gt;Deletion is guarded with &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/blob/v0.2.0/modules/state-backend/main.tf#L66" target="_blank" rel="noopener"
 &gt;&lt;code&gt;lifecycle { prevent_destroy = true }&lt;/code&gt;&lt;/a&gt; on the bucket. With that set, OpenTofu refuses to produce a plan that would destroy the bucket. A stray &lt;code&gt;tofu destroy&lt;/code&gt;, a refactor that drops the resource, an accidental rename: all of them fail loudly instead of quietly taking the state bucket with them.&lt;/p&gt;
&lt;p&gt;This is also why the &lt;code&gt;state-backend&lt;/code&gt; module is hand-rolled from raw &lt;code&gt;aws_s3_bucket&lt;/code&gt; resources rather than wrapping a community module like &lt;code&gt;terraform-aws-modules/s3-bucket&lt;/code&gt;. &lt;code&gt;prevent_destroy&lt;/code&gt; has to sit on the actual resource, and a &lt;code&gt;lifecycle&lt;/code&gt; block isn&amp;rsquo;t something you can pass into a wrapper module as an input. Hand-rolling the bucket keeps &lt;code&gt;prevent_destroy&lt;/code&gt; somewhere you can put it and, just as importantly, somewhere the next reader can see it. (There&amp;rsquo;s a whole post coming on &lt;a class="link" href="https://blog-570662.gitlab.io/why-i-hand-rolled-every-module/" &gt;why I hand-rolled every module&lt;/a&gt;; this is one of the reasons in miniature.)&lt;/p&gt;
&lt;h2 id="reject-anything-encrypted-wrong"&gt;Reject anything encrypted wrong
&lt;/h2&gt;&lt;p&gt;Confidentiality is the subtle one, because the obvious control isn&amp;rsquo;t enough.&lt;/p&gt;
&lt;p&gt;The bucket has a default encryption configuration: server-side encryption with the customer-managed KMS key. But default encryption is a &lt;em&gt;default&lt;/em&gt;. A client making a &lt;code&gt;PutObject&lt;/code&gt; call can override it per request, asking for plain &lt;code&gt;AES256&lt;/code&gt; or a different KMS key, and S3 will honour the override.&lt;/p&gt;
&lt;p&gt;So the module doesn&amp;rsquo;t rely on the default. The &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/blob/v0.2.0/modules/state-backend/main.tf#L161" target="_blank" rel="noopener"
 &gt;bucket policy&lt;/a&gt; explicitly denies the upload it doesn&amp;rsquo;t want. It denies any request not over TLS. It denies any &lt;code&gt;PutObject&lt;/code&gt; that isn&amp;rsquo;t using SSE-KMS. And it denies any &lt;code&gt;PutObject&lt;/code&gt; that names the &lt;em&gt;wrong&lt;/em&gt; KMS key. The default encryption config says &amp;ldquo;this is what you get if you don&amp;rsquo;t ask&amp;rdquo;; the bucket policy says &amp;ldquo;and you&amp;rsquo;re not allowed to ask for anything else&amp;rdquo;. State can only ever land encrypted, in transit and at rest, under the one key the module controls.&lt;/p&gt;
&lt;p&gt;One small companion setting: &lt;code&gt;bucket_key_enabled&lt;/code&gt;. With per-object SSE-KMS, every object operation is also a KMS API call, which costs money and can throttle. An S3 Bucket Key collapses those into far fewer KMS calls, cutting per-object KMS traffic by well over ninety per cent. It&amp;rsquo;s a one-line setting the module turns on and most people forget exists.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;Remote state is the most sensitive file an infrastructure repo has, and the bucket that holds it has to defend against deletion, disclosure and corruption.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&amp;rsquo;s state backend handles corruption with OpenTofu 1.10&amp;rsquo;s &lt;code&gt;use_lockfile&lt;/code&gt;, dropping the old DynamoDB lock table entirely. It guards deletion with &lt;code&gt;prevent_destroy&lt;/code&gt;, which is also why the bucket is hand-rolled rather than wrapped. And it guards confidentiality with a bucket policy that denies non-TLS traffic and denies any upload not encrypted with the right KMS key, because default encryption is only a default and a client can override it. The state bucket isn&amp;rsquo;t just a place to put state. It&amp;rsquo;s built to refuse every wrong thing that could happen to it.&lt;/p&gt;</description></item><item><title>Supporting a provider, or actually using it</title><link>https://blog-570662.gitlab.io/supporting-a-provider-or-actually-using-it/</link><pubDate>Sat, 02 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/supporting-a-provider-or-actually-using-it/</guid><description>&lt;img src="https://blog-570662.gitlab.io/supporting-a-provider-or-actually-using-it/cover-supporting-a-provider-or-actually-using-it.png" alt="Featured image of post Supporting a provider, or actually using it" /&gt;&lt;p&gt;If your CLI tool talks to an AI model, you don&amp;rsquo;t want to hard-wire one vendor. So you reach for a single client interface over several providers, which is the right call. The trap is the next step: build that interface on only what every provider has in common, and you quietly throw away the very features that made you want a particular provider in the first place. rust-tool-base&amp;rsquo;s &lt;code&gt;rtb-ai&lt;/code&gt; refuses to make that trade.&lt;/p&gt;
&lt;h2 id="the-pull-toward-one-interface"&gt;The pull toward one interface
&lt;/h2&gt;&lt;p&gt;If your CLI tool talks to an AI model, hard-wiring one vendor is a poor bet. One user has an Anthropic key, another an OpenAI key. Someone&amp;rsquo;s on Gemini. Someone runs Ollama locally because their data can&amp;rsquo;t leave the building. Someone points at an OpenAI-compatible endpoint from a provider you&amp;rsquo;ve never heard of. You don&amp;rsquo;t want a separate code path for each, so you want one &lt;code&gt;AiClient&lt;/code&gt; that all of them slot behind.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-ai&lt;/code&gt; gets that unification from the &lt;code&gt;genai&lt;/code&gt; crate, which already speaks to Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible endpoints. One interface, five providers, the tool author picks one in config. The Go sibling makes the same bet: go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package also unifies several providers, behind &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;an interface deliberately kept to four methods&lt;/a&gt;. So far this is the obvious design, and if it were the whole design there&amp;rsquo;d be nothing to write about.&lt;/p&gt;
&lt;h2 id="what-unified-quietly-costs-you"&gt;What &amp;ldquo;unified&amp;rdquo; quietly costs you
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the catch in any unified interface. It can only expose what every provider behind it has in common.&lt;/p&gt;
&lt;p&gt;The common subset is plain chat. Messages go in, text comes out, optionally streamed token by token. That&amp;rsquo;s real and it&amp;rsquo;s useful and every provider does it. But the common subset is also the &lt;em&gt;floor&lt;/em&gt;, and the features that make a particular provider worth choosing are almost never on the floor. They&amp;rsquo;re the things only that provider does.&lt;/p&gt;
&lt;p&gt;Anthropic is the sharp example, because it has three features that matter and not one of them is common-subset.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt caching.&lt;/strong&gt; You can mark the stable parts of a request, the system prompt and the tool list, as cacheable. The provider keeps them warm, and on the next turn you aren&amp;rsquo;t billed to re-send and re-process text that didn&amp;rsquo;t change. On a long agent loop, where the same large system prompt rides along on every single turn, that&amp;rsquo;s a substantial saving in both cost and latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extended thinking.&lt;/strong&gt; The model works through a hard problem in a visible, budgeted reasoning pass before it commits to an answer, and you can see that reasoning.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Citations.&lt;/strong&gt; Structured references back to source material in the response.&lt;/p&gt;
&lt;p&gt;A client built strictly on the common subset can&amp;rsquo;t express any of those. It has no field for them, because four of the five providers wouldn&amp;rsquo;t know what to do with the field. So a purely lowest-common-denominator client would &amp;ldquo;support&amp;rdquo; Anthropic and then use it badly, leaving its best features unreachable. Support as a checkbox, not as the point.&lt;/p&gt;
&lt;h2 id="the-escape-hatch"&gt;The escape hatch
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;rtb-ai&lt;/code&gt;&amp;rsquo;s answer is to not choose. It runs two implementations under one interface.&lt;/p&gt;
&lt;p&gt;For OpenAI, Gemini, Ollama and OpenAI-compatible endpoints, calls route through &lt;code&gt;genai&lt;/code&gt;, the unified path. For Anthropic, every method drops to a &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-ai/src/anthropic.rs#L1" target="_blank" rel="noopener"
 &gt;direct &lt;code&gt;reqwest&lt;/code&gt; implementation&lt;/a&gt; straight against the Messages API. Same &lt;code&gt;AiClient&lt;/code&gt; on the surface, a different implementation underneath, selected by which provider the config names.&lt;/p&gt;
&lt;p&gt;And the request type has deliberate room for the difference:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;ChatRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;: &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;u32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Anthropic-only: enables prompt caching at every stable point.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Ignored on non-Anthropic providers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cache_control&lt;/span&gt;: &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Anthropic-only: extended-thinking budget. `None` disables.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sd"&gt;/// Ignored on non-Anthropic providers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;thinking&lt;/span&gt;: &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThinkingMode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Set &lt;code&gt;cache_control&lt;/code&gt; and the Anthropic-direct path inserts cache breakpoints at the three stable points: the system prompt, the tool list, and the first message. Set &lt;code&gt;thinking&lt;/code&gt; and it adds the thinking block, and streaming surfaces a separate &lt;code&gt;ThinkingToken&lt;/code&gt; event so you can show the reasoning apart from the answer. On a non-Anthropic provider, both fields are simply ignored. The interface carries them; only the implementation that understands them acts on them.&lt;/p&gt;
&lt;h2 id="a-hatch-not-a-leak"&gt;A hatch, not a leak
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s worth being precise about why this isn&amp;rsquo;t the thing it superficially resembles, which is a leaky abstraction.&lt;/p&gt;
&lt;p&gt;A leaky abstraction is one where implementation details bleed through that you didn&amp;rsquo;t intend and can&amp;rsquo;t reason about. The abstraction quietly fails to abstract, and you&amp;rsquo;re left guessing which provider you&amp;rsquo;re really talking to.&lt;/p&gt;
&lt;p&gt;This is the opposite of that. The two Anthropic-only fields aren&amp;rsquo;t a leak. They&amp;rsquo;re named, documented as Anthropic-only, inert everywhere else, and right there in the public type for anyone to see. The interface is uniform for the common case and &lt;em&gt;deliberately, visibly&lt;/em&gt; non-uniform at exactly the points where uniformity would have cost you the good features. You opt into provider-specifics by setting a field. You stay fully portable by leaving it at its default. Nothing bleeds; you decide.&lt;/p&gt;
&lt;p&gt;The same design line explains what &lt;em&gt;does&lt;/em&gt; stay in the unified path. Structured output, &lt;code&gt;chat_structured::&amp;lt;T&amp;gt;&lt;/code&gt;, sends a JSON Schema derived from your Rust type with the request and validates the reply against it before handing you a typed &lt;code&gt;T&lt;/code&gt;. That&amp;rsquo;s a portability win that costs nothing across providers, so it belongs in the common interface. The split isn&amp;rsquo;t &amp;ldquo;Anthropic versus the rest&amp;rdquo;. It&amp;rsquo;s &amp;ldquo;features that are free to unify go in the unified path; features that aren&amp;rsquo;t get a designed door&amp;rdquo;. Prompt caching and extended thinking get the door, because flattening them away would be the expensive kind of convenient.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;A CLI tool that integrates AI wants one client over several providers, and a unified interface can only expose what those providers share. The shared floor is plain chat, and the features worth choosing a provider &lt;em&gt;for&lt;/em&gt;, like Anthropic&amp;rsquo;s prompt caching, extended thinking and citations, are never on the floor.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rtb-ai&lt;/code&gt; keeps both. &lt;code&gt;genai&lt;/code&gt; provides the unified path across five providers; an Anthropic-direct &lt;code&gt;reqwest&lt;/code&gt; path drops below the abstraction for the features &lt;code&gt;genai&lt;/code&gt; can&amp;rsquo;t reach, and &lt;code&gt;ChatRequest&lt;/code&gt; carries the Anthropic-only fields openly, ignored elsewhere. Uniform where uniformity is free, with a designed escape hatch where it isn&amp;rsquo;t. That&amp;rsquo;s the difference between supporting a provider and actually using it.&lt;/p&gt;</description></item><item><title>Errors without an error handler</title><link>https://blog-570662.gitlab.io/errors-without-an-error-handler/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-without-an-error-handler/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-without-an-error-handler/cover-errors-without-an-error-handler.png" alt="Featured image of post Errors without an error handler" /&gt;&lt;p&gt;In &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt; I said go-tool-base&amp;rsquo;s error handler was one of the bits that &lt;em&gt;didn&amp;rsquo;t&lt;/em&gt; survive the move to Rust, and promised to come back to it. Here&amp;rsquo;s the come-back. The short version is that Rust hands you, for free, the single consistent error exit that go-tool-base had to build a whole component to get.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-built"&gt;What go-tool-base built
&lt;/h2&gt;&lt;p&gt;A while ago I &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;wrote about error handling in go-tool-base&lt;/a&gt;. The core of it: an error should carry a &lt;em&gt;hint&lt;/em&gt;, a separate field of human guidance telling the user what to do next, kept apart from the error&amp;rsquo;s identity so code can still match on it.&lt;/p&gt;
&lt;p&gt;The other half of that post was about consistency. Every go-tool-base command returns its errors the idiomatic Cobra way, and they all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. Presentation decided in exactly one place, so no command can render a failure differently from its neighbour.&lt;/p&gt;
&lt;p&gt;That handler is a real object. It exists, it&amp;rsquo;s wired in, it&amp;rsquo;s the thing every error passes through. Building it was a deliberate piece of work, and it was the right call for Go.&lt;/p&gt;
&lt;p&gt;When I rebuilt this in Rust, the handler didn&amp;rsquo;t survive the move. Not because consistency stopped mattering. Because Rust gives you the single exit for free, and an object to enforce it would just be re-implementing something the language already does for you.&lt;/p&gt;
&lt;h2 id="the-shape-of-a-rust-error"&gt;The shape of a Rust error
&lt;/h2&gt;&lt;p&gt;Start with the type. In rust-tool-base every crate defines its own error enum, and every one of them derives two traits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[derive(Debug, thiserror::Error, miette::Diagnostic)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;ConfigError&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;config file not found at {path}&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;#[diagnostic(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; code(rtb::config::not_found),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; help(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;run `mytool init` to create one, or set MYTOOL_CONFIG&amp;#34;&lt;/span&gt;&lt;span class="cp"&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt; )]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NotFound&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;: &lt;span class="nc"&gt;PathBuf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;thiserror::Error&lt;/code&gt; makes it a proper error type. &lt;code&gt;miette::Diagnostic&lt;/code&gt; is the interesting one. A &lt;code&gt;Diagnostic&lt;/code&gt; is an error that also carries the things you&amp;rsquo;d want when &lt;em&gt;presenting&lt;/em&gt; it: a stable &lt;code&gt;code&lt;/code&gt;, a severity, a &lt;code&gt;help&lt;/code&gt; string, and optionally source labels pointing at spans of input. The &lt;code&gt;help&lt;/code&gt; line is the same idea as go-tool-base&amp;rsquo;s hint, the recovery step, except here it&amp;rsquo;s an attribute on the variant rather than a field threaded through a wrapper.&lt;/p&gt;
&lt;p&gt;So the guidance lives on the error, structured, from the moment the error is created.&lt;/p&gt;
&lt;h2 id="there-is-no-handler-theres-a-convention"&gt;There is no handler, there&amp;rsquo;s a convention
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where Rust does the work go-tool-base&amp;rsquo;s handler was built to do.&lt;/p&gt;
&lt;p&gt;A rust-tool-base &lt;code&gt;main&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[tokio::main]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;miette&lt;/span&gt;::&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rtb&lt;/span&gt;::&lt;span class="n"&gt;cli&lt;/span&gt;::&lt;span class="n"&gt;Application&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VersionInfo&lt;/span&gt;::&lt;span class="n"&gt;from_env&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;main&lt;/code&gt; returns &lt;code&gt;miette::Result&amp;lt;()&amp;gt;&lt;/code&gt;. Every command&amp;rsquo;s &lt;code&gt;run&lt;/code&gt; returns a &lt;code&gt;Result&lt;/code&gt; too. In between, errors propagate with the &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;&lt;code&gt;?&lt;/code&gt; operator&lt;/a&gt;: a function that hits an error returns it upward, immediately, and the caller does the same, all the way to &lt;code&gt;main&lt;/code&gt;. Nobody writes a &amp;ldquo;check this error&amp;rdquo; call. &lt;code&gt;?&lt;/code&gt; is the propagation.&lt;/p&gt;
&lt;p&gt;And when an error reaches &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;main&lt;/code&gt; returns it, &lt;em&gt;something&lt;/em&gt; has to render it for the user. That something is a &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-error/src/hook.rs#L41" target="_blank" rel="noopener"
 &gt;report hook&lt;/a&gt;. rust-tool-base installs one at startup, and from then on any &lt;code&gt;Diagnostic&lt;/code&gt; that exits &lt;code&gt;main&lt;/code&gt; is rendered through it: the code, the severity, the help text, the source labels, with colour. One renderer, installed once.&lt;/p&gt;
&lt;p&gt;Look at what that adds up to. Every error in the program flows to one place, &lt;code&gt;main&lt;/code&gt;. It&amp;rsquo;s rendered by one thing, the hook. Presentation is decided in exactly one location and no command can deviate from it. That&amp;rsquo;s precisely the property go-tool-base&amp;rsquo;s &lt;code&gt;ErrorHandler&lt;/code&gt; was built to guarantee. The difference is that nobody built it. The single exit is just where &lt;code&gt;?&lt;/code&gt; propagation ends, and the single renderer is one hook. The language&amp;rsquo;s own convention for returning errors from &lt;code&gt;main&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; the funnel.&lt;/p&gt;
&lt;h2 id="errors-are-values-all-the-way"&gt;Errors are values, all the way
&lt;/h2&gt;&lt;p&gt;The thing that took me a moment to fully trust is that there&amp;rsquo;s no funnel to maintain, because there&amp;rsquo;s no funnel as an object. go-tool-base&amp;rsquo;s handler is a component: it can drift, it has to be kept in the path, a command could in principle be wired to bypass it. The Rust version cannot be bypassed, because bypassing it would mean a command not returning its error, and an error you don&amp;rsquo;t return is a compile-time warning at best and dead-obvious wrong code at worst.&lt;/p&gt;
&lt;p&gt;So the model is just: errors are values, you return them, &lt;code&gt;?&lt;/code&gt; carries them up, &lt;code&gt;main&lt;/code&gt; hands the last one to the hook. The consistency isn&amp;rsquo;t enforced by a guard. It&amp;rsquo;s the only thing the shape of the language really lets you do.&lt;/p&gt;
&lt;p&gt;go-tool-base reaches a single, consistent error exit by building one and routing everything through it. rust-tool-base reaches the same exit by having errors be ordinary return values and letting them fall out of &lt;code&gt;main&lt;/code&gt;. Same outcome. One of them is a component you own; the other is a convention you inherit.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;go-tool-base funnels every error through one &lt;code&gt;ErrorHandler&lt;/code&gt; so presentation stays consistent. That handler is a deliberately built component, and it&amp;rsquo;s the right design in Go.&lt;/p&gt;
&lt;p&gt;rust-tool-base has no handler. Every crate&amp;rsquo;s error type derives &lt;code&gt;miette::Diagnostic&lt;/code&gt;, carrying its code, severity and help text. Errors propagate with &lt;code&gt;?&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;, which returns &lt;code&gt;miette::Result&lt;/code&gt;, and a framework-installed hook renders whatever comes out. The single consistent exit is the end of &lt;code&gt;?&lt;/code&gt; propagation, and the single renderer is one hook. The funnel go-tool-base built by hand is, in Rust, just the language&amp;rsquo;s return-from-&lt;code&gt;main&lt;/code&gt; convention.&lt;/p&gt;</description></item><item><title>The bootstrap that does almost nothing</title><link>https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-bootstrap-that-does-almost-nothing/cover-the-bootstrap-that-does-almost-nothing.png" alt="Featured image of post The bootstrap that does almost nothing" /&gt;&lt;p&gt;A brand-new AWS account is a slightly nerve-wracking thing. It can do almost anything, it&amp;rsquo;s hardened against almost nothing, and the list of stuff you ought to set up before you trust it with anything real is long. The natural instinct is to write one big &amp;ldquo;set up the account&amp;rdquo; module that does the whole list in a single apply. I want to talk you out of that, because the bootstrap module I&amp;rsquo;m happiest with does almost nothing, on purpose.&lt;/p&gt;
&lt;h2 id="the-first-apply-problem"&gt;The first-apply problem
&lt;/h2&gt;&lt;p&gt;A brand-new AWS account is not ready for anything serious. Before you&amp;rsquo;d responsibly run real infrastructure into it, you want an account baseline: a password policy, account-wide S3 public-access blocking, default EBS encryption, CloudTrail, AWS Config, GuardDuty, alerting, a sensible human operator role. It&amp;rsquo;s a long list, and all of it matters.&lt;/p&gt;
&lt;p&gt;The instinct, faced with that list, is to write one big &amp;ldquo;set up the account&amp;rdquo; module and have it do everything. One &lt;code&gt;tofu apply&lt;/code&gt;, a fully prepared account, done.&lt;/p&gt;
&lt;p&gt;That instinct is worth resisting, and &lt;a class="link" href="https://gitlab.com/phpboyscout/terraform-aws-bootstrap/-/tree/v0.2.0/modules" target="_blank" rel="noopener"
 &gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt;&lt;/a&gt; resists it deliberately.&lt;/p&gt;
&lt;h2 id="three-things-and-a-hard-line"&gt;Three things, and a hard line
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; does three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;state-backend&lt;/code&gt;&lt;/strong&gt;, an S3 bucket and a customer-managed KMS key to hold remote Terraform state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;automation-iam&lt;/code&gt;&lt;/strong&gt;, an OIDC identity provider and an IAM role that CI assumes to apply everything else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;nuke-config&lt;/code&gt;&lt;/strong&gt;, which renders an &lt;a class="link" href="https://github.com/ekristen/aws-nuke" target="_blank" rel="noopener"
 &gt;aws-nuke&lt;/a&gt; configuration scoped to the account, for tearing a throwaway account back down.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s the whole module. Account hardening, CloudTrail, AWS Config, GuardDuty, the operator role, the alerting: none of it is in here. And it&amp;rsquo;s not absent by accident. The README has a section headed &amp;ldquo;what&amp;rsquo;s deliberately NOT in scope&amp;rdquo; that lists those exclusions out loud. The boundary is written down, because the boundary is the design.&lt;/p&gt;
&lt;h2 id="why-the-line-is-exactly-there"&gt;Why the line is exactly there
&lt;/h2&gt;&lt;p&gt;The reason the line sits where it does is the most useful idea in the module.&lt;/p&gt;
&lt;p&gt;Everything bootstrap excludes belongs in a &lt;em&gt;separate&lt;/em&gt; stack, applied &lt;em&gt;through the automation role bootstrap creates&lt;/em&gt;. Bootstrap&amp;rsquo;s only job is to get the account to the point where the next &lt;code&gt;tofu apply&lt;/code&gt; can run properly: somewhere to store state, and an identity to run as. Once those two things exist, hardening the account isn&amp;rsquo;t a special bootstrapping act. It&amp;rsquo;s just another apply, done the normal way: in CI, reviewed, versioned, deployed through the role.&lt;/p&gt;
&lt;p&gt;So the account baseline doesn&amp;rsquo;t need to be bundled into the bootstrap. It needs to be &lt;em&gt;downstream&lt;/em&gt; of it. Bootstrap builds the on-ramp; it doesn&amp;rsquo;t also have to be the motorway.&lt;/p&gt;
&lt;h2 id="a-narrow-module-stays-re-runnable"&gt;A narrow module stays re-runnable
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a practical payoff to the narrowness, and it&amp;rsquo;s about fear.&lt;/p&gt;
&lt;p&gt;Bootstrap is the one stack that &lt;em&gt;can&amp;rsquo;t&lt;/em&gt; be applied through CI, because it&amp;rsquo;s what creates the CI identity in the first place. It runs locally, by a human, rarely. That&amp;rsquo;s exactly the kind of operation you want to be small, boring, and safe to repeat.&lt;/p&gt;
&lt;p&gt;A bootstrap module that also did account hardening would be a large, stateful thing managing dozens of resources. Re-running it would be a held-breath operation. Keeping it to three concerns keeps it the opposite: a small stack you can read top to bottom, re-run without anxiety, and reason about completely. The narrowness isn&amp;rsquo;t minimalism for its own sake. It&amp;rsquo;s what keeps the one human-applied stack trustworthy.&lt;/p&gt;
&lt;h2 id="the-boundary-is-the-feature"&gt;The boundary is the feature
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s tempting to judge a module by how much it does. A bootstrap module is the case where that&amp;rsquo;s exactly backwards. Its value is in how cleanly it stops.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; does the bare minimum to make an account ready for the next apply, writes down everything it refuses to do, and hands off to a downstream stack for all of it. The next post follows the trickiest of its three jobs: the state backend has a &lt;a class="link" href="https://blog-570662.gitlab.io/the-chicken-and-egg-of-remote-state/" &gt;genuine chicken-and-egg problem&lt;/a&gt;, because it has to store Terraform state in a bucket Terraform hasn&amp;rsquo;t created yet.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;A fresh AWS account needs a long list of things before it&amp;rsquo;s safe, and the obvious move is one big module that does the lot. &lt;code&gt;terraform-aws-bootstrap&lt;/code&gt; deliberately does only three: a state backend, a CI identity, and an account-scrub config. Everything else is written down as out of scope.&lt;/p&gt;
&lt;p&gt;The boundary is the design. The excluded work belongs in a downstream stack applied through the CI role bootstrap creates, so hardening is just a normal reviewed apply rather than a bootstrapping special case. And keeping the one human-run, locally-applied stack small is what keeps it safe to re-run. A bootstrap module is judged by where it stops.&lt;/p&gt;</description></item><item><title>Two kinds of feature flag</title><link>https://blog-570662.gitlab.io/two-kinds-of-feature-flag/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-kinds-of-feature-flag/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-kinds-of-feature-flag/cover-two-kinds-of-feature-flag.png" alt="Featured image of post Two kinds of feature flag" /&gt;&lt;p&gt;go-tool-base has feature flags: switches that decide which built-in commands are live in a given run. rust-tool-base has those too. But it also has a second, completely separate kind of flag, and the difference between them is one of those distinctions that&amp;rsquo;s obvious the moment you see it and dangerously easy to conflate before you do. One decides what a command &lt;em&gt;does&lt;/em&gt;. The other decides whether a chunk of code is &lt;em&gt;in the binary at all&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="a-workspace-of-crates"&gt;A workspace of crates
&lt;/h2&gt;&lt;p&gt;Before the flags, the shape that makes them possible. go-tool-base is one Go module with packages under &lt;code&gt;pkg/&lt;/code&gt;. rust-tool-base is a &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;Cargo &lt;em&gt;workspace&lt;/em&gt;&lt;/a&gt; of seventeen crates: &lt;code&gt;rtb-app&lt;/code&gt;, &lt;code&gt;rtb-config&lt;/code&gt;, &lt;code&gt;rtb-cli&lt;/code&gt;, &lt;code&gt;rtb-vcs&lt;/code&gt;, &lt;code&gt;rtb-ai&lt;/code&gt;, &lt;code&gt;rtb-mcp&lt;/code&gt;, &lt;code&gt;rtb-docs&lt;/code&gt;, &lt;code&gt;rtb-telemetry&lt;/code&gt;, and so on, with an umbrella crate called &lt;code&gt;rtb&lt;/code&gt; that re-exports the public surface.&lt;/p&gt;
&lt;p&gt;That isn&amp;rsquo;t tidiness for its own sake. Each subsystem being a separately compilable crate is what gives you a unit you can include or exclude &lt;em&gt;wholesale&lt;/em&gt;. Hold onto that, because it&amp;rsquo;s the hinge for everything below.&lt;/p&gt;
&lt;h2 id="the-flag-go-tool-base-already-has"&gt;The flag go-tool-base already has
&lt;/h2&gt;&lt;p&gt;go-tool-base has &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/props/tool.go#L43" target="_blank" rel="noopener"
 &gt;feature flags&lt;/a&gt;, and I&amp;rsquo;d describe them as runtime flags. A tool built on it can enable or disable built-in commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetFeatures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Disable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InitCmd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AiCmd&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At startup the framework resolves that set and decides which commands are reachable for this run. The &lt;code&gt;init&lt;/code&gt; command might be present in the binary but switched off; the &lt;code&gt;ai&lt;/code&gt; command might be switched on. It&amp;rsquo;s about the &lt;em&gt;user-facing surface&lt;/em&gt;: which commands exist for someone typing &lt;code&gt;--help&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base keeps this idea. A command carries a &lt;code&gt;CommandSpec&lt;/code&gt; with an optional &lt;code&gt;feature&lt;/code&gt; field, and the runtime decides whether a feature-gated command is reachable. Same purpose: shape the surface per invocation.&lt;/p&gt;
&lt;p&gt;If that were the whole story, there&amp;rsquo;d be nothing to write. The reason there&amp;rsquo;s a post is the &lt;em&gt;other&lt;/em&gt; kind of flag, which Rust makes available and Go really doesn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="the-flag-rust-adds"&gt;The flag Rust adds
&lt;/h2&gt;&lt;p&gt;Cargo features are a compile-time mechanism. The &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb/Cargo.toml" target="_blank" rel="noopener"
 &gt;&lt;code&gt;rtb&lt;/code&gt; umbrella crate declares them&lt;/a&gt; like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;credentials&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;cli&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rtb-docs?/ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;vcs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-vcs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;telemetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dep:rtb-telemetry&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;full&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cli&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mcp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;credentials&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tui&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;telemetry&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vcs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each subsystem is an &lt;em&gt;optional&lt;/em&gt; crate dependency, and a feature switches it on. This is a different kind of switch entirely, and the difference is the whole point.&lt;/p&gt;
&lt;p&gt;A runtime flag decides what a command does &lt;em&gt;while the program runs&lt;/em&gt;. The code is in the binary either way; the flag just gates it.&lt;/p&gt;
&lt;p&gt;A Cargo feature decides what&amp;rsquo;s &lt;em&gt;in the binary in the first place&lt;/em&gt;. Build a tool without the &lt;code&gt;vcs&lt;/code&gt; feature and &lt;code&gt;rtb-vcs&lt;/code&gt; is not compiled. Its dependencies are not compiled. &lt;code&gt;gix&lt;/code&gt;, the pure-Rust Git implementation &lt;code&gt;rtb-vcs&lt;/code&gt; pulls in, roughly two and a half megabytes of it, is not compiled and not linked. It isn&amp;rsquo;t switched off in the binary. It was never in the binary. The compiler never even saw it.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s something a runtime flag cannot do, because by the time anything runs, the binary already exists with everything in it.&lt;/p&gt;
&lt;h2 id="two-axes-kept-separate"&gt;Two axes, kept separate
&lt;/h2&gt;&lt;p&gt;So rust-tool-base has two flag systems answering two genuinely different questions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cargo features&lt;/strong&gt; answer: &lt;em&gt;what is this binary made of?&lt;/em&gt; They&amp;rsquo;re decided when you build the tool, in &lt;code&gt;Cargo.toml&lt;/code&gt;. They control compilation, binary size, dependency surface, and compile time. A tool that never touches Git builds without &lt;code&gt;vcs&lt;/code&gt; and is smaller, faster to compile, and has a smaller dependency tree to audit. A tool that wants everything turns on &lt;code&gt;full&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Runtime feature flags&lt;/strong&gt; answer: &lt;em&gt;what can the user do in this run?&lt;/em&gt; They&amp;rsquo;re decided as the program starts. They control which commands appear, which paths are reachable.&lt;/p&gt;
&lt;p&gt;These could have been mashed into one mechanism, and it would have been a mistake. The app-context design notes are blunt about it: feature gating doesn&amp;rsquo;t belong on the per-command context object, because a feature-gated command &amp;ldquo;either exists or doesn&amp;rsquo;t&amp;rdquo; rather than changing its behaviour mid-run. Compile-time composition is one decision, made by the person building the tool. Runtime gating is another, made per invocation. Conflating them would mean you couldn&amp;rsquo;t reason cleanly about either.&lt;/p&gt;
&lt;h2 id="the-go-version-of-this-had-to-be-hand-built"&gt;The Go version of this had to be hand-built
&lt;/h2&gt;&lt;p&gt;This isn&amp;rsquo;t a thing Go simply lacks. I &lt;a class="link" href="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/" &gt;wrote a whole post&lt;/a&gt; about how go-tool-base keeps its optional keychain dependency out of binaries that don&amp;rsquo;t want it, using a blank import and the linker&amp;rsquo;s dead-code elimination. It works. But it was a piece of deliberate engineering for &lt;em&gt;one&lt;/em&gt; dependency, and getting it right took care.&lt;/p&gt;
&lt;p&gt;Cargo features make that same outcome a first-class, declarative thing, and not for one dependency but for every subsystem the framework has. You don&amp;rsquo;t engineer the exclusion. You name a feature and leave it off. The crate, and its whole subtree, stays out. Rust&amp;rsquo;s build system was designed for exactly this, and rust-tool-base leans on it across the entire workspace rather than hand-rolling it once.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;go-tool-base has runtime feature flags: they decide, per invocation, which built-in commands are reachable. rust-tool-base keeps that, and adds a second kind that Rust makes available.&lt;/p&gt;
&lt;p&gt;Cargo features decide what the binary is &lt;em&gt;compiled from&lt;/em&gt;. Each of the framework&amp;rsquo;s seventeen crates is an optional dependency, and a feature switched off means that crate and its entire dependency subtree are never compiled or linked. A runtime flag gates what code &lt;em&gt;does&lt;/em&gt;; a Cargo feature gates whether code &lt;em&gt;is there at all&lt;/em&gt;. Two axes, two questions, deliberately kept as separate systems.&lt;/p&gt;</description></item><item><title>forbid means forbid, until linkme needs a word</title><link>https://blog-570662.gitlab.io/forbid-means-forbid-until-linkme-needs-a-word/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/forbid-means-forbid-until-linkme-needs-a-word/</guid><description>&lt;img src="https://blog-570662.gitlab.io/forbid-means-forbid-until-linkme-needs-a-word/cover-forbid-means-forbid-until-linkme-needs-a-word.png" alt="Featured image of post forbid means forbid, until linkme needs a word" /&gt;&lt;p&gt;There&amp;rsquo;s a line at the top of every production crate in rust-tool-base that I&amp;rsquo;m quietly proud of: &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;. And there are a couple of files that have to say &lt;code&gt;#![allow(unsafe_code)]&lt;/code&gt; instead. Not because I wrote anything unsafe. Because a macro did, on my behalf, and &lt;code&gt;forbid&lt;/code&gt; doesn&amp;rsquo;t care whose unsafe it is.&lt;/p&gt;
&lt;h2 id="why-forbid-and-why-it-isnt-the-whole-story"&gt;Why forbid, and why it isn&amp;rsquo;t the whole story
&lt;/h2&gt;&lt;p&gt;rust-tool-base makes a bold promise: &lt;a class="link" href="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/" &gt;no unsafe in its own code&lt;/a&gt;. The strong form of that is &lt;code&gt;forbid&lt;/code&gt;, not &lt;code&gt;deny&lt;/code&gt;. &lt;code&gt;deny(unsafe_code)&lt;/code&gt; makes unsafe a compile error that any module can quietly re-permit with its own &lt;code&gt;#[allow]&lt;/code&gt;. &lt;code&gt;forbid&lt;/code&gt; can&amp;rsquo;t be overridden from inside the crate at all. That&amp;rsquo;s the appeal: nobody gets to wave unsafe through in a hurry.&lt;/p&gt;
&lt;p&gt;So the workspace lint sits at &lt;code&gt;deny&lt;/code&gt;, and every production &lt;code&gt;lib.rs&lt;/code&gt; then tightens it to &lt;code&gt;forbid&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Cargo.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rust&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;unsafe_code&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deny&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// crates/rtb-error/src/lib.rs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#![forbid(unsafe_code)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Why &lt;code&gt;deny&lt;/code&gt; at the workspace but &lt;code&gt;forbid&lt;/code&gt; in each crate? Because &lt;code&gt;deny&lt;/code&gt; leaves an escape hatch open for the rare file that genuinely needs one, while &lt;code&gt;forbid&lt;/code&gt; slams it shut everywhere it can. Almost every file gets &lt;code&gt;forbid&lt;/code&gt;. A tiny number need the hatch.&lt;/p&gt;
&lt;h2 id="the-files-that-need-the-hatch"&gt;The files that need the hatch
&lt;/h2&gt;&lt;p&gt;The command and provider registries use &lt;code&gt;linkme&lt;/code&gt;&amp;rsquo;s &lt;code&gt;distributed_slice&lt;/code&gt; so backends can register themselves at link time, without &lt;a class="link" href="https://blog-570662.gitlab.io/registering-commands-without-life-before-main/" &gt;life before main&lt;/a&gt;. And the &lt;code&gt;linkme&lt;/code&gt; attribute expands to code carrying a &lt;code&gt;#[link_section]&lt;/code&gt;, which the &lt;code&gt;unsafe_code&lt;/code&gt; lint counts as unsafe. So any file using the attribute, whether it declares a slice or registers into one, can&amp;rsquo;t live under &lt;code&gt;forbid&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the Gitea release backend doing exactly that, from &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-vcs/src/gitea.rs#L21" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-vcs/src/gitea.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#![allow(unsafe_code)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;/// Link-time registration entry.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice(RELEASE_PROVIDERS)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;__register_gitea&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProviderRegistration&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Box&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisteredProvider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;source_type&lt;/span&gt;: &lt;span class="s"&gt;&amp;#34;gitea&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;: &lt;span class="nc"&gt;factory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProviderFactory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That &lt;code&gt;#![allow(unsafe_code)]&lt;/code&gt; isn&amp;rsquo;t there because the backend does anything dangerous. It&amp;rsquo;s there because the registration macro emits a &lt;code&gt;#[link_section]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt; would, correctly by its own rules, refuse to compile the file.&lt;/p&gt;
&lt;h2 id="where-that-leaves-the-promise"&gt;Where that leaves the promise
&lt;/h2&gt;&lt;p&gt;The guarantee survives, with an exception you can point at. Every production crate forbids unsafe outright. The &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/Cargo.toml#L37-43" target="_blank" rel="noopener"
 &gt;workspace&lt;/a&gt; sits one notch looser at &lt;code&gt;deny&lt;/code&gt;, precisely so the handful of files that use &lt;code&gt;linkme&lt;/code&gt; (and a couple of test files that need Rust 2024&amp;rsquo;s unsafe env mutation) can open a narrow, module-scoped &lt;code&gt;#![allow(unsafe_code)]&lt;/code&gt; with a written reason. The absolutist rule met a macro that writes a &lt;code&gt;link_section&lt;/code&gt; for you. The answer wasn&amp;rsquo;t to drop the rule, it was to keep &lt;code&gt;forbid&lt;/code&gt; everywhere it can hold and clearly label the one or two spots where it can&amp;rsquo;t.&lt;/p&gt;</description></item><item><title>A framework that contains no unsafe</title><link>https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/</link><pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-framework-that-contains-no-unsafe/cover-a-framework-that-contains-no-unsafe.png" alt="Featured image of post A framework that contains no unsafe" /&gt;&lt;p&gt;&amp;ldquo;It&amp;rsquo;s written in Rust&amp;rdquo; gets thrown around as if it were a memory-safety guarantee. It mostly isn&amp;rsquo;t. Rust is memory-safe by &lt;em&gt;default&lt;/em&gt;, which is a wonderful thing, but the &lt;code&gt;unsafe&lt;/code&gt; keyword exists precisely so any crate, any module, can step outside that default when it needs to. So &amp;ldquo;written in Rust&amp;rdquo; really means &amp;ldquo;mostly safe, probably&amp;rdquo;. rust-tool-base makes the stronger claim about its own code, and gets the compiler to enforce it.&lt;/p&gt;
&lt;h2 id="safe-by-default-is-not-the-same-as-safe"&gt;Safe by default is not the same as safe
&lt;/h2&gt;&lt;p&gt;People reach for Rust because of memory safety, and the reputation is earned. Write ordinary Rust and the compiler will not let you have a use-after-free, a data race, or a buffer overrun. That&amp;rsquo;s the default, and it&amp;rsquo;s a very good default.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a default, and defaults can be turned off. Rust has an &lt;code&gt;unsafe&lt;/code&gt; keyword precisely so that, when you genuinely need to, you can dereference a raw pointer, call into C, or tell the compiler you&amp;rsquo;ve upheld an invariant it can&amp;rsquo;t check itself. Inside an &lt;code&gt;unsafe&lt;/code&gt; block, the guarantees are yours to maintain, not the compiler&amp;rsquo;s to enforce.&lt;/p&gt;
&lt;p&gt;That keyword has to exist. Some of the most foundational crates in the ecosystem are built on it, carefully. But it means a fact worth being precise about: a project being &amp;ldquo;written in Rust&amp;rdquo; tells you its code is &lt;em&gt;mostly&lt;/em&gt; safe. It does not tell you the project&amp;rsquo;s own code contains &lt;em&gt;no&lt;/em&gt; &lt;code&gt;unsafe&lt;/code&gt;. Those are different claims, and only the second one is a guarantee.&lt;/p&gt;
&lt;p&gt;rust-tool-base makes the second claim about its own code, and has the compiler back it up.&lt;/p&gt;
&lt;h2 id="forbid-not-just-deny"&gt;&lt;code&gt;forbid&lt;/code&gt;, not just &lt;code&gt;deny&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The mechanism is one line at the top of every crate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#![forbid(unsafe_code)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;unsafe_code&lt;/code&gt; is a lint, and Rust lints have levels. The interesting choice is &lt;code&gt;forbid&lt;/code&gt; rather than &lt;code&gt;deny&lt;/code&gt;, because the two are not the same strength.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;deny&lt;/code&gt; makes the lint an error. But it&amp;rsquo;s an error a &lt;em&gt;downstream module can locally override&lt;/em&gt;. Anyone can write &lt;code&gt;#[allow(unsafe_code)]&lt;/code&gt; on a function or a block and the &lt;code&gt;deny&lt;/code&gt; is lifted right there. As a policy, &lt;code&gt;deny&lt;/code&gt; is &amp;ldquo;don&amp;rsquo;t do this unless you really mean to&amp;rdquo;, and &amp;ldquo;unless you really mean to&amp;rdquo; is a door.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;forbid&lt;/code&gt; is the strict one. It makes the lint an error &lt;em&gt;and&lt;/em&gt; it makes that error impossible to override from inside the crate. A module cannot &lt;code&gt;#[allow]&lt;/code&gt; its way back out. Once a crate root says &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, there&amp;rsquo;s no &lt;code&gt;unsafe&lt;/code&gt; anywhere in that crate, and no local exception can be carved out. The compiler simply refuses.&lt;/p&gt;
&lt;p&gt;So every rust-tool-base crate that ships in a built tool forbids &lt;code&gt;unsafe&lt;/code&gt; at its root. Not &amp;ldquo;discourages&amp;rdquo;. Cannot contain it.&lt;/p&gt;
&lt;h2 id="the-one-subtlety"&gt;The one subtlety
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a wrinkle, and it&amp;rsquo;s worth showing rather than hiding, because it&amp;rsquo;s where the design got specific.&lt;/p&gt;
&lt;p&gt;The workspace sets &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/Cargo.toml#L43" target="_blank" rel="noopener"
 &gt;&lt;code&gt;unsafe_code = &amp;quot;deny&amp;quot;&lt;/code&gt;&lt;/a&gt; as the baseline for &lt;em&gt;everything&lt;/em&gt;, including test files. But test code occasionally has a real need for &lt;code&gt;unsafe&lt;/code&gt;. In the 2024 edition, &lt;code&gt;std::env::set_var&lt;/code&gt; became &lt;code&gt;unsafe&lt;/code&gt;, because mutating the process environment isn&amp;rsquo;t thread-safe, and a test that exercises environment-driven configuration has to call it.&lt;/p&gt;
&lt;p&gt;So the split is deliberate. The workspace-wide level is &lt;code&gt;deny&lt;/code&gt;, which a test file can locally &lt;code&gt;#[allow]&lt;/code&gt; when it genuinely needs that one environment call. But every production &lt;code&gt;lib.rs&lt;/code&gt; and &lt;code&gt;main.rs&lt;/code&gt; additionally carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt; cannot be relaxed. Test scaffolding gets a controlled, visible exception for a specific standard-library call. Shipping code gets none. The guarantee that matters, &amp;ldquo;the code in the binary contains no &lt;code&gt;unsafe&lt;/code&gt;&amp;rdquo;, holds, and the place it&amp;rsquo;s slightly loosened is exactly the place that never reaches a user.&lt;/p&gt;
&lt;h2 id="what-the-guarantee-is-actually-worth"&gt;What the guarantee is actually worth
&lt;/h2&gt;&lt;p&gt;Two things, one for users and one for reviewers.&lt;/p&gt;
&lt;p&gt;For users: an entire family of bug is ruled out of first-party code mechanically. Use-after-free, double-free, data races on shared memory, reading off the end of a buffer. These are the classic memory-safety vulnerabilities, and in a crate that forbids &lt;code&gt;unsafe&lt;/code&gt; they cannot originate, because the constructs that produce them cannot be written. That&amp;rsquo;s not careful coding. It&amp;rsquo;s the compiler refusing to build anything else.&lt;/p&gt;
&lt;p&gt;For reviewers: the cost of an &lt;code&gt;unsafe&lt;/code&gt; block is mostly the review burden it carries. Every one is a spot where a human has to check, by hand, that an invariant holds, and has to re-check it whenever nearby code changes. A crate that forbids &lt;code&gt;unsafe&lt;/code&gt; has zero of those. There&amp;rsquo;s no &lt;code&gt;unsafe&lt;/code&gt; block to audit, ever, because the compiler guarantees there isn&amp;rsquo;t one.&lt;/p&gt;
&lt;p&gt;The promise has a boundary. It covers rust-tool-base&amp;rsquo;s &lt;em&gt;own&lt;/em&gt; code; its dependencies are another matter, and some of them do contain &lt;code&gt;unsafe&lt;/code&gt;, correctly. Keeping that side honest is a different job, done by &lt;a class="link" href="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/" &gt;vetting the dependency tree and gating it in CI&lt;/a&gt;. Within first-party code, though, the guarantee is real, and there&amp;rsquo;s no Go equivalent to it. Go has an &lt;code&gt;unsafe&lt;/code&gt; package, but nothing that lets a codebase prove, to the compiler, that it never touches it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Rust is memory-safe by default, but the &lt;code&gt;unsafe&lt;/code&gt; keyword exists so that default can be set aside. &amp;ldquo;Written in Rust&amp;rdquo; therefore does not by itself mean a project&amp;rsquo;s own code contains no &lt;code&gt;unsafe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base makes that the stronger claim. Every crate root carries &lt;code&gt;#![forbid(unsafe_code)]&lt;/code&gt;, and &lt;code&gt;forbid&lt;/code&gt;, unlike &lt;code&gt;deny&lt;/code&gt;, cannot be overridden from inside the crate. Test files get a narrow, visible &lt;code&gt;deny&lt;/code&gt;-level exception for the one standard-library call that needs it; shipping code gets none. The payoff is a whole class of memory-safety bug ruled out of first-party code by construction, and not one &lt;code&gt;unsafe&lt;/code&gt; block left for a reviewer to audit.&lt;/p&gt;</description></item><item><title>Reloading config without a restart</title><link>https://blog-570662.gitlab.io/reloading-config-without-a-restart/</link><pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/reloading-config-without-a-restart/</guid><description>&lt;img src="https://blog-570662.gitlab.io/reloading-config-without-a-restart/cover-reloading-config-without-a-restart.png" alt="Featured image of post Reloading config without a restart" /&gt;&lt;p&gt;A config file changes. Someone edits a setting, rotates a credential, flips a feature flag. How does the running process find out? For most processes the answer is blunt: it doesn&amp;rsquo;t, until you restart it. For a short-lived CLI that&amp;rsquo;s completely fine. For a long-running service, &amp;ldquo;just restart it&amp;rdquo; is a much bigger ask than it sounds.&lt;/p&gt;
&lt;h2 id="the-default-answer-is-a-restart"&gt;The default answer is a restart
&lt;/h2&gt;&lt;p&gt;Configuration lives in a file. The file changes: someone edits a setting, rotates a credential, flips a feature flag. How does the running process find out?&lt;/p&gt;
&lt;p&gt;Overwhelmingly, the answer is that it doesn&amp;rsquo;t. A process reads its config once, at startup, and that snapshot is frozen for the life of the process. Change the file and nothing happens until you restart, at which point a fresh process reads the fresh file.&lt;/p&gt;
&lt;p&gt;For a short-lived CLI invocation that&amp;rsquo;s completely fine. It reads config, does its job, exits, and the next invocation reads whatever the file says then. But the same frameworks are also used to build long-running services, and for a service &amp;ldquo;just restart it&amp;rdquo; is not the small thing it sounds like.&lt;/p&gt;
&lt;h2 id="what-a-restart-actually-costs"&gt;What a restart actually costs
&lt;/h2&gt;&lt;p&gt;Restarting a long-running service means every open connection drops. Any in-flight request is lost, or has to be retried by whoever sent it. Caches that took real time to warm are cold again. There&amp;rsquo;s a window, short but real, where the service simply isn&amp;rsquo;t serving.&lt;/p&gt;
&lt;p&gt;If the thing you changed was a log level, or a feature flag, or a timeout, you&amp;rsquo;ve paid a disruption wildly out of proportion to the change. And the calculation only gets worse as the service gets more important, because the services you least want to bounce on a whim are exactly the ones that matter most.&lt;/p&gt;
&lt;h2 id="hot-reload-re-read-in-place"&gt;Hot-reload: re-read in place
&lt;/h2&gt;&lt;p&gt;Hot-reload is the alternative, and both go-tool-base and rust-tool-base support it.&lt;/p&gt;
&lt;p&gt;The process doesn&amp;rsquo;t read config once and freeze it. It &lt;em&gt;watches&lt;/em&gt; the config file. When the file changes, it re-reads it, re-applies it, and carries on running. No new process, no dropped connections, no cold start. The change lands in the live process.&lt;/p&gt;
&lt;p&gt;The shape is the same in both frameworks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A file watcher notices the config file changed. Underneath, this is the operating system&amp;rsquo;s own file-notification facility, &lt;code&gt;inotify&lt;/code&gt; on Linux and its equivalents elsewhere. rust-tool-base reaches it through the &lt;code&gt;notify&lt;/code&gt; crate; go-tool-base, through the watcher built into Viper.&lt;/li&gt;
&lt;li&gt;A debounce step waits for the writes to settle. Saving a file is often several separate operations, and you don&amp;rsquo;t want to reload three times for one edit.&lt;/li&gt;
&lt;li&gt;The config is re-parsed from disk.&lt;/li&gt;
&lt;li&gt;The new config is swapped in atomically.&lt;/li&gt;
&lt;li&gt;Observers are notified, so the subsystems that care can react.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps four and five are the ones worth slowing down on, because they&amp;rsquo;re where a naive hot-reload quietly goes wrong.&lt;/p&gt;
&lt;h2 id="the-two-details-that-make-it-safe"&gt;The two details that make it safe
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;The atomic swap.&lt;/strong&gt; You do not mutate the live config object in place. A reader on another thread, partway through reading it, would see a torn mix of old and new values, and that&amp;rsquo;s a genuinely nasty class of bug. Instead the process builds a &lt;em&gt;new&lt;/em&gt;, complete config value and swaps the pointer to it in a single atomic operation. Any reader sees either the entire old config or the entire new one, never a blend. rust-tool-base does this with &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-config/src/watch.rs" target="_blank" rel="noopener"
 &gt;&lt;code&gt;arc-swap&lt;/code&gt;&lt;/a&gt;; go-tool-base does the equivalent. Reads stay cheap and lock-free, and an update is one pointer swap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The observer notification.&lt;/strong&gt; Re-reading the file isn&amp;rsquo;t the end of the job. Some subsystems have to &lt;em&gt;do something&lt;/em&gt; when config changes: a connection pool resizes, a logger changes level, a rate limiter takes a new ceiling. So a hot-reload system has to let those subsystems subscribe. rust-tool-base hands observers a &lt;code&gt;watch::Receiver&lt;/code&gt;, a channel that always holds the latest value; go-tool-base exposes an &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/config/observer.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Observable&lt;/code&gt; interface&lt;/a&gt;. A subsystem subscribes once and reacts every time config changes, for the life of the process.&lt;/p&gt;
&lt;h2 id="where-this-earns-its-keep-a-kubernetes-pod"&gt;Where this earns its keep: a Kubernetes pod
&lt;/h2&gt;&lt;p&gt;Hot-reload is a nicety on a developer&amp;rsquo;s laptop. Inside a Kubernetes pod it becomes genuinely valuable, and the reason is a neat fit between how Kubernetes delivers config and how a file watcher works.&lt;/p&gt;
&lt;p&gt;In Kubernetes you don&amp;rsquo;t usually bake configuration into the container image. It lives in ConfigMap and Secret objects, and the clean way to consume them is to &lt;em&gt;mount them as volumes&lt;/em&gt;. Mount a ConfigMap as a volume and each key becomes a file in the pod&amp;rsquo;s filesystem.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part that connects to everything above. When you update that ConfigMap or Secret, Kubernetes does not restart your pod. The kubelet notices the object changed and rewrites the projected files inside the still-running pod. The files on disk change underneath a process that never stopped.&lt;/p&gt;
&lt;p&gt;That file rewrite is exactly the event a hot-reload watcher exists to catch. So the whole chain becomes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You &lt;code&gt;kubectl apply&lt;/code&gt; an updated ConfigMap, or rotate a Secret.&lt;/li&gt;
&lt;li&gt;The kubelet updates the projected files inside the pod.&lt;/li&gt;
&lt;li&gt;The framework&amp;rsquo;s file watcher sees the write.&lt;/li&gt;
&lt;li&gt;The config is re-parsed, swapped in atomically, and observers are notified.&lt;/li&gt;
&lt;li&gt;The new configuration is live, and the pod never cycled.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;ve changed a running service, in a running pod, with no rollout, nothing terminated and recreated, no dropped traffic. Rotate a database credential, raise a log level to debug an incident in progress, flip a feature flag: all of it live. For a service where a restart is the very thing you&amp;rsquo;re trying hard to avoid, the kind of &lt;a class="link" href="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/" &gt;long-running service&lt;/a&gt; these frameworks are built for, that&amp;rsquo;s the difference between a config change being routine and being an event.&lt;/p&gt;
&lt;h2 id="the-caveats"&gt;The caveats
&lt;/h2&gt;&lt;p&gt;Two things, so this doesn&amp;rsquo;t read as magic.&lt;/p&gt;
&lt;p&gt;First, not everything can be hot-reloaded. Some configuration genuinely needs a restart: the port a server binds to, the size of a thread pool, anything wired up exactly once at process start. Hot-reload covers the large category of settings a subsystem can re-read and re-apply; it doesn&amp;rsquo;t abolish restarts. A config system worth its salt is clear about which settings are live and which are not.&lt;/p&gt;
&lt;p&gt;Second, a Kubernetes gotcha that catches people out. The in-place file update happens for ConfigMaps and Secrets mounted as &lt;em&gt;volumes&lt;/em&gt;. Consume the same ConfigMap as &lt;em&gt;environment variables&lt;/em&gt; instead, and those are fixed when the container starts and never update, short of a restart. If you want hot-reload in a pod, mount config and secrets as files, not env vars. And even with volumes the update isn&amp;rsquo;t instant: the kubelet syncs on a period, around a minute by default, so a reload is &amp;ldquo;within a minute or so&amp;rdquo;, not &amp;ldquo;the moment you hit apply&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;A config file changes, and the default way to pick it up is to restart the process. For a long-running service that restart costs dropped connections, lost work and a cold start, often for a change as small as a log level.&lt;/p&gt;
&lt;p&gt;go-tool-base and rust-tool-base both support hot-reload instead: a file watcher catches the change, the config is re-parsed and swapped in atomically so no reader sees torn state, and observers are notified so subsystems can react, all in a live process. The setting where it pays off most is a Kubernetes pod, where ConfigMaps and Secrets mounted as volumes are rewritten in place by the kubelet and the watcher catches that write directly. Mount them as volumes rather than env vars, allow for the kubelet&amp;rsquo;s sync delay, accept that some settings still need a restart, and within those limits &amp;ldquo;the config changed&amp;rdquo; stops meaning &amp;ldquo;cycle the pod&amp;rdquo;.&lt;/p&gt;</description></item><item><title>A signing key needs somewhere to live</title><link>https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/cover-a-signing-key-needs-somewhere-to-live.png" alt="Featured image of post A signing key needs somewhere to live" /&gt;&lt;p&gt;I left a door open a couple of posts ago, and it&amp;rsquo;s been quietly bothering me ever since. When I wrote about &lt;a class="link" href="https://blog-570662.gitlab.io/verifying-your-own-downloads/" &gt;verifying your own downloads&lt;/a&gt;, I was honest that a checksum sitting next to the binary only catches accidents. Anyone who can compromise the release platform can swap the binary and the checksum together, and the tool will happily verify one fake against the other.&lt;/p&gt;
&lt;p&gt;Closing that gap needs a signature. And a signature, it turns out, needs a surprising amount of infrastructure standing behind it. This is the first post about building that.&lt;/p&gt;
&lt;h2 id="the-door-the-last-post-left-open"&gt;The door the last post left open
&lt;/h2&gt;&lt;p&gt;A while back I wrote about verifying your own downloads: go-tool-base&amp;rsquo;s self-update command now checks the SHA-256 of every binary it downloads against the release&amp;rsquo;s published &lt;code&gt;checksums.txt&lt;/code&gt; before installing it.&lt;/p&gt;
&lt;p&gt;That post was honest about its own ceiling. A checksum file hosted &lt;em&gt;next to&lt;/em&gt; the binary it describes shares a trust root with that binary. Both come from the same release, on the same platform. Corruption, truncation, a CDN serving a stale object: a same-origin checksum catches all of those, because they&amp;rsquo;re accidents and the checksum wasn&amp;rsquo;t part of the accident. What it can&amp;rsquo;t catch is an attacker who&amp;rsquo;s compromised the release platform itself. Someone who can replace the binary can replace &lt;code&gt;checksums.txt&lt;/code&gt; in the same breath, and the tool will cheerfully verify the malicious download against the malicious checksum and call it good.&lt;/p&gt;
&lt;p&gt;The post named the fix and then deferred it: a signature whose trust root sits somewhere the release platform can&amp;rsquo;t reach. &amp;ldquo;That&amp;rsquo;s the next phase of this work.&amp;rdquo; This series is that phase.&lt;/p&gt;
&lt;h2 id="what-a-signature-actually-needs"&gt;What a signature actually needs
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s worth being precise about why a signature helps where a checksum doesn&amp;rsquo;t, because it&amp;rsquo;s easy to wave the word &amp;ldquo;signature&amp;rdquo; around and assume it settles everything.&lt;/p&gt;
&lt;p&gt;A signature closes the gap only under two conditions. The verifying key, the public half, must reach the user by a path the release platform doesn&amp;rsquo;t control. And the signing key, the private half, must live somewhere the release platform can&amp;rsquo;t reach.&lt;/p&gt;
&lt;p&gt;The second condition is the one people skip. If the signing key sits in the same CI system that builds the release, you&amp;rsquo;ve gained almost nothing. An attacker who owns the CI owns the key, and a key they own will sign whatever they hand it. The signature verifies perfectly and means precisely nothing. A signature is only worth the distance between the signing key and the thing being signed. Put them in the same place and the distance is zero.&lt;/p&gt;
&lt;p&gt;So the signing key has to live in a different security domain from the release pipeline. Not a different folder. A different account, with a different blast radius, that the release platform has no standing access to.&lt;/p&gt;
&lt;h2 id="just-sign-the-binary-is-not-a-small-feature"&gt;&amp;ldquo;Just sign the binary&amp;rdquo; is not a small feature
&lt;/h2&gt;&lt;p&gt;That reframes a line item that sounds tiny. &amp;ldquo;Sign the release binary&amp;rdquo; unpacks into a list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;there must be a private signing key;&lt;/li&gt;
&lt;li&gt;it must live outside the release platform, in its own security domain;&lt;/li&gt;
&lt;li&gt;it must be access-controlled, audited, and protected from exfiltration;&lt;/li&gt;
&lt;li&gt;only the release pipeline may ask it to sign, and only by proving a short-lived, federated identity, never by holding a copy of the key.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s not a feature you bolt onto a CLI. That&amp;rsquo;s infrastructure.&lt;/p&gt;
&lt;p&gt;The shape of it: a cloud account, with the key held in a managed key service so the private key material never exists as a file on a disk that anyone, me included, can copy. The release pipeline authenticates to that account as itself, briefly, and asks the key service to produce a signature. The key never moves.&lt;/p&gt;
&lt;p&gt;But an account you&amp;rsquo;re going to trust with a signing key is itself something you have to get right first. An account with a weak baseline, no audit trail, and long-lived credentials lying around is not a safe home for the most security-sensitive key in the whole system. Before the key can move in, the house has to be built and the locks have to actually work.&lt;/p&gt;
&lt;h2 id="what-this-series-builds"&gt;What this series builds
&lt;/h2&gt;&lt;p&gt;So this turned into a rather longer project than &amp;ldquo;add a signature&amp;rdquo;, and the series follows it in order.&lt;/p&gt;
&lt;p&gt;It starts with bootstrapping a fresh AWS account: the deliberately minimal first &lt;code&gt;tofu apply&lt;/code&gt;, and the remote state backend that has a genuine chicken-and-egg problem. Then the credential question, which is the heart of it: how a CI pipeline deploys to AWS with no stored access key at all. Then hardening the account, so it&amp;rsquo;s genuinely safe to hold something valuable. Then the discipline of deploying changes to it: plans reviewed before they&amp;rsquo;re applied. Then the shared tooling that makes all of it repeatable.&lt;/p&gt;
&lt;p&gt;Every one of those pieces exists for the same reason. The signing key needs somewhere to live, and somewhere safe is not a default you&amp;rsquo;re handed. It&amp;rsquo;s a thing you build, deliberately, before you have anything worth protecting in it.&lt;/p&gt;
&lt;p&gt;The series ends where the verifying-downloads post pointed: a signing service whose key the release platform can&amp;rsquo;t touch, so a self-updating tool can finally verify that the binary it&amp;rsquo;s about to become is genuinely the one I published.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s self-update verifies downloads against a checksum, and a same-origin checksum stops accidents but not a compromise of the release platform. The fix is a signature, and a signature is only worth the distance between its signing key and the release pipeline.&lt;/p&gt;
&lt;p&gt;Holding that key safely means a private key that never leaves a managed key service, in a separate cloud account, reached only by a short-lived federated identity. That&amp;rsquo;s infrastructure, and a safe account is something you build before you trust it with anything. The rest of this series builds it, piece by piece, right up to the signing service itself.&lt;/p&gt;</description></item><item><title>Waivers with an expiry date</title><link>https://blog-570662.gitlab.io/waivers-with-an-expiry-date/</link><pubDate>Sun, 26 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/waivers-with-an-expiry-date/</guid><description>&lt;img src="https://blog-570662.gitlab.io/waivers-with-an-expiry-date/cover-waivers-with-an-expiry-date.png" alt="Featured image of post Waivers with an expiry date" /&gt;&lt;p&gt;A vulnerability scanner gives you a yes or a no. Is there a known advisory on a path you actually use? Yes, or no. That&amp;rsquo;s genuinely useful, and you should run one. But it&amp;rsquo;s a snapshot, taken on the day you ask, and supply-chain risk in a framework is a bigger and more ongoing thing than a single yes-or-no can capture.&lt;/p&gt;
&lt;p&gt;So rust-tool-base treats its whole dependency tree as something to have a &lt;em&gt;policy&lt;/em&gt; about, not something to scan and forget.&lt;/p&gt;
&lt;h2 id="a-scanner-answers-one-question"&gt;A scanner answers one question
&lt;/h2&gt;&lt;p&gt;When I &lt;a class="link" href="https://blog-570662.gitlab.io/every-finding-was-the-same-shape/" &gt;had go-tool-base security-audited&lt;/a&gt;, part of the routine was running a vulnerability scanner over the dependencies. Go has a good one. It looks at your dependency graph, cross-references known advisories, and tells you whether any of them reach code you actually call.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s useful and you should do it. But notice the shape of what it gives back: essentially a yes or a no. Either there&amp;rsquo;s a known vulnerability on a reachable path or there isn&amp;rsquo;t. It answers one question, on the day you ask it.&lt;/p&gt;
&lt;p&gt;Supply-chain risk in a framework is broader than that one question, because a framework drags its entire dependency tree into every tool built on it. rust-tool-base treats the whole tree as something to have a &lt;em&gt;policy&lt;/em&gt; about, and the tool for that is &lt;code&gt;cargo-deny&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-gate-not-a-scan"&gt;A gate, not a scan
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;cargo-deny&lt;/code&gt; reads a &lt;code&gt;deny.toml&lt;/code&gt; and checks the dependency graph against four kinds of rule.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Licences.&lt;/strong&gt; There&amp;rsquo;s an allowlist: MIT, Apache-2.0, the BSD variants, ISC, a handful of others. Every transitive crate&amp;rsquo;s licence has to be on it. A dependency that pulls in something copyleft, or something with no licence at all, fails the build. You find out the first time it enters the tree, not during a release scramble when someone finally reads the legal implications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Advisories.&lt;/strong&gt; It checks the RustSec advisory database, and yanked crates are set to &lt;code&gt;deny&lt;/code&gt;, so a dependency that&amp;rsquo;s been pulled from the registry stops CI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bans.&lt;/strong&gt; Wildcard version requirements (&lt;code&gt;version = &amp;quot;*&amp;quot;&lt;/code&gt;) are denied outright, because a dependency that floats to whatever&amp;rsquo;s newest is a supply-chain hole by construction. Duplicate versions of the same crate get surfaced too.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sources.&lt;/strong&gt; Crates may only come from the official registry. An unknown registry or a stray git dependency is denied. Nothing sneaks in from a URL.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a gate. It encodes, as rules in a file, what the project will and won&amp;rsquo;t accept into its dependency tree, and it enforces them on every build instead of once an audit.&lt;/p&gt;
&lt;h2 id="the-honest-part-is-the-waiver-list"&gt;The honest part is the waiver list
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the thing every real project runs into. Sooner or later there&amp;rsquo;s an advisory you genuinely can&amp;rsquo;t fix this week. It&amp;rsquo;s against a crate three levels down your tree. The fix needs an upstream release that hasn&amp;rsquo;t happened. The crate is scheduled to be reworked two milestones from now anyway. The gate is going to fail, and the work to satisfy it honestly isn&amp;rsquo;t available to you yet.&lt;/p&gt;
&lt;p&gt;The lazy response is a blanket ignore: silence the advisory, move on, forget. Now your gate has a hole in it that nobody remembers opening.&lt;/p&gt;
&lt;p&gt;rust-tool-base&amp;rsquo;s &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/deny.toml#L14" target="_blank" rel="noopener"
 &gt;&lt;code&gt;deny.toml&lt;/code&gt;&lt;/a&gt; does something better. Every waiver in the &lt;code&gt;ignore&lt;/code&gt; list is a documented record. Each one carries a comment that names the crate, traces the &lt;em&gt;exact dependency path&lt;/em&gt; that reaches it, gives the reason, and names the condition that lifts it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# `instant` - reached via async-openai -&amp;gt; backoff -&amp;gt; rtb-ai (v0.3).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2024-0384&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# `paste` - reached via ratatui -&amp;gt; rtb-docs (v0.2) / rtb-tui (v0.4).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2024-0436&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The file states the policy out loud: &amp;ldquo;Every waiver points at a deferred stub crate that will be reworked before its ship milestone. Lift each waiver when the owning crate lands its v0.1.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Some waivers go further and carry a structured reason field, so the &lt;em&gt;why&lt;/em&gt; travels with the entry rather than living only in a comment above it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;RUSTSEC-2025-0140&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gix-date via gix is a stub dependency; rtb-vcs v0.5 will upgrade&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read that list and you don&amp;rsquo;t see a project that quietly stopped caring about seven advisories. You see seven advisories the project knows about, can trace, and has tied to a specific milestone. The waiver has an expiry condition. When &lt;code&gt;rtb-vcs&lt;/code&gt; reaches v0.5, that &lt;code&gt;gix&lt;/code&gt; entry is meant to come out, and the comment is the reminder that it should.&lt;/p&gt;
&lt;h2 id="why-this-is-the-bit-to-copy"&gt;Why this is the bit to copy
&lt;/h2&gt;&lt;p&gt;A gate that can&amp;rsquo;t be relaxed is a gate people route around. They&amp;rsquo;ll find the broadest possible ignore and use it, because the alternative is being blocked on someone else&amp;rsquo;s release. The pressure to do that is real, and it&amp;rsquo;s not unreasonable.&lt;/p&gt;
&lt;p&gt;So the design that actually holds up isn&amp;rsquo;t a stricter gate. It&amp;rsquo;s a gate with an honest, structured escape hatch: you &lt;em&gt;can&lt;/em&gt; waive an advisory, but a waiver costs you a documented record with a dependency path and an expiry condition. That price is small enough that nobody routes around it, and high enough that waivers don&amp;rsquo;t accumulate silently. The &lt;code&gt;ignore&lt;/code&gt; list stays readable, and every line in it is something you could defend out loud.&lt;/p&gt;
&lt;p&gt;Supply-chain hygiene framed this way isn&amp;rsquo;t an audit you survive once a year. It&amp;rsquo;s bookkeeping: a ledger of what you accepted, why, and when each exception is due to close. Which, now I write it down, is just the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;Boy Scout rule&lt;/a&gt; again, pointed at a dependency tree. Leave it tidier than you found it, and write down the bits you couldn&amp;rsquo;t tidy yet.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;A vulnerability scanner answers one question on one day. &lt;code&gt;cargo-deny&lt;/code&gt; is a standing policy gate: licences against an allowlist, advisories and yanked crates denied, wildcard versions banned, sources restricted to the official registry, enforced on every build.&lt;/p&gt;
&lt;p&gt;The part of rust-tool-base&amp;rsquo;s setup worth copying is the waiver list. Every advisory that can&amp;rsquo;t be fixed yet is recorded with its crate, its dependency path, its reason and the milestone that removes it. A waiver is a dated note, not a shrug, and that&amp;rsquo;s what keeps the gate honest enough that nobody actually wants to bypass it.&lt;/p&gt;</description></item><item><title>A builder that won't compile if you forget a field</title><link>https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-builder-that-wont-compile-if-you-forget-a-field/cover-a-builder-that-wont-compile-if-you-forget-a-field.png" alt="Featured image of post A builder that won't compile if you forget a field" /&gt;&lt;p&gt;go-tool-base configures things with functional options, and if you forget a required one, the best case is a runtime failure and the worst case is an empty value sailing silently into everything downstream. Most builder patterns share the same hole. rust-tool-base closes it in a way I find genuinely delightful: the &lt;code&gt;.build()&lt;/code&gt; method simply doesn&amp;rsquo;t &lt;em&gt;exist&lt;/em&gt; until you&amp;rsquo;ve set every required field.&lt;/p&gt;
&lt;h2 id="when-is-a-required-field-actually-required"&gt;When is a required field actually required
&lt;/h2&gt;&lt;p&gt;Every framework has constructors with a mix of required and optional inputs. An &lt;code&gt;Application&lt;/code&gt; in rust-tool-base needs tool metadata and a version. It optionally takes a custom config type, extra commands, feature toggles. The metadata needs a name and a summary; a description and a help channel are optional.&lt;/p&gt;
&lt;p&gt;The interesting question is &lt;em&gt;when&lt;/em&gt; &amp;ldquo;required&amp;rdquo; gets enforced. There are really only two moments available: when the program runs, or when it compiles. Most APIs pick the first without ever framing it as a choice.&lt;/p&gt;
&lt;h2 id="how-go-tool-base-does-it"&gt;How go-tool-base does it
&lt;/h2&gt;&lt;p&gt;go-tool-base uses functional options, the standard Go pattern:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;New&lt;/code&gt; takes a variadic list of options and applies them. It&amp;rsquo;s flexible and it reads well. But look at what the &lt;em&gt;type&lt;/em&gt; actually says. &lt;code&gt;New&lt;/code&gt; accepts zero or more options. The signature is satisfied by passing nothing at all. If &lt;code&gt;WithName&lt;/code&gt; is required, nothing in the type system knows that. Forget it and the code compiles cleanly, and you find out when the program runs, or worse, when it doesn&amp;rsquo;t visibly fail but quietly carries an empty name into everything downstream.&lt;/p&gt;
&lt;p&gt;A plain builder is no better here. &lt;code&gt;builder.name(&amp;quot;mytool&amp;quot;).build()&lt;/code&gt; and &lt;code&gt;builder.build()&lt;/code&gt; are both perfectly valid calls as far as the compiler is concerned. The builder &lt;em&gt;hopes&lt;/em&gt; you set the name. It can check at the end and return an error, but that check still happens at runtime.&lt;/p&gt;
&lt;p&gt;In every one of these the required-ness of a field is a fact that lives in documentation and in the author&amp;rsquo;s head, not in the code.&lt;/p&gt;
&lt;h2 id="typestate-putting-required-in-the-type"&gt;Typestate: putting &amp;ldquo;required&amp;rdquo; in the type
&lt;/h2&gt;&lt;p&gt;rust-tool-base builds these with &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-app/src/metadata.rs#L86" target="_blank" rel="noopener"
 &gt;&lt;code&gt;bon&lt;/code&gt;&lt;/a&gt;, and the pattern it generates is a &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;&lt;em&gt;typestate&lt;/em&gt;&lt;/a&gt; builder. The idea is that the builder&amp;rsquo;s type changes as you call it, and that type tracks which required fields you&amp;rsquo;ve set so far.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolMetadata&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mytool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my CLI tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ToolMetadata::builder()&lt;/code&gt; returns a builder in a state that records &amp;ldquo;name not set, summary not set&amp;rdquo;. Calling &lt;code&gt;.name(...)&lt;/code&gt; consumes that builder and returns a &lt;em&gt;different type&lt;/em&gt;, one whose state records &amp;ldquo;name set&amp;rdquo;. Calling &lt;code&gt;.summary(...)&lt;/code&gt; does the same for the summary.&lt;/p&gt;
&lt;p&gt;The part that matters is &lt;code&gt;.build()&lt;/code&gt;. It isn&amp;rsquo;t a method on the builder in general. It only exists on the builder type that represents &amp;ldquo;every required field has been set&amp;rdquo;. So this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolMetadata&lt;/span&gt;::&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;my CLI tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;doesn&amp;rsquo;t compile. Not because a runtime check fired, but because in the state &amp;ldquo;name not set&amp;rdquo; there&amp;rsquo;s no &lt;code&gt;.build()&lt;/code&gt; method to call in the first place. The compiler stops you, and the error points straight at the missing &lt;code&gt;.name(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Optional fields stay optional. You can call &lt;code&gt;.description(...)&lt;/code&gt; or skip it, and &lt;code&gt;.build()&lt;/code&gt; is reachable either way, because the description was never part of the state that gates it. The required and the optional are genuinely different in the type, which is exactly the distinction the functional-options version could only keep in a comment.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Application::builder()&lt;/code&gt; works the same way. It won&amp;rsquo;t produce an &lt;code&gt;Application&lt;/code&gt; until it has metadata and a version, and &amp;ldquo;won&amp;rsquo;t&amp;rdquo; there means the method is absent, not that a check returns &lt;code&gt;Err&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="why-the-moment-matters"&gt;Why the moment matters
&lt;/h2&gt;&lt;p&gt;Moving the check from run time to compile time changes who finds the mistake, and when.&lt;/p&gt;
&lt;p&gt;A runtime check finds it when that code path executes, which might be in a test, might be in CI, might be on a user&amp;rsquo;s machine at the worst possible moment. A compile-time check finds it the instant you write it, in the editor, before anything has run at all. The same mistake, caught at the cheapest possible point instead of one of the expensive ones.&lt;/p&gt;
&lt;p&gt;It also changes what the API &lt;em&gt;documents about itself&lt;/em&gt;. A functional-options constructor can&amp;rsquo;t tell you, from its signature alone, which options you must pass. A typestate builder can, because the set of methods available to you at each step &lt;em&gt;is&lt;/em&gt; the documentation. You literally cannot reach &lt;code&gt;.build()&lt;/code&gt; without having been walked past every required field on the way.&lt;/p&gt;
&lt;p&gt;This is one of those places where Rust&amp;rsquo;s type system earns its reputation. The builder isn&amp;rsquo;t more careful than the Go version. It&amp;rsquo;s that &amp;ldquo;this field is required&amp;rdquo; stopped being a convention and became something the compiler enforces. (Another entry, if you&amp;rsquo;re keeping score from &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;the porting post&lt;/a&gt;, in the column of outcomes that survived while the Go mechanism got left behind.)&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;Required fields have to be enforced somewhere. Functional options and ordinary builders enforce them at runtime, if at all, because &lt;code&gt;.build()&lt;/code&gt; is always callable and the type system never learns which inputs were mandatory.&lt;/p&gt;
&lt;p&gt;rust-tool-base uses typestate builders generated by &lt;code&gt;bon&lt;/code&gt;. The builder&amp;rsquo;s type changes as you set fields, and &lt;code&gt;.build()&lt;/code&gt; only exists once every required field is present. Forgetting one is a compile error that names the missing call, not a runtime surprise. The required-versus-optional distinction stops being a comment and becomes part of the type.&lt;/p&gt;</description></item><item><title>Process isolation won't save you from the filesystem</title><link>https://blog-570662.gitlab.io/process-isolation-wont-save-you-from-the-filesystem/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/process-isolation-wont-save-you-from-the-filesystem/</guid><description>&lt;img src="https://blog-570662.gitlab.io/process-isolation-wont-save-you-from-the-filesystem/cover-process-isolation-wont-save-you-from-the-filesystem.png" alt="Featured image of post Process isolation won't save you from the filesystem" /&gt;&lt;p&gt;A test that passed every single time I ran it on its own, and failed maybe one run in five when I ran the whole suite. The failure was always the same: the self-update test downloaded a release archive, went to extract it, and found the archive corrupt. Half-written. As if something had been scribbling in the file while it read it. Something had.&lt;/p&gt;
&lt;h2 id="the-comfort-i-was-leaning-on"&gt;The comfort I was leaning on
&lt;/h2&gt;&lt;p&gt;The self-update tests are heavier than a unit test wants to be. They stand up a fake release, download the artefact, verify its checksum, extract it, swap a binary. Real files, real I/O. So they&amp;rsquo;d been built to run as separate &lt;em&gt;processes&lt;/em&gt;, not just separate threads, each one its own little world.&lt;/p&gt;
&lt;p&gt;And I&amp;rsquo;d quietly filed that under &amp;ldquo;solved&amp;rdquo;. Separate processes don&amp;rsquo;t share an address space. One can&amp;rsquo;t reach into another&amp;rsquo;s memory and corrupt a value mid-read. That whole category of data race, the kind you reach for a mutex to fix, simply can&amp;rsquo;t happen across a process boundary. So I&amp;rsquo;d stopped thinking about concurrency in these tests at all, because I&amp;rsquo;d convinced myself the isolation was total.&lt;/p&gt;
&lt;p&gt;It wasn&amp;rsquo;t total. It was isolation of &lt;em&gt;memory&lt;/em&gt;, and I&amp;rsquo;d let myself hear it as isolation of &lt;em&gt;everything&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="two-processes-one-path"&gt;Two processes, one path
&lt;/h2&gt;&lt;p&gt;The thing two processes very much do still share is the filesystem. And the self-update flow, sensibly, caches its download rather than re-fetching it. The default cache directory is computed from the tool&amp;rsquo;s name and the release version, in &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-update/src/flow.rs#L228" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-update/src/flow.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;cache_dir_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;: &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;PathBuf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;directories&lt;/span&gt;::&lt;span class="n"&gt;ProjectDirs&lt;/span&gt;::&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_or_else&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;std&lt;/span&gt;::&lt;span class="n"&gt;env&lt;/span&gt;::&lt;span class="n"&gt;temp_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;to_path_buf&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;update&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Read that with two parallel test processes in mind. They&amp;rsquo;re testing the &lt;em&gt;same&lt;/em&gt; tool, against the &lt;em&gt;same&lt;/em&gt; fake release tag. So &lt;code&gt;tool_name&lt;/code&gt; matches and &lt;code&gt;version&lt;/code&gt; matches, which means &lt;code&gt;cache_dir_for&lt;/code&gt; hands both of them the &lt;em&gt;identical path&lt;/em&gt;. Two processes, isolated in every way that involves memory, both downloading and extracting into one shared directory on disk, at the same time. One writes the archive while the other is partway through reading it, and you get exactly the corrupt half-written file the test kept tripping over.&lt;/p&gt;
&lt;p&gt;Process isolation did nothing here, because the contention was never in memory. It was on a path string that came out the same for both of them.&lt;/p&gt;
&lt;h2 id="the-fix-is-to-stop-sharing-the-path"&gt;The fix is to stop sharing the path
&lt;/h2&gt;&lt;p&gt;Once it&amp;rsquo;s framed as &amp;ldquo;they share a path&amp;rdquo;, the fix writes itself: don&amp;rsquo;t share the path. Give each invocation its own cache directory. The updater builder already had the seam for it, and the doc comment now says exactly why it&amp;rsquo;s there, in &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-update/src/updater.rs#L396" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-update/src/updater.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;/// Tools call this when they want isolation per-invocation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;/// (e.g. CI runners, tests with parallel processes) or to honour
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt;/// a user-supplied `--cache-dir` flag.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;: &lt;span class="nc"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Into&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PathBuf&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nc"&gt;Self&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;into&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each test now builds its updater with &lt;code&gt;cache_dir(its_own_tempdir)&lt;/code&gt;, so two parallel processes land on two different directories and never meet. No lock, no serialisation, no clever cross-process file mutex. Just the realisation that the shared thing was a directory, and the cure for shared mutable state is usually to stop sharing it, not to guard it.&lt;/p&gt;
&lt;h2 id="the-fix-that-turned-out-to-be-a-feature"&gt;The fix that turned out to be a feature
&lt;/h2&gt;&lt;p&gt;The part I&amp;rsquo;m quietly pleased about is that this didn&amp;rsquo;t stay a test-only hack. The override I needed to isolate the tests is exactly the override a real tool wants for its own reasons. A CI runner doing self-update wants a writable cache path it controls, not wherever &lt;code&gt;directories-rs&lt;/code&gt; decides the system cache lives. A user might reasonably want to point the whole thing somewhere specific. That&amp;rsquo;s a &lt;code&gt;--cache-dir&lt;/code&gt; flag, and &lt;code&gt;cache_dir()&lt;/code&gt; is precisely the hook you&amp;rsquo;d wire it to.&lt;/p&gt;
&lt;p&gt;So the thing I added to stop a flaky test is the same thing a downstream tool reaches for to expose &lt;code&gt;--cache-dir&lt;/code&gt;. The test forced the seam to exist, and the seam was worth having anyway. I&amp;rsquo;ll take that trade every time over a fix that only the test suite benefits from.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;d treated &amp;ldquo;separate processes&amp;rdquo; as a synonym for &amp;ldquo;can&amp;rsquo;t race&amp;rdquo;, and it isn&amp;rsquo;t. Processes don&amp;rsquo;t share memory, so the memory races are gone. They absolutely still share the filesystem, the network, every named resource the OS will hand to anyone who asks for it by the same name. My two test processes computed the same cache path from the same tool and tag, and raced on the files in it, and no amount of address-space isolation was ever going to touch that.&lt;/p&gt;
&lt;p&gt;Shared mutable state on disk is still shared mutable state. The fix wasn&amp;rsquo;t a bigger hammer, it was giving each process its own directory and letting the isolation I thought I already had actually be true.&lt;/p&gt;</description></item><item><title>Registering commands without life before main</title><link>https://blog-570662.gitlab.io/registering-commands-without-life-before-main/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/registering-commands-without-life-before-main/</guid><description>&lt;img src="https://blog-570662.gitlab.io/registering-commands-without-life-before-main/cover-registering-commands-without-life-before-main.png" alt="Featured image of post Registering commands without life before main" /&gt;&lt;p&gt;I ended the &lt;a class="link" href="https://blog-570662.gitlab.io/what-survives-a-port/" &gt;last post&lt;/a&gt; promising to show how a Rust command registers itself when the language flatly refuses to run any of your code before &lt;code&gt;main()&lt;/code&gt;. This is that post, and it&amp;rsquo;s a lovely example of reaching the same outcome by a completely different road.&lt;/p&gt;
&lt;p&gt;The outcome I wanted to keep is self-registration.&lt;/p&gt;
&lt;h2 id="what-self-registration-buys"&gt;What self-registration buys
&lt;/h2&gt;&lt;p&gt;A command in go-tool-base lives in its own file, and that file puts the command into the framework itself. There&amp;rsquo;s no central list of commands to keep in sync. You add a file, the command appears. You delete the file, it&amp;rsquo;s gone. Nothing else changes.&lt;/p&gt;
&lt;p&gt;That property is worth protecting. The alternative, a hand-maintained registry that every new command has to be threaded into, is exactly the sort of central file that turns into a merge-conflict magnet and quietly falls out of date. So when go-tool-base moved to Rust, self-registration was firmly in the column of things that had to survive.&lt;/p&gt;
&lt;p&gt;The way Go &lt;em&gt;did&lt;/em&gt; it was not.&lt;/p&gt;
&lt;h2 id="how-go-does-it"&gt;How Go does it
&lt;/h2&gt;&lt;p&gt;A Go package can declare an &lt;code&gt;init()&lt;/code&gt; function, and the runtime guarantees every &lt;code&gt;init()&lt;/code&gt; runs before &lt;code&gt;main()&lt;/code&gt; starts. A go-tool-base command file uses this to append itself to a package-level slice:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;DeployCommand&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By the time &lt;code&gt;main()&lt;/code&gt; runs, every command file&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt; has already fired and the registry slice is populated. It&amp;rsquo;s a tidy trick, and it leans entirely on a Go feature: code that executes before &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="rust-doesnt-have-that"&gt;Rust doesn&amp;rsquo;t have that
&lt;/h2&gt;&lt;p&gt;Rust has no &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s no language-blessed phase that runs your code before &lt;code&gt;main()&lt;/code&gt;. This is a deliberate decision, not an oversight. Code running before &lt;code&gt;main()&lt;/code&gt; across many files has no well-defined order, and a startup phase whose ordering you can&amp;rsquo;t see is a classic source of subtle, miserable bugs. Rust closed that door on purpose.&lt;/p&gt;
&lt;p&gt;Which leaves a real question. If nothing runs before &lt;code&gt;main()&lt;/code&gt;, how does a command file insert itself into a registry without a central list editing it in?&lt;/p&gt;
&lt;h2 id="distributed-slices"&gt;Distributed slices
&lt;/h2&gt;&lt;p&gt;The answer is a crate called &lt;code&gt;linkme&lt;/code&gt;, and the mechanism is the &lt;em&gt;linker&lt;/em&gt; rather than a runtime phase.&lt;/p&gt;
&lt;p&gt;You declare a slice the framework will collect into:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;BUILTIN_COMMANDS&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(&lt;code&gt;Box&amp;lt;dyn Command&amp;gt;&lt;/code&gt; is just &amp;ldquo;a pointer to some value that implements the &lt;code&gt;Command&lt;/code&gt; trait, whichever concrete type it turns out to be&amp;rdquo;; the &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;primer&lt;/a&gt; covers it if that&amp;rsquo;s unfamiliar.)&lt;/p&gt;
&lt;p&gt;A command file then contributes one entry to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Greet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[distributed_slice(BUILTIN_COMMANDS)]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;register_greet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Box&lt;/span&gt;::&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s the part that makes it work. The &lt;code&gt;#[distributed_slice]&lt;/code&gt; attribute doesn&amp;rsquo;t generate any code that runs at startup. It places each entry into a dedicated section of the compiled object file. When the linker builds the final binary, it gathers everything in that section and lays it out as one contiguous array. &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-app/src/command.rs#L121" target="_blank" rel="noopener"
 &gt;&lt;code&gt;BUILTIN_COMMANDS&lt;/code&gt;&lt;/a&gt; &lt;em&gt;is&lt;/em&gt; that array.&lt;/p&gt;
&lt;p&gt;So by the time the program exists as a binary on disk, the registry is already assembled. &lt;code&gt;main()&lt;/code&gt; doesn&amp;rsquo;t build it. No &lt;code&gt;init()&lt;/code&gt; builds it. The linker built it, statically, as part of producing the executable. At runtime the framework iterates a slice that was complete before the process ever started.&lt;/p&gt;
&lt;h2 id="what-you-get-from-it"&gt;What you get from it
&lt;/h2&gt;&lt;p&gt;The outcome is the one Go&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt; gave, and then a bit more.&lt;/p&gt;
&lt;p&gt;A command still lives in one file and still self-registers. Adding a command is still adding a file. There&amp;rsquo;s still no central list.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s no startup phase to reason about, because there isn&amp;rsquo;t one. There&amp;rsquo;s no global mutable slice being appended to as &lt;code&gt;init()&lt;/code&gt;s fire, because nothing is appended at runtime; the slice is immutable and finished. There&amp;rsquo;s no ordering question, because the linker isn&amp;rsquo;t running your code, it&amp;rsquo;s collecting data. And it costs nothing at runtime: assembling the registry happened at link time, so program start just reads it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same idea go-tool-base had, expressed by the tool Rust actually gives you. Go reaches the registry through a controlled phase before &lt;code&gt;main()&lt;/code&gt;. Rust reaches it without any phase at all, because the linker did the assembly while the binary was still being built. Two roads, one destination&amp;hellip; which, if you&amp;rsquo;ve been following along, is becoming the whole theme of the Rust side of this project.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;Self-registration, where a command file inserts itself into the framework with no central list, is a property worth keeping. go-tool-base achieves it with a package-level &lt;code&gt;init()&lt;/code&gt;, leaning on Go&amp;rsquo;s guarantee that such functions run before &lt;code&gt;main()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rust has no equivalent and wants none, because code running before &lt;code&gt;main()&lt;/code&gt; has no clear ordering. rust-tool-base uses &lt;code&gt;linkme&lt;/code&gt; distributed slices instead: each command is placed into a dedicated linker section, and the linker assembles them into one contiguous, immutable slice as it builds the binary. The registry is complete before the program runs. Same outcome as Go&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt;, with no life before &lt;code&gt;main&lt;/code&gt; required.&lt;/p&gt;</description></item><item><title>Verifying your own downloads: how I solved it for self-updating CLI tools</title><link>https://blog-570662.gitlab.io/verifying-your-own-downloads/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/verifying-your-own-downloads/</guid><description>&lt;img src="https://blog-570662.gitlab.io/verifying-your-own-downloads/cover-verifying-your-own-downloads.png" alt="Featured image of post Verifying your own downloads: how I solved it for self-updating CLI tools" /&gt;&lt;p&gt;Way back in the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;introduction&lt;/a&gt; I promised I&amp;rsquo;d come back to the self-update integrity checks. Here we are. And the starting point is a slightly uncomfortable admission: for a good long while, go-tool-base&amp;rsquo;s &lt;code&gt;update&lt;/code&gt; command was the most trusting line of code in the entire tool.&lt;/p&gt;
&lt;h2 id="the-most-trusting-line-of-code-in-the-tool"&gt;The most trusting line of code in the tool
&lt;/h2&gt;&lt;p&gt;Self-update is a lovely feature. The user runs &lt;code&gt;yourtool update&lt;/code&gt;, the tool fetches the latest release, swaps itself out, and they&amp;rsquo;re current. go-tool-base has had this since early on, wired to GitHub, GitLab, Bitbucket, Gitea and a few others.&lt;/p&gt;
&lt;p&gt;But look closely at what that feature actually does. It reaches out to the internet, pulls down a file, and then &lt;em&gt;replaces the executable that&amp;rsquo;s currently running with that file&lt;/em&gt;. The next time the user invokes the tool, they&amp;rsquo;re running whatever those bytes turned out to be.&lt;/p&gt;
&lt;p&gt;The original implementation downloaded the release asset over HTTPS and extracted it. HTTPS gets you transport security: the bytes weren&amp;rsquo;t tampered with &lt;em&gt;in flight&lt;/em&gt;. It tells you nothing about whether the bytes were right when they left, or whether they&amp;rsquo;re even the bytes you meant to fetch. A truncated download, a CDN cache serving a mangled object, a release asset that got swapped after the fact&amp;hellip; HTTPS waves all of those straight through. For the one operation in the whole tool that replaces the binary, &amp;ldquo;we didn&amp;rsquo;t check&amp;rdquo; is an uncomfortable place to be sitting.&lt;/p&gt;
&lt;h2 id="goreleaser-already-does-half-the-job"&gt;GoReleaser already does half the job
&lt;/h2&gt;&lt;p&gt;The good news is that the build side was already producing exactly what I needed. GoReleaser, which builds go-tool-base&amp;rsquo;s releases, generates a &lt;code&gt;checksums.txt&lt;/code&gt; for every release: one SHA-256 per published artefact, the same format &lt;code&gt;sha256sum&lt;/code&gt; emits. It was sitting right there as a release asset and nothing was reading it.&lt;/p&gt;
&lt;p&gt;So Phase 1 of the integrity work is exactly that: read it.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;update&lt;/code&gt; downloads the platform binary, it now also fetches &lt;code&gt;checksums.txt&lt;/code&gt; from the same release, looks up the entry for the asset it just pulled, and compares the SHA-256 of the downloaded bytes against the expected hash before anything gets extracted or installed. Mismatch, and the update aborts before it has so much as touched the installed binary. The hash comparison &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/setup/checksum.go#L267" target="_blank" rel="noopener"
 &gt;runs in constant time&lt;/a&gt;, which is more defence-in-depth than strictly necessary here, but it costs nothing and means every hash comparison in the codebase is the same and reassuringly audit-boring.&lt;/p&gt;
&lt;h2 id="fail-open-or-fail-closed"&gt;Fail open, or fail closed?
&lt;/h2&gt;&lt;p&gt;The interesting design question wasn&amp;rsquo;t the hashing. It was: what do you do when there &lt;em&gt;is no&lt;/em&gt; &lt;code&gt;checksums.txt&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Plenty of older releases predate this feature. A release might have been cut by hand without GoReleaser. If go-tool-base flatly refused to update whenever a manifest was missing, the very act of shipping this feature would brick the update path for every existing tool the moment they upgraded into it. That&amp;rsquo;s a cure worse than the disease.&lt;/p&gt;
&lt;p&gt;So the default is fail-open: no manifest, log a clear warning, proceed. It matches how the existing offline-update path already behaved with its optional &lt;code&gt;.sha256&lt;/code&gt; sidecar, and it keeps upgrades working.&lt;/p&gt;
&lt;p&gt;Fail-open as a &lt;em&gt;default&lt;/em&gt; is not the same as fail-open being &lt;em&gt;right for everyone&lt;/em&gt;, though. A security-sensitive tool should be able to say &amp;ldquo;no manifest, no update, full stop&amp;rdquo;. Two ways to get there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tool authors&lt;/strong&gt; flip a compile-time switch (&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/setup/checksum.go#L42" target="_blank" rel="noopener"
 &gt;&lt;code&gt;setup.DefaultRequireChecksum = true&lt;/code&gt;&lt;/a&gt; in &lt;code&gt;main()&lt;/code&gt;) and their binary ships fail-closed from day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End users&lt;/strong&gt; override either way through config (&lt;code&gt;update.require_checksum&lt;/code&gt;) or an environment variable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;go-tool-base itself ships with the strict setting turned on, because a tool whose entire job is being a careful framework should hold itself to the stricter bar.&lt;/p&gt;
&lt;h2 id="the-caveat"&gt;The caveat
&lt;/h2&gt;&lt;p&gt;Security features oversell themselves constantly, so here is the limit, stated plainly.&lt;/p&gt;
&lt;p&gt;A checksum hosted &lt;em&gt;next to&lt;/em&gt; the binary it describes protects you from accidents. Corruption, truncation, a CDN serving stale junk, a release asset that got partially clobbered. It does not protect you from a determined attacker who&amp;rsquo;s compromised the release platform itself. If someone can replace the binary, they can replace &lt;code&gt;checksums.txt&lt;/code&gt; in the same breath, and your tool will cheerfully verify a malicious download against a malicious manifest and pronounce it good.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not a flaw in the implementation. It&amp;rsquo;s the inherent ceiling of &lt;em&gt;same-origin&lt;/em&gt; integrity: the manifest and the artefact share a trust root, so they fall together. Closing that gap needs a signature whose trust root is somewhere the release platform can&amp;rsquo;t reach, a key the attacker doesn&amp;rsquo;t have. That&amp;rsquo;s the next phase of this work, and it&amp;rsquo;s a bigger piece: &lt;a class="link" href="https://blog-570662.gitlab.io/a-signing-key-needs-somewhere-to-live/" &gt;GPG-signing the manifest&lt;/a&gt;, with the public half both embedded in the binary and published independently so a single platform compromise isn&amp;rsquo;t enough.&lt;/p&gt;
&lt;p&gt;Phase 1 is the floor, not the ceiling. But it&amp;rsquo;s a floor worth having, because the overwhelming majority of real-world &amp;ldquo;the download was wrong&amp;rdquo; incidents are accidents, not attacks, and accidents are exactly what a same-origin checksum catches.&lt;/p&gt;
&lt;h2 id="pulling-it-together"&gt;Pulling it together
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;update&lt;/code&gt; command is the most trusting code in a self-updating tool: it fetches bytes from the internet and then becomes them. go-tool-base now verifies the SHA-256 of every self-update download against the release&amp;rsquo;s own &lt;code&gt;checksums.txt&lt;/code&gt; before installing. It fails open by default so shipping the feature doesn&amp;rsquo;t strand anyone on an un-updatable version, fails closed for tool authors who ask (go-tool-base itself does), and stays honest that a same-origin checksum stops accidents, not a platform compromise.&lt;/p&gt;
&lt;p&gt;Verifying your own downloads is a low bar. The point is that the previous height of that bar was zero.&lt;/p&gt;</description></item><item><title>Two API decisions that quietly contradict each other</title><link>https://blog-570662.gitlab.io/two-api-decisions-that-quietly-contradict/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/two-api-decisions-that-quietly-contradict/</guid><description>&lt;img src="https://blog-570662.gitlab.io/two-api-decisions-that-quietly-contradict/cover-two-api-decisions-that-quietly-contradict.png" alt="Featured image of post Two API decisions that quietly contradict each other" /&gt;&lt;p&gt;Two design decisions on one enum, each sensible on its own, that would have quietly fought each other if I&amp;rsquo;d let them. I didn&amp;rsquo;t, but only because the second one is easy to get wrong and the compiler wouldn&amp;rsquo;t have said a word either way.&lt;/p&gt;
&lt;h2 id="decision-one-promise-the-list-can-grow"&gt;Decision one: promise the list can grow
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;&lt;code&gt;#[non_exhaustive]&lt;/code&gt;&lt;/a&gt; on the &lt;code&gt;Feature&lt;/code&gt; enum. It tells downstream code it can&amp;rsquo;t match the enum exhaustively, so it has to keep a wildcard arm, which in turn means adding a variant later is a non-breaking, minor-version change. Nobody&amp;rsquo;s &lt;code&gt;match&lt;/code&gt; stops compiling just because the enum grew. The doc comment says exactly that: it &amp;ldquo;keeps variant additions a minor-version change for downstream matchers.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="decision-two-hand-out-the-whole-list"&gt;Decision two: hand out the whole list
&lt;/h2&gt;&lt;p&gt;A convenience &lt;code&gt;all()&lt;/code&gt; returning every variant, because iterating over the lot is something you genuinely want to do. The tempting signature is a fixed-size array, &lt;code&gt;[Feature; 11]&lt;/code&gt;: you know precisely how many there are, so why not put it in the type?&lt;/p&gt;
&lt;h2 id="why-those-two-cant-both-be-true"&gt;Why those two can&amp;rsquo;t both be true
&lt;/h2&gt;&lt;p&gt;The catch is a quirk of Rust that often trips up people arriving from other languages: the length of a fixed-size array is part of its &lt;em&gt;type&lt;/em&gt;. &lt;code&gt;[Feature; 11]&lt;/code&gt;, an array of exactly eleven features, and &lt;code&gt;[Feature; 12]&lt;/code&gt;, exactly twelve, are not one type holding a different number of items the way they might be elsewhere. They are two genuinely different, incompatible types, about as interchangeable as &lt;code&gt;i32&lt;/code&gt; and &lt;code&gt;i64&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So the moment you add a twelfth variant, a fixed-size &lt;code&gt;all()&lt;/code&gt; forces an unhappy choice, and both options are bad. Bump the array to &lt;code&gt;[Feature; 12]&lt;/code&gt; and you break every caller who wrote the old length down. Leave it at &lt;code&gt;11&lt;/code&gt; and the new variant is silently dropped, leaving you a function called &lt;code&gt;all&lt;/code&gt; that doesn&amp;rsquo;t return all of them. Either way the &lt;code&gt;#[non_exhaustive]&lt;/code&gt; promise (adding a variant breaks nobody) is quietly cancelled by a return type that welded today&amp;rsquo;s count into the public API.&lt;/p&gt;
&lt;h2 id="so-all-returns-a-slice"&gt;So &lt;code&gt;all()&lt;/code&gt; returns a slice
&lt;/h2&gt;&lt;p&gt;Which is exactly what it does, and the doc comment spells out why, in &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-app/src/features.rs#L59" target="_blank" rel="noopener"
 &gt;&lt;code&gt;crates/rtb-app/src/features.rs&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#[non_exhaustive]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;Feature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mcp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Doctor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Ai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Telemetry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Changelog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-&amp;gt; &lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;&amp;#39;static&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;Init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;Self&lt;/span&gt;::&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ...the rest... */&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A slice length is a value, not part of the type. Add a variant, the slice gets one longer, and not a single downstream signature changes. The promise holds.&lt;/p&gt;
&lt;h2 id="the-thing-to-watch-for"&gt;The thing to watch for
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;#[non_exhaustive]&lt;/code&gt; is a promise about the future. A fixed-size array is a fact about the present. You can&amp;rsquo;t keep both at once, and nothing will warn you that you&amp;rsquo;ve contradicted yourself, because each decision is individually fine. The trap is always the second API surface that quietly re-bakes the flexibility the first one promised. When you mark a type &amp;ldquo;free to grow,&amp;rdquo; go and check that nothing in its public interface has secretly written down how big it is today.&lt;/p&gt;</description></item><item><title>What survives a port, and what doesn't</title><link>https://blog-570662.gitlab.io/what-survives-a-port/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/what-survives-a-port/</guid><description>&lt;img src="https://blog-570662.gitlab.io/what-survives-a-port/cover-what-survives-a-port.png" alt="Featured image of post What survives a port, and what doesn't" /&gt;&lt;p&gt;Rebuilding go-tool-base in Rust turned out to be the most honest design review I&amp;rsquo;ve ever sat through, and I didn&amp;rsquo;t have to do anything except keep going. Porting a framework into a language with completely different idioms forces a separation you can&amp;rsquo;t fake: the parts that survive the move are &lt;em&gt;design&lt;/em&gt;, and the parts that don&amp;rsquo;t are just &lt;em&gt;habit&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="two-columns"&gt;Two columns
&lt;/h2&gt;&lt;p&gt;When you port a system between languages that don&amp;rsquo;t share idioms, every piece of it sorts itself into one of two columns, without you having to make the call.&lt;/p&gt;
&lt;p&gt;In the first column is the &lt;em&gt;outcome&lt;/em&gt; a piece of the design produces: every command receives the framework&amp;rsquo;s services, configuration is layered with a fixed precedence, commands register themselves, errors carry guidance to the user. In the second column is the &lt;em&gt;mechanism&lt;/em&gt; that produced that outcome in the original language.&lt;/p&gt;
&lt;p&gt;Things in the first column survive the port. You rebuild them, differently, because the tool genuinely needs them. Things in the second column do not survive. You find their replacement, and the Go version turns out to have been one valid implementation of an idea, not the idea itself. Doing this for go-tool-base, mechanism by mechanism, was more honest about my own design than any amount of sitting and staring at it would have been.&lt;/p&gt;
&lt;h2 id="the-container"&gt;The container
&lt;/h2&gt;&lt;p&gt;go-tool-base hands every command a &lt;code&gt;Props&lt;/code&gt; struct. It carries the logger, the config, the assets, the filesystem handle. Some of it is reached through loosely-typed accessors. It works well, and I &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;wrote a whole post about it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;outcome&lt;/em&gt; is column one: a command should receive one object, and that object should carry the framework&amp;rsquo;s services so the command doesn&amp;rsquo;t go assembling them itself. That survived. RTB hands every command an &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-app/src/app.rs#L32" target="_blank" rel="noopener"
 &gt;&lt;code&gt;App&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The loosely-typed accessors were column two. In Rust an &lt;code&gt;App&lt;/code&gt; is a plain struct with concrete fields, each one an &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;/code&gt; so a clone is a few atomic increments rather than a deep copy. Nothing is keyed by string. Nothing is fetched by name and asserted to a type. The thing the container &lt;em&gt;is for&lt;/em&gt; survived; the way Go expressed it did not.&lt;/p&gt;
&lt;h2 id="registration"&gt;Registration
&lt;/h2&gt;&lt;p&gt;A go-tool-base command self-registers using a package-level &lt;code&gt;init()&lt;/code&gt; function, which Go runs before &lt;code&gt;main()&lt;/code&gt; and which appends the command to a global slice.&lt;/p&gt;
&lt;p&gt;The outcome, column one, is that a command lives in its own file and inserts itself into the framework with no central list to edit. That&amp;rsquo;s genuinely worth keeping.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;init()&lt;/code&gt; mechanism is column two, and Rust doesn&amp;rsquo;t even offer it: Rust deliberately has no code that runs before &lt;code&gt;main()&lt;/code&gt;. The replacement is link-time registration through distributed slices, which gets its &lt;a class="link" href="https://blog-570662.gitlab.io/registering-commands-without-life-before-main/" &gt;own post next&lt;/a&gt;. Same outcome, no global mutable state, assembled by the linker rather than by a startup phase.&lt;/p&gt;
&lt;h2 id="configuration"&gt;Configuration
&lt;/h2&gt;&lt;p&gt;go-tool-base layers configuration with a precedence: flags over environment over file over defaults. Some of it is read back through key lookups.&lt;/p&gt;
&lt;p&gt;The layering and the precedence are column one. They survived exactly. RTB layers config with the same ordering.&lt;/p&gt;
&lt;p&gt;The key lookups were column two. In Rust the merged configuration is deserialised into &lt;em&gt;your own&lt;/em&gt; &lt;code&gt;serde&lt;/code&gt; struct, so a config value is a typed field you access like any other field, and a typo is a compile error instead of a missing key at runtime. The precedence survived; reading values back out of a string-keyed bag did not.&lt;/p&gt;
&lt;h2 id="the-error-path"&gt;The error path
&lt;/h2&gt;&lt;p&gt;go-tool-base routes every error through one handler so presentation is consistent, which I &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;also wrote up&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One consistent exit for errors is column one. It survived. What didn&amp;rsquo;t survive was the &lt;em&gt;handler&lt;/em&gt;: RTB has no error-handler object at all, because Rust&amp;rsquo;s own return-from-&lt;code&gt;main&lt;/code&gt; convention plus a report hook does the job the handler was built to do. That one has &lt;a class="link" href="https://blog-570662.gitlab.io/errors-without-an-error-handler/" &gt;its own post too&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="what-the-exercise-was-actually-worth"&gt;What the exercise was actually worth
&lt;/h2&gt;&lt;p&gt;Every mechanism told the same story. The container, the registration, the config access, the error path, the cancellation signal that go-tool-base carries on a &lt;code&gt;context.Context&lt;/code&gt; and RTB carries on a &lt;code&gt;CancellationToken&lt;/code&gt;. In every case the &lt;em&gt;thing it achieved&lt;/em&gt; walked across to Rust untouched, and the &lt;em&gt;Go code that achieved it&lt;/em&gt; was left behind.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the useful result. Before this port I couldn&amp;rsquo;t have told you, for any given pattern in go-tool-base, whether it was load-bearing design or just the idiomatic Go way to write it that day. Now I can, because each one was forced to prove itself by being rebuilt from nothing in a language that flatly wouldn&amp;rsquo;t accept the original. Whatever survived was real. Whatever I had to replace was always replaceable, which means it was never really the point.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;Porting a framework into a language with different idioms separates design from habit for free. The outcome a pattern produces is design, and it survives the move. The mechanism that produced it is idiom, and it gets left behind for the new language&amp;rsquo;s equivalent.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;Props&lt;/code&gt; bag, its &lt;code&gt;init()&lt;/code&gt; registration, its key-based config access and its error handler were all idiom. The single context object, self-registration, layered precedence and a consistent error exit were all design, and all four came through to RTB intact. The next three posts take the most interesting replacements one at a time, starting with how a Rust command registers itself when the language won&amp;rsquo;t run anything before &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>rust-tool-base: the same idea, in a language that argues back</title><link>https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/</guid><description>&lt;img src="https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/cover-rust-tool-base-the-same-idea.png" alt="Featured image of post rust-tool-base: the same idea, in a language that argues back" /&gt;&lt;p&gt;I built &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;go-tool-base&lt;/a&gt; because I was sick of rebuilding the same CLI scaffolding every time I started a new Go tool. You&amp;rsquo;d think that would have taught me a lesson about doing things more than once. Apparently not, because I&amp;rsquo;ve now started building rust-tool-base: the same idea, the same itch, for Rust.&lt;/p&gt;
&lt;p&gt;In my defence, there&amp;rsquo;s method in it.&lt;/p&gt;
&lt;h2 id="the-same-itch-a-different-language"&gt;The same itch, a different language
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I kept writing the same couple of hundred lines of wiring every time I started a new Go CLI. Config loading, logging setup, an update check, an error path, a help system. None of it was the tool. All of it had to be there before the tool could be.&lt;/p&gt;
&lt;p&gt;Lately I&amp;rsquo;ve been learning Rust, and two things collided. The first is how I tend to learn a language. I&amp;rsquo;ve always picked them up reasonably quickly, and the way I do it isn&amp;rsquo;t with a tutorial that builds a toy, it&amp;rsquo;s by rebuilding something whose shape I already know cold, so that every decision is about &lt;em&gt;the language&lt;/em&gt; rather than &lt;em&gt;the problem&lt;/em&gt;. The second is that every time I started a Rust CLI of any size, I hit the very same gap I&amp;rsquo;d already filled once in Go.&lt;/p&gt;
&lt;p&gt;So rather than learn Rust on a throwaway, I decided to learn it by building rust-tool-base: the same idea, the same niche, for Rust.&lt;/p&gt;
&lt;p&gt;One housekeeping note before the series gets going. You don&amp;rsquo;t need to write Rust to follow it. The posts lean on a handful of language ideas, and rather than stop and re-explain each one mid-flow, I&amp;rsquo;ve gathered them into &lt;a class="link" href="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/" &gt;a short primer&lt;/a&gt;. If a bit of syntax trips you up along the way, that&amp;rsquo;s where to look. If you already write Rust, ignore me and read on.&lt;/p&gt;
&lt;h2 id="the-gap-in-rust"&gt;The gap in Rust
&lt;/h2&gt;&lt;p&gt;The Rust ecosystem has a well-earned reputation for sharp, focused crates and a deliberate shortage of big opinionated frameworks. &lt;code&gt;clap&lt;/code&gt; for argument parsing, &lt;code&gt;figment&lt;/code&gt; for layered config, &lt;code&gt;tracing&lt;/code&gt; for logging, &lt;code&gt;miette&lt;/code&gt; for errors, &lt;code&gt;ratatui&lt;/code&gt; for terminal UI, &lt;code&gt;reqwest&lt;/code&gt; and &lt;code&gt;tokio&lt;/code&gt; underneath. Each of them is genuinely best-in-class.&lt;/p&gt;
&lt;p&gt;What nobody hands you is the assembly. Wiring those into one coherent product, and then adding self-update, AI integration, an MCP server, embedded documentation, credential handling, telemetry and a scaffolder, is real work, and it&amp;rsquo;s the same work on every project.&lt;/p&gt;
&lt;p&gt;The closest existing neighbours stop short of it. &lt;code&gt;cli-batteries&lt;/code&gt; is a thin preamble: argument parsing plus a logging subscriber plus panic and signal handling. &lt;code&gt;starbase&lt;/code&gt; has a proper session and lifecycle model but is CLI-agnostic and shaped around the moonrepo tooling it came from. &lt;code&gt;cargo-dist&lt;/code&gt; and &lt;code&gt;cargo-release&lt;/code&gt; are about release packaging, not the runtime. Good tools, all of them, but none is the opinionated, full-lifecycle, scaffolded base that go-tool-base is in the Go world. That space is empty, and rust-tool-base is built to fill it.&lt;/p&gt;
&lt;h2 id="why-it-is-not-a-port"&gt;Why it is not a port
&lt;/h2&gt;&lt;p&gt;The obvious way to build this would be to open go-tool-base and translate it file by file. I&amp;rsquo;m not doing that, and the reason matters enough that it&amp;rsquo;s the rule the whole project is built around.&lt;/p&gt;
&lt;p&gt;go-tool-base is full of Go. It leans on a &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;&lt;code&gt;Props&lt;/code&gt; struct&lt;/a&gt; that carries the framework&amp;rsquo;s services in loosely-typed fields. It configures things with functional options. It registers commands using package-level &lt;code&gt;init()&lt;/code&gt;. It threads a &lt;code&gt;context.Context&lt;/code&gt; through every call. Those are all good, idiomatic Go. Transliterated into Rust they&amp;rsquo;d become code that argues with the compiler on every single line, because Rust has its own answers to every one of those problems and they are emphatically not the Go answers.&lt;/p&gt;
&lt;p&gt;So rust-tool-base reaches the &lt;em&gt;same outcomes&lt;/em&gt; by Rust&amp;rsquo;s means. Commands still self-register, but through link-time machinery instead of &lt;code&gt;init()&lt;/code&gt;. There&amp;rsquo;s still one context object per command, but it&amp;rsquo;s strongly typed rather than a loosely-keyed bag. Configuration is still layered, but it lands in your own typed struct instead of a string-keyed lookup. Same philosophy, same shape of product, an entirely different ecosystem underneath. The &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/README.md#L9" target="_blank" rel="noopener"
 &gt;README&lt;/a&gt; says it plainly: it&amp;rsquo;s a sibling, not a port.&lt;/p&gt;
&lt;h2 id="why-do-it-twice-at-all"&gt;Why do it twice at all
&lt;/h2&gt;&lt;p&gt;Three reasons, and they reinforce each other.&lt;/p&gt;
&lt;p&gt;The first is plain usefulness. The next time I want a Rust CLI tool, I want the same head start go-tool-base already gives me in Go.&lt;/p&gt;
&lt;p&gt;The second is the learning. Rebuilding a system I understand forces me to meet Rust&amp;rsquo;s idioms where they actually bite, not where a tutorial gently stages them. You learn ownership properly when a real design is pushing back at you.&lt;/p&gt;
&lt;p&gt;The third is the one I didn&amp;rsquo;t expect, and it&amp;rsquo;s the subject of the next post. Building the same framework twice, in two languages, turns out to be the cleanest way to find out which of your original decisions were genuine &lt;em&gt;design&lt;/em&gt; and which were merely &lt;em&gt;idiom&lt;/em&gt;. The design survives the move. The idiom does not. Sorting one from the other has been the most interesting part so far.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;rust-tool-base is the Rust sibling of go-tool-base: the same batteries-included, scaffolded, opinionated CLI framework, aimed at the same gap, which in Rust is the gap between a pile of excellent crates and a coherent product.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not a port. Transliterating Go idioms into Rust produces code that fights the language, so RTB reaches the same outcomes through Rust&amp;rsquo;s own mechanisms instead. The posts after this one walk through the specific cases: how commands register, how the builder works, how errors are reported, and a few things RTB can do that the Go version structurally can&amp;rsquo;t. First, though, the thing the exercise taught me about my own design.&lt;/p&gt;</description></item><item><title>The blank import that keeps a dependency out of your binary</title><link>https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/cover-the-blank-import-that-keeps-a-dependency-out-of-your-binary.png" alt="Featured image of post The blank import that keeps a dependency out of your binary" /&gt;&lt;p&gt;go-tool-base can stash your credentials in the OS keychain, which most people building on it are perfectly happy about. But some of them ship into regulated and air-gapped environments where the binary isn&amp;rsquo;t &lt;em&gt;permitted&lt;/em&gt; to contain keychain or session-bus code at all&amp;hellip; not dormant, not unused, simply not there.&lt;/p&gt;
&lt;p&gt;So I had a feature most users want and a minority must be able to provably not have. The way I ended up solving it is one of my favourite little bits of honest Go.&lt;/p&gt;
&lt;h2 id="a-feature-some-users-have-to-be-able-to-not-have"&gt;A feature some users have to be able to &lt;em&gt;not have&lt;/em&gt;
&lt;/h2&gt;&lt;p&gt;go-tool-base needs somewhere to keep secrets: AI provider keys, VCS tokens, the occasional app password. The best home for those on a developer&amp;rsquo;s machine is the operating system&amp;rsquo;s own keychain. macOS Keychain, GNOME Keyring or KWallet on Linux via the Secret Service, Windows Credential Manager. So I wanted go-tool-base to support all three. (This is the keychain mode I mentioned back in the &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;credentials post&lt;/a&gt;, finally getting the explanation I promised it.)&lt;/p&gt;
&lt;p&gt;The Go library for that is &lt;a class="link" href="https://github.com/zalando/go-keyring" target="_blank" rel="noopener"
 &gt;&lt;code&gt;go-keyring&lt;/code&gt;&lt;/a&gt;, and it&amp;rsquo;s good. The catch is what it drags in behind it. On Linux it talks to the Secret Service over D-Bus, which means &lt;code&gt;godbus&lt;/code&gt;. On Windows it pulls &lt;code&gt;wincred&lt;/code&gt;. Perfectly reasonable dependencies for a desktop tool.&lt;/p&gt;
&lt;p&gt;Now here&amp;rsquo;s the constraint that made this interesting. Some of the people building tools on go-tool-base don&amp;rsquo;t ship to developer laptops. They ship into regulated sectors and air-gapped deployments where a security review will scan the binary, enumerate every dependency, and ask pointed questions about anything that does inter-process communication. For those builds, &amp;ldquo;the keychain code is there but we never call it&amp;rdquo; is not an acceptable answer. The reviewer&amp;rsquo;s position, and it&amp;rsquo;s a fair one, is that code which isn&amp;rsquo;t in the binary cannot be a finding.&lt;/p&gt;
&lt;p&gt;So I had a feature that most users want, and a minority of users must be able to provably &lt;em&gt;not have&lt;/em&gt;. Same framework, same release.&lt;/p&gt;
&lt;h2 id="why-i-didnt-reach-for-a-build-tag"&gt;Why I didn&amp;rsquo;t reach for a build tag
&lt;/h2&gt;&lt;p&gt;The obvious Go answer is a build tag. Compile with &lt;code&gt;-tags keychain&lt;/code&gt; to get it, leave the tag off to not. I started down that road. I even spent a while on an inverted version, a &lt;code&gt;nokeychain&lt;/code&gt; tag, on the theory that the regulated build should be the one that has to ask, so a forgotten flag fails safe.&lt;/p&gt;
&lt;p&gt;It works. It also isn&amp;rsquo;t very nice. Build tags are invisible at the call site. Nothing in the source tells you that a file only exists in some builds. The two worlds drift, because the tagged-out path isn&amp;rsquo;t compiled in your normal editor session and quietly rots. And the ergonomics for a &lt;em&gt;downstream consumer&lt;/em&gt; are poor: every tool built on go-tool-base would have to know the right magic incantation and thread it through their own release pipeline correctly, forever.&lt;/p&gt;
&lt;p&gt;I tried a second approach too: pull the keychain backend out into a completely separate Go module. That genuinely solves the dependency question (a module you don&amp;rsquo;t require can&amp;rsquo;t contribute to your &lt;code&gt;go.sum&lt;/code&gt;). But a separate module for one backend is clunky. Separate versioning, separate release, separate repo, all for a single file&amp;rsquo;s worth of behaviour. It felt like using a shipping container to post a letter.&lt;/p&gt;
&lt;h2 id="the-shape-that-actually-fits-a-registry-and-an-init"&gt;The shape that actually fits: a registry and an &lt;code&gt;init()&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;The version I&amp;rsquo;m happy with leans on two boring, well-worn Go mechanisms and lets them do something quietly clever together.&lt;/p&gt;
&lt;p&gt;First, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/credentials/backend.go#L27" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/credentials&lt;/code&gt;&lt;/a&gt; defines a &lt;code&gt;Backend&lt;/code&gt; interface and a registry. By default the registry holds a stub backend that politely returns &amp;ldquo;unsupported&amp;rdquo; for everything. The framework only ever talks to &lt;em&gt;the registered backend&lt;/em&gt;, whatever that happens to be.&lt;/p&gt;
&lt;p&gt;Second, the keychain implementation lives in its own package, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/credentials/keychain/keychain.go#L97" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/credentials/keychain&lt;/code&gt;&lt;/a&gt;, still inside the same module, no separate release to manage. That package has an &lt;code&gt;init()&lt;/code&gt; that registers its &lt;code&gt;go-keyring&lt;/code&gt;-backed backend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//nolint:gochecknoinits // registration via import is the whole point&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterBackend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And &lt;code&gt;go-keyring&lt;/code&gt;, &lt;code&gt;godbus&lt;/code&gt;, &lt;code&gt;wincred&lt;/code&gt;, the whole IPC dependency chain, are only imported by &lt;em&gt;that&lt;/em&gt; package.&lt;/p&gt;
&lt;p&gt;Now the trick. To switch keychain support on, you import the package. You don&amp;rsquo;t have to &lt;em&gt;use&lt;/em&gt; anything from it. A blank import is enough, because a blank import still runs the package&amp;rsquo;s &lt;code&gt;init()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// cmd/gtb/keychain.go - the entire file.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/credentials/keychain&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That single line is the on/off switch for the shipped &lt;code&gt;gtb&lt;/code&gt; binary. The blank import means &lt;code&gt;init()&lt;/code&gt; runs, the keychain backend registers itself, and credential operations start routing through the OS keychain. No flag, no tag, no config.&lt;/p&gt;
&lt;h2 id="the-part-that-makes-it-provable"&gt;The part that makes it provable
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s why this beats the build tag, and it comes down to one guarantee in the Go toolchain: &lt;strong&gt;the linker only includes packages that are actually imported.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;cmd/gtb/keychain.go&lt;/code&gt; exists, the &lt;code&gt;keychain&lt;/code&gt; package is in the import graph, so &lt;code&gt;go-keyring&lt;/code&gt;, &lt;code&gt;godbus&lt;/code&gt; and &lt;code&gt;wincred&lt;/code&gt; are linked in. Delete that one file and rebuild, and the &lt;code&gt;keychain&lt;/code&gt; package is no longer reachable from &lt;code&gt;main&lt;/code&gt;. The linker performs dead-code elimination, and the entire &lt;code&gt;go-keyring&lt;/code&gt; chain is &lt;em&gt;gone&lt;/em&gt;. Not dormant. Not present-but-unused. Absent from the binary.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the bit a regulated build needs. It isn&amp;rsquo;t a promise that the code won&amp;rsquo;t run. It&amp;rsquo;s a structural fact that the code isn&amp;rsquo;t there, and you can hand a security reviewer an SBOM that proves it. &lt;code&gt;go-keyring&lt;/code&gt; won&amp;rsquo;t appear, because it genuinely isn&amp;rsquo;t linked.&lt;/p&gt;
&lt;p&gt;For a downstream tool built on go-tool-base the story is the same, and just as cheap. Want keychain support? Add the one-line blank import to your own &lt;code&gt;cmd&lt;/code&gt; package. Must ship keychain-free? Don&amp;rsquo;t. Your binary&amp;rsquo;s dependency graph follows your import graph, exactly as Go always promised it would. The default (no import) is the locked-down one, which is the right way round for a safety property.&lt;/p&gt;
&lt;h2 id="why-i-like-this-more-than-i-expected-to"&gt;Why I like this more than I expected to
&lt;/h2&gt;&lt;p&gt;Build tags hide a decision in the compiler invocation. This pattern puts the decision in the source, as an import, where it&amp;rsquo;s greppable, obvious in code review, and impossible to get subtly wrong. There&amp;rsquo;s a real file called &lt;code&gt;keychain.go&lt;/code&gt; whose entire content is one import, and it reads as exactly what it is: a switch.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also just &lt;em&gt;honest&lt;/em&gt; Go. No reflection, no plugin loader, no clever runtime. A registry, an &lt;code&gt;init()&lt;/code&gt;, and the linker doing the one job it&amp;rsquo;s always done. The cleverness, such as it is, is in the arrangement, not in any individual piece.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base needed OS keychain support for the many, and a way to provably exclude it for the few. Build tags could express the toggle but hid it in the build invocation and rotted in the dark. A separate module solved the dependency question but was far too much machinery for one backend.&lt;/p&gt;
&lt;p&gt;Putting the keychain backend in its own package, activated by a blank &lt;code&gt;import _&lt;/code&gt; that fires its &lt;code&gt;init()&lt;/code&gt;, gets you both: a one-line, in-source, code-reviewable switch, and, because the linker only links what&amp;rsquo;s imported, a build with the import omitted that contains &lt;em&gt;none&lt;/em&gt; of the keychain dependency chain. Provable absence, not promised disuse.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re carrying an optional dependency that some of your users need gone rather than merely idle, this is the pattern. Let the import graph be the feature flag.&lt;/p&gt;</description></item><item><title>Just enough Rust to follow along</title><link>https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/</guid><description>&lt;img src="https://blog-570662.gitlab.io/just-enough-rust-to-follow-along/cover-just-enough-rust-to-follow-along.png" alt="Featured image of post Just enough Rust to follow along" /&gt;&lt;p&gt;I&amp;rsquo;m about to write a run of posts about building rust-tool-base, and they lean on a handful of Rust ideas that I&amp;rsquo;d otherwise have to keep stopping to explain. So here they are, up front, in one place. You don&amp;rsquo;t need to write Rust to follow the series. You need a feel for maybe six concepts, and this is a quick, friendly tour of them. If you already write Rust, skip it with my blessing.&lt;/p&gt;
&lt;h2 id="ownership-and-borrowing"&gt;Ownership and borrowing
&lt;/h2&gt;&lt;p&gt;This is the one everybody mentions, and the one the whole language is built around. Every value in Rust has exactly one &lt;em&gt;owner&lt;/em&gt;, and when the owner goes away, the value is cleaned up. No garbage collector deciding when, no manual &lt;code&gt;free&lt;/code&gt;. If you want to let another piece of code use a value without handing over ownership, you &lt;em&gt;borrow&lt;/em&gt; it: &lt;code&gt;&amp;amp;thing&lt;/code&gt; lends it out for reading, &lt;code&gt;&amp;amp;mut thing&lt;/code&gt; for writing, and the compiler enforces that you can&amp;rsquo;t, say, change something while someone else is reading it.&lt;/p&gt;
&lt;p&gt;The payoff, and the reason people put up with the up-front fuss, is that an entire family of bug (use-after-free, data races, dangling pointers) becomes a &lt;em&gt;compile&lt;/em&gt; error rather than a 3am one. When a post says something &amp;ldquo;moves&amp;rdquo; or is &amp;ldquo;borrowed&amp;rdquo;, that&amp;rsquo;s all this is.&lt;/p&gt;
&lt;h2 id="traits-are-rusts-interfaces"&gt;Traits are Rust&amp;rsquo;s interfaces
&lt;/h2&gt;&lt;p&gt;A &lt;em&gt;trait&lt;/em&gt; is a named set of methods a type can promise to provide, exactly like an interface in Go or Java. &lt;code&gt;impl Command for Greet { ... }&lt;/code&gt; reads as &amp;ldquo;the &lt;code&gt;Greet&lt;/code&gt; type fulfils the &lt;code&gt;Command&lt;/code&gt; contract.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Two bits of syntax show up a lot. &lt;code&gt;dyn Command&lt;/code&gt; means &amp;ldquo;some value whose concrete type I don&amp;rsquo;t know, but which implements &lt;code&gt;Command&lt;/code&gt;&amp;rdquo;, decided at runtime. And because the compiler needs a known size, you usually see it wrapped: &lt;code&gt;Box&amp;lt;dyn Command&amp;gt;&lt;/code&gt; is &amp;ldquo;a pointer to some &lt;code&gt;Command&lt;/code&gt;, whatever it turns out to be.&amp;rdquo; Whenever the series talks about a registry of &lt;code&gt;Box&amp;lt;dyn Something&amp;gt;&lt;/code&gt;, it just means a list of different types that all satisfy the same trait.&lt;/p&gt;
&lt;h2 id="enums-match-and-non_exhaustive"&gt;Enums, &lt;code&gt;match&lt;/code&gt;, and &lt;code&gt;#[non_exhaustive]&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;A Rust &lt;code&gt;enum&lt;/code&gt; is more than a list of named numbers; it&amp;rsquo;s a proper &amp;ldquo;one of these&amp;rdquo; type, and each variant can carry its own data. You handle one with &lt;code&gt;match&lt;/code&gt;, which is like a &lt;code&gt;switch&lt;/code&gt; that the compiler &lt;em&gt;forces&lt;/em&gt; you to make complete: miss a case and it won&amp;rsquo;t build.&lt;/p&gt;
&lt;p&gt;That completeness is usually a gift, but it&amp;rsquo;s awkward for a library, because adding a new variant would break everyone&amp;rsquo;s &lt;code&gt;match&lt;/code&gt;. The fix is the attribute &lt;code&gt;#[non_exhaustive]&lt;/code&gt;: it tells code outside the library &amp;ldquo;you must keep a catch-all &lt;code&gt;_ =&amp;gt;&lt;/code&gt; arm, because I reserve the right to add variants later.&amp;rdquo; With that in place, growing the enum is a non-breaking change. (One whole post turns on a subtle way to &lt;em&gt;accidentally&lt;/em&gt; cancel that promise.)&lt;/p&gt;
&lt;h2 id="the-type-system-carries-facts-not-just-shapes"&gt;The type system carries facts, not just shapes
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s an idea that surprises people coming from other languages: a Rust type often encodes &lt;em&gt;more&lt;/em&gt; than &amp;ldquo;this is a number&amp;rdquo; or &amp;ldquo;this is a list.&amp;rdquo; The size of a fixed array is part of its type, so &lt;code&gt;[Feature; 11]&lt;/code&gt; and &lt;code&gt;[Feature; 12]&lt;/code&gt; are genuinely different, incompatible types, not one type holding a different count.&lt;/p&gt;
&lt;p&gt;Pushed further, you can make the type track &lt;em&gt;state&lt;/em&gt;. A &amp;ldquo;typestate&amp;rdquo; builder changes type as you call it, so &lt;code&gt;.build()&lt;/code&gt; literally doesn&amp;rsquo;t exist as a method until every required field has been set, and forgetting one is a compile error rather than a runtime surprise. When a post says the compiler &amp;ldquo;won&amp;rsquo;t let you&amp;rdquo; do something, this is usually how: the mistake was made unrepresentable in the types.&lt;/p&gt;
&lt;h2 id="result-and-the--operator"&gt;&lt;code&gt;Result&lt;/code&gt; and the &lt;code&gt;?&lt;/code&gt; operator
&lt;/h2&gt;&lt;p&gt;Rust has no exceptions. A function that can fail returns a &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;: either &lt;code&gt;Ok(value)&lt;/code&gt; or &lt;code&gt;Err(problem)&lt;/code&gt;, and you can&amp;rsquo;t use the value without acknowledging the error case. Writing that check by hand everywhere would be miserable, so there&amp;rsquo;s a shorthand: the &lt;code&gt;?&lt;/code&gt; operator. &lt;code&gt;let x = thing()?;&lt;/code&gt; means &amp;ldquo;if this returned an error, return it up to my caller right now; otherwise give me the value.&amp;rdquo; Errors travel up the call stack as ordinary return values until something handles them, or until they fall out of &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="crates-the-workspace-and-features"&gt;Crates, the workspace, and features
&lt;/h2&gt;&lt;p&gt;A &lt;em&gt;crate&lt;/em&gt; is Rust&amp;rsquo;s unit of compilation, roughly &amp;ldquo;a library or binary.&amp;rdquo; A &lt;em&gt;workspace&lt;/em&gt; is a bundle of crates built together, which is how rust-tool-base is laid out: &lt;code&gt;rtb-app&lt;/code&gt;, &lt;code&gt;rtb-cli&lt;/code&gt;, &lt;code&gt;rtb-config&lt;/code&gt; and so on, each its own crate. And &lt;em&gt;Cargo features&lt;/em&gt; are compile-time switches declared in &lt;code&gt;Cargo.toml&lt;/code&gt;: turn a feature off and the code it guards, and any dependency it pulled in, is never compiled into your binary at all. Not disabled at runtime; simply absent. That distinction does real work in one of the posts.&lt;/p&gt;
&lt;h2 id="thats-the-toolkit"&gt;That&amp;rsquo;s the toolkit
&lt;/h2&gt;&lt;p&gt;Ownership and borrowing, traits and &lt;code&gt;dyn&lt;/code&gt;, enums and &lt;code&gt;match&lt;/code&gt; and &lt;code&gt;#[non_exhaustive]&lt;/code&gt;, types that carry facts, &lt;code&gt;Result&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt;, and crates with features. Six ideas, and they&amp;rsquo;re enough to read everything else in this series without tripping over the language itself. Where a post needs a seventh thing, it&amp;rsquo;ll explain it in passing. Now, on with the actual building.&lt;/p&gt;</description></item><item><title>Where should a CLI keep your API keys?</title><link>https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/</guid><description>&lt;img src="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/cover-where-should-a-cli-keep-your-api-keys.png" alt="Featured image of post Where should a CLI keep your API keys?" /&gt;&lt;p&gt;Your CLI tool needs the user&amp;rsquo;s API key. It has to come from somewhere, and it has to survive between runs, so the obvious move is to ask once and write it into the config file. One tidy &lt;code&gt;api_key:&lt;/code&gt; line. Job done.&lt;/p&gt;
&lt;p&gt;It works beautifully on the first afternoon. And then, months later, it&amp;rsquo;s quietly become a liability nobody actually decided to create.&lt;/p&gt;
&lt;h2 id="the-config-file-that-quietly-becomes-a-liability"&gt;The config file that quietly becomes a liability
&lt;/h2&gt;&lt;p&gt;Your CLI tool needs the user&amp;rsquo;s API key. It has to come from somewhere, and it has to survive between invocations, so the obvious move is to ask once and write it into the tool&amp;rsquo;s config file. &lt;code&gt;~/.config/yourtool/config.yaml&lt;/code&gt;, a nice &lt;code&gt;api_key:&lt;/code&gt; line, done.&lt;/p&gt;
&lt;p&gt;It works on the first afternoon. It keeps working. And then, slowly, it becomes a problem nobody decided to create.&lt;/p&gt;
&lt;p&gt;The config file gets committed to a dotfiles repo. It gets caught in a &lt;code&gt;tar&lt;/code&gt; of someone&amp;rsquo;s home directory that lands in a backup bucket. It scrolls past in a screen share. It sits, world-readable, on a shared build box. None of these are exotic. They&amp;rsquo;re just a Tuesday. The plaintext key was fine right up until the file went somewhere the key shouldn&amp;rsquo;t, and config files go places.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want go-tool-base handing every tool built on it that same slow-motion liability by default. So credential handling got rebuilt around a simple idea: the config file should usually hold a &lt;em&gt;reference&lt;/em&gt; to the secret, not the secret itself.&lt;/p&gt;
&lt;h2 id="three-modes-and-which-one-you-get"&gt;Three modes, and which one you get
&lt;/h2&gt;&lt;p&gt;go-tool-base supports &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/credentials/mode.go#L18" target="_blank" rel="noopener"
 &gt;three ways to store a credential&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environment-variable reference, the default.&lt;/strong&gt; The config records the &lt;em&gt;name&lt;/em&gt; of an environment variable, not its value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The secret itself lives in your shell profile, your &lt;code&gt;direnv&lt;/code&gt; setup, or your CI platform&amp;rsquo;s secret store, wherever you already keep that sort of thing. The config file now contains nothing sensitive at all. You can commit it, back it up, paste it into a bug report. The reference is inert on its own.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OS keychain, opt-in.&lt;/strong&gt; The config holds a &lt;code&gt;&amp;lt;service&amp;gt;/&amp;lt;account&amp;gt;&lt;/code&gt; reference and the actual secret goes into the operating system&amp;rsquo;s keychain: macOS Keychain, GNOME Keyring or KWallet via the Secret Service, Windows Credential Manager.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;keychain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;mytool/anthropic.api&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This one is opt-in by design, because the keychain backend carries dependencies that some deployments simply aren&amp;rsquo;t allowed to ship. (That opt-in mechanism turned out to be an interesting little problem all of its own, and it gets &lt;a class="link" href="https://blog-570662.gitlab.io/the-blank-import-that-keeps-a-dependency-out-of-your-binary/" &gt;its own post&lt;/a&gt; in a couple of days.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Literal value, legacy and grudging.&lt;/strong&gt; The old behaviour. The secret sits in the config in plaintext:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sk-ant-...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It still works, because breaking every existing tool&amp;rsquo;s config on an upgrade would be its own kind of vandalism. But it&amp;rsquo;s the last resort, it&amp;rsquo;s documented as the last resort, and the setup wizard puts a warning in front of you when you pick it.&lt;/p&gt;
&lt;h2 id="the-one-place-literal-mode-is-not-allowed"&gt;The one place literal mode is not allowed
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a single hard &amp;ldquo;no&amp;rdquo; in all of this. If go-tool-base detects it&amp;rsquo;s running in CI (&lt;code&gt;CI=true&lt;/code&gt;, which every major CI platform sets) the setup flow will &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/setup/ai/ai.go#L177" target="_blank" rel="noopener"
 &gt;&lt;em&gt;refuse&lt;/em&gt; to write a literal credential&lt;/a&gt;, and exits non-zero.&lt;/p&gt;
&lt;p&gt;The reasoning is that a plaintext secret written during a CI run is a plaintext secret written onto an ephemeral, often shared, frequently-logged machine, by an automated process that no human is watching. That&amp;rsquo;s the exact situation where the slow-motion liability becomes a fast one. CI environments inject secrets as environment variables already; there&amp;rsquo;s no good reason for a tool to be writing one to disk there, so go-tool-base simply won&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="how-it-decides-at-runtime"&gt;How it decides at runtime
&lt;/h2&gt;&lt;p&gt;A credential can be configured more than one way at once. You might have an &lt;code&gt;env&lt;/code&gt; reference &lt;em&gt;and&lt;/em&gt; an old literal &lt;code&gt;key&lt;/code&gt; still lurking. So resolution follows a fixed precedence, highest to lowest:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;*.env&lt;/code&gt; reference. If that env var is set, use it.&lt;/li&gt;
&lt;li&gt;Otherwise the &lt;code&gt;*.keychain&lt;/code&gt; reference. If a keychain entry resolves, use it.&lt;/li&gt;
&lt;li&gt;Otherwise the literal &lt;code&gt;*.key&lt;/code&gt; / &lt;code&gt;*.value&lt;/code&gt;, the legacy path.&lt;/li&gt;
&lt;li&gt;Otherwise a well-known fallback env var (&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; and friends), so a tool still picks up the ecosystem-standard variable with no config at all.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The useful property here is that adding a more secure mode &lt;em&gt;transparently wins&lt;/em&gt;. Drop an &lt;code&gt;env&lt;/code&gt; reference next to an old literal key and the next run uses the env var. You can migrate a credential to a better home without first removing it from its worse one, which makes the migration safe to do incrementally instead of as one nervous big-bang edit.&lt;/p&gt;
&lt;h2 id="the-tool-tells-on-itself"&gt;The tool tells on itself
&lt;/h2&gt;&lt;p&gt;A precedence rule is no use if nobody knows their config still has a plaintext key three layers down. So the built-in &lt;code&gt;doctor&lt;/code&gt; command grew a check for exactly that. Run &lt;code&gt;doctor&lt;/code&gt;, and if any literal credential is sitting in your config it reports a warning, names the offending keys (the key &lt;em&gt;names&lt;/em&gt;, never the values) and points you at how to migrate.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not an error. Literal mode is still legal. But the tool will quietly keep reminding you that you left the campsite messier than you could have, until you go and tidy it. (Old Scout habits die hard, and they&amp;rsquo;ve leaked all the way into the framework.)&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;A CLI tool that writes your API key into a plaintext config file isn&amp;rsquo;t doing anything &lt;em&gt;wrong&lt;/em&gt;, exactly. It&amp;rsquo;s just handing you a liability that activates later, when the file travels somewhere the key shouldn&amp;rsquo;t. go-tool-base&amp;rsquo;s answer is three storage modes: an env-var reference by default, the OS keychain on request, and a plaintext literal only as a documented last resort that CI environments can&amp;rsquo;t use at all. Runtime resolution runs in a fixed precedence so a more secure mode always wins, which makes migrating a credential safe to do gradually. And &lt;code&gt;doctor&lt;/code&gt; keeps an eye on the config so a stray plaintext secret doesn&amp;rsquo;t get to hide forever.&lt;/p&gt;
&lt;p&gt;The secret should live in a secret store. The config file should just know its name.&lt;/p&gt;</description></item><item><title>A configurable AI endpoint is an attack surface</title><link>https://blog-570662.gitlab.io/a-configurable-ai-endpoint-is-an-attack-surface/</link><pubDate>Sun, 19 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-configurable-ai-endpoint-is-an-attack-surface/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-configurable-ai-endpoint-is-an-attack-surface/cover-a-configurable-ai-endpoint-is-an-attack-surface.png" alt="Featured image of post A configurable AI endpoint is an attack surface" /&gt;&lt;p&gt;&amp;ldquo;Let users point at their own AI endpoint&amp;rdquo; is one of those config options that looks completely harmless on the way in. People want it, for perfectly good reasons. Then you sit with it for a minute and realise you&amp;rsquo;ve handed every user a loaded gun and pointed it vaguely at their own API key.&lt;/p&gt;
&lt;h2 id="why-you-offer-it-at-all"&gt;Why you offer it at all
&lt;/h2&gt;&lt;p&gt;There are real reasons to let someone set a custom base URL. They&amp;rsquo;re running a local model and want &lt;code&gt;localhost:11434&lt;/code&gt;. They&amp;rsquo;re behind a corporate proxy that fronts the real provider. They&amp;rsquo;re on Azure&amp;rsquo;s flavour of OpenAI, which lives at a different host. They&amp;rsquo;ve a self-hosted gateway doing rate-limiting. All reasonable, all things a framework should support rather than fight.&lt;/p&gt;
&lt;h2 id="the-bit-thats-a-loaded-gun"&gt;The bit that&amp;rsquo;s a loaded gun
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s what the config option quietly decides: the base URL is &lt;em&gt;where your credential goes&lt;/em&gt;. The API key rides along in an &lt;code&gt;Authorization&lt;/code&gt; header on every request, to whatever host that URL resolves to. So the moment the endpoint is user-configurable, the destination of your secret is user-configurable too.&lt;/p&gt;
&lt;p&gt;And users do user things. They paste a URL from a gist that turned out to be a honeypot. They leave &lt;code&gt;http://&lt;/code&gt; on the front, so the key crosses the wire in plaintext. They copy &lt;code&gt;https://user:token@host/v1&lt;/code&gt; not realising the userinfo changes who they actually authenticate to. They never edit the &lt;code&gt;https://api.example.com/v1&lt;/code&gt; placeholder and wonder why the key&amp;rsquo;s been posted to a domain they don&amp;rsquo;t own. None of that is malice. It&amp;rsquo;s what happens when the destination of a secret is a free-text field.&lt;/p&gt;
&lt;h2 id="validate-before-the-first-byte-leaves"&gt;Validate before the first byte leaves
&lt;/h2&gt;&lt;p&gt;So every &lt;code&gt;chat.New&lt;/code&gt; routes through &lt;code&gt;ValidateBaseURL&lt;/code&gt; before the provider is built. The threat model is written at the top of &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/baseurl.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/chat/baseurl.go&lt;/code&gt;&lt;/a&gt;: an operator who can influence config could &amp;ldquo;redirect chat-provider traffic to an attacker-controlled HTTPS host and capture the Authorization header.&amp;rdquo; The checks run cheapest-first: a length cap, no ASCII control characters, must parse, no userinfo, &lt;code&gt;https&lt;/code&gt; only, a host must be present, and the host mustn&amp;rsquo;t be a placeholder.&lt;/p&gt;
&lt;p&gt;The userinfo rule is the sharp one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// Reject any userinfo, with or without password. Never log&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// the URL itself because it contains the credential.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ErrInvalidBaseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;base URL must not contain credentials; use the Token field instead&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The placeholder check rejects &lt;code&gt;example.com&lt;/code&gt; and friends &lt;em&gt;and any subdomain of them&lt;/em&gt;, so the unedited &lt;code&gt;https://api.example.com/v1&lt;/code&gt; from a setup wizard never reaches the wire and hits some typosquatted lookalike. And the HTTP escape hatch is test-only by construction: the &lt;code&gt;AllowInsecureBaseURL&lt;/code&gt; field that permits plain &lt;code&gt;http&lt;/code&gt; is tagged &lt;code&gt;json:&amp;quot;-&amp;quot;&lt;/code&gt;, so a config file physically cannot set it. This all came out of the 2026-04-17 security audit, finding M-3.&lt;/p&gt;
&lt;p&gt;rust-tool-base enforces the same at its own boundary: &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-ai/src/config.rs#L96" target="_blank" rel="noopener"
 &gt;&lt;code&gt;validate_base_url&lt;/code&gt;&lt;/a&gt; rejects userinfo, any scheme but &lt;code&gt;https&lt;/code&gt; (bar a test-only &lt;code&gt;allow_insecure&lt;/code&gt;), and documentation placeholder hosts like &lt;code&gt;example.com&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-it-can-and-cant-do"&gt;What it can and can&amp;rsquo;t do
&lt;/h2&gt;&lt;p&gt;It won&amp;rsquo;t stop a user who deliberately points the tool at a malicious HTTPS host they genuinely chose. If someone is set on sending their own key somewhere bad, validation can&amp;rsquo;t read their mind.&lt;/p&gt;
&lt;p&gt;What it stops is the &lt;em&gt;accidents&lt;/em&gt;: the plaintext slip, the userinfo confusion, the placeholder nobody changed. Those aren&amp;rsquo;t theoretical, they&amp;rsquo;re the ones that happen to careful people on ordinary days. Storing the key well is one job (&lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;where a CLI keeps it&lt;/a&gt;), stopping it &lt;a class="link" href="https://blog-570662.gitlab.io/redacting-the-secret-you-didnt-know-was-in-the-string/" &gt;leaking through a log&lt;/a&gt; is another, and this is the third side of the triangle: once you&amp;rsquo;ve stored it and stopped it leaking, make sure you don&amp;rsquo;t &lt;em&gt;send&lt;/em&gt; it somewhere daft.&lt;/p&gt;</description></item><item><title>Redacting the secret you didn't know was in the string</title><link>https://blog-570662.gitlab.io/redacting-the-secret-you-didnt-know-was-in-the-string/</link><pubDate>Sat, 18 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/redacting-the-secret-you-didnt-know-was-in-the-string/</guid><description>&lt;img src="https://blog-570662.gitlab.io/redacting-the-secret-you-didnt-know-was-in-the-string/cover-redacting-the-secret-you-didnt-know.png" alt="Featured image of post Redacting the secret you didn't know was in the string" /&gt;&lt;p&gt;Dammit! How did that get there?&lt;/p&gt;
&lt;p&gt;A log line that should never have existed. Not a password I&amp;rsquo;d carelessly printed, nothing as obvious as that. An upstream API handed me back an error, and it had quoted my own bearer token inside the message, and that error went straight into the logs the way errors do. I didn&amp;rsquo;t put the secret there. The error did. And I&amp;rsquo;d never have caught it by being careful, because being careful only protects you from the secrets you know you&amp;rsquo;re handling.&lt;/p&gt;
&lt;h2 id="the-easy-half-of-redaction"&gt;The easy half of redaction
&lt;/h2&gt;&lt;p&gt;Hiding the secrets you know about is the part everyone does. You&amp;rsquo;ve got an API key field, a password flag, so you mask them at the point you print them. &lt;code&gt;key=****&lt;/code&gt;. Done, and it feels like you&amp;rsquo;ve solved redaction, when really you&amp;rsquo;ve solved the half that was never going to bite you.&lt;/p&gt;
&lt;h2 id="the-half-that-bites"&gt;The half that bites
&lt;/h2&gt;&lt;p&gt;The secrets that escape are the ones that arrive inside strings you don&amp;rsquo;t control. An upstream service echoes your token back in a &lt;code&gt;401&lt;/code&gt; body. A connection string with the password in the userinfo, &lt;code&gt;https://user:pass@host&lt;/code&gt;, lands in a debug line. A library stringifies a whole request, headers and all, for a &amp;ldquo;helpful&amp;rdquo; trace. You cannot field-mask a secret you didn&amp;rsquo;t know was in the string, because you never watched it go in.&lt;/p&gt;
&lt;h2 id="you-cant-register-a-value-you-never-had-so-match-the-shape"&gt;You can&amp;rsquo;t register a value you never had, so match the shape
&lt;/h2&gt;&lt;p&gt;This is the bit I got wrong in my own head at first. I assumed redaction meant handing it the secrets I was holding so it could watch for them. But the dangerous secrets are exactly the ones I&amp;rsquo;m &lt;em&gt;not&lt;/em&gt; holding a copy of. So &lt;code&gt;pkg/redact&lt;/code&gt; doesn&amp;rsquo;t keep a registry of your values at all. It knows what secrets &lt;em&gt;look like&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/redact/redact.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/redact/redact.go&lt;/code&gt;&lt;/a&gt; carries a set of RE2 patterns: a credential in URL userinfo, an &lt;code&gt;Authorization:&lt;/code&gt; header sitting in free text, query-string credentials, and the well-known provider prefixes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;prefixPatterns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Regexp&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`sk-[A-Za-z0-9_\-]{16,}`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// OpenAI / Anthropic-style&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`ghp_[A-Za-z0-9]{30,}`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// GitHub PAT classic&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`github_pat_[A-Za-z0-9_]{30,}`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// GitHub fine-grained PAT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`xox[baprs]-[A-Za-z0-9-]{10,}`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Slack&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`AIza[A-Za-z0-9_\-]{30,}`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Google API key&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MustCompile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;`AKIA[A-Z0-9]{16}`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// AWS access key ID&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run any string through &lt;code&gt;redact.String&lt;/code&gt; and an OpenAI key, a GitHub token or an AWS access key ID gets caught wherever it&amp;rsquo;s hiding, in an error you didn&amp;rsquo;t write, in a URL, in a stack trace, because each has a recognisable shape. For the secrets that don&amp;rsquo;t announce themselves with a prefix there&amp;rsquo;s a fuzzy fallback: any opaque alphanumeric run of 41 characters or more. The 41 is chosen on purpose, to clear UUIDs (36), MD5 (32) and git SHA-1 (40) without flagging them, while accepting that a SHA-256 (64) will trip it. A deliberate, documented trade rather than a magic number.&lt;/p&gt;
&lt;h2 id="where-it-runs"&gt;Where it runs
&lt;/h2&gt;&lt;p&gt;At the boundary where a string leaves for somewhere you can&amp;rsquo;t reach back into. The telemetry backend runs every event argument and error message through &lt;code&gt;redact.String&lt;/code&gt; before it emits anything (&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/telemetry/telemetry.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/telemetry/telemetry.go&lt;/code&gt;&lt;/a&gt;), and both telemetry and HTTP logging drop the value of any header &lt;code&gt;redact&lt;/code&gt; flags as sensitive. It doesn&amp;rsquo;t matter which code path produced the string, or whether you even wrote that path; everything goes through the same gate and gets the same scrub.&lt;/p&gt;
&lt;p&gt;rust-tool-base&amp;rsquo;s &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-redact/src/lib.rs" target="_blank" rel="noopener"
 &gt;&lt;code&gt;rtb-redact&lt;/code&gt;&lt;/a&gt; crate takes the same shape-matching approach: regex patterns, the same family of well-known provider prefixes, and an &lt;code&gt;is_sensitive_header&lt;/code&gt; check for header values.&lt;/p&gt;
&lt;h2 id="a-realistic-limit"&gt;A realistic limit
&lt;/h2&gt;&lt;p&gt;It isn&amp;rsquo;t a force field. A secret with no recognisable shape, shorter than the fallback threshold, will sail through. You cannot redact what you cannot recognise. But the leak that actually keeps happening isn&amp;rsquo;t some exotic unknown, it&amp;rsquo;s a well-known token turning up in a place you didn&amp;rsquo;t expect, and a shape-matcher sitting at the edge catches exactly that, &lt;em&gt;including secrets you never told it about&lt;/em&gt;. Which is the one thing registering your own values could never have done. Storing the key safely is a separate job, &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;where a CLI keeps it&lt;/a&gt;; this is about making sure that, having stored it, it doesn&amp;rsquo;t quietly fall out through a log.&lt;/p&gt;</description></item><item><title>I had the framework audited: every finding was the same shape</title><link>https://blog-570662.gitlab.io/every-finding-was-the-same-shape/</link><pubDate>Fri, 17 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/every-finding-was-the-same-shape/</guid><description>&lt;img src="https://blog-570662.gitlab.io/every-finding-was-the-same-shape/cover-every-finding-was-the-same-shape.png" alt="Featured image of post I had the framework audited: every finding was the same shape" /&gt;&lt;p&gt;When a real security audit lands back in your inbox, the temptation is to read it as a shopping list of unrelated mistakes. Fix one, fix the next, tick them off, move on. I did exactly that the first time. The second time, I noticed something far more useful: the findings weren&amp;rsquo;t scattered at all. They clustered. Almost every one was the same sentence with the nouns swapped out.&lt;/p&gt;
&lt;h2 id="findings-cluster-they-dont-scatter"&gt;Findings cluster, they don&amp;rsquo;t scatter
&lt;/h2&gt;&lt;p&gt;When you get a real security audit back, the instinct is to read it as a list of unrelated mistakes. Finding 1, unrelated to Finding 2, unrelated to Finding 3. Triage each, fix each, move on.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s not what the go-tool-base audits looked like once I stopped reading them as a list. The findings &lt;em&gt;clustered&lt;/em&gt;. Strip away the specifics and almost every one was the same sentence with the nouns swapped: &lt;em&gt;untrusted input reaches a powerful operation, and nothing checks it in between.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That reframe is worth more than any individual fix, because it turns &amp;ldquo;we patched some bugs&amp;rdquo; into &amp;ldquo;we know where to look next time&amp;rdquo;. A framework&amp;rsquo;s attack surface isn&amp;rsquo;t spread evenly. It&amp;rsquo;s concentrated at the &lt;em&gt;boundaries&lt;/em&gt;: the handful of points where data from outside (a config file, a command-line flag, something typed into a TUI, an HTTP response) flows into machinery that can be made to misbehave. Audit the boundaries and you&amp;rsquo;ve audited most of the risk. Three examples make the pattern obvious.&lt;/p&gt;
&lt;h2 id="boundary-one-a-regex-compiler"&gt;Boundary one: a regex compiler
&lt;/h2&gt;&lt;p&gt;Somewhere in the tool, a user-supplied string gets compiled into a regular expression. A search pattern typed into the docs browser, a filter from a config file. Feeding user input to &lt;code&gt;regexp.Compile&lt;/code&gt; feels harmless. It&amp;rsquo;s just pattern matching, after all.&lt;/p&gt;
&lt;p&gt;It isn&amp;rsquo;t quite harmless. A regular expression is a tiny program, and some tiny programs are catastrophically slow. A pattern with the wrong kind of nested repetition can take exponential time to evaluate against a modestly hostile input. That&amp;rsquo;s the class of bug known as ReDoS. A user, or something feeding the user&amp;rsquo;s config, hands you a pathological pattern and your tool wedges, burning a whole core, on what looked for all the world like a search box.&lt;/p&gt;
&lt;p&gt;The fix isn&amp;rsquo;t to ban user-supplied regexes. It&amp;rsquo;s to stop treating &amp;ldquo;compile this string&amp;rdquo; as free. go-tool-base routes any regex whose pattern came from outside the binary through a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/regexutil/compile.go#L48" target="_blank" rel="noopener"
 &gt;&lt;code&gt;regexutil.CompileBounded&lt;/code&gt;&lt;/a&gt; helper. It caps the pattern length and puts a hard timeout on compilation. A pattern known at build time can still use plain &lt;code&gt;regexp.MustCompile&lt;/code&gt;, because that isn&amp;rsquo;t a boundary, it&amp;rsquo;s a constant. The discipline only applies where the input genuinely crosses in.&lt;/p&gt;
&lt;h2 id="boundary-two-a-url-opener"&gt;Boundary two: a URL opener
&lt;/h2&gt;&lt;p&gt;The tool needs to open a URL in the user&amp;rsquo;s browser, a docs link or an OAuth flow. Under the hood that&amp;rsquo;s the OS handler: &lt;code&gt;xdg-open&lt;/code&gt;, or &lt;code&gt;open&lt;/code&gt;, or &lt;code&gt;rundll32&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now ask where the URL came from. If any part of it is influenced by config, by a server response, by user input, then &amp;ldquo;open this URL&amp;rdquo; has quietly become &amp;ldquo;ask the operating system to do something with an attacker-influenced string&amp;rdquo;. A &lt;code&gt;file://&lt;/code&gt; URL. A &lt;code&gt;javascript:&lt;/code&gt; URL. Something with control characters smuggled into it. The browser-open was never the dangerous part. The &lt;em&gt;unvalidated string&lt;/em&gt; was.&lt;/p&gt;
&lt;p&gt;So go-tool-base funnels every URL-open through one package, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/browser/browser.go#L25" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/browser&lt;/code&gt;&lt;/a&gt;, and that package is a gate. It enforces an allowlist of schemes (&lt;code&gt;https&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;mailto&lt;/code&gt;, and nothing else), bounds the length, and rejects control characters before the OS ever sees the string. The rule that makes it stick is that nothing else is allowed to call the OS handler directly. One door, and the door has a lock. A scattered capability with no chokepoint can&amp;rsquo;t be secured; a capability that &lt;em&gt;has&lt;/em&gt; a chokepoint can. (You&amp;rsquo;ll have spotted the &amp;ldquo;one door out&amp;rdquo; idea by now&amp;hellip; it&amp;rsquo;s the same instinct as the &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;single error handler&lt;/a&gt;, pointed at security instead of consistency.)&lt;/p&gt;
&lt;h2 id="boundary-three-a-log-sink"&gt;Boundary three: a log sink
&lt;/h2&gt;&lt;p&gt;This one&amp;rsquo;s the sneakiest, because it runs the wrong way round. The first two boundaries are about dangerous input coming &lt;em&gt;in&lt;/em&gt;. This one is about sensitive data leaking &lt;em&gt;out&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The tool handles credentials. It also logs, emits telemetry, and reports errors, and all three of those are &lt;em&gt;exit&lt;/em&gt; boundaries: places where strings leave the process for somewhere more persistent and more public, like a log aggregator, an analytics backend, an error tracker. If a token ever ends up in a string that flows to one of those, you haven&amp;rsquo;t logged an event, you&amp;rsquo;ve published a secret.&lt;/p&gt;
&lt;p&gt;The defence is &lt;code&gt;pkg/redact&lt;/code&gt;. Any free-form string heading for an observability surface goes through it first, and it strips the usual suspects: credentials in URL userinfo, sensitive query parameters, &lt;code&gt;Authorization&lt;/code&gt; headers, the well-known provider key prefixes (&lt;code&gt;sk-&lt;/code&gt;, &lt;code&gt;ghp_&lt;/code&gt;, &lt;code&gt;AIza&lt;/code&gt; and friends), long opaque tokens. The places most likely to leak, command arguments and error messages in telemetry, get it applied automatically rather than relying on every caller to remember.&lt;/p&gt;
&lt;p&gt;Same pattern as the other two. A boundary, and something standing on it checking what goes through.&lt;/p&gt;
&lt;h2 id="the-unglamorous-part"&gt;The unglamorous part
&lt;/h2&gt;&lt;p&gt;None of these fixes is clever. There&amp;rsquo;s no exploit demo, no neat trick to show off. Bound a length. Check a scheme against an allowlist. Run a string through a redactor. The work was almost entirely in &lt;em&gt;noticing the boundary existed&lt;/em&gt;, and then making sure everything routes through the one checked path instead of dotting raw calls all over the codebase.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the actual lesson of a security audit, and it&amp;rsquo;s why the cluster reframe matters. The value wasn&amp;rsquo;t the dozen-or-so individual fixes. It was learning that the next risk will be at a boundary too, the next place untrusted input meets a powerful operation with nothing in between, and that the job is to find those points and put a single, mandatory, checked door on each.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;A security audit of a CLI framework reads like a list of unrelated bugs and isn&amp;rsquo;t one. go-tool-base&amp;rsquo;s findings nearly all reduced to the same shape: untrusted input reaching a powerful operation unchecked. A regex compiler that needed a length and time bound (&lt;code&gt;regexutil.CompileBounded&lt;/code&gt;). A URL opener that needed a scheme allowlist and a single chokepoint (&lt;code&gt;pkg/browser&lt;/code&gt;). Log and telemetry sinks that needed credentials redacted on the way out (&lt;code&gt;pkg/redact&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The fixes were structural and dull, which is exactly right. Find your boundaries (config, flags, TUI input, network responses, log and telemetry sinks), give each one a single mandatory checked path, and you&amp;rsquo;ve spent your audit effort where the risk actually lives.&lt;/p&gt;</description></item><item><title>A mutex on a flag nobody writes twice</title><link>https://blog-570662.gitlab.io/a-mutex-on-a-flag-nobody-writes-twice/</link><pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-mutex-on-a-flag-nobody-writes-twice/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-mutex-on-a-flag-nobody-writes-twice/cover-a-mutex-on-a-flag-nobody-writes-twice.png" alt="Featured image of post A mutex on a flag nobody writes twice" /&gt;&lt;p&gt;&amp;ldquo;Why is there a mutex around a boolean that only ever gets set once?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a fair question, and I&amp;rsquo;d half-asked it of myself before someone asked it of me. The answer turns out to be written, in as many words, in a code comment I&amp;rsquo;ve grown rather fond of.&lt;/p&gt;
&lt;h2 id="the-registry-and-its-one-way-latch"&gt;The registry and its one-way latch
&lt;/h2&gt;&lt;p&gt;go-tool-base keeps a feature registry: the initialisers, sub-commands, flags and checks that each feature adds to the CLI. Features register themselves into it at startup, from &lt;code&gt;init()&lt;/code&gt;, before &lt;code&gt;main&lt;/code&gt; runs. Once everything&amp;rsquo;s wired, the framework calls &lt;code&gt;SealRegistry()&lt;/code&gt; and the registry latches shut. Any &lt;code&gt;Register&lt;/code&gt; call after that point panics, on purpose, because a sub-command or flag that turns up &lt;em&gt;after&lt;/em&gt; the CLI has parsed its arguments is a bug I want to hear about at once, not discover three releases later.&lt;/p&gt;
&lt;p&gt;So there&amp;rsquo;s a &lt;code&gt;registrySealed&lt;/code&gt; bool. It starts &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;SealRegistry&lt;/code&gt; flips it to &lt;code&gt;true&lt;/code&gt; exactly once in normal operation, nothing flips it back outside of tests, and it&amp;rsquo;s read on every registration attempt. Written once, read many. The textbook shape of &amp;ldquo;you don&amp;rsquo;t need a lock for this.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="except-the-comment-disagrees-on-purpose"&gt;Except the comment disagrees, on purpose
&lt;/h2&gt;&lt;p&gt;Here is the actual declaration, in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/setup/registry.go#L46-54" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/setup/registry.go&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// registryMu protects globalRegistry and registrySealed. Acquired for write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// by all Register* and Reset/Seal helpers; acquired for read by all Get*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// accessors. The mutex is required for memory visibility of registrySealed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// across goroutines, not only mutual exclusion on the maps.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;registryMu&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RWMutex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;registrySealed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That last sentence is the entire post. The mutex has an obvious day job: the registry is a clutch of maps that get appended to during registration, and concurrent appends need genuine mutual exclusion. &lt;code&gt;registrySealed&lt;/code&gt; could have just hitched a ride on that lock and nobody would have thought twice. But the comment goes out of its way to say the lock is &lt;em&gt;also required&lt;/em&gt; for the flag, for visibility, not only exclusion.&lt;/p&gt;
&lt;h2 id="why-a-write-once-bool-still-needs-the-lock"&gt;Why a write-once bool still needs the lock
&lt;/h2&gt;&lt;p&gt;The Go memory model makes no promise that a goroutine reading &lt;code&gt;registrySealed&lt;/code&gt; will ever see the write &lt;code&gt;SealRegistry&lt;/code&gt; made, unless there is a happens-before relationship between them. No synchronisation, no guarantee. A reader can sit there seeing &lt;code&gt;false&lt;/code&gt; long after the seal happened on another goroutine, because the compiler may cache the read and the CPU may serve it from a core-local view. And a concurrent read and write of the same variable, with nothing ordering them, isn&amp;rsquo;t &amp;ldquo;probably fine&amp;rdquo;; it&amp;rsquo;s a data race, which Go defines as undefined behaviour.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;But registration is single-threaded, it&amp;rsquo;s all &lt;code&gt;init()&lt;/code&gt;.&amp;rdquo; It was, right up until we wanted the tests to run in parallel. This lock exists because of a deliberate campaign to restore &lt;code&gt;t.Parallel()&lt;/code&gt; across the codebase after a stack of races forced us to drop it (the same campaign that &lt;a class="link" href="https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/" &gt;retired the package-level mocking hooks&lt;/a&gt;). Tests build, register, seal and reset this registry from parallel goroutines. The instant that&amp;rsquo;s true, the seal check has to stay correct &lt;em&gt;while racing&lt;/em&gt;, because the very thing it guards against is concurrency. So reads take &lt;code&gt;registryMu.RLock&lt;/code&gt;, the write takes &lt;code&gt;registryMu.Lock&lt;/code&gt;, and now there&amp;rsquo;s a happens-before edge: anyone who acquires the lock after &lt;code&gt;SealRegistry&lt;/code&gt; released it is guaranteed to see &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-the-lock-is-actually-for"&gt;What the lock is actually for
&lt;/h2&gt;&lt;p&gt;It isn&amp;rsquo;t there to stop two goroutines both sealing the registry. There&amp;rsquo;s only ever the one seal. It&amp;rsquo;s there so that every reader can trust what it reads. A value written exactly once is precisely the case where you&amp;rsquo;re most tempted to skip the synchronisation, and precisely the case where skipping it can leave a reader legally staring at the stale value for good. The comment spells it out so that the next person to glance at &lt;code&gt;registrySealed&lt;/code&gt;, think &amp;ldquo;that clearly doesn&amp;rsquo;t need a lock,&amp;rdquo; and reach for the delete key, reads the sentence first.&lt;/p&gt;
&lt;p&gt;(There&amp;rsquo;s a sibling &lt;code&gt;sealed&lt;/code&gt; flag in the middleware registry that follows the identical pattern, for the identical reason.)&lt;/p&gt;</description></item><item><title>The test-mocking pattern that races</title><link>https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/</link><pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/cover-the-test-mocking-pattern-that-races.png" alt="Featured image of post The test-mocking pattern that races" /&gt;&lt;p&gt;I&amp;rsquo;m going to tell you about a bug go-tool-base shipped, because it&amp;rsquo;s one of those bugs that&amp;rsquo;s so reasonable-looking you&amp;rsquo;ll find it in textbooks, conference talks, and an awful lot of otherwise excellent Go code. We had it too. It passed every test on my laptop, every single time, and then quietly fell over on CI while blaming an innocent bystander.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the classic Go trick for mocking a dependency, and it races.&lt;/p&gt;
&lt;h2 id="a-pattern-that-looks-completely-reasonable"&gt;A pattern that looks completely reasonable
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s a thing you need to do constantly in Go tests: stop a function from really shelling out. It calls &lt;code&gt;exec.LookPath&lt;/code&gt; to find a binary, or &lt;code&gt;exec.Command&lt;/code&gt; to run one, and your test very much does not want it touching the real &lt;code&gt;$PATH&lt;/code&gt; or spawning a real process.&lt;/p&gt;
&lt;p&gt;The Go community has a well-worn answer. Hoist the function into a package-level variable, call &lt;em&gt;that&lt;/em&gt;, and let tests reassign it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// production code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LookPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;findTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;execLookPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sometool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// test&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TestFindTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;execLookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/fake/path&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...assert...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s tidy. No interface to thread through, no constructor to change. You&amp;rsquo;ll find it in a great deal of Go code, including some very respectable Go code indeed. go-tool-base had it too.&lt;/p&gt;
&lt;p&gt;And it works. It works on your machine, it works in code review, it works the first hundred times CI runs it. Which is precisely what makes it dangerous, because it&amp;rsquo;s wrong, and it&amp;rsquo;s just been biding its time.&lt;/p&gt;
&lt;h2 id="add-one-line-and-it-detonates"&gt;Add one line and it detonates
&lt;/h2&gt;&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;t.Parallel()&lt;/code&gt; is more or less free performance. Mark your tests with it and the runner overlaps them instead of plodding through one at a time. On a package with a few hundred tests it&amp;rsquo;s a real, worthwhile speed-up, so naturally you reach for it.&lt;/p&gt;
&lt;p&gt;Now picture two tests, both using the pattern above, both marked &lt;code&gt;t.Parallel()&lt;/code&gt;. They run concurrently. Test A assigns its fake to &lt;code&gt;execLookPath&lt;/code&gt;. Test B assigns &lt;em&gt;its&lt;/em&gt; fake to &lt;code&gt;execLookPath&lt;/code&gt;. Test A reads &lt;code&gt;execLookPath&lt;/code&gt; expecting its own fake. Two goroutines, one variable, writes and reads with nothing synchronising them. That&amp;rsquo;s a textbook data race, and the textbook is right: the behaviour is undefined. Test A might see B&amp;rsquo;s fake. The deferred restore might land in the wrong order and leave the variable pointing at a fake after both tests have finished, poisoning a third one for good measure.&lt;/p&gt;
&lt;p&gt;The truly nasty part is the &lt;em&gt;intermittency&lt;/em&gt;. Whether the race actually bites depends on goroutine scheduling, which depends on machine load and core count. Your laptop running eight tests at once might never lose the coin-toss. A CI runner under load, scheduling differently, loses it and fails a test that has nothing obviously to do with the change in the commit. You re-run the pipeline, it passes, everyone shrugs and moves on. A test suite that fails one run in twenty trains your team to ignore it, and an ignored CI failure is worse than no CI at all.&lt;/p&gt;
&lt;p&gt;I can tell you this one from direct, slightly embarrassed experience, because go-tool-base shipped exactly this bug and CI caught it the honest way: green on the laptop, red on the runner, with the failure cheerfully pointing at innocent bystander tests rather than the global that was actually the culprit. &lt;code&gt;go test -race&lt;/code&gt; will name it for you if you crank the parallelism up high enough to lose the toss reliably&amp;hellip; but you have to go looking, and you only go looking once it&amp;rsquo;s already ruined an afternoon.&lt;/p&gt;
&lt;h2 id="the-fix-isnt-synchronisation-its-structure"&gt;The fix isn&amp;rsquo;t synchronisation, it&amp;rsquo;s structure
&lt;/h2&gt;&lt;p&gt;The instinct is to slap a mutex around the variable. Resist it. A mutex makes the race &lt;em&gt;defined&lt;/em&gt;, but it doesn&amp;rsquo;t make the design any good. You&amp;rsquo;ve still got global mutable state, you&amp;rsquo;ve just queued the fight instead of cancelling it. And tests that serialise on a shared lock aren&amp;rsquo;t really parallel any more, so you&amp;rsquo;ve also handed back the speed-up you came for in the first place.&lt;/p&gt;
&lt;p&gt;The real fix is to not have a shared variable at all. The dependency was always an &lt;em&gt;input&lt;/em&gt; to the code; the package-level var was just a way of avoiding saying so out loud. So say it. Inject it.&lt;/p&gt;
&lt;p&gt;A struct field:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Finder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lookPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// defaults to exec.LookPath&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Finder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sometool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Or a functional option, if you&amp;rsquo;d rather keep the zero value clean. Either way, each test constructs its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;Finder&lt;/code&gt; with its &lt;em&gt;own&lt;/em&gt; fake. There&amp;rsquo;s no shared variable, so there&amp;rsquo;s no race, and &lt;code&gt;t.Parallel()&lt;/code&gt; is free again because the tests genuinely don&amp;rsquo;t touch each other.&lt;/p&gt;
&lt;p&gt;go-tool-base wrote this straight into its standing rules: no package-level mocking hooks, full stop. Dependencies come in through struct fields, functional options, or config fields. (The same injection discipline that makes &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt; so testable, applied one rung further down.) And to stop everyone hand-rolling the same &lt;code&gt;exec&lt;/code&gt; fakes, there&amp;rsquo;s a small internal package, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/internal/exectest/exectest.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;internal/exectest&lt;/code&gt;&lt;/a&gt;, with ready-made &lt;code&gt;LookPath&lt;/code&gt; and &lt;code&gt;CommandContext&lt;/code&gt; doubles you construct per-test. The pattern is gone, and the door it came in through is shut.&lt;/p&gt;
&lt;h2 id="the-rule-worth-taking-away"&gt;The rule worth taking away
&lt;/h2&gt;&lt;p&gt;A package-level variable that tests reassign is shared mutable state. It reads as a harmless convenience because in a single-threaded test run it behaves like one. &lt;code&gt;t.Parallel()&lt;/code&gt; is the thing that reveals it was never harmless, only unobserved.&lt;/p&gt;
&lt;p&gt;The general lesson is older than Go: &lt;strong&gt;if a value is an input to your code, make it an input.&lt;/strong&gt; Smuggling it in as a global is borrowing test-time convenience against a debt that comes due, with interest, the day someone wants their tests to run in parallel. Pay cash. Inject the dependency.&lt;/p&gt;
&lt;h2 id="worth-remembering"&gt;Worth remembering
&lt;/h2&gt;&lt;p&gt;Mocking via a reassignable package-level variable is a beloved Go shortcut and a latent data race. It survives because single-threaded test runs hide it; &lt;code&gt;t.Parallel()&lt;/code&gt; exposes it as intermittent, bystander-blaming CI flake that&amp;rsquo;s miserable to trace. A mutex only makes the bad design &lt;em&gt;defined&lt;/em&gt;. The fix is structural: inject the dependency as a struct field or functional option, so each test owns its own double and there&amp;rsquo;s no shared state to race over. go-tool-base banned the global-hook pattern outright and ships &lt;code&gt;internal/exectest&lt;/code&gt; so nobody&amp;rsquo;s tempted back to it.&lt;/p&gt;
&lt;p&gt;If a piece of code depends on something, let it &lt;em&gt;say&lt;/em&gt; so in its signature. Your future self, staring at a CI failure that flatly refuses to reproduce, will thank you.&lt;/p&gt;</description></item><item><title>OpenSSF Scorecard graded my supply chain</title><link>https://blog-570662.gitlab.io/openssf-scorecard-graded-my-supply-chain/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/openssf-scorecard-graded-my-supply-chain/</guid><description>&lt;img src="https://blog-570662.gitlab.io/openssf-scorecard-graded-my-supply-chain/cover-openssf-scorecard-graded-my-supply-chain.png" alt="Featured image of post OpenSSF Scorecard graded my supply chain" /&gt;&lt;p&gt;I turned OpenSSF Scorecard on expecting a pat on the head. go-tool-base is a security-minded project, I&amp;rsquo;m careful, surely the robot would agree. The robot did not agree. It handed back a report card with a fair bit of red ink, and the most pointed finding on it wasn&amp;rsquo;t about my code at all. It was about me.&lt;/p&gt;
&lt;h2 id="a-linter-for-the-things-you-dont-call-code"&gt;A linter for the things you don&amp;rsquo;t call code
&lt;/h2&gt;&lt;p&gt;Scorecard is an automated set of checks that grades a repository&amp;rsquo;s supply-chain hygiene: are your CI dependencies pinned, are your workflow tokens least-privilege, is your branch protected, do commits get reviewed. It&amp;rsquo;s a linter, but pointed at the part of the project you don&amp;rsquo;t usually think of as code, the build and release machinery and the practices around them. And like any good linter, its value is mostly in catching the things you&amp;rsquo;d swear you&amp;rsquo;d already got right.&lt;/p&gt;
&lt;p&gt;Three of its findings were worth the price of admission on their own.&lt;/p&gt;
&lt;h2 id="pin-the-actions-you-dont-control"&gt;Pin the actions you don&amp;rsquo;t control
&lt;/h2&gt;&lt;p&gt;The first was about how go-tool-base&amp;rsquo;s GitHub Actions referenced other actions. Like nearly everyone, I&amp;rsquo;d written &lt;code&gt;uses: actions/checkout@v6&lt;/code&gt;. Scorecard doesn&amp;rsquo;t like that, and it&amp;rsquo;s right not to.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@v6&lt;/code&gt; is a &lt;em&gt;tag&lt;/em&gt;, and a tag is mutable. Whoever controls that action can move &lt;code&gt;v6&lt;/code&gt; to point at different code tomorrow, and your CI will pick it up silently on the next run. For an action that runs in a job holding your repository token, that&amp;rsquo;s a supply-chain hole the width of a barn door: compromise the tag, compromise every pipeline that trusts it. The fix is to pin to an immutable commit SHA, with the human-readable version left as a comment, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/commit/5500b92" target="_blank" rel="noopener"
 &gt;which is exactly what I changed&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-go@v6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;+ - uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# v6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;+ - uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# v6&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the action is frozen at bytes I reviewed. Dependabot still bumps the SHA when a real new version lands, so I get updates as reviewable pull requests rather than as silent tag movements. The pin doesn&amp;rsquo;t stop me updating. It stops me updating &lt;em&gt;without noticing&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="give-the-workflow-token-the-least-it-can-do"&gt;Give the workflow token the least it can do
&lt;/h2&gt;&lt;p&gt;The second finding was about permissions. My workflow declared its token permissions at the top, once, for the whole file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;read&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;security-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;id-token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;write&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That reads as careful, and it&amp;rsquo;s still too broad, because top-level permissions apply to &lt;em&gt;every job in the workflow&lt;/em&gt;. A job that only needs to read the repo is now also holding &lt;code&gt;id-token: write&lt;/code&gt; and &lt;code&gt;security-events: write&lt;/code&gt;, for no reason other than that some &lt;em&gt;other&lt;/em&gt; job in the same file needed them. Scorecard rejects exactly this, and the fix is to default the whole workflow to read-only and grant write narrowly, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/commit/c41e557" target="_blank" rel="noopener"
 &gt;in the job that actually needs it&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;read-all&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Write permissions moved down into the single job that uses them. It&amp;rsquo;s the same least-privilege instinct that runs through everything else in these projects, just applied to a CI token instead of an IAM role: a credential should be able to do the one thing it&amp;rsquo;s for, and nothing else, no matter how convenient the broad grant looked.&lt;/p&gt;
&lt;h2 id="the-finding-that-was-about-me"&gt;The finding that was about me
&lt;/h2&gt;&lt;p&gt;The third one stung, because there was no YAML to fix. Scorecard&amp;rsquo;s Code-Review check scores how consistently changes are reviewed before they land, and mine scored badly for the most embarrassing possible reason: I&amp;rsquo;d set up branch protection on &lt;code&gt;main&lt;/code&gt;, and then, being the solo maintainer in a hurry, I&amp;rsquo;d been merrily bypassing it to push straight to &lt;code&gt;main&lt;/code&gt; whenever it suited me.&lt;/p&gt;
&lt;p&gt;So I had a rule, written down and enforced by the platform, that I was personally and routinely ignoring. Scorecard noticed, totted up the unreviewed commits, and graded me on it. There&amp;rsquo;s something properly humbling about a robot reading your git history and pointing out that the person breaking your security policy most often is &lt;em&gt;you&lt;/em&gt;. The fix wasn&amp;rsquo;t code. It was going through a pull request like everyone else, even when &amp;ldquo;everyone else&amp;rdquo; is just me on a different day.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;OpenSSF Scorecard is a linter for your supply chain, and like any linter it&amp;rsquo;s most useful when it tells you something you were sure you&amp;rsquo;d already handled. It dinged go-tool-base for referencing actions by mutable tag instead of pinned SHA, for granting workflow-token write permissions at the top level where every job inherited them, and for a Code-Review score I&amp;rsquo;d earned fair and square by bypassing my own branch protection.&lt;/p&gt;
&lt;p&gt;The first two were quick, satisfying changes with a clear security story. The third was the one that stuck, because the tool I&amp;rsquo;d added to grade the project ended up grading the maintainer, and was entirely right to. Turn it on. Brace yourself a little.&lt;/p&gt;</description></item><item><title>Testing code that calls an LLM: yes, you actually can</title><link>https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/</link><pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/</guid><description>&lt;img src="https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/cover-testing-code-that-calls-an-llm.png" alt="Featured image of post Testing code that calls an LLM: yes, you actually can" /&gt;&lt;p&gt;&amp;ldquo;You can&amp;rsquo;t test code that calls an AI.&amp;rdquo; I&amp;rsquo;ve heard it said with great confidence, and it&amp;rsquo;s half right, which is the most dangerous kind of right. You genuinely can&amp;rsquo;t assert on what a non-deterministic model says. But the model isn&amp;rsquo;t your code, and the bits sitting either side of it most certainly are.&lt;/p&gt;
&lt;h2 id="you-cant-test-ai-code"&gt;&amp;ldquo;You can&amp;rsquo;t test AI code&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s a fair worry. Your command calls an LLM. The LLM returns something slightly different every run. A test that asserts &lt;code&gt;response == &amp;quot;...&amp;quot;&lt;/code&gt; is broken before you&amp;rsquo;ve finished typing it. So the conclusion arrives quickly: the AI path can&amp;rsquo;t be tested, leave it uncovered.&lt;/p&gt;
&lt;p&gt;Which is a shame, because the AI call is usually the riskiest line in the whole command.&lt;/p&gt;
&lt;p&gt;The conclusion is also wrong. It mistakes &amp;ldquo;I can&amp;rsquo;t test the model&amp;rdquo; for &amp;ldquo;I can&amp;rsquo;t test my code&amp;rdquo;. The model is not your code. Your code is the two pieces sitting on either side of it.&lt;/p&gt;
&lt;h2 id="your-code-is-a-prompt-and-a-handler"&gt;Your code is a prompt and a handler
&lt;/h2&gt;&lt;p&gt;Strip the command down to what it actually does:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It builds a prompt. It assembles a system prompt, the user&amp;rsquo;s input, perhaps some context, and sends it.&lt;/li&gt;
&lt;li&gt;The model does something. This is not your code.&lt;/li&gt;
&lt;li&gt;It takes the response and does something with it. It parses it, branches on it, prints it, stores it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps one and three are entirely yours, and entirely deterministic. The same inputs build the same prompt and handle the same response the same way, every single time. That&amp;rsquo;s testable. Step two is the only part that isn&amp;rsquo;t, and step two was never yours to test in the first place.&lt;/p&gt;
&lt;p&gt;So the job is to pin step two to a known value, and then test one and three properly.&lt;/p&gt;
&lt;h2 id="test-the-prompt-snapshot-it"&gt;Test the prompt: snapshot it
&lt;/h2&gt;&lt;p&gt;Step one produces a prompt, and a prompt is just a string, which means you can pin it.&lt;/p&gt;
&lt;p&gt;Both frameworks lean on snapshot testing here. go-tool-base uses a golden-file approach: the prompt your code generates is recorded to a file, and the test re-generates it and compares against that file. rust-tool-base does the same with &lt;code&gt;insta&lt;/code&gt;, snapshotting the request body the client would send.&lt;/p&gt;
&lt;p&gt;The reason this matters is that the prompt is load-bearing and quietly easy to break. You refactor how context gets assembled. Without noticing, you&amp;rsquo;ve changed the wording, or the ordering, or dropped a line the model was leaning on. Nothing fails to compile. The behaviour just drifts, silently.&lt;/p&gt;
&lt;p&gt;A snapshot test catches exactly that. It fails, it shows you the diff between the old prompt and the new one, and it makes you stop and make a decision. Was this change intended? If yes, you accept the new snapshot and move on. If no, you&amp;rsquo;ve just caught a bug before it shipped. Either way the prompt never changes by accident, which for AI code is most of the battle.&lt;/p&gt;
&lt;h2 id="test-the-handler-mock-the-response"&gt;Test the handler: mock the response
&lt;/h2&gt;&lt;p&gt;Step three needs a response to handle, and in a unit test you don&amp;rsquo;t get that response from the real model. You supply it.&lt;/p&gt;
&lt;p&gt;go-tool-base ships &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/mocks/pkg/chat/ChatClient.go" target="_blank" rel="noopener"
 &gt;generated mocks for the &lt;code&gt;ChatClient&lt;/code&gt; interface&lt;/a&gt;. A test builds a mock client, tells it &amp;ldquo;when &lt;code&gt;Ask&lt;/code&gt; is called, return &lt;em&gt;this&lt;/em&gt; canned value&amp;rdquo;, and runs the command against it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mockClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mock_chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewMockChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mockClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EXPECT&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Anything&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Anything&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AnythingOfType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;*main.Analysis&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RunAndReturn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;critical&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;the interface is only four methods&lt;/a&gt;, that mock is trivial to set up and complete by construction. rust-tool-base takes the same idea one layer down: HTTP-bound tests use &lt;code&gt;wiremock&lt;/code&gt;, which stands up a fake server returning a canned response body. The client makes a real HTTP request; it just goes to a fake endpoint the test controls.&lt;/p&gt;
&lt;p&gt;Either way, step two is now fixed to a value you chose, which makes step three deterministic. And that unlocks the tests that actually matter: given a malformed response, does the command fail gracefully? Given a rate-limit error, an empty answer, a field missing? Those are the cases a live model almost never hands you on demand, and a mock hands you every time, on the first run.&lt;/p&gt;
&lt;p&gt;This is, incidentally, the same discipline as &lt;a class="link" href="https://blog-570662.gitlab.io/the-test-mocking-pattern-that-races/" &gt;the test-mocking work elsewhere in the framework&lt;/a&gt;: the dependency is injected, so the test gets to decide what it does.&lt;/p&gt;
&lt;h2 id="what-you-deliberately-dont-test"&gt;What you deliberately don&amp;rsquo;t test
&lt;/h2&gt;&lt;p&gt;One boundary worth stating. None of this tests whether the model gives &lt;em&gt;good&lt;/em&gt; answers. That question is real, but it&amp;rsquo;s a different activity (evaluations, run as their own suite) and not something to mix into the unit tests.&lt;/p&gt;
&lt;p&gt;The unit suite&amp;rsquo;s job is your code: that it builds a sound prompt, and that it handles every shape of response correctly, including the ugly ones. Keep that well away from &amp;ldquo;is the model clever today&amp;rdquo;. A unit test that depends on the model being clever is a unit test that fails when the weather changes, and a flaky test just teaches people to ignore the whole suite.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;Code that calls an LLM is testable; the model is not, and those are different statements. Your code is a prompt builder and a response handler, both deterministic, with the model sat in between.&lt;/p&gt;
&lt;p&gt;go-tool-base and rust-tool-base converge on the same approach. Snapshot the prompt, with golden files or &lt;code&gt;insta&lt;/code&gt;, so a refactor can&amp;rsquo;t change what you send without a test noticing. Mock the response, with generated &lt;code&gt;ChatClient&lt;/code&gt; mocks or a &lt;code&gt;wiremock&lt;/code&gt; server, so tests run with no network and you can feed in the malformed and error cases a real model won&amp;rsquo;t reliably produce. Leave &amp;ldquo;are the answers any good&amp;rdquo; to a separate evaluation suite. Test the two halves you own, and the non-determinism in the middle stops being an excuse to leave the riskiest line uncovered.&lt;/p&gt;</description></item><item><title>The AI provider that isn't an API</title><link>https://blog-570662.gitlab.io/the-ai-provider-that-isnt-an-api/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-ai-provider-that-isnt-an-api/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-ai-provider-that-isnt-an-api/cover-the-ai-provider-that-isnt-an-api.png" alt="Featured image of post The AI provider that isn't an API" /&gt;&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package puts five AI providers behind one interface. Four of them are exactly what you&amp;rsquo;d guess: HTTP calls to OpenAI, Claude, Gemini, and anything OpenAI-compatible. The fifth one isn&amp;rsquo;t an API at all. It shells out to a binary.&lt;/p&gt;
&lt;p&gt;That sounds like a slightly mad thing to want, right up until you&amp;rsquo;ve worked somewhere the network says no.&lt;/p&gt;
&lt;h2 id="the-fifth-provider-shells-out"&gt;The fifth provider shells out
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;chat&lt;/code&gt; package speaks to five providers through one &lt;code&gt;ChatClient&lt;/code&gt; interface. Four of them are what you&amp;rsquo;d expect: HTTP requests to OpenAI, to Claude, to Gemini, to any OpenAI-compatible endpoint. The tool author picks one in config, and the rest of the code never knows the difference.&lt;/p&gt;
&lt;p&gt;The fifth, &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/claude_local.go#L18" target="_blank" rel="noopener"
 &gt;&lt;code&gt;ProviderClaudeLocal&lt;/code&gt;&lt;/a&gt;, is different in kind. It doesn&amp;rsquo;t make an HTTP request at all. It shells out. It runs the &lt;code&gt;claude&lt;/code&gt; CLI binary as a child process, passes the prompt in, and reads the answer back from the binary&amp;rsquo;s output.&lt;/p&gt;
&lt;p&gt;That sounds like an odd thing to want until you&amp;rsquo;ve been stuck in the environment it was built for.&lt;/p&gt;
&lt;h2 id="why-youd-want-that"&gt;Why you&amp;rsquo;d want that
&lt;/h2&gt;&lt;p&gt;Picture a corporate network with its egress locked right down. Outbound HTTPS to &lt;code&gt;api.anthropic.com&lt;/code&gt; is blocked by policy. A tool built on go-tool-base that uses AI would simply fall over there. It tries to reach the API, there&amp;rsquo;s no route, and that&amp;rsquo;s the end of the feature.&lt;/p&gt;
&lt;p&gt;But the developer at that machine has the &lt;code&gt;claude&lt;/code&gt; CLI installed, and has run &lt;code&gt;claude login&lt;/code&gt;. That binary is permitted. It&amp;rsquo;s an approved, managed tool, and it has its own sanctioned path out. The direct API call is blocked; the &lt;code&gt;claude&lt;/code&gt; command is not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is what bridges those two facts. If your tool&amp;rsquo;s AI calls go &lt;em&gt;through&lt;/em&gt; that already-blessed binary instead of straight at the API, they work, in an environment where the direct call cannot. That&amp;rsquo;s the whole reason the provider exists. It isn&amp;rsquo;t faster (a real API call has lower latency) and it isn&amp;rsquo;t more capable. It&amp;rsquo;s for the place where the API call simply isn&amp;rsquo;t an option, and &amp;ldquo;isn&amp;rsquo;t an option&amp;rdquo; is a surprisingly common place to find yourself inside a large organisation.&lt;/p&gt;
&lt;h2 id="what-it-costs"&gt;What it costs
&lt;/h2&gt;&lt;p&gt;It&amp;rsquo;s worth being straight about the trade, because &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is the reduced-capability provider.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t do tool calling. It doesn&amp;rsquo;t do parallel tools. It doesn&amp;rsquo;t stream. Those need a live, structured connection to the model&amp;rsquo;s API, and a subprocess that runs once and prints an answer is not that. What it &lt;em&gt;does&lt;/em&gt; support is plain chat and structured output, the latter through the binary&amp;rsquo;s own &lt;code&gt;--json-schema&lt;/code&gt; flag.&lt;/p&gt;
&lt;p&gt;So the positioning, and the package&amp;rsquo;s documentation says exactly this, is: prefer the API providers when you can reach them, because they&amp;rsquo;re lower latency and feature-complete. Reach for &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; when API access is restricted. You accept the narrower capability set as the price of working at all. For a tool whose AI feature is &amp;ldquo;answer a question&amp;rdquo; or &amp;ldquo;return a structured analysis&amp;rdquo;, that price is often nothing you&amp;rsquo;d even notice. For one built on an agentic tool-calling loop, it&amp;rsquo;s a real limitation, and you&amp;rsquo;d know to expect it.&lt;/p&gt;
&lt;h2 id="how-it-stays-behind-the-same-interface"&gt;How it stays behind the same interface
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the part that makes it pleasant rather than a special case to maintain. Despite being a subprocess and not an API, &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is still a &lt;code&gt;ChatClient&lt;/code&gt;. Your feature code calls &lt;code&gt;Chat&lt;/code&gt; and &lt;code&gt;Ask&lt;/code&gt; exactly the way it would for any other provider.&lt;/p&gt;
&lt;p&gt;Everything that makes a subprocess provider awkward stays inside the provider. Spawning the binary, feeding it the prompt, parsing its output, capturing &lt;code&gt;stderr&lt;/code&gt; and surfacing it when the binary exits non-zero, and threading multi-turn continuity through session identifiers passed back on the next call with &lt;code&gt;--resume&lt;/code&gt;: all of that is the provider&amp;rsquo;s problem, and all of it sits behind the interface. The code in your tool that uses AI doesn&amp;rsquo;t know, and has no way to find out, that this particular provider is a child process rather than an HTTPS call.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a unified interface genuinely earning its place. It&amp;rsquo;s easy to put a uniform face on four things that already work the same way underneath. The real test of the abstraction is whether something that works in a &lt;em&gt;completely&lt;/em&gt; different way, a subprocess instead of a socket, can still slot in without the caller changing a line. Here it can. You swap one config value, and a tool that talked to an API now talks through a binary, and nothing downstream so much as blinks.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package puts five providers behind one &lt;code&gt;ChatClient&lt;/code&gt; interface, and &lt;code&gt;ProviderClaudeLocal&lt;/code&gt; is the one that isn&amp;rsquo;t an API. It runs the locally installed, pre-authenticated &lt;code&gt;claude&lt;/code&gt; CLI as a subprocess.&lt;/p&gt;
&lt;p&gt;It exists for the locked-down environment where outbound HTTPS to the AI API is blocked but the &lt;code&gt;claude&lt;/code&gt; binary is allowed: there, AI features keep working where a direct call would fail. The trade is a narrower capability set (no tool calling, no streaming, plain chat and structured output only) so you prefer the API providers when you can reach them and fall back to this when you can&amp;rsquo;t. And because it&amp;rsquo;s still a &lt;code&gt;ChatClient&lt;/code&gt;, all the subprocess machinery stays hidden, and your code uses it without knowing it&amp;rsquo;s there. That last part is the real test of an abstraction: a provider that works in an entirely different way still slots in unchanged.&lt;/p&gt;</description></item><item><title>AI conversations you can resume</title><link>https://blog-570662.gitlab.io/ai-conversations-you-can-resume/</link><pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/ai-conversations-you-can-resume/</guid><description>&lt;img src="https://blog-570662.gitlab.io/ai-conversations-you-can-resume/cover-ai-conversations-you-can-resume.png" alt="Featured image of post AI conversations you can resume" /&gt;&lt;p&gt;An AI conversation is, fundamentally, its own history. The model&amp;rsquo;s next answer depends on everything said so far. And a CLI tool, by its very nature, forgets everything the moment it exits. Put those two facts together and you get the problem: run an AI command, exit, run it again, and you&amp;rsquo;re talking to someone who&amp;rsquo;s never met you.&lt;/p&gt;
&lt;h2 id="a-cli-forgets-everything"&gt;A CLI forgets everything
&lt;/h2&gt;&lt;p&gt;A long-running service keeps its state in memory for as long as it runs. A CLI tool doesn&amp;rsquo;t get that luxury. It starts, does one thing, exits. The next invocation is a brand-new process with no memory of the last one.&lt;/p&gt;
&lt;p&gt;For most commands that&amp;rsquo;s exactly right, and you wouldn&amp;rsquo;t want it any other way. But an AI conversation is a different kind of beast, because a conversation &lt;em&gt;is&lt;/em&gt; its history. The model&amp;rsquo;s next answer depends on everything said so far. Run an AI command, exit, run it again, and you&amp;rsquo;ve started a fresh conversation with someone who&amp;rsquo;s never met you. For an interactive assistant, or any AI workflow that unfolds across several invocations, that&amp;rsquo;s plainly the wrong behaviour. The user expects to pick up where they left off.&lt;/p&gt;
&lt;h2 id="save-and-restore"&gt;Save and restore
&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;chat&lt;/code&gt; package handles this through a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/persistence.go#L25" target="_blank" rel="noopener"
 &gt;&lt;code&gt;PersistentChatClient&lt;/code&gt;&lt;/a&gt; interface. Like streaming, it&amp;rsquo;s an optional capability discovered with a type assertion, sitting beside &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;the four-method core&lt;/a&gt; rather than bloating it. A client that supports persistence also satisfies this interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PersistentChatClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// store the snapshot somewhere&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A snapshot is a serialisable value that captures the conversation. You store it. Next run, you load it, &lt;code&gt;Restore&lt;/code&gt; it onto a fresh client, re-register your tools, and call &lt;code&gt;Chat&lt;/code&gt; again. &amp;ldquo;Where were we?&amp;rdquo; works, because the model is handed back the whole history.&lt;/p&gt;
&lt;h2 id="a-snapshot-is-opinionated-about-what-it-carries"&gt;A snapshot is opinionated about what it carries
&lt;/h2&gt;&lt;p&gt;The interesting part is what a snapshot does and doesn&amp;rsquo;t contain, because that&amp;rsquo;s a series of deliberate decisions.&lt;/p&gt;
&lt;p&gt;It carries the messages, the system prompt, the model name, and tool &lt;em&gt;metadata&lt;/em&gt;: the names, descriptions and parameter schemas of the tools that were registered.&lt;/p&gt;
&lt;p&gt;It does not carry tool &lt;em&gt;handlers&lt;/em&gt;. Handlers are code, not data; you can&amp;rsquo;t serialise a function meaningfully, so after a restore you re-register them with &lt;code&gt;SetTools&lt;/code&gt;. The snapshot remembers that a tool called &lt;code&gt;read_file&lt;/code&gt; existed and what its shape was; it doesn&amp;rsquo;t try to remember the Go function behind it.&lt;/p&gt;
&lt;p&gt;And it does not carry API tokens. This is the one to dwell on. A snapshot is a file. A file gets synced, backed up, copied between machines, attached to a support ticket by a user trying to be helpful. A snapshot that carried the API key would be a credential leak the moment it left the laptop it was made on. So the snapshot never contains a token, at all. On restore, the client picks the credential up again the ordinary way, from &lt;a class="link" href="https://blog-570662.gitlab.io/where-should-a-cli-keep-your-api-keys/" &gt;the environment or the keychain&lt;/a&gt;. The conversation and the secret are kept in separate places on purpose, and only one of them is ever in the file.&lt;/p&gt;
&lt;h2 id="encrypted-at-rest-if-you-want-it"&gt;Encrypted at rest, if you want it
&lt;/h2&gt;&lt;p&gt;The package ships a &lt;code&gt;FileStore&lt;/code&gt; that writes snapshots as JSON files, with &lt;code&gt;0600&lt;/code&gt; permissions in a &lt;code&gt;0700&lt;/code&gt; directory, and it can encrypt them. Pass &lt;code&gt;WithEncryption&lt;/code&gt; a 32-byte key and snapshots are written with AES-256-GCM.&lt;/p&gt;
&lt;p&gt;That option exists because a conversation can hold sensitive content even when it holds no credential. The log a user pasted in for analysis, the source file they asked the model to review, the internal details tucked into their questions: none of that is an API key, and all of it might be something you&amp;rsquo;d rather not have sitting in plain JSON in a backup somewhere. Encryption at rest covers it.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;FileStore&lt;/code&gt; is also careful about the snapshot identifiers it&amp;rsquo;s handed. An ID has to be a canonical UUID, and the resolved file path is checked to lie inside the store directory, so a snapshot ID arriving from an untrusted source (a CLI flag, a request payload) can&amp;rsquo;t be bent into a path-traversal that reads or writes somewhere it shouldn&amp;rsquo;t. Persisting conversations adds a small filesystem surface, and the store treats it as exactly that.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;A CLI tool forgets everything between invocations, which is correct for most commands and wrong for an AI conversation, because a conversation is its history.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package lets you persist one. &lt;code&gt;PersistentChatClient&lt;/code&gt; saves a snapshot you can store and restore later, picking the conversation back up where it ended. The snapshot is deliberate about its contents: messages, system prompt and tool metadata yes; tool handlers no, because they&amp;rsquo;re code you re-register; API tokens never, because a snapshot is a file and a file travels. The built-in &lt;code&gt;FileStore&lt;/code&gt; can encrypt snapshots at rest with AES-256-GCM and validates snapshot IDs against path traversal. Resumable conversations, without the conversation file turning into a place secrets leak from.&lt;/p&gt;</description></item><item><title>An AI agent that has to make the build pass</title><link>https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/</link><pubDate>Thu, 02 Apr 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/</guid><description>&lt;img src="https://blog-570662.gitlab.io/an-ai-agent-that-has-to-make-the-build-pass/cover-an-ai-agent-that-has-to-make-the-build-pass.png" alt="Featured image of post An AI agent that has to make the build pass" /&gt;&lt;p&gt;Most AI code generation works on a charming little principle I&amp;rsquo;ll call generate-and-hope. The model writes the code, the model stops at the closing brace, and whether the thing actually compiles is left as an exercise for you. For a snippet you paste into an editor, fine. For a whole generated command, that&amp;rsquo;s just outsourcing the disappointment.&lt;/p&gt;
&lt;p&gt;go-tool-base does something I&amp;rsquo;m rather happier with: the AI has to make the build pass before it&amp;rsquo;s allowed to claim it&amp;rsquo;s done.&lt;/p&gt;
&lt;h2 id="generate-and-hope"&gt;Generate and hope
&lt;/h2&gt;&lt;p&gt;The usual shape of AI code generation is this. You ask for code, the model produces it, and the model&amp;rsquo;s job ends at the closing brace. Whether it compiles, whether the tests pass, whether the imports even resolve, none of that has been checked. The model produced something that &lt;em&gt;looks&lt;/em&gt; right. You find out whether it &lt;em&gt;is&lt;/em&gt; right when you build it.&lt;/p&gt;
&lt;p&gt;For a snippet you paste into an editor, that&amp;rsquo;s perfectly fine. The compiler tells you in a second. But go-tool-base&amp;rsquo;s generator, driven by &lt;code&gt;gtb generate command --script&lt;/code&gt; or &lt;code&gt;--prompt&lt;/code&gt;, produces a whole command: the implementation, its tests, the lot. &amp;ldquo;Generate and hope&amp;rdquo; at that scale means handing the user a project that may or may not build, and quietly making them the one who finds out which.&lt;/p&gt;
&lt;h2 id="drafting-is-only-step-one"&gt;Drafting is only step one
&lt;/h2&gt;&lt;p&gt;So the generator doesn&amp;rsquo;t stop at drafting. Writing the first version of the implementation and its tests is step one of two. Step two is an autonomous repair agent.&lt;/p&gt;
&lt;p&gt;Once the draft is on the filesystem, a separate agent takes over. It&amp;rsquo;s an LLM running in a loop, but a loop aimed at one narrow, checkable job: make this project build and pass its tests. It isn&amp;rsquo;t asked to be creative. It&amp;rsquo;s asked to get to green.&lt;/p&gt;
&lt;h2 id="a-fixed-set-of-tools-and-no-shell"&gt;A fixed set of tools, and no shell
&lt;/h2&gt;&lt;p&gt;The agent is not handed a shell. It&amp;rsquo;s given a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/internal/agent/tools.go" target="_blank" rel="noopener"
 &gt;fixed, defined set of tools&lt;/a&gt; and nothing else. Three of them let it explore and edit the project: &lt;code&gt;list_dir&lt;/code&gt;, &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt;. Four of them let it verify the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;go_build&lt;/code&gt; runs the build and captures the compiler errors.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go_test&lt;/code&gt; runs the tests and captures the failures.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go_get&lt;/code&gt; resolves a missing dependency.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;golangci_lint&lt;/code&gt; runs the project&amp;rsquo;s linter.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That restriction is the design, not a limitation of it. The agent can&amp;rsquo;t delete arbitrary files, can&amp;rsquo;t reach the network, can&amp;rsquo;t run anything that isn&amp;rsquo;t on the list. It has exactly what it needs to make code compile and nothing it would need to do damage. Its file writes are confined to the project directory by an explicit path check, so even &lt;code&gt;write_file&lt;/code&gt; can&amp;rsquo;t go wandering up into &lt;code&gt;/etc&lt;/code&gt;. A coding agent you&amp;rsquo;d actually let near a filesystem is one whose abilities are an allowlist, not a denylist. (I keep coming back to that principle through this series&amp;hellip; safety as a boundary you draw, not a behaviour you hope for.)&lt;/p&gt;
&lt;h2 id="the-loop"&gt;The loop
&lt;/h2&gt;&lt;p&gt;The repair loop is a ReAct loop, the same reason-act-observe shape as &lt;a class="link" href="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/" &gt;the tool-calling loop&lt;/a&gt;, only this time pointed at a goal:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The draft is on disk.&lt;/li&gt;
&lt;li&gt;Verify: run &lt;code&gt;go_build&lt;/code&gt; and &lt;code&gt;go_test&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If verification failed, read the error logs, the compiler error or the failing test.&lt;/li&gt;
&lt;li&gt;Reason about the cause: an undefined variable, a missing import, a wrong signature.&lt;/li&gt;
&lt;li&gt;Act: call &lt;code&gt;write_file&lt;/code&gt; to patch the code, or &lt;code&gt;go_get&lt;/code&gt; to add the dependency.&lt;/li&gt;
&lt;li&gt;Loop. Steps two to five repeat until the project is green, or the agent hits its bounded step limit.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What makes this work is treating the error output as &lt;em&gt;feedback&lt;/em&gt; rather than as a failure to log and walk away from. A compiler error is the single most useful sentence you can hand a model that&amp;rsquo;s trying to fix code. It says what&amp;rsquo;s wrong, and usually where. The loop feeds it straight back in, and the model fixes against it.&lt;/p&gt;
&lt;h2 id="verification-changes-what-done-means"&gt;Verification changes what &amp;ldquo;done&amp;rdquo; means
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the real shift, and the agent&amp;rsquo;s own documentation puts it well: the agent &amp;ldquo;doesn&amp;rsquo;t just say it fixed a bug; it uses a Test tool to verify the fix before reporting success.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;A generate-and-hope model reports success when it finishes &lt;em&gt;writing&lt;/em&gt;. It has no idea whether the code works, and it isn&amp;rsquo;t really claiming otherwise. &amp;ldquo;Done&amp;rdquo; means &amp;ldquo;I produced text&amp;rdquo;. The repair agent reports success when &lt;code&gt;go_build&lt;/code&gt; and &lt;code&gt;go_test&lt;/code&gt; actually &lt;em&gt;pass&lt;/em&gt;. &amp;ldquo;Done&amp;rdquo; means &amp;ldquo;the build is green&amp;rdquo;. Those are two completely different claims, and only the second is worth anything to the person who asked for the command.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the line between an AI that&amp;rsquo;s a creative writer and an AI that&amp;rsquo;s a collaborator you can hand a task to. And when the agent can&amp;rsquo;t reach green, when it spends its whole step budget and the project is still broken, the generator fails safely: it leaves the best-attempt code in place, commented out so the project still compiles, and tells the user what to finish by hand. There&amp;rsquo;s also an &lt;code&gt;--agentless&lt;/code&gt; flag for anyone who&amp;rsquo;d rather have a plain single-shot retry than the multi-step agent. The default, though, is the agent, because the default should be code that&amp;rsquo;s been checked.&lt;/p&gt;
&lt;h2 id="where-this-leaves-us"&gt;Where this leaves us
&lt;/h2&gt;&lt;p&gt;Most AI code generation generates and hopes: the model writes code and the user discovers whether it works. For a whole generated command, that pushes a may-or-may-not-build project onto the user.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s generator drafts the command and then hands it to an autonomous repair agent. The agent has a fixed set of tools (explore and edit the project, build it, test it, lint it, fetch dependencies) and no shell at all, with file writes confined to the project directory. It runs a ReAct loop, reading each error and patching against it, until the build is green or it exhausts its steps. The point is what &amp;ldquo;done&amp;rdquo; comes to mean: not &amp;ldquo;the model finished writing&amp;rdquo;, but &amp;ldquo;the build passes&amp;rdquo;. Only one of those is a claim worth trusting.&lt;/p&gt;</description></item><item><title>Stop regex-ing the LLM's prose</title><link>https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/</link><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/</guid><description>&lt;img src="https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/cover-stop-regexing-the-llms-prose.png" alt="Featured image of post Stop regex-ing the LLM's prose" /&gt;&lt;p&gt;Ask an LLM a question and it hands you back prose. Lovely to read, miserable to program against. You wanted the one number buried in the middle of it, and now you&amp;rsquo;re writing a regular expression to fish a word out of three well-written paragraphs that phrase themselves slightly differently every single time you run them.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a much better way, and it&amp;rsquo;s the difference between forever interpreting an LLM and actually building on one.&lt;/p&gt;
&lt;h2 id="the-problem-with-a-paragraph"&gt;The problem with a paragraph
&lt;/h2&gt;&lt;p&gt;You ask an LLM to analyse a log file and tell you the severity of what it found and a suggested fix. It comes back with three well-written paragraphs. Somewhere in there is the word &amp;ldquo;critical&amp;rdquo;, and somewhere is the fix.&lt;/p&gt;
&lt;p&gt;Your program now has to &lt;em&gt;extract&lt;/em&gt; those two facts from prose, and prose has no contract. The next run, the model phrases it differently. It leads with a caveat. It says &amp;ldquo;severe&amp;rdquo; where last time it said &amp;ldquo;critical&amp;rdquo;. It puts the fix first. Anything that worked by finding &amp;ldquo;critical&amp;rdquo; in the text is now quietly wrong, and you didn&amp;rsquo;t change a line. Parsing free text for structured facts is a game you lose slowly.&lt;/p&gt;
&lt;p&gt;What you actually wanted was never a paragraph. It was a value: a thing with a &lt;code&gt;severity&lt;/code&gt; field and a &lt;code&gt;fix&lt;/code&gt; field, that you can branch on and store and pass around like any other.&lt;/p&gt;
&lt;h2 id="ask-for-the-struct-not-the-prose"&gt;Ask for the struct, not the prose
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package draws the line with two methods. &lt;code&gt;Chat&lt;/code&gt; gives you text. &lt;code&gt;Ask&lt;/code&gt; gives you a struct.&lt;/p&gt;
&lt;p&gt;You define the Go type you want back:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Severity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;severity&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Fix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;fix&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Analysis&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Analyse this log file: &amp;#34;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;logText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The framework generates a JSON Schema from that struct, sends it to the model as the required response format, and unmarshals the reply straight into &lt;code&gt;result&lt;/code&gt;. You never lay a finger on the prose. You get &lt;code&gt;result.Severity&lt;/code&gt; and &lt;code&gt;result.Fix&lt;/code&gt;, typed, ready to use. If you want the model&amp;rsquo;s answer to drive a &lt;code&gt;switch&lt;/code&gt; statement, this is the method that lets it.&lt;/p&gt;
&lt;h2 id="the-struct-is-the-schema-is-the-contract"&gt;The struct is the schema is the contract
&lt;/h2&gt;&lt;p&gt;The detail that makes this hold up over time: you don&amp;rsquo;t write the schema. The struct &lt;em&gt;is&lt;/em&gt; the schema.&lt;/p&gt;
&lt;p&gt;The framework derives the JSON Schema from your type. In go-tool-base that&amp;rsquo;s &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/schema.go#L11" target="_blank" rel="noopener"
 &gt;&lt;code&gt;GenerateSchema[T]()&lt;/code&gt;&lt;/a&gt;; in rust-tool-base the schema comes from your Rust type through &lt;a class="link" href="https://gitlab.com/phpboyscout/rust-tool-base/-/blob/9c22aa8/crates/rtb-ai/src/client.rs#L208" target="_blank" rel="noopener"
 &gt;&lt;code&gt;schemars&lt;/code&gt;&lt;/a&gt;. (Yes, there&amp;rsquo;s a Rust sibling now. I&amp;rsquo;ll &lt;a class="link" href="https://blog-570662.gitlab.io/rust-tool-base-the-same-idea/" &gt;introduce it properly&lt;/a&gt; in a few weeks, but it keeps gatecrashing these posts because the two frameworks deliberately share ideas.) Either way there&amp;rsquo;s one definition, your type, and the schema is just a projection of it.&lt;/p&gt;
&lt;p&gt;That matters, because otherwise two things have to agree. There&amp;rsquo;s the schema you tell the model to obey, and there&amp;rsquo;s the type you unmarshal the answer into. Hand-write the schema and those two can drift: add a field to the struct, forget to add it to the schema, and the model is never told to produce it, so it silently never appears. Deriving the schema from the type collapses the two into one. They can&amp;rsquo;t disagree, because there&amp;rsquo;s only one of them.&lt;/p&gt;
&lt;h2 id="both-frameworks-with-one-extra-step-in-rust"&gt;Both frameworks, with one extra step in Rust
&lt;/h2&gt;&lt;p&gt;go-tool-base does this with &lt;code&gt;Ask&lt;/code&gt; and a &lt;code&gt;ResponseSchema&lt;/code&gt; set on the client config. rust-tool-base does it with &lt;code&gt;chat_structured::&amp;lt;T&amp;gt;&lt;/code&gt;, where &lt;code&gt;T&lt;/code&gt; is any type that&amp;rsquo;s both deserialisable and &lt;code&gt;JsonSchema&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;rust-tool-base adds one step worth calling out. Before it deserialises the model&amp;rsquo;s reply into your &lt;code&gt;T&lt;/code&gt;, it &lt;em&gt;validates&lt;/em&gt; the raw response against the schema with a JSON Schema validator. That splits the failure into two distinct, named cases: the response didn&amp;rsquo;t match the schema, or it matched the schema but still wouldn&amp;rsquo;t deserialise. A model that returns subtly wrong JSON fails loudly and specifically, with an error that tells you which of those happened, instead of quietly handing you a zero-valued struct that you end up debugging an hour later.&lt;/p&gt;
&lt;h2 id="when-youd-reach-for-it"&gt;When you&amp;rsquo;d reach for it
&lt;/h2&gt;&lt;p&gt;The line is simple, and it&amp;rsquo;s about who reads the answer.&lt;/p&gt;
&lt;p&gt;If a &lt;em&gt;human&lt;/em&gt; reads the answer, prose is right. &lt;code&gt;Chat&lt;/code&gt;, free text, let the model write well. A summary, an explanation, an interactive reply: leave all of those as prose.&lt;/p&gt;
&lt;p&gt;If a &lt;em&gt;program&lt;/em&gt; consumes the answer, you want a value. Classification, extraction, a code review scored out of a hundred with a list of issues, a yes-or-no with reasons: anything where the next thing that happens is your code branching on the result. There, &lt;code&gt;Ask&lt;/code&gt; and &lt;code&gt;chat_structured&lt;/code&gt; turn the LLM from something you have to interpret into something that returns a value, and a typed value is a thing you can actually build on.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;An LLM returns prose by default, and prose has no contract, so a program that picks structured facts out of it breaks the moment the model rephrases.&lt;/p&gt;
&lt;p&gt;Structured output asks for the value instead. You define a struct, the framework derives a JSON Schema from it, the model is constrained to that shape, and you get a typed result. go-tool-base&amp;rsquo;s &lt;code&gt;Ask&lt;/code&gt; and rust-tool-base&amp;rsquo;s &lt;code&gt;chat_structured&lt;/code&gt; both work this way, with the schema derived from your type so the schema and the type can&amp;rsquo;t drift; rust-tool-base additionally validates the response against the schema before deserialising. Use it whenever the answer feeds code rather than a human. It&amp;rsquo;s one of the four methods that make up &lt;a class="link" href="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/" &gt;go-tool-base&amp;rsquo;s small chat interface&lt;/a&gt;, and it&amp;rsquo;s the one that makes an LLM safe to program against.&lt;/p&gt;</description></item><item><title>Telemetry that asks first</title><link>https://blog-570662.gitlab.io/telemetry-that-asks-first/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/telemetry-that-asks-first/</guid><description>&lt;img src="https://blog-570662.gitlab.io/telemetry-that-asks-first/cover-telemetry-that-asks-first.png" alt="Featured image of post Telemetry that asks first" /&gt;&lt;p&gt;Usage telemetry is genuinely useful. Knowing which commands people actually run, where the errors cluster, whether anyone ever touched the feature you spent a fortnight on&amp;hellip; that&amp;rsquo;s the stuff that makes you a better maintainer. Wanting it is completely legitimate.&lt;/p&gt;
&lt;p&gt;The trouble is that the &lt;em&gt;usual&lt;/em&gt; way of getting it, on by default and quietly hoovering up everything, is a small betrayal of the people who installed your tool to get a job done. I wasn&amp;rsquo;t willing to build that, so go-tool-base&amp;rsquo;s telemetry starts from a different question.&lt;/p&gt;
&lt;h2 id="the-data-you-want-and-the-line-you-shouldnt-cross"&gt;The data you want, and the line you shouldn&amp;rsquo;t cross
&lt;/h2&gt;&lt;p&gt;If you maintain a tool, you want to know how it&amp;rsquo;s actually used. Which commands matter and which are dead weight. Where the error rate spikes. Whether anyone touched the feature you spent that fortnight on. That information makes you a better maintainer, and, to say it again, wanting it is completely legitimate.&lt;/p&gt;
&lt;p&gt;The trouble is the standard way of getting it. Telemetry on by default. An opt-out buried three levels down in a settings file nobody reads. And once it&amp;rsquo;s running, it quietly collects far more than it ever admitted to: the arguments people passed, the paths they were working in, an IP address for good measure.&lt;/p&gt;
&lt;p&gt;Every one of those is a small betrayal of someone who installed your tool to get a job done, not to become a data point. And the cost when users notice isn&amp;rsquo;t a slap on the wrist. It&amp;rsquo;s trust, and trust in a developer tool does not grow back quickly. A tool that surprises you once with what it was quietly collecting is a tool you uninstall and warn your colleagues about.&lt;/p&gt;
&lt;p&gt;So go-tool-base&amp;rsquo;s telemetry started from a different question. Not &amp;ldquo;how do we collect the most data&amp;rdquo; but &amp;ldquo;how do we collect &lt;em&gt;useful&lt;/em&gt; data without ever putting the user in a position they didn&amp;rsquo;t choose&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="rule-one-it-is-off-until-you-say-otherwise"&gt;Rule one: it is off until you say otherwise
&lt;/h2&gt;&lt;p&gt;The foundation is the simplest possible rule, and it&amp;rsquo;s absolute. Telemetry is &lt;strong&gt;never enabled by default.&lt;/strong&gt; A freshly installed tool built on go-tool-base sends nothing. Not a heartbeat, not a ping, nothing at all.&lt;/p&gt;
&lt;p&gt;It only starts collecting when the user makes an explicit, visible choice to let it. Three honest doors: they run &lt;code&gt;telemetry enable&lt;/code&gt;, they say yes to a clear prompt during &lt;code&gt;init&lt;/code&gt;, or they set &lt;code&gt;TELEMETRY_ENABLED&lt;/code&gt; themselves. All three are deliberate acts. None of them is a pre-ticked box or a default they have to discover and then undo.&lt;/p&gt;
&lt;p&gt;This is opt-&lt;em&gt;in&lt;/em&gt;, and the distinction from a well-hidden opt-&lt;em&gt;out&lt;/em&gt; is the entire point. Opt-out telemetry treats consent as something to be assumed and grudgingly reversed. Opt-in treats it as something that has to be &lt;em&gt;given&lt;/em&gt;. Only one of those is actually consent.&lt;/p&gt;
&lt;h2 id="rule-two-no-personally-identifiable-information-full-stop"&gt;Rule two: no personally identifiable information, full stop
&lt;/h2&gt;&lt;p&gt;Consent to &amp;ldquo;some telemetry&amp;rdquo; is not consent to &amp;ldquo;any telemetry&amp;rdquo;, so the second rule constrains what can ever be collected, even from a user who&amp;rsquo;s opted in.&lt;/p&gt;
&lt;p&gt;No personally identifiable information. The framework does not record command arguments (they routinely contain paths, hostnames, the occasional secret someone&amp;rsquo;s pasted in). It does not record file contents. It does not record IP addresses.&lt;/p&gt;
&lt;p&gt;It does need &lt;em&gt;some&lt;/em&gt; notion of &amp;ldquo;distinct installations&amp;rdquo; for the numbers to mean anything, so it derives a machine ID from a handful of system signals and runs it through &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/telemetry/machine.go#L12" target="_blank" rel="noopener"
 &gt;SHA-256&lt;/a&gt;. What leaves the machine is a hash. It tells you &amp;ldquo;this is the same install as last week&amp;rdquo; and tells you precisely nothing about whose install it is, and the hash can&amp;rsquo;t be walked backwards into the signals it came from.&lt;/p&gt;
&lt;p&gt;The events themselves are deliberately thin. Which command ran, roughly how long it took, whether it errored. The shape of usage, not a transcript of it.&lt;/p&gt;
&lt;h2 id="rule-three-the-author-picks-the-destination"&gt;Rule three: the author picks the destination
&lt;/h2&gt;&lt;p&gt;Even with consent given and PII excluded, there&amp;rsquo;s a third question: where does the data actually &lt;em&gt;go&lt;/em&gt;? go-tool-base doesn&amp;rsquo;t answer that for you, because it can&amp;rsquo;t. A corporate internal tool, an open-source CLI and an air-gapped utility have completely different right answers.&lt;/p&gt;
&lt;p&gt;So the backend is the tool author&amp;rsquo;s choice. The framework ships several (a noop backend, stdout, a file, plain HTTP, and OpenTelemetry over OTLP) and supports custom ones. The noop backend matters more than it looks: it lets a tool wire up the whole telemetry surface, commands and all, while sending data precisely nowhere. A perfectly reasonable, fully supported configuration.&lt;/p&gt;
&lt;p&gt;Pluggable backends also mean the data never has to touch any infrastructure I run. It goes where the tool&amp;rsquo;s author decides, on their terms. The framework provides the plumbing and stays well out of the destination.&lt;/p&gt;
&lt;h2 id="and-a-way-back-out"&gt;And a way back out
&lt;/h2&gt;&lt;p&gt;One last thing, because it&amp;rsquo;s the part that makes the opt-in real rather than decorative. A user who opted in can opt straight back out, and the package includes a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/telemetry/deletion.go#L24" target="_blank" rel="noopener"
 &gt;GDPR-aligned deletion path&lt;/a&gt;, so &amp;ldquo;stop, and remove what you have&amp;rdquo; is an actual supported request rather than a polite fiction.&lt;/p&gt;
&lt;p&gt;Consent you can&amp;rsquo;t withdraw isn&amp;rsquo;t consent. It&amp;rsquo;s a one-way door with a friendly sign on it. The deletion path is what keeps the front door an actual door.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;Telemetry is genuinely useful to a maintainer and genuinely dangerous to the trust of the people running the tool, and the usual implementation (on by default, opt-out buried, collecting everything) spends that trust recklessly. go-tool-base&amp;rsquo;s telemetry holds three lines: never enabled without an explicit user action, never collecting personally identifiable information even once enabled, and always sending data to a destination the tool&amp;rsquo;s author chose, up to and including nowhere. A real deletion path makes the opt-in something you can take back.&lt;/p&gt;
&lt;p&gt;You can have your usage numbers. You just have to ask for them, the way you would for anything else that wasn&amp;rsquo;t yours to begin with.&lt;/p&gt;</description></item><item><title>Letting the AI call your Go functions</title><link>https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/</guid><description>&lt;img src="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/cover-letting-the-ai-call-your-go-functions.png" alt="Featured image of post Letting the AI call your Go functions" /&gt;&lt;p&gt;An AI that can only produce text can &lt;em&gt;describe&lt;/em&gt; your system. An AI that can call your Go functions can actually operate it. That gap, between describing and doing, is the difference between a chatbot and something genuinely useful, and crossing it comes down to one fiddly mechanism: tool-calling, and the loop that drives it.&lt;/p&gt;
&lt;h2 id="talking-about-the-system-versus-operating-it"&gt;Talking about the system versus operating it
&lt;/h2&gt;&lt;p&gt;Wire an AI provider into a CLI command and you get something that can talk. Ask it a question, get a paragraph back. Useful, up to a point.&lt;/p&gt;
&lt;p&gt;But notice the ceiling. An AI that can only generate text can &lt;em&gt;describe&lt;/em&gt; things. It can tell you what it would do. What it can&amp;rsquo;t do is look at the actual current state of your system, or take a real action, because it has no hands. It&amp;rsquo;s reasoning in a vacuum about a world it can&amp;rsquo;t reach out and touch.&lt;/p&gt;
&lt;p&gt;The thing that gives it hands is tool-calling. You hand the AI a set of functions it&amp;rsquo;s allowed to call. Now, mid-conversation, it can decide it needs to &lt;em&gt;read that file&lt;/em&gt; before it can answer, or &lt;em&gt;run that query&lt;/em&gt;, or &lt;em&gt;check that status&lt;/em&gt;, and actually go and do it, and then reason about the real result. The AI stops describing your system and starts operating it.&lt;/p&gt;
&lt;h2 id="the-loop-is-the-hard-part"&gt;The loop is the hard part
&lt;/h2&gt;&lt;p&gt;Tool-calling has a shape, and the shape is a loop. The literature calls it ReAct: Reason, Act, Observe.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The AI &lt;strong&gt;reasons&lt;/strong&gt; about the prompt and decides whether it needs a tool.&lt;/li&gt;
&lt;li&gt;If it does, it &lt;strong&gt;acts&lt;/strong&gt;, asking for a specific tool with specific arguments.&lt;/li&gt;
&lt;li&gt;Your code runs the tool and feeds the result back. The AI &lt;strong&gt;observes&lt;/strong&gt; that result.&lt;/li&gt;
&lt;li&gt;Round again. Reason about the new information, maybe call another tool, maybe several. Keep going until the AI has what it needs and produces a final text answer with no more tool calls.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Conceptually simple. Tedious and error-prone to implement by hand every single time: parsing the model&amp;rsquo;s tool-call requests, dispatching to the right function, marshalling arguments in and results out, feeding observations back in the exact format the provider expects, knowing when to stop, and not looping forever if the model gets itself stuck.&lt;/p&gt;
&lt;p&gt;That orchestration is pure plumbing, and it&amp;rsquo;s identical for every tool and every command. So you can probably guess what&amp;rsquo;s coming: go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package owns it. You don&amp;rsquo;t write the loop. You write the tools.&lt;/p&gt;
&lt;h2 id="defining-a-tool"&gt;Defining a tool
&lt;/h2&gt;&lt;p&gt;A &lt;code&gt;chat.Tool&lt;/code&gt; is four things: a name, a description, a parameter schema, and a handler. The description is what the AI reads to decide &lt;em&gt;whether&lt;/em&gt; to use the tool, so it&amp;rsquo;s worth writing well. The schema describes the arguments, and you don&amp;rsquo;t hand-write it. You write a tagged Go struct and let it generate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ReadFileParams&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`json:&amp;#34;path&amp;#34; jsonschema_description:&amp;#34;Relative path to the file&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The struct is the contract. The framework derives the JSON Schema the AI is given straight from those tags, so the schema and the Go type the handler receives can&amp;rsquo;t drift apart, because they share a single source. The handler is then just an ordinary Go function that takes those parameters and returns a result.&lt;/p&gt;
&lt;p&gt;You register your tools with &lt;code&gt;SetTools&lt;/code&gt;, call &lt;code&gt;Chat&lt;/code&gt;, and that&amp;rsquo;s the whole of your involvement. The framework runs the ReAct loop and &lt;code&gt;Chat&lt;/code&gt; returns the AI&amp;rsquo;s final text answer once the loop settles.&lt;/p&gt;
&lt;h2 id="two-details-that-show-it-was-built-for-real-use"&gt;Two details that show it was built for real use
&lt;/h2&gt;&lt;p&gt;A couple of decisions in the loop tell you it&amp;rsquo;s meant for production, not a demo.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tool errors don&amp;rsquo;t abort the conversation.&lt;/strong&gt; When a handler returns an error, the framework doesn&amp;rsquo;t crash the loop. It hands the error &lt;em&gt;back to the AI as a string&lt;/em&gt;, as just another observation. That&amp;rsquo;s deliberate, and it&amp;rsquo;s right. A real agent should be able to call a tool, watch it fail, and react: try different arguments, take a different route, or tell the user it couldn&amp;rsquo;t manage it. A loop that aborted on the first tool error would be far more brittle than the model driving it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The loop is bounded.&lt;/strong&gt; There&amp;rsquo;s a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/constants.go#L16" target="_blank" rel="noopener"
 &gt;&lt;code&gt;MaxSteps&lt;/code&gt; limit, default 20&lt;/a&gt;. An AI that gets confused could otherwise call tools forever, and a CLI command that never returns is a worse failure than a wrong answer. The cap guarantees the command terminates. The agent gets room to genuinely work a problem across many steps, but not infinite room to flail about in.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also parallel tool execution: when the model asks for several tools in a single step (three independent file reads, say) the framework runs them concurrently rather than one after another, because there&amp;rsquo;s no reason to make the AI sit and wait out a sequence of things that don&amp;rsquo;t depend on each other.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;A text-only AI can describe your system; an AI that can call your functions can operate it. Bridging that gap means tool-calling, and tool-calling means the ReAct loop (reason, act, observe, repeat) whose orchestration is fiddly, identical every time, and not a problem worth solving twice.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package runs the loop for you. You define &lt;code&gt;chat.Tool&lt;/code&gt; values (name, description, a tagged parameter struct that generates its own schema, a handler), call &lt;code&gt;SetTools&lt;/code&gt; and &lt;code&gt;Chat&lt;/code&gt;, and get the final answer. Tool errors go back to the AI as observations so it can recover, and a &lt;code&gt;MaxSteps&lt;/code&gt; cap guarantees the command always terminates. You write Go functions. The framework turns them into things an agent can reach for.&lt;/p&gt;</description></item><item><title>Nobody reads the manual</title><link>https://blog-570662.gitlab.io/nobody-reads-the-manual/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/nobody-reads-the-manual/</guid><description>&lt;img src="https://blog-570662.gitlab.io/nobody-reads-the-manual/cover-nobody-reads-the-manual.png" alt="Featured image of post Nobody reads the manual" /&gt;&lt;p&gt;Let me describe the actual lifecycle of a user meeting your CLI tool, because it&amp;rsquo;s a bit humbling. They run it. It doesn&amp;rsquo;t quite do what they expected. They run it again with &lt;code&gt;--help&lt;/code&gt;. They get a wall of monospaced flag descriptions, skim it, don&amp;rsquo;t find the thing they wanted, and either give up or go and ask a human who already knows.&lt;/p&gt;
&lt;p&gt;Your documentation might be magnificent. It doesn&amp;rsquo;t matter, because the user never reached it.&lt;/p&gt;
&lt;h2 id="the-manual-loses-on-location-not-quality"&gt;The manual loses on location, not quality
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the lifecycle, and notice exactly where it breaks. The documentation might be excellent. It might answer their precise question in full. It doesn&amp;rsquo;t matter, because it&amp;rsquo;s on a website, in another window, behind a search box, and the user is &lt;em&gt;here&lt;/em&gt;, in the terminal, mid-task. The docs lost not on quality but on &lt;em&gt;location&lt;/em&gt;. They simply weren&amp;rsquo;t where the work was.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s answer starts with a decision about location: the documentation gets embedded into the binary itself. Your &lt;code&gt;docs/&lt;/code&gt; folder ships &lt;em&gt;inside&lt;/em&gt; the tool, the same way its default config does. Wherever the tool is installed, the docs are right there alongside it, no network, no browser. That embedding is what makes everything else possible, and there are two things built on top of it.&lt;/p&gt;
&lt;h2 id="a-browser-in-the-terminal"&gt;A browser, in the terminal
&lt;/h2&gt;&lt;p&gt;The first is the &lt;code&gt;docs&lt;/code&gt; command, and it&amp;rsquo;s not &lt;code&gt;--help&lt;/code&gt; with extra steps. It launches a proper Terminal User Interface, built on Bubble Tea.&lt;/p&gt;
&lt;p&gt;It has a sidebar, structured from the project&amp;rsquo;s own &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/docs/docs.go#L25" target="_blank" rel="noopener"
 &gt;&lt;code&gt;mkdocs.yml&lt;/code&gt;&lt;/a&gt;, so the docs are a navigable tree rather than one flat scroll. Markdown renders with real formatting through Glamour (colour, tables, lists, headings) instead of collapsing into monospaced soup. There&amp;rsquo;s live search across every page, regex included.&lt;/p&gt;
&lt;p&gt;Compared with &lt;code&gt;man&lt;/code&gt; and &lt;code&gt;--help&lt;/code&gt;, the difference isn&amp;rsquo;t a nicer coat of paint. &lt;code&gt;man&lt;/code&gt; gives you linear scrolling and grep; this gives you a structured tree, rich rendering and real search. It&amp;rsquo;s the documentation experience a modern developer expects, except it followed the tool &lt;em&gt;into&lt;/em&gt; the terminal instead of demanding the user leave it.&lt;/p&gt;
&lt;h2 id="a-documentation-assistant-that-wont-make-things-up"&gt;A documentation assistant that won&amp;rsquo;t make things up
&lt;/h2&gt;&lt;p&gt;The second thing built on the embedded docs is the one I find genuinely transformative: &lt;code&gt;docs ask&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The user doesn&amp;rsquo;t navigate anything. They just ask:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool docs ask &lt;span class="s2"&gt;&amp;#34;how do I point this at a self-hosted server?&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and get a direct, specific answer. Under the hood, the framework collates the tool&amp;rsquo;s embedded markdown and hands it to the configured AI provider (Claude, OpenAI, Gemini, Claude Local, any OpenAI-compatible endpoint) as the context for the question.&lt;/p&gt;
&lt;p&gt;Now, &amp;ldquo;an AI answers questions about my tool&amp;rdquo; should immediately make you nervous, and the correct thing to be nervous about is hallucination. An AI that confidently invents a flag that doesn&amp;rsquo;t exist, or describes behaviour the tool simply doesn&amp;rsquo;t have, is worse than no assistant at all, because the user &lt;em&gt;trusts&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;This is where embedding the docs pays off a second time, and it&amp;rsquo;s why I keep stressing that the corpus is &lt;em&gt;closed&lt;/em&gt;. The model is instructed to answer &lt;strong&gt;only&lt;/strong&gt; from the tool&amp;rsquo;s actual documentation, and the context it&amp;rsquo;s handed is exactly that documentation and nothing else. It isn&amp;rsquo;t drawing on a vague memory of similar tools from its training data. It&amp;rsquo;s answering from this tool&amp;rsquo;s real, shipped, version-matched docs. The corpus is small, closed and authoritative, which is the combination that keeps the answers honest. &amp;ldquo;Zero hallucination by design&amp;rdquo; isn&amp;rsquo;t a slogan about the model. It&amp;rsquo;s a property of bounding what the model is allowed to look at, which is the same instinct I &lt;a class="link" href="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/" &gt;leaned on with the &lt;code&gt;mcp&lt;/code&gt; command&lt;/a&gt;: the safety comes from the boundary you drew, not from trusting the AI to behave itself.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a nice second-order effect, too. The answer is always about the version of the tool the user actually has, because the docs were embedded into &lt;em&gt;that build&lt;/em&gt;. No mismatch between a website documenting the latest release and the slightly older binary sitting on the user&amp;rsquo;s machine.&lt;/p&gt;
&lt;h2 id="the-upshot"&gt;The upshot
&lt;/h2&gt;&lt;p&gt;Documentation usually loses to &lt;code&gt;--help&lt;/code&gt; not on quality but on location: it&amp;rsquo;s in a browser, and the user is in the terminal. go-tool-base embeds the docs into the binary and surfaces them two ways: a &lt;code&gt;docs&lt;/code&gt; command that&amp;rsquo;s a real TUI browser with a sidebar, rich markdown and search, and &lt;code&gt;docs ask&lt;/code&gt;, which answers natural-language questions using the embedded docs as context.&lt;/p&gt;
&lt;p&gt;Because that context is the tool&amp;rsquo;s own closed, shipped documentation and the model is told to use nothing else, the assistant stays grounded, and it&amp;rsquo;s always describing the exact version the user is holding. The fix for unread documentation was never to write more of it. It was to put it where the work happens and let it answer back.&lt;/p&gt;</description></item><item><title>BDD where it earns its place, and nowhere else</title><link>https://blog-570662.gitlab.io/bdd-where-it-earns-its-place/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/bdd-where-it-earns-its-place/</guid><description>&lt;img src="https://blog-570662.gitlab.io/bdd-where-it-earns-its-place/cover-bdd-where-it-earns-its-place.png" alt="Featured image of post BDD where it earns its place, and nowhere else" /&gt;&lt;p&gt;I have a slightly complicated relationship with BDD. I&amp;rsquo;ve watched it turn a tangled test suite into something the whole team could read and reason about, and I&amp;rsquo;ve watched it turn a perfectly good unit test into a paragraph of ceremonial English that nobody benefits from. So when go-tool-base brought in Cucumber-style BDD, the interesting decision wasn&amp;rsquo;t adopting it. It was being ruthless about where &lt;em&gt;not&lt;/em&gt; to.&lt;/p&gt;
&lt;h2 id="two-tests-that-hurt-for-different-reasons"&gt;Two tests that hurt for different reasons
&lt;/h2&gt;&lt;p&gt;Most of go-tool-base&amp;rsquo;s tests are ordinary table-driven Go tests, and they&amp;rsquo;re absolutely fine. A function, a slice of input/expected pairs, a loop. Nobody needs Gherkin to understand a parser test.&lt;/p&gt;
&lt;p&gt;But two areas were genuinely painful, and they were painful in the same way: the &lt;em&gt;test&lt;/em&gt; had become harder to understand than the &lt;em&gt;thing it was testing&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The first was &lt;code&gt;pkg/controls&lt;/code&gt;, the &lt;a class="link" href="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/" &gt;service-lifecycle package&lt;/a&gt;. It runs a small state machine (Unknown, Running, Stopping, Stopped) with signal handling, health monitoring, restart policies and graceful shutdown all woven through it. The integration tests for graceful shutdown had grown to over three hundred lines of imperative goroutine and channel coordination. They worked. But reviewing them was a slog, and a test you can&amp;rsquo;t review with confidence is a test you can&amp;rsquo;t trust when it fails. The behaviour being checked, &amp;ldquo;when a shutdown signal arrives mid-startup, the controller stops cleanly&amp;rdquo;, was a simple sentence buried under a heap of synchronisation scaffolding.&lt;/p&gt;
&lt;p&gt;The second was the CLI itself. &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt; are user &lt;em&gt;workflows&lt;/em&gt;. &amp;ldquo;Given a config file with a custom value, when I run init, then the custom value survives the merge.&amp;rdquo; That&amp;rsquo;s already a Given/When/Then; it just happened to be written out as Go.&lt;/p&gt;
&lt;h2 id="godog-and-the-line-i-drew"&gt;Godog, and the line I drew
&lt;/h2&gt;&lt;p&gt;Godog is the official Go implementation of Cucumber. You write &lt;code&gt;.feature&lt;/code&gt; files in plain Gherkin and bind each step to a Go function. The shutdown scenario stops being three hundred lines of channels and becomes this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gherkin" data-lang="gherkin"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;Scenario:&lt;/span&gt;&lt;span class="nf"&gt; graceful shutdown completes within the deadline
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt; Given &lt;/span&gt;&lt;span class="nf"&gt;a controller with two registered services
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt; &lt;/span&gt;&lt;span class="k"&gt;When &lt;/span&gt;&lt;span class="nf"&gt;a shutdown signal is received
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt; &lt;/span&gt;&lt;span class="k"&gt;Then &lt;/span&gt;&lt;span class="nf"&gt;both services stop in registration order
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt; &lt;/span&gt;&lt;span class="k"&gt;And &lt;/span&gt;&lt;span class="nf"&gt;the controller reports a clean shutdown
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The goroutine choreography doesn&amp;rsquo;t vanish, of course. It moves into the step definitions, written once and reused. What changes is that the &lt;em&gt;scenario&lt;/em&gt; is now readable by someone who&amp;rsquo;s never opened the file before, including someone from an ops team who&amp;rsquo;ll never write a line of Go but absolutely has opinions about how shutdown should behave.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the part I want to dwell on, because it&amp;rsquo;s the part most BDD adoptions get wrong. The first design decision written down for this work was: &lt;strong&gt;strategic, not universal.&lt;/strong&gt; Use Godog &lt;em&gt;only&lt;/em&gt; where BDD adds clarity. Keep table-driven Go tests as the baseline everywhere else.&lt;/p&gt;
&lt;p&gt;That sounds obvious written down. It is not obvious in practice, because BDD has a gravitational pull. Once a team has feature files, there&amp;rsquo;s a powerful urge to express &lt;em&gt;everything&lt;/em&gt; as feature files, for consistency. And that&amp;rsquo;s how you end up with Gherkin scenarios for a pure function (&lt;code&gt;Given the number 2, When I double it, Then I get 4&lt;/code&gt;) which is pure ceremony. You&amp;rsquo;ve wrapped a one-line table test in a paragraph of English and a step-definition indirection, and made it actively worse.&lt;/p&gt;
&lt;p&gt;The test for whether BDD belongs is this: &lt;strong&gt;is this test a narrative, or is it a matrix?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A matrix is the same logic with many input/output pairs. That&amp;rsquo;s a table-driven test, that&amp;rsquo;s most unit tests, and Gherkin actively harms them. A narrative is a sequence of steps where the &lt;em&gt;ordering&lt;/em&gt; and the &lt;em&gt;state between steps&lt;/em&gt; is the thing under test, and that&amp;rsquo;s where Gherkin pays for itself. Lifecycle transitions are narratives. A user running three commands in sequence is a narrative. Doubling a number is not.&lt;/p&gt;
&lt;p&gt;go-tool-base drew that line and stuck to it. Feature files live in &lt;code&gt;features/&lt;/code&gt; at the project root, where a non-Go developer can find and read them. Step definitions live in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/test/e2e/steps/controls_steps_test.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;test/e2e/&lt;/code&gt;&lt;/a&gt;, kept well away from the unit tests. And the unit tests stayed exactly what they were, because they were already the right tool.&lt;/p&gt;
&lt;h2 id="made-to-fit-not-bolted-on"&gt;Made to fit, not bolted on
&lt;/h2&gt;&lt;p&gt;A couple of smaller decisions kept the BDD layer from feeling like a foreign object.&lt;/p&gt;
&lt;p&gt;It runs under &lt;code&gt;go test&lt;/code&gt;. There&amp;rsquo;s no separate Cucumber runner to install or remember. A &lt;code&gt;godog.TestSuite&lt;/code&gt; is invoked from an ordinary &lt;code&gt;TestFeatures(t *testing.T)&lt;/code&gt;, so the BDD scenarios run in the same &lt;code&gt;go test ./...&lt;/code&gt; as everything else. CI didn&amp;rsquo;t need a new concept bolted onto it.&lt;/p&gt;
&lt;p&gt;And the CLI end-to-end tests build the &lt;code&gt;gtb&lt;/code&gt; binary &lt;em&gt;once&lt;/em&gt; and reuse it across every scenario. Compiling a binary per scenario would make the suite slow enough that people would quietly start skipping it, and a test suite people skip is just decoration. Build once, test many.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;go-tool-base brought in Godog for BDD, but the decision worth writing about is the restraint. BDD was applied to exactly two things: the service-lifecycle state machine, where a 300-line goroutine tangle became a four-line scenario anyone can review, and CLI workflows, which are Given/When/Then by their very nature. Everywhere else, table-driven Go tests remained the baseline, because wrapping a matrix test in Gherkin makes it worse, not better.&lt;/p&gt;
&lt;p&gt;The useful rule: BDD fits a &lt;em&gt;narrative&lt;/em&gt;, ordered steps with meaningful state in between, and fights a &lt;em&gt;matrix&lt;/em&gt;. Adopt it as a scalpel for the narratives. Resist the pull to turn it into a religion.&lt;/p&gt;</description></item><item><title>An AI interface that fits on one screen</title><link>https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/</guid><description>&lt;img src="https://blog-570662.gitlab.io/an-ai-interface-that-fits-on-one-screen/cover-an-ai-interface-that-fits-on-one-screen.png" alt="Featured image of post An AI interface that fits on one screen" /&gt;&lt;p&gt;The moment you decide a CLI tool should talk to an LLM, there&amp;rsquo;s a strong gravitational pull towards reaching for LangChain, or one of its many relatives. It&amp;rsquo;s the obvious move. It&amp;rsquo;s also, for most CLI work, a bit like hiring a removals firm to carry a single box up the stairs.&lt;/p&gt;
&lt;p&gt;Let me explain why go-tool-base went the other way, and what &amp;ldquo;the other way&amp;rdquo; actually looks like.&lt;/p&gt;
&lt;h2 id="the-instinct-and-why-it-overshoots"&gt;The instinct, and why it overshoots
&lt;/h2&gt;&lt;p&gt;When you add AI to a tool, the instinct is to reach for the big general-purpose framework. LangChain and its relatives are capable, and they exist for a real need: orchestrating complex multi-step AI applications, with retrieval pipelines, memory stores, chains of calls, whole fleets of agents.&lt;/p&gt;
&lt;p&gt;Now look at what a CLI tool actually needs from an LLM. It needs to send a prompt and get text back. Sometimes it wants structured data back instead of prose. Sometimes it wants to let the model call a few of the tool&amp;rsquo;s own functions. That&amp;rsquo;s pretty much the whole list.&lt;/p&gt;
&lt;p&gt;Pulling in a framework built to orchestrate retrieval and agent swarms in order to do &lt;em&gt;that&lt;/em&gt; is a poor trade. You take on a large new vocabulary of concepts, a wide dependency surface, and a great deal of abstraction you&amp;rsquo;ll never touch, all to perform three or four operations. The framework isn&amp;rsquo;t wrong. It&amp;rsquo;s just answering a far bigger question than the one a CLI tool is asking.&lt;/p&gt;
&lt;h2 id="what-go-tool-base-chose-instead"&gt;What go-tool-base chose instead
&lt;/h2&gt;&lt;p&gt;go-tool-base didn&amp;rsquo;t reach for a framework. The decision is on the record in its own design notes: before a single line was written, LangChain Go, go-openai, Vercel&amp;rsquo;s AI SDK and around ten other options were evaluated, and not one of them matched what a CLI framework actually needs. So the &lt;code&gt;chat&lt;/code&gt; package was built deliberately small.&lt;/p&gt;
&lt;p&gt;How small? The entire core &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/client.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;ChatClient&lt;/code&gt;&lt;/a&gt; interface is four methods:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ChatClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;SetTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Add&lt;/code&gt; appends a message to the conversation. &lt;code&gt;Chat&lt;/code&gt; sends a prompt and returns text. &lt;code&gt;Ask&lt;/code&gt; sends a prompt and returns a &lt;em&gt;typed Go struct&lt;/em&gt;, the model&amp;rsquo;s answer unmarshalled straight into a value you defined. &lt;code&gt;SetTools&lt;/code&gt; hands the model a set of your own functions it&amp;rsquo;s allowed to call. That&amp;rsquo;s the whole surface. Downstream code that uses AI never holds anything larger than this, and never has to know which provider is behind it.&lt;/p&gt;
&lt;p&gt;The package&amp;rsquo;s own documentation has a word for this: right-sized. Large enough to solve genuine provider-abstraction complexity, small enough that the full interface fits on a single screen.&lt;/p&gt;
&lt;h2 id="thin-is-not-the-same-as-does-little"&gt;&amp;ldquo;Thin&amp;rdquo; is not the same as &amp;ldquo;does little&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;This is the part worth being precise about, because &amp;ldquo;four methods&amp;rdquo; can sound like &amp;ldquo;barely does anything&amp;rdquo;, and that&amp;rsquo;s the wrong read entirely.&lt;/p&gt;
&lt;p&gt;Behind those four methods sits genuinely awkward work. Five providers (OpenAI, Claude, Gemini, a locally installed &lt;code&gt;claude&lt;/code&gt; binary, and any OpenAI-compatible endpoint) each with a different wire API, all normalised behind the one interface. A &lt;a class="link" href="https://blog-570662.gitlab.io/letting-the-ai-call-your-go-functions/" &gt;tool-calling loop&lt;/a&gt;. Structured output via JSON Schema, made to behave consistently across providers that each express it differently. Error normalisation. Token chunking.&lt;/p&gt;
&lt;p&gt;The point of a thin abstraction is not that there&amp;rsquo;s little underneath it. It&amp;rsquo;s that the &lt;em&gt;interface&lt;/em&gt; stays small while the &lt;em&gt;implementation&lt;/em&gt; quietly absorbs the complexity. Four methods on the surface; five provider integrations and a tool-calling loop below the waterline. The thinness is a property of what the caller sees, not of what the package does. A reach-for-LangChain decision gets that backwards: it exposes the caller to all the machinery, whether or not the caller will ever need it.&lt;/p&gt;
&lt;h2 id="the-core-stays-small-even-as-features-grow"&gt;The core stays small even as features grow
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a neat detail in how &lt;code&gt;chat&lt;/code&gt; keeps the interface from creeping. The package also supports streaming responses and conversation persistence, both of which are real features with real surface area. Neither of them is in the four-method core.&lt;/p&gt;
&lt;p&gt;Instead they&amp;rsquo;re &lt;em&gt;separate, optional&lt;/em&gt; interfaces. A streaming-capable client also satisfies &lt;code&gt;StreamingChatClient&lt;/code&gt;; a persistable one also satisfies &lt;code&gt;PersistentChatClient&lt;/code&gt;. Code that wants those capabilities does a type assertion to ask for them, and code that doesn&amp;rsquo;t simply never sees them. So the common path stays four methods forever. New capabilities arrive as opt-in interfaces alongside the core, not as new methods bolted onto it. The thing that fits on one screen keeps fitting on one screen.&lt;/p&gt;
&lt;h2 id="extensible-without-forking-testable-without-a-network"&gt;Extensible without forking, testable without a network
&lt;/h2&gt;&lt;p&gt;Two more properties keep the package small without making it limiting.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s extensible. The provider list isn&amp;rsquo;t closed. A &lt;code&gt;RegisterProvider&lt;/code&gt; call lets any package contribute a new provider, and &lt;code&gt;chat.New&lt;/code&gt; will route to it. You add a backend without forking &lt;code&gt;pkg/chat&lt;/code&gt; or sending a patch upstream.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s testable. The package ships generated mocks. A downstream tool&amp;rsquo;s AI features can be tested against a mock &lt;code&gt;ChatClient&lt;/code&gt; returning canned responses, with no network, no API key, and no flakiness. Because the interface is four methods, that mock is trivial to set up and complete by construction. A sprawling framework interface is a sprawling thing to fake; a four-method one is not. (I&amp;rsquo;ll come back to testing AI code properly &lt;a class="link" href="https://blog-570662.gitlab.io/testing-code-that-calls-an-llm/" &gt;in a later post&lt;/a&gt;, because it deserves a whole article of its own.)&lt;/p&gt;
&lt;h2 id="the-right-size"&gt;The right size
&lt;/h2&gt;&lt;p&gt;When a CLI tool needs AI, the instinct is a large framework like LangChain. For orchestrating retrieval pipelines and agent swarms, that&amp;rsquo;s exactly the right tool. For sending a prompt, getting a struct back, and letting the model call a few functions, it&amp;rsquo;s enormous overkill.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s &lt;code&gt;chat&lt;/code&gt; package is the deliberate alternative, chosen only after LangChain Go and a dozen others were weighed up and rejected. Its core &lt;code&gt;ChatClient&lt;/code&gt; interface is four methods. Underneath sit five normalised providers, a tool-calling loop, structured output and error handling, but the caller sees four methods and never learns which provider is active. Streaming and persistence are opt-in interfaces beside the core, not additions to it. It extends without forking and tests without a network. Right-sized: the complexity is real, but it lives under the interface rather than in it.&lt;/p&gt;</description></item><item><title>The config key that quietly did nothing</title><link>https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/</link><pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/the-config-key-that-quietly-did-nothing/cover-the-config-key-that-quietly-did-nothing.png" alt="Featured image of post The config key that quietly did nothing" /&gt;&lt;p&gt;I once spent the better part of an hour convinced a timeout setting was broken. I&amp;rsquo;d set it in the config file, the tool ignored it, and the code that read it looked perfectly correct. The setting was &lt;code&gt;tiemout&lt;/code&gt;. I&amp;rsquo;d typed it wrong, and not one thing in the entire stack had thought that worth mentioning.&lt;/p&gt;
&lt;h2 id="config-loaders-are-too-polite"&gt;Config loaders are too polite
&lt;/h2&gt;&lt;p&gt;Most config loaders have the same agreeable flaw: they&amp;rsquo;ll read whatever&amp;rsquo;s in the file and quietly ignore anything they weren&amp;rsquo;t expecting. Put a key the tool doesn&amp;rsquo;t know about and it sails straight past. No error, no warning, nothing. The loader assumes you meant it, or assumes some other layer will care, and neither turns out to be true.&lt;/p&gt;
&lt;p&gt;That politeness costs you in two directions. A key you misspelled is silently dropped, so the setting you thought you&amp;rsquo;d changed keeps running on its old value. And a key you &lt;em&gt;forgot&lt;/em&gt; leaves the field at its zero value, which you then discover at runtime, usually at the least convenient moment, when something downstream divides by a timeout of zero. The file looked fine. It parsed fine. It was just quietly wrong, and nothing was watching for that.&lt;/p&gt;
&lt;h2 id="the-struct-already-knows-the-answer"&gt;The struct already knows the answer
&lt;/h2&gt;&lt;p&gt;The thing is, the program already has a complete description of what valid config looks like. It&amp;rsquo;s the struct you unmarshal into. The field names, the types, which ones matter. That description exists; it&amp;rsquo;s just not being used to &lt;em&gt;check&lt;/em&gt; anything.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s config package puts it to work. You hand it a tagged struct and it derives a schema from the tags, in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/config/schema.go#L48" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/config/schema.go&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// WithStructSchema derives a schema from a tagged Go struct.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Supported tags: `config:&amp;#34;key&amp;#34; validate:&amp;#34;required&amp;#34; enum:&amp;#34;a,b,c&amp;#34; default:&amp;#34;value&amp;#34;`.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;WithStructSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SchemaOption&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So a feature&amp;rsquo;s config type carries its own rules inline:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ServerConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;host&amp;#34; validate:&amp;#34;required&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;port&amp;#34; validate:&amp;#34;required&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;LogMode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;`config:&amp;#34;log_mode&amp;#34; enum:&amp;#34;text,json&amp;#34;`&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s no second artefact to keep in sync, which is the same instinct go-tool-base leans on for &lt;a class="link" href="https://blog-570662.gitlab.io/stop-regexing-the-llms-prose/" &gt;structured AI output&lt;/a&gt;: the type is the schema, and the schema is a projection of the type, so the two can&amp;rsquo;t drift apart because there&amp;rsquo;s only one of them. Each package describes its own slice of config on its own struct, and &lt;code&gt;NewSchema&lt;/code&gt; composes them into the schema the loaded config gets checked against.&lt;/p&gt;
&lt;h2 id="strict-mode-turns-the-typo-into-an-error"&gt;Strict mode turns the typo into an error
&lt;/h2&gt;&lt;p&gt;Deriving the schema is half of it. The half that actually catches &lt;code&gt;tiemout&lt;/code&gt; is this one, also from &lt;code&gt;schema.go&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// WithStrictMode treats unknown keys as errors instead of warnings.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;WithStrictMode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SchemaOption&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By default a key the schema doesn&amp;rsquo;t recognise is a &lt;em&gt;warning&lt;/em&gt;: surfaced, but not fatal, which is the right call when a config file might legitimately carry extra keys for tools other than yours. Turn on strict mode and an unknown key becomes an &lt;em&gt;error&lt;/em&gt;. &lt;code&gt;tiemout&lt;/code&gt; isn&amp;rsquo;t in the schema, so the tool refuses to start and tells me which key it didn&amp;rsquo;t recognise, instead of shrugging and using the default for an hour while I lose my mind. The validator walks every key actually present in the file and checks it against the known set, so a typo has nowhere to hide.&lt;/p&gt;
&lt;h2 id="what-it-deliberately-doesnt-do"&gt;What it deliberately doesn&amp;rsquo;t do
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s one decision in here I think is worth calling out, because the obvious feature is conspicuously absent. The schema knows each field&amp;rsquo;s default value. It would be the easiest thing in the world to have validation &lt;em&gt;fill in&lt;/em&gt; missing fields from those defaults.&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t, on purpose. Validation validates. It tells you what&amp;rsquo;s wrong and what to do about it, and it stops there. Defaults are a separate job, handled by the &lt;a class="link" href="https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/" &gt;embedded default config that every feature ships&lt;/a&gt; and merges in before validation ever runs. Keeping the two apart means the validator has exactly one responsibility, and the defaults live in one place rather than being half in an embedded file and half injected by a check. A field&amp;rsquo;s &lt;code&gt;default&lt;/code&gt; tag is there for the documentation and the error hint, not as a sneaky second source of values.&lt;/p&gt;
&lt;h2 id="errors-you-can-act-on"&gt;Errors you can act on
&lt;/h2&gt;&lt;p&gt;The output isn&amp;rsquo;t a bare boolean. Validation returns a result that separates the fatal from the advisory: the missing required field and the wrong type are errors that stop the tool; the unrecognised-but-harmless key is a warning that informs you without blocking. And because each problem carries the offending key by name and a hint about the fix, the message tells you what to change, in the spirit of &lt;a class="link" href="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/" &gt;errors that tell you what to do next&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;A config loader that silently ignores keys it doesn&amp;rsquo;t recognise will, sooner or later, ignore one you meant. go-tool-base derives a validation schema straight from your tagged config struct, so there&amp;rsquo;s no separate schema to maintain, and strict mode promotes an unknown key from a quiet shrug to a real error that names the typo. It validates without injecting defaults, because defaults are the embedded config&amp;rsquo;s job and a validator with one responsibility is easier to trust. Set &lt;code&gt;tiemout&lt;/code&gt; now and the tool tells you, which is roughly fifty-nine minutes sooner than I found out.&lt;/p&gt;</description></item><item><title>One variadic, and I'd already spent it</title><link>https://blog-570662.gitlab.io/one-variadic-and-id-already-spent-it/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/one-variadic-and-id-already-spent-it/</guid><description>&lt;img src="https://blog-570662.gitlab.io/one-variadic-and-id-already-spent-it/cover-one-variadic-and-id-already-spent-it.png" alt="Featured image of post One variadic, and I'd already spent it" /&gt;&lt;p&gt;I had a constructor I was rather pleased with. Hand &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;go-tool-base&lt;/a&gt;&amp;rsquo;s root command its props and as many sub-commands as you like, and off it goes. Then I needed to thread some config file paths through it, reached for the obvious &amp;ldquo;just add a parameter,&amp;rdquo; and discovered I&amp;rsquo;d already spent my one variadic with no second one going spare.&lt;/p&gt;
&lt;h2 id="the-ergonomics-id-happily-bought"&gt;The ergonomics I&amp;rsquo;d happily bought
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;NewCmdRoot&lt;/code&gt; ends in &lt;code&gt;subcommands ...*cobra.Command&lt;/code&gt;. That trailing &lt;code&gt;...&lt;/code&gt; is a small luxury: callers write &lt;code&gt;NewCmdRoot(props, build, deploy, status)&lt;/code&gt; and never have to think about slices. Variadics are lovely for exactly this, the &amp;ldquo;and as many of these as you fancy&amp;rdquo; argument.&lt;/p&gt;
&lt;h2 id="the-parameter-i-couldnt-add"&gt;The parameter I couldn&amp;rsquo;t add
&lt;/h2&gt;&lt;p&gt;Then config arrived, and the root command needed to know about some extra configuration file paths. The instinct is to add a parameter. The instinct is wrong, because there&amp;rsquo;s nowhere legal to put it.&lt;/p&gt;
&lt;p&gt;You can&amp;rsquo;t write &lt;code&gt;NewCmdRoot(props, configPaths ...string, subcommands ...*cobra.Command)&lt;/code&gt;. Go allows a function exactly one variadic, and it must be the final parameter. Two variadics results in a compile error before you&amp;rsquo;ve finished the line (assuming your IDE does compile time checks for you), and fairly so: at the call site, how would Go ever know where the strings stopped and the commands began? So the variadic I&amp;rsquo;d spent on sub-commands was spent. There wasn&amp;rsquo;t another to hand.&lt;/p&gt;
&lt;h2 id="the-choices-and-the-one-i-took"&gt;The choices, and the one I took
&lt;/h2&gt;&lt;p&gt;You can demote the variadic. Make it &lt;code&gt;subcommands []*cobra.Command&lt;/code&gt; and you&amp;rsquo;re free to add &lt;code&gt;configPaths []string&lt;/code&gt; next to it. Correct, and it breaks every existing call: &lt;code&gt;NewCmdRoot(props, build, deploy)&lt;/code&gt; becomes &lt;code&gt;NewCmdRoot(props, []string{}, []*cobra.Command{build, deploy})&lt;/code&gt;. Uglier at every site, to solve a problem only some callers have.&lt;/p&gt;
&lt;p&gt;You can reach for functional options, and for plenty of go-tool-base&amp;rsquo;s constructors that is exactly what happened. But the root builder is the one everybody calls first, with the simplest signature in the codebase, and I didn&amp;rsquo;t want the common case lugging option machinery around for the sake of the rare one.&lt;/p&gt;
&lt;p&gt;What I actually did was add a second door. From &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/cmd/root/root.go#L334-342" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/cmd/root/root.go&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// NewCmdRoot creates the root command with Props wiring and optional subcommands.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdRoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;subcommands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdRootWithConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;subcommands&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdRootWithConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;configPaths&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;subcommands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The new argument goes in as a plain &lt;code&gt;[]string&lt;/code&gt;, sat &lt;em&gt;before&lt;/em&gt; the variadic, which is perfectly legal: one variadic, still last. Callers who care about config use &lt;code&gt;NewCmdRootWithConfig&lt;/code&gt; explicitly, and &lt;code&gt;NewCmdRoot&lt;/code&gt; becomes a one-line wrapper that delegates with an empty slice, so every existing caller compiles untouched and none the wiser. Two doors into the same room, granted, but the original door is exactly where everyone left it.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;A trailing variadic is a slot you fill once. It buys gorgeous ergonomics for the &amp;ldquo;as many as you like&amp;rdquo; argument, and in exchange it quietly forecloses on ever appending another parameter, because the next one has nowhere to stand. Once it&amp;rsquo;s spent, new arguments come in as ordinary parameters before the variadic, and the kind thing to do for your callers is to put that behind a second constructor and let the original keep delegating.&lt;/p&gt;
&lt;p&gt;So spend the variadic deliberately. Give it to the argument that genuinely wants to be a loose list, not the first one that happens to be plural, because you don&amp;rsquo;t get a second.&lt;/p&gt;</description></item><item><title>Half your users don't have eyes</title><link>https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/</guid><description>&lt;img src="https://blog-570662.gitlab.io/half-your-users-dont-have-eyes/cover-half-your-users-dont-have-eyes.png" alt="Featured image of post Half your users don't have eyes" /&gt;&lt;p&gt;Run a command in your favourite CLI tool and look at what comes back. Colour. Neatly aligned columns. A friendly little summary sentence. Lovely&amp;hellip; if you happen to be a human with eyes.&lt;/p&gt;
&lt;p&gt;But a good half of any tool&amp;rsquo;s users aren&amp;rsquo;t people at all. They&amp;rsquo;re scripts, CI pipelines, bits of automation. And that pretty output you&amp;rsquo;re so proud of is, to them, actively hostile.&lt;/p&gt;
&lt;h2 id="your-tool-has-two-audiences-and-only-serves-one"&gt;Your tool has two audiences and only serves one
&lt;/h2&gt;&lt;p&gt;I made more or less this same point about AI assistants when I argued that &lt;a class="link" href="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/" &gt;your CLI is already an AI tool&lt;/a&gt;. The machines are users too. Here it isn&amp;rsquo;t an AI doing the calling, it&amp;rsquo;s a humble shell script, but the principle is identical.&lt;/p&gt;
&lt;p&gt;Run a CLI command and look at what comes back. Colour. Aligned columns. A friendly summary sentence. It&amp;rsquo;s designed for a person reading a terminal, and for a person reading a terminal it&amp;rsquo;s great.&lt;/p&gt;
&lt;p&gt;Now picture the other half of your users. A deploy script that needs to know which version is installed. A CI job that runs &lt;code&gt;doctor&lt;/code&gt; and wants to fail the build on one specific check. A bit of automation gluing your tool to three others. None of them have eyes. They have parsers.&lt;/p&gt;
&lt;p&gt;So what do they do with your beautiful human output? They butcher it. They &lt;code&gt;grep&lt;/code&gt; for a keyword, &lt;code&gt;awk&lt;/code&gt; out the third field, &lt;code&gt;sed&lt;/code&gt; off a prefix. It works in the demo. Then someone rewords a status line, or adds a column, or the colour codes shift, and every script downstream breaks at once. Silently, too, because a broken &lt;code&gt;grep&lt;/code&gt; returns nothing rather than an error. You changed a sentence and quietly took out somebody&amp;rsquo;s pipeline without ever knowing.&lt;/p&gt;
&lt;p&gt;The human-readable output was never the contract. It just got &lt;em&gt;used&lt;/em&gt; as one, because it was the only output there was.&lt;/p&gt;
&lt;h2 id="give-the-machines-their-own-channel"&gt;Give the machines their own channel
&lt;/h2&gt;&lt;p&gt;The fix is not to make the human output more parseable. That&amp;rsquo;s a trap. You&amp;rsquo;d be constraining prose meant for people in order to satisfy programs, and end up serving neither of them well. The fix is to give programs their own output format, declared and stable, kept well away from the prose.&lt;/p&gt;
&lt;p&gt;So every command built with go-tool-base gets a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/cmd/root/root.go#L447" target="_blank" rel="noopener"
 &gt;&lt;code&gt;--output&lt;/code&gt; flag&lt;/a&gt;. Leave it alone and you get the friendly human rendering. Pass &lt;code&gt;--output json&lt;/code&gt; and you get something a parser can actually rely on.&lt;/p&gt;
&lt;p&gt;And not just &lt;em&gt;some&lt;/em&gt; JSON. JSON with a fixed shape.&lt;/p&gt;
&lt;h2 id="one-envelope-every-command"&gt;One envelope, every command
&lt;/h2&gt;&lt;p&gt;The temptation with JSON output is to let each command emit whatever structure happens to suit it. Don&amp;rsquo;t. A consumer scripting against five of your commands then has to learn five shapes, and &amp;ldquo;where&amp;rsquo;s the actual payload?&amp;rdquo; has a different answer every single time.&lt;/p&gt;
&lt;p&gt;go-tool-base wraps every command&amp;rsquo;s JSON in one standard &lt;code&gt;Response&lt;/code&gt; envelope:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;command&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;deploy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;environment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;production&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1.4.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;replicas&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;status&lt;/code&gt; says how it went. &lt;code&gt;command&lt;/code&gt; says what produced it. &lt;code&gt;data&lt;/code&gt; holds the command-specific payload, and &lt;em&gt;only&lt;/em&gt; the payload. Every built-in command (&lt;code&gt;version&lt;/code&gt;, &lt;code&gt;doctor&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt;) emits exactly this shape. So does every command you write, because &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/output/output.go#L45" target="_blank" rel="noopener"
 &gt;&lt;code&gt;pkg/output&lt;/code&gt;&lt;/a&gt; hands you the envelope rather than letting you freelance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Flags&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;deploy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The consumer-side payoff is the whole point. A script can check &lt;code&gt;.status&lt;/code&gt; without ever touching &lt;code&gt;.data&lt;/code&gt;. It can pull &lt;code&gt;.data.version&lt;/code&gt; and know the field is there because it&amp;rsquo;s typed, not scraped. It learns the envelope once, and every command in your tool, and every tool built on the framework, honours it. The contract is explicit, versioned, and the same everywhere, which is precisely what the abused human output never was.&lt;/p&gt;
&lt;h2 id="the-human-output-gets-to-relax"&gt;The human output gets to relax
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a quiet second benefit, and it&amp;rsquo;s my favourite kind: the sort you get for free. Once programs have their own reliable channel, the human output is &lt;em&gt;freed&lt;/em&gt;. It no longer has to stay accidentally parseable. You can reword a status line, add colour, restructure a table, make it genuinely nicer to read, and not break a single script, because no script is reading it any more. They&amp;rsquo;re all over on &lt;code&gt;--output json&lt;/code&gt;, where the real contract lives.&lt;/p&gt;
&lt;p&gt;Two audiences, two formats, each one actually suited to its reader. That&amp;rsquo;s the deal a CLI tool ought to be offering, and most of them don&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="in-short"&gt;In short
&lt;/h2&gt;&lt;p&gt;A CLI tool that only emits human-readable output is only half-built, because half its users are programs that end up &lt;code&gt;grep&lt;/code&gt;-ing prose and shattering the moment that prose changes. go-tool-base gives every command a &lt;code&gt;--output json&lt;/code&gt; flag and one standard &lt;code&gt;Response&lt;/code&gt; envelope (&lt;code&gt;status&lt;/code&gt;, &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;) used identically by every built-in command and by anything you write through &lt;code&gt;pkg/output&lt;/code&gt;. Machines get a stable, explicit, learn-it-once contract; humans get output that&amp;rsquo;s now free to be properly readable, because nothing fragile depends on its wording any more.&lt;/p&gt;
&lt;p&gt;If your tool will ever be called by another program (and it will), give that program a front door. Don&amp;rsquo;t make it climb in through the window.&lt;/p&gt;</description></item><item><title>Lifecycle management for when your CLI grows up into a service</title><link>https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/</link><pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/</guid><description>&lt;img src="https://blog-570662.gitlab.io/lifecycle-management-for-long-running-go-services/cover-lifecycle-management-for-long-running-go-services.png" alt="Featured image of post Lifecycle management for when your CLI grows up into a service" /&gt;&lt;p&gt;There&amp;rsquo;s a moment in the life of a lot of CLI tools where they stop being a CLI tool. Nobody quite decides it. It just happens. Someone needs the thing to also expose a little HTTP endpoint, or poll a queue, or run a scheduler, so it grows a &lt;code&gt;serve&lt;/code&gt; command&amp;hellip; and the honest command-line utility you wrote is suddenly a long-running service wearing a CLI as a hat.&lt;/p&gt;
&lt;p&gt;And a service needs a whole pile of production plumbing that a one-shot command never did.&lt;/p&gt;
&lt;h2 id="the-command-that-stops-being-a-command"&gt;The command that stops being a command
&lt;/h2&gt;&lt;p&gt;go-tool-base is CLI-first. It is not CLI-&lt;em&gt;only&lt;/em&gt;, and the reason is a pattern I&amp;rsquo;ve watched play out more times than I can count.&lt;/p&gt;
&lt;p&gt;A tool starts its life as an honest command-line utility. It runs, it does its thing, it exits. Then someone needs it to expose a small HTTP endpoint. Or poll a queue. Or run a scheduler. So it grows a &lt;code&gt;serve&lt;/code&gt; command, or a &lt;code&gt;run&lt;/code&gt; command, and the moment it does, the thing that was a CLI tool is now a long-running service that happens to have a CLI bolted on the front.&lt;/p&gt;
&lt;p&gt;And a long-running service needs a whole category of plumbing a one-shot command never did. It has to start things up in a sensible order. It has to shut them down &lt;em&gt;gracefully&lt;/em&gt; when someone sends a &lt;code&gt;SIGTERM&lt;/code&gt;, finishing in-flight work rather than dropping it on the floor. It has to tell an orchestrator whether it&amp;rsquo;s alive, and whether it&amp;rsquo;s ready. It has to do something sensible when one of its internal services quietly falls over at 3am.&lt;/p&gt;
&lt;p&gt;Hand-rolled, that&amp;rsquo;s a few hundred lines of goroutine choreography, channel-wrangling and signal handling that every such tool reinvents, slightly differently and slightly wrong each time. It&amp;rsquo;s the first-afternoon problem all over again, just turning up later in the project&amp;rsquo;s life. So go-tool-base ships it: &lt;code&gt;pkg/controls&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="a-controller-and-the-things-it-controls"&gt;A controller and the things it controls
&lt;/h2&gt;&lt;p&gt;The model is small. A &lt;code&gt;Controller&lt;/code&gt; manages any number of services. You register each with &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/controls/controller.go#L125" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Register(id, opts...)&lt;/code&gt;&lt;/a&gt; and describe it with functional options: &lt;code&gt;WithStart&lt;/code&gt; takes a &lt;code&gt;StartFunc&lt;/code&gt;, &lt;code&gt;WithStop&lt;/code&gt; a &lt;code&gt;StopFunc&lt;/code&gt;. An HTTP server, a background worker, a scheduler, anything with a &amp;ldquo;begin&amp;rdquo; and an &amp;ldquo;end&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;You register your services with the controller and it owns their collective lifecycle. They share a common set of channels (errors, OS signals, health, control messages) so the whole set can react together. A &lt;code&gt;SIGTERM&lt;/code&gt; doesn&amp;rsquo;t get caught by one service off in a corner; it reaches the controller, and the controller takes everything down in order, each &lt;code&gt;StopFunc&lt;/code&gt; handed a context with a deadline so that one sulking service can&amp;rsquo;t wedge the whole shutdown forever.&lt;/p&gt;
&lt;p&gt;That ordering and timeout handling is the bit nobody enjoys writing and everybody needs. Centralising it means a tool that adds a second service later inherits correct coordinated shutdown for free, rather than discovering on its first production &lt;code&gt;SIGTERM&lt;/code&gt; that it only half shuts down.&lt;/p&gt;
&lt;h2 id="probes-because-something-is-usually-watching"&gt;Probes, because something is usually watching
&lt;/h2&gt;&lt;p&gt;If the service ends up in Kubernetes (and a lot of them do) the orchestrator wants to ask two different questions, and they really are different questions.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Liveness:&lt;/em&gt; are you alive, or are you wedged and in need of a kill? &lt;em&gt;Readiness:&lt;/em&gt; are you alive &lt;em&gt;and&lt;/em&gt; able to take traffic right now? A service can quite easily be live but not ready&amp;hellip; still warming a cache, still waiting on a dependency. Conflate the two and you get yourself killed during a slow startup, or sent traffic before you can actually serve it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;controls&lt;/code&gt; keeps them separate. You attach a &lt;code&gt;WithLiveness&lt;/code&gt; probe and a &lt;code&gt;WithReadiness&lt;/code&gt; probe to a service, each just a function returning a health report, and the controller exposes them. The tool answers Kubernetes honestly, in Kubernetes&amp;rsquo; own terms, without you hand-wiring two more HTTP handlers.&lt;/p&gt;
&lt;h2 id="self-healing-but-only-if-you-ask"&gt;Self-healing, but only if you ask
&lt;/h2&gt;&lt;p&gt;The last piece is what happens when a service fails. A worker&amp;rsquo;s &lt;code&gt;StartFunc&lt;/code&gt; returns an error. Health checks start failing. In a hand-rolled setup this is where you either crash the whole process or write yourself a bespoke restart loop.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;controls&lt;/code&gt; has a supervisor that can restart a failed service for you, and the important word in that sentence is &lt;em&gt;can&lt;/em&gt;. It&amp;rsquo;s off by default. A service is only supervised if you hand it a &lt;code&gt;RestartPolicy&lt;/code&gt; at registration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithRestartPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RestartPolicy&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MaxRestarts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;InitialBackoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MaxBackoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HealthFailureThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With a policy in place, the controller restarts the service if its &lt;code&gt;StartFunc&lt;/code&gt; errors out, or if it racks up more consecutive health-check failures than the threshold allows. Restarts back off exponentially, from &lt;code&gt;InitialBackoff&lt;/code&gt; up to a &lt;code&gt;MaxBackoff&lt;/code&gt; ceiling, so a service that&amp;rsquo;s failing because its database is down doesn&amp;rsquo;t sit there hammering that database flat with a tight restart loop. &lt;code&gt;MaxRestarts&lt;/code&gt; caps the attempts, because a service that&amp;rsquo;s failed five times in a row is not going to be rescued by a sixth go, and at that point honest failure beats a thrashing pretence of health.&lt;/p&gt;
&lt;p&gt;Opt-in matters here. Automatic restarts are exactly right for a resilient daemon and exactly &lt;em&gt;wrong&lt;/em&gt; for a tool where a failure should stop the line and get a human&amp;rsquo;s attention. The framework doesn&amp;rsquo;t make that call for you. It gives you the supervisor and lets you point it at the services that genuinely want it.&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line
&lt;/h2&gt;&lt;p&gt;A surprising number of CLI tools become long-running services the day they grow a &lt;code&gt;serve&lt;/code&gt; command, and the day they do, they need coordinated startup, graceful ordered shutdown, real liveness and readiness probes, and a considered answer to a service falling over. That&amp;rsquo;s a few hundred lines of fiddly, easy-to-get-wrong plumbing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pkg/controls&lt;/code&gt; provides it: a &lt;code&gt;Controller&lt;/code&gt; over &lt;code&gt;Controllable&lt;/code&gt; services with shared channels and deadline-bounded graceful shutdown, separate Kubernetes-style liveness and readiness probes, and an opt-in supervisor that restarts failed services with exponential backoff and a restart ceiling. Your tool can start as a command and grow into a daemon without that growth turning into a rewrite.&lt;/p&gt;
&lt;p&gt;CLI-first, but not stuck there.&lt;/p&gt;</description></item><item><title>Middleware for CLI commands, not just web servers</title><link>https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/</link><pubDate>Tue, 24 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/</guid><description>&lt;img src="https://blog-570662.gitlab.io/middleware-for-cli-commands-not-just-web-servers/cover-middleware-for-cli-commands-not-just-web-servers.png" alt="Featured image of post Middleware for CLI commands, not just web servers" /&gt;&lt;p&gt;Every CLI tool past a certain size grows a category of logic that doesn&amp;rsquo;t really belong to any one command, and yet has to happen for loads of them. Timing. An auth check. Panic recovery, so a crash becomes a clean error instead of a stack-trace all over someone&amp;rsquo;s terminal. A log line saying the command started and how it finished.&lt;/p&gt;
&lt;p&gt;Web frameworks sorted this out years ago. CLIs, for some reason, mostly still copy-paste it around.&lt;/p&gt;
&lt;h2 id="the-logic-that-belongs-to-no-single-command"&gt;The logic that belongs to no single command
&lt;/h2&gt;&lt;p&gt;That category of logic doesn&amp;rsquo;t belong to any one command, yet needs to happen for many of them. Time how long the command took. Check the user is authenticated before a command that needs it. Recover from a panic so a crash becomes a clean error rather than a stack-trace vomited across the screen. Log that the command started and how it ended.&lt;/p&gt;
&lt;p&gt;None of that is the command&amp;rsquo;s &lt;em&gt;job&lt;/em&gt;. The &lt;code&gt;deploy&lt;/code&gt; command&amp;rsquo;s job is to deploy. But timing and recovery and auth still have to happen around it, and around &lt;code&gt;build&lt;/code&gt;, and around &lt;code&gt;sync&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Put that logic &lt;em&gt;inside&lt;/em&gt; each command&amp;rsquo;s &lt;code&gt;RunE&lt;/code&gt; and you&amp;rsquo;ve copied the same six lines into thirty functions, which means thirty places to fix when the logging format changes and thirty chances to forget one of them. Cross-cutting concerns copied by hand don&amp;rsquo;t stay consistent. They drift, every time.&lt;/p&gt;
&lt;h2 id="web-frameworks-already-solved-this"&gt;Web frameworks already solved this
&lt;/h2&gt;&lt;p&gt;This is not a new problem. It&amp;rsquo;s about the oldest problem in web frameworks, and they settled on an answer a long time ago: middleware. Gin has it, Echo has it, every HTTP stack you&amp;rsquo;ve ever touched has it. A middleware is a wrapper that sits around a handler, runs its cross-cutting logic, and calls through to the handler in the middle.&lt;/p&gt;
&lt;p&gt;A CLI command is, structurally, just a handler too. So go-tool-base brings the same pattern to the Cobra command tree, with the same functional &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/setup/middleware.go#L14" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Chain&lt;/code&gt;&lt;/a&gt; shape:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Middleware&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A middleware receives the &lt;em&gt;next&lt;/em&gt; handler in the chain and returns a new handler that wraps it. You compose a stack of them, and each command&amp;rsquo;s real &lt;code&gt;RunE&lt;/code&gt; runs in the middle of the onion. Write the timing logic once, as one middleware, and every command in the chain is timed. Change the log format once and all thirty commands change with it, because there was only ever one copy. (The &amp;ldquo;write it once, in a place where everyone inherits it&amp;rdquo; drum again, which I will keep banging until the series runs out.)&lt;/p&gt;
&lt;h2 id="but-cobra-already-has-prerun"&gt;&amp;ldquo;But Cobra already has PreRun&amp;rdquo;
&lt;/h2&gt;&lt;p&gt;It does, and this is the objection worth answering properly, because Cobra ships &lt;code&gt;PersistentPreRun&lt;/code&gt; and &lt;code&gt;PreRun&lt;/code&gt; hooks and they look, at a glance, like they cover this.&lt;/p&gt;
&lt;p&gt;They don&amp;rsquo;t, and the reason is structural. A &lt;code&gt;PreRun&lt;/code&gt; hook is a thing that happens &lt;em&gt;before&lt;/em&gt; the command. That&amp;rsquo;s all it is. It can&amp;rsquo;t run anything &lt;em&gt;after&lt;/em&gt;. It can&amp;rsquo;t wrap the command in a &lt;code&gt;defer&lt;/code&gt;. It can&amp;rsquo;t catch a panic the command throws. It can&amp;rsquo;t measure how long the command took, because measuring a duration needs a start point &lt;em&gt;and&lt;/em&gt; an end point, and the hook only owns the start.&lt;/p&gt;
&lt;p&gt;A middleware wraps the &lt;em&gt;entire&lt;/em&gt; execution. Because it&amp;rsquo;s a function that calls &lt;code&gt;next()&lt;/code&gt; in its own body, it straddles the command (with the handler signature abbreviated to &lt;code&gt;HandlerFunc&lt;/code&gt; here for readability):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;TimingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HandlerFunc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HandlerFunc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// the command runs here&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;command finished&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;took&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before, after, and around. A recovery middleware can put a &lt;code&gt;defer recover()&lt;/code&gt; in place that a &lt;code&gt;PreRun&lt;/code&gt; hook structurally cannot. An auth middleware can check a condition and return an error &lt;em&gt;instead of calling &lt;code&gt;next()&lt;/code&gt; at all&lt;/em&gt;, refusing to let the command run in the first place. &lt;code&gt;PreRun&lt;/code&gt; can&amp;rsquo;t veto the command; it runs, and then the command runs regardless.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PreRun&lt;/code&gt; is a notification that the command is about to happen. Middleware is control over whether and how it happens. For genuine cross-cutting concerns you need the second thing, not the first.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;Timing, auth, recovery and logging are cross-cutting concerns: necessary for many commands, owned by none. Hand-copied into every &lt;code&gt;RunE&lt;/code&gt;, they drift out of sync. Web frameworks fixed this with middleware years ago, and a CLI command is structurally just another handler.&lt;/p&gt;
&lt;p&gt;go-tool-base brings the functional Chain middleware pattern to the Cobra command tree. A middleware wraps a command&amp;rsquo;s whole execution, so it acts before and after and can decide whether the command runs at all&amp;hellip; strictly more than Cobra&amp;rsquo;s &lt;code&gt;PreRun&lt;/code&gt; hooks, which only fire beforehand and can&amp;rsquo;t wrap, recover, time, or veto. Write the concern once, wrap the chain, and every command inherits it consistently.&lt;/p&gt;</description></item><item><title>A logging interface that doesn't leak its backend</title><link>https://blog-570662.gitlab.io/a-logging-interface-that-doesnt-leak-its-backend/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-logging-interface-that-doesnt-leak-its-backend/</guid><description>&lt;img src="https://blog-570662.gitlab.io/a-logging-interface-that-doesnt-leak-its-backend/cover-a-logging-interface-that-doesnt-leak-its-backend.png" alt="Featured image of post A logging interface that doesn't leak its backend" /&gt;&lt;p&gt;The same tool, in two different lives, wants two completely different kinds of log.&lt;/p&gt;
&lt;p&gt;On my laptop I want logs I can actually read: colour, alignment, friendly timestamps. The very same tool running as a daemon in a container wants none of that. It wants structured JSON, one object a line, ready for a log aggregator to swallow. And in a test I want the logger to shut up entirely. The interesting question is what it costs you to move between the three.&lt;/p&gt;
&lt;h2 id="the-same-tool-wants-different-logs"&gt;The same tool wants different logs
&lt;/h2&gt;&lt;p&gt;On a developer&amp;rsquo;s machine the tool is a CLI. You want logs that are pleasant to read in a terminal: colour, alignment, human-friendly timestamps. The charmbracelet logger does that beautifully.&lt;/p&gt;
&lt;p&gt;Then the very same tool grows a &lt;code&gt;serve&lt;/code&gt; command and gets deployed as a daemon in a container. Now coloured terminal output is worse than useless. The log aggregator wants structured JSON, one object per line, machine-parseable. &lt;code&gt;slog&lt;/code&gt; does that.&lt;/p&gt;
&lt;p&gt;And in tests you want neither. You want the logger to exist, satisfy the interface, and stay completely silent.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s three different logging backends, wanted by one tool across three different lives. So what does switching between them actually cost?&lt;/p&gt;
&lt;h2 id="what-it-costs-depends-on-what-your-packages-imported"&gt;What it costs depends on what your packages imported
&lt;/h2&gt;&lt;p&gt;If your packages import a concrete logger, if &lt;code&gt;pkg/config&lt;/code&gt; and &lt;code&gt;pkg/setup&lt;/code&gt; and twenty others each have &lt;code&gt;import &amp;quot;github.com/charmbracelet/log&amp;quot;&lt;/code&gt; and take a &lt;code&gt;*log.Logger&lt;/code&gt;, then the backend is welded into the entire codebase. Switching to JSON for the container build means editing the import and the parameter type in every single one of those packages. The backend has &lt;em&gt;leaked&lt;/em&gt;. A detail that should have been one decision has become a property of a hundred files.&lt;/p&gt;
&lt;p&gt;go-tool-base doesn&amp;rsquo;t let it leak. Every package in the framework accepts a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/logger/logger.go#L16" target="_blank" rel="noopener"
 &gt;&lt;code&gt;logger.Logger&lt;/code&gt;&lt;/a&gt;, an interface, and nothing else. No package anywhere imports a concrete logging library. A package states, in its types, &amp;ldquo;I need something I can log through&amp;rdquo;, and stops right there. It has no idea, and no way to find out, what&amp;rsquo;s actually on the other end.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// what every package depends on&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The backend gets chosen once, at the top, when the tool builds its &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt;. It travels down to every package as the interface, through the &lt;code&gt;Props&lt;/code&gt; container. The packages underneath never see the concrete type, so the concrete type can change without a single one of them noticing. (There&amp;rsquo;s that &amp;ldquo;decide it once, in one place&amp;rdquo; theme again. I did warn you it runs through everything.)&lt;/p&gt;
&lt;h2 id="three-backends-and-the-swap-is-one-line"&gt;Three backends, and the swap is one line
&lt;/h2&gt;&lt;p&gt;go-tool-base ships three implementations of that interface:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;charmbracelet&lt;/strong&gt; (&lt;code&gt;logger.NewCharm(w, opts...)&lt;/code&gt;). Coloured, styled, for humans at a terminal. The CLI default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;slog JSON&lt;/strong&gt;, a &lt;code&gt;slog&lt;/code&gt;-backed backend emitting structured JSON, for daemons and containers feeding a log aggregator.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;noop&lt;/strong&gt;, which does precisely nothing, for tests that want a real &lt;code&gt;Logger&lt;/code&gt; and total silence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Switching the tool from a friendly CLI logger to container-ready JSON is a change to the one line in &lt;code&gt;main()&lt;/code&gt; that constructs the logger. That&amp;rsquo;s the lot. &lt;code&gt;pkg/config&lt;/code&gt; doesn&amp;rsquo;t change. &lt;code&gt;pkg/setup&lt;/code&gt; doesn&amp;rsquo;t change. None of the twenty packages change, because none of them ever knew which backend they had. The decision was always one line; the interface is what &lt;em&gt;kept&lt;/em&gt; it one line.&lt;/p&gt;
&lt;p&gt;The noop backend deserves its own mention, because it&amp;rsquo;s the one people underrate. A test for a command shouldn&amp;rsquo;t be spraying log output all over the test run, but the command still needs a non-nil &lt;code&gt;Logger&lt;/code&gt; to function. &lt;code&gt;logger.NewNoop()&lt;/code&gt; gives you exactly that: interface satisfied, output binned, test quiet. And because it&amp;rsquo;s just another implementation of the same interface, no test needs any special logging machinery. It passes a different backend, exactly the way the container build does.&lt;/p&gt;
&lt;h2 id="the-general-shape"&gt;The general shape
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s nothing exotic going on here. It&amp;rsquo;s &amp;ldquo;depend on interfaces, not implementations&amp;rdquo;, which every Go developer has had drilled into them at some point. The bit worth holding onto is &lt;em&gt;where&lt;/em&gt; the rule actually pays out, and it&amp;rsquo;s at the seams between a stable core and a detail you know full well you&amp;rsquo;ll want to vary.&lt;/p&gt;
&lt;p&gt;A logging backend is exactly such a detail. You will want it different in a terminal, in a container, and in a test. So the thing your code depends on has to be the interface, and the concrete backend has to be chosen at one well-known point and nowhere else. Get that boundary right and &amp;ldquo;we need JSON logs in production&amp;rdquo; is a one-line change. Get it wrong and it&amp;rsquo;s a refactor and a bad afternoon.&lt;/p&gt;
&lt;h2 id="what-it-comes-down-to"&gt;What it comes down to
&lt;/h2&gt;&lt;p&gt;One tool legitimately wants three different logging backends across its life: coloured output in a terminal, structured JSON in a container, silence in a test. The cost of moving between them is decided entirely by whether your packages imported a concrete logger or an interface.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s packages depend only on &lt;code&gt;logger.Logger&lt;/code&gt;, never a backend. Three implementations ship (charmbracelet, &lt;code&gt;slog&lt;/code&gt; JSON, noop) and the backend is chosen once, in &lt;code&gt;main()&lt;/code&gt;, then carried everywhere as the interface through &lt;code&gt;Props&lt;/code&gt;. Switching is one line at the top, because the detail was never allowed to leak into the hundred files below it.&lt;/p&gt;</description></item><item><title>Errors that tell the user what to do next</title><link>https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/</link><pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/</guid><description>&lt;img src="https://blog-570662.gitlab.io/errors-that-tell-the-user-what-to-do-next/cover-errors-that-tell-the-user-what-to-do-next.png" alt="Featured image of post Errors that tell the user what to do next" /&gt;&lt;p&gt;Here&amp;rsquo;s an error message I&amp;rsquo;ve been on the receiving end of more times than I&amp;rsquo;d care to count:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: failed to read config file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;True. Also completely useless! I now know something is broken and I haven&amp;rsquo;t the faintest idea what to do about it. Which file? Why couldn&amp;rsquo;t it be read? Should I create it, run some &lt;code&gt;init&lt;/code&gt; command, fix a permission, set an environment variable? The message states the problem and then abandons me at it, rather like a sat-nav cheerfully announcing &amp;ldquo;you have arrived&amp;rdquo; in the middle of a motorway.&lt;/p&gt;
&lt;h2 id="a-message-is-not-a-fix"&gt;A message is not a fix
&lt;/h2&gt;&lt;p&gt;The instinct, the moment you notice this, is to go and write a &lt;em&gt;better message&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;~/.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mytool&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mytool init&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;MYTOOL_CONFIG&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Better for the human, no question. But look at what you&amp;rsquo;ve just done to the error as a &lt;em&gt;value&lt;/em&gt;. The recovery advice is now welded into the error string. Any code that wants to ask &amp;ldquo;is this the config-missing error?&amp;rdquo; is reduced to substring-matching English prose. Reword the advice and you break the check. So you&amp;rsquo;ve helped the user and quietly sabotaged the program at the same time, because you&amp;rsquo;ve made one poor little string do two completely incompatible jobs&amp;hellip; being a stable identity for code, and being friendly guidance for people.&lt;/p&gt;
&lt;h2 id="why-i-changed-error-libraries"&gt;Why I changed error libraries
&lt;/h2&gt;&lt;p&gt;go-tool-base started out on &lt;code&gt;github.com/go-errors/errors&lt;/code&gt;. It&amp;rsquo;s a perfectly fine library and it gave us stack traces. What it didn&amp;rsquo;t give us was any way to attach human guidance to an error without shoving it into the message string. So the codebase did exactly the daft thing I just described: multi-line suggestion text baked straight into &lt;code&gt;errors.Errorf&lt;/code&gt; calls, user-facing content and programmatic identity all mashed into one value.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the whole reason for the migration to &lt;code&gt;github.com/cockroachdb/errors&lt;/code&gt;. Not novelty, and not because I fancied a weekend of find-and-replace. One specific capability: &lt;code&gt;cockroachdb/errors&lt;/code&gt; lets you &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/chat/baseurl.go#L113" target="_blank" rel="noopener"
 &gt;attach a &lt;strong&gt;hint&lt;/strong&gt;&lt;/a&gt; to an error as a separate, structured field.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;failed to read config file&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Run &amp;#39;mytool init&amp;#39; to create one, or set MYTOOL_CONFIG to point at an existing file.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now there are two things, cleanly apart. &lt;code&gt;errors.New(&amp;quot;failed to read config file&amp;quot;)&lt;/code&gt; is the &lt;em&gt;identity&lt;/em&gt;&amp;hellip; stable, matchable, the program&amp;rsquo;s handle on the error. The hint is the &lt;em&gt;guidance&lt;/em&gt;&amp;hellip; for the human, and rewordable as much as you like without breaking a single check, because no check ever looks at it. &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt; work properly through every wrapper layer, so code matches on identity and never has to read prose.&lt;/p&gt;
&lt;p&gt;The migration brought a few other things worth having. Stack traces print with a plain &lt;code&gt;%+v&lt;/code&gt; instead of a type assertion. Errors can carry structured, machine-readable metadata. Multiple errors from concurrent work can be combined as a first-class value. But the hint is the one that actually changed the user&amp;rsquo;s day, because the hint is the recovery step, stored where it belongs.&lt;/p&gt;
&lt;h2 id="one-door-out-and-it-knows-where-the-help-is"&gt;One door out, and it knows where the help is
&lt;/h2&gt;&lt;p&gt;Separating the hint is only half of it. The other half is making sure those hints actually reach the user, every time, and that comes down to having a single way out.&lt;/p&gt;
&lt;p&gt;Every go-tool-base command returns its errors the idiomatic Cobra way, through &lt;code&gt;RunE&lt;/code&gt;. They all funnel into one &lt;code&gt;Execute()&lt;/code&gt; wrapper at the root, which routes every error (runtime failure, flag parse error, pre-run failure) through one &lt;code&gt;ErrorHandler&lt;/code&gt;. One door out. So error &lt;em&gt;presentation&lt;/em&gt; gets decided in exactly one place, and no command can render an error differently from the command sat next to it.&lt;/p&gt;
&lt;p&gt;And because there&amp;rsquo;s one handler, it can pull off something the individual commands never could. The framework knows your tool&amp;rsquo;s metadata, including its configured support channel, be it a Slack workspace or a Teams channel. So the error handler can finish a fatal error not just with the &lt;em&gt;what&lt;/em&gt; and the recovery hint, but with &lt;em&gt;where to go if the hint didn&amp;rsquo;t help&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mytool init&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;set&lt;/span&gt; &lt;span class="n"&gt;MYTOOL_CONFIG&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;Still&lt;/span&gt; &lt;span class="n"&gt;stuck&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="c1"&gt;#mytool-support on Slack.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The user is never left at a dead end. The error tells them what broke, the hint tells them the most likely fix, and if that&amp;rsquo;s still not enough the handler tells them which door to go and knock on. A failure becomes a signpost instead of a full stop.&lt;/p&gt;
&lt;h2 id="the-short-version"&gt;The short version
&lt;/h2&gt;&lt;p&gt;An error that only reports what went wrong leaves the user stranded, and the obvious fix (writing the recovery advice into the message) quietly wrecks the error as a value, because now your code has to substring-match prose just to work out what it&amp;rsquo;s looking at.&lt;/p&gt;
&lt;p&gt;go-tool-base moved from &lt;code&gt;go-errors&lt;/code&gt; to &lt;code&gt;cockroachdb/errors&lt;/code&gt; to get hints: a structured, separate field for human guidance that leaves the error&amp;rsquo;s identity clean for &lt;code&gt;errors.Is&lt;/code&gt; and &lt;code&gt;errors.As&lt;/code&gt;. Every command&amp;rsquo;s errors leave through one &lt;code&gt;Execute()&lt;/code&gt; wrapper and one &lt;code&gt;ErrorHandler&lt;/code&gt;, so presentation stays consistent, and because that handler knows the tool&amp;rsquo;s support channel it can point a stuck user at real help.&lt;/p&gt;
&lt;p&gt;State the problem for the program. Give the fix to the human. And for pity&amp;rsquo;s sake, keep the two in different fields.&lt;/p&gt;</description></item><item><title>Many embedded filesystems, one merged view</title><link>https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/</guid><description>&lt;img src="https://blog-570662.gitlab.io/many-embedded-filesystems-one-merged-view/cover-many-embedded-filesystems-one-merged-view.png" alt="Featured image of post Many embedded filesystems, one merged view" /&gt;&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;embed&lt;/code&gt; package is one of those features that makes you slightly giddy the first time you use it. One &lt;code&gt;//go:embed&lt;/code&gt; directive and your default config, your templates, your docs are all baked into the binary. The tool just works the moment it&amp;rsquo;s installed, with nothing external to lose or forget to ship.&lt;/p&gt;
&lt;p&gt;And then you go and build something modular on top of it, and you discover the catch nobody warned you about.&lt;/p&gt;
&lt;h2 id="embedfs-is-an-island"&gt;&lt;code&gt;embed.FS&lt;/code&gt; is an island
&lt;/h2&gt;&lt;p&gt;An &lt;code&gt;embed.FS&lt;/code&gt; has a property that&amp;rsquo;s easy to miss until it bites: it&amp;rsquo;s local to the package that declared it. The &lt;code&gt;//go:embed&lt;/code&gt; directive can only see files at or below its own source file. So in any project bigger than a toy, you don&amp;rsquo;t have &lt;em&gt;an&lt;/em&gt; embedded filesystem. You have many. The root package embeds one. Each feature, each subcommand that ships its own templates or defaults, embeds another. They&amp;rsquo;re islands, one per package, and Go gives you no native way to make them behave as a whole.&lt;/p&gt;
&lt;p&gt;For most files that&amp;rsquo;s perfectly fine. A feature&amp;rsquo;s templates can stay on the feature&amp;rsquo;s island; nothing else needs them.&lt;/p&gt;
&lt;p&gt;It stops being fine the moment features need to contribute to something shared.&lt;/p&gt;
&lt;h2 id="the-shared-config-problem"&gt;The shared-config problem
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the case that forces the issue. A go-tool-base tool has a global &lt;code&gt;config.yaml&lt;/code&gt; of defaults, embedded at the root. Now you add a feature, and that feature has its own configuration keys, with their own sensible defaults.&lt;/p&gt;
&lt;p&gt;Where do those defaults go?&lt;/p&gt;
&lt;p&gt;The naive answer is: edit the root &lt;code&gt;config.yaml&lt;/code&gt; and add the feature&amp;rsquo;s section. And that&amp;rsquo;s a genuinely bad answer, because it inverts the dependency. The root config now has to know about every feature. Add a feature, edit the centre. Remove one, edit the centre again. The central file becomes a pinch point that every feature has to reach into, and a modular architecture where you can&amp;rsquo;t add a module without editing the core isn&amp;rsquo;t really modular at all&amp;hellip; it just has more files.&lt;/p&gt;
&lt;p&gt;What you actually want is for the feature to ship its own slice of default config, on its own island, and for the global config the tool reads to somehow already contain it. The feature contributes; the centre doesn&amp;rsquo;t budge.&lt;/p&gt;
&lt;h2 id="propsassets-merge-the-islands"&gt;&lt;code&gt;props.Assets&lt;/code&gt;: merge the islands
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s the job of &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/props/assets.go#L30" target="_blank" rel="noopener"
 &gt;&lt;code&gt;props.Assets&lt;/code&gt;&lt;/a&gt;. (Yes, it lives on &lt;a class="link" href="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/" &gt;Props&lt;/a&gt;, the load-bearing container I keep going on about. Most of the good stuff does.) It&amp;rsquo;s a layer that implements the standard &lt;code&gt;fs.FS&lt;/code&gt; interface, and into it you &lt;code&gt;Register&lt;/code&gt; each &lt;code&gt;embed.FS&lt;/code&gt; under a name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// root main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewAssets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AssetMap&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;root&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// a feature&amp;#39;s command constructor&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;//go:embed assets/*&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;feature&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now &lt;code&gt;Props&lt;/code&gt; carries one &lt;code&gt;Assets&lt;/code&gt; value that represents all the islands as a single filesystem. The root&amp;rsquo;s files and every registered feature&amp;rsquo;s files, addressable through one &lt;code&gt;fs.FS&lt;/code&gt;. Each registration is named, so the islands stay individually identifiable, but they read as one.&lt;/p&gt;
&lt;p&gt;That alone solves the addressing problem. The genuinely clever part is what happens for structured files.&lt;/p&gt;
&lt;h2 id="opening-a-file-that-exists-in-several-places"&gt;Opening a file that exists in several places
&lt;/h2&gt;&lt;p&gt;When you &lt;code&gt;Open&lt;/code&gt; a path through &lt;code&gt;props.Assets&lt;/code&gt; and that path has a structured extension it recognises (&lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.toml&lt;/code&gt;, &lt;code&gt;.csv&lt;/code&gt;, and a few more) it doesn&amp;rsquo;t simply return the first match it stumbles across. It does this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; It finds every instance of that path, across every registered filesystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parsing.&lt;/strong&gt; It unmarshals each one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Merging.&lt;/strong&gt; It deep-merges the parsed data, using &lt;code&gt;mergo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-serialisation.&lt;/strong&gt; It hands you back a single &lt;code&gt;fs.File&lt;/code&gt; whose contents are the combined, merged result.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So picture the shared-config problem again, only solved this time. The root ships a &lt;code&gt;config.yaml&lt;/code&gt; with the base defaults. Each feature ships a &lt;code&gt;config.yaml&lt;/code&gt; on its own island carrying only its own keys. Nobody edits anybody else&amp;rsquo;s file. When the &lt;code&gt;init&lt;/code&gt; command opens &lt;code&gt;config.yaml&lt;/code&gt; through &lt;code&gt;props.Assets&lt;/code&gt;, it doesn&amp;rsquo;t get the root&amp;rsquo;s copy. It gets the deep-merge of the root&amp;rsquo;s copy and every registered feature&amp;rsquo;s copy: one &lt;code&gt;config.yaml&lt;/code&gt; that contains every default in the tool, assembled at runtime from contributions that never knew about each other.&lt;/p&gt;
&lt;p&gt;A feature contributes its defaults simply by existing and registering. The centre never changes. That&amp;rsquo;s the modular property the naive approach couldn&amp;rsquo;t give you, and it generalises well beyond config&amp;hellip; the same merge applies to a shared &lt;code&gt;commands.csv&lt;/code&gt;, or any structured file features want to add rows or keys to.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s also a &lt;code&gt;Mount&lt;/code&gt; method for attaching an arbitrary &lt;code&gt;fs.FS&lt;/code&gt; at a virtual path, which is handy for surfacing something external (a temp directory, say) as part of the same tree. But the structured merge is the feature that really earns &lt;code&gt;Assets&lt;/code&gt; its place.&lt;/p&gt;
&lt;h2 id="boiling-it-down"&gt;Boiling it down
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;embed.FS&lt;/code&gt; is per-package by design, so a modular CLI ends up with many embedded filesystems, one island per feature. Most of the time that&amp;rsquo;s fine. It fails specifically when features need to contribute to a shared resource like the global &lt;code&gt;config.yaml&lt;/code&gt;, because the naive fix forces every feature to reach in and edit a central file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;props.Assets&lt;/code&gt; merges all the registered islands into a single &lt;code&gt;fs.FS&lt;/code&gt;, and for structured files it goes further: opening a &lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt; or &lt;code&gt;.csv&lt;/code&gt; discovers every copy across every island, deep-merges them, and returns the combined whole. A feature drops its own defaults onto its own island, registers, and the merged config the tool reads already includes them. Contribution without coupling, which is rather the whole point of being modular in the first place.&lt;/p&gt;</description></item><item><title>Props: the container that does the heavy lifting</title><link>https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/</link><pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/</guid><description>&lt;img src="https://blog-570662.gitlab.io/props-the-container-that-does-the-heavy-lifting/cover-props.png" alt="Featured image of post Props: the container that does the heavy lifting" /&gt;&lt;p&gt;I name-dropped &lt;code&gt;Props&lt;/code&gt; back in the &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;introduction&lt;/a&gt; and then rather glossed over it, which was a bit unfair of me, because it&amp;rsquo;s the single most important design decision in the whole framework. So let&amp;rsquo;s give it the attention it actually deserves.&lt;/p&gt;
&lt;p&gt;And the best place to start, oddly enough, is the name.&lt;/p&gt;
&lt;h2 id="start-with-the-name"&gt;Start with the name
&lt;/h2&gt;&lt;p&gt;The container at the centre of go-tool-base is called &lt;code&gt;Props&lt;/code&gt;, and the name is doing real work, so we&amp;rsquo;ll start there.&lt;/p&gt;
&lt;p&gt;It is not short for &amp;ldquo;properties&amp;rdquo;, though it does hold a few. A &lt;em&gt;prop&lt;/em&gt; is the heavy timber or steel beam that stops a structure quietly collapsing in on itself. And for anyone who follows the rugby: a prop is the position in the scrum, the broad-shouldered forward whose entire job is to provide structural support so everyone else can get on with the game.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the design brief, in a single word. &lt;code&gt;Props&lt;/code&gt; is not where the clever, flashy work happens. It scores no tries. It&amp;rsquo;s the unglamorous, load-bearing thing that holds the framework up so that your actual command logic gets to be the interesting part. Understand the name and you understand what the struct is &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="what-it-carries"&gt;What it carries
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/props/props.go#L15" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Props&lt;/code&gt;&lt;/a&gt; is the single object passed to every command constructor in a go-tool-base tool. It holds the dependencies a command might need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Tool&lt;/code&gt;, metadata about the CLI (name, summary, release source).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Logger&lt;/code&gt;, the logging abstraction.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Config&lt;/code&gt;, the loaded configuration container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FS&lt;/code&gt;, a filesystem abstraction (&lt;code&gt;afero&lt;/code&gt;), so a command never touches the real disk directly.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Assets&lt;/code&gt;, the embedded-resource manager.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Version&lt;/code&gt;, build information.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ErrorHandler&lt;/code&gt;, the centralised error reporter.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Collector&lt;/code&gt;, the telemetry collector (always present, a no-op when telemetry is off).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A command constructor&amp;rsquo;s signature is, accordingly, boring on purpose:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;NewCmdExample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cobra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One parameter. Everything the command could possibly need is reachable through it. No globals, no &lt;code&gt;init()&lt;/code&gt;-time wiring, no twelve-argument constructor that quietly grows a thirteenth argument next month.&lt;/p&gt;
&lt;h2 id="why-a-struct-and-not-contextcontext"&gt;Why a struct, and not &lt;code&gt;context.Context&lt;/code&gt;
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s the design decision I actually want to defend, because it&amp;rsquo;s the one Go developers tend to raise an eyebrow at. Go already has a well-known way to carry things through a call tree: &lt;code&gt;context.Context&lt;/code&gt;. So why not just put the logger and the config in the context and pass that around?&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;context.Context&lt;/code&gt; carries its values as &lt;code&gt;interface{}&lt;/code&gt;, and that&amp;rsquo;s the wrong trade for &lt;em&gt;dependencies&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Pull a dependency out of a context and you get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;logger&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// a runtime type assertion&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That one line has two separate ways to hurt you. The key is a bare string, so a typo compiles perfectly happily and then fails at runtime. The type assertion is unchecked, so if the wrong thing is sitting under that key, your tool panics in front of a user. Neither failure is visible to the compiler. Neither is visible to your IDE. You find out when it breaks, which is to say at the worst possible time.&lt;/p&gt;
&lt;p&gt;Pull the same dependency out of &lt;code&gt;Props&lt;/code&gt; and you get this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;starting&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// a field access&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;p.Logger&lt;/code&gt; is a typed field. If it doesn&amp;rsquo;t exist, or you&amp;rsquo;ve used it wrong, the code simply doesn&amp;rsquo;t compile. Your IDE autocompletes it. Refactor the &lt;code&gt;Logger&lt;/code&gt; interface and every misuse lights up at build time. There&amp;rsquo;s no runtime type assertion, because there&amp;rsquo;s no &lt;code&gt;interface{}&lt;/code&gt; to assert from in the first place.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;context.Context&lt;/code&gt; is the right tool for what it was designed for: cancellation, deadlines, request-scoped signals that genuinely cross API boundaries. It&amp;rsquo;s the wrong tool for &amp;ldquo;here are my program&amp;rsquo;s services&amp;rdquo;, because it trades away the compiler&amp;rsquo;s help for a flexibility you really don&amp;rsquo;t want here. Dependencies should be &lt;em&gt;declared&lt;/em&gt;, somewhere the compiler checks them. &lt;code&gt;Props&lt;/code&gt; is that somewhere.&lt;/p&gt;
&lt;h2 id="what-you-get-back-for-it"&gt;What you get back for it
&lt;/h2&gt;&lt;p&gt;That one decision pays out in three currencies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testability.&lt;/strong&gt; A command is now a pure function of its &lt;code&gt;Props&lt;/code&gt;. To test it, you build a &lt;code&gt;Props&lt;/code&gt; with the doubles you want (an in-memory &lt;code&gt;FS&lt;/code&gt; instead of the real disk, a no-op &lt;code&gt;Logger&lt;/code&gt;, a config you&amp;rsquo;ve populated by hand) and call the constructor. No global state to reset between tests, no monkey-patching, no &lt;code&gt;init()&lt;/code&gt; order to puzzle over. The dependency is an argument, so the test just passes a different one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consistency.&lt;/strong&gt; Cross-cutting changes have exactly one place to happen. When the global &lt;code&gt;--debug&lt;/code&gt; flag flips the log level, it does so on the &lt;code&gt;Logger&lt;/code&gt; inside &lt;code&gt;Props&lt;/code&gt;, and because every command reads its logger from the same &lt;code&gt;Props&lt;/code&gt;, every command gets the new level. No command can drift, because none of them owns its own copy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extensibility.&lt;/strong&gt; Adding a new framework-wide service is just adding a field to one struct. Every command can immediately reach it; none of them needed touching to make it reachable.&lt;/p&gt;
&lt;h2 id="to-sum-up"&gt;To sum up
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Props&lt;/code&gt; is the dependency-injection container at the heart of go-tool-base: one struct, passed to every command, holding the logger, config, filesystem, assets, error handler and tool metadata. It&amp;rsquo;s a concrete struct rather than a &lt;code&gt;context.Context&lt;/code&gt; payload entirely on purpose, because dependencies belong somewhere the compiler can check them, not behind a string key and a hopeful runtime type assertion. That single choice buys you testability, consistency and easy extension.&lt;/p&gt;
&lt;p&gt;The name says it best, really. &lt;code&gt;Props&lt;/code&gt; doesn&amp;rsquo;t score the tries. It&amp;rsquo;s the broad-shouldered thing in the scrum that stops the whole framework folding, so the rest of your code is free to go and play.&lt;/p&gt;</description></item><item><title>Design your whole CLI in one file</title><link>https://blog-570662.gitlab.io/design-your-whole-cli-in-one-file/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/design-your-whole-cli-in-one-file/</guid><description>&lt;img src="https://blog-570662.gitlab.io/design-your-whole-cli-in-one-file/cover-design-your-whole-cli-in-one-file.png" alt="Featured image of post Design your whole CLI in one file" /&gt;&lt;p&gt;Here&amp;rsquo;s a question that sounds trivial and really isn&amp;rsquo;t: where, exactly, does a CLI tool&amp;rsquo;s &lt;em&gt;structure&lt;/em&gt; live? Not the logic of each command&amp;hellip; the structure. Which commands exist, what they&amp;rsquo;re called, which flags they take, what&amp;rsquo;s nested under what.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d never properly thought to ask it until go-tool-base forced me to, and the answer turned out to be a little bit embarrassing.&lt;/p&gt;
&lt;h2 id="where-does-a-clis-structure-actually-live"&gt;Where does a CLI&amp;rsquo;s structure actually live?
&lt;/h2&gt;&lt;p&gt;Picture a CLI tool with twenty commands, some nested under others. In a typical project, where does its structure live? The answer is &amp;ldquo;smeared across the codebase&amp;rdquo;. It&amp;rsquo;s in twenty &lt;code&gt;cmd.go&lt;/code&gt; files. It&amp;rsquo;s in the &lt;code&gt;AddCommand&lt;/code&gt; calls that stitch them together. It&amp;rsquo;s in the flag registrations. To understand the shape of the tool you have to read all of it and assemble the picture in your head, because the picture exists nowhere as a single thing you can point at.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s a strange state of affairs for the single most important design fact about a CLI. The command tree is the tool&amp;rsquo;s interface, it&amp;rsquo;s the thing users actually touch, and yet it hasn&amp;rsquo;t got a home.&lt;/p&gt;
&lt;h2 id="the-manifest-gives-it-one"&gt;The manifest gives it one
&lt;/h2&gt;&lt;p&gt;go-tool-base&amp;rsquo;s generator gives that structure a home: &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/internal/generator/manifest.go#L54" target="_blank" rel="noopener"
 &gt;&lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;&lt;/a&gt;. The manifest is a single readable file describing the command tree. Every command, its name, its short description, its flags, its place in the hierarchy, whether it carries assets or an initialiser. The shape of the whole tool, in one place you can open and read top to bottom.&lt;/p&gt;
&lt;p&gt;And the manifest isn&amp;rsquo;t documentation &lt;em&gt;about&lt;/em&gt; the project. It&amp;rsquo;s the thing the project&amp;rsquo;s wiring is generated &lt;em&gt;from&lt;/em&gt;. When you run &lt;code&gt;regenerate project&lt;/code&gt;, the generator reads the manifest and rebuilds the boilerplate to match it: the command registration, the &lt;code&gt;AddCommand&lt;/code&gt; wiring, the flag definitions. The manifest is the source of truth, and the Go wiring is its output.&lt;/p&gt;
&lt;h2 id="design-first-when-you-want-it"&gt;Design-first, when you want it
&lt;/h2&gt;&lt;p&gt;This unlocks a way of working that the smeared-across-the-codebase approach simply can&amp;rsquo;t offer. You can design the interface first, in the manifest, and let the code follow.&lt;/p&gt;
&lt;p&gt;Want to rename a command? Edit one line in the manifest, run &lt;code&gt;regenerate&lt;/code&gt;, and the rename propagates through every wiring file that ever mentioned it. Want to move a subcommand under a different parent? Change its place in the manifest hierarchy and regenerate. Want to add a flag to three related commands? Add it in the manifest, in three obvious places, and regenerate, instead of going on a little hunting expedition for three flag-registration blocks scattered across the tree.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re editing the tool&amp;rsquo;s interface as a design, in the file whose entire job is to hold that design, and the generator does the mechanical donkey-work of making the code reflect it. The thing you change is the thing that describes the structure. The code is downstream.&lt;/p&gt;
&lt;p&gt;If that shape sounds familiar, it should. It&amp;rsquo;s the same instinct behind spec-driven and test-driven development: write down what the thing should &lt;em&gt;be&lt;/em&gt; before you assemble how it works, and keep that statement of intent as a first-class, living artefact rather than a comment that quietly rots in a corner. The manifest is a spec for your command tree, and &lt;code&gt;regenerate&lt;/code&gt; is what keeps the implementation honest to it.&lt;/p&gt;
&lt;h2 id="it-doesnt-trap-you"&gt;It doesn&amp;rsquo;t trap you
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s an obvious worry about any generated-from-a-manifest system: am I now locked into editing the manifest? What if I just want to open a Go file and write some Go like a normal person?&lt;/p&gt;
&lt;p&gt;You can. The generator is careful not to own everything. It owns the wiring (the registration and the structural boilerplate) and it leaves your command logic well alone. The &lt;code&gt;RunE&lt;/code&gt; function where your command actually does its work is yours; the manifest hasn&amp;rsquo;t got an opinion about it. And the generator tracks the files it produces by content hash, so if you do hand-edit something it generated, regeneration notices and asks before overwriting rather than steamrolling you. That mechanism turned out interesting enough to get &lt;a class="link" href="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/" &gt;its own post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So the manifest is an option, not a cage. Design-first via the manifest when that suits the change. Drop into Go directly when that suits it better. The two stay in sync because regeneration reconciles them, not because one of them has been forbidden.&lt;/p&gt;
&lt;h2 id="pulling-it-together"&gt;Pulling it together
&lt;/h2&gt;&lt;p&gt;A CLI&amp;rsquo;s command tree is its most important design surface, and in most projects it has no single home&amp;hellip; it gets reconstructed in your head from twenty scattered files every time you need to reason about it. go-tool-base gives it one: &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;, a readable description of the whole tree that the generator rebuilds the wiring code from. Edit the manifest, run &lt;code&gt;regenerate&lt;/code&gt;, and the boilerplate follows.&lt;/p&gt;
&lt;p&gt;It makes CLI structure something you design in one place, in the spirit of spec-driven development, while still leaving you free to write Go directly when that&amp;rsquo;s the better tool for the job. The manifest is the spec for your interface. The generator just keeps the code faithful to it.&lt;/p&gt;</description></item><item><title>Scaffolding that respects your edits</title><link>https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/</link><pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/</guid><description>&lt;img src="https://blog-570662.gitlab.io/scaffolding-that-respects-your-edits/cover-scaffolding-that-respects-your-edits.png" alt="Featured image of post Scaffolding that respects your edits" /&gt;&lt;p&gt;When I &lt;a class="link" href="https://blog-570662.gitlab.io/introducing-go-tool-base/" &gt;introduced go-tool-base&lt;/a&gt; I made a passing promise to come back to &amp;ldquo;the generator that won&amp;rsquo;t clobber your edits&amp;rdquo;. This is me keeping it, partly because it&amp;rsquo;s the feature I&amp;rsquo;m quietly most proud of, and partly because it took the most head-scratching of anything to get right.&lt;/p&gt;
&lt;p&gt;The problem it solves is one that every code generator runs into eventually, usually the hard way and usually at the worst possible moment.&lt;/p&gt;
&lt;h2 id="the-generators-awkward-second-act"&gt;The generator&amp;rsquo;s awkward second act
&lt;/h2&gt;&lt;p&gt;A project generator has an easy first act. &lt;code&gt;gtb generate project&lt;/code&gt;, and you&amp;rsquo;ve got a complete, wired, idiomatic Go CLI project. Everyone&amp;rsquo;s happy, me included.&lt;/p&gt;
&lt;p&gt;The second act is the hard one. The framework moves on. A convention changes, a new built-in capability appears, the recommended CI shape shifts. Your project, scaffolded three months ago, is now subtly out of date, and you&amp;rsquo;d quite like the generator to drag it back up to spec.&lt;/p&gt;
&lt;p&gt;Except by now it isn&amp;rsquo;t a fresh scaffold. It&amp;rsquo;s &lt;em&gt;your&lt;/em&gt; project. You tuned the CI workflow. You rewrote the &lt;code&gt;justfile&lt;/code&gt;. You added a stanza to the Dockerfile that took an afternoon and a fair bit of swearing to get right. The generated files and your edited files are one and the same files.&lt;/p&gt;
&lt;p&gt;A naive generator handles this with breathtaking confidence: it regenerates everything from the template and overwrites the lot. Run it once, lose your afternoon. You learn that lesson exactly once and then never run regeneration again, which means the upkeep feature you were sold is dead on arrival. A scaffold you can&amp;rsquo;t safely re-run is just a one-shot &lt;code&gt;cp&lt;/code&gt; with extra steps.&lt;/p&gt;
&lt;h2 id="what-the-generator-needs-to-know"&gt;What the generator needs to know
&lt;/h2&gt;&lt;p&gt;The thing standing between &amp;ldquo;safe to overwrite&amp;rdquo; and &amp;ldquo;absolutely do not&amp;rdquo; is a single fact: has this file changed since the generator last wrote it?&lt;/p&gt;
&lt;p&gt;If it hasn&amp;rsquo;t, the file is still pristine boilerplate and the generator owns it. Overwrite away. If it has, a human has been in there, and the generator must not touch it without asking first.&lt;/p&gt;
&lt;p&gt;The generator can&amp;rsquo;t just eyeball that, of course. It needs a record. So every time &lt;code&gt;gtb generate&lt;/code&gt; writes a file, it computes a SHA-256 of the content and stores it in the project&amp;rsquo;s manifest, &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;, as a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/internal/generator/manifest.go#L54" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Hashes&lt;/code&gt; map&lt;/a&gt; of relative path to hash. The manifest is the generator&amp;rsquo;s memory of the exact bytes it last produced.&lt;/p&gt;
&lt;h2 id="regeneration-becomes-a-three-way-decision"&gt;Regeneration becomes a three-way decision
&lt;/h2&gt;&lt;p&gt;With that record in hand, regeneration stops being &amp;ldquo;overwrite everything&amp;rdquo; and becomes a per-file decision with three branches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The file doesn&amp;rsquo;t exist.&lt;/strong&gt; Easy. Write it, store its hash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The file exists and its current hash matches the manifest.&lt;/strong&gt; It&amp;rsquo;s byte-for-byte what the generator last wrote, so nobody has touched it. The generator owns it outright, regenerates from the template and updates the stored hash. No prompt, no fuss. This is the common case, and it&amp;rsquo;s silent precisely because it&amp;rsquo;s safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The file exists and its hash does &lt;em&gt;not&lt;/em&gt; match.&lt;/strong&gt; Someone has been in there since generation. The generator stops and asks. It will not silently overwrite your hard-won afternoon. You decide: take the new version, or keep yours.&lt;/p&gt;
&lt;p&gt;The detail I&amp;rsquo;m genuinely fond of is what happens when you decline. Declining is non-fatal. Generation carries on with the rest of the files, and the manifest &lt;em&gt;keeps&lt;/em&gt; the file&amp;rsquo;s stored hash rather than dropping it. That matters more than it looks, because it means the file stays tracked. Next time you regenerate, the generator can still tell that file has been modified, and still asks. Skipping a file once doesn&amp;rsquo;t quietly evict it from the generator&amp;rsquo;s awareness forever. It stays a known, watched, customised file across every future run.&lt;/p&gt;
&lt;h2 id="when-you-want-it-to-stop-asking"&gt;When you want it to stop asking
&lt;/h2&gt;&lt;p&gt;Per-file prompting is the right default, but for files you&amp;rsquo;ve &lt;em&gt;permanently&lt;/em&gt; taken ownership of, being asked on every single regeneration is just noise. If you&amp;rsquo;ve rewritten the CI workflows wholesale and you are never, ever going back to the generated version, you don&amp;rsquo;t want a prompt. You want the generator to leave them well alone and not bring it up again.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/internal/generator/ignore.go" target="_blank" rel="noopener"
 &gt;&lt;code&gt;.gtb/ignore&lt;/code&gt;&lt;/a&gt; is for. It sits next to the manifest and takes gitignore-style patterns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# I own the CI workflows now
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.github/workflows/**
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# ...except the release workflow, keep that managed
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;!.github/workflows/release.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# and my build config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;justfile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anything matching is skipped during regeneration with no prompt at all. Patterns evaluate top to bottom and later ones win, so the negation (&lt;code&gt;!&lt;/code&gt;) behaves the way you&amp;rsquo;d expect from &lt;code&gt;.gitignore&lt;/code&gt;: exclude a whole directory, then claw one file back.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a deliberate escalation ladder. Unmodified files are handled silently. Modified files get a prompt. Files you&amp;rsquo;ve formally claimed get total silence. Each rung asks for less of your attention than the last, and you choose how far up to climb, file by file.&lt;/p&gt;
&lt;h2 id="stepping-back"&gt;Stepping back
&lt;/h2&gt;&lt;p&gt;A generator earns its keep twice: once when it scaffolds your project, and then continuously, every time it drags that project back up to the framework&amp;rsquo;s current shape. The second job is worth nothing if regeneration flattens your customisations, because you&amp;rsquo;ll simply stop running it, and who could blame you.&lt;/p&gt;
&lt;p&gt;go-tool-base&amp;rsquo;s generator gets around that by remembering. It hashes every file it writes into &lt;code&gt;.gtb/manifest.yaml&lt;/code&gt;, and on regeneration it re-hashes before overwriting: unchanged files it owns and updates silently, changed files it stops and asks about, and &lt;code&gt;.gtb/ignore&lt;/code&gt; lets you mark files as permanently yours. Skipped files stay tracked, so the generator never loses sight of what you&amp;rsquo;ve made your own.&lt;/p&gt;
&lt;p&gt;The point of a scaffold isn&amp;rsquo;t the first five minutes. It&amp;rsquo;s that you can still run it in month three without holding your breath.&lt;/p&gt;</description></item><item><title>Your CLI is already an AI tool</title><link>https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/</link><pubDate>Thu, 19 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/</guid><description>&lt;img src="https://blog-570662.gitlab.io/your-cli-is-already-an-ai-tool/cover-your-cli-is-already-an-ai-tool.png" alt="Featured image of post Your CLI is already an AI tool" /&gt;&lt;p&gt;&amp;ldquo;Make it work with AI&amp;rdquo; has become one of those requests that lands on a developer&amp;rsquo;s desk with a thud and not much further detail attached. My instinct, the first time, was to brace for a big lump of integration work&amp;hellip; a bespoke adapter for this assistant, another for that one, a treadmill of little wrappers stretching off into the distance.&lt;/p&gt;
&lt;p&gt;Turns out I&amp;rsquo;d already done most of the work. So have you, if your CLI tool is any good. Let me explain what I mean.&lt;/p&gt;
&lt;h2 id="you-already-described-your-capabilities"&gt;You already described your capabilities
&lt;/h2&gt;&lt;p&gt;Stop and think for a second about what a well-built CLI tool actually is. It&amp;rsquo;s a set of named operations, each with a human-readable description, each taking a set of typed, named, documented parameters. You wrote all of that already, because a CLI without it is unusable by &lt;em&gt;people&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Now look at what an AI assistant needs in order to call a tool. A set of named operations. A description of each, so it knows when to reach for them. A typed parameter schema for each, so it knows how to call them.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s the same list! A good CLI is already, structurally, a description of a set of capabilities. The information an AI agent needs isn&amp;rsquo;t extra work you have to go and do. It&amp;rsquo;s work you finished the moment your &lt;code&gt;--help&lt;/code&gt; output was any good.&lt;/p&gt;
&lt;p&gt;The only thing missing is a translator. Something that takes &amp;ldquo;this is a CLI&amp;rdquo; and presents it as &amp;ldquo;this is a set of tools an AI can call&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="mcp-is-that-translator-and-its-a-standard"&gt;MCP is that translator, and it&amp;rsquo;s a standard
&lt;/h2&gt;&lt;p&gt;The temptation, when you want your tool to be AI-usable, is to sit down and write an integration. A little adapter for Claude Desktop. Another for Cursor. Another for whatever turns up next month. Each one a bespoke wrapper, each one a thing to maintain, and the list never stops growing because new assistants keep appearing. That&amp;rsquo;s the treadmill I was bracing for.&lt;/p&gt;
&lt;p&gt;The Model Context Protocol exists to kill that list. MCP is an open standard for how an AI model discovers and calls local tools. Implement it once and your tool works with every assistant that speaks it. Write once, not once-per-client.&lt;/p&gt;
&lt;p&gt;So go-tool-base implements it once, in the framework, for everyone. (That&amp;rsquo;s rather the theme of this whole series, if you hadn&amp;rsquo;t spotted it yet&amp;hellip; do the annoying thing once, properly, in a place where every tool inherits it.)&lt;/p&gt;
&lt;h2 id="the-mcp-command-and-the-mapping-it-does-for-free"&gt;The &lt;code&gt;mcp&lt;/code&gt; command, and the mapping it does for free
&lt;/h2&gt;&lt;p&gt;Every tool built on go-tool-base inherits a built-in &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/props/tool.go#L15" target="_blank" rel="noopener"
 &gt;&lt;code&gt;mcp&lt;/code&gt; command&lt;/a&gt;. Run it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mytool mcp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the tool starts a JSON-RPC server over standard I/O, speaking MCP. That&amp;rsquo;s the whole user-facing surface. One command.&lt;/p&gt;
&lt;p&gt;Behind it, the framework walks your Cobra command tree and maps it straight onto MCP tool definitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each &lt;strong&gt;command&lt;/strong&gt; becomes a &lt;strong&gt;tool&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each command&amp;rsquo;s &lt;strong&gt;short description&lt;/strong&gt; becomes the &lt;strong&gt;tool&amp;rsquo;s description&lt;/strong&gt;, the text the AI reads to decide whether this is the tool it wants.&lt;/li&gt;
&lt;li&gt;Each command&amp;rsquo;s &lt;strong&gt;flags and arguments&lt;/strong&gt; become the tool&amp;rsquo;s &lt;strong&gt;JSON Schema parameters&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s no second schema to write and then keep in sync (and we all know how well &amp;ldquo;keep these two things aligned by hand&amp;rdquo; tends to go). The command tree &lt;em&gt;is&lt;/em&gt; the schema. Add a new command to your CLI and it&amp;rsquo;s a new tool for the agent, automatically, with the description and flags you already gave it. Nobody has to remember to update an MCP manifest, because there&amp;rsquo;s no separate MCP manifest to forget about.&lt;/p&gt;
&lt;h2 id="configuring-an-assistant-to-use-it"&gt;Configuring an assistant to use it
&lt;/h2&gt;&lt;p&gt;On the assistant&amp;rsquo;s side it&amp;rsquo;s just as undramatic. You tell your AI client (Claude Desktop, Cursor, anything MCP-aware) to launch &lt;code&gt;mytool mcp&lt;/code&gt;. From then on the assistant:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Starts your tool in MCP mode when it boots.&lt;/li&gt;
&lt;li&gt;Discovers every command as a callable tool.&lt;/li&gt;
&lt;li&gt;Calls the right one, with the right parameters, when a user&amp;rsquo;s request needs it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your CLI tool has quietly become something the AI can pick up and use, mid-conversation, on its own initiative.&lt;/p&gt;
&lt;h2 id="the-safety-property-worth-noticing"&gt;The safety property worth noticing
&lt;/h2&gt;&lt;p&gt;Now, &amp;ldquo;let an AI run things on my machine&amp;rdquo; is rightly a sentence that makes people nervous. It makes me nervous, and I built the thing. So it&amp;rsquo;s worth noticing the constraint sitting quietly in this design.&lt;/p&gt;
&lt;p&gt;The AI can only call what you defined. The tools it sees are exactly the commands in your tree, and the parameters it can pass are exactly the flags and arguments you declared, validated against the JSON Schema generated from them.&lt;/p&gt;
&lt;p&gt;It can&amp;rsquo;t invent a command. It can&amp;rsquo;t pass a parameter you never defined. The boundary of what the agent can do is the boundary of what your CLI does, and you drew that boundary already, back when you built the tool. Exposing the CLI over MCP doesn&amp;rsquo;t widen the surface one inch. It just makes the existing surface reachable. The AI isn&amp;rsquo;t running &lt;em&gt;things&lt;/em&gt;. It&amp;rsquo;s running &lt;em&gt;your commands&lt;/em&gt;, the ones you wrote, tested and shipped, and nothing else.&lt;/p&gt;
&lt;h2 id="the-gist"&gt;The gist
&lt;/h2&gt;&lt;p&gt;A CLI tool, built properly, is already a structured description of a set of capabilities: named operations, descriptions, typed parameters. Which is also exactly what an AI agent needs in order to call a tool. The gap between the two is only a translator, and writing a bespoke one per assistant is a treadmill you don&amp;rsquo;t need to step onto.&lt;/p&gt;
&lt;p&gt;go-tool-base puts the translator in the framework. Every tool gets an &lt;code&gt;mcp&lt;/code&gt; command that serves the command tree over the Model Context Protocol&amp;hellip; commands become tools, descriptions become descriptions, flags become JSON Schema parameters, with no second schema to maintain. Point any MCP-aware assistant at it and your CLI is an agent-callable tool, bounded to exactly the commands you shipped.&lt;/p&gt;
&lt;p&gt;You did the hard part when you built a good CLI. MCP just opens the door you&amp;rsquo;d already framed.&lt;/p&gt;</description></item><item><title>go-tool-base: I got tired of reinventing the wheel</title><link>https://blog-570662.gitlab.io/introducing-go-tool-base/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/introducing-go-tool-base/</guid><description>&lt;img src="https://blog-570662.gitlab.io/introducing-go-tool-base/cover-introducing-go-tool-base.png" alt="Featured image of post go-tool-base: I got tired of reinventing the wheel" /&gt;&lt;p&gt;If you&amp;rsquo;ve written more than two or three command-line tools in Go, you&amp;rsquo;ll recognise the shape of the first afternoon. I certainly do! You reach for &lt;a class="link" href="https://github.com/spf13/cobra" target="_blank" rel="noopener"
 &gt;Cobra&lt;/a&gt; for the command tree, &lt;a class="link" href="https://github.com/spf13/viper" target="_blank" rel="noopener"
 &gt;Viper&lt;/a&gt; for config, and then you start the part nobody ever puts in the README&amp;hellip; the plumbing.&lt;/p&gt;
&lt;p&gt;Where does config live? A file, an env var, an embedded default? In what order do they override each other? How does the tool tell the user there&amp;rsquo;s a newer version, and how does it actually update itself? What does logging look like, and is it the same logging the next tool will use? And how do you wire all of that into each command without every command reaching into a pile of globals?&lt;/p&gt;
&lt;p&gt;None of it is hard. That&amp;rsquo;s the problem! It&amp;rsquo;s not hard, it&amp;rsquo;s just &lt;em&gt;there&lt;/em&gt;, every single time, and every single time I&amp;rsquo;d find myself reinventing it slightly differently to the last time. Different override precedence here. A subtly different update flow there. Logging that didn&amp;rsquo;t quite match the tool I&amp;rsquo;d written three months earlier. Each new tool was a fresh re-litigation of decisions I&amp;rsquo;d already made and then promptly forgotten.&lt;/p&gt;
&lt;p&gt;Now, I&amp;rsquo;ve banged on about the Boy Scout rule for years (leave the codebase better than you found it), but it has an uncomfortable corollary. If you keep turning up to the same campsite and finding it in the same mess, at some point the honest thing to do is to stop tidying it and go and build a better campsite.&lt;/p&gt;
&lt;h2 id="first-just-packages"&gt;First, just packages
&lt;/h2&gt;&lt;p&gt;So I started pulling the recurring pieces out into their own packages. Nothing grand. A config package that did the hierarchical merge the way I always ended up doing it anyway. A version package that knew how to compare semver and spot a development build. A setup package that handled first-run bootstrap and self-updating from a release. They lived as separate repos, and if you go digging through my GitHub history you can still find the scruffy ancestors of them scattered about.&lt;/p&gt;
&lt;p&gt;Separate packages was the right &lt;em&gt;first&lt;/em&gt; move. It forced each piece to stand on its own and earn its keep on a real project before I trusted it on the next one. A package that&amp;rsquo;s only ever been used in the repo it was born in hasn&amp;rsquo;t really been tested&amp;hellip; it&amp;rsquo;s just been agreed with.&lt;/p&gt;
&lt;p&gt;But separate packages come with a tax. Each one has its own release cadence, its own changelog, its own CI. Worse, they have to agree with each other at the seams, and when they&amp;rsquo;re versioned independently those seams drift. I&amp;rsquo;d bump the config package, and the setup package that depended on it would quietly need a matching bump, and the tool that used both would need telling about both. I&amp;rsquo;d traded &amp;ldquo;reinvent the wheel&amp;rdquo; for &amp;ldquo;keep a dozen wheels in sync&amp;rdquo;, and I&amp;rsquo;m really not convinced that&amp;rsquo;s a better deal.&lt;/p&gt;
&lt;h2 id="then-one-library"&gt;Then, one library
&lt;/h2&gt;&lt;p&gt;Once the packages had been used enough (used in anger, on real tools, by people who weren&amp;rsquo;t me) the shape of them stopped moving. The interfaces settled. The arguments about precedence and defaults were over, because the answers had survived contact with reality.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the point where separate packages stop being a virtue and start being friction. So I forged them into one and called it &lt;strong&gt;go-tool-base&lt;/strong&gt;. One module, one version number, one changelog, and one set of seams that are now internal and can&amp;rsquo;t drift, because they ship together.&lt;/p&gt;
&lt;p&gt;The heart of it is a dependency-injection container, a &lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/5c78fc9/pkg/props/props.go#L15" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Props&lt;/code&gt; struct&lt;/a&gt;, that holds the things every command needs: the logger, the config, the embedded assets, the filesystem handle, the error handler, the tool&amp;rsquo;s own metadata. Commands are handed &lt;code&gt;Props&lt;/code&gt; explicitly rather than reaching for globals, which means a command is just a function of its inputs and is therefore trivially testable. That one decision has quietly paid for itself on every tool I&amp;rsquo;ve built since.&lt;/p&gt;
&lt;p&gt;Around that container sits all the stuff I was so tired of rewriting: hierarchical config, structured logging, version checking, self-update from GitHub or GitLab releases, an interactive TUI documentation browser, AI integration, service lifecycle management. A new tool inherits the lot and gets to spend its first afternoon on the thing that&amp;rsquo;s actually novel&amp;hellip; its own logic.&lt;/p&gt;
&lt;h2 id="finally-a-generator"&gt;Finally, a generator
&lt;/h2&gt;&lt;p&gt;A library still leaves you staring at a blank &lt;code&gt;main.go&lt;/code&gt;. You still have to know the conventions, wire the container, lay out the directories, register the commands. All knowable, but all boilerplate. And boilerplate is exactly the enemy I set out to kill in the first place.&lt;/p&gt;
&lt;p&gt;So go-tool-base ships a generator. &lt;code&gt;gtb generate project&lt;/code&gt; scaffolds a complete, working, idiomatic project: directory layout, the wired &lt;code&gt;Props&lt;/code&gt; container, the command tree, CI, the whole lot. &lt;code&gt;gtb generate command&lt;/code&gt; adds a new command and registers it for you. The generator also handles upkeep: when the framework&amp;rsquo;s conventions move, it can regenerate the scaffolding of an existing project without trampling all over the code you&amp;rsquo;ve written on top. (That last bit turned out to be a properly interesting problem in its own right, and a future post.)&lt;/p&gt;
&lt;p&gt;The goal is blunt. Creating a CLI tool should be about the tool, not the scaffolding. The first afternoon should be spent on the part that&amp;rsquo;s actually worth writing.&lt;/p&gt;
&lt;h2 id="one-thing-i-was-careful-about"&gt;One thing I was careful about
&lt;/h2&gt;&lt;p&gt;There&amp;rsquo;s a nasty failure mode with &amp;ldquo;batteries-included&amp;rdquo; frameworks: the day you outgrow them, they hold you hostage. You either stay inside the framework&amp;rsquo;s worldview forever, or you face a rewrite. I&amp;rsquo;ve been burned by that before and I had no intention of inflicting it on anyone else.&lt;/p&gt;
&lt;p&gt;So go-tool-base generates idiomatic, standard-library-compliant Go. There&amp;rsquo;s no magic runtime you can&amp;rsquo;t see, no clever code you couldn&amp;rsquo;t have written by hand. If you ever outgrow the framework the generated code stands on its own and you walk away with a perfectly normal Go project. A framework should be a starting point you&amp;rsquo;re glad you took, not a room you can&amp;rsquo;t get out of.&lt;/p&gt;
&lt;h2 id="where-this-leaves-me"&gt;Where this leaves me
&lt;/h2&gt;&lt;p&gt;go-tool-base exists because I was spending the first afternoon of every Go CLI tool rebuilding the same plumbing, and rebuilding it slightly wrong relative to last time. It started life as separate packages so each piece could earn its place on real projects; once they&amp;rsquo;d stopped moving I forged them into a single library so the seams couldn&amp;rsquo;t drift; and then I wrapped a generator around it so a new tool starts as a working project rather than a blank file.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a framework for the unglamorous 80% (config, versioning, updates, logging, lifecycle) so you can spend your time on the 20% that&amp;rsquo;s actually yours.&lt;/p&gt;
&lt;p&gt;Over the coming posts I&amp;rsquo;ll dig into the individual pieces&amp;hellip; the generator that won&amp;rsquo;t clobber your edits, the credential handling, the self-update integrity checks, and a few Go techniques I&amp;rsquo;m rather pleased with along the way. Stay tuned!&lt;/p&gt;</description></item><item><title>Migrating away from Mediawiki and how to export its data</title><link>https://blog-570662.gitlab.io/migrating-away-from-mediawiki-and-how-to-export-its-data/</link><pubDate>Wed, 19 Aug 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/migrating-away-from-mediawiki-and-how-to-export-its-data/</guid><description>&lt;p&gt;I like Mediawiki, it is a simple tool capable of doing a lot and can be very flexible and easy to customise. However its not always the right solution! I had a situation where we needed to migrate away from using it for a combination of security and usability reasons. So I thought it would be good to document it.&lt;/p&gt;
&lt;p&gt;After reviewing a few things it was decided to move things over to the companies already existing O365 SharePoint as a new site. This sounded laborious as first, but actually turned out to be pretty straight forward.&lt;/p&gt;
&lt;p&gt;We start with getting data out of Mediawiki, thankfully we only wanted the most recent revision and not the full history of a page. We use PostgreSQL as a back-end so it was reasonably straight forward to figure out how to extract the data in a sensible query.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page_touched&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;edited&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_latest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_revision_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_content_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;LEFT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;OVERLAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;placing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;page_touched&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It tool a little sleuthing to realize that the &lt;code&gt;slots&lt;/code&gt; table was the pivotal in extracting the latest page version. With the right join and a little mangling of the &lt;code&gt;content_address&lt;/code&gt; field from the &lt;code&gt;contents&lt;/code&gt; table to remove the &amp;ldquo;tt:&amp;rdquo; from the value and convert to an integer we now have a result set of all the page names and the latest revision of that page. I also added in the date the page was last updated to allow me to see when it was last edited as it was a live system migration and helped me to ensure content remained sync while both were still in play.&lt;/p&gt;
&lt;p&gt;Once I had the query it was a simple job of writing an &amp;ldquo;Exporter&amp;rdquo; using Go Lang to extract the data and write it to files, I&amp;rsquo;ll chuck a snippet of code at the bottom of the post for you.&lt;/p&gt;
&lt;p&gt;Mediawiki uses &lt;code&gt;wikitext&lt;/code&gt; as a format so I needed to convert it to something more widely understood. Having used Pandoc in the past successfully I plumped for this as I knew it would handle a lot of options and was simple to use to convert to the &lt;code&gt;markdown_mmd&lt;/code&gt; format&lt;/p&gt;
&lt;p&gt;I Installed it via the ubuntu apt package available on my system and wired this in as a hacky &lt;code&gt;exec&lt;/code&gt; command into my script&amp;hellip; and voila! I had hardcopies of all the Mediawiki pages on my system in both &lt;code&gt;wikitext&lt;/code&gt; and &lt;code&gt;markdown_mmd&lt;/code&gt; format.&lt;/p&gt;
&lt;p&gt;Why &lt;code&gt;markdown_mmd&lt;/code&gt; I hear you ask&amp;hellip; mainly because it gave me the cleanest conversion for use with the new markdown web page widget for Sharepoint&amp;rsquo;s modern interface.&lt;/p&gt;
&lt;p&gt;Now we have the files we could do a little munging and parsing to convert URLs into the format needed for the new location in Sharepoint, easily done with a bit of regex pattern matching, which I wont go into as yours will be very different from mine&amp;hellip; suffice to say looking for &lt;code&gt;&amp;quot;wikilink&amp;quot;&lt;/code&gt; in my regex helped massively in finding all the occurrences I needed to update. I used &lt;code&gt;sed&lt;/code&gt; but you could use whatever tool you like or add it into your version of the exporter&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#39;SysAdmin/(.+) &amp;#34;wikilink&amp;#34;&amp;#39; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and with a little back referencing to substitute the values we need to keep and its all good.&lt;/p&gt;
&lt;p&gt;Next came the import of the data into Sharepoint, but that is a post for another day.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;bytes&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;github.com/jmoiron/sqlx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;github.com/rs/zerolog/log&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;io/ioutil&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;os&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;os/exec&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;path/filepath&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="s2"&gt;&amp;#34;wiki-export/src/util&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="ne"&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Edited&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Exporter&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;Config&lt;/span&gt; &lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExporterConfig&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;DB&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sqlx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Exporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Export&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;page_id&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_title&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_text&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page_touched&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;edited&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;page_latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_revision_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slot_content_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;LEFT&lt;/span&gt; &lt;span class="n"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;mediawiki&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pagecontent&lt;/span&gt; &lt;span class="n"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pagecontent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;old_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OVERLAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content_address&lt;/span&gt; &lt;span class="n"&gt;placing&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;ORDER&lt;/span&gt; &lt;span class="n"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;page_touched&lt;/span&gt; &lt;span class="n"&gt;DESC&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queryx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructScan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;wikiFilename&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;.mediawiki&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;mdFilename&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;.md&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;wikiDir&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/mediawiki&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;mdDir&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetFormat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="n"&gt;wikiDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/mediawiki/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="n"&gt;mdDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/md/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetDir&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MkdirAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wikiDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MkdirAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mdDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;wikiTarget&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiFilename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;mdTarget&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mdDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mdFilename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Msgf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; =&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mdTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wikiTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pandoc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-f&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mediawiki&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-t&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wikiTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errorBuffer&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputBuffer&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;outputBuffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;errorBuffer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Msgf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ERROR: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errorBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;			&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ioutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mdTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outputBuffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;0777&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Pre-populating Neo4J using Kubernetes Init Containers and neo4j-admin import</title><link>https://blog-570662.gitlab.io/pre-populating-neo4j-using-kubernetes-init-containers-and-neo4j-admin-import/</link><pubDate>Wed, 15 Jul 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/pre-populating-neo4j-using-kubernetes-init-containers-and-neo4j-admin-import/</guid><description>&lt;img src="https://blog-570662.gitlab.io/pre-populating-neo4j-using-kubernetes-init-containers-and-neo4j-admin-import/maxresdefault.jpg" alt="Featured image of post Pre-populating Neo4J using Kubernetes Init Containers and neo4j-admin import" /&gt;&lt;p&gt;Recently there has been an uptake in the use of Neo4j by the Data Scientists. This is a good thing! they are wanting to use the right tool for the job. However we need to run it inside our k8s cluster as a portable readable data source that has been dynamically populated from a pile of data in a combination of PostgreSQL and MongoDB.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t a problem for them working locally, they install and spin up a local copy of Neo4j and can interact with it quite happily. They even realised that they can generate CSV&amp;rsquo;s from PostgreSQL and MongoDB and then import them, blindingly fast, into Neo4j using the &lt;code&gt;neo4j-admin&lt;/code&gt; tool that comes bundled. Fantastic!&lt;/p&gt;
&lt;p&gt;At least until they come to want to run their Neo instance inside our k8s cluster. That&amp;rsquo;s where I step in and turn them aside from creating their own custom neo4j image with a bespoke entry point that loads all the data for them in some crazy threaded bash scripting!&lt;/p&gt;
&lt;p&gt;&amp;ldquo;No, No, No!&amp;rdquo; I tell them. &amp;ldquo;It&amp;rsquo;s far easier to just add an init container to your pod, that will preload the data before Neo starts up&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Init containers, if you haven&amp;rsquo;t come across before, them are a special type of container that lives inside a k8s pod and are set to run &lt;strong&gt;BEFORE&lt;/strong&gt; your main container runs. In this case it means we can easily sequence a bash script to run the &lt;code&gt;neo4j-admin import&lt;/code&gt; before Neo4j is even started. And here is how we did it!&lt;/p&gt;
&lt;h2 id="the-script"&gt;The script
&lt;/h2&gt;&lt;p&gt;The data scientists had been using Neo4j 3.5.x locally because they had a need for the graph algorithms plugin (&lt;a class="link" href="https://github.com/neo4j-contrib/neo4j-graph-algorithms" target="_blank" rel="noopener"
 &gt;https://github.com/neo4j-contrib/neo4j-graph-algorithms&lt;/a&gt;) which at the time they were looking didn&amp;rsquo;t support Neo4j 4.x. The plugin is now deprecated and its replacement (&lt;a class="link" href="https://github.com/neo4j/graph-data-science" target="_blank" rel="noopener"
 &gt;https://github.com/neo4j/graph-data-science&lt;/a&gt;) thankfully supports 3.5.x and 4.x.&lt;/p&gt;
&lt;p&gt;As Neo4j 4.x introduces a lot of new features and improves performance so I recommended we switch to using that. This meant a refactor of their bash script for neo4j-admin there some very subtle differences and a few caveats to work with. This is what they came up with&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;DBNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;neo4j&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -eq &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;DBNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# extract data from SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 extract_data.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remove old db for rebuild&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf &lt;span class="s2"&gt;&amp;#34;/data/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DNBAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;neo4j-admin import &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --database&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$DBNAME&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --delimiter&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;|&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --nodes&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;Protein&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_protein_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_proteins.csv &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --nodes&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;UniProtKB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_uniprot_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/nodes_uniprot.csv &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --relationships&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;HAS_AMINO_ACID_SEQUENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EDGE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_protein_sequence_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_protein_sequence.csv &lt;span class="se"&gt;\ &lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --relationships&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;HAS_AMINO_ACID_SEQUENCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;EDGE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_chembl_protein_biotherapeutic_molregno_header.csv,&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DATA_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/edges_chembl_protein_biotherapeutic_molregno.csv &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --skip-bad-relationships&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --skip-duplicate-nodes&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;import&lt;/code&gt; command here is significantly shorter for example purposes, as the original is about 120 lines long. As you can see it&amp;rsquo;s pretty straight forward, they had another script in &lt;code&gt;extract_data.py&lt;/code&gt;, that I wont bore you with suffice to say that it pulled out all the data they wanted from PostgreSQL and MongoDB, which got saved to disk as CSV files in the relevant directories.&lt;/p&gt;
&lt;p&gt;Great, it worked on their local version!&lt;/p&gt;
&lt;h2 id="the-dockerfile"&gt;The Dockerfile
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ROM neo4j:latest
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4JLABS_PLUGINS [&amp;#34;graph-data-science&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RUN apt update &amp;amp;&amp;amp; apt install -y python3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WORKDIR /srv
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY src /srv/src
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY headers /srv/headers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The plan is always to keep it simple. We have one image that we can run for both the init container and the main container. This docker file gives a vanilla neo4j instance with python and our scripts for extracting the data loaded into it&lt;/p&gt;
&lt;h2 id="the-k8s-manifest"&gt;The k8s Manifest
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Pod&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;NEO4J_AUTH&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j/password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;registry.example.com/phpboyscout/rnd_graph:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Always&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumeMounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;initContainers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;importer&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;neo4j_import.sh&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/bin/bash&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DATA_DIR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/import/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;HEADER_DIR&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/srv/headers&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;registry.example.com/phpboyscout/rnd_graph:latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Always&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;workingDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/srv/src&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumeMounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;mountPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/import&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;import&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;claimName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;neo4j&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we can pull it all together with our k8s manifest. From here you can see that we have our default neo4j container that we pass in our default authentication details to and an init container that runs our &lt;code&gt;import.sh&lt;/code&gt; script. Both containers have access to a shared volume for the &lt;code&gt;/import&lt;/code&gt; and &lt;code&gt;/data&lt;/code&gt; folders.&lt;/p&gt;
&lt;p&gt;And now we get to&amp;hellip;&lt;/p&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting
&lt;/h2&gt;&lt;p&gt;So right off the bat it didn&amp;rsquo;t work! No surprises there but here are a few things that caused us some issues and how we resolved them.&lt;/p&gt;
&lt;h3 id="database-offline"&gt;Database offline
&lt;/h3&gt;&lt;p&gt;At first glance everything seemed to work. Until we tried to connect to the &lt;code&gt;neo4j&lt;/code&gt; database with the default UI, at which point we were presented with the error message&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Database &amp;#34;neo4j&amp;#34; is unavailable, its status is &amp;#34;offline.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This took a little sleuthing and shelling into the neo4j container to take a look at the &lt;code&gt;/var/debug.log&lt;/code&gt; file which gives significantly more useful information about whats going on with the server. First we were getting stack traces that contained messages like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Component &amp;#39;org.neo4j.kernel.impl.transaction.log.files.TransactionLogFiles@59d6a4d1&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;was successfully initialized, but failed to start. Please see the attached cause 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exception &amp;#34;/data/transactions/neo4j/neostore.transaction.db.0&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From experience this sounded like a permissions issue and lo and behold, checking the files on the filesystem showed that because the import script was run as root the database files were owned by root. We resolved this by adding:-&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown -R neo4j:neo4j /data/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;to the bottom of the import script. Next we were then presented with an error that looked like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2020-07-14 16:56:33.919+0000 WARN [o.n.k.d.Database] [neo4j] Exception occurred while
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;starting the database. Trying to stop already started components. Mismatching store id.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This one seems like it would be an obvious one to google and I did come up with few pages that seemed to describe what was happening to me but gave some varied solutions, from starting and stopping the sever and running &lt;code&gt;neo4j-admin unbind&lt;/code&gt; in between to deleting various files. It seemed very strange because we did test this with the 3.5.17 version of Neo and it worked fine.&lt;/p&gt;
&lt;p&gt;The solution we needed was to wipe the slate clean properly. The line in our script to remove the previous build of the db&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# remove old db for rebuild&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf &lt;span class="s2"&gt;&amp;#34;/data/databases/&lt;/span&gt;&lt;span class="nv"&gt;$DNBAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;just didn&amp;rsquo;t cut it. It turns out that because the 4.x version of Neo4j supports multiple databases the &lt;code&gt;import&lt;/code&gt; command writes additional information to the system database and transactions database in the form of some identifiers for each database, BUT if you don&amp;rsquo;t do something to clear that value for the database your are building it wont match up when the server starts and you get a declaration of &lt;code&gt;Mismatching store id&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if the developers are aware of this flaw, so in the mean time we have to expand our cleanup to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# clean up for fresh import&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf /data/databases/*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf /data/transactions/*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;removing the neoj4, system and store_lock databases and transaction logs from the data store. This solved the problem and the server was able to start and we could connect to neo4j database successful.&lt;/p&gt;
&lt;p&gt;Its not an ideal solution, I can foresee definite situations we will have to work around when we get to a point where multiple databases may be needed and are built separately and independently from each other. but it will suffice for now.&lt;/p&gt;
&lt;h3 id="malloc-error-message-goes-here"&gt;Malloc(): Error message goes here
&lt;/h3&gt;&lt;p&gt;Once it was up and running we noticed that we were getting lots of restarts on the main neo4j container a quick look at the &lt;code&gt;stdout&lt;/code&gt; log and we could see each restart ending with something that looked like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;malloc(): corrupted top size
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;instantly this looks like an issue with memory sizing inside the container for the JVM. Thankfully the team at Neo4j have accounted for this and give you a nice tool in the form of&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;neo4j-admin memrec
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which interrogates the databases and gives some sensible values you can set in the output which in our case looked like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Memory settings recommendation from neo4j-admin memrec:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Assuming the system is dedicated to running Neo4j and has 376.6GiB of memory,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# we recommend a heap size of around 31g, and a page cache of around 331500m,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# and that about 22400m is left for the operating system, and the native memory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# needed by Lucene and Netty.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: If the indexing storage use is high, e.g. there are many indexes or most&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# data indexed, then it might advantageous to leave more memory for the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# operating system.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: Depending on the workload type you may want to increase the amount&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# of off-heap memory available for storing transaction state.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# For instance, in case of large write-intensive transactions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# increasing it can lower GC overhead and thus improve performance.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# On the other hand, if vast majority of transactions are small or read-only&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# then you can decrease it and increase page cache instead.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: The more concurrent transactions your workload has and the more updates&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# they do, the more heap memory you will need. However, don&amp;#39;t allocate more&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# than 31g of heap, since this will disable pointer compression, also known as&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# &amp;#34;compressed oops&amp;#34;, in the JVM and make less effective use of the heap.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tip: Setting the initial and the max heap size to the same value means the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# JVM will never need to change the heap size. Changing the heap size otherwise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# involves a full GC, which is desirable to avoid.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Based on the above, the following memory settings are recommended:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dbms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dbms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dbms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pagecache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;331500&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# It is also recommended turning out-of-memory errors into full crashes,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# instead of allowing a partially crashed database to continue running:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#dbms.jvm.additional=-XX:+ExitOnOutOfMemoryError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# The numbers below have been derived based on your current databases located at: &amp;#39;/var/lib/neo4j/data/databases&amp;#39;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# They can be used as an input into more detailed memory analysis.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Total size of lucene indexes in all databases: 0k&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Total size of data and native indexes in all databases: 17300m&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So how to get these values into the container&amp;hellip; Thankfully this is handled for you in the form of Environment Variables you can pass into the docker image. A bit of a google and i found this little snippet which is a goldmine for telling us how to translate settings into environment variables.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Env variable naming convention:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - prefix NEO4J_&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - double underscore char &amp;#39;__&amp;#39; instead of single underscore &amp;#39;_&amp;#39; char in the setting name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - underscore char &amp;#39;_&amp;#39; instead of dot &amp;#39;.&amp;#39; char in the setting name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# NEO4J_dbms_tx__log_rotation_retention__policy env variable to set&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# dbms.tx_log.rotation.retention_policy setting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As for getting the variables into the container, you could do this from the pod and inject it in. I this case because the data we are going to be using is reasonably stable and tested we decided to stick them into the Docker file with the &lt;code&gt;ENV&lt;/code&gt; directive.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4J_dbms_memory_heap_initial__size 31g
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4J_dbms_memory_heap_max__size 31g
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ENV NEO4J_dbms_memory_pagecache_size 331500m
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And so far we haven&amp;rsquo;t had a restart yet!&lt;/p&gt;</description></item><item><title>Adding Ambient Sounds to your Discord Server On LInux</title><link>https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/</link><pubDate>Tue, 30 Jun 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/</guid><description>&lt;img src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/tfOnZwZBwA-e1593539639171.jpg" alt="Featured image of post Adding Ambient Sounds to your Discord Server On LInux" /&gt;&lt;p&gt;I&amp;rsquo;m a Dungeon Master! I don&amp;rsquo;t mean that in the S&amp;amp;M sense! As in the game Dungeons &amp;amp; Dragons (&lt;a class="link" href="https://dnd.wizards.com" target="_blank" rel="noopener"
 &gt;https://dnd.wizards.com&lt;/a&gt;), where I run a weekly game as well as take part in a couple of campaigns as a player. It&amp;rsquo;s a lot of fun and something I would definitely recommend you have a go at if you are so inclined&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://discord.com" target="_blank" rel="noopener"
 &gt;&lt;img class="gallery-image" data-flex-basis="245px" data-flex-grow="102" height="240" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/2c21aeda16de354ba5334551a883b481.png" width="245"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There is a vast amount of tooling &amp;amp; tech out there that allows you to play remotely such as Virtual Table Tops, Character builders, online resources, etc. One such tool that gets used quite often is a chat service called Discord (&lt;a class="link" href="https://discord.com" target="_blank" rel="noopener"
 &gt;https://discord.com&lt;/a&gt;) It&amp;rsquo;s really useful and allows you to easily be part of and manage communities of people&amp;hellip;. Think IRC &amp;amp; Slack, but more up to date than IRC and less &amp;ldquo;workish&amp;rdquo; than Slack.&lt;/p&gt;
&lt;p&gt;As part of my online games I like being able to have ambient music to match the surroundings the players are traveling through, as well as some active elements thrown in for good measure. This is possible in a few different ways using discord but the way I want to set it up can be somewhat frustrating to set up. Let me explain:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://syrinscape.com" target="_blank" rel="noopener"
 &gt;&lt;img class="gallery-image" data-flex-basis="709px" data-flex-grow="295" height="114" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/logo-1.jpg" width="337"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have taken a shine to two tools in particular&amp;hellip; Syrinscape (&lt;a class="link" href="https://syrinscape.com" target="_blank" rel="noopener"
 &gt;https://syrinscape.com&lt;/a&gt;) and Table Top Audio (&lt;a class="link" href="https://tabletopaudio.com" target="_blank" rel="noopener"
 &gt;https://tabletopaudio.com&lt;/a&gt;). The former being a windows app with an nice interactive mixing UI that allows you to combine and generate unique sounds, the latter being a lovely web service that has some fantastic loop-able ambient background tracks all 100% free.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://tabletopaudio.com" target="_blank" rel="noopener"
 &gt;&lt;img class="gallery-image" data-flex-basis="1008px" data-flex-grow="420" height="120" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/header3.png" width="504"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I am wanting to be able to pipe the audio from these two services into my Discord server so that I can make use of the fantastic audio they offer. This is the journey of how I managed to get this working, partly as a reminder for me if I ever need to do this again and also to help others that may be looking to do the same.&lt;/p&gt;
&lt;h2 id="my-setup"&gt;My Setup
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;ve been a big fan of Ubuntu for a number of years, but since 20.04 I&amp;rsquo;ve found that the shine I&amp;rsquo;ve had for it has waned significantly. I wont go into the why and wherefore of it but I&amp;rsquo;m now running the excellent Pop_OS! from System76 (&lt;a class="link" href="https://pop.system76.com" target="_blank" rel="noopener"
 &gt;https://pop.system76.com&lt;/a&gt;) its an Ubuntu variant but with the bits I dislike removed. So assume that anything I&amp;rsquo;m doing is compatible with Ubuntu 20.04.&lt;/p&gt;
&lt;h2 id="the-requirements"&gt;The Requirements
&lt;/h2&gt;&lt;p&gt;The ideal solution should see me being able to have a single instance of discord running that allows me to still use my mic to be able to talk, and to have my selected background playing with the ability to control the volumes of both the mic and the background independently.&lt;/p&gt;
&lt;h2 id="finding-a-solution"&gt;Finding A Solution
&lt;/h2&gt;&lt;p&gt;A lot of googling led me to realise that there isn&amp;rsquo;t a perfect solution to fit my brief. The hardest part being not actually knowing what to google and a lot of the terminology being somewhat foreign to me as I&amp;rsquo;m not much of an audio engineer. However I finally stumbled upon a &lt;a class="link" href="https://endless.ersoft.org/pulseaudio-loopback/" target="_blank" rel="noopener"
 &gt;blog post&lt;/a&gt; by Emma Anderson dated June 2016 and thankfully it gives me a lot of the heavy lifting that I needed along with some explanation of what I&amp;rsquo;m trying to achieve, though I&amp;rsquo;m hopefully going to be more verbose here in what this all means and how it works.&lt;/p&gt;
&lt;h2 id="pulseaudio"&gt;PulseAudio
&lt;/h2&gt;&lt;p&gt;The first thing we need to do is make sure the packages for &lt;code&gt;pulseaudio&lt;/code&gt; and &lt;code&gt;pavucontrol&lt;/code&gt; are installed. These will allow us to manipulate the way we capture sound and redirect it to the appropriate inputs and outputs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt install pulseaudio pavucontrol
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="virtual-input--virtual-mic"&gt;Virtual Input &amp;amp; Virtual Mic
&lt;/h2&gt;&lt;p&gt;What we are going to try to achieve, is to create two new elements inside of Pulseaudio;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a Virtual input that we can channel the applications creating our background sounds which will allow us to control the volume independently.&lt;/li&gt;
&lt;li&gt;a Virtual Microphone that we can channel our both our normal microphone and the new Virtual input into.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By creating these elements we can then use the &lt;code&gt;pavucontrol&lt;/code&gt; tool to select what needs to be redirected where. so lets get started.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pactl load-module module-null-sink &lt;span class="nv"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-sink-proplist VirtualInput device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-source-proplist VirtualInput.monitor device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput.monitor
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we have two commands, the first will create our new Virtual Input as what is referred to as a &amp;ldquo;null sink&amp;rdquo;. This on its own is not really very useful for us as we also need what is referred to as a &amp;ldquo;source&amp;rdquo;, thankfully when we run this command it also created a new &amp;ldquo;source&amp;rdquo; for us.&lt;/p&gt;
&lt;p&gt;On it&amp;rsquo;s own that should be more than enough, but running the 2nd &amp;amp; 3rd command makes our live a lot easier because it will apply some very useful labels to both of the newly created sink and source. In this case &lt;code&gt;VirtualInput&lt;/code&gt; for the sink and &lt;code&gt;VirtualInput.monitor&lt;/code&gt; for the source. Having these in place makes it a lot simpler to configure things with &lt;code&gt;pavucontrol&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next we need to create our Virtual Mic using some very familiar looking commands.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;VM&lt;/span&gt;&lt;span class="o"&gt;=$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pactl&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt; &lt;span class="n"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pacmd&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;proplist&lt;/span&gt; &lt;span class="n"&gt;VirtualMic&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pacmd&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;proplist&lt;/span&gt; &lt;span class="n"&gt;VirtualMic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;again we have now created a new new pair of sink and source with some nice easy to recognise labels that we will use when we start working with &lt;code&gt;pavucontrol&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next piece to our puzzle is creating the components that will let us define a connection from the &lt;code&gt;VirtualInput&lt;/code&gt; and our physical microphone to the newly created &lt;code&gt;VirtualMic&lt;/code&gt;. We do this with two identical commands;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pactl&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;loopback&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pactl&lt;/span&gt; &lt;span class="nb"&gt;load&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;loopback&lt;/span&gt; &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VirtualMic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;we now have most of the elements that we need to configure everything to work.&lt;/p&gt;
&lt;h2 id="listening-to-my-own-ambience"&gt;Listening to my own Ambience
&lt;/h2&gt;&lt;p&gt;Before we can start wiring it all together we need to ensure we can also listen back to our own ambience. This involves us creating one more &amp;ldquo;loopback&amp;rdquo; module that points to the speakers we are wanting to listen to. Lets find out what our options are by running;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd list-sinks &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="err"&gt;&amp;#39;&lt;/span&gt;/index:/ &lt;span class="o"&gt;{&lt;/span&gt;print &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; /name:/ &lt;span class="o"&gt;{&lt;/span&gt;print &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; /device.description/ &lt;span class="o"&gt;{&lt;/span&gt;print &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This lists all of the available &amp;ldquo;sinks&amp;rdquo; that we can use. on my daily driver laptop I get;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * index: &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	name: &amp;lt;alsa_output.pci-0000_00_1f.3.analog-stereo&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;		device.description &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Built-in Audio Analogue Stereo&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells us the &amp;ldquo;index&amp;rdquo; for the device, its name and also some kind of description. The important bit for us here is the name as we will need that to create our new &amp;ldquo;loopback&amp;rdquo; with the command;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;alsa_output.pci-0000_00_1f.3.analog-stereo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates the last piece for our puzzle!&lt;/p&gt;
&lt;h2 id="connecting-it-all-together"&gt;Connecting it all together
&lt;/h2&gt;&lt;p&gt;I&amp;rsquo;m now going to assume you have logged yourself into the Discord client and fired up your copy of Syrinscape&amp;hellip; but you should just as easily swap out these for something else of your choice.&lt;/p&gt;
&lt;p&gt;Now we can start &lt;code&gt;pavucontrol&lt;/code&gt; either from the command line or you can look for it in your applications menu. Once it loads you will hopefully be presented with something that looks like;&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="317px" data-flex-grow="132" height="644" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-40-23.png" srcset="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-40-23_hu_56270fcef40843d0.png 800w, https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-40-23.png 853w" width="853"&gt;&lt;/p&gt;
&lt;p&gt;For this next step I am specifically starting on the &amp;ldquo;Recording&amp;rdquo; tab of &lt;code&gt;pavucontrol&lt;/code&gt; this is to allow us to set up what is going to be captured. I have updated the drop-down at the bottom left to show &amp;ldquo;All Streams&amp;rdquo; as this will make it quicker to configure… Starting at the top we have two entries for;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback to VirtualMic from&lt;/strong&gt;: These are the result of the first two &amp;ldquo;loopback&amp;rdquo; modules we created with the &lt;code&gt;pactl&lt;/code&gt; command we ran previously. They are going to allow us to capture the audio streams from our physical microphone, mine here is the &lt;code&gt;TONOR TC-777&lt;/code&gt; and our newly created &lt;code&gt;VirtualMic&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;followed by a single entry for;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback to Built-in-Audio Analougue Stereo from&lt;/strong&gt;: which is the last &amp;ldquo;loopback&amp;rdquo; module that we create to let us hear our own Ambience, Having this set to our &lt;code&gt;VirtualInput&lt;/code&gt; means that anything that pipe into our &lt;code&gt;VirtualInput&lt;/code&gt; will also come out of our speakers.&lt;/p&gt;
&lt;p&gt;and finally;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WEBRTC VoiceEngine&lt;/strong&gt;: Once you connect to a voice channel in discord this will appear and it allows us to specify which of our devices it should be reading the audio feed from. For our purposes we have this set to our VirtualMic so that we can have our mixed audio feeds&lt;/p&gt;
&lt;p&gt;Now that recording is configured we can sort out our playback.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="203px" data-flex-grow="84" height="1003" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-41-57.png" srcset="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-41-57_hu_b9e9d12b3e2e9294.png 800w, https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-07-06-09-41-57.png 850w" width="850"&gt;&lt;/p&gt;
&lt;p&gt;Here we can see the &amp;ldquo;Playback&amp;rdquo; tab of &lt;code&gt;pavucontrol&lt;/code&gt;, again set to show &amp;ldquo;All Streams&amp;rdquo;. This time I&amp;rsquo;m going to run through the elements here starting from the bottom of the list and working my way up&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WEBRTC VoiceEngine&lt;/strong&gt;: This again is our connection to a Discord voice channel, as you can see I have this set to play back all of its output via &lt;code&gt;Built-in Audio Analogue Stereo&lt;/code&gt; which is how my Operating system has labelled my physical speakers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Syrinscape.exe&lt;/strong&gt;: This is the Syrinscape application, that I run through PlayOnLinux (&lt;a class="link" href="https://www.playonlinux.com" target="_blank" rel="noopener"
 &gt;https://www.playonlinux.com&lt;/a&gt;), and I will use to generate all of my lovely ambient sounds. This is set to play all of its &amp;ldquo;audio stream&amp;rdquo; on our &lt;code&gt;VirtualInput&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next two items in our list, providing you have configured the Recording tab first should look as in the image. Changing the &amp;ldquo;Loopback to VirtualMic&amp;rdquo; entries on the Recording tab will change the labels of these two entries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback of VirtualInput.monitor on&lt;/strong&gt;: it seems we have two of these entries and where we can tell the to pip all of the audio we are now capturing on our &lt;code&gt;VirtualInput&lt;/code&gt; In this case we want it to go to two places, our &lt;code&gt;VirtualMic&lt;/code&gt; so that it can be sent to both our Discord audio channel and also to our &lt;code&gt;Built-in Audio Analogue Stereo&lt;/code&gt; speakers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Loopback of Built-in Audio Analogue Stereo on&lt;/strong&gt;: is where we now direct the input from our physical microphone and feed that straight into our &lt;code&gt;VirtualMic&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The other entries in the list here are for firefox and the system itself and are not relevant to what we are trying to achive.&lt;/p&gt;
&lt;h2 id="winner-winner-chicken-dinner"&gt;Winner Winner Chicken Dinner
&lt;/h2&gt;&lt;p&gt;That&amp;rsquo;s is effectively all we need to do&amp;hellip; From here on in anything you play via the Syrinscape app will be merged with your microphone input and passed to Discord. You can then use the volume sliders in &lt;code&gt;pavucontrol&lt;/code&gt; to adjust the levels of all the inputs to suit your own personal preference.&lt;/p&gt;
&lt;p&gt;Though I will make a few small suggestions about how to configure your discord settings. You shouldn&amp;rsquo;t need to make any adjustments to the input and output devices which should now be set to &lt;code&gt;Default&lt;/code&gt; as your &amp;ldquo;Input Device&amp;rdquo; if you change this it will override the changes we have made and you will need to go back to the Recording tab of &lt;code&gt;pavucontrol&lt;/code&gt; and switch &lt;strong&gt;WEBRTC VoiceEngine&lt;/strong&gt; back to InputMic , but&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="322px" data-flex-grow="134" height="692" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-06-30-18-44-02.png" srcset="https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-06-30-18-44-02_hu_f7eba952aec4ac92.png 800w, https://blog-570662.gitlab.io/adding-ambient-sounds-to-your-discord-server-on-linux/Screenshot-from-2020-06-30-18-44-02.png 931w" width="931"&gt;&lt;/p&gt;
&lt;p&gt;I would recommend disabling automatic input sensitivity and lowering the sensitivity slider all the way down to -100dB&amp;hellip; this is to allow for the potential low and subtle tones and ambient elements you may want to play&amp;hellip; be warned though it makes it very very easy for an low quality microphone (such as the &lt;code&gt;Built-in Audio Analogue Stereo&lt;/code&gt; microphone found on my laptop) to pick up other noises such as your systems fans, mouse clicks and typing. A simple way to combat this is to get a reasonable quality external cardioid condenser microphone which eliminates a lot of this unwanted background.&lt;/p&gt;
&lt;h2 id="one-last-thing"&gt;One last thing
&lt;/h2&gt;&lt;p&gt;That should be it for now&amp;hellip; I&amp;rsquo;ll leave you with one final thing. This is a simple little bash script I threw together that I run in can a terminal to create all the components and if I want will then clean them all up and remove them. If you really want you could set it up as a permanent implementation, but I&amp;rsquo;ll let you google for that solution!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;LB1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenback&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Listing all possible output devices&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pacmd list-sinks &lt;span class="p"&gt;|&lt;/span&gt; awk &lt;span class="s1"&gt;&amp;#39;/index:/ {print $0}; /name:/ {print $0}; /device\.description/ {print $0}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Please enter the name of the output device to create a loopback for (leave blank to skip): &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;read&lt;/span&gt; S1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$S1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 	&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating Loopback for &amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;$S1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 	&lt;span class="nv"&gt;LB1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$S1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; true&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;read&lt;/span&gt; -p &lt;span class="s2"&gt;&amp;#34;Finished? do you want to clean up and remove modules [Yn]: &amp;#34;&lt;/span&gt; yn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$yn&lt;/span&gt; in
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt;Yy&lt;span class="o"&gt;]&lt;/span&gt;* &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; 0&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt;Nn&lt;span class="o"&gt;]&lt;/span&gt;* &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; 1&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * &lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Please answer yes or no.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenback
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating VirtualInput&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-null-sink &lt;span class="nv"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-sink-proplist VirtualInput device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-source-proplist VirtualInput.monitor device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualInput.monitor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating VirtualMic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-null-sink &lt;span class="nv"&gt;sink_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-sink-proplist VirtualMic device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pacmd update-source-proplist VirtualMic.monitor device.description&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic.monitor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; * Creating loopbacks for VirtualMic&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VML1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;VML2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pactl load-module module-loopback &lt;span class="nv"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VirtualMic&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;All modules have been loaded have been configured! Run pavucontrol to configure your devices.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; cleanup&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VML2&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VML1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VM&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$VI&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$LB1&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pactl unload-module &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$LB1&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;All modules have been unloaded&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$LB1&lt;/span&gt; !&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Modules &lt;/span&gt;&lt;span class="nv"&gt;$LB1&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VI&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VM&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VML1&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp; &lt;/span&gt;&lt;span class="nv"&gt;$VML2&lt;/span&gt;&lt;span class="s2"&gt; remain loaded&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 	&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Modules &lt;/span&gt;&lt;span class="nv"&gt;$VI&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VM&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="nv"&gt;$VML1&lt;/span&gt;&lt;span class="s2"&gt;, &amp;amp; &lt;/span&gt;&lt;span class="nv"&gt;$VML2&lt;/span&gt;&lt;span class="s2"&gt; remain loaded&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Encrypting additional drives with LUKS on Linux</title><link>https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/</link><pubDate>Mon, 29 Jun 2020 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/</guid><description>&lt;img src="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing.jpg" alt="Featured image of post Encrypting additional drives with LUKS on Linux" /&gt;&lt;p&gt;Encryption is king nowadays with everyone having mobile devices. We have a significant number of people on laptops that travel around and also workstations that live in open plan offices. This means we encrypt all of our disks&amp;hellip; just in case. 99% of the time is super simple to do as most OS installers give you the option to do it, some now ven enforce it as a default option. This post however is about adding an additional disk to the system and making it automatically mount on system startup.&lt;/p&gt;
&lt;p&gt;So let me set the scene, we have a data-scientist that&amp;rsquo;s running out of disk space for a task they are running on their Ubuntu 18.04 Workstation. At some point the workstation had an upgrade to the HDD in the past to a shiny new SSD, and the old 4Tb spinning disk was left in the chassis that they want to use for this very specific task.&lt;/p&gt;
&lt;p&gt;Now this workstation has been through a couple of data-scientists over the last 12 months and unfortunately the LUKS password that had been set up for the old spinning disk has gone walkabouts&amp;hellip; so the plan is as follows&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;flatten the old disk and set up a new partition using the whole disk&lt;/li&gt;
&lt;li&gt;generate a new secure encryption key&lt;/li&gt;
&lt;li&gt;set up LUKS encryption on the new partition&lt;/li&gt;
&lt;li&gt;use Ext4 as a filesystem&lt;/li&gt;
&lt;li&gt;enable auto decryption of the disk&lt;/li&gt;
&lt;li&gt;add the new partition to the fstab to mount on system startup&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;N.B Assume that we are running everything as the root user&lt;/p&gt;
&lt;h2 id="flatten-the-disk"&gt;Flatten the disk
&lt;/h2&gt;&lt;p&gt;As we cant recover anything we are going to flatten the disk using &lt;code&gt;parted&lt;/code&gt; (&lt;code&gt;apt install parted&lt;/code&gt; to install) to allow is to create a partition greater than 2Tb, but first we are going to identify the disk we are working with&amp;hellip; I tend to favour using either &lt;code&gt;fdisk -l&lt;/code&gt; or as a more concise option &lt;code&gt;lsblk -p&lt;/code&gt; which gives us a an easy to interpret overview something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda 8:0 0 1.8T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda1 8:1 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda2 8:2 0 732M 0 part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sda3 8:3 0 1.8T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sda3_crypt 253:0 0 1.8T 0 crypt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─/dev/mapper/ubuntu--vg-root 253:1 0 1.8T 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/ubuntu--vg-swap_1 253:2 0 976M 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sdb 8:16 0 3.7T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I can tell from this that we are looking at using the disk that is currently at /dev/sdb and its showing as being 3.7Tb in size.&lt;/p&gt;
&lt;p&gt;Great&amp;hellip; now to set up our new partition using the command &lt;code&gt;parted /dev/sdb&lt;/code&gt; which gives us an interactive shell to work with (you can see the prompts in the output below are prefixed with (parted)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GNU Parted 3.2 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using /dev/sdb 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Welcome to GNU Parted! Type &amp;#39;help&amp;#39; to view a list of commands. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) mklabel gpt 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The command &lt;code&gt;mklabel gpt&lt;/code&gt; will wipe the partition table for &lt;code&gt;/dev/sdb&lt;/code&gt; and give us a clean slate to work from&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) unit TB 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We now set parted to think in Terabytes as the default reference size using the command above.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) mkpart primary 0.00TB 3.70TB 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we get to create the actual partition. You can see from the command above that we are using the command &lt;code&gt;mkpart&lt;/code&gt; and telling it to create a &lt;code&gt;primary&lt;/code&gt; partition type.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) print 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Model: ATA WDC WD4005FZBX-0 (scsi) 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/sdb: 4.00TB 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sector size (logical/physical): 512B/4096B 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Partition Table: gpt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk Flags: 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Number Start End Size File system Name Flags 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1 0.00TB 4.00TB 4.00TB primary 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can chek everything went smoothly using the &lt;code&gt;print&lt;/code&gt; command which gives us confirmation that a new primary partition is present. We can now leave &lt;code&gt;parted&lt;/code&gt; with a simple.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;(parted) quit 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we can now use &lt;code&gt;fdisk -l&lt;/code&gt; or &lt;code&gt;lsblk -p&lt;/code&gt; to see that we now have a partition waiting for us at &lt;code&gt;/dev/sdb1&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda 8:0 0 1.8T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda1 8:1 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda2 8:2 0 732M 0 part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sda3 8:3 0 1.8T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sda3_crypt 253:0 0 1.8T 0 crypt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─/dev/mapper/ubuntu--vg-root 253:1 0 1.8T 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/ubuntu--vg-swap_1 253:2 0 976M 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sdb 8:16 0 3.7T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sdb1 8:17 0 3.7T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="generating-an-encryption-key"&gt;Generating an encryption key
&lt;/h2&gt;&lt;p&gt;Our disk is now ready for use, but not yet encrypted, so our next step is to create a key that can be used when we encrypt the disk. As we are going to be mounting it automatically we want to use a keyfile to store the key. You can of course create a key by mashing the keys on the keyboard, but I tend to prefer letting something else do the hard part for me.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="429px" data-flex-grow="178" height="559" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing-1.jpg" srcset="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing-1_hu_5a8dd024f6ad6817.jpg 800w, https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/encryption-encoding-hashing-1.jpg 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;First we create somewhere to store the key&amp;hellip; I opted for,&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /etc/crypt/keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But feel free to put it wherever you want just as long as its only accessible by the &lt;code&gt;root&lt;/code&gt; user. Next we generate the keyfile using the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dd bs=512 count=4 if=/dev/urandom of=/etc/crypt/keys/sdb1 iflag=fullblock
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here I am using &lt;code&gt;/dev/urandom&lt;/code&gt; as my randomness generator, but you could use any valid generator of your choice. With this set of parameted &lt;code&gt;dd&lt;/code&gt; will read the stream of &amp;ldquo;randomeness&amp;rdquo; and write 2048 bytes to our keyfile at &lt;code&gt;/etc/crypt/keys/sdb1&lt;/code&gt;. If you want to be a little more complex about teh size and shape of your key then have a look at &lt;a class="link" href="https://man7.org/linux/man-pages/man1/dd.1.html" target="_blank" rel="noopener"
 &gt;https://man7.org/linux/man-pages/man1/dd.1.html&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="encrypting-the-disk"&gt;Encrypting the Disk
&lt;/h2&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="707px" data-flex-grow="294" height="112" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/encrypting-additional-drives-with-luks-on-linux/luks-logo-cropped.png" width="330"&gt;&lt;/p&gt;
&lt;p&gt;Hopefully it will already be installed because you encrypted your root disk at installation, but if not you can run &lt;code&gt;apt install cryptsetup&lt;/code&gt; to get going.&lt;/p&gt;
&lt;p&gt;The command to do the encryption is actually very simple.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup luksFormat /dev/sdb1 /etc/crypt/keys/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can see that using the &lt;code&gt;cryptsetup&lt;/code&gt; tool we are asking it to execute teh command &lt;code&gt;luksFormat&lt;/code&gt; but while it says format in the command this is a little misleading as it doesn&amp;rsquo;t actually format the disk but just rewrites a portion of bytes at the beginning of the partition to enable encryption. we then tell it the partition we want encrypting, here its &lt;code&gt;/dev/sdb1&lt;/code&gt; and finally we pass in the keyfile we just generated and saved at &lt;code&gt;/etc/crypt/keys/sdb1&lt;/code&gt;. If you omit the keyfile it will still encrypt teh disk but will prompt you to enter the key manually.&lt;/p&gt;
&lt;p&gt;As soon as you press enter you will be warned of teh danager of what you are doing&amp;hellip; so double check you are encrypting the right partition and follow the instructions that should look something like :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WARNING! 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;======== 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This will overwrite data on /dev/sdb1 irrevocably. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Are you sure? (Type uppercase yes): YES 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Command successful. 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And that&amp;rsquo;s it&amp;hellip; the disk is encrypted and ready to use. There are a few ways you can now work with the disk. the quickest and easiest is to just decrypt the disk manually using cryptsetup to &lt;code&gt;open&lt;/code&gt; the disk.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup open /dev/sdb1 sdb1_crypt -d /etc/crypt/keys/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we open &lt;code&gt;/dev/sdb1&lt;/code&gt; and give is a new name of sdb1_crypt and we unlock it using the &lt;code&gt;-d&lt;/code&gt; argument to tell it the keyfile we generated before.&lt;/p&gt;
&lt;p&gt;That is the dis decrypted and ready to roll&amp;hellip; you can now use &lt;code&gt;fdisk -l&lt;/code&gt; or &lt;code&gt;lsblk -p&lt;/code&gt; to confirm that it is now available at &lt;code&gt;/dev/mapper/sdb1_crypt&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda 8:0 0 1.8T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda1 8:1 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├─/dev/sda2 8:2 0 732M 0 part /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sda3 8:3 0 1.8T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sda3_crypt 253:0 0 1.8T 0 crypt 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─/dev/mapper/ubuntu--vg-root 253:1 0 1.8T 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/ubuntu--vg-swap_1 253:2 0 976M 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sdb 8:16 0 3.7T 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└─/dev/sdb1 8:17 0 3.7T 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─/dev/mapper/sdb1_crypt 253:3 0 3.7T 0 crypt /mnt/4tb-1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This tells us that the newly decrypted disk is now available at &lt;code&gt;/dev/mapper/sdb1_crypt&lt;/code&gt; and is a volume of 3.7Tb&amp;hellip; Exactly what we were hoping for!&lt;/p&gt;
&lt;p&gt;All finished with your encrypted disk&amp;hellip; you can just as easily close it again using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup close sdb1_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="setting-up-the-filesystem"&gt;Setting up the Filesystem
&lt;/h2&gt;&lt;p&gt;Ok, we have an encrypted partition, we can decrypt it but we cant mount it yet as we don&amp;rsquo;t have a file system to work with. Let&amp;rsquo;s take care of that real quick by opening up the partition again.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptsetup open /dev/sdb1 sdb1_crypt -d /etc/crypt/keys/sdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now that its available we are going to set up an ext4 filesystem using the command &lt;code&gt;mkfs.ext4 /dev/mapper/sdb1_crypt&lt;/code&gt; which, all going according to plan, should look something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mke2fs 1.44.1 (24-Mar-2018) 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Creating filesystem with 976753664 4k blocks and 244195328 inodes 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem UUID: d797be67-c53e-49d3-897e-c624b21a22d3 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Superblock backups stored on blocks: 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 102400000, 214990848, 512000000, 550731776, 644972544 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Allocating group tables: done 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Writing inode tables: done 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Creating journal (262144 blocks): done 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Writing superblocks and filesystem accounting information: done
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;we are now good to go&amp;hellip; lets try mounting the filesystem with&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -t ext4 /dev/mapper/sdb1_crypt /mnt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m just mounting straight to &lt;code&gt;/mnt&lt;/code&gt; but obviously this can be any folder you want. If the command worked we can easily confirm it with a quick &lt;code&gt;df&lt;/code&gt; -h:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Filesystem 1K-blocks Used Available Use% Mounted on
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;udev 32846708 0 32846708 0% /dev
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 6578140 2308 6575832 1% /run
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/mapper/ubuntu--vg-root 1919562064 993193376 828790500 55% /
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 32890688 200 32890488 1% /dev/shm
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 5120 4 5116 1% /run/lock
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tmpfs 32890688 0 32890688 0% /sys/fs/cgroup
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda2 721392 276068 392860 42% /boot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/sda1 523248 6232 517016 2% /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/mapper/sdb1_crypt 3844637680 0 3844637680 1% /mnt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Excellent&amp;hellip; you can now start working with your new partition&amp;hellip; however lets un-mount and close the drive quickly with a&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;umount /mnt &amp;amp;&amp;amp; cryptsetup close sdb1_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then we can move onto&amp;hellip;&lt;/p&gt;
&lt;h2 id="automatic-decryption"&gt;Automatic Decryption
&lt;/h2&gt;&lt;p&gt;This is a lot simpler that you may realise&amp;hellip; all we need to do is add a new line to the file &lt;code&gt;/etc/crypttab&lt;/code&gt;! But first we need one last piece of information we don&amp;rsquo;t yet have, but we can easily get with the command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo cryptsetup luksDump /dev/sdb1 | grep &amp;#34;UUID&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will use &lt;code&gt;luksDump&lt;/code&gt; to get information about the encrypted partition and then uses &lt;code&gt;grep&lt;/code&gt; to specifically target the property UUID which we will need to identify the partition in the next step.&lt;/p&gt;
&lt;p&gt;Now in your favourite editor of choice add the following line, replacing the spoof UUID here with the one we just found.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sdb1_crypt UUID=1111111111-2222-3333-4444-555555555555 /etc/crypt/keys/sdb1 luks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here we are giving the decrypted volume a unique label for teh decrypted label to be made available at the appropriate &lt;code&gt;/dev/mapper/*&lt;/code&gt; location. We also specify the UUID to identify the partition to decrypt&amp;hellip; we could use the path &lt;code&gt;/dev/sdb1&lt;/code&gt; but using the UUID is more explicit and prevents any confusion if another partition happens to present itself as &lt;code&gt;/dev/sdb1&lt;/code&gt; at some point in the future. Third we have the path to our newly generated keyfile and finally we have the encryption mode that we are using for encryption which here is &lt;code&gt;luks&lt;/code&gt;. For more info on crypttab have a look at &lt;a class="link" href="https://www.freedesktop.org/software/systemd/man/crypttab.html" target="_blank" rel="noopener"
 &gt;https://www.freedesktop.org/software/systemd/man/crypttab.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We can now test that auto decryption is working using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cryptdisks_start sdb1_crypt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which if successful should have an output like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * Starting crypto disk... 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * sdb1_crypt: INSECURE MODE FOR /etc/crypt/keys/sdb1, see /usr/share/doc/cryptsetup/README.Debian. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * sdb1_crypt (starting).. 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; * sdb1_crypt (started)... 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All that&amp;rsquo;s left to do now is set up&lt;/p&gt;
&lt;h2 id="auto-mount-the-filesystem"&gt;Auto-mount the filesystem
&lt;/h2&gt;&lt;p&gt;Hopefully we now are on really familiar ground&amp;hellip; we can now treat &lt;code&gt;/dev/mapper/sdb1_crypt&lt;/code&gt; as a bog standard ext4 partition that can be mounted via the &lt;code&gt;/etc/fstab&lt;/code&gt; by adding the line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/mapper/sdb1_crypt /mnt ext4 defaults 0 2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see its pretty ordinary, exactly as you would expect, obviously swapping out &lt;code&gt;/mnt&lt;/code&gt; with the location of your choice to mount the filesystem. If you are not wholly familiar with &lt;code&gt;fstab&lt;/code&gt; then its definitely worth having a look at &lt;a class="link" href="https://help.ubuntu.com/community/Fstab" target="_blank" rel="noopener"
 &gt;https://help.ubuntu.com/community/Fstab&lt;/a&gt; as it gives a good overview for those who are new to it&amp;hellip;&lt;/p&gt;
&lt;p&gt;Finally we can check that it all works with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mount -a
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And at this point I am pretty sure I can hear the fat lady singing&amp;hellip;&lt;/p&gt;</description></item><item><title>Connecting to Ubuntu 18.04+ using RDP</title><link>https://blog-570662.gitlab.io/connecting-to-ubuntu-18-04-using-rdp/</link><pubDate>Mon, 20 May 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/connecting-to-ubuntu-18-04-using-rdp/</guid><description>&lt;p&gt;We have a mix of different setups that the Software Engineer and Data Scientists use to get their work done. There are some using just Linux on laptops, Some on MacBooks and some on the various versions of Windows.&lt;/p&gt;
&lt;p&gt;For those not using Linux as their primary OS we have a bunch of Desktops that run Ubuntu 18.04+ for them to connect to. SSH can do quite a lot but a few of the team work remotely and in house we prefer RDP for that kind of thing rather than VNC.&lt;/p&gt;
&lt;p&gt;We have had some issues with connections in the past so this post exists to remind me how next time I need to set it up. First we need to install the xRDP server package.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install xrdp
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next we need to ensure that we have the right ports open on the workstation. If like me you also use UFW to manage your firewall rules then open port 3389 using&amp;hellip;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ufw allow &lt;span class="m"&gt;3389&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The issue left, is that you will get an annoying pop up when you log in about a colour management profile needing to be set up and asking you to provide your password. Even then you may still get some annoying crash pop-ups.&lt;/p&gt;
&lt;p&gt;I found a really good solution to this at &lt;a class="link" href="http://c-nergy.be/blog/?p=12043" target="_blank" rel="noopener"
 &gt;http://c-nergy.be/blog/?p=12043&lt;/a&gt; which I&amp;rsquo;ve cribbed and paraphrased below&lt;/p&gt;
&lt;p&gt;Create the file &lt;code&gt;/etc/polkit-1/localauthority/50-local.d/45-allow-colord.pkla&lt;/code&gt; (using your editor of choice and &lt;code&gt;sudo&lt;/code&gt;)and add the following contents&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[Allow Colord all Users]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Identity=unix-user:*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Action=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ResultAny=no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ResultInactive=no
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ResultActive=yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We now need to clear any crash dumps from the workstation&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;sudo&lt;/span&gt; &lt;span class="n"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;crash&lt;/span&gt;&lt;span class="o"&gt;/*&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You should then be good to connect to using whatever RDP client you prefer&amp;hellip; I like &lt;a class="link" href="https://remmina.org/" target="_blank" rel="noopener"
 &gt;Remmina&lt;/a&gt; myself but each to their own.&lt;/p&gt;</description></item><item><title>Technical CV writing is hard</title><link>https://blog-570662.gitlab.io/technical-cv-writing/</link><pubDate>Wed, 15 May 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/technical-cv-writing/</guid><description>&lt;img src="https://blog-570662.gitlab.io/technical-cv-writing/writing-a-cv.jpg" alt="Featured image of post Technical CV writing is hard" /&gt;&lt;p&gt;Recruiting people is effing hard!&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s all&amp;hellip; I&amp;rsquo;ll get back to reading through CVs now and let you get on with your day!&lt;/p&gt;
&lt;p&gt;Medicines Discovery Catapult is, at the time of writing, recruiting Software Engineers, and as &amp;ldquo;Head of&amp;rdquo; it falls to me to start filtering through the CVs that land in my inbox. But what a mess! I get all sorts, from 1-page masterpieces that look amazing and all glossy, but tell me nothing about the individual to 10 pages of war and peace that have so much in it spanning 20 years of experience that 70% is actually completely irrelevant to the job they have applied for.&lt;/p&gt;
&lt;p&gt;So now I get to say that this is what the perfect portrait of a CV should look like&amp;hellip; I&amp;rsquo;m sorry to disappoint but I don&amp;rsquo;t think there is such a thing as a &amp;ldquo;perfect&amp;rdquo; CV. It&amp;rsquo;s far too subjective and open to interpretation. Instead, I&amp;rsquo;m gonna rip apart my own CV and explain why I chose to write it like I have and justify reasons why I would expect to see similar things on the CVs I am forced to read.&lt;/p&gt;
&lt;p&gt;If you want the &amp;ldquo;TL;DR&amp;rdquo; version you can find a copy of my CV at &lt;a class="link" href="https://docs.google.com/document/d/1MM_6nXIVU_wbrvkhdGL5xaJEwc46L7JblUqUot_g1ec" target="_blank" rel="noopener"
 &gt;https://docs.google.com/document/d/1MM_6nXIVU_wbrvkhdGL5xaJEwc46L7JblUqUot_g1ec&lt;/a&gt; but if you have gotten this far you may as well stick around and read the rest. I promise it won&amp;rsquo;t take long.&lt;/p&gt;
&lt;h2 id="the-basics"&gt;The Basics
&lt;/h2&gt;&lt;p&gt;OK! let&amp;rsquo;s start with some general observations I follow with regard to my own CV. Then we can get into the nitty-gritty of how bad mine is!&lt;/p&gt;
&lt;h3 id="recruiters"&gt;Recruiters
&lt;/h3&gt;&lt;p&gt;Love them or loathe them they are a part of the recruitment ecosystem and they will help you get hired. When you send them a copy of your CV bear in mind that they will most likely try to squeeze it into a format that will include their own branding and a cover sheet with some pertinent details on it. They may also then start redacting information off your CV in an effort to anonymise you. It is also possible that they will use the same CV for multiple roles meaning you will need to insist that they send your tailored CV if you are really bothered about winning a specific position.&lt;/p&gt;
&lt;p&gt;Make sure to discuss this with your recruiter before having them put you forward for a role you are keen on.&lt;/p&gt;
&lt;h3 id="keeping-up-to-date"&gt;Keeping Up to Date
&lt;/h3&gt;&lt;p&gt;Design your CV so that it is easy to tweak and update! Keeping your CV up to date is essential, and I don&amp;rsquo;t mean just adding your last role when you finally get fed up with your current employer abusing your good nature and rage quit!&lt;/p&gt;
&lt;p&gt;Keeping your CV in top form means that you need to review the &lt;strong&gt;entire&lt;/strong&gt; thing whenever you make an update. As your career progresses you will find that your perspective will change on what previous roles consisted of and how they would impact/reflect on the position you now desire.&lt;/p&gt;
&lt;p&gt;You will also find that an up-to-date CV is easier to tailor to any specific job you might be applying for&lt;/p&gt;
&lt;h3 id="prettiness"&gt;Prettiness
&lt;/h3&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="154px" data-flex-grow="64" height="300" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-05-15-09-34-26-193x300.png" width="193"&gt;&lt;/p&gt;
&lt;p&gt;I know his name is Riccardo, but I&amp;rsquo;m struggling to focus on anything else&lt;/p&gt;
&lt;p&gt;Having a pretty CV is great! and if creativity is relevant to the vacancy you are hoping to fill then go for it&amp;hellip; make it gorgeous. It is however very subjective so make sure that you understand what your potential employer is looking for. Sometimes less is more, especially considering they may be scanning through hundreds of CVs. If they have to spend half an hour looking for something specific, then it&amp;rsquo;s gotten lost in all that creativity. If you want to see some gorgeous-looking CVs take a look at &lt;a class="link" href="https://weare.guru/creative-cvs/" target="_blank" rel="noopener"
 &gt;https://weare.guru/creative-cvs/&lt;/a&gt; all of them are creative and beautiful&amp;hellip;&lt;/p&gt;
&lt;p&gt;But&amp;hellip; they could take 20 minutes, or more, for someone to read and extract the right information. Which will waste a lot of time for your potential new boss. Plus we are talking about technical CVs and I am not the most creative of individuals, so keeping it clean and well formatted with consistent fonts usage and sizing&lt;/p&gt;
&lt;h3 id="length"&gt;Length
&lt;/h3&gt;&lt;p&gt;I&amp;rsquo;ve heard a lot of people say different things about how long your CV should be. 1 side of A4&amp;hellip; 2 sides of A4 but only if it&amp;rsquo;s printed double-sided, even &amp;ldquo;length doesn&amp;rsquo;t matter&amp;rdquo; because the more information you put in the better.&lt;/p&gt;
&lt;p&gt;I would recommend taking a middle-of-the-road approach. Keeping things concise is paramount&amp;hellip; but if you need 3 or 4 pages then that&amp;rsquo;s OK&amp;hellip; as long as you make the content captivating and interesting for the reader, that is what matters&lt;/p&gt;
&lt;h3 id="pdf"&gt;PDF
&lt;/h3&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="150" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/adobe-pdf-icon-logo-png-transparent-150x150.png" width="150"&gt;&lt;/p&gt;
&lt;p&gt;This one feels like it should be a &amp;ldquo;no brainier&amp;rdquo;&amp;hellip; Make sure to submit your CV as a PDF!&lt;/p&gt;
&lt;p&gt;There are two very good reasons behind this. First, it ensures that the reader will view it exactly as you intended. If you send it over as a Word, Google, OpenOffice or other such documents, then you are not guaranteed the reader will be using the exact same tools. I for one don&amp;rsquo;t use Word and an awful lot of presentation is lost because someone used a fancy word feature or have a font that I can&amp;rsquo;t get hold of.&lt;/p&gt;
&lt;p&gt;Second, While it is not impossible it discourages recruiters from tampering with it and spoiling all your hard work. I have known only a few recruiters in my time that are willing to learn (or pay for) a good PDF editing suite. It is possible for them to alter things, but they tend to have to jump through hoops.&lt;/p&gt;
&lt;h2 id="beginnings"&gt;Beginnings
&lt;/h2&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="1055px" data-flex-grow="439" height="188" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.intro_.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.intro__hu_6f720dcded107b1.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.intro_.png 827w" width="827"&gt;&lt;/p&gt;
&lt;p&gt;And so we have the start of My CV. Looks pretty boring, doesn&amp;rsquo;t it? Black text on a white background. Nothing fancy!&lt;/p&gt;
&lt;p&gt;We have some basic contact details that can be used to contact me and a very short profile statement. That is all I feel an employer needs to see before we get into the next section of my CV. On the surface, it doesn&amp;rsquo;t actually say much but lets scratch a little deeper.&lt;/p&gt;
&lt;h4 id="font"&gt;Font
&lt;/h4&gt;&lt;p&gt;I have specifically chosen a font that I think looks clean and professional, With a nice easy typeface, it becomes easy for an employer to scan the CV. I keep my CV in Google Docs so I went with Raleway, which I think is nice, clean, professional and easy to read.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also chosen consistent font sizes and spacing;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;16 for the main title&lt;/li&gt;
&lt;li&gt;14 for headings&lt;/li&gt;
&lt;li&gt;12 for subheadings&lt;/li&gt;
&lt;li&gt;11 for everything else&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="limiting-liability"&gt;Limiting Liability
&lt;/h4&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="1000" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/matthew-cockayne-bw-square_1000.jpg" srcset="https://blog-570662.gitlab.io/technical-cv-writing/matthew-cockayne-bw-square_1000_hu_3287dab37d2965d3.jpg 800w, https://blog-570662.gitlab.io/technical-cv-writing/matthew-cockayne-bw-square_1000.jpg 1000w" width="1000"&gt;&lt;/p&gt;
&lt;p&gt;Middle-aged, liberal, heterosexual white male programmer Swipe right to Hire Me!!! Please!&lt;/p&gt;
&lt;p&gt;It may sound odd but I put as little personal information in my CV as possible. Hence why just my name and contact details exist. I don&amp;rsquo;t mention my age, gender, race, driver status or political preferences. People come with inherent biases, that is just a fact of life. so putting as little as possible negates triggering these biases.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve seen people put all sorts of information into CVs&amp;hellip; a personal bugbear is photos. As they can introduce a massive amount of opinion in the eyes of the beholder. Think Tinder but for recruitment!&lt;/p&gt;
&lt;p&gt;So unless specifically asked for I would strongly recommend keeping it to a minimum and using the space it would consume in selling the things that matter.&lt;/p&gt;
&lt;h4 id="profile-statement"&gt;Profile statement
&lt;/h4&gt;&lt;p&gt;1 paragraph! that&amp;rsquo;s all I needed to say regarding what is effectively a personal statement. This is not a &lt;a class="link" href="https://www.ucas.com/" target="_blank" rel="noopener"
 &gt;UCAS&lt;/a&gt; application and I don&amp;rsquo;t need to detail a million things about myself. What is to come next will be the real sales pitch&lt;/p&gt;
&lt;p&gt;My profile statement focuses specifically on what my future employer is going to get from me if they hire me. Passion! It is a statement of intent, specifically written to demonstrate that I will strive to bring my &amp;ldquo;A game&amp;rdquo; to anything I do in the future and also that I intend to encourage those around me to do the same. In effect, it is the opening line to what is a sales pitch.&lt;/p&gt;
&lt;p&gt;I could expand on this to state other goals and ambitions but that would just detract from the main objective of the CV. Plus we will get an opportunity to elaborate later on in the recruitment process.&lt;/p&gt;
&lt;h2 id="skills"&gt;Skills
&lt;/h2&gt;&lt;p&gt;Now we come to the meat and bones of the CV. This is the headline! the bit that you will tailor the most in order to impress whoever is scheduling those interviews and guarantee you a chance to shine. If you can make this captivating enough for the person you need to impress they will then be more than happy to read the rest&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="251px" data-flex-grow="104" height="792" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.skill_.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.skill__hu_d839434784c23aee.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.skill_.png 831w" width="831"&gt;&lt;/p&gt;
&lt;p&gt;My CV has a LOT of skills listed. This is mainly because I&amp;rsquo;m a show off more than anything else and in reality, I will tailor this heavily to suit whatever role I&amp;rsquo;m applying for. For example, if it doesn&amp;rsquo;t have a requirement for managerial skills I would drop that section entirely. Depending on the required attributes and skills asked for I would happily add/remove bits to any section of my skills.&lt;/p&gt;
&lt;p&gt;With different types of skills, I will define them differently. As you can see I am quite generic with Managerial skills. A lot of people understand these and are looking for confidence in your ability to do certain types of tasks. Technical skills define different aspects of what I do that are not specifically tied to languages. It can be very difficult to be curt and to the point here. I favour labelling a technology area and then listing a few of the most prominent or recent items from my repertoire. A bad example on my CV is databases&amp;hellip; I can use quite a few as you can see, but in reality, I&amp;rsquo;ve been greedy by listing MySQL, MariaDB &amp;amp; Percona&amp;hellip; they are all effectively connected to each other, but my need to show off is far too much for me to resist.&lt;/p&gt;
&lt;h4 id="experience"&gt;Experience
&lt;/h4&gt;&lt;p&gt;When I first started out as a Dev I had a hiring manager who once told me that most of the time the main thing he wanted to see was;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;did the applicant have the skills he wanted&lt;/li&gt;
&lt;li&gt;what was the candidates&amp;rsquo; personal assessment of their ability&lt;/li&gt;
&lt;li&gt;how much genuine commercial experience do they have&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Three really simple things that stuck with me. So the next time I came to write my CV I added a table for Skills which had only a few things on it back then but has grown massively over the years. I&amp;rsquo;ve tweaked and tailored it over the years, adding and removing things that were relevant as needed.&lt;/p&gt;
&lt;p&gt;By quite clearly labelling my own perception of my skills, I give the person reading it an indication of my potential value. It allows them to tailor the interviews and technical tests to fit me as an individual. It also acts as a double-edged sword as the higher I grade myself the more chance of falling flat on my face when I get asked a question I can&amp;rsquo;t answer&amp;hellip; So it&amp;rsquo;s always better to try and be accurate, but also not to be timid, there are always other competitors in the recruitment race.&lt;/p&gt;
&lt;p&gt;I favour using really simple words to identify my skill level; beginner, intermediate, expert and expert+. It makes it simple for the reader to gauge and also allows me to mix the terms around&lt;/p&gt;
&lt;p&gt;Stating how many years of commercial experience you have lets the reader understand your commercial experience. The number of times I&amp;rsquo;ve had recruiters do a typical keyword match and see that I mention c# once on my CV and then try to wave a job advert under my nose. I did .NET for 6 months 10 odd years ago. I legitimately can claim that experience, but it should not be the core on which I base my next adventure.&lt;/p&gt;
&lt;p&gt;As a useful side effect, by stating a duration in years prompts me to review and update my CV regularly to keep it up to date.&lt;/p&gt;
&lt;h2 id="previous-employment"&gt;Previous Employment
&lt;/h2&gt;&lt;p&gt;By this point, I&amp;rsquo;m hoping that I&amp;rsquo;ve captured the readers&amp;rsquo; imagination. That they now envision a Development God has graced them with a CV worthy of filling any role they have&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="354px" data-flex-grow="147" height="670" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-06-12-12-34-34.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-06-12-12-34-34_hu_b129b9df07d8b244.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/Screenshot-from-2019-06-12-12-34-34.png 990w" width="990"&gt;&lt;/p&gt;
&lt;p&gt;And then come back down to earth with a bump! Next up in the firing line is Work Experience. Here I define some of the previous positions I have held in my illustrious career in somewhat chronological order. I say &lt;strong&gt;some&lt;/strong&gt; because I have a general rule of limiting what I put on the CV to either 10 years or 10 positions, whichever comes first. This hasn&amp;rsquo;t been a hard and fast rule over the years, and it&amp;rsquo;s fine to flex in order to suit the roles I&amp;rsquo;ve gone for. There is no reason whatsoever though to go so far back in time as to describe my time as a pot-washer when I was 17.&lt;/p&gt;
&lt;p&gt;In some cases, I&amp;rsquo;ve also omitted some things from my CV. Somewhere in and around 2012 - 2015; I happened to found and run a co-working space in Manchester City Centre. It was an interesting venture that I&amp;rsquo;m really proud of and am glad to say is still running even though I&amp;rsquo;m no longer a part of it. But in truth, it does not add anything of value to my CV for the positions I plan to go for in the future.&lt;/p&gt;
&lt;h4 id="to-the-point"&gt;To the point
&lt;/h4&gt;&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="360px" data-flex-grow="150" height="200" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/bigstock-From-Point-A-To-Point-B-41405269-300x200.jpg" width="300"&gt;&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t like big sprawling paragraphs of text if you haven&amp;rsquo;t already got the gist. So for me, bullet points are the way forward. I try to keep it fairly obvious and detail achievements and document things I have done, and not the tools I have used.&lt;/p&gt;
&lt;p&gt;I try to explain why I joined the company and what my purpose was. I highlight important successes and demonstrate improvements I made to the company. Short succinct sentences should be chosen to illustrate aspects of your accomplishments that actually relate to your future goals in your new position.&lt;/p&gt;
&lt;p&gt;By doing it this way you also make it easier to tweak and change things without having to restructure a whole piece of prose in the future.&lt;/p&gt;
&lt;p&gt;That said my own CV has a glaring exception! When I was doing freelance work I was not able to accurately describe everything I was doing due to some pesky Non Disclosure Agreements. So instead I have a simple paragraph providing a positive high-level explanation of what benefits I brought to my clients.&lt;/p&gt;
&lt;h4 id="formatting"&gt;Formatting
&lt;/h4&gt;&lt;p&gt;There is a lot of information condensed into this section which makes could make it hard to read if left unformatted. I stuck to the font sizes I had chosen previously and decided to use a more subtle combination of indentation, italics, underline and bold to make it more pleasing to the eye and easier to scan.&lt;/p&gt;
&lt;p&gt;Each company name acts as a subheading with a font size of 12. I take a little liberty and include on the same line the dates that I was with them. This gives a clear timeline of events that a manager can then refer to quickly when they need it, such as in an interview.&lt;/p&gt;
&lt;p&gt;Indenting everything under a subheading makes it easier for the reader to separate out the content easily. I include an address for the company. I am not actually sure why if I&amp;rsquo;m honest, it&amp;rsquo;s just something I&amp;rsquo;ve always done. I put this in Italics mainly because it&amp;rsquo;s an aside to the core of the information.&lt;/p&gt;
&lt;p&gt;The job title will come next in bold of course to help it stand out. Followed immediately by the relevant bullet points. My last role obviously plays the most prominent part as it&amp;rsquo;s my headliner. In the case of Medicines Discovery Catapult. I&amp;rsquo;ve held two roles, &amp;ldquo;DevOps Engineer&amp;rdquo; and &amp;ldquo;Head of Software Engineering&amp;rdquo; so I break these into their own sections within this piece of experience. Prior to that, I was with &lt;a class="link" href="https://wakelet.com/" target="_blank" rel="noopener"
 &gt;Wakelet&lt;/a&gt; and here I merge the two roles I held there in order to conserve space.&lt;/p&gt;
&lt;h2 id="education"&gt;Education
&lt;/h2&gt;&lt;p&gt;The bulk of the hard work is now done! We have put in the sales pitch and hopefully, we are close to being invited to an interview. Time to put in some supporting information&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="704px" data-flex-grow="293" height="281" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.education.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.education_hu_b537610f276f7de3.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.education.png 825w" width="825"&gt;&lt;/p&gt;
&lt;p&gt;This may come as a surprise to some people but I didn&amp;rsquo;t do the whole University thing&amp;hellip; I mean I lived in a University city and frequented the student union bars, but was never actually enrolled in a course. To this effect, I bolstered my CV when I started out by going and obtaining some (now significantly outdated) professional certifications.&lt;/p&gt;
&lt;p&gt;Regardless of my lack of educational achievements I would always recommend keeping it simple unless it is your first position and you have no work experience (writing a CV in that situation probably needs to be another blog post entirely). List them in chronological order with some dates and a summary of what you obtained&amp;hellip; no one needs to know that I completely fluked getting a GCSE in Art.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="426px" data-flex-grow="177" height="169" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/educ-300x169.jpg" width="300"&gt;&lt;/p&gt;
&lt;p&gt;Were I to have a Degree I would obviously have the institution and dates in there along with my final marks. I would also look at listing the modules I completed that are relevant to my career, providing I did a Computer Sciences degree and not something like Biology. Again, it&amp;rsquo;s all about keeping to the point and providing specific information to bolster everything else you may have done.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping up
&lt;/h2&gt;&lt;p&gt;Time to finish off with a little bit of something personal. I don&amp;rsquo;t want to spend too much time here but I want to show that there is more to who I am than just work. I decided to keep it simple, a simple list of things that I enjoy doing in my spare time (When I have any, being a father of 3). The idea here is that these can become conversation pieces with the people that may be interviewing you. I&amp;rsquo;ve ended up a number of times having interviews where I talk about Scouts.&lt;/p&gt;
&lt;p&gt;&lt;img class="gallery-image" data-flex-basis="904px" data-flex-grow="377" height="222" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/technical-cv-writing/cv.prsonal.png" srcset="https://blog-570662.gitlab.io/technical-cv-writing/cv.prsonal_hu_1a75be4cf4f02610.png 800w, https://blog-570662.gitlab.io/technical-cv-writing/cv.prsonal.png 837w" width="837"&gt;&lt;/p&gt;
&lt;p&gt;The final flourish here should be something simple but gives someone a helping hand at learning more about you if they feel so inclined. I include a link to my blog and my GitHub account, but you could include anything that may be relevant.&lt;/p&gt;
&lt;p&gt;Last but not least&amp;hellip; References are always available on request. My referees are varied and have changed over time. Adding them to your CV doesn&amp;rsquo;t actually impart any other information that could get you hired. Name-dropping is &lt;strong&gt;not&lt;/strong&gt; the right way to get a job.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion
&lt;/h2&gt;&lt;p&gt;OK! so it&amp;rsquo;s not a pretty CV by any stretch of the imagination. It&amp;rsquo;s a bit on the long side in its full un-tailored, raw form, though not as long as it could be if I wasn&amp;rsquo;t being diligent in how I want to present myself. But this is the format I&amp;rsquo;ve used as my CV for at least 15 years now and I would say that I&amp;rsquo;ve been really successful in getting interviews out of it. I would say I get at least an 80% success rate of conversions from seeing my CV to a first-stage interview (watch me now jinx myself for the future).&lt;/p&gt;
&lt;p&gt;A CV will never get you the job! It&amp;rsquo;s all down to you excelling in an interview situation and proving how awesome you are and that you can do everything you say you can on your CV. All it is meant to do is get your foot in the door. Hopefully, this breakdown of my CV will help you to take a look at your own CV and work on ways to improve your chances.&lt;/p&gt;</description></item><item><title>Dell DisplayLink D6000 &amp; Ubuntu 18.04+ Issues</title><link>https://blog-570662.gitlab.io/dell-displaylink-d6000-ubuntu-18-04-issues/</link><pubDate>Tue, 14 May 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/dell-displaylink-d6000-ubuntu-18-04-issues/</guid><description>&lt;img src="https://blog-570662.gitlab.io/dell-displaylink-d6000-ubuntu-18-04-issues/20190514_124153.png" alt="Featured image of post Dell DisplayLink D6000 &amp; Ubuntu 18.04+ Issues" /&gt;&lt;p&gt;I love Ubuntu&amp;hellip; I&amp;rsquo;m pretty fond of dell kit too!&lt;/p&gt;
&lt;p&gt;So I was rather chuffed when I started working at &lt;a class="link" href="https://md.catapult.org.uk" target="_blank" rel="noopener"
 &gt;Medicines Discovery Catapult&lt;/a&gt; because they let me have both. When you look at my desk it looks like it could be an advert for Dell. Laptop, monitors, dock, keyboard and mouse&amp;hellip;. its great when you have a corporate account with a Dell reseller&lt;/p&gt;
&lt;p&gt;However while I&amp;rsquo;ve had a lot of success with the D3000 DisplayLink dock on Ubuntu I found that I&amp;rsquo;m now having to deal with the upgraded D6000&amp;hellip; which doesn&amp;rsquo;t play very nicely with the more recent versions of Ubuntu (we are talking 18.04 and later)&lt;/p&gt;
&lt;p&gt;I kept finding that after a random amount of time the D6000 would randomly seem to power down&amp;hellip; I would lose the screens, audio, networking and USB. and the only way I could fix it is to unplug it from teh laptop and plug it back in. Not ideal, especially if I&amp;rsquo;m in the middle of a video call or debugging something on the net&lt;/p&gt;
&lt;p&gt;Being the kind of techie I am my first port of call checking my logs&amp;hellip; but I couldn&amp;rsquo;t see anything that would cause this random disconnect. So off to google I went&amp;hellip; eventually I found a lot of information telling me it was part of power management causing things to start powering down&amp;hellip; In this case it implied that it was something trying to suspend USB&amp;hellip; which sounded really plausible!&lt;/p&gt;
&lt;p&gt;So a little more research suggested that I should be using laptop mode tools to disable the ability for USB to be suspended. I gave it a go, though I was dubious as in my mind I shouldn&amp;rsquo;t have needed to install an additional package (albeit a great one for tweaking your power management on a laptop running Linux)&lt;/p&gt;
&lt;p&gt;Alas no joy! And I had too much to do to start debugging in depth and ripping apart other peoples code to figure it out.&lt;/p&gt;
&lt;p&gt;What did I do? you ask. Well, I just put up with it for a few weeks, but gradually it began to grate on my nerves. However there was that one day where it didn&amp;rsquo;t turn off&amp;hellip; and that left me perplexed&amp;hellip; I checked if any updates had been applied in my last &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade&lt;/code&gt; &amp;hellip; nothing&amp;hellip;. it then dawned on me that I had plugged in the headset I used for conference calling into the audio in/out on the dock instead of directly into the laptop.&lt;/p&gt;
&lt;p&gt;Now I had a little more information I was able to deduce (with googles help) that the laptop was actually suspending USB, but that the trigger was actually pulseaudio. At this point it becomes really easy to solve the problem.&lt;/p&gt;
&lt;h2 id="solution"&gt;Solution
&lt;/h2&gt;&lt;p&gt;Edit &lt;code&gt;/etc/pulse/default.pa&lt;/code&gt; using your preferred editor (and &lt;code&gt;sudo&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;Find the line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;### Automatically suspend sinks/sources that become idle for too long&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;load-module module-suspend-on-idle
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And comment it out and save!&lt;/p&gt;
&lt;p&gt;Lastly, because its run as a user service you need to restart the Pulse Audio daemon using the command&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;systemctl --user restart pulseaudio.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or you could just logout and back in again&lt;/p&gt;</description></item><item><title>A reboot and a legacy moniker</title><link>https://blog-570662.gitlab.io/a-reboot-and-a-legacy-moniker/</link><pubDate>Tue, 26 Feb 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/a-reboot-and-a-legacy-moniker/</guid><description>&lt;p&gt;So&amp;hellip; my last post was a good 2 years ago now&amp;hellip;. Hi how have you been?&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been a very busy couple of years with a lot of stuff shifting in my personal life meaning things inevitably take a back seat. However its been long enough that I needed to give myself a new start and see about blogging again and getting back into speaking again.&lt;/p&gt;
&lt;h3 id="still-phpboyscout"&gt;Still PHPBoyScout?
&lt;/h3&gt;&lt;p&gt;Since 2016 I&amp;rsquo;ve jumped around in my career a lot! This has exposed me to a lot of new languages and tech, all of which has been awesome but it now means that the moniker of PHPBoyScout probably isn&amp;rsquo;t accurate all that much any more. That said I still have a love for PHP and my first thought when presented with a new challenge is still &amp;ldquo;How would I approach this using PHP?&amp;rdquo;, so I think that means I can keep my twitter handle for a little longer rather than re-brand myself :-D&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve always had a penchant for picking up languages pretty quickly and have been an advocate of the idea that as long as you can think in the right way then languages can be learnt easily enough.&lt;/p&gt;
&lt;h3 id="employed-and-employable"&gt;Employed and Employable
&lt;/h3&gt;&lt;p&gt;October 2017 saw me take on a new role with a company called Medicines Discovery Catapult. They are a grant funded not for profit organisation that is specifically focused on shaking up the medicine discovery pipeline by helping SMEs &amp;amp; CROs innovate and collaborate.&lt;/p&gt;
&lt;p&gt;My role is very broad as I appear to have fallen in to the role of Head of Software Engineering very quickly. Though I started as a DevOps Engineer it has evolved very quickly. We work with a wide array of languages and tools and nothing is off the table if it helps to solve the problems we are working on. Currently we actively have code being written in Python, Node, Go &amp;amp; Scala (I live in hope that I&amp;rsquo;ll be ale to bring PHP into that mix eventually). Vast data sets and AI are the name of the game with my team of engineers helping to support the data scientists and Informaticians.&lt;/p&gt;
&lt;p&gt;At the time of writing we are also hiring so if you are curious take a look at &lt;a class="link" href="https://md.catapult.org.uk/about/careers/" target="_blank" rel="noopener"
 &gt;https://md.catapult.org.uk/about/careers/&lt;/a&gt; and see if you like the sound of what we are doing and the challenge on offer.&lt;/p&gt;
&lt;h3 id="moving-forward"&gt;Moving forward
&lt;/h3&gt;&lt;p&gt;The plan moving forward is to post about the tech we are working with, talk about the types of challenges we are facing and also maybe restart my public speaking career. Stay tuned for more and if you don&amp;rsquo;t hear from me soon then give me a nudge&lt;/p&gt;</description></item><item><title>Project Slayer: The Critical Path</title><link>https://blog-570662.gitlab.io/project-slayer-the-critical-path/</link><pubDate>Tue, 26 Feb 2019 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/project-slayer-the-critical-path/</guid><description>&lt;img src="https://blog-570662.gitlab.io/project-slayer-the-critical-path/c7c3a029d172b33287003d26a0c693f9.png" alt="Featured image of post Project Slayer: The Critical Path" /&gt;&lt;p&gt;The first of my new round of talk abstracts! In all honesty this isn&amp;rsquo;t a talk but something more that came out of a very drunken Saturday night at #phpbnl19. There were a bunch of us sat talking and somehow the topic of D&amp;amp;D came up which sent my mind racing with this idea&amp;hellip; By the time 2am rolled around I had a fully formed idea along with some willing players to help with the idea. Now I just need to find a conference willing to take a chance on it! If you know a conference that would be interested let me know&lt;/p&gt;
&lt;h3 id="descriptionabstract"&gt;&lt;strong&gt;Description/Abstract&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;Our heroes have just completed our latest quest! Having successfully delivered the latest iteration of their project they are approached by the aged and holy sage &amp;ldquo;PeeEm&amp;rdquo; with a new quest! How quickly can they implement the sacred and forgotten art of &amp;ldquo;Lo-ging&amp;rdquo; into the codebase&lt;/p&gt;
&lt;p&gt;Do they accept? If they do will they be able to defeat the trials and tribulations that await them? Will they find treasure and glory, or suffer defeat at the hands of the vile and depraved Stakeholder?&lt;/p&gt;
&lt;h3 id="what-will-people-learn"&gt;&lt;strong&gt;What will people learn&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;The only way to find out if our heroes will complete this epic quest will be to join us and see if the dice of fate will be kind to them!&lt;/p&gt;
&lt;p&gt;Along the way we will will learn some truths about feature implementation and how our heroes handle the challenges that lie ahead. And hopefully gain enough experience to level up!&lt;/p&gt;
&lt;h3 id="additional-information"&gt;&lt;strong&gt;Additional Information&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;This is an extremely unique talk! It takes the form of a live Dungeons &amp;amp; Dragons game. I will be on stage playing the part of Dungeon Master looking to guide our team of adventurers through the process of delivering a new feature for a project.&lt;/p&gt;
&lt;p&gt;Our heroes currently consist of a heroic bard who will be inspiring our heroes, and audience, with ballads of past glories. A warlock with the demonic power to fork and merge code like no other in existence&amp;hellip;. Our sorcerer has the innate and wild magic of Fire(base)! Finally our team is held together with the support of our cleric, worshipping the ancient god Rasmus.&lt;/p&gt;
&lt;p&gt;The outcome of this adventure will genuinely be determined by the roll of the dice! It will be a game of 5th edition D&amp;amp;D that we can probably fit into an hour&amp;hellip; but 2 or more would be better and far more fun.&lt;/p&gt;
&lt;p&gt;Audience participation is expected! Cosplay is hoped for! I will, of course, be in full scout uniform!&lt;/p&gt;</description></item><item><title>Monty Python explains why your project failed!</title><link>https://blog-570662.gitlab.io/monty-python-explains-project-failed/</link><pubDate>Sun, 07 Feb 2016 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/monty-python-explains-project-failed/</guid><description>&lt;img src="https://blog-570662.gitlab.io/monty-python-explains-project-failed/9780563558200.jpg" alt="Featured image of post Monty Python explains why your project failed!" /&gt;&lt;p&gt;As part of the attempt to develop my profile as a speaker, I&amp;rsquo;ve realised that I sometimes need to explain a few of my current talk abstracts a bit too much. This is mainly due to my lack of experience writing them and that the majority of my current talk ideas cover large topics that are not as technical as I would like.&lt;/p&gt;
&lt;p&gt;My favourite so far is one title &lt;strong&gt;&amp;ldquo;Python explains why your project failed&amp;rdquo;&lt;/strong&gt;. This is a tongue in cheek talk which aims to poke fun at the Developer, PM and of course the client!&lt;/p&gt;
&lt;h2 id="the-tldr"&gt;The TL;DR
&lt;/h2&gt;&lt;p&gt;This talk has yet to be accepted by anyone&amp;hellip; but will be eventually I hope. In the mean time I wanted to share some of the funny thoughts and comparisons I&amp;rsquo;ve had coming up with the content for the talk. I plan on doing this by writing a series of blog posts one for each topic or sketch that features in the talk.&lt;/p&gt;
&lt;h2 id="the-abstract"&gt;The Abstract
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Python is fantastic! If you haven’t seen it you really need to. Its simple, elegant, powerful and gives you an amazing perspective on what we do as Developers, it is also hilariously funny…. Yes Funny!!!!&lt;/p&gt;
&lt;p&gt;Wait! You thought I was talking about Python the programming language didn&amp;rsquo;t you? I&amp;rsquo;m sorry to tell you but we are actually talking about the most awesome of British comedy acts&amp;hellip; Monty Python.&lt;/p&gt;
&lt;p&gt;Throughout this talk I will take you through the development life-cycle of a project and use the Comedy of Monty Python to illustrate both the Good and the Bad (mainly the bad) aspects of our industry. All the way from Client introduction, requirements gathers, spec writing, team selection, planning and scoping all the way through Development to Testing, Delivery and Support!&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="the-delivery"&gt;The Delivery
&lt;/h2&gt;&lt;p&gt;This is a little harder to explain as I&amp;rsquo;ve not given the talk (yet) and I don&amp;rsquo;t think I could ever match the delivery better than the Pythons themselves.&lt;/p&gt;
&lt;p&gt;However I have been known to dress for the occasion, so it&amp;rsquo;s quite possible that you may find me standing on stage in a red cassock at some point.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve also managed to convince &lt;a class="link" href="https://twitter.com/phpcodemonkey" target="_blank" rel="noopener"
 &gt;@phpcodemonkey&lt;/a&gt;, who is as big a Python fan as myself, that this talk should really be performed as a 2 man show, rather than me monologuing at a room full of people.&lt;/p&gt;
&lt;h2 id="the-sketches"&gt;The Sketches
&lt;/h2&gt;&lt;p&gt;Due to the prolific variety of skits and sketches that Pythons created I found it extremely hard to select the few needed to fill a single talk. I have however managed to select a few and will change them around from time to time to suit the audience. I&amp;rsquo;ve provided a short list of a few of my favourites Sketches and a couple of words describing what they explain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spanish inquisition&lt;/strong&gt; - Client Indecision&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dead parrot sketch&lt;/strong&gt; - Stubborn Project Managers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ministry of silly &lt;em&gt;(array)&lt;/em&gt; walks&lt;/strong&gt; - Tool Selection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Brian&amp;rsquo;s Latin Lesson&lt;/strong&gt; - Planning and Preparation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We demand a shrubbery&lt;/strong&gt; - Demanding the Impossible&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Black knight&lt;/strong&gt; - Solution fixation / Code Blindness&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Camelot Song&lt;/strong&gt; - Stakeholder Morale&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The People&amp;rsquo;s Front&lt;/strong&gt; - Team Fragmentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Silly Job Interview&lt;/strong&gt; - Stakeholder Communications&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Four Yorkshireman&lt;/strong&gt; - Rockstar Developers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Argument clinic&lt;/strong&gt; - Product Delivery&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architects sketch&lt;/strong&gt; - Taking Shortcuts and Cutting Corners&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-finale"&gt;The Finale
&lt;/h2&gt;&lt;p&gt;These are just a few of the potential topics I will be looking to cover in the coming posts, but while your reading them I want you to remember to&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://youtu.be/WlBiLNN1NhQ" target="_blank" rel="noopener"
 &gt;https://youtu.be/WlBiLNN1NhQ&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Are you a Good Code Scout? - NomadPHP lightning talk video</title><link>https://blog-570662.gitlab.io/good-code-scout-nomadphp-lightning-talk-video/</link><pubDate>Wed, 06 Jan 2016 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/good-code-scout-nomadphp-lightning-talk-video/</guid><description>&lt;p&gt;&lt;a class="link" href="https://www.youtube.com/watch?v=Tt0lnauF5lI" target="_blank" rel="noopener"
 &gt;https://www.youtube.com/watch?v=Tt0lnauF5lI&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Just before the Christmas period I was lucky enough to be able to give my &amp;ldquo;Are you a good Code Scout?&amp;rdquo; talk as a lightning talk for NomadPHP. Here is the video that was recorded from it.&lt;/p&gt;</description></item><item><title>Using Gmail aliases with Evolution</title><link>https://blog-570662.gitlab.io/using-gmail-aliases-with-evolution/</link><pubDate>Wed, 06 Jan 2016 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/using-gmail-aliases-with-evolution/</guid><description>&lt;p&gt;If your anything like me you have a large number of email aliases that you use with Gmail which is great. However I use &lt;a class="link" href="https://wiki.gnome.org/Apps/Evolution" target="_blank" rel="noopener"
 &gt;Evolution&lt;/a&gt; as a mail client more often than not when using &lt;a class="link" href="https://www.gnome.org/" target="_blank" rel="noopener"
 &gt;Gnome3&lt;/a&gt; as a desktop.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s very easy to set up Evolution to create separate outbound email accounts that you can use for handling all of your aliases. It doesn&amp;rsquo;t yet support OAuth2 as an authentication mechanism for any account that is not set up using the built-in Gnome Online Accounts integration.&lt;/p&gt;
&lt;p&gt;This is a real pain as Google have disabled the more common &amp;lsquo;plain&amp;rsquo; and &amp;rsquo;login&amp;rsquo; authentication mechanisms for use with an SMTP only account. Meaning that any time that you try to connect to smtp.gmail.com:587 with STARTTLS you will get some form of error message to the effect of &amp;ldquo;Bad Authentication&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Hopefully I&amp;rsquo;ll find a workaround at some point in the near future or Evolution will add the facility to enable OAuth2 as an available authentication mechanism.&lt;/p&gt;
&lt;p&gt;In the mean time there is a workaround if you visit &lt;a class="link" href="https://www.google.com/settings/security/lesssecureapps" target="_blank" rel="noopener"
 &gt;https://www.google.com/settings/security/lesssecureapps&lt;/a&gt; you can enable these less secure authentication mechanisms allowing you to once again connect and send email via email addresses using SMTP&lt;/p&gt;</description></item><item><title>A metaphor about PSR-7 and Middleware for non-developers</title><link>https://blog-570662.gitlab.io/metaphor-psr7-middleware/</link><pubDate>Thu, 08 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/metaphor-psr7-middleware/</guid><description>&lt;img src="https://blog-570662.gitlab.io/metaphor-psr7-middleware/fire-bucket-brigade.jpg" alt="Featured image of post A metaphor about PSR-7 and Middleware for non-developers" /&gt;&lt;p&gt;Never one to shy away from coming up with a metaphor for explaining something technical I found myself having to come up with one on the spot for PSR-7 and Middleware while at the recent PHPNW15 Conference.&lt;/p&gt;
&lt;p&gt;Normally my brain will come up with something completely inappropriate but this time round I found I quite liked the imagery that came to mind.&lt;/p&gt;
&lt;p&gt;If you would like to find out more of the specifics about PSR-7 you can take a look at &lt;a class="link" href="http://www.php-fig.org/psr/psr-7/" target="_blank" rel="noopener"
 &gt;http://www.php-fig.org/psr/psr-7/&lt;/a&gt; which will make a better job of explaining it than I could ever do.&lt;/p&gt;
&lt;p&gt;Now on to the metaphor&lt;/p&gt;
&lt;p&gt;Imagine a house on fire, a bizarre way to start I know but bear with me. The nearest well with water that can put out the fire is 500 meters away! We then have a human chain stretching between the well and the house with a bucket going back and forth between trying to put the fire out. So lets break this down, the house represents the internet, or more specifically you and your browser. The fact you are on fire means that you are desperately needing water to quench the flames. At this point you send an empty bucket which represents your &amp;ldquo;request&amp;rdquo;, along the human chain, which in itself represents the application, to the well.&lt;/p&gt;
&lt;p&gt;At the start of the chain the bucket is pretty normal, it&amp;rsquo;s a bucket of course, its round, made of wood with a rope handle, lets say it has a small leak in it.&lt;/p&gt;
&lt;p&gt;As it travels down the chain it&amp;rsquo;s passed from person to person, everyone in it has the opportunity to do something with the bucket, or not as the case may be and could just pass it to the next person in the chain. Others may attempt to fix the leak in the bucket, someone may choose to replace it with a metal bucket, change the handle or make it bigger. Regardless of what may be done to the bucket in essence it remains a bucket.&lt;/p&gt;
&lt;p&gt;&lt;img alt="colonial_bucket3" class="gallery-image" data-flex-basis="240px" data-flex-grow="100" height="500" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/metaphor-psr7-middleware/colonial_bucket3.jpg" width="500"&gt;&lt;/p&gt;
&lt;p&gt;Inexorably the bucket will continue to move down the chain to the well. When it reaches the well it changes state because now it has been filled with water. All of the interaction with the bucket thus far, mean that what happens at the well could vary depending on the changes have been made . If its been made bigger, for example, it could be filled with significantly more water, if swapped for a metal one it could imply that the bucket descends the well to get the water quicker because its heavier. Either way it is filled with water and begins its journey back towards the house.&lt;/p&gt;
&lt;p&gt;Again it passes through the hands of each person in the chain, but now that its state has changed it now has the opportunity to be modified again. Someone may empty some water out as there is too much in the bucket, others may say that there is not enough and send it back down the line towards the well to be refilled. Either way the bucket continues to change hands over and over until it reaches the house and the contents are thrown on the fire to complete the request for water.&lt;/p&gt;
&lt;p&gt;During this whole time the human chain could have been in flux. Some people may have swapped places, left the chain, added to the chain, some extraordinary people may have played leapfrog in the chain and appeared to handle the bucket more than once. Regardless of these changes the chain remains and continues to pass the bucket from one person to the another as long as the requests for water keep coming.&lt;/p&gt;
&lt;p&gt;This, in the simplest possible form, explains PSR-7 and the concept of Middleware.&lt;/p&gt;
&lt;p&gt;The bucket remains a bucket because PSR-7 says that is what is needed to complete the request for water, it also defines how you should interact with it regardless of what modifications have been made. If the bucket cant be used according to how PSR-7 describes a bucket to be, then the middleware can&amp;rsquo;t complete the request.&lt;/p&gt;
&lt;p&gt;Every person in the human chain can be classed as a piece of middleware all the way from the house to the well and back again. If at any point someone enters the chain that doesn&amp;rsquo;t agree that the bucket is a bucket or doesn&amp;rsquo;t know how to handle it, then the it is dropped on the ground and the request fails.&lt;/p&gt;</description></item><item><title>Badges &amp; Stickers</title><link>https://blog-570662.gitlab.io/badges-and-stickers/</link><pubDate>Sun, 04 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/badges-and-stickers/</guid><description>&lt;img src="https://blog-570662.gitlab.io/badges-and-stickers/Screen-Shot-2015-10-04-at-12.03.38.png" alt="Featured image of post Badges &amp; Stickers" /&gt;&lt;p&gt;One of the most prominent things I&amp;rsquo;ve been asked about regarding my promoting being a Good Code Scout, is where can we get the badges?&lt;/p&gt;
&lt;p&gt;Following on from a number of questions and subsequent tweets about it&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="https://twitter.com/stuherbert/status/650591775732670466" target="_blank" rel="noopener"
 &gt;https://twitter.com/stuherbert/status/650591775732670466&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well&amp;hellip; I&amp;rsquo;ve decided that (providing I can get permission from all the right people) I&amp;rsquo;ll start creating a range of Badges &amp;amp; Stickers for you to earn as a Good Code Scout.&lt;/p&gt;
&lt;p&gt;So if your interested in having some stickers or badges let me know using the form below and if I get enough interest I will most definitely get some made up for you.&lt;/p&gt;
&lt;h2 id="update"&gt;Update
&lt;/h2&gt;&lt;p&gt;Unfortunately I&amp;rsquo;m have no more stickers left, Though I&amp;rsquo;ll be working on diversifying some of the designs in the near future I wont be looking at ordering any more for a little while yet. Keep your eyes on twitter as I&amp;rsquo;ll most likely post there when they are available again.&lt;/p&gt;</description></item><item><title>Wow... What a Conference</title><link>https://blog-570662.gitlab.io/wow-phpnw15-conference/</link><pubDate>Sun, 04 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/wow-phpnw15-conference/</guid><description>&lt;p&gt;So I attended the PHPNW15 conference this weekend and what a weekend. I&amp;rsquo;ve been an attendee of the conference for a number of years and have always enjoyed it immensely. However this year turned out to be something special.&lt;/p&gt;
&lt;p&gt;Following on from my first ever appearance as the PHP Boy Scout I decided to submit to the Unconference at PHPNW15.&lt;/p&gt;
&lt;p&gt;It was a good talk, an extension of the previous lightening talk I&amp;rsquo;d given and felt really good to give. Unbeknownst to me however there was mischief afoot. Normally the Unconference talks are rated by the organisers and the one that they selected as the best gets to have a guaranteed slot in next years PHPNW conference. All of which I had genuinely either no idea about or had forgotten had happened in previous conferences,&lt;/p&gt;
&lt;p&gt;As you may have guessed from the fact this post exists, I ended up winning that slot.&lt;/p&gt;
&lt;p&gt;However&amp;hellip;. it appeared that a Speaker had taken ill at the last minute and couldn&amp;rsquo;t make it at which point I was asked a mere 3 minutes before it was announce that I was also going to be given the hangover slot on track three for the Sunday sessions!!!!!&lt;/p&gt;
&lt;p&gt;Suffice to say I had an interesting evening to say the least, in preparing my talk for &amp;ldquo;the big time&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Amazingly I felt really calm about everything, and even had a good chuckle about managing to find some props to help break the ice!&lt;/p&gt;
&lt;p&gt;Everything is ready, I&amp;rsquo;ve practiced, knowing my talk was going to be a bit short&amp;hellip; but that was ok considering the short notice, and I had plenty of anecdotes I could use as filler. I&amp;rsquo;m sat there waiting for the moment I have to put my head above the parapet and all of a sudden&amp;hellip;&amp;hellip;..&lt;/p&gt;
&lt;p&gt;nothing&lt;/p&gt;
&lt;p&gt;My mind goes blank!&lt;/p&gt;
&lt;p&gt;The long and the short is that I survived, and the feedback I have had has been amazing and I&amp;rsquo;ll be taking all of it on board to make sure that next time its even better!&lt;/p&gt;
&lt;p&gt;The recordings should be available in the near future so when they are I will share a link so you can judge how it went for yourselves. In the mean time I&amp;rsquo;ve published the revised slide deck for you on &lt;a class="link" href="http://slideshare.net/phpboyscout/are-you-a-good-scout-phpnw15-track-3" target="_blank" rel="noopener"
 &gt;slideshare.net/phpboyscout/are-you-a-good-scout-phpnw15-track-3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m hoping that I can now find some opportunities to practice for my slot at #phpnw16&lt;/p&gt;
&lt;p&gt;[slideshare id=53508432&amp;amp;doc=areyouagoodscout-151004073451-lva1-app6892]&lt;/p&gt;</description></item><item><title>What is a PHP Scout</title><link>https://blog-570662.gitlab.io/php-scout/</link><pubDate>Fri, 02 Oct 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/php-scout/</guid><description>&lt;p&gt;Recently I&amp;rsquo;ve had a lot of people asking me what a PHP Scout is! I thought it would be a good opportunity to explain.&lt;/p&gt;
&lt;p&gt;To understand what a PHP Scout is it helps to know a little of the background basics of Scouting in general. Knowing this helps to make it easier later on as well as we draw some direct parallels. If you would like to investigate more about the history of Scouting you can find a good starting point at &lt;a class="link" href="http://scouts.org.uk/about-us/history/" target="_blank" rel="noopener"
 &gt;http://scouts.org.uk/about-us/history/&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For now I&amp;rsquo;m going to give a tl;dr version;&lt;/p&gt;
&lt;p&gt;Scouting started in 1908 as a movement for training young people to encourage them to develop physically, mentally and spiritually by Robert Baden-Powell. Over the next 100+ years it has evolved to encompass people of all ages, races, colours and creeds to get involved and try to be the best they can be.&lt;/p&gt;
&lt;p&gt;The primary ethos of the movement today is to bring Everyday Adventure to young people and this is achieved through a comprehensive programme scheme that is designed to touch on all aspects of that young persons development. This is then rewarded in a variety of ways with the primary reward being the experience itself, the awarding of badges also strengthens then sense of achievement and desire to work towards the next goal.&lt;/p&gt;
&lt;p&gt;All members of the Scouting movement are required to make and frequently renew a promise:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;On my honour, I promise that I will do my best to do my duty to {insert deity/monarchy here}, to help other people and to keep the Scout Law.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;The key part here is &lt;strong&gt;I will do my best&lt;/strong&gt;. Scouts are continually encouraged to improve themselves in everything they do.&lt;/p&gt;
&lt;h2 id="the-boy-scout-rule"&gt;The Boy Scout Rule
&lt;/h2&gt;&lt;p&gt;Lets start with something easy! There is a pretty common piece of guidance that gets bandied about in a lot of different circles that is normally referred to as &amp;ldquo;The Boy Scout Rule&amp;rdquo; which promotes leaving things better than you found it. It came about as common practice for scouts to always try to leave a campsite cleaner and tidier than when they arrived so that its in a good state for the next group.&lt;/p&gt;
&lt;p&gt;This is quite generic but can easily be made very specific to us as programmers:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Leave the codebase better than you found it.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;So what do I mean by this? Ultimately I mean that regardless of the state of the code you are working on you should always try to find a way to improve it.&lt;/p&gt;
&lt;p&gt;This can be something as simple as;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;refactoring the code to make it more readable&lt;/li&gt;
&lt;li&gt;adding some docblock to explain a file/class/method/function/variable&lt;/li&gt;
&lt;li&gt;create a Readme file or add some documentation&lt;/li&gt;
&lt;li&gt;remove obsolete code, old backup files, stray files, unused components&lt;/li&gt;
&lt;li&gt;fix a failing test&lt;/li&gt;
&lt;li&gt;write a new test even&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Its not an exhaustive list at all but it gives you an idea of what kind of things you can be doing to improve your codebase. Any good Scout group leaving a campsite would also make sure to put out the fire and close the gate on your way out. Which is exactly what you should be doing by making sure all your Acceptance, Functional, Integration &amp;amp; Unit tests pass and writing a good commit message.&lt;/p&gt;
&lt;h2 id="right-tool-for-the-job"&gt;Right tool for the job
&lt;/h2&gt;&lt;p&gt;In every activity that a Scout takes part in they are always taught the correct way to work with their tools and equipment, such as how to use an penknife property. They are then encouraged to explore different ways of using those to achieve their goals. This is no different for a PHP Scout, by knowing how to use their languages and tools properly they can then use it to maximum effect.&lt;/p&gt;
&lt;h2 id="self-development"&gt;Self Development
&lt;/h2&gt;&lt;p&gt;As a child you assimilate massive amounts of information every day that helps you to grow and develop. This is creatively harnessed by Scouts through a variety of different activities that are designed to help them learn new skills that can help them grow as people.&lt;/p&gt;
&lt;p&gt;Now that we are older our brains don&amp;rsquo;t have the same capacity to soak up that volume of information. But that doesn&amp;rsquo;t mean we shouldn&amp;rsquo;t be trying! A good PHP Scout will continually strive to push the boundaries of what they know, to pick up new skills that can be used to make them more capable. This can be learning a new technique, or language or tool be it via formal training, conferences, social events or even just a good Google.&lt;/p&gt;
&lt;p&gt;Granted the Scouts are rewarded with some cool badges, but I&amp;rsquo;m sure its only a matter if time before some entrepreneurial PHP Scout decides to start creating some achievement badges of their own (see &lt;a class="link" href="http://phpboyscout.uk/php-scout-membership-badge" target="_blank" rel="noopener"
 &gt;http://phpboyscout.uk/php-scout-membership-badge&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id="helping-others"&gt;Helping Others
&lt;/h2&gt;&lt;p&gt;We&amp;rsquo;ve all heard the adage of a Scout helping someone across the street. It&amp;rsquo;s a somewhat stereotypical example but extremely apt as it highlights that they are encouraged to take into consideration other peoples needs and to provide assistance wherever possible. Modern Scouting however goes far beyond aiding with avoiding getting run over on a road.&lt;/p&gt;
&lt;p&gt;By encouraging Scouts to not only help individuals, communities and groups we make them more considerate of the needs of others as well as developing their sense of self. A fantastic example of this is the 2015/16 initiative &lt;a class="link" href="https://www.amillionhands.org.uk/" target="_blank" rel="noopener"
 &gt;A Million Hands&lt;/a&gt; which promotes finding ways to identify the needs of others and to take action to provide aid.&lt;/p&gt;
&lt;p&gt;This is a fantastic trait to be teaching children and is something that any good PHP Scout would applaud, and would then go forward to do the same things but with the development community. This can be something as simple as;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;helping a colleague at work (without being told to do so by your boss)&lt;/li&gt;
&lt;li&gt;organising an event with a local user group&lt;/li&gt;
&lt;li&gt;contributing to an open source project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All very simple stuff to do and all it takes up is a little of your time! Where is the reward? I here some of you ask! I would say that the act itself is its own reward, and in reality that is true as when working helping others your generate some very positive Karma which will eventually be paid back when the day comes that you yourself need some help. You should (hopefully) also have an opportunity to maybe learn something new and improve your ability to communicate, a soft skill yes, but essential to your growth if your are striving to be better than you are now.&lt;/p&gt;
&lt;h2 id="problem-solving"&gt;Problem Solving
&lt;/h2&gt;&lt;p&gt;The Scout motto is a very simple two words&amp;hellip; &amp;ldquo;Be Prepared&amp;rdquo;, but be prepared to do what? Its quite open ended really it could be anything at all! I like to think that its nearly impossible to be equipped with every possible skill and tool possible to meet any and every task you will encounter through life, though being a Scout does try to help arm you with as many as possible.&lt;/p&gt;
&lt;p&gt;Yet as a PHP Scout we should always &amp;ldquo;Be Prepared&amp;rdquo; to solve problems. If we are doing our jobs right we should be looking to solve problems through the solutions we provide every day. Quite often I talk to developers and hear them make pigeon holing statements like&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I am a WordPress developer&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;And then complaining that they are bored at work or that they cant get jobs working with anything other than their chosen platform. Now this infuriates me as a PHP Scout would never do this, when asked they profess loud and clear&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;I am a Problem Solver&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;and be prepared to prove it by making sure they &lt;em&gt;are prepared&lt;/em&gt; by knowing more than one or two platforms or frameworks or even programming languages. This can be encouraged by actively seeking &amp;ldquo;problems&amp;rdquo; that you can solve with tools and techniques you are not familiar with.&lt;/p&gt;
&lt;p&gt;This is echoed throughout the challenges that are presented to Scouts, where they are tasked with solving a practical problem such as putting up a tent without any instructions, the best way to light a fire with two sticks and a bit of kindling, how to cross a stream with only a few bits of wood and rope. In solving these types of problems the Scouts not only receive the obvious of shelter, food, heat etc but they also become more prepared for the next time a similar scenario presents itself.&lt;/p&gt;
&lt;h2 id="team-work"&gt;Team work
&lt;/h2&gt;&lt;p&gt;No man is an island as the saying goes and the same goes for being a Scout. By being organised into lodges,packs &amp;amp; patrols they have a ready made team to work with and the only way they can progress is to work together. They may not like the people in their team (and as a Scout Leader I will quite readily admit to putting Scouts into groups with others they may clash with).&lt;/p&gt;
&lt;p&gt;We may all have teams that we work with as part of our Jobs, and a PHP Scout will take the opportunity to work with as many different combinations of teams as possible both in and outside of the workplace. By diversifying the people you have to interact with you develop a broader understanding about the problems you may be trying to solve.&lt;/p&gt;
&lt;p&gt;This can then be expanded upon as mentioned previously by then branching out into the community and working with user groups and opensource projects.&lt;/p&gt;
&lt;h2 id="to-summarise"&gt;To Summarise
&lt;/h2&gt;&lt;p&gt;The ethos behind the Scouting movement is a solid foundation not only for children aged 7-18 but for everyone. By being a PHP Scout you strive to keep improving your ability to create great code, solve problems, work with others and in doing so become a better developer.&lt;/p&gt;
&lt;p&gt;As with all Scouts they are Hard Working, Determined, Ingenious &amp;amp; Tenacious and so is a PHP Scout.&lt;/p&gt;</description></item><item><title>The PHP Scout Membership Badge</title><link>https://blog-570662.gitlab.io/php-scout-membership-badge/</link><pubDate>Tue, 11 Aug 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/php-scout-membership-badge/</guid><description>&lt;img src="https://blog-570662.gitlab.io/php-scout-membership-badge/elephpant.png" alt="Featured image of post The PHP Scout Membership Badge" /&gt;&lt;p&gt;As the PHP Boy Scout I&amp;rsquo;m having some badges made and I wanted to introduce the all new PHP Scout Membership Badge.&lt;/p&gt;
&lt;p&gt;This badge shows that you are more than just a PHP Developer but also a good PHP Scout. This means that you have all the qualities it takes to be a PHP Scout and will :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;always leave the codebase better than you found it&lt;/li&gt;
&lt;li&gt;help other PHP developers be good Scouts&lt;/li&gt;
&lt;li&gt;get involved with your local User Group &amp;amp; PHP Community&lt;/li&gt;
&lt;li&gt;Contribute to at least one open source project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you would like to find out how to get hold of a PHP Scout Membership Badge then fill out the form below.&lt;/p&gt;
&lt;p&gt;[contact-form &lt;a class="link" href="mailto:to=%27matt@phpboyscout.uk" &gt;to='matt@phpboyscout.uk&lt;/a&gt;&amp;rsquo; subject=&amp;lsquo;Someone wants a Membership Badge&amp;rsquo;][contact-field label=&amp;lsquo;Name&amp;rsquo; type=&amp;lsquo;name&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;Email&amp;rsquo; type=&amp;lsquo;email&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will always leave the codebase better than I found it&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will help other PHP developers&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will get involved with my local User Group&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;I will contribute to an open source project&amp;rsquo; type=&amp;lsquo;checkbox&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][/contact-form]&lt;/p&gt;</description></item><item><title>My first ever public appearance as PHPBoyScout</title><link>https://blog-570662.gitlab.io/public-appearance-phpboyscout/</link><pubDate>Thu, 06 Aug 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/public-appearance-phpboyscout/</guid><description>&lt;img src="https://blog-570662.gitlab.io/public-appearance-phpboyscout/20111036639_d7c8ec153d_z.jpg" alt="Featured image of post My first ever public appearance as PHPBoyScout" /&gt;&lt;p&gt;So it&amp;rsquo;s finally happened!&lt;/p&gt;
&lt;p&gt;I stood up in front of a group of developers and gave a lightning talk about how Scouting Principles should be applied to every day development.&lt;/p&gt;
&lt;p&gt;[slideshare id=51344270&amp;amp;doc=areyouagoodscout-150806121954-lva1-app6892]&lt;/p&gt;
&lt;p&gt;The amazing thing is that I didn&amp;rsquo;t get any rotten tomatoes thrown at me! quite the contrary in fact. Even with me doing the talk in full Scout uniform.&lt;/p&gt;
&lt;p&gt;Now to see about finding some more places to speak and actually fleshing out the talk into something that can last a full hour and not just shy of 5 minutes.&lt;/p&gt;</description></item><item><title>Goodbye Dev in Charge</title><link>https://blog-570662.gitlab.io/goodbye-dev-charge/</link><pubDate>Wed, 05 Aug 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/goodbye-dev-charge/</guid><description>&lt;p&gt;Over all the time that I&amp;rsquo;ve been a developer I&amp;rsquo;ve had people telling me that I should get in front of an audience and speak. However I&amp;rsquo;ve always suffered from a rather bad case of &amp;lsquo;Imposter Syndrome&amp;rsquo; which meant my automatic response to those kind of statements has always been&amp;hellip; I don&amp;rsquo;t really know enough about any one topic.&lt;/p&gt;
&lt;p&gt;This is very true, I&amp;rsquo;ve spent a lot of my career learning a really broad swathe of technologies and techniques so I can turn my hand to any task that&amp;rsquo;s been presented to me so far. Even so people continue to try convince me that it would be a worthwhile pursuit.&lt;/p&gt;
&lt;p&gt;Now that I work at Magma Digital I find that I&amp;rsquo;m often talking with &lt;a class="link" href="https://twitter.com/phpcodemonkey" target="_blank" rel="noopener"
 &gt;@phpcodemonkey&lt;/a&gt; about all sorts of things and the topic of creating a talk came up while we were enjoying the most excellent &lt;a class="link" href="http://2015.phpsouthcoast.co.uk/" target="_blank" rel="noopener"
 &gt;PHP South Coast Conference&lt;/a&gt;. He knows I&amp;rsquo;ve been a Scout Leader for around 4 years now, and he suggested that I do a talk on the &amp;lsquo;Boy Scout Rule&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t know if he was serious or not at the time but it set my mind racing! This is a topic that I actually know quite a lot about!&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;m now going to leave behind the Dev in Charge and have now rebranded as the PHP Boy Scout. I&amp;rsquo;ve already managed to pull together the basis of a talk on how Scouting principles can be used in conjunction with what we do as Developers and have a few other ideas that I&amp;rsquo;m going to work on over the next few weeks.&lt;/p&gt;
&lt;p&gt;Fingers crossed I will be better at doing this kind of thing than I suspect I will be&amp;hellip; but nothing ventured and nothing gained!&lt;/p&gt;</description></item><item><title>Free Open Source Website for Scouts</title><link>https://blog-570662.gitlab.io/free-open-source-website-scouts/</link><pubDate>Mon, 12 Jan 2015 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/free-open-source-website-scouts/</guid><description>&lt;img src="https://blog-570662.gitlab.io/free-open-source-website-scouts/scouts-snapshot.png" alt="Featured image of post Free Open Source Website for Scouts" /&gt;&lt;p&gt;I&amp;rsquo;ve been a Scout Leader for a few years now and the District I work within have very little by way of internet presence. As a bit of a pet project I started building a simple Scout based website for them to use.&lt;/p&gt;
&lt;p&gt;Its nothing too fancy, I created a simple module and theme for the Silverstripe CMS and have now put it into a GitHub Repository to share with the wider scouting community.&lt;/p&gt;
&lt;p&gt;I chose Silverstripe because of the speed with which I could develop something usable as well as providing a super simple management interface that can be handled by users of all skill levels.&lt;/p&gt;
&lt;p&gt;The module itself extends some very common extensions available for the CMS and makes them scout focused. Features include.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customisable theme&lt;/li&gt;
&lt;li&gt;Multi tiered Event Calendars&lt;/li&gt;
&lt;li&gt;Customisable Group/Section Pages&lt;/li&gt;
&lt;li&gt;Dynamic Forms&lt;/li&gt;
&lt;li&gt;A reliable News/Blog system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are just a few of the most obvious features and hopefully I will continue to add more.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also offering to help any Scout Groups/Districts/Counties if they are wanting to use these modules and get their sites built and up and running for them free of charge.&lt;/p&gt;
&lt;p&gt;If you want to take a look at the code and have a play yourselves you can find it online at&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/phpboyscout/silverstripe-scouts" target="_blank" rel="noopener"
 &gt;https://github.com/phpboyscout/silverstripe-scouts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/phpboyscout/silverstripe-scouts-theme" target="_blank" rel="noopener"
 &gt;https://github.com/phpboyscout/silverstripe-scouts-theme&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to get in touch or would like more information about having a website built for you please fill in the form below.&lt;/p&gt;
&lt;p&gt;[contact-form &lt;a class="link" href="mailto:to=%27matt@phpboyscout.uk" &gt;to='matt@phpboyscout.uk&lt;/a&gt;&amp;rsquo; subject=&amp;lsquo;Request for Scouts Website&amp;rsquo;][contact-field label=&amp;lsquo;Name&amp;rsquo; type=&amp;lsquo;name&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;Email&amp;rsquo; type=&amp;lsquo;email&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][contact-field label=&amp;lsquo;Scout Group/District/County&amp;rsquo; type=&amp;lsquo;url&amp;rsquo; required=&amp;lsquo;1&amp;rsquo;/][/contact-form]&lt;/p&gt;</description></item><item><title>Its time for a change</title><link>https://blog-570662.gitlab.io/time-change/</link><pubDate>Tue, 02 Dec 2014 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/time-change/</guid><description>&lt;p&gt;So&amp;hellip; Its been a long time since I posted anything of any relevance. This is due to having been super busy with my previous company Zucchi.&lt;/p&gt;
&lt;p&gt;However that has all changed now! After three and a half years of running my own company I have decided that its not for me. I gave it my all, but in the end I was becoming too much of a Salesman and I missed getting stuck in with code.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve now moved on and have joined the fantastic team at Magma Digital who have been leaders in PHP software development for somewhere in the region of 14 years as well as heavily involved in the PHP community having been a essential part of the PHPNW user group and conference.&lt;/p&gt;
&lt;p&gt;This means I should be able to pick up where I left off all those years ago and start being more active again.&lt;/p&gt;
&lt;p&gt;See you soon&lt;/p&gt;</description></item><item><title>Flexbox cross browser</title><link>https://blog-570662.gitlab.io/flexbox-cross-browser/</link><pubDate>Fri, 16 Aug 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/flexbox-cross-browser/</guid><description>&lt;p&gt;Despite having been around for a while and having been through a couple of revisions, its support across browsers can vary greatly. From &amp;ldquo;Candidate Recommendation&amp;rdquo; on Chrome/Opera, &amp;ldquo;legacy flexbox&amp;rdquo; on Firefox and no support at all on IE9 and earlier.&lt;/p&gt;
&lt;p&gt;Making flexbox work consistently across browsers was a challenge for us on a recent project, but I have found a solution that seems to work quite well.&lt;/p&gt;
&lt;p&gt;Below is an SCSS @mixin that will attempt to handle compatibility between CR and legacy cross browsers flexbox.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;@mixin flex($content: flex-start, $items: stretch, $direction: row, $wrap: wrap) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $packLegacy: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @if $packLegacy == flex-start {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $packLegacy: start;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } @else if $packLegacy == flex-end {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $packLegacy: end;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $alignLegacy: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @if $alignLegacy ==flex-start {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $alignLegacy: start;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } @else if $alignLegacy == flex-end {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $alignLegacy: end;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $oritentLegacy: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $directionLegacy: normal;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; @if $oritentLegacy == row {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $oritentLegacy: horizontal;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } @else if $oritentLegacy == column {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $oritentLegacy: vertical;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** SAFARI **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -webkit-box;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-orient: $oritentLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-pack: $packLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-align: $alignLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** FIREFOX LEGACY **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -moz-box;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-orient: $oritentLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-direction: $directionLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-pack: $packLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-align: $alignLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** LEGACY **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: box;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-orient: $oritentLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-direction: $directionLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-pack: $packLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-align: $alignLegacy;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** IE 10+ **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -ms-flexbox;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-flex-wrap: $wrap;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-flex-direction: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-justify-content: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-align-items: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** CHROME **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: -webkit-flex;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-flex-wrap: $wrap;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-flex-direction: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-justify-content: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-align-items: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/** NATIVE **/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; display: flex;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flex-wrap: $wrap;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flex-direction: $direction;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; justify-content: $content;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; align-items: $items;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;} //@mixin flex
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;@mixin flexItem($width) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-box-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -moz-box-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; box-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -ms-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -webkit-flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; flex: $width;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; min-height: 0;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Firefox however only half supports flexbox (all revisions) and to get around this I would recommend using &lt;a class="link" href="http://modernizr.com/" title="Modernizr"
 target="_blank" rel="noopener"
 &gt;Modernizr&lt;/a&gt; as this will add the class &amp;ldquo;no-flexbox&amp;rdquo; to the &lt;html&gt; tag. This provides us with a simple work around that allows non flexbox supporting browsers render correctly by using specifically crafted and targeted CSS for non-flexbox browsers&lt;/p&gt;
&lt;p&gt;I found that IE9 support could be implemented using the &lt;a class="link" href="http://flexiejs.com/" title="FlexieJS"
 target="_blank" rel="noopener"
 &gt;flexie&lt;/a&gt; javascript plugin. In IE8 M&lt;a class="link" href="http://modernizr.com/" title="Modernizr"
 target="_blank" rel="noopener"
 &gt;odernizr&lt;/a&gt; will add the class &amp;ldquo;no-flexboxlegacy&amp;rdquo; which can again allow you to create targeted CSS that wont affect your Flexbox layout.&lt;/p&gt;
&lt;p&gt;For a great overview of the &amp;ldquo;CR&amp;rdquo; of flexbox, CSS Tricks has an amazingly comprehensive coverage of the functionality here &lt;a class="link" href="http://css-tricks.com/snippets/css/a-guide-to-flexbox/" target="_blank" rel="noopener"
 &gt;http://css-tricks.com/snippets/css/a-guide-to-flexbox/&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Creating Custom Routes in Silverstripe 3.1</title><link>https://blog-570662.gitlab.io/creating-custom-routes-silverstripe/</link><pubDate>Wed, 31 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/creating-custom-routes-silverstripe/</guid><description>&lt;p&gt;We wanted to create a Route to our custom Products Controller in our products module for SilverStripe 3.1, such as: &amp;ldquo;&lt;a class="link" href="http://www.examplesite.com/products/" target="_blank" rel="noopener"
 &gt;http://www.examplesite.com/products/&lt;/a&gt;&lt;product-slug&gt;&amp;rdquo;&lt;/p&gt;
&lt;p&gt;However looking at the &lt;a class="link" href="http://doc.silverstripe.org/framework/en/3.1/topics/controller" title="Controller Documentation"
 target="_blank" rel="noopener"
 &gt;Controller Documentation&lt;/a&gt; it was not clear how to create a route without an Action being supplied. In our example above the action is not specified, as we just want to use &amp;lsquo;view&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;Solution:&lt;/p&gt;
&lt;p&gt;Create a &lt;module-name&gt;/_config/routes.yml file containing the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Name: productsroutes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;After: &amp;#39;framework/routes#coreroutes&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Director:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rules:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;product&amp;#39;: &amp;#39;Product_Controller&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The above will redirect any Url that starts with &amp;ldquo;/product&amp;rdquo; to our Product_Controller. Note that everything after the rule, so after &amp;ldquo;/product&amp;rdquo;, is used in the next bit for matching.&lt;/p&gt;
&lt;p&gt;Now we need to add &lt;code&gt;private static $url_handers&lt;/code&gt; to Product_Controller to match our path, so in this example we need to match &amp;ldquo;$Slug!&amp;rdquo; which will match &amp;ldquo;&lt;product-slug&gt;&amp;rdquo;. Note the ! means the slug is required. Of course we want to direct this to a specific action, in this case &amp;ldquo;view&amp;rdquo;, this gives us:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;private static $url_handlers = array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;$Slug!&amp;#39; =&amp;gt; &amp;#39;view&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now just add &amp;ldquo;view&amp;rdquo; to the $allow_actions and add the &amp;ldquo;view&amp;rdquo; function. This gives the final Product_Controller as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;Product_Controller&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Page_Controller&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;url_handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;$Slug!&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;view&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;allowed_actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;view&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SS_HTTPRequest&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Your&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;goes&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Handy note:&lt;/p&gt;
&lt;p&gt;You can put ?&lt;code&gt;debug_request=1 on the end of your URL to see how it determines which Controller to use.&lt;/code&gt;&lt;/p&gt;</description></item><item><title>Disabling Cache in Silverstripe 3.1</title><link>https://blog-570662.gitlab.io/disabling-cache-silverstripe/</link><pubDate>Wed, 31 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/disabling-cache-silverstripe/</guid><description>&lt;p&gt;While working with Silverstripe we found ourselves having to run &amp;ldquo;?flush=1&amp;rdquo; a lot to clear the Cache. To switch it off, while you work, add the following to your mysite/_config.php:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SS_Cache::set_cache_lifetime(&amp;#39;default&amp;#39;, -1, 100);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Set up SilverStripe 3.1 using only Git (No Composer)</title><link>https://blog-570662.gitlab.io/set-up-silverstripe-3-1-using-only-git/</link><pubDate>Mon, 29 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/set-up-silverstripe-3-1-using-only-git/</guid><description>&lt;p&gt;We recently tried to use composer to set up SilverStripe 3.1, but ended up with a dependency nightmare. In order to work around this we decided to make use of Git submodules.&lt;/p&gt;
&lt;p&gt;First set up your Git repository and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next set up a site directory for the code inside your Git repository. Then navigate to &lt;a class="link" href="https://github.com/silverstripe/silverstripe-installer" target="_blank" rel="noopener"
 &gt;SilverStripe Installer&lt;/a&gt; in your browser and Download a copy. Extract files, and copy contents to site folder. Now we need to add the CMS and Framework. Navigate in a browser to the Git Hub repositories for &lt;a class="link" href="https://github.com/silverstripe/silverstripe-cms" target="_blank" rel="noopener"
 &gt;CMS&lt;/a&gt; and &lt;a class="link" href="https://github.com/silverstripe/silverstripe-framework" target="_blank" rel="noopener"
 &gt;Framework.&lt;/a&gt; Now copy the HTTPS clone URL for each project and run the following, to add these as Git sub modules.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule add https://github.com/silverstripe/silverstripe-framework.git site/framework
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git submodule add https://github.com/silverstripe/silverstripe-cms.git &amp;lt;path-to-site&amp;gt;site/cms
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now delete mysite/_config.php and load the site. Follow the normal install instructions displayed and you will have a running version of &lt;a class="link" href="http://www.silverstripe.org/" title="SilverStripe"
 target="_blank" rel="noopener"
 &gt;SilverStripe 3.1&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Enabling MYSQL_CLIENT_INTERACTIVE with Doctrine 2 on Rackspace Cloud Database</title><link>https://blog-570662.gitlab.io/mysql-client-interactive-with-doctrine-on-rackspace/</link><pubDate>Fri, 26 Jul 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/mysql-client-interactive-with-doctrine-on-rackspace/</guid><description>&lt;p&gt;We recently ran into problem using Doctrine 2 connecting to a Rackspace Cloud Database using the MySqli Driver.&lt;/p&gt;
&lt;p&gt;Problem:&lt;/p&gt;
&lt;p&gt;We have a long running PHP script that can sometimes run for hours at a time whilst processing information. This script requires a connection to a database, but has long periods of inactivity where there is no actual interaction with MySQL. By default MySQL uses the &amp;ldquo;wait_timeout&amp;rdquo; setting which states, how long an inactive connection can exist before it is killed. This is normally fine with web pages requests, as it is usually a short lived request. Unfortunately you do not have the ability to alter this setting when using Rackspaces Cloud Database.&lt;/p&gt;
&lt;p&gt;Solution:&lt;/p&gt;
&lt;p&gt;When using the MySQLi extension you can create a connection in &amp;ldquo;interactive mode&amp;rdquo; by passing the &amp;ldquo;MYSQLI_CLIENT_INTERACTIVE&amp;rdquo; flag, which will then use the &amp;ldquo;interactive_timeout&amp;rdquo; setting. On Rackspace this is set to 8 hours!&lt;/p&gt;
&lt;p&gt;Annoyingly Doctrine does not allow you to pass any flags to the MySQLi Connection. So we overrode Doctrine\DBAL\Driver\Connection with our own &lt;a class="link" href="https://github.com/zucchi/ZucchiDoctrine/blob/master/src/ZucchiDoctrine/Driver/Mysqli/MysqliConnection.php" title="ZucchiDoctrine/Driver/Mysqli/MysqliConnection.php"
 target="_blank" rel="noopener"
 &gt;Driver&lt;/a&gt; which then allows us to pass a &amp;ldquo;flags&amp;rdquo; parameter through.&lt;/p&gt;
&lt;p&gt;Feel free to look at some of the other helpful features in we have added to Doctrine 2 here: &lt;a class="link" href="https://github.com/zucchi/ZucchiDoctrine" title="ZucchiDoctrine"
 target="_blank" rel="noopener"
 &gt;ZucchiDoctrine&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Installing PECL extensions for Zend Server 6</title><link>https://blog-570662.gitlab.io/installing-pecl-extensions-zend-server-6/</link><pubDate>Mon, 13 May 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/installing-pecl-extensions-zend-server-6/</guid><description>&lt;p&gt;Recently we have revisited using Zend Server for some of our projects and decided to give the new version 6 a chance to prove itself.&lt;/p&gt;
&lt;p&gt;Overall its a big improvement over version 5. There are still some things that are extremely annoying but we have decided that we can overlook them.&lt;/p&gt;
&lt;p&gt;However there is one thing that we couldn&amp;rsquo;t do without. By default you will find that a number of PECL extensions will not install out of the box (at least this is what we experience using the Debian based install).&lt;/p&gt;
&lt;p&gt;To fix this you will need to make sure you install the additional packages in ubuntu&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;php-5.4-source-zend-server&lt;/strong&gt; or &lt;strong&gt;php-5.3-source-zend-server&lt;/strong&gt; depending on the php version you are using&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;autoconf&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;build-essential&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once this is done you should now be able to install extensions from PECL without too much hassle.&lt;/p&gt;</description></item><item><title>Better Output for MySQL Select Command Using \G</title><link>https://blog-570662.gitlab.io/better-output-mysql-command-line/</link><pubDate>Wed, 24 Apr 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/better-output-mysql-command-line/</guid><description>&lt;p&gt;If you ever find yourself using MySQL via command line and end up with something like this:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/04/mysql-command-line.jpg" target="_blank" rel="noopener"
 &gt;&lt;img alt="mysql-command-line" class="gallery-image" data-flex-basis="667px" data-flex-grow="278" height="319" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-command-line.jpg" srcset="https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-command-line_hu_a6381c4c6e0774e6.jpg 800w, https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-command-line.jpg 887w" width="887"&gt;&lt;/a&gt; And thought there must be another way, well here it is: Use &lt;strong&gt;\G&lt;/strong&gt; instead of &lt;strong&gt;;&lt;/strong&gt; at the end of your select command.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;select * from CHARACTER_SETS\G
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Below is an image of the output from this select:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/04/mysql-nice-output.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="mysql-nice-output" class="gallery-image" data-flex-basis="489px" data-flex-grow="204" height="250" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/better-output-mysql-command-line/mysql-nice-output.png" width="510"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Happy Querying!&lt;/p&gt;</description></item><item><title>Introducing ZuQ - A Simple ZeroMQ Queuing Daemon</title><link>https://blog-570662.gitlab.io/introducing-zuq/</link><pubDate>Tue, 19 Mar 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/introducing-zuq/</guid><description>&lt;p&gt;We recently had the need to create a queuing system to replace an implementation of RabbitMQ that was being used on a previous project. The reasoning behind this is that the requirements of the project required a very custom implementation of a queuing system that would drastically alter in architecture as the project grew and RabbitMQ just wasn&amp;rsquo;t going to fit the bill. However to start with we required something super simple and efficient that could be expanded and developed as required. After a little investigation and a lot of recommendation from others we decided to use ZeroMQ as our transport layer for that very reason, as we could build something which could span across multiple servers and was fast.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/clientQueue.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="This is a simple Queue and Client Diagram" class="gallery-image" data-flex-basis="1082px" data-flex-grow="450" height="165" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/introducing-zuq/clientQueue.png" width="744"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The diagram above helps describe our basic queuing system. We have a queue daemon that is continuously listening for connections and two clients, one that populates the queue and the other that retrieves from the queue.&lt;/p&gt;
&lt;h2 id="the-clients"&gt;The Clients
&lt;/h2&gt;&lt;p&gt;Each client is written in PHP and uses a 0mq socket to communicate with a service, in this case our queue service. We used a SOCKET_REQ type of socket in order to have a request/response communication with our queue service.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;function client_socket(\ZMQContext $context)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // SOCKET_REQ used to create a client that sends requests to and receive from a service
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client = new \ZMQSocket($context,\ZMQ::SOCKET_REQ);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client-&amp;gt;connect(&amp;#34;tcp://localhost:5555&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // SOCKOPT_LINGER = 0 Configure socket to not wait at close time
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $client-&amp;gt;setSockOpt(\ZMQ::SOCKOPT_LINGER, 0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return $client;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;public function injectIntoQueue()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $context = new \ZMQContext();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $client = $this-&amp;gt;client_socket($context);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $msg = &amp;#34;This is a message&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $retries_left = 3;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  $read = $write = array();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;  while ($retries_left) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    // We send a request, then we wait to get a reply
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        $client-&amp;gt;send($msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        $expect_reply = true;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        while ($expect_reply) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;        // Poll socket for a reply, with timeout
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $poll = new \ZMQPoll();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $poll-&amp;gt;add($client, \ZMQ::POLL_IN);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            $events = $poll-&amp;gt;poll($read, $write, 2500);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            // If we got a reply, process it
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            if ($events &amp;gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            // We got a reply from the server, must match sequence
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $reply = $client-&amp;gt;recv();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                if (intval($reply) == $msg) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $retries_left = 0;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                    $expect_reply = false;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;             } elseif (--$retries_left == 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;            break;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;             } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;               // Old socket will be confused; close it and open a new one
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $client = $this-&amp;gt;client_socket($context);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                // Send request again, on new socket
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;                $client-&amp;gt;send($msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;You can see from the code above, have a 3 strike rule. The reasoning behind this is that if the client fails to connect to the queue service more than 3 times, we can stop trying to inject into the queue and move on to the next item. As we ultimately intend to adapt the &lt;a class="link" href="http://zguide.zeromq.org/page:all#Client-side-Reliability-Lazy-Pirate-Pattern" title="lazy pirate pattern"
 target="_blank" rel="noopener"
 &gt;lazy pirate pattern&lt;/a&gt; we have made it so that if the socket times out, we can then create a new socket and retry. Without this, as the architecture becomes more complicated we may then end up in a situation where we might have errors, thus the recommend solution is to create a new socket. Once the client has sent its message to the queue, we poll for a response (i.e. which is the message we sent returned back). Once we have a response that is valid, meaning that the queue has been populated, we can stop polling until the next message.&lt;/p&gt;
&lt;p&gt;The Frontend Client&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;protected function getFromQueue()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $context = new \ZMQContext();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker = new \ZMQSocket($context, \ZMQ::SOCKET_REQ);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $read = $write = array();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Set random identity to make tracing easier
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker-&amp;gt;connect(&amp;#34;tcp://localhost:5556&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // Tell queue we&amp;#39;re ready for work
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $worker-&amp;gt;send(&amp;#34;ready&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; $reply = $worker-&amp;gt;recv();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; return $reply;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;Our Frontend Client is much simpler as it is part of a process that is being continually updated, therefore it doesn&amp;rsquo;t need the same connection retries are the Backend Client. We simply send &amp;ldquo;ready&amp;rdquo; to the queue system and if the queue is populated it will return us the first item.&lt;/p&gt;
&lt;h2 id="the-queuing-service"&gt;The Queuing Service
&lt;/h2&gt;&lt;p&gt;This is a continuously running executable created using c++.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::context_t context(1);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::socket_t frontend (context, ZMQ_ROUTER);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::socket_t backend (context, ZMQ_ROUTER);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;backend.bind(&amp;#34;tcp://*:5555&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;frontend.bind(&amp;#34;tcp://*:5556&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;We create two sockets of type ZMQ_ROUTER which is an advanced pattern used for extending request/reply sockets. This means when we improve our queuing system we will be able to route packets to specific recipients using an address in the message.&lt;/p&gt;
&lt;p&gt;After creating our sockets, we initialise them&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// Initialize poll set
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::pollitem_t items [] = {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { frontend, 0, ZMQ_POLLIN, 0 },
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; { backend, 0, ZMQ_POLLIN, 0 }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;//poll the sockets - this seems to poll both sockets at the same time
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zmq::poll (items, 2, -1);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;h2 id="backend-handler"&gt;Backend Handler
&lt;/h2&gt;&lt;p&gt;If we get a message from the backend, we check the contents to see if it contains purge at which point we empty the queue, otherwise we push the msg contents onto the queue. Finally we send the message back to the backend to show that the message has been received.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;//receive msg from client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if (items [1].revents &amp;amp; ZMQ_POLLIN) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //get message from client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zmq::message_t message(0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string client_addr = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string empty = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; assert (empty.size() == 0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string msg = s_recv (backend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //allow the backend to purge the queue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(msg == &amp;#34;purge&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; while (!queue.empty()) queue.pop();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queue.push(msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //send response back to the backend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (backend, client_addr);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (backend, &amp;#34;&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_send (backend, msg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt; &lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="frontend-handler"&gt;Frontend Handler
&lt;/h2&gt;&lt;p&gt;If we get the &amp;ldquo;ready&amp;rdquo; message from the frontend client, we pop a message off the queue and return it to the frontend client. If the queue is empty we send an &amp;ldquo;empty&amp;rdquo; message back instead.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;// Handle activity on frontend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;if (items [0].revents &amp;amp; ZMQ_POLLIN) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //get message from worker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; zmq::message_t message(0);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string worker_addr = s_recv (frontend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string empty = s_recv (frontend);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; assert (empty.size() == 0);}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; string msg = s_recv (frontend); string queueMsg;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(msg == &amp;#34;ready&amp;#34;) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; if(queue.size() &amp;gt; 0) {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queueMsg = queue.front();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queue.pop();
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; queueMsg = &amp;#34;empty&amp;#34;;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; //send reply to worker with contents of queue
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (frontend, worker_addr);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_sendmore (frontend, &amp;#34;&amp;#34;);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; s_send (frontend, queueMsg);
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/blockquote&gt;
&lt;p&gt;That is our complete queuing system using ZeroMQ with PHP and C++.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary
&lt;/h2&gt;&lt;p&gt;Using the above has allowed us to create a very simple in memory queue daemon that we can use to quickly pass data from one system to a another. On the whole it works well and we are looking to expand on it in the near future to increase both its functionality and scalability.&lt;/p&gt;
&lt;p&gt;You can find the queueing daemon (christened as &amp;ldquo;ZuQ&amp;rdquo;) on github @ &lt;a class="link" href="https://github.com/zucchi/ZuQ" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/ZuQ&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Glorious Gluster - How to setup GlusterFS on Rackspace Cloud and Ubuntu 12.10</title><link>https://blog-570662.gitlab.io/gluster-licious/</link><pubDate>Fri, 15 Mar 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/gluster-licious/</guid><description>&lt;p&gt;A few of our projects recently called for a distributed file-system that provided high availability and redundancy. After a tip off from a fellow techie and a quick browse around the net it appeared that a solution called GlusterFS appeared to tick all the boxes for what we were wanting.&lt;/p&gt;
&lt;p&gt;However setting it up turned out not to be as trivial as I had originally anticipated. I&amp;rsquo;m going to try and put down the process we have evolved for setting it up on Ubuntu in the cloud&lt;/p&gt;
&lt;p&gt;A couple of things to clear up first.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We are using Rackspace for our cloud but beyond the setup of the servers it should still be relevant&lt;/li&gt;
&lt;li&gt;There are a number of ways to interact with Rackspaces set up but for this we are going to use the cloud control panel&lt;/li&gt;
&lt;li&gt;We use Ubuntu as our preferred server which means that our config tends to be all over the place compared to other guides&lt;/li&gt;
&lt;li&gt;You will need to set up a minimum of 2 servers and a separate block storage device for each.&lt;/li&gt;
&lt;li&gt;We have set up and broken a few different variations of gluster setup so far and make no guarantees that the setup in this blog is infallable but its the best wehave so far.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="setting-up-the-hardware"&gt;Setting up the hardware
&lt;/h2&gt;&lt;p&gt;First things first. We are going to need to set up are some servers.&lt;/p&gt;
&lt;p&gt;Feel free to create any size server you want. Just make sure to select Ubuntu 12.10 (or whatever version you may have that is newer).&lt;/p&gt;
&lt;p&gt;You will also need to define a new network to work with. We use this to isolate the traffic between the nodes of our new gluster.&lt;/p&gt;
&lt;p&gt;You can create a new network when creating the first of your servers. On the creation page under the networks heading you can find a &amp;ldquo;Create Network&amp;rdquo; button.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/create-network.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="create-network" class="gallery-image" data-flex-basis="403px" data-flex-grow="168" height="316" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/create-network.png" width="531"&gt;&lt;/a&gt;Hopefully this should be quite self explanatory. Now when you create subsequent servers you will then have the option to attach your new network (&amp;ldquo;GlusterNet&amp;rdquo; in my example).&lt;/p&gt;
&lt;p&gt;Once the two starting nodes have been created then you need to add some additional block storage to store your data on. Make sure that you create blocks that have sufficient capacity for your needs. Something else to consider is using High Performance SSD storage. Its a little on the pricy side but well worth the expense if you are trying to eak out every ounce of performance from the implementation.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/block-storage.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="block-storage" class="gallery-image" data-flex-basis="238px" data-flex-grow="99" height="456" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/block-storage.png" width="453"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You will then need to attach one to each of your servers.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/attach-storage.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="attach-storage" class="gallery-image" data-flex-basis="320px" data-flex-grow="133" height="426" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/attach-storage.png" width="568"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once attached you will be able to see the details of the block mount point from the block storage details page.&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/03/storage-details.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="storage-details" class="gallery-image" data-flex-basis="366px" data-flex-grow="152" height="297" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/gluster-licious/storage-details.png" width="453"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Make a note of the mount point (in this case &amp;ldquo;/dev/xvdb&amp;rdquo;) as we will need that in a minute.&lt;/p&gt;
&lt;h2 id="prepare-the-server"&gt;Prepare the Server
&lt;/h2&gt;&lt;p&gt;Now that we have a the hardware ready we can shell into a server to set it up.&lt;/p&gt;
&lt;p&gt;First you need to shell into your server and update its OS as the images provided by most cloud supplier tends not to have the latest patches and updates. In our case it&amp;rsquo;s as simple as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get update 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once that&amp;rsquo;s done we then need to prepare the Block Storage device ( henceforth refered to as a &amp;ldquo;brick&amp;rdquo;)&lt;/p&gt;
&lt;p&gt;if you run fdisk -l  you should see that an entry that looks something like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/xvdb: 107.4 GB, 107374182400 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;255 heads, 63 sectors/track, 13054 cylinders, total 209715200 sectors
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Units = sectors of 1 * 512 = 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sector size (logical/physical): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O size (minimum/optimal): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk identifier: 0x00000000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/xvdb doesn&amp;#39;t contain a valid partition table
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This indicates that our brick needs a partition table and formatting. We can achieve this be doing the following&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Building a new DOS disklabel with disk identifier 0xe7da4288.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Changes will remain in memory only, until you decide to write them.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;After that, of course, the previous content won&amp;#39;t be recoverable.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Command (m for help): n
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Partition type:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; p primary (0 primary, 0 extended, 4 free)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; e extended
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Select (default p): p
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Partition number (1-4, default 1): 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;First sector (2048-209715199, default 2048): 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using default value 2048
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Last sector, +sectors or +size{K,M,G} (2048-209715199, default 209715199): 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Using default value 209715199
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Command (m for help): w
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The partition table has been altered!
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Calling ioctl() to re-read partition table.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Syncing disks.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve highlighted the prompts and my responses. All we are doing here is creating a default partition table that has a single partition which uses up the whole disk.&lt;/p&gt;
&lt;p&gt;now running fdisk -l  should give us something that looks like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk /dev/xvdb: 107.4 GB, 107374182400 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;43 heads, 44 sectors/track, 110843 cylinders, total 209715200 sectors
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Units = sectors of 1 * 512 = 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Sector size (logical/physical): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O size (minimum/optimal): 512 bytes / 512 bytes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Disk identifier: 0xe7da4288
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Device Boot Start End Blocks Id System
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/xvdb1 2048 209715199 104856576 83 Linu
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can now see we have a valid device of  /dev/xvdb1 that we can mount_._ However we need to create a valid filesystem on the new brick before we can mount it. I have been doing this with Ext4 rather than XFS (which is the recommened filesystem from gluster), this is mainly down to the fact that when i tried using XFS I kept getting some issues with performance and access. I&amp;rsquo;m sure that with further investigation I could resolve this but as of yet haven&amp;rsquo;t had chance to. So far though I have had zero issues using Ext4. To create the filesystem we run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkfs.ext4 -j /dev/xvdb1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, create a folder to mount to, easily done by executing:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p /glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, the simplest way to mount the device is via your /etc/fstab by adding the line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/dev/xvdb1 /glusterfs/brick ext4 defaults 1 2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and running mount -a  as root (this will also mean that it mounts on boot for you automatically as well.)&lt;/p&gt;
&lt;p&gt;Next we need to install the latest gluster version. At the time of writing this was v3.3.1. You can find a version to suit your OS at &lt;a class="link" href="http://www.gluster.org/download" target="_blank" rel="noopener"
 &gt;http://www.gluster.org/download&lt;/a&gt;. If you are using Ubuntu you can do the following&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install software-properties-common
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;add-apt-repository ppa:semiosis/ubuntu-glusterfs-3.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install glusterfs-server glusterfs-client
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By this point you will now have a single working server to continue on your going to need to set up your second server ready to create your new volume.&lt;/p&gt;
&lt;p&gt;Once you have your second (or third, fourth, etc) setup its a good idea to add a reference to each one of them to your /etc/hosts  file. This is not really necessary and you can just use the IP addresses of each server but it saves you having to remember each IP and makes it easier to identify.&lt;/p&gt;
&lt;p&gt;Remember that we are going to be working with the new network interface you created earlier (i.e &amp;ldquo;GlusterNet&amp;rdquo;). to get the IP of your GlusterNet interface a quick ifconfig will show you an interface with an IP that matched the CIDR from earlier. In my case I now have 2 IPs of 192.168.3.1 &amp;amp; 192.168.3.2.&lt;/p&gt;
&lt;p&gt;So now I add the following lines to my /etc/hosts  file&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.3.1 gluster1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;192.168.3.2 gluster2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="creating-our-volume"&gt;Creating our volume
&lt;/h2&gt;&lt;p&gt;Now that the servers are prepared we can now play with the the tool gluster.This tool is a life saver in getting everything configured quickly and you can easily get a list of what its capable of by running gluster help. Now Im not going to take you through every command and option and would recomend reading the gluster manual to learn more.&lt;/p&gt;
&lt;p&gt;What this tool actually does is help generate and manipulate all the required config that is then stored at /var/lib/glusterd/.&lt;/p&gt;
&lt;p&gt;Firstly we need to tell gluster is that we have a pool of servers that will communicate with each other. Gluster refers to these as peers. To do this you need to run gluster peer probe gluster2 on each server for each server that will be used, replacing &amp;ldquo;gluster2&amp;rdquo; with the name names you defined in your /etc/hosts  file. This will then create the appropriate files at /var/lib/glusterd/peers/&lt;/p&gt;
&lt;p&gt;Now that all our peers have been defined we can get to actually creating the new distributed volume. This however requires a little consideration as there are some decisions you need to make.&lt;/p&gt;
&lt;p&gt;If we take a look at the help for creating a new volume we can see that we need to decide on what options to use&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume create &amp;lt;NEW-VOLNAME&amp;gt; [stripe &amp;lt;COUNT&amp;gt;] [replica &amp;lt;COUNT&amp;gt;] [transport &amp;lt;tcp|rdma|tcp,rdma&amp;gt;] &amp;lt;NEW-BRICK&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;&lt;NEW-VOLNAME&gt;  - what are we going to name our volume&lt;/li&gt;
&lt;li&gt;[stripe &lt;COUNT&gt;] [replica &lt;COUNT&gt;] - are we going to crate a striped or replicated volume and how many &amp;ldquo;bricks&amp;rdquo; are we going to create this volume with&lt;/li&gt;
&lt;li&gt;[transport {tcp|rdma|tcp,rdma&amp;gt;] - What transport protocol do you want the peers to communicate with&lt;/li&gt;
&lt;li&gt;&lt;NEW-BRICK&gt; - which servers/bricks do you want to use.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;for more information on how to create you volume and what all the options mean have a look at these links&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Replicated_Volumes" target="_blank" rel="noopener"
 &gt;http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Replicated_Volumes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Striped_Volumes" target="_blank" rel="noopener"
 &gt;http://gluster.org/community/documentation/index.php/Gluster_3.2:_Configuring_Distributed_Striped_Volumes&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;for our purposes we are going to run&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gluster volume create myvolume replica 2 transport tcp gluster1:/glusterfs/brick gluster2:/glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This now creates a new volume that spans both of our servers. you can confirm that this is the case by running gluster volume info  and you should get something that looks like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Volume Name: myvolume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Type: Replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Volume ID: d3dd24fd-9482-44c3-9503-24291fad8193
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Status: Created
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Number of Bricks: 1 x 2 = 2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Transport-type: tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Bricks:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Brick1: gluster1:/glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Brick2: gluster2:/glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;running this on both servers should give you the same results.&lt;/p&gt;
&lt;p&gt;What you will now find is that the gluster  command has created a plethora of files at /var/lib/glusterd/vols/myvolume/. As you work with gluster more and more you will find yourself drawn to these files as they control all the different aspects of how the volume works and performs. Most importantly we will need some information from these files when we come to configure a client to mount the volume.&lt;/p&gt;
&lt;p&gt;All that is left to do now is start the volume which can be easily done with a quick gluster volume start myvolume&lt;/p&gt;
&lt;p&gt;At this point we have now completed setting up our volume but we need to add some security. I would strongly recommend setting up a firewall using ufw to control access to the server. The easiest way to do this is to allow all traffic on your &amp;ldquo;GlusterNet&amp;rdquo; network interface as only the servers you attach to that network will have access. you can find a guide to using ufw at &lt;a class="link" href="https://help.ubuntu.com/12.10/serverguide/firewall.html" target="_blank" rel="noopener"
 &gt;https://help.ubuntu.com/12.10/serverguide/firewall.html&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="mounting-a-client"&gt;Mounting a Client
&lt;/h2&gt;&lt;p&gt;Now that we have a working volume we need to add some clients. To do this you will need to create a new server as above that is attached to the &amp;ldquo;GlusterNet&amp;rdquo; network but without the block storage (unless you really want it that is).&lt;/p&gt;
&lt;p&gt;Make sure to add your gluster dfinitions to your /etc/hosts file&lt;/p&gt;
&lt;p&gt;Once you have your new client server ready we can install the gluster client&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install software-properties-common
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;add-apt-repository ppa:semiosis/ubuntu-glusterfs-3.3
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install glusterfs-client
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve seen a number of different guides that tell you to install glusterfs-server as well but I have as yet had no need to as it all works without it.&lt;/p&gt;
&lt;p&gt;Now there are a lot of way that you can mount your new Gluster volume. I have tried a few and have had varying results. What I have found is that the best way is to create a volume file. To do this we create a new file at /etc/glusterfs.vol.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume gluster1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type protocol/client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option transport-type tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-host gluster1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-subvolume /glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option username &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option password &amp;lt;password&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume gluster2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type protocol/client
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option transport-type tcp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-host gluster2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option remote-subvolume /glusterfs/brick
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option username &amp;lt;username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option password &amp;lt;password&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type cluster/replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subvolumes gluster1 gluster2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume writebehind
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type performance/write-behind
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option cache-size 1MB
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subvolumes replicate
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;volume cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; type performance/io-cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; option cache-size 400MB
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; subvolumes writebehind
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end-volume
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What you will notice is that there is a &lt;username&gt;  and &lt;password&gt;  required for this to work. You can find these details on one of your peer servers in the file /var/lib/glusterd/vols/myvolume/trusted-myvolume-fuse.vol.&lt;/p&gt;
&lt;p&gt;This /etc/gluster.vol file is basically going to inform the gluster-client software about how to connect to the gluster volume and all the available nodes to connect to. This provides us with some level of fail-over so should one node become unavailable the gluster client will seamlessly switch to a different one. It also allows us to define additional &amp;ldquo;&lt;a class="link" href="http://www.gluster.org/community/documentation/index.php/Translators" title="translators"
 target="_blank" rel="noopener"
 &gt;translators&lt;/a&gt;&amp;rdquo; such as the &lt;a class="link" href="http://www.gluster.org/community/documentation/index.php/Translators/performance/io-cache" title="performance-io"
 target="_blank" rel="noopener"
 &gt;performance-io&lt;/a&gt; one that you can see here. I would strongly recommend reading through the available translators to see which may be useful to you.&lt;/p&gt;
&lt;p&gt;Now one of the main issues you will find with Ubuntu is that it will fail on boot if you try to add this mount to your fstab. To get around this you can use Upstart. if you create the following file at /etc/init/glusterfs-mount.conf  making sure to change &lt;interface&gt;  to the interface for your GlusterNet network (i.e. eth0 or eth1 or eth2, you get the idea)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;author &amp;#34;Matt Cockayne&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;description &amp;#34;Mount GlusterFS after networking available&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start on net-device-up IFACE=&amp;lt;interface&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop on stopping network
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop on starting shutdown
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;script
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;    mount -t glusterfs /etc/glusterfs.vol /glusterfs
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;end script
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see we are using a straight mount command. The magic is that this will not be executed until the start clause validates which in this case is not until the network interface for &amp;ldquo;GlusterNet&amp;rdquo; is up and running properly. You will also see that we are mounting the /etc/gluster.vol  file to /gluster  (remember to create this folder to mount to) rather than mounting a network path as you might when mounting an NFS share.&lt;/p&gt;
&lt;p&gt;If you wanted you could also add more to your upstart script to handle clean un-mounting of gluster thus allowing you to then use the service gluster-mount (start|stop|restart)  commands&lt;/p&gt;
&lt;p&gt;A quick reboot of the client server should confirm that it boots successfully and you will now end up with your volume mounted at /gluster. You can now test this by creating a new file. I tend to create an empty file at /gluster/mounted  just so I have a quick reference that the folder is mounted. Once that&amp;rsquo;s created if you now go and take a look at the /gluster/brick  on your &amp;ldquo;peers&amp;rdquo; you should see that there is now a file called &amp;ldquo;mounted&amp;rdquo; sat there looking all smug that it worked.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats
&lt;/h2&gt;&lt;p&gt;Some important things for you to be made aware of&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Never write directly to a brick. Make sure to write to the volume only through a configured client&lt;/li&gt;
&lt;li&gt;Beware of split-brain. &lt;a class="link" href="http://community.gluster.org/q/what-is-split-brain-in-glusterfs-and-how-can-i-cause-it/" target="_blank" rel="noopener"
 &gt;http://community.gluster.org/q/what-is-split-brain-in-glusterfs-and-how-can-i-cause-it/&lt;/a&gt; &lt;a class="link" href="http://www.gluster.org/2012/06/healing-split-brain/" target="_blank" rel="noopener"
 &gt;http://www.gluster.org/2012/06/healing-split-brain/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;RTFM - Read The F***ing Manual. Gluster is big and complex and there is a lot for you to understand. You can download a copy of the manual from &lt;a class="link" href="http://www.gluster.org/wp-content/uploads/2012/05/Gluster_File_System-3.3.0-Administration_Guide-en-US.pdf" title="PDF"
 target="_blank" rel="noopener"
 &gt;here&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Docblock, Oh Docblock, wherefore art thou Docblock (hint: Zend Optimizer Plus lost them)</title><link>https://blog-570662.gitlab.io/docblock-docblock-wherefore-art/</link><pubDate>Fri, 01 Mar 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/docblock-docblock-wherefore-art/</guid><description>&lt;p&gt;tl;dr&amp;gt; I make a terrible assumption about Zend Optimizer+ and am corrected by Dominic in the comments;&lt;/p&gt;
&lt;p&gt;Terrible post title I know but its the best I could come up with.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve just come up for air after spending the majority of the day debugging some issues on our current development sandbox.&lt;/p&gt;
&lt;p&gt;Now our sandbox tends to be quite bleeding edge in some circumstances and as such we run a fair few bits of unstable code. On the sandbox in question we have been running PHP 5.4.11 and unfortunately we have struggled to get APC working with it just the way we need it to. The lack of APC tends to make this sandbox quite slow.&lt;/p&gt;
&lt;p&gt;We recently saw that Zend have open-sourced their OptimizerPlus extension (&lt;a class="link" href="https://github.com/zend-dev/ZendOptimizerPlus" title="https://github.com/zend-dev/ZendOptimizerPlus"
 target="_blank" rel="noopener"
 &gt;https://github.com/zend-dev/ZendOptimizerPlus&lt;/a&gt;) and that it was compatible with 5.4&amp;hellip;. Fantastic, or so we thought.&lt;/p&gt;
&lt;p&gt;So I added the new OptimiserPlus to the sandbox and everything was going swimmingly. That was until we had to run one of the utility scripts that we use to rebuild some of our data structures. These scripts make use of different parts of both Zend Framework and Doctrine which tend to rely on some heavy DocBlock annotations.&lt;/p&gt;
&lt;p&gt;Now having used both APC and Zend Server knowing that they done affect this kind of functionality I had expected that OptimizerPlus would be fine&amp;hellip;. Wrongo. It took me a good few hours of head scratching trying to figure out what had happened.&lt;/p&gt;
&lt;p&gt;It turns out that OptimizerPlus suffers from the same flaws that eAccellerator does and strips Docblocks when caching the bytecode. This results in Reflection returning false when you call methods such as `getDocComment()`.&lt;/p&gt;
&lt;p&gt;All in all its not the end of the world I just disable OptimizerPlus and have to wait till I can get APC working. Not my ideal scenario but I can live with it.&lt;/p&gt;
&lt;p&gt;Something that does concern me is that there is currently an RFC that has gone to vote (&lt;a class="link" href="https://wiki.php.net/rfc/optimizerplus" title="https://wiki.php.net/rfc/optimizerplus"
 target="_blank" rel="noopener"
 &gt;https://wiki.php.net/rfc/optimizerplus&lt;/a&gt;) about integrating OptimizerPlus into the PHP 5.5 distribution. While this is great I do worry how many other things may break and will they be picked up and fixed for the 5.5 release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; Since writing this post the RFC has finished being voted upon and has been approved. You can expect to see Optimizer Plus appearing bundled with PHP soon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (15th Mar 13):&lt;/strong&gt; Thanks to Dominics&amp;rsquo; comment I now know that you can tell Optimizer+ to retain your Docblocks by setting your config using&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;zend_optimizerplus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save_comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="n"&gt;PHPDoc&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;optimized&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Disabling&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Doc Comments&amp;#34;&lt;/span&gt; &lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="n"&gt;some&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="n"&gt;applications&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;frameworks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Doctrine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ZF2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PHPUnit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;zend_optimizerplus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PHPDoc&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;SHM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Doc Comments&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;may&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;always&lt;/span&gt; &lt;span class="n"&gt;stored&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;save_comments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;loaded&lt;/span&gt; &lt;span class="n"&gt;by&lt;/span&gt; &lt;span class="n"&gt;applications&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;	&lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;don&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;t need them anyway.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;ll teach me to write a blog post without investigating more first.&lt;/p&gt;</description></item><item><title>Our Redmine install died, We all cried!</title><link>https://blog-570662.gitlab.io/redmine-install-died-we-cried/</link><pubDate>Sat, 23 Feb 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/redmine-install-died-we-cried/</guid><description>&lt;p&gt;We have been using redmine for quite a long time and a few months ago attempted to upgrade from 1.3 to 2.something. Unfortunately I (quite typically) borked the installation and since then its been hobbling along after my attempts to fix it left it crippled.&lt;/p&gt;
&lt;p&gt;Yesterday it finally gave up the fight and my attempts to resurrect the installation were futile. After a quick funeral (the eulogy was very touching), and wake in a nearby emporium of alcoholic beverages to commiserate our loss, I set about trying to figure out what to do next.&lt;/p&gt;
&lt;h2 id="alternatives"&gt;Alternatives
&lt;/h2&gt;&lt;p&gt;Now while Redmine is a worthy tool and has always managed to do what I needed in the past, recently its just not cut the mustard. I&amp;rsquo;ve kept toying with the idea of creating our own project management system but as with all in-house projects that we dream up its just never going to happen.&lt;/p&gt;
&lt;p&gt;A quick google around our options are to either go for a hosted solution (not possible as we have some very specific requirements regarding our SCM that mean we have to host our own repos for client work) or Redmine (or chilli project).&lt;/p&gt;
&lt;p&gt;Yes we looked at a number of other management tools and of them all Redmine is still the closes to what we needed.&lt;/p&gt;
&lt;h2 id="installation"&gt;Installation
&lt;/h2&gt;&lt;p&gt;So I spin up a new server instance of ubuntu 12.10 on the cloud and get to work installing the latest version.&lt;/p&gt;
&lt;p&gt;As root I then run through these steps (you should assume that ALL of these steps require you to be root and files should be owned by root)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# update/upgrade base installation of ubuntu packages&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;upgrade&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install the requisite scm tools that we use&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt; &lt;span class="n"&gt;subversion&lt;/span&gt; &lt;span class="n"&gt;mercurial&lt;/span&gt; &lt;span class="n"&gt;cvs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up ruby&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;ruby&lt;/span&gt; &lt;span class="n"&gt;rubygems&lt;/span&gt; &lt;span class="n"&gt;libruby&lt;/span&gt; &lt;span class="n"&gt;ruby&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up apache &amp;amp; mysql&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;apache2&lt;/span&gt; &lt;span class="n"&gt;libapache2&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mod&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;passenger&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="n"&gt;libmysqlclient&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install imagemagick and the magick wand&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;imagemagick&lt;/span&gt; &lt;span class="n"&gt;libmagickcore&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="n"&gt;libmagickwand5&lt;/span&gt; &lt;span class="n"&gt;libmagickwand&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# create our user and database in mysql &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# replace uniquePassword with your own password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;create user &amp;#39;redmine&amp;#39;@&amp;#39;localhost&amp;#39; identified by &amp;#39;uniquePassword&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;create database redmine&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;grant all on redmine.* to &amp;#39;redmine&amp;#39;@&amp;#39;localhost&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;flush privileges&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# clone redmine code to target location&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;share&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="n"&gt;clone&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;redmine&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;redmine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set apache as the owner of redmine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;chown&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;redmine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# move into our new redmine folder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="n"&gt;redmine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up your database configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;cp&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;vim&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;production:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; adapter: mysql2
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; database: redmine
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; host: localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; username: redmine
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; password: uniquePassword
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# install bundler gem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;bundler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# use bundler to set up redmine installation and without specified dependencies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;without&lt;/span&gt; &lt;span class="n"&gt;development&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="n"&gt;postgresql&lt;/span&gt; &lt;span class="n"&gt;sqlite&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up our secret token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;rake&lt;/span&gt; &lt;span class="n"&gt;generate_secret_token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# set up our database and load default configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="n"&gt;rake&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;migrate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;production&lt;/span&gt; &lt;span class="n"&gt;rake&lt;/span&gt; &lt;span class="n"&gt;redmine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;load_default_data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# edit /etc/apache2/sites-available/default
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;VirtualHost *:80&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAdmin webmaster@localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerName mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAlias www.mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; DocumentRoot /usr/local/share/redmine/public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options FollowSymLinks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /usr/local/share/redmine/public&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options Indexes FollowSymLinks MultiViews
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride All
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Order allow,deny
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; allow from all
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory &amp;#34;/usr/lib/cgi-bin&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Order allow,deny
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Allow from all
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ErrorLog ${APACHE_LOG_DIR}/error.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # Possible values include: debug, info, notice, warn, error, crit,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # alert, emerg.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; LogLevel warn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CustomLog ${APACHE_LOG_DIR}/access.log combined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/VirtualHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# restart apache 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;service apache2 restart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That should be enough for you to have a working installation of redmine ready for you to use/customise&lt;/p&gt;
&lt;h2 id="additional-config"&gt;Additional Config
&lt;/h2&gt;&lt;p&gt;We typically have additional steps that we would configure for our own installation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# add plugin assets folder
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir /usr/local/share/redmine/public/plugin_assets
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chown www-data:www-data /usr/local/share/redmine/public/plugin_assets
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# enable some additional apache modules
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a2enmod rewrite
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# disable mod ssl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a2dismod ssl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# install gnutls 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install libapache2-mod-gnutls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# install ssl certificate bundle and key (this assumes that you have already copied the key and bundle to ~/)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv ~/my_certificate.bnd /etc/ssl/certs/my_certificate.bnd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod 0644 /etc/ssl/certs/my_certificate.bnd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv ~/my_certificate.crt /etc/ssl/private/my_certificate.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod 0600 /etc/ssl/private/my_certificate.key
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# now configure your /etc/apache2/sites-available/default-tls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;IfModule mod_gnutls.c&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;VirtualHost _default_:443&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAdmin webmaster@localhost
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerName mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAlias www.mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; DocumentRoot /usr/local/share/redmine/public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options FollowSymLinks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;Directory /usr/local/share/redmine/public&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options Indexes FollowSymLinks MultiViews
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride All
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Order allow,deny
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; allow from all
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/Directory&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ErrorLog ${APACHE_LOG_DIR}/error.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # Possible values include: debug, info, notice, warn, error, crit, alert, emerg.
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; LogLevel warn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CustomLog ${APACHE_LOG_DIR}/ssl_access.log combined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSEnable On
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSCertificateFile /etc/ssl/certs/my_certificate.bnd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSKeyFile /etc/ssl/private/my_certificate.key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; GnuTLSPriorities NORMAL:!DHE-RSA:!DHE-DSS:!AES-256-CBC:%COMPAT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/VirtualHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/IfModule&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# Add some Rails / Passenger specific config to /etc/apache2/sites-available/default-tls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RailsEnv production
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerDefaultUser www-data
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerSpawnMethod smart
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerPoolIdleTime 300
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerMaxRequests 5000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerStatThrottleRate 5
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PassengerHighPerformance On
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# change your /etc/apache2/sites-available/default to redirect to ssl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;VirtualHost *:80&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAdmin sysadmin@zucchi.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerName mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ServerAlias www.mysite.co.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RewriteEngine On
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RewriteCond %{HTTPS} off
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Options FollowSymLinks
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; AllowOverride None
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ErrorLog ${APACHE_LOG_DIR}/error.log
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; LogLevel warn
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; CustomLog ${APACHE_LOG_DIR}/access.log combined
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/VirtualHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# enable your new default-tls vhost and restart apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;a2ensite default-tls
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;service apache2 restart
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# setup &amp;amp;amp; configure email
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# when prompted select &amp;#34;internet site&amp;#34; and enter the domain you are hosting redmine from i.e. mysite.co.uk)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install postfix
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# create config file and uncomment the production settings for sendmail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp /usr/local/share/redmine/config/configuration.yml.example /usr/local/share/redmine/config/configuration.yml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vim /usr/local/share/redmine/config/configuration.yml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;production:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; email_delivery:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; delivery_method: :sendmail
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;service apache2 restart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;#install pixel cookers theme cos we like it
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone git://github.com/pixel-cookers/RedmineThemePixelCookers.git /usr/local/share/redmine/public/themes/pixel-cookers
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Debug PHP CLI on Remote Server with Xdebug and PHPStorm</title><link>https://blog-570662.gitlab.io/debug-cli-remote-server/</link><pubDate>Wed, 06 Feb 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/debug-cli-remote-server/</guid><description>&lt;p&gt;This was a head scratcher when I ran into this yesterday and I thought I would share my solution to the following scenario:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;I need to debug PHP Command Line script, located on Remote LAMP Virtual WebServer running in Virtual Box with a Shared Folder, using local PHPStorm 5.0.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The solution:&lt;/p&gt;
&lt;p&gt;You first must set PHPStorm to use remote file paths. To set these go to the following:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHPStorm -&amp;gt; Peferences -&amp;gt; PHP -&amp;gt; Servers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This gives the following display:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/02/PHPStorm-Peferences.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="PHPStorm Peferences" class="gallery-image" data-flex-basis="656px" data-flex-grow="273" height="312" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/debug-cli-remote-server/PHPStorm-Peferences.png" srcset="https://blog-570662.gitlab.io/debug-cli-remote-server/PHPStorm-Peferences_hu_a6aad5ce5c233f20.png 800w, https://blog-570662.gitlab.io/debug-cli-remote-server/PHPStorm-Peferences.png 854w" width="854"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Replace the Name, Host and Absolute path on the server, to match your own settings. Note keep the Name and Host the same for ease.&lt;/p&gt;
&lt;p&gt;Next add some breakpoints in PHPStorm and set it to listen for any debug connections using the listener icon:&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2013/02/Listen-to-debug-connections.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="Listen to debug connections" class="gallery-image" data-flex-basis="546px" data-flex-grow="227" height="90" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/debug-cli-remote-server/Listen-to-debug-connections.png" width="205"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now login to your Remote Server via SSH etc.&lt;/p&gt;
&lt;p&gt;You now need to change settings for Xdebug in either xdebug.ini or php.ini depending on how you installed it. You also need to know the IP of the local machine. This can permanently set in the Network Setting of your VM in Virtual Box, so you will never have to change it. In my example the local machine running PHPStorm is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;192.168.56.1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Now edit the ini file that contains your Xdebug settings and set the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_host = 192.168.56.1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_connect_back = 0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_port = 9000
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_handler = dbgp
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_mode = req
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.remote_enable = 1
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;xdebug.idekey = phpstorm1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Be aware you might have to change the &lt;strong&gt;remote_host&lt;/strong&gt; and the &lt;strong&gt;idekey&lt;/strong&gt; based on your own environment. To better understand what each option does, see &lt;a class="link" href="http://xdebug.org/docs/all_settings" title="Xdebug Settings"
 target="_blank" rel="noopener"
 &gt;Xdebug Settings&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Finally, when running the script you must set the following variables:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PHP_IDE_CONFIG=&amp;#34;serverName=dev.example.com&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PHP_IDE_CONFIG will tell PHPStorm how to map the Remote File Paths to what it sees Locally. Again replace the URL with the Name/Host you set in PHPStorm. &lt;strong&gt;Note:&lt;/strong&gt; You can export this, if your system is only running one site; mine is not.&lt;/p&gt;
&lt;p&gt;You can run this inline with your script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PHP_IDE_CONFIG=&amp;#34;serverName=dev.example.com&amp;#34; ./testscript.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This should send you to PHPStorm where you earlier placed breakpoints.&lt;/p&gt;
&lt;p&gt;Happy Debugging!&lt;/p&gt;</description></item><item><title>Quick and easy setup of and connection to NRPE on Ubuntu</title><link>https://blog-570662.gitlab.io/quick-dirty-setup-nrpe-ubuntu/</link><pubDate>Wed, 06 Feb 2013 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/quick-dirty-setup-nrpe-ubuntu/</guid><description>&lt;h2 id="about-nrpe"&gt;About NRPE
&lt;/h2&gt;&lt;p&gt;NRPE (Nagios Remote Plugin Executor) is a useful tool that allows you to execute scripts on remote servers and return the output for ingestion by some form of monitoring software.&lt;/p&gt;
&lt;h2 id="setup"&gt;Setup
&lt;/h2&gt;&lt;p&gt;We currently have our own instance of Icinga running to monitor our servers and have recently started to offer access to it for our clients.&lt;/p&gt;
&lt;p&gt;The majority of our servers (and our clients servers if we set them up) use one variant or another of Ubuntu. This means we can very quickly get our servers connected to a Nagios/Icinga instance.&lt;/p&gt;
&lt;p&gt;First things first we need to install the nrpe server and all the associated plugins&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install nagios-nrpe-server \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nagios-plugins-basic \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nagios-plugins \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nagios-plugins-extra
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next we need to edit the main nrpe config file to be found @ /etc/nagios/nrpe.cfg. What your looking for is the lines&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ALLOWED HOST ADDRESSES&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This is an optional comma-delimited list of IP address or hostnames &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# that are allowed to talk to the NRPE daemon.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Note: The daemon only does rudimentary checking of the client&amp;#39;s IP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# address. I would highly recommend adding entries in your /etc/hosts.allow&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# file to allow only the specified host to connect to the port&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# you are running this daemon on.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# NOTE: This option is ignored if NRPE is running under either inetd or xinetd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;allowed_hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# COMMAND ARGUMENT PROCESSING&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This option determines whether or not the NRPE daemon will allow clients&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# to specify arguments to commands that are executed. This option only works&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# if the daemon was configured with the --enable-command-args configure script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# option. &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# *** ENABLING THIS OPTION IS A SECURITY RISK! *** &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Read the SECURITY file for information on some of the security implications&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# of enabling this variable.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Values: 0=do not allow arguments, 1=allow command arguments&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;dont_blame_nrpe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You will want to change this to the IP of your Nagios/Icinga instance and set the dont_blame_nrpe value to 1. Feel free to take a look round the rest of the file. Its all quite interesting and generally will documented. Be careful what you change though in case something breaks.&lt;/p&gt;
&lt;p&gt;You will also want to look for some lines that are refererd to as &amp;ldquo;COMMAND DEFINITIONS&amp;rdquo; and look something like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_hda1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_disk&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;hda1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_zombie_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Z&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_total_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can go ahead and comment these out as we will be adding our own definitions shortly. The main reason for removing these is that we will be configuring some specific scripts for our own use later that allow you to configure your requirements and thereshold from within your Nagios/Icinga config.&lt;/p&gt;
&lt;h2 id="configuration-of-monitoring-server"&gt;Configuration of Monitoring Server
&lt;/h2&gt;&lt;p&gt;Once this is complete you can now configure a new &amp;ldquo;check command&amp;rdquo; for use with your nagios/icinga server.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;define command {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_name check_nrpe
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;define command {
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_name check_nrpe_command_args
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$ -a $ARG2$
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here you can see that we have set up 2 different check commands. The first is a simple command requiring only one argument of $ARG1$ which would be the name of the command we want to run on the remote server. The second command is almost identical except for the fact it takes a second argument which allows you to input a series of &amp;ldquo;arguments&amp;rdquo; to be passed to the command on your remote server. each argument should be separated by a space.&lt;/p&gt;
&lt;p&gt;Now that you have these you can then configure your hosts and services to make use of it. I would recommend having a trawl through the Nagios/Icinga sites &amp;amp; documentation to find out how to create a config that suits you.&lt;/p&gt;
&lt;h2 id="configuration-of-remote-server"&gt;Configuration of Remote Server
&lt;/h2&gt;&lt;p&gt;Now that we have our monitoring server ready its time to add the command we want to run to the remote server.&lt;/p&gt;
&lt;p&gt;To do this your /etc/nagios/nrpe.cfg shoudl hopefully have a line in it that looks like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;include=/etc/nagios/nrpe_local.cfg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;if it doesn&amp;rsquo;t have a line like that then add it and edit the `/etc/nagios/nrpe_local.cfg` file to look a little like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_apt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_apt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_users&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_load&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_disk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_disk&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sda1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG1&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG2&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ARG3&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_zombie_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Z&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;check_total_procs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;nagios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check_procs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These are a few simple commands that I tend to use most often. These translate to your &amp;ldquo;check_nrpe&amp;rdquo; commands like so&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$ARG1$ = everything inside the square brackets [ ]&lt;/li&gt;
&lt;li&gt;$ARG2$ = each of the $ARG?$ keys as a single string separated by a space&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that&amp;rsquo;s done you should be able restart your nrpe server with `/etc/init.d/nagios-nrpe-server restart`&lt;/p&gt;
&lt;p&gt;It really is that simple. Do bear in mind that because you can pass arbitrary arguments into nrpe this was you could leave yourself vulnerable to a bit of maliciousness so its a good idea to make sure your firewall restricts port 5666 (the default port) to IPs you trust.&lt;/p&gt;</description></item><item><title>Compiling Apache 2.4 on Ubuntu 12.04</title><link>https://blog-570662.gitlab.io/compiling-apache-2-4-ubuntu-12-04/</link><pubDate>Tue, 06 Nov 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/compiling-apache-2-4-ubuntu-12-04/</guid><description>&lt;p&gt;I&amp;rsquo;ve decided that I need to up my game when it comes to webservers. However I&amp;rsquo;m not yet ready to switch to Nginx or one of the other webservers out in the wild as I need something up and running rapidly.&lt;/p&gt;
&lt;p&gt;Granted the numbers are definitely against Apache in a lot of benchmarks but historically I&amp;rsquo;ve always had a good experience and the entry level makes it much more appropriate for me to stick with it.&lt;/p&gt;
&lt;p&gt;However Apache 2.2 is rather long in the tooth, thankfully 2.4 has been out for a while now. The problem I have is that I tend to favour Ubuntu as a platform and there is no sign of a 2.4 version appearing on the horizon anytime soon as they are waiting for it to be implemented upsteam in Debian before including it in Ubuntu.&lt;/p&gt;
&lt;p&gt;Now there are PPAs available out there but im not overly happy using them (especially on production environments) So the only option is to compile.&lt;/p&gt;
&lt;p&gt;First thing is to install all the dependencies we are going to need. Thankfuly ubuntu has a nice and simple way of handling this.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get build-dep apache2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can then download the source code and start the compilation.&lt;/p&gt;
&lt;p&gt;So from the root of our new copy of the source we need to run our configure.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./configure --prefix=/usr/local/apache2 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-mods-shared=all \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-http \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-deflate \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-expires \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-slotmem-shm \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-headers \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-rewrite \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy-balancer \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy-http \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-proxy-fcgi \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-mime-magic \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --enable-log-debug \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --with-mpm=event
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You will notice that I&amp;rsquo;m installing it using the event mpm. Hopefully I&amp;rsquo;ll be covering more about the event mpm in the future.&lt;/p&gt;
&lt;p&gt;Next we need to run make&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make &amp;amp;&amp;amp; make install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once that&amp;rsquo;s complete you should be able to run&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/usr/local/apache2/bin/apachectl start
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and get the &amp;ldquo;it works&amp;rdquo; message through your webrowser when accessing the server IP.&lt;/p&gt;
&lt;p&gt;Dont forget to configure apache to suit your specific requirements.&lt;/p&gt;
&lt;p&gt;Something that will come up is how to start apache on boot. Seeing as Ubuntu uses Upstart it makes sense to utilise it for controlling apache.&lt;/p&gt;
&lt;p&gt;So in the file `/etc/ini/apache.conf` we need to put&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# apache2 - http server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Apache is a web server that responds to HTTP and HTTPS requests.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Required-Start: $local_fs $remote_fs $network $syslog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Required-Stop: $local_fs $remote_fs $network $syslog&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Matt Cockayne &amp;lt;matt@zucchi.co.uk&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Apache 2.4 HTTP Server&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;runlevel&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2345&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;runlevel&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="mi"&gt;2345&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pre&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mkdir&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ssl_scache shouldn&amp;#39;t be here if we&amp;#39;re just starting up.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# (this is bad if there are several apache2 instances running)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;rm&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/*&lt;/span&gt;&lt;span class="n"&gt;ssl_scache&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Give up if restart occurs 10 times in 30 seconds.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;respawn&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;respawn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;envvars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;envvars&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ULIMIT_MAX_FILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ulimit -S -n `ulimit -H -n`&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x$ULIMIT_MAX_FILES&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;ULIMIT_MAX_FILES&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;httpd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt; &lt;span class="n"&gt;FOREGROUND&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a rather simple upstart script and I will be looking to update it at some point&amp;hellip; but it works&lt;/p&gt;
&lt;p&gt;Once that&amp;rsquo;s done you should find that on reboot Apache will start and take advantage of all the management features of upstart including attempting to respawn Apache should it end unexpectedly. You should also be able to then use the following commands to control Apache.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# how to start start apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;start apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl start apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# how to stop apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl stop apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# how to restart apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;restart apache 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl restart apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# check the status of apache 
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;status apache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# or
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initctl status apache
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I generally tend to avoid using the apachectl script found at /usr/local/apache/bin/apachectl once upstart takes control.&lt;/p&gt;</description></item><item><title>Compiling PHP 5.4 on Ubuntu 12.04</title><link>https://blog-570662.gitlab.io/compiling-php-5-4-ubuntu-12-04/</link><pubDate>Tue, 06 Nov 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/compiling-php-5-4-ubuntu-12-04/</guid><description>&lt;p&gt;So recently I&amp;rsquo;ve been working with PHP 5.4 a LOT. Unfortunately Ubuntu (my main dev environment) is behind the times. So I&amp;rsquo;m resorting to compiling PHP manually.&lt;/p&gt;
&lt;p&gt;Not a daunting as it may first appear. The really tricky part is working out your dependencies and `configure` script.&lt;/p&gt;
&lt;p&gt;Hence the reason for this post as a reminder for myself and others that may want to do a quick compile. (I would recommend that if your compiling for a production/live environment that you make sure you understand what it is your compiling though before just using what&amp;rsquo;s here)&lt;/p&gt;
&lt;p&gt;So where to start. Dependencies first I think&lt;/p&gt;
&lt;p&gt;Ubuntu allows you to install dependencies for building source `apt-get build-deps`. We will use this and install any extras we may need.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;apt-get install \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxml2 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxml2-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libssl-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg-config \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libcurl4-nss-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;enchant \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libenchant-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libjpeg8 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libjpeg8-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libpng12-0 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libpng12-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libvpx1 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libvpx-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libfreetype6 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libfreetype6-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libt1-5 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libt1-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libgmp10 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libgmp-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libicu48 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libicu-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mcrypt \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libmcrypt4 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libmcrypt-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libpspell-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libedit2 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libedit-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libsnmp15 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libsnmp-dev \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxslt1.1 \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;libxslt1-dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And now the configure&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;./&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;apxs2&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apache2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;apxs&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fpm&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fpm&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;fpm&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;openssl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;kerberos&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zlib&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;calendar&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;curl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;curlwrappers&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;enchant&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;exif&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ftp&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gd&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;jpeg&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;png&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;vpx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;freetype&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;t1lib&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;exif&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;native&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;ttf&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gd&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;jis&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;conv&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gettext&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;gmp&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mhash&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;intl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mbstring&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mcrypt&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mysqli&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pcntl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pdo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pgsql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pgsql&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pspell&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;libedit&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;shmop&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;snmp&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;soap&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sockets&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sysvmsg&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sysvshm&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;xsl&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zip&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pear&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zend&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt; \
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;enable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;maintainer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;zts&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once these are done then we follow the standard make process. Notice we are also running make test&amp;hellip; very important as it givges more data for the developers to work with.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make &amp;amp;&amp;amp; make test &amp;amp;&amp;amp; make install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The next thing is configuring your php.ini file as the install doesn&amp;rsquo;t have one yet so we copy either the production or development default from the source code to the new conf dir and edit to suit your needs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp {php-source-dir}/php.ini-(development|production) /usr/local/php/conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Thats it. All ready to roll&amp;hellip; almost, this installation is the one I use for use with a webserver so you will want to add the appropriate directives to apache.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;LoadModule php5_module modules/libphp5.so
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AddHandler php5-script .php
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;AddType text/html .php
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Rsync and custom SSH commands</title><link>https://blog-570662.gitlab.io/rsync-custom-ssh-commands/</link><pubDate>Tue, 31 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/rsync-custom-ssh-commands/</guid><description>&lt;p&gt;Rsync is a great tool but can be a pain if you have to jump through hoops to connect via ssh such as connecting via a different port.&lt;/p&gt;
&lt;p&gt;A simple solution is to use the &lt;strong&gt;-e&lt;/strong&gt; flag (also knows as &amp;ndash;rsh=COMMAND). This flag allows you manually define the ssh command to use when connecting&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -e &amp;#39;ssh -p2020&amp;#39; -rav ./* user@server:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Will allow me to connect to a server with SSH listening on port 2020&lt;/p&gt;</description></item><item><title>Nice New Nexus7</title><link>https://blog-570662.gitlab.io/nice-nexus7/</link><pubDate>Thu, 19 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/nice-nexus7/</guid><description>&lt;img src="https://blog-570662.gitlab.io/nice-nexus7/tablet-n7-features-ushome-family.png" alt="Featured image of post Nice New Nexus7" /&gt;&lt;p&gt;This morning I woke up to an email telling me that my Nexus7 that I had ordered 3 weeks ago was&amp;hellip; &amp;ldquo;out for delivery&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I couldn&amp;rsquo;t contain my excitement. I sat patiently waiting by my door. Finally 11 o&amp;rsquo;clock rolls around and there is a knock. I&amp;rsquo;m handed a brown parcel and hand over the obligatory signature. I close the door behind me and carefully place the box on the desk. I contemplate teasing myself and seeing how long I can hold out before opening it.&lt;/p&gt;
&lt;p&gt;That lasted about 20 seconds!!!!&lt;/p&gt;
&lt;p&gt;In fact&amp;hellip; this video says it all&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://www.youtube.com/watch?v=Xijcwbg8CGQ" target="_blank" rel="noopener"
 &gt;http://www.youtube.com/watch?v=Xijcwbg8CGQ&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not gonna bore you with how awesome it is (and it is awesome). I will however point out a few obvious foibles with it though (not that they would have ever stopped me from buying it).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No way to expand storage&lt;/li&gt;
&lt;li&gt;Lack of Flash (may not seem important, but until everyone else catches up there is loads of content I cant use i.e. BBC iPlayer)&lt;/li&gt;
&lt;li&gt;A number of apps (games specifically) that I have run on my phone are not yet supported&lt;/li&gt;
&lt;li&gt;Google Now feels a little clunky at times and struggles with some of the regional British accents&lt;/li&gt;
&lt;li&gt;MTP doesn&amp;rsquo;t appear to work out of the box with my Linux OS (I&amp;rsquo;m sure this will be remedied soon)&lt;/li&gt;
&lt;li&gt;No obvious way to directly access the front facing camera (easily remedied with an app from &lt;a class="link" href="https://play.google.com/store/apps/details?id=com.modaco.cameralauncher" title="Camera Launcher"
 target="_blank" rel="noopener"
 &gt;MoDaCo&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Registering custom view helpers in ZF2</title><link>https://blog-570662.gitlab.io/registering-custom-view-helpers-zf2/</link><pubDate>Wed, 18 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/registering-custom-view-helpers-zf2/</guid><description>&lt;p&gt;If you want to register custom view helpers with a module you can do so by using the service location built into the Skeleton Application and creating a module config that looks something like.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;return array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;view_helpers&amp;#39; =&amp;gt; array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;invokables&amp;#39; =&amp;gt; array(
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // generic view helpers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;truncate&amp;#39; =&amp;gt; &amp;#39;Zucchi\View\Helper\Truncate&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; // form based view helpers
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;bootstrapForm&amp;#39; =&amp;gt; &amp;#39;Zucchi\Form\View\Helper\BootstrapForm&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;bootstrapRow&amp;#39; =&amp;gt; &amp;#39;Zucchi\Form\View\Helper\BootstrapRow&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;#39;bootstrapCollection&amp;#39; =&amp;gt; &amp;#39;Zucchi\Form\View\Helper\BootstrapCollection&amp;#39;,
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Bootstrapping ZF2 Forms</title><link>https://blog-570662.gitlab.io/bootstrapping-zf2-forms/</link><pubDate>Tue, 17 Jul 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/bootstrapping-zf2-forms/</guid><description>&lt;p&gt;So&amp;hellip;&lt;/p&gt;
&lt;p&gt;With the release of beta 5 for Zend Framework 2 I thought it time for me to tidy up and fix a few modules I created back at beta 3.&lt;/p&gt;
&lt;p&gt;Now I&amp;rsquo;m a big fan of Twitter Bootstrap CSS framework as I&amp;rsquo;m sure a lot of other people are as well. Seeing that the Zend Skeleton Application comes with bootstrap already included it was easy enough to set up my forms using the old ZF Forms found in ZF1.&lt;/p&gt;
&lt;p&gt;However a brand spanking new Forms component has been rolled out with ZF2. The long and the short of this new component meant that I had the opportunity to hand roll a new way of making my forms work with Twitter Bootstrap.&lt;/p&gt;
&lt;p&gt;So, a little tinkering, a quick &lt;a class="link" href="https://github.com/zendframework/zf2/pull/1893" target="_blank" rel="noopener"
 &gt;pull request&lt;/a&gt; to ZF2 to allow the definition of arbitrary options and I came up with some useful View Helpers that can be dropped into a project and used.&lt;/p&gt;
&lt;p&gt;You can find them at &lt;a class="link" href="https://github.com/zucchi/Zucchi/tree/master/src/Zucchi/Form/View/Helper" title="https://github.com/zucchi/Zucchi"
 target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/Zucchi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So how to use them. Lets start by creating a new form (we&amp;rsquo;ll keep it simple for now)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;MyForm&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Form&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;public&lt;/span&gt; &lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;myform&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;price&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;attributes&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;required&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;required&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;placeholder&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;0.99&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;label&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;help&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;style&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;block&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;content&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;The price you wish to use&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;prepend&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;$&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;append&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;¢&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;actions&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;form-actions&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;attributes&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;submit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Save&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;btn btn-primary&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;style&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;inline&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reset&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;attributes&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;type&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reset&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;reset&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;class&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;btn&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;options&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;style&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;inline&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;ll notice that I have highlighted some lines. Thanks to the ability to set arbitrary options we can define a &amp;ldquo;bootstrap&amp;rdquo; option which we can then use to allow us to pass data into our new bootstrap view helpers. You can also see that I have added a save and reset button to a collection. I&amp;rsquo;ll explain that later.&lt;/p&gt;
&lt;p&gt;So what next&amp;hellip; Rather than go into the mechanics of how to work with forms I&amp;rsquo;ll refer you to &lt;a class="link" href="http://zend-framework-2-doc.readthedocs.org/en/latest/modules/zend.form.intro.html" title="the ZF documentation"
 target="_blank" rel="noopener"
 &gt;the ZF documentation&lt;/a&gt; and this excellent &lt;a class="link" href="http://www.michaelgallego.fr/blog/?p=190" title="New Zend\Form features explained"
 target="_blank" rel="noopener"
 &gt;blog post&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We then pick up by looking at your view, and the helpers I have created.&lt;/p&gt;
&lt;h2 id="bootstrapformform-formstyle"&gt;BootstrapForm($form, $formStyle)
&lt;/h2&gt;&lt;p&gt;One of the few things I miss from the ZF1 implementation of Forms is the self rendering aspect! So what did I decide to do? That&amp;rsquo;s right I created a view helper to render everything in one command.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;$this-&amp;gt;bootstrapForm()&lt;/code&gt; takes two parameters. The first is quite obviously the form. The second is the style of form. This is directly related to the form types that can be found &lt;a class="link" href="http://twitter.github.com/bootstrap/base-css.html#forms" title="here"
 target="_blank" rel="noopener"
 &gt;http://twitter.github.com/bootstrap/base-css.html#forms&lt;/a&gt;. You can use any of &amp;lsquo;vertical&amp;rsquo;, &amp;lsquo;inline&amp;rsquo;, &amp;lsquo;search&amp;rsquo; &amp;amp; &amp;lsquo;horizontal&amp;rsquo;. If you dont specify a formStyle then it will default to &amp;lsquo;vertical&amp;rsquo;&lt;/p&gt;
&lt;p&gt;Caveat: This helper will then iterate through all of the associated elements and render them first. Only after the direct elements have been generated will it then move onto Collections or Fieldsets (as soon as I work out how I&amp;rsquo;ll fix this).&lt;/p&gt;
&lt;h2 id="bootstraprowelement-formstyle"&gt;BootstrapRow($element, $formStyle)
&lt;/h2&gt;&lt;p&gt;This is a straightforward modification of the &lt;code&gt;FormRow&lt;/code&gt; helper that come bundled with the new component.&lt;/p&gt;
&lt;p&gt;We have a few differences now though. We have a second parameter as with the &lt;code&gt;BootstrapForm&lt;/code&gt; view helper and the output is generated using sprintf and a set of templates that mimic the structures of the different form styles from bootstrap.&lt;/p&gt;
&lt;p&gt;This helper can be used by itself to generate an element row and is used by the &lt;code&gt;BootstrapForm&lt;/code&gt; helper&lt;/p&gt;
&lt;p&gt;We can also now take advantage of the &amp;ldquo;bootstrap&amp;rdquo; options we set earlier.&lt;/p&gt;
&lt;h3 id="bootstrap-options"&gt;Bootstrap Options
&lt;/h3&gt;&lt;p&gt;style&lt;/p&gt;
&lt;p&gt;The style of form element to use regardless of what style may be passed into the view helper (you can see an example of this in the buttons from the MyForm example above)&lt;/p&gt;
&lt;p&gt;help&lt;/p&gt;
&lt;p&gt;This works in the same way as &amp;ldquo;description&amp;rdquo; did from ZF1 but allows you to define it either as a string or an array with the keys &amp;ldquo;style&amp;rdquo; for either &amp;lsquo;inline&amp;rsquo; or &amp;lsquo;block&amp;rsquo; and &amp;ldquo;Content&amp;rdquo; which should be self explainatory&lt;/p&gt;
&lt;p&gt;prepend&lt;/p&gt;
&lt;p&gt;Takes advantage of Bootstraps ability to prepend blocks to an input field. This can be defined as a single string, or an array of strings to allow you to add multiple blocks should you want to&lt;/p&gt;
&lt;p&gt;prepend&lt;/p&gt;
&lt;p&gt;Takes advantage of Bootstraps ability to append blocks to an input field. This can be defined as a single string, or an array of strings to allow you to add multiple blocks should you want to&lt;/p&gt;
&lt;p&gt;These options get evaluated and spat out from the new &lt;code&gt;renderBootstrapOptions()&lt;/code&gt; method as part of the &amp;ldquo;render&amp;rdquo;.&lt;/p&gt;
&lt;h2 id="bootstrapcollectionelement-style-wrap"&gt;BootstrapCollection($element, $style, $wrap)
&lt;/h2&gt;&lt;p&gt;Again this is a direct rip off of the &lt;code&gt;FormCollection&lt;/code&gt; helper found in the ZF2 Form component witha few modifications. The main difference is that is makes use of the &lt;code&gt;BootstrapRow&lt;/code&gt; helper and has methods and properties to allow the setting of the form style to use.&lt;/p&gt;
&lt;p&gt;You can see from the &lt;code&gt;MyForm&lt;/code&gt; example above that we set a &lt;code&gt;Collection&lt;/code&gt; called &amp;lsquo;actions&amp;rsquo;. This is a pretty standard way of grouping elements together. You can also see that we set a class for the &lt;code&gt;Collection&lt;/code&gt; which may look familiar to those that have used Twitter Bootstrap for a while.&lt;/p&gt;
&lt;p&gt;What our helper will then do is wrap the buttons in a div with the appropriate class attached. If you were to define a &lt;code&gt;label&lt;/code&gt; for the Collection/Fieldset You would then also find that the fieldset and legend tags are also spat out with our &lt;code&gt;&amp;lt;div class=&amp;quot;form-actions&amp;quot;&amp;gt;&lt;/code&gt; sandwiched between them and the elements.&lt;/p&gt;
&lt;h3 id="result"&gt;Result
&lt;/h3&gt;&lt;p&gt;So what we now get when we use &lt;code&gt;MyForm&lt;/code&gt; with out helpers.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$this-&amp;gt;bootstrapForm($form, &amp;#39;horizontal&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Should now look something like this&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://phpboyscout.uk/wp-content/uploads/2012/07/bootstrap-result1.png" target="_blank" rel="noopener"
 &gt;&lt;img alt="results of bootstrap helper" class="gallery-image" data-flex-basis="760px" data-flex-grow="316" data-title-escaped="bootstrap-result" height="159" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/bootstrapping-zf2-forms/bootstrap-result1.png" title="bootstrap-result" width="504"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="how-you-can-use-it"&gt;How you can use it
&lt;/h3&gt;&lt;p&gt;As of right now you can get the library from its repo on github @ &lt;a class="link" href="https://github.com/zucchi/Zucchi" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/Zucchi&lt;/a&gt; and can be found on &lt;a class="link" href="http://packagist.org/packages/zucchi/zucchi" title="zucchi/zucchi"
 target="_blank" rel="noopener"
 &gt;packagist&lt;/a&gt; for use with composer&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edit:&lt;/strong&gt; The bootstrap stuff has moved to a new location as a separate ZF2 module. you can find it @ &lt;a class="link" href="https://github.com/zucchi/ZucchiBootstrap" target="_blank" rel="noopener"
 &gt;https://github.com/zucchi/ZucchiBootstrap&lt;/a&gt; or @ &lt;a class="link" href="http://packagist.org/packages/zucchi/bootstrap" title="zucchi/bootstrap"
 target="_blank" rel="noopener"
 &gt;packagist&lt;/a&gt; for use with composer&lt;/p&gt;</description></item><item><title>Loaded Testing</title><link>https://blog-570662.gitlab.io/loaded-testing/</link><pubDate>Sat, 30 Jun 2012 00:00:00 +0000</pubDate><guid>https://blog-570662.gitlab.io/loaded-testing/</guid><description>&lt;p&gt;I recently had to do some load testing for a site recently that would allow me to test in excess of 100k requests in a 60 second period&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://jmeter.apache.org/" target="_blank" rel="noopener"
 &gt;&lt;img alt="JMeter" class="gallery-image" data-flex-basis="520px" data-flex-grow="216" data-title-escaped="jmeter-logo" height="102" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/loaded-testing/jmeter-logo.jpg" title="jmeter-logo" width="221"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So I decided to do some testing using JMeter as it seemed like a suitable tool for doing what I needed and I had used it for some simpler testing in the past.&lt;/p&gt;
&lt;p&gt;After a little fumbling around I managed to get a test plan designed that would simulate 10k users actually navigating the site and adding to a cart etc, with a number of various interactions. It wasnt perfect but it would correctly simulate over 100k requests.&lt;/p&gt;
&lt;p&gt;So feeling quite pleased with myself I started the test from my laptop. Now I&amp;rsquo;m not a big gamer, I&amp;rsquo;m known to play a little World or Warcraft from time to time but that&amp;rsquo;s about it. So when it comes to computing power i tend to opt for battery life over sheer grunt.&lt;/p&gt;
&lt;p&gt;Suffice to say, my laptop fell flat on its face, and if it hadn&amp;rsquo;t it turns out that the connection I was using just wasn&amp;rsquo;t up to the task of handling that much traffic adequately.&lt;/p&gt;
&lt;p&gt;So plan B&amp;hellip;&lt;/p&gt;
&lt;p&gt;I quickly fired up the largest AWS instance available and got a copy of jmeter installed. A little tinkering with my test plan and some googling on how to run jmeter without a gui and a quick&lt;/p&gt;
&lt;p&gt;&lt;code&gt;./jmeter -n -t test-plan.jmx&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and it appeared to be running.&lt;/p&gt;
&lt;p&gt;(Please bear in mind that I&amp;rsquo;m being overly kind&amp;hellip; it took a LOT of tinkering and twice as much Googling to work out how to get the test results out so i could actually get some idea of WTF was happening during the test)&lt;/p&gt;
&lt;p&gt;So&amp;hellip; client &amp;ldquo;happy&amp;rdquo;&amp;hellip; I decided to go and find a better way to do my load testing in the future.&lt;/p&gt;
&lt;p&gt;Sticking with JMeter I managed to find this gem of a page&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://jmeter.apache.org/usermanual/remote-test.html" title="Remote Testing"
 target="_blank" rel="noopener"
 &gt;http://jmeter.apache.org/usermanual/remote-test.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;tl;dr &amp;gt; use your local install of jmeter to trigger tests to run on one or more remote &amp;ldquo;nodes&amp;rdquo; and then have all the results sent to your local install.&lt;/p&gt;
&lt;p&gt;So I set to work!&lt;/p&gt;
&lt;h2 id="building-a-node"&gt;&lt;strong&gt;Building a Node&lt;/strong&gt;
&lt;/h2&gt;&lt;p&gt;First I need to set up an AWS instance that we can use and duplicate so I can quickly build a cluster of nodes on demand. I&amp;rsquo;m a big fan of Ubuntu so I spin up a micro instance of 12.04 server. Next I shell into the instance and install the default Java runtime from apt&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt-get install openjdk-7-jre&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Yes I know there are other more appropriate runtimes, but i dont really care&amp;hellip; i just need it to work and it does.&lt;/p&gt;
&lt;p&gt;next I grab a copy of the latest stable from &lt;a class="link" href="http://jmeter.apache.org/download_jmeter.cgi" title="Download Apache JMeter"
 target="_blank" rel="noopener"
 &gt;http://jmeter.apache.org/download_jmeter.cgi&lt;/a&gt; and un-tar it to &lt;code&gt;/usr/local/jmeter&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;(N.B. JMeter is available through the apt but I had issues with that version and you need to make sure that both your local version and all the nodes run the same version of jmeter)&lt;/p&gt;
&lt;p&gt;We can now test that the install is working running &lt;code&gt;/usr/local/jmeter/bin/jmeter-server&lt;/code&gt; and you should get some output that looks similar to&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Created remote object: UnicastServerRef [liveRef: [endpoint:[10.???.???.???:38939](local),objID:[46522b57:138381f1023:-7fff, 2635011707874933136]]]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which tells us that the server is running.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BUT&lt;/strong&gt; unfortunately its not going to work just yet. Because we are using Amazons EC2 we are going to relying on their NAT for routing. Out of the box JMeter just wont work properly.&lt;/p&gt;
&lt;p&gt;However there is something we can do to combat this. We can set the parameter &lt;code&gt;RMI_HOST_DEF&lt;/code&gt; that the &lt;code&gt;/usr/local/jmeter/bin/jmeter-server&lt;/code&gt; script will include in starting the server.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="n"&gt;RMI_HOST_DEF&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="n"&gt;Djava&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rmi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wget&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ll explain what we are doing here. Amazon have been quite clever by providing a &lt;a class="link" href="http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html" target="_blank" rel="noopener"
 &gt;meta-data endpoint&lt;/a&gt; that you can poll from within your instance to get key pieces of data&amp;hellip; Including the public dns record.&lt;/p&gt;
&lt;p&gt;We can use this endpoint and using wget pipe that into the &lt;code&gt;RMI_HOST_DEF&lt;/code&gt; param (ensuring that we prepend &lt;code&gt;-D&lt;/code&gt;) and then export that so it becomes available to the &lt;code&gt;/usr/local/jmeter/bin/jmeter-server&lt;/code&gt; script.&lt;/p&gt;
&lt;p&gt;Now to get the server to start on boot.&lt;/p&gt;
&lt;p&gt;a quick upstart script should solve this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Upstart script to initialise jmeter-server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;JMeter Server&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Dev in Charge &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="n"&gt;networking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;stopping&lt;/span&gt; &lt;span class="n"&gt;networking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;stopping&lt;/span&gt; &lt;span class="n"&gt;shutdown&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# get the current public DNS record&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="n"&gt;RMI_HOST_DEF&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="n"&gt;Djava&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rmi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;=$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wget&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;169.254&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# start jmeter in server mde&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jmeter&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jmeter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;saving this to &lt;code&gt;/etc/init/jmeter-server.conf&lt;/code&gt; will mean that it will auto-start jmeter-server on boot and allow you to manually control the process using &lt;code&gt;start jmeter-server&lt;/code&gt; and &lt;code&gt;stop jmeter-server&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;and thats it&amp;hellip; instance configured&lt;/p&gt;
&lt;p&gt;&lt;a class="link" href="http://aws.amazon.com/" target="_blank" rel="noopener"
 &gt;&lt;img alt="Powered by AWS" class="gallery-image" data-flex-basis="590px" data-flex-grow="245" data-title-escaped="AWS_Logo_PoweredBy_300px" height="122" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://blog-570662.gitlab.io/loaded-testing/AWS_Logo_PoweredBy_300px.png" title="AWS_Logo_PoweredBy_300px" width="300"&gt;&lt;/a&gt;All you need to do now is save the instance as an AMI and you have an on-demand image for spinning up a cluster of remote JMeter servers for you to play with.&lt;/p&gt;
&lt;h2 id="configuring-your-local-installation"&gt;Configuring your local installation
&lt;/h2&gt;&lt;p&gt;Now that the server side is working we need to configure our local installation to allow it to connect.&lt;/p&gt;
&lt;p&gt;First things first however, make sure you are using the same version of JMeter as you are running on the server.&lt;/p&gt;
&lt;p&gt;We need to edit the &lt;code&gt;jmeter.properties&lt;/code&gt; file that can be found in the bin folder of the installtion you downloaded. Look for the parameter &lt;code&gt;remote_hosts&lt;/code&gt; This needs to be set with the public dns of the remote server(s) your connecting to. for example&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;remote_hosts=ec2-176-34-164-170.eu-west-1.compute.amazonaws.com,ec2-123-34-456-789.eu-west-1.compute.amazonaws.com
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Thats your local version configured. You will now be able to tell your local version to run tests on any or all of your specified remotes.&lt;/p&gt;
&lt;p&gt;However if your like me you work behind a router/firewall. If so this isnt the end of the story. When you send a test plan to a remote from your local install it will also send the IP address of your local machine for it to send the results back to. JMeter does this by looking up where your current hostname resolves to. In my circumstance it resolved to &lt;code&gt;127.0.1.1&lt;/code&gt;. The reason it did this is down to the fact my systems host file had the line&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;127.0.1.1 devincharge.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To resolve this I had to change it to my external IP address&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;89.345.871.79 devincharge.local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And set up port forwarding from my router to my local machine for all ports from 1024 to 65535. Now, you can if you want use specific ports so you dont have to port forward everything from your router, but i&amp;rsquo;ll leave that for you to lookup as there are plenty resources on how to do this for you to google and I&amp;rsquo;ve waffled on for far too long already.&lt;/p&gt;
&lt;p&gt;Happy testing&lt;/p&gt;</description></item></channel></rss>