<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Wilhelm Codes · Archive</title>
    <link>https://wilhelm.codes/blog/</link>
    <description>Slinger of nibbles, bits and bytes. Over 25 years of professional experience as a software engineer. Love making glowing rectangles go &#34;beep-boop&#34;.</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <managingEditor>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</managingEditor>
    <webMaster>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</webMaster>
    <lastBuildDate>Fri, 17 Jul 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://wilhelm.codes/blog/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>It&#39;s OK to Let Go</title>
      <link>https://wilhelm.codes/blog/its-ok-to-let-go/</link>
      <pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/its-ok-to-let-go/</guid>
      <category>go</category>
      <category>open-source</category>
      <description>So, I&amp;rsquo;m a sucker for wordplay and puns. Anyway&amp;hellip;&#xA;There&amp;rsquo;s a line in my shell history that goes back to 2022: go get github.com/wilhelm-murdoch/go-collection. For years it was the first dependency into almost every Go project I tinkered with, sometimes before I&amp;rsquo;d even give the idea a solid shape. I&amp;rsquo;d open Neovim, initialise a module and then my little collection library got pulled in. Just like muscle memory.&#xA;Today I&amp;rsquo;m archiving it. Not because it broke and not because I got bored of it, but because the Go standard library quietly walked up and did its job better than it ever could. This post is part explanation and part eulogy, because I think we&amp;rsquo;re generally quite bad at ending software on purpose and it&amp;rsquo;s worth practising in public.&#xA;</description>
      <content:encoded><![CDATA[<p>So, I&rsquo;m a sucker for wordplay and puns. Anyway&hellip;</p>
<p>There&rsquo;s a line in my shell history that goes back to 2022: <code>go get github.com/wilhelm-murdoch/go-collection</code>. For years it was the first dependency into almost every Go project I tinkered with, sometimes before I&rsquo;d even give the idea a solid shape. I&rsquo;d open Neovim, initialise a module and then my little collection library got pulled in. Just like muscle memory.</p>
<p>Today I&rsquo;m archiving it. Not because it broke and not because I got bored of it, but because the Go standard library quietly walked up and did its job better than it ever could. This post is part explanation and part eulogy, because I think we&rsquo;re generally quite bad at ending software on purpose and it&rsquo;s worth practising in public.</p>
<h2 id="why-even-write-it-in-the-first-place">
  <a class="heading-link" href="#why-even-write-it-in-the-first-place">Why even write it in the first place?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Waaaay back in 2022 Go 1.18 had just shipped generics after roughly a decade of the community asking, arguing and writing increasingly unhinged <code>interface{}</code> workarounds. I wanted to actually <em>learn</em> the new type parameter machinery rather than just read about it and I was also deeply tired of writing the same <code>for</code> loop to check whether a slice contained a thing for the hundredth time.</p>
<p>So <a href="https://github.com/wilhelm-murdoch/go-collection">go-collection</a> happened. A single generic <code>Collection[T]</code> type wrapping a slice, with all the conveniences I kept reaching for: <code>Contains</code>, <code>Find</code>, <code>Filter</code>, <code>Map</code>, <code>Sort</code>, <code>Batch</code>, <code>Push</code>, <code>Pop</code> and a few dozen friends. All chainable, fluent, tested and small.</p>
<blockquote class="pull-quote">
  There are far more comprehensive modules out there, but this one works quite well for my purposes.
  <cite>me, in the README, setting expectations appropriately for once</cite>
</blockquote>

<p>That sentence was doing more work than I realised at the time. It wasn&rsquo;t trying to compete with the big functional-utility libraries. It was scratching a personal itch and for a few years it scratched it well. Then Go&rsquo;s standard library grew and slurped most of it up.</p>
<h2 id="taking-a-trip-to-the-morgue">
  <a class="heading-link" href="#taking-a-trip-to-the-morgue">Taking a trip to the morgue.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Cause of death is easy to establish here, because it happened in three well-documented releases. Go 1.21 shipped the <code>slices</code> package. Go 1.22 and 1.23 finished the job. Let&rsquo;s walk through it, method by method, like the nerds we are.</p>
<h3 id="does-it-have-the-thing">
  <a class="heading-link" href="#does-it-have-the-thing">Does it have the thing?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The bread and butter. Probably eighty percent of my actual usage of this library was some flavour of &ldquo;is this thing in there?&rdquo;:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fruits</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">collection</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;apple&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;orange&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;strawberry&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fruits</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="s">&#34;orange&#34;</span><span class="p">)</span><span class="w"> </span><span class="c1">// true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">fruits</span><span class="p">.</span><span class="nf">FindIndex</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">i</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">item</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;str&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w"> </span><span class="c1">// 2</span><span class="w">
</span></span></span></code></pre></div><p>As of Go 1.21, the standard library does both on a plain slice with no wrapper type in sight:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">fruits</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">&#34;apple&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;orange&#34;</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;strawberry&#34;</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="nx">fruits</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;orange&#34;</span><span class="p">)</span><span class="w"> </span><span class="c1">// true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">IndexFunc</span><span class="p">(</span><span class="nx">fruits</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">item</span><span class="w"> </span><span class="kt">string</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">HasPrefix</span><span class="p">(</span><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="s">&#34;str&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w"> </span><span class="c1">// 2</span><span class="w">
</span></span></span></code></pre></div><p>No <code>New</code>, no <code>.Items()</code> to unwrap at the end when some other API wants a real slice. It&rsquo;s just my function with better ergonomics and a compiler team maintaining it. I&rsquo;m fine with this.</p>
<h3 id="sorting-where-i-dont-even-get-to-feel-bitter">
  <a class="heading-link" href="#sorting-where-i-dont-even-get-to-feel-bitter">Sorting, where I don&rsquo;t even get to feel bitter.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Here&rsquo;s my <code>Sort</code>. I want you to look at what using it actually required:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">numbers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">collection</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">numbers</span><span class="p">.</span><span class="nf">Sort</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">i</span><span class="p">,</span><span class="w"> </span><span class="nx">j</span><span class="w"> </span><span class="kt">int</span><span class="p">)</span><span class="w"> </span><span class="kt">bool</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">left</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">numbers</span><span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="nx">i</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">right</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">numbers</span><span class="p">.</span><span class="nf">At</span><span class="p">(</span><span class="nx">j</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">left</span><span class="w"> </span><span class="p">&lt;</span><span class="w"> </span><span class="nx">right</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span></code></pre></div><p>Index-based comparators, so you had to reach <em>back into the collection you were currently sorting</em> to get at the values. I inherited that design from the old <code>sort.Slice</code> idiom and it was clunky then too. The modern equivalent:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">numbers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">Sort</span><span class="p">(</span><span class="nx">numbers</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>And when you&rsquo;re sorting something with actual structure, <code>SortFunc</code> hands you the two elements directly, with <code>cmp.Compare</code> doing the boring part:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">slices</span><span class="p">.</span><span class="nf">SortFunc</span><span class="p">(</span><span class="nx">people</span><span class="p">,</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="nx">Person</span><span class="p">)</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="k">return</span><span class="w"> </span><span class="nx">cmp</span><span class="p">.</span><span class="nf">Compare</span><span class="p">(</span><span class="nx">a</span><span class="p">.</span><span class="nx">Age</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nx">Age</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">})</span><span class="w">
</span></span></span></code></pre></div><p>This isn&rsquo;t a case of the standard library catching up to my library. It blew my version right out of the water.</p>
<h3 id="the-killing-blow-came-with-iterators">
  <a class="heading-link" href="#the-killing-blow-came-with-iterators">The killing blow came with iterators.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Go 1.22 added <code>slices.Concat</code>, which retired my <code>Concat</code>. Fine, that one was three lines anyway. But Go 1.23 shipped range-over-function iterators and with them <code>slices.Chunk</code>, and that&rsquo;s the release where I stopped pretending this was still a contest. <code>Batch</code> was one of the few methods I was genuinely proud of:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">records</span><span class="p">.</span><span class="nf">Batch</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">batch</span><span class="p">,</span><span class="w"> </span><span class="nx">index</span><span class="w"> </span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">item</span><span class="w"> </span><span class="nx">Record</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// process item as part of batch N</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">},</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span><span class="w">
</span></span></span></code></pre></div><p>Now it&rsquo;s a language feature wearing a standard library hat:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">chunk</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">slices</span><span class="p">.</span><span class="nf">Chunk</span><span class="p">(</span><span class="nx">records</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// chunk is a []Record of up to 100 items</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>A real <code>for</code> loop. You can <code>break</code> out of it, <code>continue</code> past things and return early. My callback-based version couldn&rsquo;t do any of that without contorting itself. The trouble is that a wrapper library can only ever be as expressive as the language it wraps and the language just grew.</p>
<h3 id="the-stuff-that-never-needed-a-library">
  <a class="heading-link" href="#the-stuff-that-never-needed-a-library">The stuff that never needed a library.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>An honest confession while we&rsquo;re standing over the body: a decent chunk of the API never deserved to exist in the first place. <code>Push</code>, <code>Pop</code>, <code>Shift</code>, <code>Unshift</code>. I wrote JavaScript for years before Go was ever a public thing and apparently nobody walks away from that unscathed.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">c</span><span class="p">.</span><span class="nf">Push</span><span class="p">(</span><span class="s">&#34;e&#34;</span><span class="p">)</span><span class="w">         </span><span class="c1">// s = append(s, &#34;e&#34;)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nx">item</span><span class="p">,</span><span class="w"> </span><span class="nx">ok</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nf">Pop</span><span class="p">()</span><span class="w"> </span><span class="c1">// item = s[len(s)-1]; s = s[:len(s)-1]</span><span class="w">
</span></span></span></code></pre></div><p>The comments on the right are the entire implementation. Idiomatic Go slice handling was already good at this in 2012. Those methods existed because my fingers missed <code>Array.prototype</code>, not because Go was missing anything.</p>
<h3 id="the-survivors-if-you-could-even-call-them-that">
  <a class="heading-link" href="#the-survivors-if-you-could-even-call-them-that">The &ldquo;survivors&rdquo;, if you could even call them that.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Here&rsquo;s the wrinkle that makes this more interesting than a straight obituary. <code>Map</code>, <code>Filter</code>, <code>Reduce</code>, <code>Some</code>, <code>None</code>; the functional stalwarts <em>still</em> aren&rsquo;t in the standard library, all these years later. On paper, that&rsquo;s the corner of the market my little module could have retired to. A niche! Sustained relevance!</p>
<p>Except, no. First, because my <code>Map</code> had a secret shame:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="p">(</span><span class="nx">c</span><span class="w"> </span><span class="o">*</span><span class="nx">Collection</span><span class="p">[</span><span class="nx">T</span><span class="p">])</span><span class="w"> </span><span class="nf">Map</span><span class="p">(</span><span class="nx">f</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">out</span><span class="w"> </span><span class="nx">Collection</span><span class="p">[</span><span class="nx">T</span><span class="p">])</span><span class="w">
</span></span></span></code></pre></div><p>See it? <code>T</code> in, <code>T</code> out. Go methods can&rsquo;t introduce new type parameters, so a method-based <code>Map</code> can never change the element type. Strings to strings, ints to ints. A <code>Map</code> that can only map onto itself is a <code>Map</code> in vibes only; the moment you want lengths of strings you&rsquo;re back to writing a loop anyway.</p>
<p>And second, more fundamentally: the Go community collectively shrugged and decided the loop was fine. The proposal discussions happened, the experiments ran and the ecosystem largely settled on this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="nx">names</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="nb">len</span><span class="p">(</span><span class="nx">ducks</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">for</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">duck</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">ducks</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">names</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">names</span><span class="p">,</span><span class="w"> </span><span class="nx">strings</span><span class="p">.</span><span class="nf">ToUpper</span><span class="p">(</span><span class="nx">duck</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Four lines. Obvious to anyone who&rsquo;s read any Go at all, no import, nothing to learn, nothing to maintain and debugger-friendly. Even where <code>go-collection</code> wasn&rsquo;t superseded, it turned out to be unnecessary, which is somehow a more thorough defeat.</p>
<h2 id="software-is-a-tool-not-a-legacy">
  <a class="heading-link" href="#software-is-a-tool-not-a-legacy">Software is a tool, not a legacy.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>So that&rsquo;s the technical half. Here&rsquo;s the part I actually wanted to write about.</p>
<p>We don&rsquo;t really have a culture of <em>finishing</em> software. A project is either &ldquo;actively maintained&rdquo; or it&rsquo;s &ldquo;abandoned&rdquo;, with all the guilt that word drags behind it. Issues accumulate, badges rot and the README slowly becomes a lie because nobody wants to be the person who admits the thing is done, or no longer necessary. I&rsquo;ve had this module sitting in that limbo for a while now, still getting pulled into new projects out of habit while a strictly better replacement sat in the standard library the whole time.</p>
<p>But software isn&rsquo;t a legacy; it&rsquo;s a tool. Tools serve a purpose. This one&rsquo;s purpose was to teach me generics in their first year of existence and to spare me a few thousand <code>for</code> loops while the language sorted itself out. Purpose served on both counts. And of all the ways for a library to die, being absorbed by the standard library is the best one available. It means the gap you filled was real and now it&rsquo;s just <em>gone</em>, fixed at the correct layer, for everyone and forever. That&rsquo;s not a failure state. That&rsquo;s the mission succeeding so hard the mission ceases to exist.</p>
<p>Keeping it alive from here would mean maintaining a slightly worse, slightly slower, personally-branded copy of <code>slices</code> out of pure sentiment. Nobody needs that, least of all me. I&rsquo;m busy enough and would much rather move on to newer, more interesting things.</p>
<h2 id="the-practical-bits">
  <a class="heading-link" href="#the-practical-bits">The practical bits.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>So today, the repo gets the full send-off:</p>
<ul>
<li>A deprecation notice at the top of the README, pointing anyone who lands there at the <a href="https://pkg.go.dev/slices">slices</a> package and at this post.</li>
<li>The repository gets <a href="https://github.com/wilhelm-murdoch/go-collection">archived</a> on GitHub. Read-only, clearly signposted and locked in eternal stasis.</li>
</ul>
<p>If you&rsquo;re one of the however-many people with this module in a <code>go.mod</code> somewhere nothing breaks. The Go module proxy caches published versions more or less permanently, so <code>v1.0.11</code> will keep resolving long after we&rsquo;re all dust. Archiving just makes the end of our relationship official rather than leaving it implied.</p>
<p>If you&rsquo;ve got a little module of your own in the same situation that has been superseded, purpose spent, kept alive out of habit, then consider this your permission slip. Write the notice, hit archive and go build that next thing. An honest ending is a kindness to your users and to yourself. It&rsquo;s OK to let go.</p>
<p><a href="https://github.com/wilhelm-murdoch/glazier/commit/8b6b84a220225fe714b8623aac1b183d64faf09a">Here</a> is an example of me tearing <code>go-collection</code> out of my very recent Glazier project. This one stung a little at how easily it was replaced.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Thanks, little buddy. You were there before the ecosystem knew what generics were supposed to look like, you appeared in more of my <code>go.mod</code> files than any dependency I didn&rsquo;t write, and you were, right to the end, exactly as comprehensive as I needed you to be.</p>
<p><code>go get</code> in peace.</p>
]]></content:encoded>
    </item>
    <item>
      <title>I Added Variable Block Support To Glazier</title>
      <link>https://wilhelm.codes/blog/i-couldnt-talk-myself-out-of-typed-variables/</link>
      <pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/i-couldnt-talk-myself-out-of-typed-variables/</guid>
      <category>go</category>
      <category>hcl</category>
      <category>tmux</category>
      <category>glazier</category>
      <description>I knew the moment I thought about potentially adding variable definition blocks as a first-class feature Glazier it was going to bother me until I went about implementing it. So, as expected, a week later, here we are. I am happy to report that I&amp;rsquo;ve maintained my spotless record of not talking myself out of something tricky.&#xA;So, over the course of the past few days Glazier learned a new trick. Profiles can now declare the variables they accept, give them primitive types, mark them as required and read them back through a proper var. namespace.&#xA;</description>
      <content:encoded><![CDATA[<p>I knew the moment I thought about potentially adding variable definition blocks as a first-class feature Glazier it was going to bother me until I went about implementing it. So, as expected, a week later, here we are. I am happy to report that I&rsquo;ve maintained my spotless record of <em>not</em> talking myself out of something tricky.</p>
<p>So, over the course of the past few days Glazier learned a new trick. Profiles can now declare the variables they accept, give them primitive types, mark them as required and read them back through a proper <code>var.</code> namespace.</p>
<h2 id="out-with-the-old">
  <a class="heading-link" href="#out-with-the-old">Out with the old.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Technically speaking, Glazier has had variable support since day one. You could pass <code>--var region=watson</code> and reference <code>${region}</code> anywhere in your profile and that was that. Anything you passed got dumped into a flat namespace and any name you <em>referenced</em>, but forgot to set, quietly resolved to an empty string.</p>
<p>That&rsquo;s fine right up until you fat-finger <code>${reigon}</code> and spend ten minutes wondering why your session name has a hole in it. The whole reason I built this thing was to get Terraform&rsquo;s &ldquo;here is exactly what you did wrong and where&rdquo; experience.</p>
<p>Letting variables fail silently just felt a bit like an own-goal.</p>
<h2 id="profiles-can-now-declare-what-they-accept">
  <a class="heading-link" href="#profiles-can-now-declare-what-they-accept">Profiles can now declare what they accept.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Because this whole thing is kind of a love letter to Terraform&rsquo;s parser, the idea behind variable definitions is functionally the same. You declare a <code>variable</code> block, it sits at the top level next to your session and that&rsquo;s the input you&rsquo;re allowed to pass:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;district&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  description</span> <span class="o">=</span> <span class="s2">&#34;the district the gig is themed after&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  type</span>        <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl"><span class="n">  default</span>     <span class="o">=</span> <span class="s2">&#34;watson&#34;</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;fixer&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span> <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;gig-${var.district}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;${var.fixer}-ops&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;echo ${var.fixer} has the next job&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>First, you reach a variable through <code>var.</code> and <em>only</em> <code>var.</code> now, so it reads exactly like it would in a <code>.tf</code> file. Second, <code>district</code> has a default and <code>fixer</code> doesn&rsquo;t, which turns out to matter quite a lot in a second.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">fixer</span><span class="o">=</span>wakako <span class="c1"># district falls back to &#34;watson&#34;</span>
</span></span><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">district</span><span class="o">=</span>arasaka --var <span class="nv">fixer</span><span class="o">=</span>wakako
</span></span></code></pre></div><p>So, here you are using the same templated profile to define two seperate sessions decided entirely by flags.</p>
<h2 id="pass-it-something-it-doesnt-know-and-it-says-so">
  <a class="heading-link" href="#pass-it-something-it-doesnt-know-and-it-says-so">Pass it something it doesn&rsquo;t know and it says so.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>If you pass a <code>--var</code> for a variable the profile never declared, you no longer get a silent shrug. You get a proper error:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Undefined variable
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">A value for &#34;ghost&#34; was passed with --var, but the profile declares no
</span></span><span class="line"><span class="cl">variable &#34;ghost&#34;. Add a variable &#34;ghost&#34; {} block, or remove the flag.
</span></span></code></pre></div><p>I went back and forth on this one. Being strict about undeclared flags is the sort of thing that feels pedantic until the day it catches a typo you&rsquo;d have otherwise chased for half an hour. In this case strict won.</p>
<p>A <code>--var</code> you can&rsquo;t explain is almost always a mistake, so Glazier now treats it like one.</p>
<h2 id="no-default-means-you-have-to-mean-it">
  <a class="heading-link" href="#no-default-means-you-have-to-mean-it">No default means you have to mean it.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A variable <em>without</em> a default is required. Leave it unset and <code>up</code> refuses to do anything until you supply it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Missing required variable
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  on .glaze line 6, in variable &#34;fixer&#34;:
</span></span><span class="line"><span class="cl">   6: variable &#34;fixer&#34; {
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The variable &#34;fixer&#34; has no default, so a value must be supplied with
</span></span><span class="line"><span class="cl">--var fixer=&lt;value&gt;.
</span></span></code></pre></div><p>This is the bit that makes a profile a genuine contract rather than a suggestion. If a layout is useless without a fixer, declare <code>fixer</code> with no default and the tool will make absolutely sure you provided one before it touches tmux.</p>
<h2 id="proper-type-checking">
  <a class="heading-link" href="#proper-type-checking">Proper type-checking.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Every variable declares a <code>type</code>. Use one of the bare keywords <code>string</code>, <code>number</code> or <code>bool</code>. The value you hand over on the command line is coerced into that type and rejected if it doesn&rsquo;t fit:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;base_index&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">number</span>
</span></span><span class="line"><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">$ glaze up --var base_index=two
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Error: Invalid variable value
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  on .glaze line 1, in variable &#34;base_index&#34;:
</span></span><span class="line"><span class="cl">   1: variable &#34;base_index&#34; {
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The value passed for variable &#34;base_index&#34; with --var cannot be used as
</span></span><span class="line"><span class="cl">number: a number is required.
</span></span></code></pre></div><p><code>two</code> is not a number, so you find out <em>before</em> anything launches rather than after tmux has already swallowed a garbage option and started behaving oddly. Booleans get the same treatment: <code>true</code> and <code>false</code> are fine, anything else is shown the door. Unfortunately, I decided not to go with truthy or falsey value coercion.</p>
<p>I kept it to the three primitives on purpose. The moment you add <code>list(string)</code> and friends you&rsquo;re also signing up for coercing comma-soup off the command line into nested types. That&rsquo;s a much bigger can of worms than a tmux helper needs to open today.</p>
<p>Though, obviously, if you disagree you&rsquo;re more than welcome to leave a comment below!</p>
<h2 id="a-namespace-for-everything">
  <a class="heading-link" href="#a-namespace-for-everything">A namespace for everything.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I&rsquo;ll be honest about the consequence here. This is a breaking change and pretending otherwise would be rude. The old flat <code>${region}</code> style is gone. If a value comes from a declared variable, you read it as <code>var.region</code>.</p>
<p>Which raised a genuinely interesting design question I sat with for a while. If variables are only ever used <em>inside</em> a session, why not declare them inside the session block too? It feels more intuitive at first though, but it&rsquo;s also wrong.</p>
<p>However you nest the declaration, the <em>reference</em> is still flat and global; you write <code>var.fixer</code>, never <code>session.var.fixer</code>. Declaring something file-global but reading it block-scoped is incoherent and a variable is really an input to the <em>invocation</em>, sitting conceptually above the session and not a property of it. So they live at the top level, exactly where Terraform puts them. I borrowed both the syntax and the reasoning.</p>
<h2 id="where-did-the-environment-variables-go">
  <a class="heading-link" href="#where-did-the-environment-variables-go">Where did the environment variables go?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>They also got their own namespace. Previously <code>GLAZE_ENV_token</code> showed up as a bare <code>${token}</code>, sharing the same flat namespace as everything else. Now it lives under an <code>env.</code> namespace, which makes the three sources nicely symmetric:</p>
<ul>
<li><code>var.*</code> for variables you declared and passed with <code>--var</code></li>
<li><code>env.*</code> for <code>GLAZE_ENV_*</code> environment variables</li>
<li><code>path.pwd</code> &amp; <code>path.base</code> the unchanged built-ins</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;host-${env.box}&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {}
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nv">GLAZE_ENV_box</span><span class="o">=</span>nightcity glaze up
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">session = host-nightcity
</span></span></code></pre></div><p>There&rsquo;s no declaration needed for <code>env.*</code>; the environment is the environment. Only the things you pass with <code>--var</code> have to be declared, because those are the inputs <em>you</em> are claiming the profile accepts.</p>
<h2 id="tearing-down-a-session">
  <a class="heading-link" href="#tearing-down-a-session">Tearing down a session.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>If a required variable now blocks <code>up</code>, doesn&rsquo;t that wreck <code>glaze down</code>? I recently taught the <code>down</code> command to evaluate <em>only</em> the session name so it doesn&rsquo;t need every variable buried deep in the profile just to kill a session. Surely required variables undo that?</p>
<p>They don&rsquo;t and this was the fiddly bit to get right. <code>down</code> resolves variables leniently. A variable used solely in some pane command, far away from the session name, is neither required nor evaluated when you&rsquo;re tearing things down:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">variable</span> <span class="s2">&#34;beep&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  type</span> <span class="o">=</span> <span class="k">string</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;daemon-run&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;echo ${var.beep}&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze down
</span></span><span class="line"><span class="cl"><span class="go">INF nothing to do; session is not running session=daemon-run
</span></span></span></code></pre></div><p>If <code>beep</code> isn&rsquo;t provided, the command just accepts it and moves on, because <code>down</code> never needed it. <code>up</code> enforces the full contract; <code>down</code> only asks for what the name actually depends on. Obviously, the exception here is if a variable is used to construct the actual session name. Other than that, this is an idempotent no-op.</p>
<h2 id="validation-has-been-updated-too">
  <a class="heading-link" href="#validation-has-been-updated-too">Validation has been updated too.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p><code>glaze format --validate</code> decodes a profile and reports diagnostics without ever touching tmux and it now enforces the <em>entire</em> variable contract. Miss a required variable and validation fails right there:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze format --validate
</span></span><span class="line"><span class="cl"><span class="go">Error: Missing required variable
</span></span></span><span class="line"><span class="cl"><span class="go">  ...
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="gp">$</span> glaze format --validate --var <span class="nv">region</span><span class="o">=</span>us-east-1
</span></span><span class="line"><span class="cl"><span class="gp">#</span> clean. nothing to report.
</span></span></code></pre></div><p>So, you can lint a parameterised profile in CI, or just before committing it and know it holds together before you ever try to bring it up.</p>
<h2 id="this-literally-broke-all-my-current-profiles">
  <a class="heading-link" href="#this-literally-broke-all-my-current-profiles">This literally broke all my current profiles.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>And I regret nothing. The flat-namespace migration touched pretty much everything; sample profiles, the docs, a pile of tests and all the profiles I actually use day to day. Normally this is the part of a post where I&rsquo;d sketch out a careful migration path and apologise profusely.</p>
<p>But, the entire userbase of this tool is almost, as far as I can tell, me. So the migration path is &ldquo;I&rsquo;ll fix all my profiles tomorrow morning over coffee.&rdquo;</p>
<blockquote class="pull-quote">
  There is a real freedom in shipping a breaking change to an audience you can fit in a mirror.
  <cite>it me.</cite>
</blockquote>

<p>To migrate is easy enough, just define your variables and prefix all references with <code>var.</code>. This <em>is</em> still currently in beta after all.</p>
<h2 id="in-closing">
  <a class="heading-link" href="#in-closing">In closing&hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Under the hood this turned into a more interesting refactor than I expected. <code>hcldec</code> wants to decode a whole file body against one spec and it flatly refuses to tolerate a <code>variable</code> block sitting next to the session it doesn&rsquo;t know about. The trick was to stop handing it the whole file, pull the session block out myself and decode <em>just its body</em>, which leaves the variable blocks to be gathered and resolved in their own quiet little pass beforehand. There&rsquo;s a tidy <code>PartialContent</code> implementation in there that was heaps of fun to figure out.</p>
<p>None of which a tmux wrapper strictly needed, of course. But &ldquo;strictly needed&rdquo; stopped being the bar around the time I added a fuzzer. The point was to feel out how Terraform turns a typo into a friendly, located error and now Glazier does a respectable impression of it for variables too.</p>
<p>I&rsquo;m still ironing out all the kinks and ensuring I still have an 80% baseline in testing and code coverage, but I&rsquo;m planning to release this and a few bug fixes and dependency bumps this weekend. Tomorrow-me gets to read this back with fresh eyes and decide how much of it past-me oversold. Very nice.</p>
<p>Until I merge it in, you can play with the latest release <a href="https://github.com/wilhelm-murdoch/glazier">here</a>.</p>
<p>And now I&rsquo;m thinking about adding <code>*.tfvars</code>-like functionality. Surely, I won&rsquo;t fall for this again?</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why I Built Glazier</title>
      <link>https://wilhelm.codes/blog/why-i-built-glazier/</link>
      <pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/why-i-built-glazier/</guid>
      <category>go</category>
      <category>tmux</category>
      <category>hcl</category>
      <category>glazier</category>
      <description>I live in tmux. I typically have an editor here, a dev server there, logs tailing in the corner or a spare pane for poking at things. The trouble is that this little world is frustratingly ephemeral. Rebooting my machine ( thank you, compuslory MacOS updates ), kill the wrong session or just close the laptop lid for too long and it can all evaporate. Then, I&amp;rsquo;m back to rebuilding the same layout by hand, one split-window at a time, like some kind of animal.&#xA;</description>
      <content:encoded><![CDATA[<p>I live in <a href="https://github.com/tmux/tmux/wiki">tmux</a>. I typically have an editor here, a dev server there, logs tailing in the corner or a spare pane for poking at things. The trouble is that this little world is frustratingly ephemeral. Rebooting my machine ( thank you, compuslory MacOS updates ), kill the wrong session or just close the laptop lid for too long and it can all evaporate. Then, I&rsquo;m back to rebuilding the same layout by hand, one <code>split-window</code> at a time, like some kind of animal.</p>
<p>So I built <a href="https://github.com/wilhelm-murdoch/glazier">Glazier</a>; a small command-line tool that lets me describe a tmux workspace once and recreate it on demand. Type <code>glaze up</code> and the sessions, windows and panes I described spring back into existence exactly how I left them.</p>
<p>This has been a slow-burning labour of love for the better part of two years and it&rsquo;s finally in a state where I feel comfortable letting other people look at it. So let&rsquo;s talk about why it exists, what else is out there and how this one is different.</p>
<h2 id="its-just-a-config-file-right">
  <a class="heading-link" href="#its-just-a-config-file-right">It&rsquo;s just a config file, right?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>That was the idea, at least. I just wanted to stop rebuilding the same layouts over and over. But, as is tradition, I didn&rsquo;t want to make it <em>too</em> easy for myself.</p>
<p>There was a second, more selfish motivation. As a platform engineer, there isn&rsquo;t a day that goes by where I don&rsquo;t work with <a href="https://www.terraform.io/">Terraform</a>. I&rsquo;ve always been quietly fascinated by how it parses and validates its configuration. That whole experience of getting a precise, friendly error pointing at the exact line you fat-fingered, rather than a stack trace and a 🖕. I wanted to understand how that machinery actually worked.</p>
<p>So, I&rsquo;m a heavy tmux user <em>and</em> I wanted to learn HCL parsing from the inside. These two things lined up a little too perfectly. So Glazier&rsquo;s profiles aren&rsquo;t YAML; they&rsquo;re <a href="https://github.com/hashicorp/hcl">HCL</a>. The same configuration language Terraform uses and parsed with the same underlying library.</p>
<p>Here&rsquo;s a basic profile:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;daemon-run&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>   <span class="o">=</span> <span class="s2">&#34;ice-breaker&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    layout</span> <span class="o">=</span> <span class="s2">&#34;main-vertical&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;nvim ./payloads&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;watch -n1 netwatch --target arasaka-mainframe&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>Drop that in a file called <code>.glaze</code>, run <code>glaze up</code> next to it and you&rsquo;re jacked in.</p>
<h2 id="im-not-the-first-to-do-this-not-even-the-3rd-or-the-4th">
  <a class="heading-link" href="#im-not-the-first-to-do-this-not-even-the-3rd-or-the-4th">I&rsquo;m not the first to do this; not even the 3rd&hellip; or the 4th.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This is a thoroughly-solved problem and I&rsquo;d be doing you a disservice if I pretended otherwise. There&rsquo;s a whole shelf of mature, battle-tested tools that do effectively the same thing:</p>
<ul>
<li><a href="https://github.com/tmuxinator/tmuxinator">tmuxinator</a> is the one most are familiar with. Written in Ruby with YAML profiles. Probably what most people reach for.</li>
<li><a href="https://github.com/remi/teamocil">teamocil</a> is also written Ruby; also uses YAML.</li>
<li><a href="https://github.com/ivaaaan/smug">smug</a> is written in Go and uses YAML and is the closest in spirit to Glazier if we&rsquo;re being honest.</li>
<li><a href="https://github.com/tmux-python/tmuxp">tmuxp</a> is written in good&rsquo;ole reliable Python and it&rsquo;ll happily eat YAML <em>or</em> JSON.</li>
</ul>
<p>If you already use and trust one of these, I&rsquo;ll be straight with you: there isn&rsquo;t a compelling reason to switch. Keep using what works. I&rsquo;m not here to convince anyone to rip out a tool they&rsquo;re happy with.</p>
<p>But if you&rsquo;re still reading, here&rsquo;s what <em>I</em> like about mine.</p>
<h2 id="so-whats-actually-different">
  <a class="heading-link" href="#so-whats-actually-different">So what&rsquo;s actually different?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<h3 id="the-profile-validates-itself">
  <a class="heading-link" href="#the-profile-validates-itself">The profile validates itself!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>This is the part I set out to build, so it&rsquo;s the part I&rsquo;m fondest of. Because Glazier is built on HCL, it inherits Terraform-style diagnostics for free. Mistype a layout, point a starting directory at somewhere that doesn&rsquo;t exist or forget a required block and you don&rsquo;t get a vague &ldquo;something went wrong fuck you&rdquo;. You get told exactly what and where you messed up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: Invalid layout specified
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  on .glaze line 4, in session.window:
</span></span><span class="line"><span class="cl">   4:     layout = &#34;main-plumbus&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The layout value of &#34;main-plumbus&#34; is not a supported preset
</span></span><span class="line"><span class="cl">(even-horizontal, even-vertical, main-horizontal, main-vertical,
</span></span><span class="line"><span class="cl">tiled) nor a valid tmux layout string.
</span></span></code></pre></div><p>There&rsquo;s a <code>glaze format</code> command that rewrites your profile into a canonical style and, with <code>--validate</code>, reports any of these diagnostics without touching tmux at all. Both of these scratch exactly the itch that started the whole project.</p>
<h3 id="variables-templates-and-string-functions-oh-my">
  <a class="heading-link" href="#variables-templates-and-string-functions-oh-my">Variables, templates and string functions! Oh, my!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>A static layout is useful. A <em>templated</em> one is better. Profiles can reference variables and you can feed those in from <code>--var</code> flags or <code>GLAZE_ENV_*</code> environment variables:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span>               <span class="o">=</span> <span class="s2">&#34;ops-${region}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  starting_directory</span> <span class="o">=</span> <span class="k">path</span><span class="p">.</span><span class="k">pwd</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="k">upper</span><span class="p">(</span><span class="k">region</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;k9s --context ${region}&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze up --var <span class="nv">region</span><span class="o">=</span>ap-southeast-2
</span></span></code></pre></div><p>Look familiar? If you work with Terraform it does!</p>
<p>There&rsquo;s a handful of built-in string functions too. <code>upper</code>, <code>lower</code>, <code>replace</code>, <code>trimspace</code>, <code>join</code> and friends which act as thin wrappers over the same <code>go-cty</code> standard library Terraform uses. Plus, a couple of freebies like <code>path.pwd</code> and <code>path.base</code> so a profile can adapt to wherever it&rsquo;s run from.</p>
<h3 id="it-doesnt-lie-to-you-about-timing">
  <a class="heading-link" href="#it-doesnt-lie-to-you-about-timing">It doesn&rsquo;t lie to you about timing.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>This is the bit of engineering I&rsquo;m quietly proudest of, even though nobody will ever see it. When you fire a sequence of commands into a tmux pane, the naive approach is to blast them in with a <code>sleep</code> between each one and hope the previous command finished. That&rsquo;s flaky and it&rsquo;s how a few other tools handle it.</p>
<p>Glazier instead serialises commands through tmux&rsquo;s own <a href="https://man.openbsd.org/tmux#wait-for"><code>wait-for</code></a> signalling, so each command genuinely waits for the previous one to finish before the next is sent. There are no fixed sleeps and no races. The <em>one</em> exception is the final command in a list, which is sent fire-and-forget. If your last command is a long-running dev server or <code>tail -f</code>, waiting on it would hang <code>up</code> forever.</p>
<p>I learned that last part the hard way.</p>
<h3 id="it-can-mostly-save-a-session">
  <a class="heading-link" href="#it-can-mostly-save-a-session">It can <em>mostly</em> save a session!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Run <code>glaze save</code> inside a live session and it&rsquo;ll capture the structure back out into a profile: windows, panes, names, layouts, focus and starting directories. The &ldquo;mostly&rdquo; is doing some load-bearing work in that sentence and it&rsquo;s a deliberate choice, not a missing feature.</p>
<p><code>save</code> will <strong>not</strong> export your pane commands, environment variables, hooks or tmux options. Why? Because each of those is a footgun:</p>
<ul>
<li><strong>Commands</strong> would re-execute on the next <code>up</code>. A forgotten <code>rm -rf</code> captured from some pane could ruin your whole day on replay.</li>
<li><strong>Environment variables</strong> can only be read as the <em>entire</em> session environment. This means inherited secrets, tokens and keys getting written into a file you might commit. That&rsquo;s a big fat no from me, dawg.</li>
<li><strong>Options</strong> read back as effective state, hopelessly tangling up with your <code>tmux.conf</code> and manual tweaks.</li>
</ul>
<p>So, a saved profile is a scaffold. It gets the geometry right and you add back the commands and config you actually want by hand.</p>
<h2 id="a-real-profile">
  <a class="heading-link" href="#a-real-profile">A real profile.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Enough talk. Here&rsquo;s the profile I actually use when working this website:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">session</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="k">path</span><span class="p">.</span><span class="k">base</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">  envs</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">    HUGO_GITHUB_TOKEN</span>        <span class="o">=</span> <span class="s2">&#34;${github}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    HUGO_ACTIVITY_GRAPH_DEMO</span> <span class="o">=</span> <span class="s2">&#34;${demo}&#34;</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>  <span class="o">=</span> <span class="s2">&#34;editor&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;nvim&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span>  <span class="o">=</span> <span class="s2">&#34;terminal&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    focus</span> <span class="o">=</span> <span class="kt">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {}
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;server&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;hugo server --disableFastRender&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">window</span> {
</span></span><span class="line"><span class="cl"><span class="n">    name</span> <span class="o">=</span> <span class="s2">&#34;git&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">pane</span> {
</span></span><span class="line"><span class="cl"><span class="n">      commands</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;lazygit&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>Running <code>glaze up --var github=*** --var demo=true</code> gives me the following windows:</p>
<ul>
<li>My editor of choice; NeoVim.</li>
<li>A dedicated terminal session.</li>
<li>The Hugo server along with some <code>HUGO_</code> specific environment variables; values sourced from the CLI.</li>
<li>Finally, <code>lazygit</code> to manage and commit my changes.</li>
</ul>
<p>Further, the <code>--var</code> arguments aren&rsquo;t really necessary as you could just <code>export GLAZE_var_name</code> as well and Glazier will pick it up automatically.</p>
<p>Anyways, because the session is just a named thing, the rest of the lifecycle is tidy too. List what&rsquo;s running with <code>glaze ls</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze ls
</span></span><span class="line"><span class="cl"><span class="go">NAME      WINDOWS  PATH
</span></span></span><span class="line"><span class="cl"><span class="go">glazier*  2        /home/wilhelm/Development/wilhelm.codes
</span></span></span><span class="line"><span class="cl"><span class="go">scratch   1        /tmp
</span></span></span></code></pre></div><p>The asterisk marks the session I&rsquo;m currently attached to. When I&rsquo;m done, I tear it down by profile:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> glaze down
</span></span></code></pre></div><p><code>down</code> is idempotent and that&rsquo;s on purpose. Bringing down a session that isn&rsquo;t running is a no-op, not an error, so it&rsquo;s safe to drop in scripts without defensive checks.</p>
<h2 id="some-over-engineered-bits-">
  <a class="heading-link" href="#some-over-engineered-bits-">Some over-engineered bits &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>To be frank a tool that shells out to tmux did not strictly <em>need</em> a fuzzed HCL parser, build provenance attestations on its release binaries or a CI pipeline that runs the test suite against multiple Go versions and operating systems.</p>
<p>But, that was never really the point. The point was learning how Terraform&rsquo;s parser ticks, working out how to drive tmux reliably without sleeps and over-engineering the ever-loving-shit out of an already-solved problem because it was <em>fun</em>. Every constraint I imposed upon myself taught me something new. This is ultimately the only metric I actually care about for a hobby project like this.</p>
<h2 id="where-to-get-it">
  <a class="heading-link" href="#where-to-get-it">Where to get it?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>It&rsquo;s <a href="https://github.com/wilhelm-murdoch/glazier">up on GitHub</a> with a shiny MIT license. If you&rsquo;ve got Go installed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> go install github.com/wilhelm-murdoch/glazier/cmd/glaze@latest
</span></span></code></pre></div><p>Or, grab a prebuilt binary from the <a href="https://github.com/wilhelm-murdoch/glazier/releases">releases page</a>. I currently build for Linux and macOS on both <code>amd64</code> and <code>arm64</code>. Each one ships with a checksum and a signed provenance attestation, because of course it does.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I would confidently say Glazier has moved on from &ldquo;experimental&rdquo; to &ldquo;stable&rdquo;. It works and I use it every single day. But, there are rough edges and there may be any number of unencountered failure modes. The <code>down</code> and <code>ls</code> commands only landed recently and I&rsquo;m not sure if the latter should remain. I&rsquo;ve also got a running list of ideas I haven&rsquo;t talked myself out of yet. Like, something similar to Terraform&rsquo;s <code>*.tfvars</code> files or defining typed variables.</p>
<p>If you&rsquo;re already happy with Tmuxinator or Smug, then stick with them. But if the idea of a declarative, self-validating, slightly-too-clever tmux profile appeals to you, or you just want to read some Go that wraps tmux in ways it was probably never meant to be wrapped, I&rsquo;d love for you to take it for a spin.</p>
<p>I sincerely hope you find <a href="https://github.com/wilhelm-murdoch/glazier">Glazier</a> as useful as I had fun building it.</p>]]></content:encoded>
    </item>
    <item>
      <title>Let Me Show You My Bits</title>
      <link>https://wilhelm.codes/blog/let-me-show-you-my-bits/</link>
      <pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/let-me-show-you-my-bits/</guid>
      <category>hugo</category>
      <category>css</category>
      <category>tailwindcss</category>
      <category>wilhelm.codes</category>
      <description>A while back, in Reading Between the Posts, I wired up the home page so the quiet gaps between articles would narrate themselves - &amp;ldquo;1 bit and 41 changelog events in between&amp;rdquo;. There was just one tiny problem with that sentence. At the time I had written exactly zero bits.&#xA;</description>
      <content:encoded><![CDATA[<p>A while back, in <a href="https://wilhelm.codes/blog/reading-between-the-posts/">Reading Between the Posts</a>, I wired up the home page so the quiet gaps between articles would narrate themselves - &ldquo;<em>1 bit and 41 changelog events in between</em>&rdquo;. There was just one tiny problem with that sentence. At the time I had written exactly <em>zero</em> bits.</p>
<p>So let me finally show you my bits.</p>
<h2 id="what-even-is-a-bit">
  <a class="heading-link" href="#what-even-is-a-bit">What even is a bit?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A bit is the smallest unit of &ldquo;I made a thing&rdquo; I&rsquo;m willing to commit to. It could be a quote that stuck with me, a link worth keeping, the odd YouTube video, or a one-line thought too small to earn its own long-form entry. Each one is a little markdown file in <code>/bits</code> and  the only thing it really has to declare is what <em>kind</em> of bit it is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">quote</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">date</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-06-17</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Edsger Dijkstra&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;Simplicity is a great virtue but it requires hard work to achieve it.&#34;</span><span class="w">
</span></span></span></code></pre></div><p><code>type</code> is the whole trick that everything else hangs off.</p>
<h2 id="one-shape-per-thought">
  <a class="heading-link" href="#one-shape-per-thought">One shape per thought.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The thing I didn&rsquo;t want was for a pithy quote to render like a bare link or to render like a video. They&rsquo;re different shapes of thought, so they ought to look different.</p>
<p>I&rsquo;d already learned this lesson building the <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">changelog</a>, which leans on one tiny partial per event type. So I stole from myself. There&rsquo;s a single lookup table mapping each <code>type</code> to an icon, an accent colour and a label:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">meta</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;quote&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;quote-right&#34;</span><span class="w">    </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;rose&#34;</span><span class="w">    </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;quote&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;link&#34;</span><span class="w">  </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;arrow-up-right&#34;</span><span class="w"> </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;blue&#34;</span><span class="w">    </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;link&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;video&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;youtube&#34;</span><span class="w">        </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;red&#34;</span><span class="w">     </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;video&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;blurb&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;icon&#34;</span><span class="w"> </span><span class="s">&#34;pen-nib&#34;</span><span class="w">        </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;emerald&#34;</span><span class="w"> </span><span class="s">&#34;label&#34;</span><span class="w"> </span><span class="s">&#34;blurb&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>It then matches a micro-partial per type that decides how the content actually renders. Adding a brand new kind of bit is now &ldquo;add one row, add one tiny template&rdquo;, which is about as many steps as I&rsquo;m willing to tolerate.</p>
<p><img src="/blog/let-me-show-you-my-bits/bits.png" alt=""></p>
<h2 id="hugo-why-are-you-like-this">
  <a class="heading-link" href="#hugo-why-are-you-like-this">Hugo, why are you like this?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>My bits started life with two perfectly reasonable front-matter fields: <code>type</code> and <code>url</code>. Hugo took one look and quietly robbed me of both.</p>
<p>It turns out <code>url</code> is a <em>reserved</em> key which Hugo uses to treat as the page&rsquo;s permalink. So, the moment I put a <code>https://...</code> in there, the build fell over with a stern little message about unsupported protocols. <code>type</code> doesn&rsquo;t even have the decency to error. It silently moves out of <code>.Params.type</code> and into <code>.Type</code>, which <em>also</em> hijacks the layout lookup. So my <code>.Params.type</code> was resolving to nothing and every single bit was quietly rendering as the fallback.</p>
<p>Turns out hte fix for this was simply to rename <code>url</code> to <code>link</code> and  read <code>.Type</code> instead of <code>.Params.type</code>. Half an hour of my life dedicated to two words I didn&rsquo;t know were already spoken for.</p>
<p>Coding is my passion.</p>
<h2 id="bits-flapping-in-the-wind">
  <a class="heading-link" href="#bits-flapping-in-the-wind">Bits flapping in the wind.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Rather than dumping bits into the same fold-out list as the GitHub noise, I pulled them out as their own first-class timeline rows. The lower-signal changelog chatter stays tucked behind the &ldquo;<em>N changelog events in between</em>&rdquo; disclosure. So, the home page now reads in three tiers:</p>
<ul>
<li>the loud headline articles</li>
<li>the medium-volume bits</li>
<li>the quiet changelog hum</li>
</ul>
<p>The same interstitial machinery I built last time, but bits just get to stand a little taller than a commit.</p>
<p><img src="/blog/let-me-show-you-my-bits/home.png" alt=""></p>
<h2 id="the-tag-that-went-nowhere">
  <a class="heading-link" href="#the-tag-that-went-nowhere">The tag that went nowhere.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Bits are deliberately <code>render: never</code> in Hugo. They live inline on the bits page and never get their own URL. Except &ldquo;no page&rdquo; also quietly means &ldquo;no place in the taxonomy&rdquo;, which means a tag only ever used by a bit has no <code>/tags/...</code> page to point at. If I link it anyway I&rsquo;ve shipped a <code>404</code>.</p>
<p>So, the tags only become links if there&rsquo;s actually somewhere to go:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">site</span><span class="p">.</span><span class="nf">GetPage</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;/tags/%s&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">urlize</span><span class="w"> </span><span class="p">.))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">&lt;</span><span class="nx">a</span><span class="w"> </span><span class="nx">href</span><span class="p">=</span><span class="s">&#34;{{ .RelPermalink }}&#34;</span><span class="p">&gt;</span><span class="err">#</span><span class="p">{{</span><span class="w"> </span><span class="p">.</span><span class="nx">Data</span><span class="p">.</span><span class="nx">Term</span><span class="w"> </span><span class="p">}}&lt;</span><span class="o">/</span><span class="nx">a</span><span class="p">&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">&lt;</span><span class="nx">span</span><span class="p">&gt;</span><span class="err">#</span><span class="p">{{</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}&lt;</span><span class="o">/</span><span class="nx">span</span><span class="p">&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>A tag with a real page becomes a link and a bit-only tag stays as plain text. No broken links and the nice part is it heals itself the moment a proper article picks up the same tag.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The front page has quietly turned into a proper little stream. Long-form when I&rsquo;ve actually got something to say, bits when I don&rsquo;t and  a <a href="https://wilhelm.codes/blog/a-year-in-circles/">year of squircles</a> up top keeping a tally of it all.</p>
<p>Which is the entire point. The less effort it takes to post a half-formed thought, the more likely I am to actually post it. And the more I post, the more those gaps fill in.</p>
<p>And, as always, you can check everything out for yourself in the <a href="https://github.com/wilhelm-murdoch/wilhelm.codes">repo</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Reading Between the Posts</title>
      <link>https://wilhelm.codes/blog/reading-between-the-posts/</link>
      <pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/reading-between-the-posts/</guid>
      <category>hugo</category>
      <category>css</category>
      <description>The front page of this site has always been a plain list of articles. Which would be fine if I wrote longer-form articles a bit more often. I do, however, make code commits to this site far more frequently. I&amp;rsquo;m also working on implementing a short-form section ( a.k.a. &amp;ldquo;bits&amp;rdquo; ) for links, videos and other small low-stakes thoughts. So the front page doesn&amp;rsquo;t really show the full story. In reality, there&amp;rsquo;s a lot more going on behind the scenes.&#xA;</description>
      <content:encoded><![CDATA[<p>The front page of this site has always been a plain list of articles. Which would be fine if I wrote longer-form articles a bit more often. I do, however, make code commits to this site far more frequently. I&rsquo;m also working on implementing a short-form section ( a.k.a. &ldquo;bits&rdquo; ) for links, videos and other small low-stakes thoughts. So the front page doesn&rsquo;t really show the full story. In reality, there&rsquo;s a lot more going on behind the scenes.</p>
<p>I want to fill those gaps in and not with more full-fat rows, but with a small line between each pair of articles. Something like &ldquo;<em>1 bit and 41 changelog events in between</em>&rdquo;; a kind of interstitial event timeline for all the work that never quite became a full blog post.</p>
<h2 id="why-cant-i-hold-all-these-events">
  <a class="heading-link" href="#why-cant-i-hold-all-these-events">Why can&rsquo;t I hold all these events?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The trick to merging things that don&rsquo;t look alike&hellip; You&rsquo;re never going to believe this&hellip; is to make them look alike. I already pull my GitHub activity for the <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">changelog</a>, or, at least, the last 100 or so events. So, I reused that, threw the bits in alongside it, and normalised the whole lot into one boring, uniform shape. This data structure doesn&rsquo;t hold all the data for each event, but just enough to cover the highlights.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;kind&#34;</span><span class="w">  </span><span class="s">&#34;commit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;date&#34;</span><span class="w">  </span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">AsTime</span><span class="w"> </span><span class="p">.</span><span class="nx">commit</span><span class="p">.</span><span class="nx">author</span><span class="p">.</span><span class="nx">date</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;title&#34;</span><span class="w"> </span><span class="p">(</span><span class="nf">index</span><span class="w"> </span><span class="p">(</span><span class="nx">split</span><span class="w"> </span><span class="p">.</span><span class="nx">commit</span><span class="p">.</span><span class="nx">message</span><span class="w"> </span><span class="s">&#34;\n&#34;</span><span class="p">)</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;url&#34;</span><span class="w">   </span><span class="p">.</span><span class="nx">html_url</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;icon&#34;</span><span class="w">  </span><span class="s">&#34;code-commit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;colour&#34;</span><span class="w"> </span><span class="s">&#34;green&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="s">&#34;data&#34;</span><span class="w">  </span><span class="p">.)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>Every piece of content on this site has <em>at the very last</em> these attributes. So created a unifying model is pretty straight-forward. A <code>kind</code>, a <code>date</code>, a <code>title</code>, a link, and enough leftover metadata to do something creative with later if I feel like it. I made a point of keeping the original object tucked away in <code>data</code> too; a gift to future Wilhelm.</p>
<h2 id="minding-the-gaps">
  <a class="heading-link" href="#minding-the-gaps">Minding the gaps.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With one big sorted pile of events, the rest is just bookkeeping. Walk the articles newest to oldest, and for each one work out the window between it and the article above it. Then, scoop up every event that falls within the relevant dates:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">upper</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="err">$</span><span class="nx">now</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nx">gt</span><span class="w"> </span><span class="err">$</span><span class="nx">i</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}}{{</span><span class="w"> </span><span class="err">$</span><span class="nx">upper</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">(</span><span class="nx">index</span><span class="w"> </span><span class="err">$</span><span class="nf">articles</span><span class="w"> </span><span class="p">(</span><span class="nx">sub</span><span class="w"> </span><span class="err">$</span><span class="nx">i</span><span class="w"> </span><span class="mi">1</span><span class="p">)).</span><span class="nx">Date</span><span class="w"> </span><span class="p">}}{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="err">$</span><span class="nx">e</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">and</span><span class="w"> </span><span class="p">(</span><span class="err">$</span><span class="nx">e</span><span class="p">.</span><span class="nx">date</span><span class="p">.</span><span class="nx">After</span><span class="w"> </span><span class="err">$</span><span class="nx">article</span><span class="p">.</span><span class="nx">Date</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nf">not</span><span class="w"> </span><span class="p">(</span><span class="err">$</span><span class="nx">e</span><span class="p">.</span><span class="nx">date</span><span class="p">.</span><span class="nx">After</span><span class="w"> </span><span class="err">$</span><span class="nx">upper</span><span class="p">))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">group</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">group</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">append</span><span class="w"> </span><span class="err">$</span><span class="nx">e</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>The newest article&rsquo;s &ldquo;window&rdquo; runs all the way up to <em>now</em>, so whatever I&rsquo;ve been doing since the last post shows up at the very top. If there&rsquo;s an empty gap the event list doesn&rsquo;t render.</p>
<p><img src="/blog/reading-between-the-posts/interstitial-events-top.png" alt=""></p>
<h2 id="listing-the-interstitial-events">
  <a class="heading-link" href="#listing-the-interstitial-events">Listing the interstitial events.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A summary is nice, but I wanted to be able to actually <em>see</em> the events without leaving the page. Normally that&rsquo;s where a pile of JS shows up. But, just as with the activity graph, we&rsquo;re going to rely on pure HTML and CSS. The <code>&lt;details&gt;</code> element does the entire job for free:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">details</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;group&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">summary</span><span class="p">&gt;</span>... 41 changelog events in between ...<span class="p">&lt;/</span><span class="nt">summary</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="c">&lt;!-- one little row per event, complete with icon and link --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">details</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Click the summary, the gap unfolds into a tidy little list of every commit, PR and bit. Each linked off to wherever it lives. The default disclosure triangle gets binned and replaced with a chevron that flips on open, courtesy of Tailwind&rsquo;s <code>group-open:</code> variant. Still no JS and I&rsquo;ll remain quietly smug about that.</p>
<p><img src="/blog/reading-between-the-posts/interstitial-events-timeline.png" alt=""></p>
<h2 id="saving-myself-a-small-headache">
  <a class="heading-link" href="#saving-myself-a-small-headache">Saving myself a small headache.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Finally, there&rsquo;s the the responsive question. This site&rsquo;s overall design isn&rsquo;t exactly &ldquo;mobile first&rdquo; and viewing it on a small screen strips away a lot of detail already. All these thin dividers and fold-out lists look great on a wide screen and turn into a cramped mess on a phone.</p>
<p>I thought about it for a while and weighed up some clever reflowing. In the end I did the only sensible thing and hid the whole timeline below the <code>sm</code> breakpoint. On a phone you get the clean list of articles, but on a desktop you get the full story between them.</p>
<blockquote class="pull-quote">
  Sometimes the right amount of effort is zero and the discipline is in admitting it.
  <cite>Me. Wilhelm. I said that.</cite>
</blockquote>

<p>That being said, I do need to return and focus on making the mobile experience a bit more palatable.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>What I like about this is that it turns my worst blogging habit - vanishing for months and then resurfacing in a manic flurry - into something the site can actually narrate. The big rows are the headlines while the quiet lines between them are everything else.</p>
<p>It&rsquo;s also wired up so I can grow it later without unpicking anything. Right now it&rsquo;s counts and lists, but tomorrow it could filters, more details, or do something I haven&rsquo;t thought of yet. All off the same pile of events.</p>
<p>For now, though, the gaps have something interesting in them. Which is a much nicer thing to scroll past than a year of silence.</p>]]></content:encoded>
    </item>
    <item>
      <title>A Year in Circles... I mean Squares. Or, is it Squircles?</title>
      <link>https://wilhelm.codes/blog/a-year-in-circles/</link>
      <pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/a-year-in-circles/</guid>
      <category>hugo</category>
      <category>css</category>
      <category>tailwindcss</category>
      <description>If you scroll up to the top of the home page, you&amp;rsquo;ll find a pair of little rows of green squircles. It&amp;rsquo;s my own dumbed-down take on GitHub&amp;rsquo;s contribution graph. Effectively, it&amp;rsquo;s the same idea, except instead of a year of days it&amp;rsquo;s a year of weeks. Two rows of twenty-six with one squircle per week, with each squircle coloured a deeper shade of green the busier that week was.&#xA;</description>
      <content:encoded><![CDATA[<p>If you scroll up to the top of the home page, you&rsquo;ll find a pair of little rows of green squircles. It&rsquo;s my own dumbed-down take on GitHub&rsquo;s contribution graph. Effectively, it&rsquo;s the same idea, except instead of a year of days it&rsquo;s a year of <em>weeks</em>. Two rows of twenty-six with one squircle per week, with each squircle coloured a deeper shade of green the busier that week was.</p>
<p>I wanted something that summed up &ldquo;Has Wilhelm actually been doing anything lately?&rdquo; at a glance, without the density of 365 tiny day cells. Fifty-two squircles felt about right. Coarse enough to read across the room, fine enough to still give a picture.</p>
<h2 id="what-counts-as-a-weeks-worth-of-activity">
  <a class="heading-link" href="#what-counts-as-a-weeks-worth-of-activity">What counts as a &ldquo;week&rsquo;s worth of activity&rdquo;?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The short answer is <em>everything</em>. The <a href="https://wilhelm.codes/blog/a-changelog-that-builds-itself/">changelog</a> already pulls my GitHub activity at build time, so I reuse that exact same data. Then, I throw in the things GitHub doesn&rsquo;t know about; every type of blog post on this site.</p>
<p>Because the changelog fetching already lives in a tidy little partial, sourcing the everything is just a matter of asking for each one and collecting all the publish dates:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">slice</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;changelog/fetch.html&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;url&#34;</span><span class="w"> </span><span class="err">$</span><span class="nx">commitsUrl</span><span class="w"> </span><span class="s">&#34;fixture&#34;</span><span class="w"> </span><span class="err">$</span><span class="nx">fixture</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nf">append</span><span class="w"> </span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">AsTime</span><span class="w"> </span><span class="p">.</span><span class="nx">commit</span><span class="p">.</span><span class="nx">author</span><span class="p">.</span><span class="nx">date</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">where</span><span class="w"> </span><span class="nx">site</span><span class="p">.</span><span class="nx">RegularPages</span><span class="w"> </span><span class="s">&#34;Section&#34;</span><span class="w"> </span><span class="s">&#34;blog&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="err">$</span><span class="nx">dates</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">append</span><span class="w"> </span><span class="p">.</span><span class="nx">Date</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>Nothing crazy going on here. I&rsquo;m just creating a big list of timestamps from wherever I happen to leave a trail.</p>
<h2 id="sorting-a-pile-of-dates-into-weekly-buckets">
  <a class="heading-link" href="#sorting-a-pile-of-dates-into-weekly-buckets">Sorting a pile of dates into weekly buckets.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Each of these dates now need to land in one of fifty-two buckets. Working out which week a given date belongs to is just some boring epoch arithmetic; how many whole weeks ago was it?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="p">(</span><span class="nf">div</span><span class="w"> </span><span class="p">(</span><span class="nx">sub</span><span class="w"> </span><span class="nx">now</span><span class="p">.</span><span class="nx">Unix</span><span class="w"> </span><span class="err">$</span><span class="nx">d</span><span class="p">.</span><span class="nx">Unix</span><span class="p">)</span><span class="w"> </span><span class="mi">604800</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">and</span><span class="w"> </span><span class="p">(</span><span class="nx">ge</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">(</span><span class="nx">lt</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="w"> </span><span class="mi">52</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">counts</span><span class="p">.</span><span class="nf">Add</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;%d&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">sub</span><span class="w"> </span><span class="mi">51</span><span class="w"> </span><span class="err">$</span><span class="nx">ago</span><span class="p">))</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>That <code>604800</code> is the number of seconds in a week. And yes, I did have to look that up. Anything older than fifty-two weeks just falls off the back and is quietly ignored.</p>
<h2 id="painting-the-squircles">
  <a class="heading-link" href="#painting-the-squircles">Painting the squircles.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>My favourite part of this is there&rsquo;s no JS involved. The whole thing is a CSS grid of twenty-six columns, and because each squircle is <code>aspect-square</code>, the rows just work and the squircles stretch to fill whatever container I drop the partial into:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;grid grid-cols-26 gap-1.5&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="c">&lt;!-- 52 of these green little dudes --&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;aspect-square rounded-full bg-emerald-400&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Working out the shade was the fiddly bit. My first attempt simply scaled each week against the busiest one, which sounds sensible right up until you remember my data is basically one enormous week and a whole lot of flat nothing. That single monster week hogged the dark end and squashed everything else into the same pale green. So I did what GitHub does and reached for <a href="https://en.wikipedia.org/wiki/Quartile">quartiles</a> instead. You rank the weeks that actually saw some activity, chop them into four groups, and let a week&rsquo;s colour come from where it lands in the pack rather than from some absolute number. Empty weeks stay a faint <code>emerald-100</code>; the rest climb through four steps of green up to <code>emerald-600</code>. The part I like is that it&rsquo;s relative to <em>me</em>; a busy week is only busy compared to my <em>other</em> weeks.</p>
<p>The little tooltip that pops up when you hover over a squircle is also pure CSS. A <code>group</code> on the wrapper, a <code>group-hover:opacity-100</code> on the popup, a <code>transition</code> is all I needed. Each squircle now gives a little <code>scale</code> on hover too, just because it&rsquo;s nice.</p>
<h2 id="the-graphs-painful-honesty">
  <a class="heading-link" href="#the-graphs-painful-honesty">The graph&rsquo;s painful honesty.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Now on to the more embarrassing part. When I first rendered it with real data, I got <em>one</em> lonely green squircle and fifty-one empty ones. Surely, you&rsquo;ve noticed it on the front page.</p>
<p>For the curious, this is what it <em>would</em> look like if I didn&rsquo;t have commitment issues.
<img src="/blog/a-year-in-circles/activity-graph-demo.png" alt=""></p>
<p>At the time I assumed I&rsquo;d done something wrong. But, to my great shame, I hadn&rsquo;t. Turns out that when you vanish from your own website for close to 18 months and then cram an <a href="https://wilhelm.codes/blog/some-long-overdue-housekeeping/">entire renovation</a> into a single week, the graph renders exactly that. A long, flat, pale stretch of road ending with one pathetic little green emerald.</p>
<p>There&rsquo;s an extra little indignity baked into the quartiles, too. They need a <em>spread</em> to rank against, and with exactly one active week there&rsquo;s nothing to compare it to. So my massive renovation-cramming week doesn&rsquo;t even get to be properly dark green. It turns up as a polite, middling shade.</p>
<p>I effectively built a little a tool whose entire job is to hold up a mirror to my own lack of consistency and commitment. Very cool! 😬👌</p>
<h2 id="drop-it-in-anywhere">
  <a class="heading-link" href="#drop-it-in-anywhere">Drop it in anywhere.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This whole thing is a single self-contained partial. It was waaaay easier to build out than I had originally thought. And, thanks to Tailwind I didn&rsquo;t even have to fall back on any JS! I count that as a bonus.</p>
<p>For the moment, it&rsquo;ll live on the home page, but I can now just place this anywhere in my Hugo site and it&rsquo;ll work:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">partial</span><span class="w"> </span><span class="s">&#34;activity-graph.html&#34;</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>I might even build it out a bit more to support different colour schemes or specificy types of targeted site content. So, the more I write, the more those squircles fill in.</p>
<p>Consider yourself warned, <em>me</em>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Some Long Overdue Housekeeping</title>
      <link>https://wilhelm.codes/blog/some-long-overdue-housekeeping/</link>
      <pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/some-long-overdue-housekeeping/</guid>
      <category>hugo</category>
      <category>tailwindcss</category>
      <category>css</category>
      <category>refactoring</category>
      <category>wilhelm.codes</category>
      <description>This blog has been quietly chugging along for a while now without me paying it much attention. Which is, I suppose, the whole point of a set-and-forget setup. But &amp;ldquo;set-and-forget&amp;rdquo; has a sneaky way of becoming &amp;ldquo;forgotten&amp;rdquo;, and the longer you leave something untouched, the more it quietly rots behind your back. So I rolled up my sleeves and gave the whole thing a proper tune-up.&#xA;</description>
      <content:encoded><![CDATA[<p>This blog has been quietly chugging along for a while now without me paying it much attention. Which is, I suppose, the whole point of a <a href="https://wilhelm.codes/blog/my-blog-publishing-setup/">set-and-forget</a> setup. But &ldquo;set-and-forget&rdquo; has a sneaky way of becoming &ldquo;forgotten&rdquo;, and the longer you leave something untouched, the more it quietly rots behind your back. So I rolled up my sleeves and gave the whole thing a proper tune-up.</p>
<h2 id="confession-time">
  <a class="heading-link" href="#confession-time">Confession Time<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>While poking around the build, I discovered something a little embarrassing. My Tailwind setup - the source, the config, and the <em>entire</em> <code>node_modules</code> directory - was living inside Hugo&rsquo;s <code>static/</code> folder.</p>
<p>If you know Hugo, you already know where this is going. Everything in <code>static/</code> gets copied, verbatim, into the final site. Which means I had been cheerfully publishing my whole build toolchain - megabytes of it - to the live site on every single deploy. So, yeah. Production was shipping <code>node_modules</code>. Coding is my passion.</p>
<p>Nobody noticed, nothing broke, and the world kept turning. But it&rsquo;s the kind of thing that, once you see it, you can&rsquo;t <em>un</em>-see. To be fair to myself, when I originally put this Hugo site together, I only learned enough to get something shipped.</p>
<h2 id="letting-hugo-do-the-heavy-lifting">
  <a class="heading-link" href="#letting-hugo-do-the-heavy-lifting">Letting Hugo do the heavy lifting.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The reason that mess existed in the first place was that I&rsquo;d wired up Tailwind as a separate, manual build step that spat out a compiled stylesheet for Hugo to pick up. It worked, but it was a second moving part I had to remember existed.</p>
<p>The good news is that recent versions of Hugo can drive <a href="https://gohugo.io/functions/css/tailwindcss/">Tailwind</a> itself, natively, as part of the normal asset pipeline. Combined with Tailwind v4 - which finally ditches the JavaScript config file in favour of configuring everything in CSS - I got to delete a <em>lot</em> of stuff:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ git diff --shortstat main...modernize
</span></span><span class="line"><span class="cl"><span class="m">64</span> files changed, <span class="m">1872</span> insertions<span class="o">(</span>+<span class="o">)</span>, <span class="m">40197</span> deletions<span class="o">(</span>-<span class="o">)</span>
</span></span></code></pre></div><p>The whole stylesheet now starts its life as a single entry point:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">@</span><span class="k">import</span> <span class="s2">&#34;tailwindcss&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">plugin</span> <span class="s2">&#34;@tailwindcss/typography&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">@</span><span class="k">source</span> <span class="s2">&#34;hugo_stats.json&#34;</span><span class="p">;</span>
</span></span></code></pre></div><p>That <code>hugo_stats.json</code> bit is the clever part. Hugo writes out a list of every utility class it actually emits, and Tailwind reads <em>that</em> to decide what to generate. No more pointing Tailwind at my templates and hoping it guesses right.</p>
<p>Then a small partial hands it all off to Hugo to compile, minify and fingerprint:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nf">with</span><span class="w"> </span><span class="p">(</span><span class="nx">templates</span><span class="p">.</span><span class="nf">Defer</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;key&#34;</span><span class="w"> </span><span class="s">&#34;css&#34;</span><span class="p">))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">resources</span><span class="p">.</span><span class="nx">Get</span><span class="w"> </span><span class="s">&#34;css/main.css&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;minify&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">not</span><span class="w"> </span><span class="nx">hugo</span><span class="p">.</span><span class="nx">IsDevelopment</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">css</span><span class="p">.</span><span class="nx">TailwindCSS</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="cm">/* ...do very cool things... */</span><span class="w"> </span><span class="o">-</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>The <code>templates.Defer</code> wrapper is there because the CSS can&rsquo;t compiled ( transpiled? ) until Hugo has finished rendering every page and knows the full list of classes. So, in a very real way, Hugo solves an annoying 🐔 and 🥚 problem.</p>
<p>This means no more standalone Tailwind config, no committed stylesheet, no <code>node_modules</code> in <code>static/</code>, and no build toolchain leaking onto the live site. Very cool!</p>
<h2 id="so-many-deprecations">
  <a class="heading-link" href="#so-many-deprecations">So. Many. Deprecations.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Of course, nothing that&rsquo;s been left alone for a year comes back to life cleanly. Bumping Hugo to the latest release lit up the console like a Christmas tree.</p>
<p>A few of my templates were leaning on things that have since been politely shown the door:</p>
<ul>
<li><code>resources.GetRemote ... .Err</code> for the <a href="https://wilhelm.codes/changelog/">changelog page</a> - that pattern was removed in favour of a shiny new <code>try</code> keyword.</li>
<li><code>.Language.LanguageCode</code> and <code>.Language.LanguageDirection</code>, both deprecated in favour of <code>.Locale</code> and <code>.Direction</code>.</li>
<li>The <code>_build</code> and <code>cascade._target</code> front matter keys, now just <code>build</code> and <code>cascade.target</code>.</li>
</ul>
<p>None of it was hard to fix, but it&rsquo;s a good reminder that &ldquo;it still builds&rdquo; and &ldquo;it builds <em>without complaints</em>&rdquo; are two very different bars.</p>
<h2 id="a-footgun">
  <a class="heading-link" href="#a-footgun">A Footgun!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Here&rsquo;s a fun one. The changelog page pulls in a Github event fixture file during local development by fetching it over <code>http://localhost:1313</code>. Effectively, from the very dev server that&rsquo;s <em>trying to build the page</em>.</p>
<p>Those of you who are familiar with such things can probably see the problem. Hugo builds the site <em>before</em> it starts listening on that port, so the build sits there waiting for a server that doesn&rsquo;t exist yet. A deadlock of my own making. The obvious fix was to just read the file off disk instead of asking the network nicely.</p>
<h2 id="some-honourable-mentions">
  <a class="heading-link" href="#some-honourable-mentions">Some honourable mentions.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>A grab-bag of smaller wins while I had the hood up:</p>
<ul>
<li>Fonts are now served as <code>woff2</code> instead of raw <code>ttf</code>, with <code>font-display: swap</code> so text shows up immediately instead of hanging around invisible. This alone shaved the font payload down by about 60%.</li>
<li>I also deleted a few MB of fluff that wasn&rsquo;t being loaded by anything.</li>
<li>Prettier got a nice version bump and I taught it to sort my Tailwind classes, so I can stop pretending I do that consistently by hand.</li>
</ul>
<h2 id="was-the-juice-worth-the-squeeze">
  <a class="heading-link" href="#was-the-juice-worth-the-squeeze">Was the juice worth the squeeze?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Aside from a few cosmetic updates here and there, the site should look pretty much the same as it did before. Which is, weirdly, the whole point; all of this work was about the parts you can&rsquo;t see. I got a leaner build, faster page loads, and a project I can come back to in another year without wincing; famous last words, etc&hellip;</p>
<p>For now, the house is clean. Very nice.</p>]]></content:encoded>
    </item>
    <item>
      <title>A Changelog That Builds Itself</title>
      <link>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</link>
      <pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</guid>
      <category>hugo</category>
      <category>github</category>
      <category>api</category>
      <description>This site has a changelog page. It&amp;rsquo;s not one I write by hand as it builds itself from my activity on GitHub every time the site deploys. I think it&amp;rsquo;s a neat little trick. It also spent a solid afternoon teaching me that &amp;ldquo;works on my machine&amp;rdquo; and &amp;ldquo;works in production&amp;rdquo; are, once again, two very different things.&#xA;</description>
      <content:encoded><![CDATA[<p>This site has a <a href="https://wilhelm.codes/changelog/">changelog</a> page. It&rsquo;s not one I write by hand as it builds itself from my activity on GitHub every time the site deploys. I think it&rsquo;s a neat little trick. It also spent a solid afternoon teaching me that &ldquo;works on my machine&rdquo; and &ldquo;works in production&rdquo; are, once again, two very different things.</p>
<p>If you use Github, you know it quietly records nearly everything you do as a stream of <a href="https://docs.github.com/en/rest/activity/events">events</a>. Better still, for a public repository, that stream is available over a public, unauthenticated API endpoint through a simple <code>GET</code> request.</p>
<p>So the plan more or less writes itself. We fetch the stream at build time, group the events by day, and let Hugo render them. Hugo even has a tidy <a href="https://gohugo.io/functions/resources/getremote/">little function</a> for pulling in remote resources that works like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;https://api.github.com/repos/wilhelm-murdoch/wilhelm.codes/events&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">.</span><span class="nx">Value</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="cm">/* ...range over them and render... */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>Because this all happens at <em>build</em> time, the visitor never waits on GitHub. By the time the page reaches a browser it&rsquo;s just static HTML like everything else. The changelog is always current as of the last deploy, and I never have to think about it.</p>
<p>That&rsquo;s the part that worked. Now for the parts that didn&rsquo;t.</p>
<h2 id="-then-production-happened">
  <a class="heading-link" href="#-then-production-happened">&hellip; Then, production happened.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I&rsquo;ve been more or less absent from this site for close to a year, but this past week I&rsquo;ve decided to breathe a bit more life into it. I made a few changes to <a href="https://wilhelm.codes/blog/some-long-overdue-housekeeping/">modernise</a> the stack, pushed my changes up and waited on Cloudflare to do its thing and&hellip;</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ERROR error building site: ... error calling GetRemote:
</span></span><span class="line"><span class="cl">failed to fetch remote resource from &#39;https://api.github.com/...&#39;: Forbidden
</span></span></code></pre></div><p>I was greeted with a <code>403</code>. Except&hellip; I could paste that exact URL into my browser and get a perfectly happy wall of JSON back. So what gives?</p>
<p>Rate limiting, that&rsquo;s what. GitHub&rsquo;s unauthenticated API is capped at <strong>60 requests per hour, per IP address</strong>. My build doesn&rsquo;t run on <em>my</em> IP. It runs on a shared build machine alongside who-knows-how-many other people&rsquo;s deploys, all of them hammering GitHub from the same handful of addresses. By the time my build rolled around, that bucket was bone dry.</p>
<p>Honestly, a <code>429</code> instead here would have saved me a bit of investigating, but I digress.</p>
<p>The solution was to created a properly scoped Github PAT. Using an authenticated request gets <strong>5000 requests per hour</strong>, so I went about implementing support and handed it to the build as an environment variable, and taught the template to send it along:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="w"> </span><span class="s">&#34;HUGO_GITHUB_TOKEN&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;headers&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;Authorization&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;Bearer %s&#34;</span><span class="w"> </span><span class="p">.))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>It&rsquo;s worth noting that Hugo won&rsquo;t read just <em>any</em> environment variable. Its security policy only lets <code>os.Getenv</code> see variables prefixed with <code>HUGO_</code>. Name your token <code>GITHUB_TOKEN</code> and you&rsquo;ll get a confusing fistful of nothing; name it <code>HUGO_GITHUB_TOKEN</code> and it works. Ask me how I know.</p>
<h2 id="hark-another-footgun">
  <a class="heading-link" href="#hark-another-footgun">Hark! Another Footgun!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With the token in place, the fetch succeeded and the build promptly fell over somewhere new:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">error calling len: reflect: call of reflect.Value.Type on zero Value
</span></span></code></pre></div><p>This never showed up locally, which tells me I should do myself a favour and do a quick local <em>production</em> deployment as a preflight <em>before</em> I push to Github and trigger an automated build. Originally, in development I rendered the changelog from a single large fixture file stored locally on disk. This was before I implemented the PAT so I wouldn&rsquo;t rate limit myself locally when testing. The live stream, however, contains event shapes my fixture simply didn&rsquo;t. One of them was a <code>push</code> event carrying no <code>commits</code> at all, and my template did this without a second thought:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">eq</span><span class="w"> </span><span class="p">(</span><span class="nx">len</span><span class="w"> </span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="p">)</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>Call <code>len</code> on something that exists and isn&rsquo;t there and Go has a small panic about it. The fix is boring; stop assuming the field is there. Hugo&rsquo;s <code>with</code> only enters the block when its argument is actually&hellip; something:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">eq</span><span class="w"> </span><span class="p">(</span><span class="nx">len</span><span class="w"> </span><span class="p">.)</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="cm">/* the one-commit case */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="cm">/* the many-commits case */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>Since I&rsquo;ve implemented PAT support, I&rsquo;m not so concerned about being rate-limited during local development, though I now use the fixtures as a fallback if any request fails. This allows me to keep working locally without disruption.</p>
<h2 id="dont-let-someone-elses-server-break-your-build">
  <a class="heading-link" href="#dont-let-someone-elses-server-break-your-build">Don&rsquo;t let someone else&rsquo;s server break your build.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The real personal lesson is the moment your build depends on a third party at build time, you&rsquo;ve effectively ceded control of your deployment to them. GitHub rate-limits you, or has a wobble, or changes a payload shape, and suddenly your perfectly good site won&rsquo;t deploy and you&rsquo;ll have a hard time.</p>
<p>So the changelog no longer treats GitHub as load-bearing. If the fetch fails for <em>any</em> reason it shrugs, logs a warning, and falls back to that saved fixture instead of taking the whole deploy down with it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="p">.</span><span class="nx">Err</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">warnf</span><span class="w"> </span><span class="s">&#34;changelog: GitHub fetch failed (%s); using the fixture&#34;</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">response</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">ReadFile</span><span class="w"> </span><span class="s">&#34;static/github.json&#34;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="p">.</span><span class="nx">Value</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">response</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span></code></pre></div><p>The <code>try</code> keyword is the hero here as it catches the error that <code>GetRemote</code> would otherwise throw and hands it back to me as a value I can actually <em>do</em> something with, rather than a smoking crater where my build used to be. Worst case, the page shows slightly stale history. That&rsquo;s a trade I&rsquo;ll take every single time over a red deploy or disruption to my local flow.</p>
<h2 id="in-conclusion">
  <a class="heading-link" href="#in-conclusion">In conclusion&hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Ironically, this is something I&rsquo;ve dealt with quite frequently at work. Github &amp; AWS API rate limits ( yes, we frequently got rate limited by the VPC API of all things ), <a href="https://yarnpkg.com/">Yarn</a> will throw a <code>5xx</code> and even Sentry will chimp out during source map uploads. All of these things can disrupt deployments.</p>
<p>The point is just assume these things will happen and then build defensively around them. I, ehhhh, just need to apply this wisdom to my personal stuff. 😅</p>
<p>Anyway, the changelog still builds itself. It just doesn&rsquo;t get to take the rest of the site down with it anymore.</p>]]></content:encoded>
    </item>
    <item>
      <title>Printing Ordinal Numbers in Hugo</title>
      <link>https://wilhelm.codes/blog/printing-ordinal-numbers-in-hugo/</link>
      <pubDate>Fri, 17 Jan 2025 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/printing-ordinal-numbers-in-hugo/</guid>
      <category>hugo</category>
      <category>snippets</category>
      <description>I&amp;rsquo;ve been having such a good time building up this website and Hugo has been incredibly fun – and relatively simple – to work with. Though, from time to time, I find myself scratching my head at the absence of a few bits and bobs.&#xA;</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve been having such a good time building up this website and <a href="https://gohugo.io">Hugo</a> has been incredibly fun – and relatively simple – to work with. Though, from time to time, I find myself scratching my head at the absence of a few bits and bobs.</p>
<p>In this case, what I really needed was a simple way to assign an ordinal to an arbitrary number. For instance, if I have a value of <code>2</code>, I might want to tack on a <code>nd</code> as a suffix, eg; <code>1st</code>, <code>540th</code> or  <code>9001st</code> and so on.</p>
<p>Specifically, I&rsquo;d like to use this with dates, but from what I can tell, Hugo doesn&rsquo;t support this out-of-the-box. Luckily, the framework gives you a few options to extend its functionality.</p>
<h2 id="shortcodes">
  <a class="heading-link" href="#shortcodes">Shortcodes<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With <a href="https://gohugo.io/content-management/shortcodes/">shortcodes</a> you can create snippets that act as functions which can be used to dynamically inject HTML – among other things – directly into your rendered markdown. You can even pass both named and positional arguments through the shortcode to modify their behaviour as needed.</p>
<p>For instance, I have <a href="https://github.com/wilhelm-murdoch/wilhelm.codes/blob/main/layouts/shortcodes/blockquote.html">this custom</a> shortcode used for displaying styled block quotes like:</p>
<blockquote class="pull-quote">
  Out of all the things I have lost, I miss my mind the most.
  <cite>Mark Twain</cite>
</blockquote>

<p>I also have <a href="https://github.com/wilhelm-murdoch/wilhelm.codes/blob/main/layouts/shortcodes/callouts.html">this one</a> that let&rsquo;s me print out some fancy callouts:</p>
<div class="callout callout-warning">
  You should probably pay attention.
</div>

<div class="callout callout-info">
  This may be of some small interest.
</div>

<div class="callout callout-error">
  Yeah, so, we&rsquo;re all gonna die.
</div>

<p>Hugo ships with a <a href="https://gohugo.io/content-management/shortcodes/#embedded-shortcodes">default set</a> of shortcodes covering a variety of additional kinds of embeddings.</p>
<p>Unfortunately, for my purposes, I&rsquo;m not planning on using dynamic numeric ordinals in my markdown files. I need this to work with templates and for that, we use&hellip;</p>
<h2 id="partials">
  <a class="heading-link" href="#partials">Partials<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<div class="callout callout-notice">
  &hellip; that while you cannot use partials directly within your markdown files, you <em>can</em> reference them <em>indirectly</em> by embedding them within a shortcode .
</div>

<p>Unlike shortcodes, <a href="">partials</a> are small re-usable HTML components that are typically used to keep code duplication down. They are effectively context-aware templates that can accept arbitrary data which can be used in generating desired output.</p>
<p><a href="https://github.com/wilhelm-murdoch/wilhelm.codes/blob/main/layouts/partials/views/small.html">Here</a> is a small example of how I use partials for this blog. It&rsquo;s a small data card component you might find scattered throughout the site. Partials let you quickly change a UI component in one place while having it propagate everywhere else.</p>
<p>For the purpose of this article, they can also be used to create custom template &ldquo;functions&rdquo; like this which solves my very specific problem:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{- if and ( eq ( mod . 10 ) 1 ) ( ne ( mod . 100 ) 11 ) -}}
</span></span><span class="line"><span class="cl">    st
</span></span><span class="line"><span class="cl">{{ else if and ( eq ( mod . 10 ) 2 ) ( ne ( mod . 100 ) 12 ) -}}
</span></span><span class="line"><span class="cl">    nd
</span></span><span class="line"><span class="cl">{{ else if and ( eq ( mod . 10 ) 3 ) ( ne ( mod . 100 ) 13 ) -}}
</span></span><span class="line"><span class="cl">    rd
</span></span><span class="line"><span class="cl">{{ else -}}
</span></span><span class="line"><span class="cl">    th
</span></span><span class="line"><span class="cl">{{ end -}}
</span></span></code></pre></div><p>This may look a bit unreadable to people who aren&rsquo;t super-familiar with Hugo&rsquo;s template syntax, but it effectively resolves to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">number</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;th&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nx">number</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="nx">number</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">!=</span> <span class="mi">11</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;st&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">number</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">2</span> <span class="o">&amp;&amp;</span> <span class="nx">number</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">!=</span> <span class="mi">12</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;nd&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">number</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">3</span> <span class="o">&amp;&amp;</span> <span class="nx">number</span> <span class="o">%</span> <span class="mi">100</span> <span class="o">!=</span> <span class="mi">13</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nx">ordinal</span> <span class="o">=</span> <span class="s2">&#34;rd&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">number</span> <span class="o">+</span> <span class="nx">ordinal</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// prints 10th
</span></span></span></code></pre></div><p>Anyways, I have this saved as <code>partials/functions/ordinal.html</code> and can reference this in any of my templates like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ partial &#34;functions/ordinal.html&#34; $number }}
</span></span></code></pre></div><p>Where <code>$number</code> is any arbitrary number I&rsquo;d like an ordinal suffix for. Take the following example template which generates a random range of numbers and prints them out using my ordinal partial:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">{{ range seq 10 }}
</span></span><span class="line"><span class="cl">  {{.}}{{ partial &#34;functions/ordinal.html&#34; . }} 
</span></span><span class="line"><span class="cl">{{ end }}
</span></span></code></pre></div><p>Which generates:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">1st 2nd 3rd 4th 5th 6th 7th 8th 9th 10th
</span></span></code></pre></div><p>Ground-breaking work! I can finally use ordinals in dates. Not exactly something I expected to write an article about, but here we are. 🤷</p>]]></content:encoded>
    </item>
    <item>
      <title>My Blog Publishing Setup &amp; Workflow</title>
      <link>https://wilhelm.codes/blog/my-blog-publishing-setup/</link>
      <pubDate>Sat, 11 Jan 2025 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/my-blog-publishing-setup/</guid>
      <category>obsidian</category>
      <category>hugo</category>
      <category>bash</category>
      <category>cloudflare</category>
      <description>Late last year I parted ways with Hashnode as the platform of choice for my blog. They pivoted a bit too far into the generative AI space which — to me for a blogging platform — made little to no sense. What? You write your content with AI, which then trains their model only to spit out more AI-generated slop? A slop-based Ouroboros, if you will. Good luck with that, but I&amp;rsquo;ll pass thanks.&#xA;</description>
      <content:encoded><![CDATA[<p>Late last year I parted ways with <a href="https://hashnode.com/">Hashnode</a> as the platform of choice for my blog. They pivoted a bit too far into the generative AI space which —  to me for a blogging platform —  made little to no sense. What? You write your content with AI, which then trains their model only to spit out more AI-generated slop?  A slop-based <a href="https://en.wikipedia.org/wiki/Ouroboros">Ouroboros</a>, if you will. Good luck with that, but I&rsquo;ll pass thanks.</p>
<h2 id="first-a-small-rant">
  <a class="heading-link" href="#first-a-small-rant">First, a Small Rant<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Before you call me a <a href="https://en.wikipedia.org/wiki/Luddite">Luddite</a>, let&rsquo;s be clear here, I don&rsquo;t have an issue with the technology itself. I&rsquo;m an engineer. I think it all has its uses, but force-feeding it into almost every aspect of our lives with no regard to any social and environmental impacts with frequently no chance of being able to easily opt-out does not sit right with me one bit.</p>
<p>We&rsquo;ve seen this happen over the last decade with touchscreens, IoT, blockchain, NFTs, Web3 the list goes on. All promoted by the endless parade of interchangeable, nameless faceless talentless hacks and disingenuous grifters who only care about making &ldquo;BIG LINE GO UP&rdquo;.</p>
<p>It&rsquo;s all so tiring, so perhaps you can understand why I&rsquo;d immediately recoil in disgust and go my own way with an alternative I can control.</p>
<p>Or, to put it in even simpler terms:</p>
<p><img src="/blog/my-blog-publishing-setup/image-1.png" alt=""></p>
<h2 id="the-stack">
  <a class="heading-link" href="#the-stack">The &ldquo;Stack&rdquo;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I suppose it&rsquo;s technically a &ldquo;stack&rdquo;, but it seems a bit funny to use the term when referencing a blog setup. Just as with my <a href="https://plantsm.art">Plant Smart</a> project, the goal is to automate as much as possible while keeping maintenance requirements and hard- and soft-dollar costs as low as possible. For this, I need only use a handful of tools to keep this space operational.</p>
<p>As far as the question over &ldquo;cost&rdquo;, outside of time spent, it&rsquo;s about ~$100 AUD per year for the domain name. This blog is a very small and inconsequential part of this domain, so the overall cost is absorbed by &ldquo;other stuff&rdquo;.</p>
<p>So, the greatest cost is&hellip; time? I guess 🤷</p>
<h3 id="obsidian">
  <a class="heading-link" href="#obsidian">Obsidian<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>While I won&rsquo;t be going into what <a href="">Obsidian</a> is and how it works, I will say it&rsquo;s been a personal boon in how I keep notes and track of ideas. I have a lot of fleeting / ephemeral thoughts I that would otherwise lose instantly if I didn&rsquo;t immediately jot them down using this app.</p>
<p>So, it makes perfect sense for me to use my personal <a href="https://help.obsidian.md/Getting+started/Create+a+vault">vault</a> to store all my blog content. The fact Obsidian content nothing more than Markdown files makes working with them using other bits of tech that much easier.</p>
<p>Oh, it&rsquo;s free btw.</p>
<h3 id="hugo">
  <a class="heading-link" href="#hugo">Hugo<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p><a href="https://gohugo.io/">This</a> is probably one of the more popular options when it comes to static site generators. It&rsquo;s easy to install, is incredibly extensible and has loads of documentation to help you along. The template syntax sits upon Golang&rsquo;s templating engine, so if you have a strong background as a Go developer like myself, you&rsquo;ll be right at home.</p>
<p>However, knowing Go isn&rsquo;t a hard requirement though it will make getting on top of things quite a bit easier.</p>
<p>What I enjoyed while learning about this project was I got to control every aspect of how I wanted my blog to look and function. Building the current design from the ground up with Hugo was incredibly fun.</p>
<p>Also, free. Very cool.</p>
<h3 id="cloudflare">
  <a class="heading-link" href="#cloudflare">Cloudflare<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>I already used Cloudflare to manage this domain&rsquo;s DNS settings, so it only made sense to publish my static site to Cloudflare Pages over, say, Vercel or Github Pages. There&rsquo;s nothing spectacular going on here outside of me just wanting to keep these two things in the same place.</p>
<p>That being said, I have no issues with migrating else where if the need should ever arise. Though, I doubt this blog will hit the 20k file limit of Cloudflare Pages any time soon.</p>
<p>Again, free for the purposes of this article. If you don&rsquo;t have a dedicated personal domain, you will be provided a subdomain attached to the <code>pages.dev</code> apex.</p>
<h2 id="the-good-stuff">
  <a class="heading-link" href="#the-good-stuff">The Good Stuff<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>How is this all put together for day-to-day usage? Luckily, the Cloudflare stuff runs on auto-pilot, so it&rsquo;s pretty much set-and-forget. I suppose you could say the same about the other components, but they&rsquo;re the parts I touch the most.</p>
<p>I have a single vault in Obsidian. It&rsquo;s where all my thoughts go. Stored within is a top-level <code>Blog/</code> directory. Care to hazard a guess as to what it may contain?</p>
<p><img src="/blog/my-blog-publishing-setup/image-2.png" alt=""></p>
<p>Hugo has the concept of <a href="https://gohugo.io/content-management/page-bundles/">page bundles</a> where you can group all resources associated with your blog articles within a single directory. You&rsquo;ll notice above all directories mirror the blog posts hosted on this site. In my case, we can consider the directory names to be the human-friendly slugs you see in your address bar that point to the associated content. Within these directories, you&rsquo;d see any other resource I may link to; source code, images, etc&hellip; The best thing about this is I only have to perform a relative reference to these resources. If I want to link to <code>image-1.png</code>, I only have to reference it as <code>![](/blog/my-blog-publishing-setup/image-1.png)</code> without worrying about specifying an absolute path; very nice.</p>
<p>When I want to write a new article, I first create a new directory here. I may already have the title of the article in mind, but it can be in flux until I decide to publish it. In the new directory, I create a file called <code>index.md</code>. This file contains all the content of the associated article written using Markdown.</p>
<p>I then use an Obsidian <a href="https://help.obsidian.md/Plugins/Templates">template</a> dedicated to new blog posts and apply it to the new file. This ensures I have all <a href="https://jekyllrb.com/docs/front-matter/">front matter</a> properties ready to go. This makes configuring my posts in the editor that much easier.</p>
<p><img src="/blog/my-blog-publishing-setup/image-3.png" alt=""></p>
<p>All that&rsquo;s left is writing my articles! Well, not really. I need a way to synchronise these files with my local Hugo instance. As you can see <a href="https://github.com/wilhelm-murdoch/wilhelm.codes/tree/main/content/blog">here</a>, the content mirrors what I have stored in my Obsidian vault.</p>
<p>This can be easily addressed with 2 small apps:</p>
<ol>
<li><code>fswatch</code> which monitors a specified directory for changes and allows you to trigger subsequent commands for specific file system events. Basically, I want to watch for any changes in my Obsidian vaults <code>Blog/</code> directory.</li>
<li><code>rsync</code> to dynamically keep the target Hugo directory in sync with the Obsidian source directory.</li>
</ol>
<p>I, of course, have this placed in a script that I have running in the background while I write. This is a great help during the drafting phase as I have Hugo dynamically processing and displaying these changes locally. This allows me to easily proof my work visually as I carry on writing.</p>
<p>Here&rsquo;s the full script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SOURCE</span><span class="o">=</span><span class="si">${</span><span class="nv">SOURCE</span><span class="p">:=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> SOURCE
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DESTINATION</span><span class="o">=</span><span class="si">${</span><span class="nv">DESTINATION</span><span class="p">:=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="si">}</span><span class="s2">&#34;</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> DESTINATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">fswatch -o <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SOURCE</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r event<span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl">    rsync -av --delete <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SOURCE</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DESTINATION</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>I keep this running whenever I&rsquo;m writing a new article. Obsidian saves in almost realtime, so as I type the changes almost immediately sync across to the local Hugo server so I can review my changes.</p>
<p>You effectively run it as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ ./sync.sh /path/to/obsidian/blog/ /path/to/hugo/content/
</span></span></code></pre></div><p>For <code>rsync</code> to work as intended and keep the destination directory completely in sync with the source directory, remember to add a trailing slash to both directory arguments.</p>
<h3 id="no-auto-commit">
  <a class="heading-link" href="#no-auto-commit">No Auto-Commit?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Nope. Not here. There are a few other peeps with blogs out there that &ldquo;commit on change&rdquo;, but I prefer to have a bit more control over what actually gets published. For that, I simply use plain-old git commands to publish my new article:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ git add -A
</span></span><span class="line"><span class="cl">$ git commit -m <span class="s1">&#39;some banal new bullshit screed&#39;</span>
</span></span><span class="line"><span class="cl">$ git push origin main
</span></span></code></pre></div><p>Cloudflare monitors the associated repository, picks up the change and the auto-magic-ally builds and pushes everything to the public eye.</p>
<p>With this flow, I can see my changes locally in near realtime and push to &ldquo;production&rdquo; only when I&rsquo;m happy with the results.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In Closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I&rsquo;m stoked to have my very own place again that I have complete control over. I&rsquo;ve thoroughly enjoyed creating this new blog design and I may even open-source it one day.</p>
<p>That said, I&rsquo;ve made a very loose commitment to post a new article every week this year of our lord 2025 and I&rsquo;m hoping this will make things heaps simpler. I only want to worry about what I&rsquo;m going to write and just get the content out there.</p>
<p>I sincerely hope I can keep this self-imposed weekly commitment. 😬👍</p>]]></content:encoded>
    </item>
    <item>
      <title>Breaking Up Log Output Using Bash</title>
      <link>https://wilhelm.codes/blog/breaking-up-log-output-with-bash/</link>
      <pubDate>Thu, 09 Jan 2025 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/breaking-up-log-output-with-bash/</guid>
      <category>bash</category>
      <category>snippets</category>
      <category>tutorials</category>
      <description>I tend to look at a lot of log output throughout the day as part of my role as platform engineer. This obviously extends to any backend, or ops-related, role. One little niggle that always gets to me is tailing output where the lines only change when something interesting happens.&#xA;</description>
      <content:encoded><![CDATA[<p>I tend to look at a <em>lot</em> of log output throughout the day as part of my role as platform engineer. This obviously extends to any backend, or ops-related, role. One little niggle that always gets to me is tailing output where the lines only change when something interesting happens.</p>
<p>I have no idea if time is actually passing. Obviously, I can eyeball timestamps if available, but there are times when so many lines zip through the buffer that it&rsquo;s easy to lose track or even go cross-eyed.</p>
<p>It can be tricky to notice that lines are still being tailed if the output doesn&rsquo;t drastically change in some meaningful or noticeable way. Something I like to is intercept each line and then output some kind of divider whenever <code>n</code> lines have been added to the buffer.</p>
<p>It&rsquo;s as simple as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">lines</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r line<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">	<span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">line</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">	<span class="o">((</span>lines++<span class="o">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">	<span class="k">if</span> <span class="o">((</span>lines % <span class="nv">5</span> <span class="o">==</span> 0<span class="o">))</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">		<span class="nb">echo</span> <span class="s2">&#34;----------&#34;</span>
</span></span><span class="line"><span class="cl">	<span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>Effectively, all this does is:</p>
<ol>
<li>Intercept each line of output being piped into the script.</li>
<li>Keep a running tally of lines we&rsquo;ve intercepted so far; <code>$lines++</code>.</li>
<li>If the current tally is divisible by <code>n</code>, or in this case <code>5</code>, spit out an additional line containing a divider.</li>
<li>Keep doing this forever until the process is terminated.</li>
</ol>
<p>If you were to save this in a file named as <code>divider.sh</code> and set it to execute with something like <code>chmod a+x</code> you could test it by doing something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="k">while</span> true<span class="p">;</span> <span class="k">do</span> <span class="nb">echo</span> <span class="s2">&#34;emitting a noop&#34;</span><span class="p">;</span> sleep 1<span class="p">;</span> <span class="k">done</span> <span class="p">|</span> ./divider.sh
</span></span></code></pre></div><p>And you&rsquo;ll see something like the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">----------
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">emitting a noop
</span></span><span class="line"><span class="cl">----------
</span></span></code></pre></div><p>This is pretty dumb, but now you can see that output is still being placed in your terminal buffer. I&rsquo;ve set it to every <code>5</code> lines, but you can update the script to make that configurable. You could also play a sound when the script places a divider in the buffer using something like <code>tput bel</code>.</p>
<p>Anyway&hellip; Enjoy.</p>]]></content:encoded>
    </item>
    <item>
      <title>How to Mass-Unfollow Instagram Accounts</title>
      <link>https://wilhelm.codes/blog/how-to-mass-unfollow-instagram-accounts/</link>
      <pubDate>Tue, 07 Jan 2025 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/how-to-mass-unfollow-instagram-accounts/</guid>
      <category>bash</category>
      <category>api</category>
      <category>tutorials</category>
      <category>snippets</category>
      <description>I&amp;rsquo;m not super-active on Instagram these days and Threads was a real let down. I have a dormant Mastodon account, but find myself being quite happy on Bluesky at the moment. Who knows how long that will last. Having recently read about Meta creating loads of fake AI-based accounts, I thought it was time to do some spring cleaning.&#xA;</description>
      <content:encoded><![CDATA[<p>I&rsquo;m not super-active on Instagram these days and Threads was a real let down. I have a dormant Mastodon account, but find myself being quite happy on <a href="https://bsky.app/profile/wilhelm.codes">Bluesky</a> at the moment. Who knows how long that will last. Having recently read about Meta creating loads of <a href="https://www.404media.co/metas-ai-profiles-are-indistinguishable-from-terrible-spam-that-took-over-facebook/">fake AI-based accounts</a>, I thought it was time to do some spring cleaning.</p>
<h2 id="before-we-begin">
  <a class="heading-link" href="#before-we-begin">Before we begin<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Today, I went ahead and made my Instagram profile private. I have no desire to delete my posts just yet, but decided to go ahead and unfollow everyone as a start. There are several ways to do this:</p>
<ul>
<li>API calls using an SDK, which requires a developer account.</li>
<li>Manually deleting &ldquo;handraulically&rdquo; via the native app or browser, but that can quickly become tedious if you have more than a hundred or so follows.</li>
<li>Random &ldquo;GreaseMonkey&rdquo; scripts off the web that may, or may not, work properly if at all.</li>
<li>Reverse-engineering XHR calls from your browser and writing a simple Bash script to automate the process.</li>
</ul>
<p>If you&rsquo;ve read some of my <a href="/blog/why-cant-i-hold-all-these-slack-emojis/">previous</a> <a href="/blog/liberating-custom-slack-emojis/">blog</a> <a href="/blog/falsifying-github-participation-graphs-for-fun-and-profit/">posts</a>, you probably can guess which one I&rsquo;ll be going with.</p>
<h3 id="requirements">
  <a class="heading-link" href="#requirements">Requirements<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>This guide assumes you have <em>some</em> programming or scripting knowledge.</p>
<p>You will need the following on your machine:</p>
<ul>
<li>A shell terminal with Bash, or ZSH, support.</li>
<li><code>curl</code> to make API calls from the command line.</li>
<li><code>jq</code> to easily parse JSON-based responses from said API calls. This can easily be installed using <a href="https://brew.sh/">Homebrew</a> or some other supported package manager.</li>
<li>A modern browser that supports a developer tools console to intercept XHR calls.</li>
</ul>
<p>It&rsquo;s worth mentioning that I am using a MacBook with the Chrome browser, but other webkit-based browsers should support similar functionality.</p>
<h2 id="fire-up-the-browser">
  <a class="heading-link" href="#fire-up-the-browser">Fire up the browser!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Open up your browser, or new tab, and navigate to your Instagram profile. Open your browser&rsquo;s developer console and navigate to the &ldquo;Network&rdquo; tab. Clear out whatever requests are currently listed and then filter by &ldquo;Fetch/XHR&rdquo;.</p>
<h3 id="your-following-list">
  <a class="heading-link" href="#your-following-list">Your following list<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>We need to intercept an XHR request that returns a JSON body containing some of your followers. Click the &ldquo;following&rdquo; link and monitor the resulting requests that pop in the list on the &ldquo;Network&rdquo; tab.</p>
<p><img src="/blog/how-to-mass-unfollow-instagram-accounts/image-1.png" alt=""></p>
<p>You <em>should</em> see something like <code>following/count=12...</code> in the list. Right-click that item and select &ldquo;Copy as cURL&rdquo;. Paste the new value in your clipboard into a scratch file for reference later.</p>
<p><img src="/blog/how-to-mass-unfollow-instagram-accounts/image-2.png" alt=""></p>
<p>Be aware this command will contain all the headers required to make requests from your terminal on your behalf. Do NOT share these details with anyone. I haven&rsquo;t checked how long until the tokens within the auth headers expire, so assume they are long(ish)-lived and treat them accordingly.</p>
<h3 id="an-unfollow-request">
  <a class="heading-link" href="#an-unfollow-request">An unfollow request<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Next, we need to intercept an XHR request to <em>unfollow</em> someone. Going back to your following list, select a random user and unfollow them while once again keeping an eye on the &ldquo;Network&rdquo; tab.</p>
<p><img src="/blog/how-to-mass-unfollow-instagram-accounts/image-3.png" alt=""></p>
<p>You&rsquo;re going to see something like the following; an XHR event making a <code>POST</code> request to &ldquo;destroy&rdquo; your chosen follower. Once again, you&rsquo;ll right-click and &ldquo;Copy and cURL&rdquo;. Paste this into your scratch file as well for future reference.</p>
<p><img src="/blog/how-to-mass-unfollow-instagram-accounts/image-4.png" alt=""></p>
<h2 id="putting-it-all-together">
  <a class="heading-link" href="#putting-it-all-together">Putting it all together<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Now that you have your two types of requests, we&rsquo;ll need to script the following steps:</p>
<ul>
<li>Execute a <code>curl</code> request to fetch a JSON body containing the accounts you follow, one page of results at a time.</li>
<li>Use <code>jq</code> to extract their Instagram IDs.</li>
<li>Store the results in a variable named&hellip; drumroll, pls&hellip; <code>$ids</code>.</li>
<li>Iterate through each <code>$id</code>.</li>
<li>Execute a <code>curl</code> request to unfollow each <code>$id</code>.</li>
</ul>
<p>Here is the meat of the script. I have included only the necessary headers required to successfully make these requests on your Instagram account&rsquo;s behalf. You&rsquo;ll need to swap out the <code>...</code> with your own values.</p>
<p>You may also notice that I&rsquo;ve change the <code>count=...</code> parameter to <code>100</code>. It may be able to go higher, but I haven&rsquo;t tested it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">ids</span><span class="o">=</span><span class="k">$(</span>
</span></span><span class="line"><span class="cl">  curl <span class="s1">&#39;https://www.instagram.com/api/v1/friendships/.../following/?count=100&amp;hl=en&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;cookie: ...&#39;</span>           <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-asbd-id: ...&#39;</span>        <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-csrftoken: ...&#39;</span>      <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-ig-app-id: ...&#39;</span>      <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-ig-www-claim: ...&#39;</span>   <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-requested-with: ...&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-web-session-id: ...&#39;</span> <span class="p">|</span> jq -r <span class="s1">&#39;.users[].id&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> -r id<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -n <span class="s2">&#34;Unfollowing user &#39;</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  curl -s -o /dev/null <span class="s2">&#34;https://www.instagram.com/api/v1/friendships/destroy/</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">/?hl=en&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;cookie: ...&#39;</span>           <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-asbd-id: ...&#39;</span>        <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-csrftoken: ...&#39;</span>      <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-ig-app-id: ...&#39;</span>      <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-ig-www-claim: ...&#39;</span>   <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-requested-with: ...&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    -H <span class="s1">&#39;x-web-session-id: ...&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --data-raw <span class="s2">&#34;container_module=profile&amp;nav_chain=PolarisProfilePostsTabRoot%3AprofilePage%3A1%3Avia_cold_start%2CPolarisProfilePostsTabRoot%3AprofilePage%3A2%3Aunexpected&amp;user_id=</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s1">&#39; ... done!&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> <span class="o">&lt;&lt;&lt;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">ids</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Finished&#34;</span>
</span></span></code></pre></div><p>You can save this as a file and make it executable, but I just paste this bad boy into your terminal. The above script will remove <code>100</code> accounts at a time. Just keep executing the script until you no longer see results like the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Unfollowing user &#39;111&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;222&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;333&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;444&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;555&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;666&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;777&#39;... done
</span></span><span class="line"><span class="cl">Unfollowing user &#39;888&#39;... done
</span></span><span class="line"><span class="cl">Finished
</span></span></code></pre></div><h3 id="caveats-gotchas--other-things">
  <a class="heading-link" href="#caveats-gotchas--other-things">Caveats, gotchas &amp; other things<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>I&rsquo;ve only had a few hundred accounts that I followed, so I didn&rsquo;t feel the need to make this a bullet-proof solution. There&rsquo;s quite a bit missing and things can go wrong. Here are some things to consider off the top of my head :</p>
<ul>
<li>The session tokens will expire eventually, so you will have to refer back to your &ldquo;Network&rdquo; tab in your developer tools console to get new ones.</li>
<li>As this example only unfollows a static amount at a time, re-executing the script may become tedious for very large follow numbers. Perhaps, you can modify this script to add some basic pagination.</li>
<li>There is no error handling here, so keep an eye out for potential rate limiting or expired token issues with the Instagram API.</li>
<li>Constantly updating session header values can be made a bit simpler by resorting to variables instead.</li>
</ul>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In closing &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Cleaning out old, unused or unwanted social media accounts can be liberating. I&rsquo;ve recently gutted my Twitter presence and I haven&rsquo;t been on Facebook in years, though I still do use Messenger to keep in touch with a handful of people. I&rsquo;d prefer using Signal for this, but trying to convince other people to install yet another messaging app is like pulling teeth.</p>
<p>Anyway, I actually enjoy writing these kinds of posts. What other social networks should I cover? LinkedIn? Threads? What are some ways you can improve and build on the above? Lemme know in the comment section below.</p>]]></content:encoded>
    </item>
    <item>
      <title>The Longer Something Doesn&#39;t Happen, the Sooner it Will</title>
      <link>https://wilhelm.codes/blog/the-longer-something-doesnt-happen-the-sooner-it-will/</link>
      <pubDate>Sat, 27 Jan 2024 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/the-longer-something-doesnt-happen-the-sooner-it-will/</guid>
      <category>devops</category>
      <category>sre</category>
      <category>chaos-engineering</category>
      <description>This is often referred to as the &amp;ldquo;Mean Time Between Failures (MTBF)&amp;rdquo; in the context of Site Reliability Engineering. It&amp;rsquo;s a somewhat counterintuitive concept that highlights the fact that failures or incidents tend to occur when you least expect them, especially if you haven&amp;rsquo;t experienced one for a while. While it may sound paradoxical, there is some reasoning behind it.&#xA;</description>
      <content:encoded><![CDATA[<p>This is often referred to as the &ldquo;<a href="https://en.wikipedia.org/wiki/Mean_time_between_failures">Mean Time Between Failures (MTBF)</a>&rdquo; in the context of Site Reliability Engineering. It&rsquo;s a somewhat counterintuitive concept that highlights the fact that failures or incidents tend to occur when you least expect them, especially if you haven&rsquo;t experienced one for a while. While it may sound paradoxical, there is some reasoning behind it.</p>
<h2 id="accumulation-of-underlying-issues">
  <a class="heading-link" href="#accumulation-of-underlying-issues">Accumulation of Underlying Issues<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Over time, systems and processes can accumulate small issues, technical debt, or unnoticed problems. These issues can build up, leading to a higher likelihood of a significant failure or incident occurring as time goes on.</p>
<h2 id="complacency-and-reduced-vigilance">
  <a class="heading-link" href="#complacency-and-reduced-vigilance">Complacency and Reduced Vigilance<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>When a system or service has been running smoothly for an extended period, teams may become complacent and less vigilant. They might not be as proactive in monitoring, testing, and maintaining the system, which can increase the risk of failure.</p>
<h2 id="evolving-environments">
  <a class="heading-link" href="#evolving-environments">Evolving Environments<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>As technology and business environments evolve — as they inevitably do in our space —, the context in which a system operates also changes. What was once a stable and reliable configuration may no longer be suitable, leading to unexpected issues or failures when the system is finally pushed to its limits.</p>
<h2 id="regression-to-the-mean">
  <a class="heading-link" href="#regression-to-the-mean">Regression to the Mean<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p><a href="https://en.wikipedia.org/wiki/Law_of_large_numbers">The law of large numbers</a> suggests that over time, events tend to revert to their average or &ldquo;mean&rdquo; frequency. If you&rsquo;ve experienced an unusually long period without incidents, statistics may suggest that you&rsquo;re due for one soon, just as a run of heads in a coin toss doesn&rsquo;t make tails any less likely on the next toss.</p>
<h2 id="maintaining-awareness">
  <a class="heading-link" href="#maintaining-awareness">Maintaining Awareness<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<blockquote class="pull-quote">
  The price of <s>freedom</s> stability is eternal vigilance.
  <cite>Ancient Klingon Proverb ( probably )</cite>
</blockquote>

<h3 id="mitigation-strategies">
  <a class="heading-link" href="#mitigation-strategies">Mitigation Strategies<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Recognize the importance of proactively addressing issues before they accumulate. This involves regular monitoring, capacity planning, load testing, and maintenance to reduce the likelihood of a sudden failure.</p>
<h3 id="risk-management">
  <a class="heading-link" href="#risk-management">Risk Management<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Focus on identifying and managing risks, even during periods of relative stability. They plan for various failure scenarios and aim to minimize their impact through redundancy, graceful degradation, and fault-tolerant design.</p>
<h3 id="continuous-improvement">
  <a class="heading-link" href="#continuous-improvement">Continuous improvement<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Promote a culture of continuous improvement, encouraging teams to learn from past incidents, conduct post-mortems, even live-fire exercises and apply those lessons to prevent similar issues in the future.</p>
<h3 id="metrics--monitoring">
  <a class="heading-link" href="#metrics--monitoring">Metrics &amp; Monitoring<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Use metrics and monitoring tools to maintain a vigilant eye on system health and performance. They set thresholds and alarms to detect anomalies early, regardless of how long it&rsquo;s been since the last incident.</p>
<p>At any given point your production workloads may be operating under any number of unknown failure modes. Things break; it&rsquo;s inevitable. Adjust your expectations accordingly and build around this fact.</p>
<h2 id="in-conclusion">
  <a class="heading-link" href="#in-conclusion">In Conclusion<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The idea that the longer something doesn&rsquo;t happen, the sooner it will is a <em>reminder</em> of the importance of vigilance, proactive maintenance, and risk management not only in site reliability engineering, but software engineering as a whole.</p>
<p>This may not be a deterministic law as it highlights the tendency for issues to accumulate over time if not addressed, making it crucial to maintain a robust and resilient system.</p>]]></content:encoded>
    </item>
    <item>
      <title>Why I Built Plant Smart</title>
      <link>https://wilhelm.codes/blog/why-i-built-plant-smart/</link>
      <pubDate>Sun, 08 Jan 2023 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/why-i-built-plant-smart/</guid>
      <category>svelte</category>
      <category>plantsm.art</category>
      <description>Simply put, I love plants and I love animals. I can&amp;rsquo;t count how many times I&amp;rsquo;ve been to a plant nursery and had to stop and do a Google search to see if something was safe enough to bring home and keep around our little Pandora, or 🐼 for short.&#xA;</description>
      <content:encoded><![CDATA[<p>Simply put, I love plants and I love animals. I can&rsquo;t count how many times I&rsquo;ve been to a plant nursery and had to stop and do a Google search to see if something was safe enough to bring home and keep around our little Pandora, or 🐼 for short.</p>
<h2 id="its-as-simple-as-that">
  <a class="heading-link" href="#its-as-simple-as-that">It&rsquo;s as simple as that?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>That was the idea, at least. I just wanted a single place that had all the information I needed so I could make informed purchasing decisions. I also figured I couldn&rsquo;t possibly be the only person who has this problem. So, I set off to make this project during the holiday break of 2022.</p>
<p>However, I didn&rsquo;t want to make it <em>too</em> easy for myself&hellip;</p>
<h2 id="a-challenge">
  <a class="heading-link" href="#a-challenge">A challenge!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I work as a weird combination of <a href="https://en.wikipedia.org/wiki/Site_reliability_engineering">SRE</a> and <a href="https://en.wikipedia.org/wiki/DevOps">DevOps engineer</a>. I have plenty of smarts when it comes to the technical side of hosting and running things, but it&rsquo;s been years since I&rsquo;ve done any front-end work. Career-wise, I need to be able to speak the same language as the teams I have to support. So, understanding how far behind that portion of my skillset had become, I thought this would be the perfect opportunity to brush up.</p>
<h3 id="the-requirements">
  <a class="heading-link" href="#the-requirements">The Requirements:<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<ul>
<li>Must keep operational costs as close to zero as possible. The only money I&rsquo;ve put down on this so far is the $20 AUD to purchase the domain name.</li>
<li>Must be hosted on <a href="https://pages.cloudflare.com/">Cloudflare Pages</a>. I could&rsquo;ve chosen <a href="https://pages.github.com/">Github Pages</a> to keep everything in one place, but automated Cloudflare builds &amp; deployments work out of the box with minimal configuration. Besides, I&rsquo;ve already worked with the latter and I wanted the challenge of trying something new.</li>
<li>Must be completely static. There should be no server-side rendering, processing or other explicit backend dependencies to manage. For this, I use the pre-rendering functionality that comes packaged with <a href="https://kit.svelte.dev/">SvelteKit</a>. I&rsquo;ve been working with this for only a few weeks now and I&rsquo;m a full convert.</li>
<li>All data must be served statically as well. The entirety of the data set is contained within a single JSON file, which you can view at <a href="https://plantsm.art/plants.json">/plants.json</a>. This is the source of truth for all derivative data sets and lookup tables used on this site. It&rsquo;s effectively what I call a &ldquo;dumb API&rdquo;. Check out the <a href="http://localhost:5173/api">API documentation</a> if you&rsquo;d like to know more about it.</li>
<li>Must use <a href="https://kit.svelte.dev/">SvelteKit</a>, <a href="https://www.typescriptlang.org/">TypeScript</a>, <a href="https://tailwindcss.com/">TailwindCSS</a> and <a href="https://vitejs.dev/">Vite</a>. I had zero working knowledge of any of these and my frontend peers can&rsquo;t seem to shut up about them. So&hellip; why not?</li>
<li>Must be <em>fast</em>. Everything is static, compressed, cached and sitting behind a world-class CDN. I&rsquo;ve worked in a network performance and load testing SaaS for close to 5 years now. I wouldn&rsquo;t be able to look myself in the mirror if I couldn&rsquo;t easily do this one. 😅</li>
<li>Must be open-source and community-driven. At one point, I would like to take the hands off the wheel and see if other interested parties would like to get involved and help out with managing datasets and fixing bugs. GitHub allows for pretty much all of this. Check how to <a href="http://plantsm.art/contribute">contribute</a> or see the <a href="http://plantsm.art/updates">latest contributions</a>.</li>
</ul>
<p>So far, it&rsquo;s going quite well. I haven&rsquo;t had any issues with meeting any of these self-imposed development constraints.</p>
<h3 id="any-caveats">
  <a class="heading-link" href="#any-caveats">Any Caveats?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Definitely. The most obvious one would be image storage which has a direct effect on not only repository size — which currently clocks in at over 1.5GB — but, also build and deployment speeds. I could shell out $5 - $10 for object storage and image processing, but that would go against the first constraint. For now, image data will live in the GitHub repository as a perfectly reasonable compromise.</p>
<h2 id="where-did-you-source-all-this-data">
  <a class="heading-link" href="#where-did-you-source-all-this-data">Where did you source all this data?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>There are quite a few sources I&rsquo;ve collated from, but these are the main ones.</p>
<ol>
<li><a href="https://www.inaturalist.org/">iNaturalist</a> is the best source of high-quality, community-driven creative commons license photography. All images have been sourced from this site along with licensing and attribution data.</li>
<li><a href="https://www.aspca.org/">ASPCA</a> was used to initially prime the first dataset. This is also where I sourced most of the common name and symptom data.</li>
<li><a href="https://en.wikipedia.org/wiki/Plant">Wikipedia</a> is the best source of scientific classification data out there.</li>
</ol>
<p>All of this disparate data was collated and munged together by several processing scripts written in <a href="https://go.dev/">Go</a> as <a href="https://magefile.org/">Magefiles</a>. It got me about 95% there, but it still needs quite a bit of handraulic finessing.</p>
<h2 id="where-to-go-from-here">
  <a class="heading-link" href="#where-to-go-from-here">Where to go from here?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I think I&rsquo;ve met most, if not all, of my requirements. The datasets for the project still need a lot of love and I plan on supporting listings for a variety of other pet species. I already have the data; just need to go through it with a fine-toothed comb. Not to mention responsiveness for smaller screens needs a solid amount of work.</p>
<p>I&rsquo;m having loads of fun at the moment learning new things. I hope I can keep doing this for a while longer. However, I do have other things planned for the future and will be using what I&rsquo;ve learned here as a kind of launching pad.</p>
<p>I sincerely hope you find <a href="https://plantsm.art">Plant Smart</a> as useful as I had fun making it.</p>]]></content:encoded>
    </item>
    <item>
      <title>Learning Bash Through Pointless Fun</title>
      <link>https://wilhelm.codes/blog/learning-bash-through-pointless-fun/</link>
      <pubDate>Wed, 05 Oct 2022 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/learning-bash-through-pointless-fun/</guid>
      <category>bash</category>
      <description>Responding to company chat messages in a mocking and sarcastic tone is one of my favourite past times. Classic engineer snark as it were. We’re all in on the joke. However, the artful nuances of snark via online chat tend to get confused by others; Did they mean to sound sarcastic?&#xA;</description>
      <content:encoded><![CDATA[<p>Responding to company chat messages in a mocking and sarcastic tone is one of my favourite past times. Classic engineer snark as it were. We’re all in on the joke. However, the artful nuances of snark via online chat tend to get confused by others; Did they <em>mean</em> to sound sarcastic?</p>
<p>Let’s leave out all doubt regarding our god-tier levels of snarkiness.</p>
<p>Today, we’re going to write a laughably-simple Bash script that lets our coworkers know <em><a href="https://knowyourmeme.com/memes/mocking-spongebob">exactly</a></em> what we think. Along the way, you’ll learn a little bit more about Bash such as:</p>
<ol>
<li>Setting some default flags.</li>
<li>Reading text from <code>stdin</code>.</li>
<li>Parameter expansion.</li>
<li>Looping through each character of a string.</li>
<li>Creating random numbers.</li>
<li>Swapping character types using the <code>tr</code> command.</li>
<li>And, finally, making your long-suffering coworkers’ eyes roll.</li>
</ol>
<hr>
<h2 id="here-we-go">
  <a class="heading-link" href="#here-we-go">Here We Go!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>First, I’m going to just paste the entire script here and then we’ll go through all the important bits line-by-line:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -eo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">read</span> text
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="o">((</span> <span class="nv">i</span><span class="o">=</span>0<span class="p">;</span> i &lt; <span class="s2">&#34;</span><span class="si">${#</span><span class="nv">text</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> i++ <span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="k">$((</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">RANDOM</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">%</span> <span class="m">2</span> <span class="k">))</span> -eq <span class="m">0</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">text</span><span class="p">:</span><span class="si">${</span><span class="nv">i</span><span class="si">}</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> tr <span class="s1">&#39;[:lower:]&#39;</span> <span class="s1">&#39;[:upper:]&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">text</span><span class="p">:</span><span class="si">${</span><span class="nv">i</span><span class="si">}</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> tr <span class="s1">&#39;[:upper:]&#39;</span> <span class="s1">&#39;[:lower:]&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span>
</span></span></code></pre></div><p>Alright, let’s take it from the top. All Bash scripts should start with a <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)">“shebang line”</a>. This tells your terminal which environments and runtimes your script should run under. There are more traditional versions of this — <code>#!/bin/bash</code>, for instance — but, our way is considered to be the most portable overall.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span></code></pre></div><p>Next, we setup some environmental flags. I use at least the following for <em>all</em> of my personal scripts:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">set</span> -eo pipefail
</span></span></code></pre></div><p>Here is what they do:</p>
<ol>
<li><code>set -e</code>: Instructs Bash to immediately exit if any command has a non-zero exit status. This is how it works in most languages, but with Bash, it just keeps on trying to execute subsequent commands. This is generally acceptable on the command line, but not in a script. If we encounter an error, we want to exit immediately.</li>
<li><code>set -o pipefail</code>: This tells Bash <em>not</em> to mask errors that may appear in a pipeline of commands. We want any failed command’s exit code in a pipeline to bubble up to the script itself and then exit with that code.</li>
</ol>
<p>Speaking of pipelines, we want to be able to pipe text to this script so we can do something like the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;wilhelm, i asked you to patch the server.&#34;</span> <span class="p">|</span> ./spongebob
</span></span><span class="line"><span class="cl">wiLHELm, I AsKEd yOu TO PatCh The seRvEr.
</span></span></code></pre></div><p>Let&rsquo;s halt the script and wait for user input and then assign that input to variable <code>text</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">read</span> text
</span></span></code></pre></div><p>Now that we have some text, we need to start randomly-swapping between upper and lower case characters. In order to do that, we need to know the number of characters in our string so we can build a nice loop:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> <span class="o">((</span> <span class="nv">i</span><span class="o">=</span>0<span class="p">;</span> i &lt; <span class="s2">&#34;</span><span class="si">${#</span><span class="nv">text</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">;</span> i++ <span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ... sweet code goes here </span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>We could do something in a sub-shell here like <code>$(echo &quot;${text}&quot; | wc -c)</code> to get the character count, but why do that when we could get the same result with <code>&quot;${#text}&quot;</code>. This lovely bit of <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html">“shell parameter expansion”</a> helps us avoid sub-commands and sub-shells.</p>
<p>Next, we want to be able to randomly swap the capitalisation of each character in the string to get the appropriate effect. A character is either upper- or lower-case, so minimum we need only to swap randomly between 2 values; <code>0</code> and <code>1</code>. We can get this effect in Bash by using the internal <a href="https://tldp.org/LDP/abs/html/randomvar.html"><code>${RANDOM}</code> function</a> like so with the <a href="https://tldp.org/LDP/abs/html/ops.html">modulo</a> ( or “mod” ) operator:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="k">$((</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">RANDOM</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">%</span> <span class="m">2</span> <span class="k">))</span> -eq <span class="m">0</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ... do something</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># ... do the opposite</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>We’re now at the point where we want to modify the character associated with the <code>for</code> loop’s current iteration, but how do we get it from the <code>text</code> variable? Once again we use some parameter expansion in the form of <code>${text:offset:length}</code>. We have the value for “offset” already; it’s <code>${i}</code>. We only want a single character returned, so we use <code>1</code> for “length”.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">text</span><span class="p">:</span><span class="si">${</span><span class="nv">i</span><span class="si">}</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>This spits out the current character for each iteration of our loop. The <code>-n</code> in the <code>echo</code> statement simply stops Bash from adding a newline to the end of the result. Otherwise, you’d get a line per character as output.</p>
<p>Finally, we want to do case swapping. For this, we pipe the output of the above <code>echo</code> command into the <code>tr</code> command. Within the above <code>if</code> statement, if our random number equals <code>0</code>, let’s swap a <em>lowercase</em> <code>[:lower:]</code> character with an <em>uppercase</em> <code>[:upper:]</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">text</span><span class="p">:</span><span class="si">${</span><span class="nv">i</span><span class="si">}</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> tr <span class="s1">&#39;[:lower:]&#39;</span> <span class="s1">&#39;[:upper:]&#39;</span>
</span></span></code></pre></div><p>And, then, we do the opposite for any other result:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">text</span><span class="p">:</span><span class="si">${</span><span class="nv">i</span><span class="si">}</span><span class="p">:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> tr <span class="s1">&#39;[:upper:]&#39;</span> <span class="s1">&#39;[:lower:]&#39;</span>
</span></span></code></pre></div><p>You’ll notice a final <code>echo</code> command at the bottom of the script. Thanks to the final <code>echo -n ...</code> command from the previous <code>for</code> loop, you may find your results prepended to your command prompt. This ensures a newline makes it to the end of your 🧽 output.</p>
<h2 id="testing-time">
  <a class="heading-link" href="#testing-time">Testing Time<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Save the script as <code>spongebob</code> and make it executable with <code>chmod a+x spongebob</code>. That should be it! Here are a few of my results:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;abandon all hope, ye who enter here.&#34;</span> <span class="p">|</span> ./spongebob
</span></span><span class="line"><span class="cl">abaNDON ALl Hope, yE wHO EnTer heRE.
</span></span><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;Wilhelm, that last deployment failed. Could you roll it back, please?&#34;</span> <span class="p">|</span> ./spongebob
</span></span><span class="line"><span class="cl">WilhelM, THAT LASt dEployMeNT fAILED. coUlD YOu RoLL It BaCk, PlEaSE?
</span></span><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;Wilhelm, I am your manager. Please, stop mocking me.&#34;</span> <span class="p">|</span> ./spongebob
</span></span><span class="line"><span class="cl">wIlHElM, i aM yOUr MANagER. PlEasE, sTOP mOCKINg mE.
</span></span><span class="line"><span class="cl">$ <span class="nb">echo</span> <span class="s2">&#34;Wilhelm, should we use Kubernetes for this?&#34;</span> <span class="p">|</span> ./spongebob
</span></span><span class="line"><span class="cl">WilHELM, should We Use KuBerNeTEs <span class="k">for</span> THiS?
</span></span></code></pre></div><h2 id="in-conclusion">
  <a class="heading-link" href="#in-conclusion">In Conclusion…<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Ok, obviously this was all a clever ploy to get you to learn a few more Bash things. Definitely do <em>not</em> use this new knowledge to frustrate and annoy your coworkers. Please, be considerate of other people’s mental well being.
I mean, what I <em>meant</em> to say was:</p>
<blockquote class="pull-quote">
  DefInIteLY dO Not UsE THis nEw knoWleDGE to FRUStRATe aND aNnOY YoUR cOWorkers. pLeAsE, be ConsiDeRAtE oF oThEr peOPle’s mEnTal well beiNG.
  <cite>Me.</cite>
</blockquote>

<p>There are any number of ways you could change this script. Instead of liberal use of <code>echo</code>, you could just build a string assigned to a variable and spit that out at the end. Instead of using <code>else</code> you could use <code>continue</code> to skip the final <code>if</code> fallback. Try a few and see what changes.</p>
<p>For the purposes of this article I felt the above sequence of commands was clear enough for most people to follow along.</p>
<p>I hope you learned something!</p>]]></content:encoded>
    </item>
    <item>
      <title>Filtering Docker Containers with jq</title>
      <link>https://wilhelm.codes/blog/filtering-docker-containers-with-jq/</link>
      <pubDate>Thu, 29 Sep 2022 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/filtering-docker-containers-with-jq/</guid>
      <category>docker</category>
      <category>js</category>
      <category>json</category>
      <category>bash</category>
      <description>As is the case with any seasoned DevOps engineer, I have a set of tools in my kit that I simply cannot live without. If I had to distill them into a top-5 list it would be the following.&#xA;</description>
      <content:encoded><![CDATA[<p>As is the case with any seasoned DevOps engineer, I have a set of tools in my kit that I simply cannot live without. If I had to distill them into a top-5 list it would be the following.</p>
<ol>
<li>Bash for portability.</li>
<li><a href="https://stedolan.github.io/jq/">jq</a> to filter JSON objects from RESTful APIs.</li>
<li>AWS CLI as I cannot stand dealing with the web console.</li>
<li><code>curl</code> ( with <code>wget</code> as a reasonable fallback ) to interact with various APIs.</li>
<li><a href="https://www.terraform.io/">Terraform</a> for <em>most</em> of my IaC needs.</li>
</ol>
<p>It wouldn’t be an exaggeration to say I <em>literally</em> use these tools <em>every</em> day. Specifically, the first 3 items. Furthermore, to say that manually sorting through Docker containers is a practice in tedium would be a massive understatement. Because of this, not a day passes where I am not grateful to have found <code>jq</code>.</p>
<p>In this case, my favourite duo <em>is</em> Docker and <code>jq</code>&hellip; which is what this article is about. Let’s learn some fun filtering patterns to make your life a bit easier if you’re stuck in a terminal all day like myself.</p>
<h2 id="setting-up">
  <a class="heading-link" href="#setting-up">Setting Up<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>First and foremost, you’re going to need to install <code>jq</code> in your test environment. If you’re on most common Linux distributions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt-get install jq
</span></span></code></pre></div><p>Or, on MacOS:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew update <span class="o">&amp;&amp;</span> brew install jq
</span></span></code></pre></div><p>I’m assuming that if you’re reading this you already have Docker installed in your test environment. If not, head out to their website and read their <a href="https://docs.docker.com/get-docker/">installation documentation</a>. It tends to vary wildly depending on your OS.</p>
<p>Let’s spin up some containers we can play around with. I use <code>redis</code> for stuff like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">for</span> i in <span class="o">{</span>1..5<span class="o">}</span><span class="p">;</span> <span class="k">do</span> docker run -it --rm -d redis<span class="p">;</span> <span class="k">done</span>
</span></span></code></pre></div><h2 id="environmental-variables">
  <a class="heading-link" href="#environmental-variables">Environmental Variables<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<h3 id="sourceable-blocks">
  <a class="heading-link" href="#sourceable-blocks">Sourceable Blocks<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>There may be times I wish to collate all environmental variables associated with a set of containers and stick them a <code>.env</code> file to source later. Here we create a block for all the <code>redis</code> containers we just spun up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[].Config.Env
</span></span></span><span class="line"><span class="cl"><span class="s1">  | flatten 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .[]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>This one will generate a lot of duplicates as all our test containers are effectively clones. What if we want a distinct set of instead? We can use the <code>unique</code> filter:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">	[
</span></span></span><span class="line"><span class="cl"><span class="s1">	  .[].Config.Env
</span></span></span><span class="line"><span class="cl"><span class="s1">	] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | flatten 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | unique 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .[]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>What if I want a set from specific containers?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    [
</span></span></span><span class="line"><span class="cl"><span class="s1">	    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">      | select([.Id] | inside([
</span></span></span><span class="line"><span class="cl"><span class="s1">	      &#34;747c2c92855f88a8e9aa9709dcdf3a01f1677e70bf4c0bf0e520eb38ac502876&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">	      &#34;2cf70261ef62bc36d19aba02f8ef7b8d7aabfcb1e9f4593a919baa17f80aca5b&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">	    ]))
</span></span></span><span class="line"><span class="cl"><span class="s1">      | .Config.Env
</span></span></span><span class="line"><span class="cl"><span class="s1">	] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | flatten 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | unique 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .[]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>How about filtering by a specific ip address for the default <code>bridge</code> network? Using a different network? Just replace <code>bridge</code> with the alternate network name.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select(.NetworkSettings.Networks.bridge.IPAddress == &#34;172.17.0.2&#34;) 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .Config.Env 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .[]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>Multiple ip addresses?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select([.NetworkSettings.Networks.bridge.IPAddress] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | inside([
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;172.17.0.2&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;172.17.0.3&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">    ])) 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .Config.Env 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .[]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><h3 id="as-flags-instead">
  <a class="heading-link" href="#as-flags-instead">As Flags Instead<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Here’s a strange one I’ve had to do. What if we want to recreate a list of <code>--env</code> flags we could pass to other <code>docker run ...</code> commands?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    [
</span></span></span><span class="line"><span class="cl"><span class="s1">        [
</span></span></span><span class="line"><span class="cl"><span class="s1">          .[].Config.Env
</span></span></span><span class="line"><span class="cl"><span class="s1">        ] 
</span></span></span><span class="line"><span class="cl"><span class="s1">      | flatten 
</span></span></span><span class="line"><span class="cl"><span class="s1">      | unique 
</span></span></span><span class="line"><span class="cl"><span class="s1">      | &#34;--env=\(.[])&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">    ] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | join(&#34; &#34;)
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>You’ll get a string that looks similar to the following.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">--env=GOSU_VERSION=1.14 --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --env=REDIS_DOWNLOAD_SHA=f0e65fda74c44a3dd4fa9d512d4d4d833dd0939c934e946a5c622a630d057f2f --env=REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-7.0.4.tar.gz --env=REDIS_VERSION=7.0.4
</span></span></code></pre></div><h2 id="filtering-by-relative-time">
  <a class="heading-link" href="#filtering-by-relative-time">Filtering By Relative Time<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I’ve had to use these several times for things like garbage collecting and cleaning up long-running containers. This uses a bit of Bash to help generate the relative timestamps. We use the <code>date</code> command for this, but if you’re on a <code>darwin</code>-based OS you’ll need to install <code>gdate</code> first via Homebrew with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install coreutils
</span></span></code></pre></div><p>Substitute the following calls to <code>date</code> with <code>gdate</code> below if on MacOS.</p>
<h3 id="newer-than-">
  <a class="heading-link" href="#newer-than-">Newer Than …<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Filtering out recent containers with relative time is fairly straight-forward. The following gives us all containers created in the last hour as JSON output from <code>docker inspect ...</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">threshold</span><span class="o">=</span><span class="k">$(</span>date -d <span class="s2">&#34;1 hour ago&#34;</span> +%s<span class="k">)</span>
</span></span><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq --argjson threshold <span class="s2">&#34;</span><span class="si">${</span><span class="nv">threshold</span><span class="si">}</span><span class="s2">&#34;</span> -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>Perhaps you just want the names of the containers instead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">threshold</span><span class="o">=</span><span class="k">$(</span>date -d <span class="s2">&#34;1 hour ago&#34;</span> +%s<span class="k">)</span>
</span></span><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq --argjson threshold <span class="s2">&#34;</span><span class="si">${</span><span class="nv">threshold</span><span class="si">}</span><span class="s2">&#34;</span> -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .Name[1:]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>Docker likes to put a <code>/</code> at the beginning of each generated container pet name, so we use <code>.Name[1:]</code> to snip it off.</p>
<p>What about just returning the associated ids?</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">threshold</span><span class="o">=</span><span class="k">$(</span>date -d <span class="s2">&#34;1 hour ago&#34;</span> +%s<span class="k">)</span>
</span></span><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq --argjson threshold <span class="s2">&#34;</span><span class="si">${</span><span class="nv">threshold</span><span class="si">}</span><span class="s2">&#34;</span> -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .Id
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>Or, ip addresses:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">threshold</span><span class="o">=</span><span class="k">$(</span>date -d <span class="s2">&#34;1 hour ago&#34;</span> +%s<span class="k">)</span>
</span></span><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq --argjson threshold <span class="s2">&#34;</span><span class="si">${</span><span class="nv">threshold</span><span class="si">}</span><span class="s2">&#34;</span> -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .NetworkSettings.Networks.bridge.IPAddress
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><h3 id="older-than">
  <a class="heading-link" href="#older-than">Older Than???<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>The same filters apply, but you’re just flipping the compare operator from <code>&gt;</code> to <code>&lt;</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">threshold</span><span class="o">=</span><span class="k">$(</span>date -d <span class="s2">&#34;1 hour ago&#34;</span> +%s<span class="k">)</span>
</span></span><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq --argjson threshold <span class="s2">&#34;</span><span class="si">${</span><span class="nv">threshold</span><span class="si">}</span><span class="s2">&#34;</span> -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">    .[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &lt; $threshold)
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><h2 id="other-common-patterns">
  <a class="heading-link" href="#other-common-patterns">Other Common Patterns<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>There have been times where I need a block of container ip addresses so I can dynamically update some Nginx upstreams somewhere. With <code>jq</code> it’s as easy as:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  .[].NetworkSettings.Networks.bridge.IPAddress
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>The output would look something like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">172.17.0.6
</span></span><span class="line"><span class="cl">172.17.0.5
</span></span><span class="line"><span class="cl">172.17.0.4
</span></span><span class="line"><span class="cl">172.17.0.3
</span></span><span class="line"><span class="cl">172.17.0.2
</span></span></code></pre></div><p>Perhaps, I want to find a specific container id by it’s ip address:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">	.[]
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select(.NetworkSettings.Networks.bridge.IPAddress == &#34;172.17.0.3&#34;)
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .Id
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>Or, just give me the names of all running containers:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  .[].Name[1:]
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span>
</span></span></code></pre></div><p>Which would give you something like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">zealous_hertz
</span></span><span class="line"><span class="cl">fervent_dijkstra
</span></span><span class="line"><span class="cl">focused_galileo
</span></span><span class="line"><span class="cl">zealous_poincare
</span></span><span class="line"><span class="cl">naughty_wescoff
</span></span></code></pre></div><h2 id="cleaning-up-">
  <a class="heading-link" href="#cleaning-up-">Cleaning Up …<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>We’re considerate people, so let’s clean up after ourselves by killing all containers created using the <code>redis</code> image:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker <span class="nb">kill</span> <span class="k">$(</span>docker inspect <span class="k">$(</span>docker ps -q<span class="k">)</span> <span class="p">|</span> jq -r <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">	.[] 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | select(.Config.Image == &#34;redis&#34;) 
</span></span></span><span class="line"><span class="cl"><span class="s1">  | .Id
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span><span class="k">)</span>
</span></span></code></pre></div><p>Keep in mind this will kill <em>all</em> <code>redis</code> containers.</p>
<h2 id="in-closing-">
  <a class="heading-link" href="#in-closing-">In Closing …<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Having a toolbox filled with utilities you can mix and match together is great if you spend most of your day working in a terminal on a glowing rectangle. Hopefully, I’ve made a strong enough case to for Docker and <code>jq</code> as a great combo.</p>
<h2 id="but-wait-theres-more">
  <a class="heading-link" href="#but-wait-theres-more">But, Wait! There’s More!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Why copy and paste these commands in your terminal, when you could just <a href="https://github.com/wilhelm-murdoch/dq">download and install</a> a handy little Bash script to do it all for you? <code>dq</code> has all the above built-in as well as heaps more bells and whistles.</p>
<p>Here are a few command examples if you’re interested:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dq older-than <span class="m">4</span> months --only-return ips --network pebkac
</span></span><span class="line"><span class="cl">dq newer-than <span class="m">2</span> fortnights --only-return ids
</span></span><span class="line"><span class="cl">dq filter <span class="s1">&#39;.[].Name[1:]&#39;</span>
</span></span><span class="line"><span class="cl">dq find-by-ip-address 172.17.0.2 --only-return names --network foo
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>Why Can&#39;t I Hold All These Slack Emojis?</title>
      <link>https://wilhelm.codes/blog/why-cant-i-hold-all-these-slack-emojis/</link>
      <pubDate>Wed, 28 Sep 2022 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/why-cant-i-hold-all-these-slack-emojis/</guid>
      <category>slack</category>
      <category>emoji</category>
      <category>bash</category>
      <category>api</category>
      <description>Previously, last blog post I wrote covered how to make a quick escape with your precious hoard of custom Slack emojis. It was fairly well-received, but didn’t quite cover the next step in the migration process; how do you upload your millions of little images to your new Slack workspace?&#xA;</description>
      <content:encoded><![CDATA[<p>Previously, <a href="https://wilhelm.codes/liberating-custom-slack-emojis">last blog post</a> I wrote covered how to make a quick escape with your precious hoard of custom Slack emojis. It was fairly well-received, but didn’t quite cover the next step in the migration process; how do you upload your millions of little images to your <em>new</em> Slack workspace?</p>
<h2 id="gathering-requirements">
  <a class="heading-link" href="#gathering-requirements">Gathering Requirements<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Since we’re doing some destructive operations, we’ll need to make sure our Slack user has the appropriate permissions to manage emoji; adding &amp; deleting. This means using a different section of the Slack interface which uses an entirely seperate set of API endpoints.</p>
<p>You’ll still be using the following, which can be found using instructions from <a href="https://wilhelm.codes/liberating-custom-slack-emojis#heading-some-investigative-work">the previous article</a>:</p>
<ul>
<li>An API request token.</li>
<li>A session cookie.</li>
<li>A workspace, or team, id.</li>
</ul>
<p>In addition, you will need your workspaces’s subdomain, or URL. This can easily be found within the Slack app itself:</p>
<p><img src="/blog/why-cant-i-hold-all-these-slack-emojis/image-1.png" alt=""></p>
<h2 id="testing-your-findings">
  <a class="heading-link" href="#testing-your-findings">Testing Your Findings<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This is an incredibly straight-forward process as it’s quite similar to what we already know. The primary difference is this is a <code>multipart/form-data</code> upload. So, instead of shipping of a JSON payload, it’s a form with associated fields.</p>
<p>With the information gathered above, you can upload directly to Slack using a simple cURL command like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s --compressed <span class="s2">&#34;https://&lt;domain&gt;.slack.com/api/emoji.add&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s1">&#39;content-type: multipart/form-data&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -H <span class="s2">&#34;cookie: d=&lt;cookie&gt;;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -F <span class="s2">&#34;token=&lt;token&gt;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -F <span class="s2">&#34;name=&lt;name&gt;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -F <span class="nv">mode</span><span class="o">=</span>data <span class="se">\
</span></span></span><span class="line"><span class="cl">  -F <span class="s2">&#34;image=@&lt;local-emoji-path&gt;&#34;</span>
</span></span></code></pre></div><p>Replace the following:</p>
<ul>
<li><code>&lt;domain&gt;</code> is your workspace’s, or team’s, private URL.</li>
<li><code>&lt;cookie&gt;</code> is your session cookie value.</li>
<li><code>&lt;token&gt;</code> is your request token.</li>
<li><code>&lt;name&gt;</code> will be the named reference of your new emoji.</li>
<li><code>&lt;path&gt;</code> is the relative, or absolute, local path of your emoji file to upload.</li>
</ul>
<p>The JSON response to this request will have the following structure if all goes well:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> <span class="nt">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">true</span> <span class="p">}</span>
</span></span></code></pre></div><p>And, if something went wrong:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> 
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;error&#34;</span><span class="p">:</span> <span class="s2">&#34;err_code&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>That’s it! Now that we know how to upload from the command line.</p>
<h2 id="the-implementation">
  <a class="heading-link" href="#the-implementation">The Implementation<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>If you’re only uploading a handful of emojis, it might make sense to just do it via Slack’s UI. However, this can get a bit tedious if you have dozens, or even hundreds, to upload.  We can  automate things even further by:</p>
<ul>
<li>Gathering all supported images from a specific source directory.</li>
<li>Iterating through our findings and bulk-upload them all in one go.</li>
</ul>
<p>So, let’s do just that. But, first, let’s set up some environmental variables so we can easily configure out script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_DOMAIN</span><span class="p">:=</span><span class="si">}</span>
</span></span></code></pre></div><p>Slack only supports <code>png</code>, <code>gif</code> and <code>jpg</code> image formats, so lets see what we can find from our present working directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">files</span><span class="o">=</span><span class="k">$(</span>find . -iname <span class="se">\*</span>.gif -o -iname <span class="se">\*</span>.png -o -iname <span class="se">\*</span>.png -maxdepth 1<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">files</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span></code></pre></div><p>Exit if we can’t find any results. No need to continue if we haven’t a thing to upload.</p>
<p>Next, we just iterate through our findings using <code>read</code> and some more <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html">Bash parameter expansion</a> to determine what will be the new emoji’s reference name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">files</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r path<span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl">  <span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="si">${</span><span class="nv">path</span><span class="p">%.*</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>We can easily parse the response bodies with <code>jq</code> for some error checking and we’re good to go:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.ok&#34;</span><span class="k">)</span> <span class="o">==</span> <span class="nb">false</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.error&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>We now have all the information we need to upload each file in bulk. Put it all together and you’ve got a working bulk emoji uploader:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -eo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_DOMAIN</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">files</span><span class="o">=</span><span class="k">$(</span>find . -iname <span class="se">\*</span>.gif -o -iname <span class="se">\*</span>.png -o -iname <span class="se">\*</span>.png -maxdepth 1<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">files</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">files</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r path<span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl">  <span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="s2">&#34;</span><span class="si">${</span><span class="nv">path</span><span class="p">%.*</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nv">result</span><span class="o">=</span><span class="k">$(</span>
</span></span><span class="line"><span class="cl">    curl -s --compressed <span class="s2">&#34;https://</span><span class="si">${</span><span class="nv">SLACK_DOMAIN</span><span class="si">}</span><span class="s2">.slack.com/api/emoji.add&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -H <span class="s1">&#39;content-type: multipart/form-data&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -H <span class="s2">&#34;cookie: d=</span><span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="si">}</span><span class="s2">;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -F <span class="s2">&#34;token=</span><span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -F <span class="s2">&#34;name=</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -F <span class="nv">mode</span><span class="o">=</span>data <span class="se">\
</span></span></span><span class="line"><span class="cl">      -F <span class="s2">&#34;image=@</span><span class="si">${</span><span class="nv">path</span><span class="si">}</span><span class="s2">&#34;</span> 
</span></span><span class="line"><span class="cl">  <span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.ok&#34;</span><span class="k">)</span> <span class="o">==</span> <span class="nb">false</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.error&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;uploaded :</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">:!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>There you go! To test it out yourself, save this as an executable script and make sure your present working directory has some supported images in it. Pass through the your environmental variables when executing and you’re off:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="nv">SLACK_DOMAIN</span><span class="o">=</span>*** <span class="nv">SLACK_COOKIE</span><span class="o">=</span>*** <span class="nv">SLACK_TOKEN</span><span class="o">=</span>*** ./upload.sh
</span></span><span class="line"><span class="cl">uploaded :stonks:!
</span></span><span class="line"><span class="cl">uploaded :boop:!
</span></span><span class="line"><span class="cl">uploaded :derp:!
</span></span><span class="line"><span class="cl">uploaded :booyah:!
</span></span><span class="line"><span class="cl">uploaded :merp-flakes:!
</span></span></code></pre></div><p>Nice! 😊</p>
<h2 id="once-again-something-better">
  <a class="heading-link" href="#once-again-something-better">Once Again, Something Better!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>While perfectly functional, there’s not a lot of flexibility. No error checking, filtering or confirmation checks. If you’re looking for something a bit more fleshed out, <a href="https://github.com/wilhelm-murdoch/slack-emoji-toolkit">look no further</a>!</p>
<h2 id="the-final-piece-">
  <a class="heading-link" href="#the-final-piece-">The Final Piece &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>So far, we’ve gone over how to download and upload large sets of emoji, but what if you want to nuke them from orbit? The final article in this series will cover how to bulk delete while using advanced filters to pin-point specific sets of emoji you wish to remove.</p>
<p>Hope you’ve learned something useful!</p>]]></content:encoded>
    </item>
    <item>
      <title>Liberating Custom Slack Emojis</title>
      <link>https://wilhelm.codes/blog/liberating-custom-slack-emojis/</link>
      <pubDate>Tue, 20 Sep 2022 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/liberating-custom-slack-emojis/</guid>
      <category>emoji</category>
      <category>api</category>
      <category>bash</category>
      <description>As is stupid tradition, whenever I start at a new company, one of the first things I like to do while getting settled in is upload my favourite emojis to whatever real-time messaging platform is in use. I know it’s childish, but silly memes and emojis are great ways for you to break the ice with your new coworkers.&#xA;</description>
      <content:encoded><![CDATA[<p>As is stupid tradition, whenever I start at a new company, one of the first things I like to do while getting settled in is upload my favourite emojis to whatever real-time messaging platform is in use. I know it’s childish, but silly memes and emojis are great ways for you to break the ice with your new coworkers.</p>
<p>But, when it comes time to part ways it’s only understandable to want to gather your stuff for the next place to start the process again. Over the years one can collect and curate quite a nice hoard of stupid images.</p>
<p>I’ve been using Slack prolifically for the past few years. Unfortunately, there is no simple way of exporting your precious collection outside of the old “right-click and save” method. This is effective for smaller collections of a couple dozen or so, but becomes impractical when there are hundreds or even thousands.</p>
<p>And, of course, I’m not alone. There have been a non-zero number of times when someone left the company only to hit me up later asking for an emoji archive.</p>
<p>Yes, this really happens and something <em>must</em> be done about it!</p>
<p>What follows is an unnecessarily long dive into a little bit of reverse engineering one of Slack’s API endpoints, subverting it for our own use and writing a stupid little tool to help ourselves ( and others ) automate this process for the future.</p>
<h2 id="some-investigative-work">
  <a class="heading-link" href="#some-investigative-work">Some Investigative Work<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>There just has to be a straightforward way we can <em>mostly</em> automate this. So, I did some cursory digging around the Chrome Developer Tools console and found this endpoint in the “Network” tab:</p>
<pre tabindex="0"><code>https://edgeapi.slack.com/cache/T0XXXX/emojis/list?fp=97
</code></pre><p>The <code>T0XXXX</code> will be your workspace, or team, id. Make note of it, you’ll need it later.</p>
<p>If you intercept one of these requests and take a peak at the “Response” tab in the “Network” panel, you’ll see a JSON response with a structure similar to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;next_marker&#34;</span><span class="p">:</span> <span class="s2">&#34;a-custom-emoji&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;results&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">		<span class="p">{</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;another-custom-emoji&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">			<span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="s2">&#34;https://emoji.slack-edge.com/T0XXXX/another-custom-emoji/xxxxxxx.png&#34;</span>
</span></span><span class="line"><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="cl">		<span class="err">...</span> <span class="err">more</span> <span class="err">emojis</span> <span class="err">...</span>
</span></span><span class="line"><span class="cl">	<span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><ul>
<li><code>ok</code> tells you whether the request was successful or not. If it returns <code>false</code> there will be an additional field named <code>error</code> that should clue you into what went wrong.</li>
<li><code>next_marker</code> tells you where in the list of custom emoji the <em>next</em> page of results should start. We use this as the value of <code>marker</code> in subsequent request payloads ( see below ). This is effectively how you page through large lists of emoji.</li>
<li><code>results</code> should be obvious, but it contains an array of objects with <code>name</code> and <code>value</code> fields.
<ul>
<li><code>name</code> is the alias of the emoji.</li>
<li><code>value</code> is the direct CDN URL to the emoji.</li>
</ul>
</li>
</ul>
<p>You may see other fields, but <code>next_marker</code> and <code>value</code> are the only fields we really care about.</p>
<p>Next, If you take a look at the “Payload” tab in the “Network” panel for the same request, you’ll see something similar to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;token&#34;</span><span class="p">:</span> <span class="s2">&#34;xoxc-xxxx-xxxx-xxxx&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;count&#34;</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">	<span class="nt">&#34;marker&#34;</span><span class="p">:</span> <span class="s2">&#34;some-other-emoji&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><ul>
<li><code>token</code> is the API request token the Slack client uses to make the emoji list request on your behalf.</li>
<li><code>count</code> is the amount of emojis to return as a page.</li>
<li><code>marker</code> tells the API where in the emoji list to start paging.</li>
</ul>
<p>The final piece of the puzzle is getting the value of your session cookie. You can grab this by clicking the “Request Headers” section under the “Headers” tab. You should see a header labeled <code>cookie</code>.</p>
<p>There’s going to be a fairly large block of text to parse through, but you’re looking for the cookie named <code>d</code>. Save everything between the <code>d=</code> and the closing <code>;</code>. You’re going to need this to authenticate with the Slack API itself.</p>
<h2 id="testing-your-findings">
  <a class="heading-link" href="#testing-your-findings">Testing Your Findings<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>We now understand the structure of our payload. We also have the endpoint to hit as well as our tokens and session cookie to authenticate. Piecing it all together, you can now use cURL to hit the API directly from your terminal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ curl --silent https://edgeapi.slack.com/cache/&lt;team&gt;/emojis/list?fp<span class="o">=</span><span class="m">97</span>
</span></span><span class="line"><span class="cl">  -H <span class="s1">&#39;cookie: d=&lt;cookie&gt;;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --data-raw <span class="s1">&#39;{&#34;token&#34;:&#34;&lt;token&gt;&#34;,&#34;count&#34;:10}&#39;</span>
</span></span></code></pre></div><p>Replace the following:</p>
<ul>
<li><code>&lt;team&gt;</code> is your workspace, or team, id.</li>
<li><code>&lt;cookie&gt;</code> is your session cookie value.</li>
<li><code>&lt;token&gt;</code> is your request token.</li>
</ul>
<p>If everything worked out, you should have successful API response like the one above. If the response has root field named <code>marker_next</code>, make note of the associated value to help page through subsequent results. Keep doing this in a loop until your response no longer returns a <code>marker_next</code> field.</p>
<p>Once that happens, you’re at the end of the list.</p>
<p>With a little bit of help from <code>jq</code> to parse and filter the JSON responses, we can grab a simple list of URLs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ &lt;previous_curl_command&gt; <span class="p">|</span> jq -r <span class="s1">&#39;.results[].value&#39;</span>
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/no/060fca9aa2581a93.png
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/nods/c7fe54342ab5b8b6.gif
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/nods-back/fbd1b5384cdd0905.gif
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/nomnomnom/612cfc74c785010d.gif
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/octocat/627964d7c9.png
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/ohyou/6a352df984d9c076.gif
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/one-sec-cooking/e749627cb088859d.png
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/oof/db94710445cbb206.png
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/petrol/35ed3db238f795fc.png
</span></span><span class="line"><span class="cl">https://emoji.slack-edge.com/T0XXXX/piggy/b7762ee8cd.png
</span></span></code></pre></div><p>We now understand how to collect everything we need, so let’s automate this a bit more.</p>
<h2 id="the-implementation">
  <a class="heading-link" href="#the-implementation">The Implementation<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>We’re going to write a little script that’ll download all the custom emojis from the target Slack workspace. It’s going to perform the following steps:</p>
<ol>
<li>Define a starting JSON payload.</li>
<li>Create a run loop.</li>
<li>With each loop we make a request using the API endpoint.</li>
<li>Do some error checking.</li>
<li>Iterate through the results and save them to disk.</li>
<li>If the results contain a <code>next_marker</code> update the JSON payload with a <code>marker</code> field and continue the run loop.</li>
<li>Keep going until the last request no longer provides a <code>next_marker</code>.</li>
</ol>
<p>First, we want to define some environmental variables so we can easily configure the script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_WORKSPACE_ID</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="p">:=</span><span class="si">}</span>
</span></span></code></pre></div><p>Now we define our JSON payload object. We use <code>jq</code> here to keep things easy. Not only is it great for parsing and filtering JSON in Bash, it also makes building and manipulating objects simple:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">payload</span><span class="o">=</span><span class="k">$(</span>
</span></span><span class="line"><span class="cl">  jq -nc <span class="se">\
</span></span></span><span class="line"><span class="cl">    --arg     token <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --argjson count <span class="s2">&#34;100&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">      token: $token, 
</span></span></span><span class="line"><span class="cl"><span class="s1">      count: $count
</span></span></span><span class="line"><span class="cl"><span class="s1">    }&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">)</span>
</span></span></code></pre></div><p>Next, is the run loop where we make our API requests. This passes through the payload object we just defined while also targeting our workspace:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nv">result</span><span class="o">=</span><span class="k">$(</span>
</span></span><span class="line"><span class="cl">    curl -s --compressed <span class="s2">&#34;https://edgeapi.slack.com/cache/</span><span class="si">${</span><span class="nv">SLACK_WORKSPACE_ID</span><span class="si">}</span><span class="s2">/emojis/list?fp=97&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -H <span class="s2">&#34;cookie: d=</span><span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="si">}</span><span class="s2">;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      --data-raw <span class="s2">&#34;</span><span class="si">${</span><span class="nv">payload</span><span class="si">}</span><span class="s2">&#34;</span> 
</span></span><span class="line"><span class="cl">  <span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>We obviously want to do some simple error checking so we know what we did wrong if the requests fail. Grab the error, spit it out and terminate the script.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.ok&#34;</span><span class="k">)</span> <span class="o">==</span> <span class="nb">false</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.error&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p>We now start iterating through any of the results we got from the initial request. We really only care about the <code>value</code> field as the URL gives us all the pieces we need to give our emojis a human-readable filename.</p>
<p>Here we treat the resulting URL as a standard file path and use <code>dirname</code> paired with <code>basename</code> to get the name of the parent directory of the file. The parent directory holds the actual name of the emoji, while the file itself is only a random hash. We use <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html">Bash’s parameter expansion</a> feature to construct our filename.</p>
<p>Finally, we download the remote file to the current directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span>  <span class="p">|</span> jq -r <span class="s1">&#39;.results[].value&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r url<span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl"><span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="si">${</span><span class="nv">url</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">))</span>
</span></span><span class="line"><span class="cl">curl -s -o <span class="s2">&#34;</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">.</span><span class="si">${</span><span class="nv">url</span><span class="p">##*.</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">url</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span> 
</span></span></code></pre></div><p>We need to check if there are more results to page through, so let’s look for the <code>marker_next</code> field. If we can’t find one, then mission accomplished; all done. However, if we do get a value, we take the previous payload we constructed and <em>add</em> the <code>marker</code> field.</p>
<p>The next loop will pick this change up and pass it on through to the API request telling Slack to give us the next page of precious emojis.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">marker</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.next_marker&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">marker</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;null&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">payload</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">payload</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq --arg marker <span class="s2">&#34;</span><span class="si">${</span><span class="nv">marker</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s1">&#39;. + { marker: $marker }&#39;</span><span class="k">)</span>
</span></span></code></pre></div><h2 id="the-finished-product">
  <a class="heading-link" href="#the-finished-product">The Finished Product<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -eo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_WORKSPACE_ID</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">: <span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="p">:=</span><span class="si">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">payload</span><span class="o">=</span><span class="k">$(</span>
</span></span><span class="line"><span class="cl">  jq -nc <span class="se">\
</span></span></span><span class="line"><span class="cl">    --arg     token <span class="s2">&#34;</span><span class="si">${</span><span class="nv">SLACK_TOKEN</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    --argjson count <span class="s2">&#34;100&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">      token: $token, 
</span></span></span><span class="line"><span class="cl"><span class="s1">      count: $count
</span></span></span><span class="line"><span class="cl"><span class="s1">    }&#39;</span>
</span></span><span class="line"><span class="cl"><span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">while</span> true<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nv">result</span><span class="o">=</span><span class="k">$(</span>
</span></span><span class="line"><span class="cl">    curl -s --compressed <span class="s2">&#34;https://edgeapi.slack.com/cache/</span><span class="si">${</span><span class="nv">SLACK_WORKSPACE_ID</span><span class="si">}</span><span class="s2">/emojis/list?fp=97&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      -H <span class="s2">&#34;cookie: d=</span><span class="si">${</span><span class="nv">SLACK_COOKIE</span><span class="si">}</span><span class="s2">;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">      --data-raw <span class="s2">&#34;</span><span class="si">${</span><span class="nv">payload</span><span class="si">}</span><span class="s2">&#34;</span> 
</span></span><span class="line"><span class="cl">  <span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> <span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.ok&#34;</span><span class="k">)</span> <span class="o">==</span> <span class="nb">false</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.error&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span>  <span class="p">|</span> jq -r <span class="s1">&#39;.results[].value&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r url<span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl">    <span class="nv">name</span><span class="o">=</span><span class="k">$(</span>basename <span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="si">${</span><span class="nv">url</span><span class="si">}</span><span class="s2">&#34;</span><span class="k">))</span>
</span></span><span class="line"><span class="cl">    curl -s -o <span class="s2">&#34;</span><span class="si">${</span><span class="nv">name</span><span class="si">}</span><span class="s2">.</span><span class="si">${</span><span class="nv">url</span><span class="p">##*.</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">url</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span> 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nv">marker</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">result</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq -r <span class="s2">&#34;.next_marker&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">[[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">marker</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;null&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nv">payload</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">payload</span><span class="si">}</span><span class="s2">&#34;</span> <span class="p">|</span> jq --arg marker <span class="s2">&#34;</span><span class="si">${</span><span class="nv">marker</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s1">&#39;. + { marker: $marker }&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>This is pretty much it. A barebones script that downloads all findings to its present working directory. To test it out yourself, make the script executable, define the environmental variables and fire it off like so:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="nv">SLACK_WORKSPACE_ID</span><span class="o">=</span>*** <span class="nv">SLACK_COOKIE</span><span class="o">=</span>*** <span class="nv">SLACK_TOKEN</span><span class="o">=</span>*** ./fetch.sh
</span></span></code></pre></div><h2 id="an-even-better-version">
  <a class="heading-link" href="#an-even-better-version">An Even Better Version!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I know I just made you read through all this, but you also could’ve just <a href="https://github.com/wilhelm-murdoch/slack-emoji-toolkit">downloaded the tool from here</a>. It’s got heaps more bells and whistles ( if you’re into that sort of thing):</p>
<h2 id="the-bitter-irony">
  <a class="heading-link" href="#the-bitter-irony">The Bitter Irony<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>In typical bored engineer fashion, I’ve spent more time writing this article, documenting and publishing the code than I ever would have personally just “handraulically” doing this from time to time.</p>
<p>Hopefully, you’ve learned something new! 🤞</p>
<h2 id="bonus-relevant-xkcd-image">
  <a class="heading-link" href="#bonus-relevant-xkcd-image">Bonus Relevant XKCD Image<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p><img src="/blog/liberating-custom-slack-emojis/image-1.png" alt="">
source: <a href="https://xkcd.com/1319/">xkcd: automation</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Save Money by Keeping Your AWS Account Clean</title>
      <link>https://wilhelm.codes/blog/save-money-by-keeping-your-aws-account-clean/</link>
      <pubDate>Tue, 06 Sep 2022 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/save-money-by-keeping-your-aws-account-clean/</guid>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>cost-optimisation</category>
      <description>At a prior role, I managed just under 20 AWS accounts. Their uses varied from production workloads, to dedicated CI/CD environments, sandboxed areas for our engineers to experiment in, log aggregation, the list goes on.&#xA;</description>
      <content:encoded><![CDATA[<p>At a prior role, I managed just under 20 AWS accounts. Their uses varied from production workloads, to dedicated CI/CD environments, sandboxed areas for our engineers to experiment in, log aggregation, the list goes on.</p>
<p>Because we had so many to manage and not a whole lot of people power ( it was just 2 of us ), it got easy for a lot of hidden costs to pop up here and there if we weren&rsquo;t vigilant enough with housekeeping.</p>
<p>We liked to keep roughly 95% of <em>all</em> static infrastructure nicely tucked away in Terraform, but over the years you get so many small extant changes that the dirt begins to pile up and, along with it, costs. Just a little bit here and there, but the cumulative effect becomes more and more obvious as time moves on.</p>
<p>So, what do you do? Do you manually audit every account? Every <em>region</em> in every account? AWS doesn’t give you an easy way to view <em>all</em> resources across the entirety of your org outside of what you can glean from the billing console. So, we do what comes natural; find a way to make the glowing rectangle do the job for you.</p>
<p>The goal here isn&rsquo;t to find a solution that does <em>everything</em>, but just enough to make our jobs easier.</p>
<h2 id="enter-aws-nuke">
  <a class="heading-link" href="#enter-aws-nuke">Enter AWS Nuke<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I found <a href="https://github.com/rebuy-de/aws-nuke">this utility</a> sometime last year when asked to find a way to trim some extra fat off our monthly bill. It is <em>excellent</em> and works precisely as advertised. It wound up only saving us a few hundred bucks per month, which is a drop in the bucket considering our total monthly spend, but saving money isn’t the only benefit.</p>
<h3 id="added-security">
  <a class="heading-link" href="#added-security">Added Security<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Cleaning up unused resources is a low touch way to keeping up your security posture. Fewer forgotten things, means a smaller attack surface.</p>
<p>The longer you leave something unchecked, the harder it becomes to manage. Entropy affects everything and while code can be immutable, the world acting upon it is not.</p>
<p>Think back and ask yourself how many times you’ve forgotten about a service with ageing dependencies, or an EC2 instance you haven’t patched in a while, or some critical, but undocumented, component stored away in some forgotten area of one of your accounts.</p>
<p>If you don’t need it, or don’t plan on actively maintaining it, find a way to get rid of it.</p>
<h3 id="soft-dollar-cost">
  <a class="heading-link" href="#soft-dollar-cost">Soft-Dollar Cost<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>People tend to forget the hidden cost of maintaining lots of things; it requires human attention. Have a lot of random things you need to manage? Well, the more things there are the more time you spend on them. That literally translates to money spent at the end of the billing period and it won&rsquo;t be showing up on the invoice.</p>
<p>Wouldn’t you rather spend your time on more important things at work? Reduce hidden costs by cleaning up after yourself.</p>
<h2 id="ok-lets-nuke-some-stuff">
  <a class="heading-link" href="#ok-lets-nuke-some-stuff">Ok, Let&rsquo;s Nuke Some Stuff<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>First things first, determine the account id you&rsquo;re going to use.</p>
<p>This is a highly-destructive operation, so make sure you&rsquo;re targeting the right account. Get yourself some credentials either through privileged IAM or an SSO user session. Assume from here on out that <code>11111111111</code> is your target account and ensure your user has administrative privileges. You&rsquo;ll need this level of authorization if you&rsquo;re going to be doing this kind of deep cleaning.</p>
<p>Ensure your credentials are associated with the proper account by doing something similar to the following. As a side note, if you&rsquo;re using a credentials sourced by SSO, you&rsquo;re going to need the <code>AWS_SESSION_TOKEN</code>, otherwise it&rsquo;s safe to ignore.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_ACCESS_KEY_ID</span><span class="o">=</span><span class="s2">&#34;***&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_SECRET_ACCESS_KEY</span><span class="o">=</span><span class="s2">&#34;***&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">AWS_SESSION_TOKEN</span><span class="o">=</span><span class="s2">&#34;***&#34;</span>
</span></span><span class="line"><span class="cl">aws sts get-caller-identity
</span></span><span class="line"><span class="cl"><span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;UserId&#34;</span>: <span class="s2">&#34;XXXXXXXXXXXXXXXX:DEADBEEF&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Account&#34;</span>: <span class="s2">&#34;11111111111&#34;</span>,
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;Arn&#34;</span>: <span class="s2">&#34;arn:aws:sts::11111111111:assumed-role/AWSReservedSSO_AdministratorAccess/DEADBEEF&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span></code></pre></div><h2 id="configuration">
  <a class="heading-link" href="#configuration">Configuration<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>There may be resources you wish to keep while cleaning your account. AWS Nuke exposes comprehensive filtering capabilities that allow you to ignore, or target, specific sets of resources. In our case, it would be stuff like GuardDuty, AWS Config, CloudTrail and S3 buckets dedicated to Terraform state storage. For resources we don&rsquo;t want to wipe, the following configuration provides a basic example of how you may bootstrap the cleaning process.</p>
<p>Ultimately, this will be highly specific to your unique use case, but it&rsquo;s a great way to demonstrate how it all works. Let&rsquo;s go through all the important sections:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">regions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">global</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">eu-north-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ap-south-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">eu-west-3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">eu-west-2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">eu-west-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ap-northeast-3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ap-northeast-2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ap-northeast-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">sa-east-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ca-central-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ap-southeast-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">ap-southeast-2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">eu-central-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">us-east-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">us-east-2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">us-west-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">us-west-2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">account-blocklist</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="m">12345678910</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">resource-types</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">excludes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">CloudWatchAlarm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">Route53ResolverRule</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">Route53HostedZone</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">S3Object</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">S3Bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">GuardDutyDetector</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">SNSSubscription</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">IAMSAMLProvider</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">CloudTrailTrail</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">accounts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">11111111111</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">filters</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">IAMPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">AWS-Chatbot-NotificationsOnly-Policy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">IAMRolePolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">CloudTrailCloudWatchLogsRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">OrganizationAccountAccessRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">IAMRole</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">AWSReservedSSO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">CloudWatchAlarmToSlackRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">CloudTrailCloudWatchLogsRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">OrganizationAccountAccessRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">IAMRolePolicyAttachment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">AWSReservedSSO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">CloudWatchAlarmToSlackRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SNSTopic</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">contains</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">CloudWatchAlarms</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">CloudWatchLogsLogGroup</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="l">CloudTrail/DefaultLogGroup</span><span class="w">
</span></span></span></code></pre></div><h4 id="regions">
  <a class="heading-link" href="#regions"><code>regions</code><span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h4>
<p>Lists the AWS regions to screen for resources. By default, we allow the tool to cycle through all supported regions. Unfortunately, there is no support for &ldquo;all regions&rdquo;, so this is a list that will have to be manually maintained. Also, depending on the amount of resources, or use-case for this tool, you may not want to scan all regions. Trim this list to suit your purposes.</p>
<p><code>global</code> refers to <em>services</em> which are considered global and not tied to any specific region. For example, IAM users, policies and roles.</p>
<h4 id="account-blocklist">
  <a class="heading-link" href="#account-blocklist"><code>account-blocklist</code><span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h4>
<p>A list of AWS accounts to completely avoid. If you&rsquo;re not confident in your account selections, this is a great place to put accounts that run production workloads as an added failsafe. Remember, be explicit in your selections and use this tool <em>only</em> in accounts you wish to clean.</p>
<h4 id="resource-typesexcludes">
  <a class="heading-link" href="#resource-typesexcludes"><code>resource-types.excludes</code><span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h4>
<p>A list of AWS resources we wish to ignore. For us, these are typically associated with the resources we&rsquo;ve primed using Terraform. In this case, we just skip them completely. Trim this list to suit your purposes, or just manually delete these resources in the AWS console if you&rsquo;re unsure.</p>
<h4 id="accounts">
  <a class="heading-link" href="#accounts"><code>accounts</code><span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h4>
<p>Contains the id of the account you wish to clean. Within this block, you will see filters used to skip certain resources. In our case, the filter almost always contains a list of things we wish to keep in all accounts. Resources like CloudWatch alarms, GuardDuty and AWS Config settings, etc&hellip;</p>
<p>You can read more about configuring <code>aws-nuke</code> in the associated repository&rsquo;s <a href="https://github.com/rebuy-de/aws-nuke/blob/main/README.md">README</a>. Feel free to customise this file locally to suit your own needs. There are a number of ways to filter and target resources with a high degree of precision.</p>
<h2 id="using-the-tool">
  <a class="heading-link" href="#using-the-tool">Using the Tool<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>By default, <code>aws-nuke</code> does not perform any destructive operations. You must explicitly add the <code>--no-dry-run</code> flag with a subsequent run after performing the initial scan of the target account.</p>
<p>You can install the tool locally on your machine, but I prefer using Docker to invoke the tool:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -it <span class="se">\
</span></span></span><span class="line"><span class="cl">    -v ~/aws-nuke-config.yml:/home/aws-nuke/config.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">    -e AWS_ACCESS_KEY_ID <span class="se">\
</span></span></span><span class="line"><span class="cl">    -e AWS_SECRET_ACCESS_KEY <span class="se">\
</span></span></span><span class="line"><span class="cl">    -e AWS_SESSION_TOKEN <span class="se">\
</span></span></span><span class="line"><span class="cl">    quay.io/rebuy/aws-nuke:main <span class="se">\
</span></span></span><span class="line"><span class="cl">    --config /home/aws-nuke/config.yml
</span></span></code></pre></div><p>You will be asked to manually type the alias of the target account for confirmation. For our purposes, the alias for account <code>11111111111</code> is <code>aws-nuke-account</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">aws-nuke version v2.19.0.15.gb46fbe0 - Wed Oct  5 10:01:13 UTC 2022 - b46fbe0e9f63266f56b5afd9635b4e4d5a3108d4
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Do you really want to nuke the account with the ID 11111111111 and the alias &#39;aws-nuke-account&#39;?
</span></span><span class="line"><span class="cl">Do you want to continue? Enter account alias to continue.
</span></span><span class="line"><span class="cl">&gt; aws-nuke-account
</span></span></code></pre></div><p>You will then see a list of AWS resources as <code>aws-nuke</code> cycles through each region:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">global - IAMRole - RDSBucketAccessRole - [Name: &#34;RDSBucketAccessRole&#34;, Path: &#34;/service-role/&#34;] - filtered by config
</span></span><span class="line"><span class="cl">global - IAMRole - S3BucketAccessRole - [Name: &#34;S3BucketAccessRole&#34;, Path: &#34;/&#34;] - would remove
</span></span><span class="line"><span class="cl">global - IAMRole - EBSSnapshotReplicationRole - [Name: &#34;EBSSnapshotReplicationRole&#34;, Path: &#34;/&#34;] - would remove
</span></span><span class="line"><span class="cl">global - IAMRole - VPCFlowLogsRole - [Name: &#34;VPCFlowLogsRole&#34;, Path: &#34;/&#34;] - would remove
</span></span><span class="line"><span class="cl">global - IAMRole - OrganizationAccountAccessRole - [Name: &#34;OrganizationAccountAccessRole&#34;, Path: &#34;/&#34;] - filtered by config
</span></span><span class="line"><span class="cl">us-west-2 - EC2RouteTable - rtb-0000000000 - [tag:ManagedBy: &#34;terraform&#34;, tag:Name: &#34;VPCRouteTablePublic&#34;, tag:Purpose: &#34;vpc-route-table&#34;] - would remove
</span></span><span class="line"><span class="cl">us-west-2 - EC2RouteTable - rtb-1111111111 - [tag:ManagedBy: &#34;terraform&#34;, tag:Name: &#34;VPCRouteTablePrivate&#34;, tag:Purpose: &#34;vpc-route-table&#34;] - would remove
</span></span><span class="line"><span class="cl">Scan complete: 161 total, 83 nukeable, 78 filtered.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The above resources would be deleted with the supplied configuration. Provide --no-dry-run to actually destroy resources.
</span></span></code></pre></div><p>It is important during the dry run that you closely-examine each resource that has been flagged for removal. Here are some common flags you should be paying attention to:</p>
<ol>
<li><code>would remove</code>: these resources will be removed by AWS Nuke on a subsequent run with <code>--no-dry-run</code>.</li>
<li><code>filtered by config</code>: these are resources that have been skipped as a result of your filtering configurations.</li>
<li><code>cannot delete *</code>: marks AWS-managed resources that simply cannot be deleted.</li>
</ol>
<p>If you notice something is marked as <code>would remove</code>, but should be kept, add a filter for it to the configuration file. Take an iterative approach until you&rsquo;re satisfied with the scanning results.</p>
<p>Once you have scanned the list and updated any relevant filters, you can execute the nuking process by passing the <code>--no-dry-run</code> flag. You will be presented with the same steps as above as well as a final confirmation message similar to the initial dry run scan.</p>
<h2 id="in-conclusion-">
  <a class="heading-link" href="#in-conclusion-">In Conclusion &hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>This is a very destructive process that will permanently remove selected resources. Always make sure you have backups of selected data sources and they are properly-filtered within your configuration file.</p>
<p>There may also be times when resources just aren&rsquo;t deleted as expected. That&rsquo;s ok as this process isn&rsquo;t 100% perfect. AWS Nuke is still under heavy development and AWS updates APIs around various services constantly. The point isn&rsquo;t for this tool to do 100% of the heavy lifting, but to get you mostly there.</p>
<p>90% complete still means you&rsquo;ve successfully avoided 90% of the work. 😊</p>
<h2 id="100-completion-speed-run">
  <a class="heading-link" href="#100-completion-speed-run">100% Completion Speed-Run<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Just stop paying the bill. Seriously. Just stop paying for the account. Cancel the card. Delete those billing alerts and just walk away. If you stop paying, AWS will eventually just tear down your account with everything in it. All of it. The entire account. Credit score be damned!</p>
<p><em>obviously, do not do this&hellip; Or, go nuts! After all, don&rsquo;t you sometimes wish you could burn it all down and just walk away? j/k</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1664982128441/xnlblxy1F.gif" alt="burn-koala.gif"></p>
<p>Hope you&rsquo;ve learned something useful!</p>
<p>Behave! (mostly) 🙉🙈🙊</p>]]></content:encoded>
    </item>
    <item>
      <title>Deleting Massive S3 Buckets the Easy Way</title>
      <link>https://wilhelm.codes/blog/deleting-massive-s3-buckets-the-easy-way/</link>
      <pubDate>Sat, 04 Dec 2021 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/deleting-massive-s3-buckets-the-easy-way/</guid>
      <category>aws</category>
      <category>devops</category>
      <category>cost-optimisation</category>
      <description>I&amp;rsquo;m in the middle of decommissioning a service at the moment and I have to do the typical process of performing final snapshots and cleaning up extant resources. It&amp;rsquo;s incredibly tedious, so let&amp;rsquo;s walk through it.&#xA;</description>
      <content:encoded><![CDATA[<p>I&rsquo;m in the middle of decommissioning a service at the moment and I have to do the typical process of performing final snapshots and cleaning up extant resources. It&rsquo;s incredibly tedious, so let&rsquo;s walk through it.</p>
<p>For this particular service, we have an  <a href="https://en.wikipedia.org/wiki/Extract,_transform,_load">ETL</a>  process that stores raw data points for downstream ingestion by some other&hellip; thing. Anyway, the storage mechanism for this is a cross-region replicated S3 bucket containing just over 10 <em>million</em> objects.</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-1.png" alt=""></p>
<p>All associated buckets and objects must be deleted. 😬</p>
<p>Have you ever had to delete a bucket with this many objects? Not exactly straight-forward. Even by AWS standards.</p>
<h2 id="whats-the-problem">
  <a class="heading-link" href="#whats-the-problem">What&rsquo;s the problem?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>As you may or may not know, you can&rsquo;t delete a bucket that still contains objects. For smaller buckets, this isn&rsquo;t much of an issue as you can saunter on over to the &ldquo;empty bucket&rdquo; screen. This will take you to a confirmation interstitial where you have to sit and wait until the deletion process finishes. Though, you had better not close that tab unless you want to start the process all over.</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-2.png" alt=""></p>
<p>Not exactly a feasible solution for our use case. No, for larger buckets there really is only one pragmatic solution.</p>
<h2 id="lifecycle-policies">
  <a class="heading-link" href="#lifecycle-policies">Lifecycle Policies!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>You may have noticed the tone of this article as being somewhat snarky. Well, that&rsquo;s because what follows shouldn&rsquo;t be nearly as convoluted as it is. This process should be a single button with a confirmation step where the work is done asynchronously.</p>
<p>That being said, I know better than to question another team&rsquo;s design decisions. Who knows what weirdness they encountered during implementing this functionality. In all fairness, they do provide the mechanisms necessary drop millions, or even billions, of objects. The main concern is it&rsquo;s just not at all intuitive.</p>
<p>So, let&rsquo;s get to it! 🦾</p>
<h3 id="expire-those-objects">
  <a class="heading-link" href="#expire-those-objects">Expire those objects!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Alright, we&rsquo;re going to have to perform the following steps for the first policy:</p>
<ol>
<li>Expire all current objects.</li>
<li>Permanently delete all non-current versions of objects.</li>
<li>Delete expired object delete markers.</li>
</ol>
<p>First and foremost, give this policy and name and confirm your intent:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-3.png" alt=""></p>
<p>Select the following actions. We will need to select the last option in the list, but due to conflicting settings you will have to add it in separate, secondary, lifecycle policy:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-4.png" alt=""></p>
<p>Set the following option to <code>1</code> day:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-5.png" alt=""></p>
<p>Again, set this option to <code>1</code> day as well. Unfortunately, this is the lowest value you can select. You also have the option to specify how many versions of each object to keep. This is only really relevant if you have object versioning activated for this bucket, but you&rsquo;re going do nuke it all, so just go ahead and leave this blank:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-6.png" alt=""></p>
<p>Look at you! All done except looking over your work and saving your new lifecycle policy:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-7.png" alt=""></p>
<h3 id="delete-those-objects">
  <a class="heading-link" href="#delete-those-objects">Delete those objects!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>A secondary policy is necessary to <em>delete</em> the objects you&rsquo;ve just told AWS to expire. Begin the process by clicking the <code>Create lifecycle rule</code> button and performing the first step from the previous policy:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-8.png" alt=""></p>
<p>Next, you&rsquo;ll select the last action in the list:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-9.png" alt=""></p>
<p>Select the options to delete all expired objects. Optionally, you can also delete impartial uploads older than <code>1</code> day as well.</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-10.png" alt=""></p>
<p>And that&rsquo;s it! Click <code>Save</code> and view the summary screen. It should look similar to this:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-11.png" alt=""></p>
<h2 id="and-now-we-wait">
  <a class="heading-link" href="#and-now-we-wait">And now&hellip; we wait.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>It will probably take an additional day for all pre-existing objects to be marked as &ldquo;expired&rdquo;. Next, AWS will trigger the lifecycle policies at 12am UTC. Depending on the size of your bucket, it could possibly take several days to empty. Just go on an extended ☕️ break.</p>
<p><em>Something to keep in mind is that you will not be charged for storage that has been marked as expired while AWS empties the associated bucket.</em></p>
<h2 id="and-thats-that">
  <a class="heading-link" href="#and-thats-that">And that&rsquo;s that!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Look, this works. But, it isn&rsquo;t exactly intuitive. In my opinion, you shouldn&rsquo;t have to google &ldquo;how to empty large S3 bucket&rdquo; or even go out of your way to create these policies. Ideally, this would all be neatly abstracted away from you and handled with a single button click and a confirmation interstitial ( one that you can walk away from ).</p>
<p>Anyway, it is what it is. I&rsquo;ll keep checking back over the next few days to chase up its progress.</p>
<p>Hope this helped! Happy hacking! 🤘</p>
<h2 id="several-days-later">
  <a class="heading-link" href="#several-days-later">Several days later&hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Ok, it&rsquo;s been a few days and we&rsquo;re finally seeing progress on one of the buckets in question:</p>
<p><img src="/blog/deleting-massive-s3-buckets-the-easy-way/image-12.png" alt=""></p>
<p>All it takes is a little bit of patience.</p>]]></content:encoded>
    </item>
    <item>
      <title>Messing Around with TXT Records</title>
      <link>https://wilhelm.codes/blog/messing-around-with-txt-records/</link>
      <pubDate>Thu, 11 Nov 2021 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/messing-around-with-txt-records/</guid>
      <category>dns</category>
      <category>devops</category>
      <category>networking</category>
      <description>If you&amp;rsquo;ve spent any time in an ops-related position, you&amp;rsquo;ve had to add these records when using custom domain names while integrating with some 3rd-party service. Especially, if you&amp;rsquo;ve ever wanted to configure an email service provider, like Google&amp;rsquo;s Gmail, to appear as if you&amp;rsquo;re sending emails from a personal domain name.&#xA;</description>
      <content:encoded><![CDATA[<p>If you&rsquo;ve spent any time in an ops-related position, you&rsquo;ve had to add these records when using custom domain names while integrating with some 3rd-party service. Especially, if you&rsquo;ve ever wanted to configure an email service provider, like Google&rsquo;s Gmail, to appear as if you&rsquo;re sending emails from a personal domain name.</p>
<h2 id="wanna-see-a-neat-trick">
  <a class="heading-link" href="#wanna-see-a-neat-trick">Wanna see a neat trick?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Open up your terminal of choice and invoke this <code>dig</code> command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig txt contact.wilhelm.codes +short
</span></span></code></pre></div><p>You should see, hopefully 🤞, the following block of text as a response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&#34;twitter: @wilhelm&#34;
</span></span><span class="line"><span class="cl">&#34;github:  wilhelm-murdoch&#34;
</span></span><span class="line"><span class="cl">&#34;keybase: 7F89 5036 2816 06F6&#34;
</span></span><span class="line"><span class="cl">&#34;blog:    https://wilhelm.codes/&#34;
</span></span></code></pre></div><h2 id="what-youre-looking-at">
  <a class="heading-link" href="#what-youre-looking-at">What you&rsquo;re looking at.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Simply put, you&rsquo;re looking at my contact details using a simple DNS <code>TXT</code> record lookup. There are <a href="https://dns.google/query?name=contact.wilhelm.codes">heaps</a> <a href="https://toolbox.googleapps.com/apps/dig/#TXT/">of</a> <a href="https://www.diggui.com/">online tools</a> out there to help you directly query DNS records, but <code>dig</code> tends to be the most common utility included included in most Linux distributions as well as MacOS.</p>
<h2 id="why-might-you-do-this">
  <a class="heading-link" href="#why-might-you-do-this">Why might you do this?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Originally, <code>TXT</code> records were meant to allow DNS administrators to attach simple plain text human-readable &ldquo;notes&rdquo; to their zones. A bit later, it became common practice to use this flexible record type as a way to verify ownership of a domain.</p>
<p>If you&rsquo;ve spent any time in an ops-related position, you&rsquo;ve had to add these records when using custom domain names while integrating with some 3rd-party service. Especially, if you&rsquo;ve ever wanted to configure an email service provider, like Google&rsquo;s Gmail, to appear as if you&rsquo;re sending emails from a personal domain name.</p>
<p>So, why not also create personal, human-readable way of declaring ownership over a domain as well? Like a globally-accessible low-tech &ldquo;business card&rdquo;.</p>
<p>DNS is effectively a globally-distributed, (mostly) fault-tolerant, eventually-consistent key / value store. These details can be easily looked up and verified by anyone with an Internet connection.</p>
<p>And as for &ldquo;Why?&rdquo;, why not?</p>
<h2 id="heres-you-would-implement-it">
  <a class="heading-link" href="#heres-you-would-implement-it">Here&rsquo;s you would implement it.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Honestly, there&rsquo;s nothing special about how to implement this. It&rsquo;s just another record. In my case, I ended up creating a separate <code>TXT</code> record <em>per</em> contact detail. Depending on which server software your DNS provider is using, you may or may not be able to create multi-line text blocks. So, your safest bet is just multiple one-liners attached to the same <code>CNAME</code>.</p>
<p>I recently migrated my zone for <code>wilhelm.codes</code> from <a href="https://www.vultr.com/">Vultr</a> to <a href="https://www.cloudflare.com/">CloudFlare</a> and this is pretty much what it all looks like:</p>
<p><img src="/blog/messing-around-with-txt-records/image-1.png" alt=""></p>
<p>Obviously, this interface will be different if you use a different service, but that&rsquo;s all there is to it!</p>
<h2 id="thats-it">
  <a class="heading-link" href="#thats-it">That&rsquo;s it!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Yep. This isn&rsquo;t groundbreaking stuff and I&rsquo;m surely not the first person to do something like this. Just a bit of (mostly) harmless DNS fun!</p>]]></content:encoded>
    </item>
    <item>
      <title>Falsifying Github Participation Graphs for Fun &amp; Profit</title>
      <link>https://wilhelm.codes/blog/falsifying-github-participation-graphs-for-fun-and-profit/</link>
      <pubDate>Thu, 04 Nov 2021 00:00:00 +0000</pubDate>
      <author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author>
      <guid>https://wilhelm.codes/blog/falsifying-github-participation-graphs-for-fun-and-profit/</guid>
      <category>bash</category>
      <category>git</category>
      <description>If you&amp;rsquo;ve spent any time job hunting in the tech industry, you&amp;rsquo;ve had to deal with recruiters. I like to joke around with my peers about how they&amp;rsquo;re a necessary evil, but in all seriousness, great recruiters are worth their weight in gold. However &amp;hellip;&#xA;</description>
      <content:encoded><![CDATA[<p>If you&rsquo;ve spent any time job hunting in the tech industry, you&rsquo;ve had to deal with recruiters. I like to joke around with my peers about how they&rsquo;re a necessary evil, but in all seriousness, great recruiters are worth their weight in gold. However &hellip;</p>
<p>But, good or bad, far too many of them will pass over your resume for the wildest reasons. We&rsquo;re not here to go over all of them. No, today we&rsquo;re going to address the somewhat recent trend of glancing at your public Github profile page and grading you based on how many green squares show up in your participation graph.</p>
<h2 id="whats-your-point">
  <a class="heading-link" href="#whats-your-point">What&rsquo;s your point?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Like basing your performance on LoC, using &ldquo;commits made&rdquo; as a recruiting metric is beyond useless. There can be any number of reasons why your graph isn&rsquo;t as populated as they&rsquo;d like:</p>
<ol>
<li>You work on private repositories and you haven&rsquo;t configured your profile to show anonymised private contributions.</li>
<li>You have an alternate account from your personal one to keep work and private life separate. Or, maybe it&rsquo;s just company policy.</li>
<li>You&rsquo;ve signed an NDA and can&rsquo;t publish your contributions publicly.</li>
<li>You set personal boundaries for your work-life balance and don&rsquo;t care to stare at glowing rectangles in your free time.</li>
<li>Or, maybe you&rsquo;ve just had a slow year? 🤷</li>
</ol>
<p>Personally, I have <em>thousands</em> of commits that will never see the light of day for some of the ☝️ points. If I&rsquo;m out job hunting, I would hate to be passed over because I don&rsquo;t tick some mundane box on an arbitrary checklist.</p>
<h2 id="what-to-do">
  <a class="heading-link" href="#what-to-do">What to do?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Well, we light up that graph like it&rsquo;s a switch board, of course. Make this information utterly useless. What do they care if your profile says you&rsquo;ve had <code>2,345</code> commits in the last week? Maybe you did. If it&rsquo;s 🟩 , they cross it off their checklist and move on to something else.</p>
<h2 id="ok-show-me">
  <a class="heading-link" href="#ok-show-me">Ok, show me.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Crack your knuckles and get ready to do some coding. For this little project, we&rsquo;ll be doing some simple Bash scripting. I&rsquo;m currently working on MacOS, but this code should be fairly portable on other Linux-based distributions.</p>
<p>You will, naturally, need a Github account. Let&rsquo;s start there.</p>
<h3 id="configure-your-participation-graph">
  <a class="heading-link" href="#configure-your-participation-graph">Configure Your Participation Graph<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Did you know you can configure your participation graph to display anonymised private commit statistics? Ensure that setting is ✅ and move on to the next step.</p>
<p><img src="/blog/falsifying-github-participation-graphs-for-fun-and-profit/image-1.png" alt=""></p>
<h3 id="new-private-repository">
  <a class="heading-link" href="#new-private-repository">New Private Repository<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Let&rsquo;s create a new <em>empty</em> repository entitled <code>graph-participation</code> and make sure it&rsquo;s private.</p>
<p><img src="/blog/falsifying-github-participation-graphs-for-fun-and-profit/image-2.png" alt=""></p>
<h3 id="checkout-local-working-copy">
  <a class="heading-link" href="#checkout-local-working-copy">Checkout Local Working Copy<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Follow the post-setup instructions Github gives you to check out a local working copy of your new 🔒 repository!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Let&#39;s goooooooo!&#34;</span> &gt;&gt; README.md
</span></span><span class="line"><span class="cl">git init
</span></span><span class="line"><span class="cl">git add README.md
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;The first and final legitimate commit in this repository ...&#34;</span>
</span></span><span class="line"><span class="cl">git branch -M main
</span></span><span class="line"><span class="cl">git remote add origin git@github.com:&lt;account&gt;/github-participation.git
</span></span><span class="line"><span class="cl">git push -u origin main
</span></span></code></pre></div><p>Of course, replace <code>&lt;account&gt;</code> with the name of your Github account.</p>
<h3 id="writing-some-code">
  <a class="heading-link" href="#writing-some-code">Writing Some Code<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h3>
<p>Here comes the best part! What, you didn&rsquo;t think you&rsquo;d walk away from this article without writing a bit of code, did you?</p>
<p>This script is super-simple. There&rsquo;s no need for elegance here as we&rsquo;re probably only ever going to run this code once or twice. I&rsquo;ll show you the full snippet and then walk you through the details.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">start</span><span class="o">=</span>2021-05-01
</span></span><span class="line"><span class="cl"><span class="nv">stop</span><span class="o">=</span>2021-05-09
</span></span><span class="line"><span class="cl"><span class="nv">max</span><span class="o">=</span><span class="m">20</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">start</span><span class="si">}</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">stop</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">do</span> 
</span></span><span class="line"><span class="cl">  <span class="nv">start</span><span class="o">=</span><span class="k">$(</span>gdate -I -d <span class="s2">&#34;</span><span class="si">${</span><span class="nv">start</span><span class="si">}</span><span class="s2"> + 1 day&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="o">((</span><span class="nv">run</span><span class="o">=</span>1<span class="p">;</span> run &lt;<span class="o">=</span> <span class="k">$((</span>RANDOM <span class="o">%</span> max <span class="o">+</span> <span class="m">1</span><span class="k">))</span><span class="p">;</span> run++<span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    openssl rand -hex <span class="m">32</span> &gt; touch.txt
</span></span><span class="line"><span class="cl">    git add -A
</span></span><span class="line"><span class="cl">    <span class="nb">export</span> <span class="nv">GIT_AUTHOR_DATE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">start</span><span class="si">}</span><span class="s2">T00:01&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">export</span> <span class="nv">GIT_COMMITTER_DATE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">GIT_AUTHOR_DATE</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    git commit -m <span class="s2">&#34;Touch #</span><span class="si">${</span><span class="nv">run</span><span class="si">}</span><span class="s2"> for </span><span class="si">${</span><span class="nv">GIT_AUTHOR_DATE</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>First things first, we only need to know three things to get started:</p>
<ol>
<li><code>start</code>: The day of our first &ldquo;commit&rdquo; in the format of <code>YYYY-MM-DD</code>.</li>
<li><code>stop</code>: The day of our last &ldquo;commit&rdquo; in the format of <code>YYYY-MM-DD</code>.</li>
<li><code>max</code>: We don&rsquo;t want a single commit per day. Otherwise, the graph will be all one colour and not at all random enough to look legitimate. So, to account for this, we randomly create between <code>1</code> and <code>20</code> commits per day.</li>
</ol>
<p>We start a <code>while</code> loop that keeps going until our <code>start</code> and <code>stop</code> strings are no longer different. With each iteration, we add <code>1</code> day to our <code>start</code> variable. Eventually, this date will increment until both <code>start</code> and <code>stop</code> strings are equal, thus ending the loop.</p>
<p>For each day iteration, we create another loop that randomly-iterates anywhere between <code>1</code> and <code>max</code> times. Within each nested iteration we replace the content of the file named <code>touch.txt</code> with a random value making it a unique commit.</p>
<p>We then stage this change and assign a date value to both the <code>GIT_AUTHOR_DATE</code> and <code>GIT_COMMITTER_DATE</code> environmental variables. These are what allow you to easily back date a commit.</p>
<p>Finally, we commit this unique change locally. This keeps happening for every day until the script stops.</p>
<p>Write this to an executable file, or just paste it into your terminal of choice and hit <code>enter</code>. If you see the following kind of log output, you know you&rsquo;re doing the right thing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span>main 2ebb5b9<span class="o">]</span> Touch <span class="c1">#1 2021-05-07T00:01</span>
</span></span><span class="line"><span class="cl"> <span class="m">1</span> file changed, <span class="m">1</span> insertion<span class="o">(</span>+<span class="o">)</span>, <span class="m">1</span> deletion<span class="o">(</span>-<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>main e104b0a<span class="o">]</span> Touch <span class="c1">#2 2021-05-07T00:01</span>
</span></span><span class="line"><span class="cl"> <span class="m">1</span> file changed, <span class="m">1</span> insertion<span class="o">(</span>+<span class="o">)</span>, <span class="m">1</span> deletion<span class="o">(</span>-<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>main 873fa39<span class="o">]</span> Touch <span class="c1">#3 2021-05-07T00:01</span>
</span></span><span class="line"><span class="cl"> <span class="m">1</span> file changed, <span class="m">1</span> insertion<span class="o">(</span>+<span class="o">)</span>, <span class="m">1</span> deletion<span class="o">(</span>-<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>main 9def564<span class="o">]</span> Touch <span class="c1">#4 2021-05-07T00:01</span>
</span></span><span class="line"><span class="cl"> <span class="m">1</span> file changed, <span class="m">1</span> insertion<span class="o">(</span>+<span class="o">)</span>, <span class="m">1</span> deletion<span class="o">(</span>-<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>main 1b83187<span class="o">]</span> Touch <span class="c1">#1 2021-05-08T00:01</span>
</span></span><span class="line"><span class="cl"> <span class="m">1</span> file changed, <span class="m">1</span> insertion<span class="o">(</span>+<span class="o">)</span>, <span class="m">1</span> deletion<span class="o">(</span>-<span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>main b9f00db<span class="o">]</span> Touch <span class="c1">#2 2021-05-08T00:01</span>
</span></span><span class="line"><span class="cl"> <span class="m">1</span> file changed, <span class="m">1</span> insertion<span class="o">(</span>+<span class="o">)</span>, <span class="m">1</span> deletion<span class="o">(</span>-<span class="o">)</span>
</span></span></code></pre></div><p>Now, you simply have to push all your changes up to your repository on Github!</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git push origin main
</span></span></code></pre></div><p>All you now have to do is sit back and wait for everything to update.</p>
<p>We&rsquo;ve gone from barren landscape:</p>
<p><img src="/blog/falsifying-github-participation-graphs-for-fun-and-profit/image-3.png" alt=""></p>
<p>To a verdant garden paradise:</p>
<p><img src="/blog/falsifying-github-participation-graphs-for-fun-and-profit/image-4.png" alt=""></p>
<p>And that&rsquo;s it! 🎉</p>
<p>If you&rsquo;ve made a mistake, simply delete your private repository and try again with a fresh one.</p>
<h2 id="any-final-thoughts">
  <a class="heading-link" href="#any-final-thoughts">Any final thoughts?<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Job hunting is hard enough. Especially with more and more companies becoming increasingly risk-averse in their interviewing and recruiting processes. It&rsquo;s my hope that this little bit of harmless mischief will make things a bit easier for you!</p>]]></content:encoded>
    </item>
  </channel>
</rss>
